/** * Ratio module. * @module lib/ratio */ const Ajv = require("ajv"); const { Map } = require("immutable"); const is = require("is_js"); const R = require("ramda"); const { randomInt } = require("./random"); /** * Class sampling keys based on ratio. * @example * new Ratio({key1: 1, key2: 2}); // => Ratio's instance */ class Ratio { /** * Create a ratio. * @param {Object.<string, integer>} weights - Map representing pairs of key and weight. */ constructor(weights) { const withoutUndefined = is.json(weights) ? R.reject(is.undefined, weights) : weights; // NOTICE: keys with "undefined" are not ignored by this schema! const schema = { $schema: "http://json-schema.org/schema#", type: "object", additionalProperties: { type: "integer" } }; const ajv = new Ajv(); const isValid = ajv.validate(schema, withoutUndefined); if (!isValid) { const error = ajv.errors[0]; throw new Error(`weights${error.dataPath} ${error.message}`); } this.map = withoutUndefined; [this.ranges, this.max] = Map(withoutUndefined) .filter(v => v > 0) .reduce( ([map, sum], v, k) => { const upper = sum + v; return [map.set(k, [sum + 1, upper]), upper]; }, [Map(), 0] ); } /** * Sample a key based on ratio. * @return {(string|null)} * @example * new Ratio({key1: 1, key2: 2}).sample(); // => "key1" with a third, or "key2" with two third */ sample() { const num = randomInt(1, this.max); const key = this.ranges.findKey( ([lower, upper]) => lower <= num && num <= upper ); return is.existy(key) ? key : null; } } module.exports = Ratio;