'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 }