UNPKG

groupster-engine

Version:

Randomly group objects using do-group and don't-group rules.

201 lines (166 loc) 6.98 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = Group; exports.determineGroupSizes = void 0; var _lodash = _interopRequireDefault(require("lodash")); var _joiBrowser = _interopRequireDefault(require("joi-browser")); var _SafeJoi = _interopRequireDefault(require("./SafeJoi")); var _buildRuleMap = _interopRequireDefault(require("./buildRuleMap")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } var determineGroupSizes = function determineGroupSizes(idCount, desiredGroupSize) { var errors = [(0, _SafeJoi["default"])(idCount, _joiBrowser["default"].number().min(1), "idCount must be a number greater than 0 (it was ".concat(idCount, ")")), (0, _SafeJoi["default"])(desiredGroupSize, _joiBrowser["default"].number().min(2).max(idCount), "desiredGroupSize must be a number greater than 1 and less than or equal to idCount (it was ".concat(idCount, ")"))].filter(Boolean); var groups = []; if (!errors.length) { while (idCount >= desiredGroupSize) { groups.push(desiredGroupSize); idCount -= desiredGroupSize; } // If some are remaining then the end goal is to have max # of groups of desired size, with minimal # of groups off by 1. // If not possible just drop remaining into group of their own. // Universal rule is that you should never have a group of size 1. if (idCount > 0) { // See if possible to distribute into other groups var canDistribute = idCount <= groups.length; // See if possible to skim other groups into this group var canSkim = idCount + groups.length >= desiredGroupSize - 1; if (idCount < desiredGroupSize - 1 && (canSkim || canDistribute) || idCount === 1) { // See how many groups would be affected by distributing remaining into other groups var groupsAffectedByDistribute = idCount; // See how many groups would be affected by skimming other groups by 1 until remainder is almost a full group var groupsAffectedBySkim = desiredGroupSize - idCount; if (canSkim && canDistribute) { if (groupsAffectedBySkim <= groupsAffectedByDistribute && idCount > 1) { // Prefer skimming -> this leads to smaller groups canDistribute = false; } else { canSkim = false; } } if (canSkim) { // Skim groups, starting at end, until remaining is enough for its own group for (var i = groups.length - 1; i >= 0; i -= 1) { groups[i] -= 1; idCount += 1; if (idCount >= desiredGroupSize - 1) { groups.push(idCount); break; } } } else { // Distribute remaining among groups, starting at end for (var _i = groups.length - 1; _i >= 0; _i -= 1) { groups[_i] += 1; idCount -= 1; if (idCount === 0) { break; } } } } // If neither are possible to get all groups within 1 of the desiredGroupSize then give up and just use remainder as group else { groups.push(idCount); } } } return { groups: groups, errors: errors }; }; /** * Given a array of IDs and an array of the group sizes desired, along with * an optional set of rules to govern how they should be grouped, returns * an array of arrays containing grouped IDs. * * @export * @param { array } IDs - Array containing [unique] ids to be grouped. * @param { number[] } groupSizes - * Array containing sizes of groups desired. The sum of its values must equal the length of 'IDs' param. * @param { array } [rules] - * Array of rule objects. A rule object must contain a boolean 'shouldGroup' property and an 'IDs' property * containing an array of IDs for which the rule applies. If the 'shouldGroup' property is false these IDs * will be placed into separate groups (to the extent possible). If true then they will be placed into * the same group (to the extent possible). For now 'shouldGroup: false' rules will take precedence. * * @returns Array of groups. Each group is an array containing ids. * @throws ValidationError if any of the parameters don't conform to their schemas. * @throws Error if any IDs are duplicates, or rules reference IDs not present in 'IDs' param. */ exports.determineGroupSizes = determineGroupSizes; function Group(IDs, groupSizes) { var rules = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; // The keys of ruleMap are deleted as each id is placed var ruleMap = (0, _buildRuleMap["default"])(IDs, groupSizes, rules); var groups = groupSizes.map(function (size) { var list = []; return { get size() { return size; }, get list() { return list; }, get roomLeft() { return size - list.length; }, add: function add(id) { list.push(id); }, fitRating: function fitRating(id) { // If room is left if (size - list.length > 0) { return list.reduce(function (acc, otherID) { var rating = ruleMap[id][otherID]; if (rating === undefined) { return acc + 0; } return acc + (rating ? 1 : -1); }, 0); } // If no room return -Infinity; } }; }); var addToBestGroup = function addToBestGroup(id) { // Add to the best group (preferring one with the most room) var groupRatings = groups.map(function (group, index) { return { index: index, roomLeft: group.roomLeft, rating: group.fitRating(id) }; }); var bestGroupIndex = _lodash["default"].sortBy(groupRatings, ['rating', 'roomLeft'])[groupRatings.length - 1].index; groups[bestGroupIndex].add(id); // Remove from ruleMap delete ruleMap[id]; }; var dontGroups = rules.filter(function (rule) { return !rule.shouldGroup; }).map(function (rule) { return _lodash["default"].shuffle(rule.IDs); }).sort(function (a, b) { return b.length - a.length; }); dontGroups.forEach(function (dontGroup) { var remaining = dontGroup.filter(function (id) { return _lodash["default"].has(ruleMap, id); }); remaining.forEach(addToBestGroup); }); var doGroups = rules.filter(function (rule) { return rule.shouldGroup; }).map(function (rule) { return _lodash["default"].shuffle(rule.IDs); }).sort(function (a, b) { return b.length - a.length; }); doGroups.forEach(function (doGroup) { var remaining = doGroup.filter(function (id) { return _lodash["default"].has(ruleMap, id); }); remaining.forEach(addToBestGroup); }); // Place remaining IDs _lodash["default"].shuffle(Object.keys(ruleMap)).forEach(addToBestGroup); return groups.map(function (group) { return group.list; }); }