Source: ratio.js

/**
 * 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;