lomath
Version:
Lomath is a tensorial math library extended from lodash.
1,711 lines (1,684 loc) • 87.3 kB
JavaScript
////////////
// lomath //
////////////
// Prepare lodash for extension and export
var _ = require('lodash').runInContext();
// the module: lodash extended with math mixins
var lomath = _.mixin({
AUTHOR: "kengz",
VERSION: "0.2.9",
//////////////////////////////
// Function builder backend //
//////////////////////////////
// We employ clearer terminologies to distinguish the "depth" or the "dimension" of the objects. In general, we call generic array of depth-N a "N-tensor" or "rank-N tensor". A scalar is "0-tensor"; a simple array/vector is "1-tensor", matrix (array of arrays) is "2-tensor", and so on.
// A generic function that operates over tensor is built from an atomic function fn taking two scalar arguments.
// Applying a function into depths of tensor is done via distribution, and evaluating a multi-argument function is done via associativity.
/**
* Sample operation to demonstrate function composition.
*
* @category composition
* @param {*} x An argument.
* @param {*} y An argument.
*
* @example
* _.op('a', 'b')
* // → 'a*b'
*
*/
// sample operation to demonstrate function composition
op: function(x, y) {
return x + '*' + y;
},
/**
* Distributes a unary function over every scalar in tensor Y.
*
* @category composition
* @param {Function} fn A unary function.
* @param {tensor} Y A non-scalar tensor.
* @returns {tensor} A tensor from the function applied element-wise to Y.
*
* @example
* _.distributeSingle(_.square, [1, 2, 3, 4])
* // → [ 1, 4, 9, 16 ]
*
* _.distributeSingle(_.square, [[1, 2], [3, 4]])
* // → [ [ 1, 4 ], [ 9, 16 ] ]
*
*/
// distribute a unary function over every scalar in tensor Y;
distributeSingle: function(fn, Y) {
if (!(Y instanceof Array)) return fn(Y);
var len = Y.length,
res = Array(len);
while (len--) res[len] = Y[len] instanceof Array ?
lomath.distributeSingle(fn, Y[len]) : fn(Y[len])
return res;
},
/**
* Distributes a binary function with left tensor X over right scalar y. Preserves the order of arguments.
*
* @category composition
* @param {Function} fn A binary function.
* @param {tensor} X A non-scalar tensor.
* @param {number} y A scalar.
* @returns {tensor} A tensor from the function applied element-wise between X and y.
*
* @example
* _.distributeLeft(_.op([1, 2, 3, 4], 5))
* // where _.op is used to show the order of composition
* // → [ '1*5', '2*5', '3*5', '4*5' ]
*
* _.distributeLeft(_.op, [[1, 2], [3, 4]], 5)
* // → [ [ '1*5', '2*5' ], [ '3*5', '4*5' ] ]
*
*/
// Distribute fn with left tensor X over right scalar y.
distributeLeft: function(fn, X, y) {
var len = X.length,
res = Array(len);
while (len--) res[len] = X[len] instanceof Array ?
lomath.distributeLeft(fn, X[len], y) : fn(X[len], y)
return res;
},
/**
* Distributes a binary function with left scalar x over right tensor Y. Preserves the order of arguments.
*
* @category composition
* @param {Function} fn A binary function.
* @param {number} x A scalar.
* @param {tensor} Y A non-scalar tensor.
* @returns {tensor} A tensor from the function applied element-wise between x and Y.
*
* @example
* _.distributeRight(_.op, 5, [1, 2, 3, 4])
* // where _.op is used to show the order of composition
* // → [ '5*1', '5*2', '5*3', '5*4' ]
*
* _.distributeRight(_.op, 5, [[1, 2], [3, 4]])
* // → [ [ '5*1', '5*2' ], [ '5*3', '5*4' ] ]
*
*/
// Distribute fn with left scalar x over right tensor Y.
distributeRight: function(fn, x, Y) {
var len = Y.length,
res = Array(len);
while (len--) res[len] = Y[len] instanceof Array ?
lomath.distributeRight(fn, x, Y[len]) : fn(x, Y[len])
return res;
},
/**
* Distributes a binary function between non-scalar tensors X, Y: pair them up term-wise and calling `_.distribute` recursively. Perserves the order of arguments.
* If at any depth X and Y have different lengths, recycle if the mod of lengths is 0.
*
* @category composition
* @param {Function} fn A binary function.
* @param {tensor} X A non-scalar tensor.
* @param {tensor} Y A non-scalar tensor.
* @returns {tensor} A tensor from the function applied element-wise between X and Y.
*
* @example
* _.distributeBoth(_.op, ['a', 'b', 'c'], [1, 2, 3])
* // where _.op is used to show the order of composition
* // → [ 'a*1', 'b*2', 'c*3' ]
*
* _.distributeBoth(_.op, ['a', 'b', 'c'], [1, 2, 3, 4, 5, 6])
* // → [ 'a*1', 'b*2', 'c*3' , 'a*4', 'b*5', 'c*6']
*
* _.distributeBoth(_.op, ['a', 'b', 'c'], [[1, 2], [3, 4], [5, 6]])
* // → [ [ 'a*1', 'a*2' ], [ 'b*3', 'b*4' ], [ 'c*5', 'c*6' ] ]
*
*/
// Distribute fn between non-scalar tensors X, Y: pair them up term-wise and calling distribute recursively.
// If at any depth X and Y have different lengths, recycle if the mod of lengths is 0.
distributeBoth: function(fn, X, Y) {
var Xlen = X.length,
Ylen = Y.length;
if (Xlen % Ylen == 0 || Ylen % Xlen == 0) {
var res;
if (Xlen > Ylen) {
res = Array(Xlen);
while (Xlen--) res[Xlen] = lomath.distribute(fn, X[Xlen], Y[Xlen % Ylen]);
} else {
res = Array(Ylen);
while (Ylen--) res[Ylen] = lomath.distribute(fn, X[Ylen % Xlen], Y[Ylen]);
}
return res;
} else throw "Cannot distribute arrays of different dimensions.";
},
/**
* Generic Distribution: Distribute fn between left tensor X and right tensor Y, while preserving the argument-ordering (vital for non-commutative functions).
* Pairs up the tensors term-wise while descending down the depths recursively using `_.distributeBoth`, until finding a scalar to `_.distributeLeft/Right`.
*
* @category composition
* @param {Function} fn A binary function.
* @param {tensor} X A tensor.
* @param {tensor} Y A tensor.
* @returns {tensor} A tensor from the function applied element-wise between X and Y.
*
* @example
* _.distribute(_.op, 'a', [1, 2, 3])
* // where _.op is used to show the order of composition
* // → ['a*1', 'a*2', 'a*3']
*
* _.distribute(_.op, 'a', [[1, 2], [3, 4])
* // → [ [ 'a*1', 'a*2' ], [ 'a*3', 'a*4' ] ]
*
* _.distribute(_.op, ['a', 'b', 'c'], [1, 2, 3])
* // → [ 'a*1', 'b*2', 'c*3' ]
*
* _.distribute(_.op, ['a', 'b', 'c'], [1, 2, 3, 4, 5, 6])
* // → [ 'a*1', 'b*2', 'c*3' , 'a*4', 'b*5', 'c*6']
*
* _.distribute(_.op, ['a', 'b', 'c'], [[1, 2], [3, 4], [5, 6]])
* // → [ [ 'a*1', 'a*2' ], [ 'b*3', 'b*4' ], [ 'c*5', 'c*6' ] ]
*
*/
// Generic Distribute: Distribute fn between left tensor X and right tensor Y, while preserving the argument-ordering (vital for non-commutative functions).
// lomath pairs up the tensors term-wise while descending down the depths recursively, until finding a scalar to distributeLeft/Right.
// Method is at its fastest, and assuming the data depth isn't too deep (otherwise JS will have troubles with it)
distribute: function(fn, X, Y) {
if (X instanceof Array)
return Y instanceof Array ?
lomath.distributeBoth(fn, X, Y) : lomath.distributeLeft(fn, X, Y);
else
return Y instanceof Array ?
lomath.distributeRight(fn, X, Y) : fn(X, Y);
},
/**
* Generic association: take the arguments object or array and apply atomic function (with scalar arguments) from left to right.
*
* @category composition
* @param {Function} fn An atomic binary function (both arguments must be scalars).
* @param {...number} [...x] Scalars; can be grouped in a single array.
* @returns {number} A scalar from the function applied to all arguments in order.
*
* @example
* _.asso(_.op, 'a', 'b', 'c')
* // where _.op is used to show the order of composition
* // → 'a*b*c'
*
* _.asso(_.op, ['a', 'b', 'c'])
* // → 'a*b*c'
*
*/
// Generic associate: take the arguments object or array and apply atomic fn (non-tensor) from left to right
asso: function(fn, argObj) {
var len = argObj.length,
i = 0;
// optimize arg form based on length or argObj
/* istanbul ignore next */
var args = len < 3 ? argObj : _.toArray(argObj),
res = fn(args[i++], args[i++]);
while (i < len) res = fn(res, args[i++]);
return res;
},
/**
* Generic association with distributivity: Similar to `_.asso` but is for tensor functions; apply atomic fn distributively in order using `_.distribute`.
* Usage: for applying fn on tensors element-wise if they have compatible dimensions.
*
* @category composition
* @param {Function} fn An atomic binary function (both arguments must be scalars).
* @param {...tensors} [...X] tensors.
* @returns {tensor} A tensor from the function applied to all arguments in order.
*
* @example
* _.assodist(_.op, 'a', 'b', 'c')
* // where _.op is used to show the order of composition
* // → 'a*b*c'
*
* _.assodist(_.op, 'a', [1, 2, 3], 'b')
* // → ['a*1*b', 'a*2*b', 'a*3*b']
*
* _.assodist(_.op, 'a', [[1, 2], [3, 4]])
* // → [['a*1', 'a*2'], ['a*3', 'a*4']]
*
* _.assodist(_.op, ['a', 'b'], [[1, 2], [3, 4]])
* // → [['a*1', 'a*2'], ['b*3', 'b*4']]
*
*/
// Associate with distributivity: Similar to asso but is for tensor functions; apply atomic fn distributively from left to right.
// Usage: for applying fn on tensors element-wise if they have matching dimensions.
assodist: function(fn, argObj) {
var len = argObj.length,
i = 0;
// optimize arg form based on length or argObj
var args = len < 3 ? argObj : _.toArray(argObj),
res = lomath.distribute(fn, args[i++], args[i++]);
while (i < len) res = lomath.distribute(fn, res, args[i++]);
return res;
},
// Future:
// Future:
// Future:
// cross and wedge, need index summation too, matrix mult.
/////////////////////
// Basic functions //
/////////////////////
/**
* Concatenates all arguments into single vector by `_.flattenDeep`.
*
* @category basics
* @param {...tensors} [...X] tensors.
* @returns {vector} A vector with the scalars from all tensors.
*
* @example
* _.c('a', 'b', 'c')
* // → ['a', 'b', 'c']
*
* _.c(1, ['a', 'b', 'c'], 2)
* // → [1, 'a', 'b', 'c', 2]
*
* _.c([[1, 2], [3, 4])
* // → [1, 2, 3, 4]
*
*/
// Concat all arguments into single vector by _.flattenDeep
c: function() {
return _.flattenDeep(_.toArray(arguments));
},
// atomic sum: takes in a tensor (any rank) and sum all values
a_sum: function(T) {
// actual function call; recurse if need to
var total = 0,
len = T.length;
while (len--) total += (T[len] instanceof Array ?
lomath.a_sum(T[len], 0) : T[len])
return total;
},
/**
* Sums all scalars in all argument tensors.
*
* @category basics
* @param {...tensors} [...X] tensors.
* @returns {scalar} A scalar summed from all scalars in the tensors.
*
* @example
* _.sum('a', 'b', 'c')
* // → 'abc'
*
* _.sum(0, [1, 2, 3], [[1, 2], [3, 4])
* // → 16
*
*/
// sum all values in all arguments
sum: function() {
var res = 0;
var len = arguments.length;
while (len--) res += (arguments[len] instanceof Array ?
lomath.a_sum(arguments[len]) : arguments[len])
return res;
},
/**
* Functional sum, Basically Sigma_i fn(T[i]) with fn(T, i), where T is a tensor, i is first level index.
*
* @category basics
* @param {tensor} T A tensor.
* @param {function} fn A function fn(T, i) applied to the i-th term of T for the sum. Note the function can access the whole T for any term i for greater generality.
* @returns {scalar} A scalar summed from all the terms from the mapped T fn(T, i).
*
* @example
* // sum of the elements multiplied by indices in a sequence, i.e. Sigma_i i*(x_i)
* _.fsum([1,1,1], function(T, i){
* return T[i] * i;
* })
* // → 0+1+2
*
*/
fsum: function(T, fn) {
var sum = 0;
for (var i = 0; i < T.length; i++)
sum += fn(T, i);
return sum;
},
// atomic prod, analogue to a_sum. Multiply all values in a tensor
a_prod: function(T) {
// actual function call; recurse if need to
var total = 1,
len = T.length;
while (len--) total *= (T[len] instanceof Array ?
lomath.a_prod(T[len], 1) : T[len])
return total;
},
/**
* Multiplies together all scalars in all argument tensors.
*
* @category basics
* @param {...tensors} [...X] tensors.
* @returns {scalar} A product scalar from all scalars in the tensors.
*
* @example
* _.prod(1, 2, 3)
* // → 6
*
* _.prod([1, 2, 3])
* // → 6
*
* _.prod(1, [1, 2, 3], [[1, 2], [3, 4]])
* // → 144
*
*/
// product of all values in all arguments
prod: function() {
var res = 1,
len = arguments.length;
while (len--) res *= (arguments[len] instanceof Array ?
lomath.a_prod(arguments[len]) : arguments[len])
return res;
},
// atomic add: add two scalars x, y.
a_add: function(x, y) {
return x + y;
},
/**
* Adds tensors using `_.assodist`.
*
* @category basics
* @param {...tensors} [...X] tensors.
* @returns {tensor} A tensor.
*
* @example
* _.add(1, 2, 3)
* // → 6
*
* _.add(1, [1, 2, 3])
* // → [2, 3, 4]
*
* _.add(1, [[1, 2], [3, 4]])
* // → [[2, 3], [4, 5]]
*
* _.add([10, 20], [[1, 2], [3, 4]])
* // → [[11, 12], [23, 24]]
*
*/
// add all tensor arguments element-wise/distributively and associatively
add: function() {
// sample call pattern: pass whole args
return lomath.assodist(lomath.a_add, arguments);
},
// atomic subtract
a_subtract: function(x, y) {
return x - y;
},
/**
* Subtracts tensors using `_.assodist`.
*
* @category basics
* @param {...tensors} [...X] tensors.
* @returns {tensor} A tensor.
*
* @example
* _.subtract(1, 2, 3)
* // → -5
*
* _.subtract(1, [1, 2, 3])
* // → [0, -1, -2]
*
* _.subtract(1, [[1, 2], [3, 4]])
* // → [[0, -1], [-2, -3]]
*
* _.subtract([10, 20], [[1, 2], [3, 4]])
* // → [[9, 8], [17, 16]]
*
*/
// subtract all tensor arguments element-wise/distributively and associatively
subtract: function() {
return lomath.assodist(lomath.a_subtract, arguments);
},
// atomic multiply
a_multiply: function(x, y) {
return x * y;
},
/**
* Multiplies tensors using `_.assodist`.
*
* @category basics
* @param {...tensors} [...X] tensors.
* @returns {tensor} A tensor.
*
* @example
* _.multiply(1, 2, 3)
* // → 6
*
* _.multiply(1, [1, 2, 3])
* // → [1, 2, 3]
*
* _.multiply(1, [[1, 2], [3, 4]])
* // → [[1, 2], [3, 4]]
*
* _.multiply([10, 20], [[1, 2], [3, 4]])
* // → [[10, 20], [60, 80]]
*
*/
// multiply all tensor arguments element-wise/distributively and associatively
// Note: lomath is generic; is different from matrix multiplication
multiply: function() {
return lomath.assodist(lomath.a_multiply, arguments);
},
// atomic divide
a_divide: function(x, y) {
return x / y;
},
/**
* Divides tensors using `_.assodist`.
*
* @category basics
* @param {...tensors} [...X] tensors.
* @returns {tensor} A tensor.
*
* @example
* _.divide(3, 2, 1)
* // → 1.5
*
* _.divide([1, 2, 3], 2)
* // → [0.5, 1, 1.5]
*
* _.divide([[1, 2], [3, 4]], 2)
* // → [[0.5, 1], [1.5, 2]]
*
* _.divide([[1, 2], [3, 4]], [1, 2])
* // → [[1, 2], [1.5, 2]]
*
*/
// divide all tensor arguments element-wise/distributively and associatively
divide: function() {
return lomath.assodist(lomath.a_divide, arguments);
},
// atomic log. Use base e by default
a_log: function(x, base) {
return base == undefined ? Math.log(x) : Math.log(x) / Math.log(base);
},
/**
* Takes the log of tensor T to base n (defaulted to e) element-wise using `_.distribute`.
*
* @category basics
* @param {tensor} T A tensor.
* @param {number} [n=e] The optional base; defaulted to e.
* @returns {tensor} A tensor.
*
* @example
* _.log([1, Math.E])
* // → [0, 1]
*
*/
// take the log of tensor T to the n element-wise
log: function(T, base) {
return lomath.distribute(lomath.a_log, T, base);
},
// atomic square
a_square: function(x) {
return x * x;
},
/**
* Squares a tensor element-wise using `_.distributeSingle`.
*
* @category basics
* @param {tensor} T A tensor.
* @returns {tensor} A tensor.
*
* @example
* _.square([1, 2])
* // → [1, 4]
*
*/
square: function(T) {
return lomath.distributeSingle(lomath.a_square, T);
},
// atomic root
a_root: function(x, base) {
var n = base == undefined ? 2 : base;
return n % 2 ?
// if odd power
Math.sign(x) * Math.pow(Math.abs(x), 1 / n) :
Math.pow(x, 1 / n);
},
/**
* Takes the n-th root (defaulted to 2) of tensor T element-wise using `_.distribute`.
*
* @category basics
* @param {tensor} T A tensor.
* @param {number} [n=2] The optional base; defaulted to 2 for squareroot.
* @returns {tensor} A tensor.
*
* @example
* _.root([1, 4])
* // → [1, 2]
*
* _.root([-1, -8], 3)
* // → [-1, -2]
*
*/
// take the n-th root of tensor T element-wise
root: function(T, n) {
return lomath.distribute(lomath.a_root, T, n);
},
// atomic logistic
a_logistic: function(z) {
return 1 / (1 + Math.exp(-z))
},
/**
* Applies the logistic (sigmoid) function to tensor T element-wise.
*
* @category basics
* @param {tensor} T A tensor.
* @returns {tensor} A tensor.
*
* @example
* _.logistic([-10, 0, 10])
* // → [ 0.00004539786870243441, 0.5, 0.9999546021312976 ]
*
*/
logistic: function(T) {
return lomath.distributeSingle(lomath.a_logistic, T);
},
////////////////////
// Basic checkers //
////////////////////
/**
* Checks if `x` is in range, i.e. `left ≤ x ≤ right`.
*
* @category signature
* @param {number} left The lower bound.
* @param {number} right The upper bound.
* @param {number} x The value to check.
* @returns {boolean} true If `x` is in range.
*
* @example
* _.inRange(0, 3, 3)
* // → true
*
* _.inRange.bind(null, 0, 3)(3)
* // → true
*
*/
// check if x is in range set by left, right
inRange: function(left, right, x) {
return left - 1 < x && x < right + 1;
},
/**
* Checks if `x` is an integer.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
// check if x is an integer
isInteger: function(x) {
return x == Math.floor(x);
},
/**
* Checks if `x` is a double-precision number/non-Integer.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
// check if x is a double
isDouble: function(x) {
return x != Math.floor(x);
},
/**
* Checks if `x > 0`.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
// check if x is positive
isPositive: function(x) {
return x > 0;
},
/**
* Checks if `x ≤ 0`.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
// check if x less than or eq to 0
nonPositive: function(x) {
return !(x > 0);
},
/**
* Checks if `x < 0`.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
// check if x is negative
isNegative: function(x) {
return x < 0;
},
/**
* Checks if `x ≥ 0`.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
// check if x greater than or eq to 0
nonNegative: function(x) {
return !(x < 0);
},
/**
* Checks if `x == 0`.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
isZero: function(x) {
return x == 0;
},
/**
* Checks if `x != 0`.
*
* @category signature
* @param {number} x The value to check.
* @returns {boolean} true If so.
*/
nonZero: function(x) {
return x != 0;
},
/**
* Checks the parity (even/odd) of x. Useful for when doing the sum with alternating sign.
*
* @category signature
* @param {number} x The value to check.
* @returns {number} n -1 if odd, +1 if even.
*/
parity: function(x) {
return x % 2 ? -1 : 1;
},
/**
* Checks if signature function is true for all scalars of a tensor.
*
* @category signature
* @param {tensor} T The tensor whose values to check.
* @param {Function} sigFn The signature function.
* @returns {boolean} true If all scalars of the tensor return true.
*
* @example
* _.sameSig([1, 2, 3], _.isPositive)
* // → true
*
*/
// check if all tensor entries are of the same sign, with the specified sign function
sameSig: function(T, sigFn) {
return Boolean(lomath.prod(lomath.distributeSingle(sigFn, T)));
},
/**
* Checks if two tensors have identifcal entries.
*
* @category signature
* @param {tensor} T A tensor
* @param {tensor} S Another tensor
* @returns {boolean} match True if tensors are identical, false otherwise.
*
* @example
* _.deepEqual([[1,2],[3,4]], [[1,2],[3,4]])
* // → true
* _.deepEqual([1,2,3], [1,2,3])
* // → true
* _.deepEqual([1,2,3], [1,2,3,4])
* // → false
* _.deepEqual([1,2,3], [1,2,0])
* // → false
*
*/
// check if all tensor entries are of the same sign, with the specified sign function
deepEqual: function(T, S) {
if (T.length != S.length) return false;
var Left = T,
Right = S;
if (!lomath.isFlat(T)) {
Left = _.flattenDeep(T);
Right = _.flattenDeep(S);
};
var Llen = Left.length,
Rlen = Right.length;
while (Llen--) {
if (Left[Llen] != Right[Llen]) return false;
}
return true;
},
//////////////////////////////////////////
// Unary functions from JS Math object, //
//////////////////////////////////////////
// wrapped to function with generic tensor
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
* @example
* _.abs([-1, -2, -3])
* // → [1, 2, 3]
*/
abs: function(T) {
return lomath.distributeSingle(Math.abs, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
acos: function(T) {
return lomath.distributeSingle(Math.acos, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
acosh: function(T) {
return lomath.distributeSingle(Math.acosh, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
asin: function(T) {
return lomath.distributeSingle(Math.asin, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
asinh: function(T) {
return lomath.distributeSingle(Math.asinh, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
atan: function(T) {
return lomath.distributeSingle(Math.atan, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
atanh: function(T) {
return lomath.distributeSingle(Math.atanh, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
ceil: function(T) {
return lomath.distributeSingle(Math.ceil, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
cos: function(T) {
return lomath.distributeSingle(Math.cos, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
cosh: function(T) {
return lomath.distributeSingle(Math.cosh, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
exp: function(T) {
return lomath.distributeSingle(Math.exp, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
floor: function(T) {
return lomath.distributeSingle(Math.floor, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
log10: function(T) {
return lomath.distributeSingle(Math.log10, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
log1p: function(T) {
return lomath.distributeSingle(Math.log1p, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
log2: function(T) {
return lomath.distributeSingle(Math.log2, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
round: function(T) {
return lomath.distributeSingle(Math.round, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
pow: function(T, n) {
return lomath.distribute(Math.pow, T, n);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
sign: function(T) {
return lomath.distributeSingle(Math.sign, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
sin: function(T) {
return lomath.distributeSingle(Math.sin, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
sinh: function(T) {
return lomath.distributeSingle(Math.sinh, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
sqrt: function(T) {
return lomath.distributeSingle(Math.sqrt, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
tan: function(T) {
return lomath.distributeSingle(Math.tan, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
tanh: function(T) {
return lomath.distributeSingle(Math.tanh, T);
},
/**
* Generalized JS Math applicable to tensor using function composition.
* @category native-Math
* @param {tensor} T A tensor.
* @returns {tensor} T A tensor.
*/
trunc: function(T) {
return lomath.distributeSingle(Math.trunc, T);
},
/////////////////////
// Regex functions //
/////////////////////
/**
* Returns a boolean function that matches the regex.
* @category regexp
* @param {RegExp} regex A RegExp.
* @returns {Function} fn A boolean function used for matching the regex.
*
* @example
* var matcher1 = _.reMatch('foo') // using a string
* matcher1('foobarbaz')
* // → true
*
* var matcher2 = _.reMatch(/\d+/) // using a regexp
* matcher2('May 1995')
* // → true
*
*/
// return a function that matches regex,
// e.g. matchRegex(/red/)('red Apple') returns true
reMatch: function(regex) {
return function(str) {
/* istanbul ignore next */
if (str != undefined)
return str.search(regex) != -1;
}
},
/**
* Returns a boolean function that dis-matches the regex.
* @category regexp
* @param {RegExp} regex A RegExp to NOT match.
* @returns {Function} fn A boolean function used for dis-matching the regex.
*
* @example
* var matcher1 = _.reNotMatch('foo') // using a string
* matcher1('barbaz')
* // → true
*
* var matcher2 = _.reNotMatch(/\d+/) // using a regexp
* matcher2('foobar')
* // → true
*
*/
// negation of reMatch
reNotMatch: function(regex) {
return function(str) {
/* istanbul ignore next */
if (str != undefined)
return str.search(regex) == -1;
}
},
/**
* Returns a function that returns the first string portion matching the regex.
* @category regexp
* @param {RegExp} regex A RegExp to match.
* @returns {Function} fn A function that returns the string matching the regex.
*
* @example
* var getBar = _.reGet('bar') // using a string
* getBar('foobarbaz')
* // → 'bar'
*
* var getNum = _.reGet(/\d+/) // using a regex
* getNum('May 1995')
* // → '1995'
* getNum('May')
* // → null
*
*/
// return the string matched by regex
reGet: function(regex) {
return function(str) {
/* istanbul ignore next */
if (str != undefined) {
var matched = str.match(regex);
return matched == null ? null : matched[0];
}
}
},
/**
* Wraps a regex into string for regex set operation.
* @category regexp
* @param {RegExp} regex A RegExp to wrap.
* @returns {string} wrapped The regex wrapped into the form `(?:regex)`
*
* @example
* _.reWrap('foo')
* // → '(?:foo)'
*
*/
// wrap a regex into string for regex set operation
reWrap: function(reg) {
return '(?:' + String(reg).replace(/\//g, '') + ')'
},
/**
* Returns a single regex as the "AND" conjunction of all input regexs. This picks up as MANY adjacent substrings that satisfy all the regexs in order.
* @category regexp
* @param {...RegExp} regexs All the regexs to conjunct together.
* @returns {RegExp} regex The conjuncted regex of the form `(?:re1)...(?:reN)`
*
* @example
* var reg1 = _.reAnd('foo', /\d+/)
* // → /(?:foo)(?:\d+)/
* _.reGet(reg1)('Mayfoo1995')
* // → 'foo1995'
*
* var reg2 = _.reAnd(/\d+/, 'foo') // order matters here
* // → /(?:\d+)(?:foo)/
* _.reGet(reg2)('Mayfoo1995')
* // → null
*
*/
// return a single regex as the "AND" of all arg regex's
reAnd: function() {
return new RegExp(_.map(_.toArray(arguments), lomath.reWrap).join(''));
},
/**
* Returns a boolean function that matches all the regexs conjuncted in the specified order.
*
* @category regexp
* @param {...RegExp} regexs All the regexs to conjunct together.
* @returns {Function} fn A boolean function used for matching the conjuncted regexs.
*
* @example
* _.reAndMatch('foo', /\d+/)('Mayfoo1995')
* // → true
*
* _.reAndMatch(/\d+/, 'foo')('Mayfoo1995') // order matters
* // → false
*
*/
// return a function that matches all(AND) of the regexs
reAndMatch: function() {
return lomath.reMatch(lomath.reAnd.apply(null, arguments));
},
/**
* Returns a single regex as the "OR" union of all input regexs. This picks up the FIRST substring that satisfies any of the regexs in any order.
* @category regexp
* @param {...RegExp} regexs All the regexs to union together.
* @returns {RegExp} regex The unioned regex of the form `(?:re1)|...|(?:reN)`
*
* @example
* var reg1 = _.reOr('foo', /\d+/)
* // → /(?:foo)|(?:\d+)/
* _.reGet(reg1)('Mayfoo1995')
* // → 'foo'
*
* var reg2 = _.reOr(/\d+/, 'foo') // order doesn't matter here
* // → /(?:\d+)|(?:foo)/
* _.reGet(reg2)('Mayfoo1995')
* // → 'foo'
*
*/
// return a single regex as the "OR" of all arg regex's
reOr: function() {
return new RegExp(_.map(_.toArray(arguments), lomath.reWrap).join('|'));
},
/**
* Returns a boolean function that matches any of the regexs in any order.
*
* @category regexp
* @param {...RegExp} regexs All the regexs to try to match.
* @returns {Function} fn A boolean function used for matching the regexs.
*
* @example
* _.reOrMatch('foo', /\d+/)('Mayfoo1995')
* // → true
*
* _.reOrMatch(\d+/, 'foo')('Mayfoo1995') // order doesn't matter
* // → true
*
*/
// return a function that matches at least one(OR) of the regexs
reOrMatch: function() {
return lomath.reMatch(lomath.reOr.apply(null, arguments));
},
/**
* Converts a regexp into a string that can be used to reconstruct the same regex from 'new RegExp()'. This is a way to store a regexp as string. Note that the flags are excluded, and thus must be provided during regexp reconstruction.
*
* @category regexp
* @param {RegExp} regexp To be stored as string.
* @returns {string} reStr The regex as string, to be used in the RegExp() constructor with a supplied flag.
*
* @example
* var reStr = _.reString(/\d+|\s+/ig)
* // → '\d+|\s+'
*
* new RegExp(reStr, "gi")
* // → /\d+|\s+/gi
*
*/
reString: function(regexp) {
return regexp.toString().replace(/^\/|\/.*$/g, '')
},
////////////////////
// Array creation //
////////////////////
// union, intersection, difference, xor
/**
* Returns a sequence of numbers from start to end, with interval. Similar to lodash's `_.range` but the default starts from 1; this is for `R` users who are familiar with `seq()`.
*
* @category initialization
* @param {number} [start=0] The start value.
* @param {number} end The end value.
* @param {number} [step=1] The interval step.
* @returns {Array} seq An array initialized to the sequence.
*
* @example
* _.seq(3)
* // → [1, 2, 3]
*
* _.seq(2, 4)
* // → [2, 3, 4]
*
* _.seq(1, 9, 2)
* [ 1, 3, 5, 7, 9 ]
*
*/
// seq from R: like _.range, but starts with 1 by default
seq: function(start, stop, step) {
if (stop == null) {
/* istanbul ignore next */
stop = start || 1;
start = 1;
}
step = step || 1;
var length = Math.max(Math.ceil((stop - start) / step), 0) + 1;
var range = Array(length);
for (var idx = 0; idx < length; idx++, start += step) {
range[idx] = start;
}
return range;
},
/**
* Returns an initialized array of length N filled with the value (defaulted to 0). Reminiscent of `numeric()` of `R`.
*
* @category initialization
* @param {number} N The length of array.
* @param {*} [val=0] The value to fill array with.
* @returns {Array} filled An array initialized to the value.
*
* @example
* _.numeric(3)
* // → [0, 0, 0]
*
* _.numeric(3, 'a')
* // → ['a', 'a', 'a']
*
*/
// return an array of length N initialized to val (default to 0)
numeric: function(N, val) {
return val == undefined ? _.fill(Array(N), 0) : _.fill(Array(N), val);
},
///////////////////////
// Tensor properties //
///////////////////////
// Note that a tensor has homogenous depth, that is, there cannot tensors of different ranks in the same vector, e.g. [1, [2,3], 4] is prohibited.
/**
* Returns the depth of an (nested) array, i.e. the rank of a tensor.
* Scalar = rank-0, vector = rank-1, matrix = rank-2, ... so on.
* Note that a tensor has homogenous depth, that is, there cannot tensors of different ranks in the same vector, e.g. [1, [2,3], 4] is prohibited.
*
* @category properties
* @param {tensor} T The tensor.
* @returns {number} depth The depth of the array.
*
* @example
* _.depth(0)
* // → 0
*
* _.depth([1, 2, 3])
* // → 1
*
* _.depth([[1, 2], [3, 4]])
* // → 2
*
*/
// return the depth (rank) of tensor, assuming homogeneity
depth: function(T) {
var t = T,
d = 0;
while (t.length) {
d++;
t = t[0];
}
return d;
},
/**
* Returns the "volume" of a tensor, i.e. the totaly number of scalars in it.
*
* @category properties
* @param {tensor} T The tensor.
* @returns {number} volume The number of scalar entries in the tensor.
*
* @example
* _.volume(0)
* // → 0
*
* _.volume([1, 2, 3])
* // → 3
*
* _.volume([[1, 2], [3, 4]])
* // → 4
*
*/
// return the size of a tensor (total number of scalar entries)
// return 0 for scalar
volume: function(T) {
return _.flattenDeep(T).length;
},
/**
* Returns the "dimension" of a tensor.
* Note that a tensor has homogenous depth, that is, there cannot tensors of different ranks in the same vector, e.g. [1, [2,3], 4] is prohibited.
*
* @category properties
* @param {tensor} T The tensor.
* @returns {Array} dim The dimension the tensor.
*
* @example
* _.dim(0)
* // → []
*
* _.dim([1, 2, 3])
* // → [3]
*
* _.dim([[1, 2, 3], [4, 5, 6]])
* // → [2, 3]
*
* _.dim([
[[1,1,1,1],[2,2,2,2],[3,3,3,3]],
[[4,4,4,4],[5,5,5,5],[6,6,6,6]]
])
* // → [2, 3, 4]
*
*/
// Get the dimension of a (non-scalar) tensor by _.flattenDeep, assume rectangular
dim: function(T) {
var dim = [],
ptr = T;
while (ptr.length) {
dim.push(ptr.length);
ptr = ptr[0];
}
return dim;
},
/**
* Checks if a tensor is "flat", i.e. all entries are scalars.
*
* @category properties
* @param {tensor} T The tensor.
* @returns {boolean} true If tensor is flat.
*
* @example
* _.isFlat(0)
* // → true
*
* _.isFlat([1, 2, 3])
* // → true
*
* _.isFlat([[1, 2], [3, 4]])
* // → false
*
*/
// check if a tensor is rank-1
isFlat: function(T) {
var flat = true,
len = T.length;
while (len--) {
flat *= !(T[len] instanceof Array);
if (!flat) break;
}
return Boolean(flat);
},
/**
* Returns the maximum length of the deepest array in (non-scalar) tensor T.
* Useful for probing the data structure and ensuring tensor is rectangular.
*
* @category properties
* @param {tensor} T The tensor.
* @returns {number} length The maximum length of the deepest array in T.
*
* @example
* _.maxDeepestLength(0)
* // → 0
*
* _.maxDeepestLength([1, 2, 3])
* // → 3
*
* _.maxDeepestLength([[1, 2], [3, 4]])
* // → 2
*
*/
// get the maximum length of the deepest array in (non-scalar) tensor T.
maxDeepestLength: function(T) {
if (!(T instanceof Array)) return 0;
var stack = [],
sizes = [];
stack.push(T);
while (stack.length) {
var curr = stack.pop(),
len = curr.length;
if (lomath.isFlat(curr))
sizes.push(len);
else
while (len--)
stack.push(curr[len]);
}
return _.max(sizes);
},
///////////////////////////
// Tensor transformation //
///////////////////////////
// lodash methods
// _.chunk
// _.flatten, _.flattenDeep
/**
* Swaps entries at indices `i,j`.
* Mutates the array.
*
* @category transformation
* @param {Array} T The array.
* @param {number} i The swap-index.
* @param {number} j The swap-index.
* @returns {Array} T The mutated array after swapping.
*
* @example
* _.swap([1, 2, 3], 0, 2)
* // → [3, 2, 1]
*
*/
// swap at index i, j
// Mutates the array
swap: function(arr, i, j) {
arr[i] = arr.splice(j, 1, arr[i])[0];
return arr;
},
/**
* Returns a copy of the array reversed, optionally from index `i` to `j` inclusive.
*
* @category transformation
* @param {Array} T The array.
* @param {number} [i=0] The from-index.
* @param {number} [j=T.length-1] The to-index.
* @returns {Array} R The reversed copy of the array.
*
* @example
* _.reverse([0, 1, 2, 3, 4, 5])
* // → [5, 4, 3, 2, 1, 0]
*
* _.reverse([0, 1, 2, 3, 4, 5], 2) // reverse from index 2
* // → [0, 1, 5, 4, 3, 2]
*
* _.reverse([0, 1, 2, 3, 4, 5], null, 2) // reverse to index 2
* // → [2, 1, 0, 3, 4, 5]
*
* _.reverse([0, 1, 2, 3, 4, 5], 2, 4) // reverse from index 2 to 4
* // → [0, 1, 4, 3, 2, 5]
*
*/
// return a copy of reversed arr from index i to j inclusive
reverse: function(arr, i, j) {
var vec = arr.slice(0);
var k = i == undefined ? 0 : i;
var l = j == undefined ? arr.length - 1 : j;
var mid = Math.ceil((k + l) / 2);
while (k < mid)
lomath.swap(vec, k++, l--);
return vec;
},
/**
* Extends an array till `toLen` by prepending with `val`.
* Mutates the array.
*
* @category transformation
* @param {Array} T The array.
* @param {number} toLen The new length of the array. Must be longer than T.length.
* @param {number} [val=0] The value to prepend with.
* @returns {Array} T The mutated array after extending.
*
* @example
* _.extend([1, 2, 3], 6)
* // → [1, 2, 3, 0, 0, 0]
*
* _.extend([1, 2, 3], 6, 'a')
* // → [1, 2, 3, 'a', 'a', 'a']
*
*/
// return a copy: extend an array till toLen, filled with val defaulted to 0.
// Mutates the array
extend: function(arr, toLen, val) {
var lendiff = toLen - arr.length,
rePal = (val == undefined ? 0 : val);
if (lendiff < 0)
throw new Error("Array longer than the length to extend to")
while (lendiff--)
arr.push(rePal);
return arr;
},
/**
* Searches the array in batch by applying `_.indexOf` in batch; returns the indices of the results in order.
* Useful for grabbing the headers in a data set.
*
* @category transformation
* @param {Array} T The array.
* @param {Array} fields The array of fields to search for in T.
* @returns {Array} inds The indices returned by applying `_.indexOf` to fields.
*
* @example
* _.batchIndexOf(['a','b','c','d','e','f'], [1, 'b', 'a', 'a'])
* // → [-1, 1, 0, 0]
*
*/
// applying _.indexOf in batch; returns -1 for field if not found
batchIndexOf: function(arr, fieldArr) {
return _.map(fieldArr, function(t) {
return _.indexOf(arr, t)
});
},
/**
* Filters out the invalid indices (negatives) in an array of indices. Basically keeps `x` where `0 ≤ x ≤ maxLen`.
* Used with `_.batchIndexOf`.
*
* @category transformation
* @param {Array} T The array of indices, can be from `_.batchIndexOf`.
* @param {number} maxLen The max value the indices can have.
* @returns {Array} inds A copy of the array with only valid indices.
*
* @example
* _.validInds([-2, 4, 0, 2, -1], 2)
* // → [0, 2]
*
*/
// return valid indices from indArr, i.e. in range 0 to maxLen
validInds: function(indArr, maxLen) {
return _.filter(indArr, lomath.inRange.bind(null, 0, maxLen));
},
/**
* Returns a new matrix with the selected rows from a matrix. Same as `rbind()` from `R`.
* Useful for picking certain rows from a matrix/tensor.
*
* @category transformation
* @param {tensor} M The original matrix.
* @param {Array} indArr The array of indices specifying the rows of M to pick.
* @returns {tensor} M' The matrix with the selected rows from the indices.
*
* @example
* _.rbind([[1,2,3],[4,5,6],[7,8,9]], [1, 1, 2])
* // → [[4, 5, 6], [4, 5, 6], [7, 8, 9]]
*
*/
// return a copy with sub rows from matrix M
rbind: function(M, indArr) {
indArr = lomath.validInds(indArr, M.length);
if (indArr.length == 0) return [];
return _.map(indArr, function(i) {
return _.cloneDeep(M[i]);
});
},
/**
* Returns a new matrix with the selected columns from a matrix. Same as `cbind()` from `R`.
* Useful for picking columns from a data matrix/tensor with specified header indices.
*
* @category transformation
* @param {tensor} M The original matrix.
* @param {Array} indArr The array of indices specifying the columns of M to pick.
* @returns {tensor} M' The matrix with the selected columns from the indices.
*
* @example
* _.cbind([['a','b','c'],[1,2,3],[-1,-2,-3]], [1, 1, 2])
* // → [ [ 'b', 'b', 'c' ], [ 2, 2, 3 ], [ -2, -2, -3 ] ]
*
* var M = [['a','b','c'],[1,2,3],[-1,-2,-3]]; // using on a dataset
* var titles = M[0];
* _.cbind(M, _.batchIndexOf(titles, ['b', 'b', 'c']))
* // → [ [ 'b', 'b', 'c' ], [ 2, 2, 3 ], [ -2, -2, -3 ] ]
*
*/
// return a copy with sub rows from matrix M
cbind: function(M, indArr) {
indArr = lomath.validInds(indArr, M[0].length)
if (indArr.length == 0) return [];
return _.map(M, function(row) {
return _.map(indArr, function(i) {
return row[i];
});
});
},
/**
* Returns a new matrix with the selected columns from a matrix. Short for `_.cbind(M, _.batchIndexOf())`
* Useful for picking columns from a data matrix by directly specifying the header titles.
*
* @category transformation
* @param {tensor} M The original matrix.
* @param {Array} fields The array of fields of the columns of M to pick.
* @returns {tensor} M' The matrix with the selected columns from the fields.
*
* @example
* var M = [['a','b','c'],[1,2,3],[-1,-2,-3]]; // using on a dataset
* _.cbindByField(M, ['b', 'b', 'c'])
* // → [ [ 'b', 'b', 'c' ], [ 2, 2, 3 ], [ -2, -2, -3 ] ]
*
*/
// Assuming matrix has header, rbind by header fields instead of indices
cbindByField: function(M, fieldArr) {
// assuming header is first row of matrix
var header = M[0],
fieldInds = lomath.batchIndexOf(header, fieldArr);
return lomath.cbind(M, fieldInds);
},
/**
* Makes a tensor rectangular by filling with val (defaulted to 0).
* Mutates the tensor.
*
* @category transformation
* @param {tensor} T The original tensor.
* @returns {tensor} T The mutated tensor that is now rectangular.
*
* @example
* _.rectangularize([
[1, 2, 3],
[4]
])
* // → [[ 1, 2, 3 ], [ 4, 0, 0 ]]
*
* _.rectangularize([
[1, 2, 3],
[4]
], 'a')
* // → [[ 1, 2, 3 ], [ 4, 'a', 'a' ]]
*
*/
// make a tensor rectangular by filling with val, defaulted to 0.
// mutates the tensor
rectangularize: function(T, val) {
var toLen = lomath.maxDeepestLength(T),
stack = [];
stack.push(T);
while (stack.length) {
var curr = stack.pop();
if (lomath.isFlat(curr))
lomath.extend(curr, toLen, val);
else
_.each(curr, function(c) {
stack.push(c);
})
}
return T;
},
/**
* Reshapes an array into a multi-dimensional tensor. Applies `_.chunk` using a dimension array in sequence.
*
* @category transformation
* @param {Array} A The original flat array.
* @param {Array} dimArr The array specifying the dimensions.
* @returns {tensor} T The tensor reshaped from the copied array.
*
* @example
* _.reshape([1, 2, 3, 4, 5, 6], [2, 3])
* // → [[ 1, 2, 3 ], [ 4, 5, 6 ]]
*
* _.reshape([1, 2, 3, 4], [2, 3])
* // → [[ 1, 2, 3 ], [ 4 ]]
*
*/
// use chunk from inside to outside:
reshape: function(arr, dimArr) {
var tensor = arr;
var len = dimArr.length;
while (--len)
tensor = _.chunk(tensor, dimArr[len]);
return tensor;
},
/**
* Flattens a JSON object into depth 1, using an optional delimiter.
*
* @category transformation
* @param {JSON} obj The original JSON object.
* @returns {JSON} flat_obj The flattened (unnested) object.
*
* @example
* formData = {
* 'level1': {
* 'level2': {
* 'level3': 0,
* 'level3b': 1
* },
* 'level2b': {
* 'level3': [2,3,4]
* }
* }
* }
*
* _.flattenJSON(formData)
* // → { 'level1.level2.level3': 0,
* // 'level1.level2.level3b': 1,
* // 'level1.level2b.level3': [ 2, 3, 4 ] }
* // The deepest values are not flattened (not stringified)
*
* _.flattenJSON(formData, '_')
* // → { 'level1_level2_level3': 0,
* // 'level1_level2_level3b': 1,
* // 'level1_level2b_level3': [ 2, 3, 4 ] }
* // The deepest values are not flattened (not stringified)
*
*/
flattenJSON: function(obj, delimiter) {
var delim = delimiter || '.'
var nobj = {}
_.each(obj, function(val, key) {
if (_.isPlainObject(val) && !_.isEmpty(val)) {
var strip = lomath.flattenJSON(val, delim)
_.each(strip, function(v, k) {
nobj[key + delim + k] = v
})
} else if (_.isArray(val) && !_.isEmpty(val)) {
_.each(val, fu