Source: foft.js

'use strict'
/**
* @class Foft is a class that evaluates the
* properties of Function or Foft objects that
* generally receive and return objects of type
*  - Number
*  - Array of Number,
*  - Array of Array
* @example
* // returns a Foft instance
* let MoT = new Foft()
* @example
* // returns a Foft instance that
* // describes an equation over the range [0,1] (inclusive)
* // split into 22 segments (23) total points
* let MoT = new Foft({
*   range: [0, 1],
*   segmentDivisor: 22,
*   terms: (t) => t*2;
* });
* Foft also evaluates some of the properties
* of the Function or Foft objects in its terms
* @see Foft.oft
*/
class Foft {
  /**
  * create a Foft instance that evaluates its Function terms
  *    when given some parameter "t".
  *
  * accepts an argument params that may be a(n):
  * - function see params.terms for details
  * - object see params for details
  * - array of numbers see params.range for details
  * - Foft instance will return a copy of the instance
  *
  * @param  {(Object|Function|Array.<Number>|Foft)} params
  * @param {(Number|Array.<Number>)} [params.range] Range of two numerical values over which t is evaluated inclusively. If given a single number t0, range is [-t0,t0]
  * @param {Number} [params.segmentDivisor] The number of segments to divide the range into when picking t values for evaluation.
  * @param {(Function|Array.<Function>|Array.<Foft>)} [params.terms=[]] A function that accepts a parameter t and returns a result of some operation on t
  * @param {boolean} [params.rangeoverride=false] if true, will override any range provided and set range equal to [0, params.segmentDivisor]
  * @param {boolean} [params.harmonize=false] if true, will harmonize the domains of any Foft terms to that of the new parent instance @see range, @see segmentDivisor
  * @throws TypeError
  */
  constructor (params) {
    params = params || {}

    if (typeof params === 'function') {
      let thefunc = params
      params = {
        terms: thefunc
      }
    } else if (Foft.isArrayLike(params)) {
      let thearray = params
      params = {
        range: thearray
      }
    } else if (params instanceof Foft) {
      let thefoft = params
      let { segmentDivisor, range, terms } = thefoft
      params = { segmentDivisor, range: Array.from(range), terms: Array.from(terms) }
    }

    // define the division of the evaluation range
    const segmentDivisor = params.segmentDivisor || Foft.divisor
    this.segmentDivisor = segmentDivisor

    let rangeoverride = (typeof params.rangeoverride === 'boolean')
      ? params.rangeoverride
      : false
    let harmonize = (typeof params.harmonize === 'boolean')
      ? params.harmonize
      : false
    // create an evaluation range
    let range = (rangeoverride)
      ? [0, this.segmentDivisor]
      : params.range || Foft.sweep
    range = Foft.isCalculable(range)
      ? [-range, range]
      : range
    if (!Foft.isArrayLike(range)) throw new TypeError('range should be array')
    // if(range.length!==2) throw new RangeError('range should have two elements')
    this.range = Array.from(range)

    // this Foft can use these terms
    // define terms
    this._terms = []
    let terms = params.terms || [(t) => t]
    terms = (typeof terms === 'function')
      ? [terms]
      : terms
    if (!Foft.isArrayLike(terms) && (typeof terms !== 'function') && !(terms instanceof Foft)) {
      throw new TypeError('params.terms should be array, function or Foft instance')
    }
    for (let term of terms) {
      // const term = terms[termIndex];
      // console.log(term);``
      this.addTerm(term, harmonize)
    }
    // console.log(params.opcode)
    // debugger;
    this.opcode = params.opcode
  }

  /**
  * the Function terms of this Foft object
  *
  * @return {Array}
  */
  get terms () {
    return this._terms
  }

  /**
  * add a term to the terms of this Foft instance
  *
  * @param  {(function|Foft)} term A Function that takes a parameter (t) or
  *   Foft
  * @param {boolean} [harmonize=false] if true, and term is a Foft
  *  instance, this overwrites the range and segmentDivisor of term to make them
  *  reference the same-named parameters of this instance.
  * @returns {boolean} true if length of terms grew
  */
  addTerm (term, harmonize) {
    let numterms = this.terms.length
    const push = (_term) => {
      this.terms.push(_term)
      this[numterms] = this.terms[numterms]
    }
    harmonize = (typeof harmonize === 'boolean') ? harmonize : false
    if (typeof term === 'function' && !(term instanceof Foft)) {
      push(term)
    } else if (term instanceof Foft) {
      push(term)
      if (harmonize) {
        // term.range = this.range;
        // term.segmentDivisor = this.segmentDivisor;
        const keys = ['_range', '_segmentDivisor']
        for (let key of keys) {
          Object.defineProperty(term, key, {
            get: () => this[key], // reference to parent
            set: (value) => Object.defineProperty(term, key, { value })// dareference from parent
          })
        }
      }
    }

    return numterms === this.terms.length - 1
  }

  /**
   * set segmentDivisor - set the segment divisor for the evaluation range where
   * - the range will be divided into (segmentDivisor+1) segments,
   * - if given an arraylike parameter, use the 0th value
   * - if given a calculable parameter, use it as-is
   *
   *
   * @param  {number|arraylike<number>} segmentDivisor
   * @throws {TypeError} segmentDivisor should be calculable number
   */
  set segmentDivisor (segmentDivisor) {
    segmentDivisor = Foft.isArrayLike(segmentDivisor)
      ? segmentDivisor[0]
      : segmentDivisor
    if (!Foft.isCalculable(segmentDivisor)) {
      // console.log('NaN segment Divisor')
      throw new TypeError('segmentDivisor should be calculable number, not: ' + segmentDivisor)
    } else {
      this._segmentDivisor = [segmentDivisor]
    }
  }
  /**
  * get segmentDivisor The number of segment divisors
  * (number of t evaluation points -1)
  *   in this Foft
  *
  * @return {Number}
  */
  get segmentDivisor () {
    return this._segmentDivisor[0]
  }

  /**
   * The number of actual segments the Foft divides the evaluation range into
   *
   * @return {Number}
   */
  get numSegments () {
    return this.segmentDivisor + 1
  }

  /**
   * get the delta for t between the first and final values of the
   * evaluation range. May be innacurate when the range has more than two
   * terms
   *
   * @return {number}
   */
  get dt () {
    return this.drange / this.segmentDivisor
  }

  /**
   * set range - only accepts an arraylike of calculables
   *
   * @param  {Array<number>} range
   */
  set range (range) {
    if (!(Foft.isArrayLike(range) && Foft.areCalculables(range))) throw new TypeError('range values should be Array of calculable numbers')
    this._range = Array(range.length)

    for (let rangeIndex in range) {
      this._range[rangeIndex] = range[rangeIndex]
    }
  }
  /**
  * get the evaluation range is the minimum and maximum values for t
  *
  * @return {Array.<Number>}
  */
  get range () {
    return this._range
  }

  /**
  * get the first value of t in the evaluation range T
  * @see T
  * @return {Number}
  */
  get t0 () {
    return this.range[0]
  }

  /**
  * the last value of t in the evaluation range T
  * @see T
  * @return {Number}
  */
  get tt () {
    return this.range[this.range.length - 1]
  }

  /**
  * a Foft can have an opcode as defined in
  * Foft.ops. These codes represent mathematical operations
  * between Numbers and other types. They are useful for performing
  * said operations when the Function or Foft in the terms
  * Array
  *
  * @see Foft.ops
  * @see terms
  * @return {string} @see Foft.ops
  */
  get opcode () {
    return this._opcode
  }

  /**
  * set opcode - set the opcode to one of the opcodes
  *  defined in Foft.ops
  *
  * @param  {string} [opcode=null] @see Foft.ops
  */
  set opcode (opcode) {
    this._opcode = (Foft.isOp(opcode))
      ? opcode
      : null
  }

  /**
   * given indices n & nn
   * returns the delta between sub values in the Foft instance's
   * evaluation range, or: range[nn%range.length]-range[n%range.length],
   *
   * when given no parameters, it uses 0 and 1
   *
   * @param {Number} [n=0] the starting range index.
   * @param {Number} [nn=n+1%this.range.length] the end range index
   * @return {Number}
   * @throws {TypeError} when given non-number parameter
   */
  dSubrange (n, nn) {
    n = Foft.isNum(n)
      ? n
      : 0
    if (nn && !Foft.isNum(nn)) {
      throw new TypeError(`Foft.dSubRange only accepts Numbers, given ${[...arguments]}`)
    }
    n = n % this.range.length
    if (!Number.isInteger(n)) {
      throw new RangeError(`Foft.dSubRange only accepts Integers, given ${[...arguments]}`)
    }
    // for this conditional, we use the explicit isNum to avoid logical
    // error for zero case: if(0) is falsy
    nn = Foft.isNum(nn)
      ? nn % this.range.length
      : (n + 1) % this.range.length
    if (!Number.isInteger(nn)) {
      throw new RangeError(`Foft.dSubRange only accepts Integers, given ${[...arguments]}`)
    }
    return this.range[nn] - this.range[n]
  }

  /**
  * the delta between the the first and final values of the evaluation range
  *
  * @return {Number}
  */
  get drange () {
    return this.range[this.range.length - 1] - this.range[0]
  }
  /**
  * the absolute value of the delta
  * between the first and final values of the evaluation range
  *
  * @return {Number}
  */
  get dabsrange () {
    return Math.abs(this.drange)
  }

  /**
   * get a generator function that yields segmentDivisor+1 values of t spanning the range [this.range[n], this.range[n+1]], where if n or n+1 fall beyond the bounds of this.range.length, they are constrained to fit
   *
   * @param  {Number} [n=0] integer start index of range
   *
   * @return {type}   description
   */
  subT (n, omitLast) {
    omitLast = (typeof omitLast === 'boolean')
      ? omitLast
      : false
    let defaultN = 0
    n = Foft.isNum(n)
      ? n % this.range.length
      : defaultN
    let a = this.range[n]

    let b = this.range[(n + 1) % this.range.length]
    let tsubmax = (omitLast)
      ? this.segmentDivisor - 1
      : this.segmentDivisor
    let dt = (b - a) / this.segmentDivisor

    /**
    * @yields {Number}
    */
    return function * () {
      for (let tsubindex = 0; tsubindex <= tsubmax; tsubindex++) {
        yield a + tsubindex * dt
      }
    }
  }

  /**
  *  get a Generator yielding all values of t across instance evaluation range
  * @example
  * // get the default t values for which a Foft is
  * // evaluated
  * let MoT = new Foft({
  *   range: [0, Math.PI*2],
  *   segmentDivisor: 22,
  *   terms: (t) => [sin(t), cos(t)];
  * });
  * let T = [...MoT.T()]
  * @return {Generator<Number>}
  */
  get T () {
    let rangelimit = this.range.length - 2
    /**
    * @yields {Number}
    */
    return function * () {
      for (let rangeIndex = 0; rangeIndex <= rangelimit; rangeIndex++) {
        yield * (rangeIndex === rangelimit)
          ? this.subT(rangeIndex)()
          : this.subT(rangeIndex, true)() // chop last to eliminate double values
      }
    }
  }

  /**
  * given a Number t, return a normalized (to Foft.sweep)
  * representation of the ratio between t and the delta of the evaluation
  * range of this Foft
  *
  * If t falls out of bounds of range, the value is returned as -/+ Infinity
  *
  * If the evaluation range has more than two values, e.g. [0,1,2],
  * then normalizeT checks in each subrange, e.g. [0,1], [1,2]
  * and returns an Array of normalized values corresponding to each range
  *
  * @see normToRange
  * @see sweep
  * @param  {Number} [t=0]
  * @param {boolean} [doAnti=false]
  * @return {Number|Array<Number>}
  */
  normalizeT (t, doAnti) {
    doAnti = (typeof doAnti === 'boolean')
      ? doAnti
      : false
    let func = (doAnti)
      ? Foft.antiNormToRange
      : Foft.normToRange
    let arr = Array(this.range.length - 1)
    for (let r = 0; r < arr.length; r++) {
      arr[r] = func(t, [
        this.range[r],
        this.range[r + 1]
      ])
    }
    return (arr.length === 1)
      ? arr[0]
      : arr
  }

  /**
   * given a number t return a normalized representation of the ratio of a quantity n to the delta of the instance evaluation range such that the ratio of t to the delta of the evaluation range N satisfies
   * 1.n = maximum normal - N
   *
   * This is the remaining range for the normalized value provided by normalizeT
   *
   * if t falls beyond the lower bound of the evaluation range, return +Infinity
   * if t falls beyond the upper bound of the evaluation range, return -Infinity
   *
   * @see normalizeT
   * @param  {number} t
   * @return {number} n
   */
  antinormalizeT (t) {
    return this.normalizeT(t, true)
  }

  /**
   * given a Number t within a the evaluation range of this instance
   * (inclusive), return the value of the corresponding i such that
   *   1. i is proportional to the location of t within the range
   *   2. i is scaled to segmentDivisor
   *
   * If t falls out of bounds of the range, nothing is returned
   *
   * If the range has more than two elements, return an array of length range-1
   * @see iInRange
   * @see segmentDivisor
   * @see normalizeT
   * @param  {Number} t
   * @return {Number|null|Array<number|null>} [0, segmentDivisor]
   */
  i (t) {
    let arr = Array(this.range.length - 1)
    for (let r = 0; r < arr.length; r++) {
      arr[r] = Foft.iInRange(t, [
        this.range[r],
        this.range[r + 1]
      ], this.segmentDivisor)
    }
    return (arr.length === 1)
      ? arr[0]
      : arr
  }

  /**
   * return true IFF a given t falls within the evaluation range of this instance
   *
   * @param  {number} t
   * @return {boolean}
   */
  isInRange (t) {
    return Foft.inRange(t, this.range)
  }
  /**
  * evaluate all of the terms held by this Mathoft for the
  * given t value.
  *
  * When evaluating a Function or Foft term, the function or Foft is called with a this object containing certain useful data regarding the calling instance's evaluation of t @see tThis
  *
  * When evaluating a Foft term, any t that falls outside that term's evaluation range will produce a null result. If the filterNulls parameter is true, then null values will be stripped from the returned result.
  * @see isInRange
  * @param  {Number} [t=t0]
  * @param {boolean} [filterNulls=false]
  * @param {boolean} [maketthis=true]
  * @return {(Number|Array.<Number>|Array<Array>)}
  */
  oft (t, filterNulls, maketthis) {
    // console.log(this)
    t = Foft.isCalculable(t)
      ? t
      : this.t0
    filterNulls = (typeof filterNulls === 'boolean')
      ? filterNulls
      : false
    maketthis = (typeof maketthis === 'boolean')
      ? maketthis
      : true
    // debugger;
    let tthis = (maketthis)
      ? Foft.tThis(t, this)
      : (typeof this.tthis === 'object')
        ? this.tthis
        : null

    let result = []
    for (let i in this.terms) {
      let _term = this.terms[i]
      if (typeof _term === 'function') {
        result[i] = _term.call(tthis, t)
      } else if (_term instanceof Foft) {
        // console.log(_term);
        let subres = _term.isInRange(t)
          ? _term.oft.call(Object.assign(_term, { tthis }), t, null, false)
          : null
        result[i] = subres // OVERRIDE?
      }
    }
    result = (filterNulls)
      ? result.filter((v) => v)
      : result
    return (result.length === 1)
      ? result[0]
      : result
  }

  /**
  * return the oft for the first t in the evaluation range
  *
  * @see t0
  * @see oft
  * @return {(Number|Array.<Number>|Array.<Array>)}
  */
  get ofFirstt () {
    return this.oft(this.t0)
  }
  /**
  * return the oft for the final t in the evaluation range
  *
  * @see range
  * @see oft
  * @return {(Number|Array.<Number>|Array.<Array>)}
  */
  get ofLastt () {
    return this.oft(this.tt)
  }

  /**
  * Accepts a Number tNormal that falls within Foft.sweep, inclusive, and when provided
  *  1 . A NaN value, return NaN
  *  2.  +Infinity, returns evaluation from end bound of range
  *  3.  -Infinity, returns evaluation from start bound of range
  *  4. Any other number even outside of normal range, returns value of that t.
  *
  * @see isCalculable
  * @see ofFirstt
  * @see ofLastt
  * @see sweep
  * @see oft
  * @param  {Number} [tNormal=[-1,1]]
  * @return {(Number|Array.<Number>)}
  */
  oftNormal (tNormal) {
    let dNormal = Foft.sweep[1] - Foft.sweep[0]
    let midNormal = Foft.sweep[0] + dNormal / 2
    tNormal = Foft.isNum(tNormal)
      ? tNormal
      : midNormal
    let t; let midt = (this.tt - this.t0) / 2 + this.t0
    if (Foft.isCalculable(tNormal)) {
      t = midt + (tNormal - midNormal) * this.drange / 2
    }
    // debugger;
    return (t !== undefined)
      ? this.oft(t)
      : (Number.isNaN(tNormal))
        ? NaN
        : tNormal === -Infinity
          ? this.ofFirstt
          : this.ofLastt
  }

  /**
  * Calculate the value of performing an operation _op on the
  * values returned by calculating this Foft instance's terms for
  * some evaluation value t. When given a parameter _acc, the calculation of _op will use _acc as its starting value.
  *
  * This is intended to facilitate convenient manipulation of terms and results.
  *
  * @see oft
  *
  * @param  {Number} t the t to evaluate
  * @param  {string} [_op=this.opcode]  an opcode to perform @see Foft.ops
  * @param  {(Number|Array.<Number>|Array.<Array>)} [_acc=null] an accumulator value to start with @see Foft.ops -> base
  * @return {(Number|Array.<Number>|Array.<Array>)}
  */
  oftOp (_t, _op, _acc) {
    _op = (_op in Foft.ops)
      ? _op
      : this.opcode
    const op = Foft.ops[_op]
    // debugger;
    _acc = ((_acc || Number.isNaN(_acc)))
      ? (Foft.areNums(_acc))
        ? _acc
        : NaN
      : null // op.base
    // debugger;
    const transform = (acc, val) => {
      let transformRes
      // console.log(acc,val);
      switch (Foft.mathTypeOf(val)) {
        case Foft.mathTypes.numberlike:
          if (Foft.isArrayLike(acc)) {
            throw new TypeError('Can\'t apply an arraylike accumulator to a scalar.')
          } else if (Foft.isNum(acc)) {
            transformRes = op(acc, val)
          }
          break
        case Foft.mathTypes.arraylike:
          let isNested = Foft.isArrayLike(val[0])
          if (Foft.isArrayLike(acc)) {
            if (acc.length !== val.length) {
              let areMismatched = (isNested)
                ? acc.length !== val[0].length
                : true
              if (areMismatched) {
                throw new TypeError('Can\'t apply an op to arraylike values of dissimilar lengths.')
              }
            }
            if (isNested) {
              return val.reduce(transform, acc)
            } else {
              for (let i = 0; i < val.length; i++) {
                val[i] = transform(acc[i], val[i]) // overwrite in place
              }
            }
          } else if (Foft.isNum(acc)) {
            for (let i = 0; i < val.length; i++) {
              val[i] = transform(acc, val[i]) // overwrite in place
            }
          }
          transformRes = val
          break
      }
      return transformRes
    }

    let _oft = this.oft(_t)
    //
    if (!_op) {
      return op(_oft)
    }
    // console.log(_oft)
    // console.log(_acc);

    let res
    switch (this.terms.length) {
      case 1:
        res = (_acc || Number.isNaN(_acc))
          ? transform(_acc, _oft)
          : _oft
        break
      default:
        res = (_acc || Number.isNaN(_acc))
          ? Foft.isArrayLike(_acc)
            ? transform(_acc, _oft)
            : _oft.reduce(transform, _acc)
          : _oft.reduce(transform)
        break
    }
    return res
  }

  /**
  * get a Generator that yields
  * all Array=[t, this.oft(t)] for t in evaluation range
  * in form [t, this.oft(t)]
  *
  * @see oft
  * @return {Generator}
  */
  get ofAlltT () {
    return function * () {
      for (let t of [...this.T()]) {
        yield [
          t,
          this.oft(t)
        ]
      }
    }
  }

  /**
  * Symbol.iterator get a Generator that yields
  *    all this.oft(t) for t in evaluation range
  * @see oft
  * @return {Generator} Generator function yielding this.oft(t)
  */
  get [Symbol.iterator] () {
    return function * () {
      yield * [...this.T()].map((t, i) => this.oft(t))
    }
  }

  /**
  * get a Generator that yields all
  * this.oftOp(_t, _acc, _op) for _t in T,
  * _acc, _op provided by user
  *
  * @see ofAlltTOp
  * @return {Generator}  description
  */
  get ofAlltTOp () {
    return function * (_acc, _op) {
      yield * [...this.T()].map((_t) => this.oftOp(_t, _op, _acc))
    }
  }

  /**
  * apply the Array.map native function to the elements yielded by
  * this[Symbol.iterator] with the given callback function and this argument
  *    *
  * @see get [Symbol.iterator]
  * @param  {Function} [callback] callback to apply
  * @param  {Object} [thisArg]  this argument
  * @return {Array}         map result
  */
  mapT (callback, thisArg) {
    if (!(callback instanceof Function)) throw new TypeError('map needs Function callback')
    return [...this].map(callback, thisArg)
  }

  /**
  * apply the Array.map native function to the elements of
  * this.ofAllTOp() with the given callback function and this argument
  * @see Array.map
  * @see ofAllTOp
  * @param  {Function} [callback] callback to apply
  * @param  {Object} [thisArg]  this argument
  * @return {Array}         map result
  */
  mapTOp (callback, thisArg) {
    if (!(callback instanceof Function)) throw new TypeError('map needs Function callback')
    return [...this.ofAlltTOp()].map(callback, thisArg)
  }

  /**
   * toString
   *
   * @override
   * @return {string}
   */
  toString () {
    let res = 'Foft\n'
    res += `range:\n\t[${this.range}]\n`
    res += `segments:\n\t[${this.numSegments}]\n`
    res += `terms:\n`
    for (var i = 0; i < this.terms.length; i++) {
      res += `\t[${i}]: ${this.terms[i]}\n`
    }
    res += `opcode:\n\t${this.opcode}`
    return res
  }

  /**
   * get Symbol.toStringTag
   *
   * @override
   * @return {string}
   */
  get [Symbol.toStringTag] () {
    return 'Foft Function'
  }

  /**
   * give precision warning in the form of an object that can be converted to a primitive.
   *
   * Floating point math has inherent imprecisions. This function is for examining them.
   * @return {object}  the object with primitive values:
   *         {number}  the maximum value for which no precision is lost
   *         {string}  as brief message
   * @static
   */
  static precision (maxtestnum) {
    let res
    let getmsg = (e) => `Maximum safe unit divisor: ${e}`
    let tests = [
      (a) => ((1 / a) * a === 1), //  multiplicative identity
      (a) => Array(a).fill(1 / a).reduce((acc, val) => acc + val, 0) // sum of inverses

    ]
    // only do so many tests
    let testnum = 0
    const maxtest = Foft.isCalculable(maxtestnum)
      ? maxtestnum
      : 144
    // tests[0]
    let lastgoodmultidendivisor, msg
    // tests[1]
    let roundinaccura = [] // all divisors for which rounding error causes failure
    let rounddeltas = [] // the difference between erroneous rounding and 1
    let roundexcesses = [] // divisors for which rounding error causes excess failure
    let rounddeficits = [] // divisors for which rounding error causes deficiency failure

    while (testnum < maxtest) {
      // tests[0]
      if (!tests[0](++testnum)) {
        lastgoodmultidendivisor = testnum - 1
        msg = getmsg(lastgoodmultidendivisor)
      }
      // tests[1]
      const test1result = tests[1](testnum) // testnum has been incremented already
      if (test1result !== 1) {
        roundinaccura.push(testnum)
        rounddeltas.push(test1result - 1)
        if (test1result < 1) rounddeficits.push(testnum)
        if (test1result > 1) roundexcesses.push(testnum)
      }
    };
    res = {
      [Symbol.toPrimitive]: (hint) => {
        if (hint === 'number') return lastgoodmultidendivisor
        return msg
      },
      inaccurateDivisors: {
        all: roundinaccura,
        errors: rounddeltas,
        excessive: roundexcesses,
        deficient: rounddeficits
      },
      [Symbol.iterator]: function * () {
        yield * roundinaccura
      }
    }

    return res
  }

  /**
   * return true IFF both of the following conditions are met
   *   1. there was ONE argument provided, and
   *   2. the sole provided argument was a Number
   *
   *
   * @return {boolean}
   * @static
   */
  static isNum () {
    return (arguments.length === 1) && (typeof arguments[0] === 'number')
  }

  /**
   * return true IFF all of the following conditions are met
   * 1. argument satisfies isNum (One Number argument)
   * 2. argument is not NaN
   * 3. argument is not +/-Infinity
   *
   * This function does more calls than just using isFinite, however, it is used in the Foft class because the class also deals with arrays and objects.
   *
   * @see isNum
   *
   * @return {boolean}  description
   * @static
   */
  static isCalculable () {
    return (arguments.length === 1) && Number.isFinite(arguments[0])
  }

  /**
   * determine whether a given argument x is "like" an array for the purposees of Foft calculations and parsing, returning true IFF x satisfies one of the following conditions:
   * 1 - It is an Array
   * 2 - It is a TypedArray
   * 3 - It provides a Symbol.iterator property
   *
   * The third condition allows for the parsing of various iterable objects, but it does not guarantee that such actions will produce calculable values.
   *
   * @param  {?} x
   * @return {boolean}
   * @static
   */
  static isArrayLike (x) {
    return x && (Object.getOwnPropertySymbols(x).includes(Symbol.iterator) || Array.isArray(x) || ArrayBuffer.isView(x))
  }

  /**
   * areNums return true IFF one of these conditions are met
   *   1. The provided arguments are ALL of Number type,
   *   2. The sole provided argument is an Array whose members are ALL of Number type,
   *   3. Any provided argument is an Array whose members are
   *      A. ALL of Number type, or
   *      B. nested Arrays whose submembers are all number types or Arrays
   *      and ALL other arguments are Number type or Array with ALL members of Number type,
   *
   * @see areCalculables
   *
   * @params {} [arguments] figure out whether the arguments are numbers or
   *  an Array thereof
   * @return {boolean}
   * @static
   */
  static areNums () {
    if (arguments.length === 0) {
      return false
    } else {
      return [...arguments].every(v => {
        return Foft.isArrayLike(v)
          ? Foft.areNums(...v)
          : Foft.isNum(v)
      })
    }
  };

  /**
   * areCalculables return true IFF one of these conditions are met
   *   1. The provided arguments are ALL of Number type,
   *   2. The sole provided argument is an Array whose members are ALL of Number type,
   *   3. Any provided argument is an Array whose members are
   *      A. ALL of Number type, or
   *      B. nested Arrays whose submembers are all number types or Array,
   *   and ALL other arguments are Number type or Array with ALL members of Number type, and
   *   4. All arguments of number type are neither NaN nor -/+Infinity
   *
   * @see areNums
   *
   * @params {} [arguments] figure out whether the arguments are numbers or
   *  an Array thereof
   * @return {boolean}
   * @static
   */
  static areCalculables () {
    if (arguments.length === 0) {
      return false
    } else {
      return [...arguments].every(v => {
        return Foft.isArrayLike(v)
          ? Foft.areCalculables(...v)
          : Foft.isCalculable(v)
      })
    }
  };

  /**
   * determine whether a given number n falls
   * within any of the follwoing inclusive ranges
   *    0. [ Foft.sweep[0], Foft.sweep[1] ]
   *    1. [0, m],
   *    2. [0, m[0]], (when provided unit-length array)
   *    3. [m[0], m[m.length-1]]
   *    4, [m, mm]
   *
   * @param  {Number} n the number to test
   * @param  {(Number|Array<Number>)} [m] the end of the range starting with 0, or
   *                                       the Array representing the range [m[0], m[last]]
   *                                       the begining of range ending in mm, or
   * @param  {Number} [mm] the optional end of the range
   * @return {boolean}
   * @static
   */
  static inRange (n, m, mm) {
    let test = (a, b, c) => {
      // console.log(a,b,c)
      return (a > b)
        ? a <= c
        : (a < b)
          ? a >= c
          : true // a === b
    }
    if (!(Foft.areNums(...arguments) && Foft.isNum(n))) {
      return false
    } else {
      if (arguments.length === 1) {
        return Foft.inRange(n, Foft.sweep)
      } else if (Foft.isArrayLike(m)) {
        // console.log(n,m)
        return (m.length === 1)
          ? test(n, 0, m[0])
          : test(n, m[0], m[m.length - 1])
      } else if (Foft.isNum(m)) {
        if (!Foft.isNum(mm)) {
          return test(n, 0, m)
        } else if (Foft.isNum(mm)) {
          return test(n, m, mm)
        }
      }
    }
  }

  /**
   * given a Number t, amd a ramge TT, return a normalized (to an optional range NN or Foft.sweep)
   * representation of the ratio between the two deltas A and B where
   *   1. A is the difference betewen t and TT[first]
   *   2. B is the difference between TT[last] and TT[first]
   *
   *
   * If t falls out of bounds of range, the value is returned as -/+ Infinity,
   * where:
   *   1. -Infinity corresponding to beyond the bound of TT[first], and
   *   2. +Infinity corresponding to beyond the bound of TT[last]
   *
   *
   * @param  {number} [t=0] the t to evaluate
   * @param  {Array<number>} [TT=sweep] the range in which to test for TT
   * @param  {Array<number>} [NN=sweep] the target normalization range * @return {number}
   * @static
   */
  static normToRange (t, TT, NN) {
    if (!Foft.isNum(t)) {
      t = 0
    }
    if (!Foft.areNums(NN)) {
      NN = Foft.sweep
    }
    if (!Foft.areNums(TT)) {
      TT = Foft.sweep
    }
    let [normA, normB] = NN
    let minNorm = (normA < normB)
      ? normA
      : normB
    // let maxNorm = (normB > normA)
    //   ? normB
    //   : normA
    let res = (t - TT[0]) / (TT[1] - TT[0]) // [0-1]
    res = normA + (normB - normA) * res // [normA, normB]

    if (!Foft.inRange(res, normA, normB)) {
      res = (res < minNorm)
        ? -Infinity
        : Infinity
    }

    return res
  }

  /**
   * given a Number t, amd a ramge TT, return a normalized (to Foft.sweep)
   * representation of the ratio between the two deltas A and B where
   *   1. A is the difference betewen t and TT[last]
   *   2. B is the difference between TT[last] and TT[first]
   *
   * If t falls out of bounds of range, the value is returned as -/+ Infinity,
   * where:
   *   1. +Infinity corresponding to beyond the bound of TT[first], and
   *   2. -Infinity corresponding to beyond the bound of TT[last]
   *
   * @see normToRange
   * @param  {number} [t=0] the t to evaluate
   * @param  {Array<number>} [TT=sweep] the range in which to test for TT
   * @return {number}
   * @static
   */
  static antiNormToRange (t, TT, NN) {
    if (!Foft.isNum(t)) {
      t = 0
    }
    if (!Foft.areNums(TT)) {
      TT = Foft.sweep
    }
    if (!Foft.areNums(NN)) {
      NN = Foft.sweep
    }
    let res = Foft.normToRange(t, TT, NN)
    return (Math.abs(res) === Infinity)
      ? -res
      : NN[NN.length - 1] - res
  }

  /**
   * given a number t, a range TT and a divisor d, return the value of the corresponding i such that
   *   1. i is proportional to the location of t within the range
   *   2. i is proportional to d
   *
   * @param  {number} t  t to find in TT
   * @param  {Array<number>} TT range
   * @param  {type} d  divisor
   * @return {null|number}
   * @static
   */
  static iInRange (t, TT, d) {
    d = (Foft.isNum(d))
      ? Math.floor(d)
      : Math.floor(Foft.divisor)
    let res = Foft.normToRange(t, TT, [0, 1])
    return (res === Infinity)
      ? null
      : (res === -Infinity)
        ? null
        : Math.floor(res * d)
  }

  /**
   * return the size of the given x, where x can be a number or an arraylike or a nested arraylike
   *
   * @param  {(number|Array)} x the structure to get dimensions of
   * @return {Array}
   * @static
   */
  static dimensions (x) {
    let dim = Promise.resolve([])
    if (Foft.isNum(x)) {
      return dim.then(dimarr => dimarr.concat(0))
    } else if (Foft.isArrayLike(x)) {
      if (x.length === 0) {
        return dim.then(dimarr => dimarr.concat(0))
      } else {
        let subarrayIndices = []
        let isNotSubarrayTest = (acc, v, i) => {
          if (!Foft.isArrayLike(v)) {
            return acc && true
          } else {
            subarrayIndices.push(i)
            return acc && false
          }
        }
        if (x.reduce(isNotSubarrayTest, true)) {
          return dim.then(dimarr => dimarr.concat(x.length))
        } else {
          return dim.then(dimarr => {
            return Promise.all(subarrayIndices.map((v) => {
              let xSubArr = x[v]
              // console.log(xSubArr)
              return Foft.dimensions(xSubArr)
            })).then(subdims => {
              // console.log(subdims, x.length)
              let flatsubdims = Foft.ops['...'](subdims)
              // console.log(flatsubdims, x.length)
              let mag
              if (Foft.equiv(flatsubdims.length, subdims.length, x.length)) {
                mag = Foft.ops.magest(...flatsubdims)
              } else {
                mag = Foft.ops.magest(...subdims)
              }
              // mag = (Number.isNaN(mag) || mag === 0)
              //   ? []
              //   : mag
              return dimarr.concat(x.length, mag)
            })
          })
        }
      }
    }
    return dim
  }

  /**
   * determine whether the given number or Array-like arguments are satisfying the conditions:
   * 1) all of a single shared type,
   * 2) if numbers, of equal value,
   * 3) if arrays, composed of equal positional elements
   * 4) if other types, satisfying strict equality test
   *
   * When comparing values that are NaN or containing NaN in the same positions, the function will return false because NaN doesn't equal NaN
   *
   * @params {?} [arguments]
   * @return {boolean}
   * @static
   */
  static equiv () {
    if (arguments.length === 0) {
      return false
    }
    if (arguments.length === 1) {
      return !Number.isNaN(arguments[0])
    }
    const a0 = arguments[0]
    const a0type = Foft.mathTypeOf(a0)
    let res = true
    // const dim = Foft.dimensions(arguments[0]);
    for (let i = 1; i < arguments.length; i++) {
      let a = arguments[i]
      if (Foft.mathTypeOf(a) !== a0type) return false
      switch (a0type) {
        case Foft.mathTypes.arraylike:
          if (a.length !== a0.length) return false
          for (let ai = 0; ai < a0.length; ai++) {
            if (!Foft.equiv(a0[ai], a[ai])) return false
          }
          break
        case Foft.mathTypes.numberlike:
          res = res && (a === a0)
          break
        default:
          // console.log(a)
          res = res && (a === a0)
      }
    }
    return res
  }

  /**
   * tell whether the given argument a is of one of the types that Foft can do math with  and if so, which type
   *
   * @param  {?} [a]
   * @return {(Symbol|null)}
   * @static
   */
  static mathTypeOf (a) {
    return Foft.isArrayLike(a)
      ? Foft.mathTypes.arraylike
      : Foft.isNum(a)
        ? Foft.mathTypes.numberlike
        : null
  }

  /**
   * given a t and a Foft instance, produces an object with some keys for inter-instance communication corresponding to:
   * 1 the result of evaluating certain methods of the calling instance for t @see funcKeys
   * 2 certain members of the calling instance @see membKeys
   *
   * @see oft
   * @param  {Number} t the t of the instance communicatiing
   * @param {Foft} foft the instance doing communication
   * @return {object|Array<string>} communication object, or array of communication keys
   * @static
   */
  static tThis (t, foft) {
    let o = (Foft.isCalculable(t))
      ? { t }
      : { }
    let populateFunc = (foft instanceof Foft) && (Foft.isCalculable(t))
      ? (key) => {
        o[key] = foft[key](t)
      }
      : () => null
    let populateMemb = (foft instanceof Foft)
      ? (key) => {
        o[key] = foft[key]
      }
      : () => null
    Foft.funcKeys.map(fkey => populateFunc(fkey))
    Foft.membKeys.map(mkey => populateMemb(mkey))
    if (Object.keys(o).length === 0) {
      return Foft.funcKeys.concat(Foft.membKeys)
    } else {
      return o
    }
  };

  /**
   * given a string codeToParse, return true when code is found
   *  in Foft.opDict
   * @see Foft.opDict
   * @param  {string} codeToParse
   * @return {boolean}
   * @static
   */
  static isOp (codeToParse) {
    return Foft.opDict.includes(codeToParse)
  }

  /**
   * given a string codeToParse, return
   * the corresponding operation function from Foft.ops
   *
   * @see Foft.ops
   * @param  {string} codeToParse
   * @return {function} Foft.ops function corresponding to op
   * @static
   */
  static opParse (codeToParse) {
    return (Foft.isOp(codeToParse))
      ? Foft.ops[codeToParse]
      : Foft.ops[null]
  }
}

Object.defineProperties(Foft, {
  /**
   * valid types that Foft can do math on
   * @see Foft.mathTypeOf
   * @memberof Foft
   * @static
   */
  'mathTypes': {
    value: (() => {
      let o = {};
      [
        'arraylike', 'numberlike'
      ].map(function (e) {
        this[e] = Symbol(e)
        this[this[e]] = e
      }, o)
      return o
    })(),
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * an array of valid op keys
   * @see Foft.ops
   * @memberof Foft
   * @static
   */
  'opDict': {
    value: [null, '+', '-', '*', '/', '**', '...', 'magest', 'magesti'],
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * ops an object containing operations, or ops, that
   * perform mathematical functions corresponding to their keys
   * @see Foft.isOp
   * @see Foft.opDict
   * @see Foft.opParse
   * @see Foft.areNums
   * @memberof Foft
   * @static
   */
  'ops': {
    value: Object.defineProperties({}, {
      'opfunc': {
        value: (code, f) => {
          if (typeof code !== 'string') {
            throw new TypeError('Foft.ops.opfunc takes one string')
          } else if (!Foft.opDict.includes(code)) {
            throw new RangeError('Foft.ops.opfunc takes one string in Foft.opDict')
          }
          return (args) => [...args].reduce((acc, c, i) => f(acc, c))
        },
        writable: false,
        configurable: false,
        enumerable: false
      },
      'resfunc': {
        value: (code, f, base, args) => {
          if (!Foft.isCalculable(base)) {
            throw new TypeError('Foft.ops.resfunc requires a calculable base parameter')
          }
          if (!Foft.isArrayLike(args)) {
            throw new TypeError('Foft.ops.resfunc requires an iterable, Array or ArrayBuffer view args parameter')
          }
          if (Foft.areNums(...args)) {
            return Foft.ops.opfunc(code, f)(args)
          }
          let nullsReplaced = [...args].map(v => (v === null) ? base : v)
          if (Foft.areNums(nullsReplaced)) {
            return Foft.ops.opfunc(code, f)(nullsReplaced)
          }
          return NaN
        },
        writable: false,
        configurable: false,
        enumerable: false
      },
      [null]: {
        enumerable: true,
        value: (() => {
          let code = null
          let base = null
          let desc = 'null op'
          return Object.assign(
            function (...args) {
              return args
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      '+': {
        enumerable: true,
        value: (() => {
          let base = 0
          let code = '+'
          let desc = 'summation'
          return Object.assign(
            function () {
              return Foft.ops.resfunc(code, (a, b) => a + b, base, arguments)
            }, {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      '-': {
        enumerable: true,
        value: (() => {
          let base = 0
          let code = '-'
          let desc = 'subtraction'
          return Object.assign(
            function () {
              return Foft.ops.resfunc(code, (a, b) => a - b, base, arguments)
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      '*': {
        enumerable: true,
        value: (() => {
          let base = 1
          let code = '*'
          let desc = 'multiplication'
          return Object.assign(
            function () {
              return Foft.ops.resfunc(code, (a, b) => a * b, base, arguments)
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      '/': {
        enumerable: true,
        value: (() => {
          let base = 1
          let code = '/'
          let desc = 'division'
          return Object.assign(
            function () {
              return Foft.ops.resfunc(code, (a, b) => a / b, base, arguments)
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      '**': {
        enumerable: true,
        value: (() => {
          let base = 1
          let code = '**'
          let desc = 'exponentiation'
          return Object.assign(
            function () {
              return Foft.ops.resfunc(code, (a, b) => a ** b, base, arguments)
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      /**
       * ... recursive arraylike flatten. given parameter arr, return
       * - if arr is arraylike, a 1-d  Array containing the elements of arr with their order preserved, or
       * - if arr isn't arraylike, arr
       * @memberof Foft#ops
       * @param {arraylike} arr
       * @return {arraylike|?}
       */
      '...': {
        enumerable: true,
        value: (() => {
          let code = '...'
          let base = []
          let desc = 'flatten'
          return Object.assign(
            function flatten (arr) {
              if (!Foft.isArrayLike(arr)) {
                return arr
              } else {
                return arr.reduce((acc, v) => {
                  return (Foft.isArrayLike(v))
                    ? acc.concat(flatten(v))
                    : acc.concat(v)
                }, base)
              }
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      /**
       * @name magest - given some
       * - calculable values, return the one with the greatest MAGNITUDE
       * - arraylike values, return the one with the greatest MAGEST
       *
       * @memberof ops
       * @params {(number|Array<number>)} arguments
       * @return {(number|Array<number>)}
       */
      'magest': {
        enumerable: true,
        value: (() => {
          let code = 'magest'
          let base = 0
          let desc = 'greatest magnitude'
          return Object.assign(
            function (...args) {
              let max = base
              if ([...arguments].every(Foft.isArrayLike) && (arguments.length > 1)) {
                let maxmagest = base
                let magesti = base
                for (let i = 0; i < arguments.length; i++) {
                  let cur = Foft.ops['magest'](...arguments[i])
                  // console.log(cur)
                  if (cur > maxmagest) {
                    maxmagest = cur
                    magesti = i
                  }
                }
                return arguments[magesti]
              } else if (Foft.areCalculables(...arguments)) {
                for (var i = 0; i < arguments.length; i++) {
                  let cur = Math.abs(arguments[i])
                  max = (cur > max)
                    ? cur
                    : max
                }
                return max
              } else {
                return NaN
              }
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      },
      /**
       * @name magesti - given some values, return the first INDEX of the one with the greatest MAGEST
       *
       * @memberof ops
       * @params {(number|Array<number>)}
       * @return {number}
       */
      'magesti': {
        enumerable: true,
        value: (() => {
          let code = 'magesti'
          let base = 0
          let desc = 'index of greatest magnitude'
          return Object.assign(
            function () {
              let magests = Array(arguments.length)
              for (let i = 0; i < arguments.length; i++) {
                let a = arguments[i]
                magests[i] = (Foft.isArrayLike(a))
                  ? Foft.ops['magest'](...a)
                  : Foft.ops['magest'](a)
              }
              let index = magests.findIndex((v) => {
                // console.log(v)
                return Foft.equiv(
                  v,
                  Foft.ops['magest'](...magests)
                )
              })
              return index
            },
            {
              code,
              base,
              desc
            }
          )
        })(),
        configurable: false,
        writable: false
      }

    }),
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * dimensional labeling
   * @memberof Foft
   * @static
   */
  'R': {
    value: ['x', 'y', 'z'],
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * methods for inter-instance communication
   * @see tThis
   * @memberof Foft
   * @static
   */
  'funcKeys': {
    value: [
      'normalizeT',
      'antinormalizeT',
      'i'
    ],
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * members for inter-instance communication
   * @see tThis
   * @memberof Foft
   * @static
   */
  'membKeys': {
    value: [
      'range',
      'drange',
      't0',
      'segmentDivisor'
    ],
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * divisor By default, Foft instances divide into
   * this many segments
   * @type {Number}
   * @memberof Foft
   * @default 10
   * @static
   */
  'divisor': {
    value: 10,
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * maxSafeDivisor the maximum safe to use divisor
   * @borrows Foft.precision
   * @memberof Foft
   * @static
   */
  'maxSafeDivisor': {
    value: Foft.precision(),
    enumerable: true,
    configurable: false,
    writable: false
  },
  /**
   * sweep By default, Foft instances evaluate functions over this range
   * @type {Array.<Number>}
   * @default [-1,1]
   * @memberof Foft
   * @static
   */
  'sweep': {
    value: [-1, 1],
    enumerable: true,
    configurable: false,
    writable: false
  }
})

export { Foft }