Source: Range.js

/**
 * xethya-range
 *
 * Copyright © 2016 Joel A. Villarreal Bertoldi. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

/**
 * Represents a range of number, described through boundaries.
 */
class Range {
  /**
   * Instantiates a Range object.
   * @param  {Number} lowerBound The minimum number in this range.
   * @param  {Number} upperBound The maximum number in this range.
   * @constructor
   * @throws {Error} If lowerBound and upperBound are the same.
   */
  constructor(lowerBound, upperBound) {
    if (typeof lowerBound === 'undefined') {
      throw new Error('Range#constructor: lowerBound is required');
    }
    if (typeof lowerBound !== 'number') {
      throw new Error('Range#constructor: lowerBound must be a Number');
    }
    if (typeof upperBound === 'undefined') {
      throw new Error('Range#constructor: upperBound is required');
    }
    if (typeof upperBound !== 'number') {
      throw new Error('Range#constructor: upperBound must be a Number');
    }
    if (lowerBound === upperBound) {
      throw new Error('Range#constructor: lowerBound and upperBound cannot be equal');
    }

    /**
     * @property {Number} lowerBound The minimum number in this range.
     */
    this.lowerBound = Math.min(lowerBound, upperBound);

    /**
     * @property {Number} upperBound The maximum number in this range.
     */
    this.upperBound = Math.max(lowerBound, upperBound);
  }

  /**
   * Checks if a value is in the defined range.
   *
   * @param  {Number} value Value to compare.
   * @return {Boolean} true if in range, false otherwise.
   * @function
   */
  includes(value) {
    if (typeof value === 'undefined') {
      throw new Error('Range#includes: value is required');
    }
    if (typeof value !== 'number') {
      throw new Error('Range#includes: value must be a Number');
    }
    return this.lowerBound <= value && value <= this.upperBound;
  }

  /**
   * Converts the Range object to a string representation.
   * @return {String}
   * @function
   */
  toString() {
    return `${this.lowerBound.toString()} ~ ${this.upperBound.toString()}`;
  }

  /**
   * Creates a Range from an array of two numbers.
   *
   * @param  {Array.<Number>} values Boundaries of the range.
   * @return {Range}
   * @function
   * @static
   */
  static fromArray(values) {
    if (!Array.isArray(values)) {
      throw new Error('Range#fromArray: values must be an Array of 2 numerical elements');
    }
    if (values.length !== 2) {
      throw new Error('Range#fromArray: values must be an Array of 2 numerical elements');
    }
    if (!values.every(value => typeof value === 'number')) {
      throw new Error('Range#fromArray: values must be an Array of 2 numerical elements');
    }
    return new Range(Number(values[0]), Number(values[1]));
  }

  /**
   * Creates a Range from a string-based notation.
   *
   * @param  {String} notedRange A string representation of a Range,
   * using delimiters. Accepted formats: x,y x;y x:y x~y.
   * @return {Range}
   * @function
   * @static
   */
  static fromNotation(notedRange) {
    if (!notedRange) {
      throw new Error('Range#fromNotation: notedRange must use one of these formats: '
        + 'x,y x;y x:y x~y');
    }
    if (typeof notedRange !== 'string') {
      throw new Error('Range#fromNotation: notedRange must use one of these formats: '
        + 'x,y x;y x:y x~y');
    }

    let range;
    const allowedDelimiters = [',', ';', ':', '~'];
    if (!allowedDelimiters.some(delimiter => notedRange.includes(delimiter))) {
      throw new Error('Range#fromNotation: notedRange must use one of these formats: '
        + 'x,y x;y x:y x~y');
    }
    let delimiterFound = false;
    while (!delimiterFound) {
      const delimiter = allowedDelimiters.shift();
      delimiterFound = notedRange.includes(delimiter);
      if (delimiterFound) {
        const data = notedRange.split(delimiter).map(d => d.trim());
        if (data.length !== 2) {
          throw new Error('Range#fromNotation: notedRange must use one of these formats: '
            + 'x,y x;y x:y x~y');
        } else {
          range = Range.fromArray(data.map(d => Number(d)));
        }
      }
    }
    if (!range) {
      throw new Error('Range#fromNotation: notedRange must use one of these formats: '
        + 'x,y x;y x:y x~y');
    }
    return range;
  }
}

export default Range;