groupster-engine
Version:
Randomly group objects using do-group and don't-group rules.
201 lines (166 loc) • 6.98 kB
JavaScript
;
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;
});
}