Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated Diff function #1920

Merged
merged 19 commits into from Jul 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/expression/embeddedDocs/embeddedDocs.js
Expand Up @@ -84,6 +84,7 @@ import { subsetDocs } from './function/matrix/subset'
import { squeezeDocs } from './function/matrix/squeeze'
import { sortDocs } from './function/matrix/sort'
import { sizeDocs } from './function/matrix/size'
import { diffDocs } from './function/matrix/diff'
import { reshapeDocs } from './function/matrix/reshape'
import { resizeDocs } from './function/matrix/resize'
import { rangeDocs } from './function/matrix/range'
Expand Down Expand Up @@ -403,6 +404,7 @@ export const embeddedDocs = {
ctranspose: ctransposeDocs,
det: detDocs,
diag: diagDocs,
diff: diffDocs,
dot: dotDocs,
getMatrixDataType: getMatrixDataTypeDocs,
identity: identityDocs,
Expand Down
29 changes: 29 additions & 0 deletions src/expression/embeddedDocs/function/matrix/diff.js
@@ -0,0 +1,29 @@
export const diffDocs = {
name: 'diff',
category: 'Matrix',
syntax: [
'diff(arr)',
'diff(arr, dim)'
],
description: [
'Create a new matrix or array with the difference of the passed matrix or array.',
'Dim parameter is optional and used to indicant the dimension of the array/matrix to apply the difference',
'If no dimension parameter is passed it is assumed as dimension 0',
'Dimension is zero-based in javascript and one-based in the parser',
'Arrays must be \'rectangular\' meaning arrays like [1, 2]',
'If something is passed as a matrix it will be returned as a matrix but other than that all matrices are converted to arrays'
],
examples: [
'diff([1, 2, 4, 7, 0])',
'diff([1, 2, 4, 7, 0], 0)',
'diff(matrix([1, 2, 4, 7, 0]))',
'diff([[1, 2], [3, 4]])',
'diff([[1, 2], [3, 4]], 0)',
'diff([[1, 2], [3, 4]], 1)',
'diff([[1, 2], [3, 4]], bignumber(1))',
'diff(matrix([[1, 2], [3, 4]]), 1)',
'diff([[1, 2], matrix([3, 4])], 1)'

],
seealso: ['subtract', 'partitionSelect']
}
37 changes: 37 additions & 0 deletions src/expression/transform/diff.transform.js
@@ -0,0 +1,37 @@
import { isBigNumber, isCollection, isNumber } from '../../utils/is'
import { factory } from '../../utils/factory'
import { errorTransform } from './utils/errorTransform'
import { createDiff } from '../../function/matrix/diff'

const name = 'diff'
const dependencies = ['typed', 'matrix', 'subtract', 'number', 'bignumber']

export const createDiffTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, subtract, number, bignumber }) => {
const diff = createDiff({ typed, matrix, subtract, number, bignumber })

/**
* Attach a transform function to math.diff
* Adds a property transform containing the transform function.
*
* This transform creates a range which includes the end value
*/
return typed(name, {
'...any': function (args) {
// change last argument dim from one-based to zero-based
if (args.length === 2 && isCollection(args[0])) {
const dim = args[1]
if (isNumber(dim)) {
args[1] = dim - 1
} else if (isBigNumber(dim)) {
args[1] = dim.minus(1)
}
}

try {
return diff.apply(null, args)
} catch (err) {
throw errorTransform(err)
}
}
})
}, { isTransformFunction: true })
2 changes: 2 additions & 0 deletions src/factoriesAny.js
Expand Up @@ -74,6 +74,7 @@ export { createGetMatrixDataType } from './function/matrix/getMatrixDataType'
export { createIdentity } from './function/matrix/identity'
export { createKron } from './function/matrix/kron'
export { createMap } from './function/matrix/map'
export { createDiff } from './function/matrix/diff'
export { createOnes } from './function/matrix/ones'
export { createRange } from './function/matrix/range'
export { createReshape } from './function/matrix/reshape'
Expand Down Expand Up @@ -320,6 +321,7 @@ export { createRangeTransform } from './expression/transform/range.transform'
export { createRowTransform } from './expression/transform/row.transform'
export { createSubsetTransform } from './expression/transform/subset.transform'
export { createConcatTransform } from './expression/transform/concat.transform'
export { createDiffTransform } from './expression/transform/diff.transform'
export { createStdTransform } from './expression/transform/std.transform'
export { createSumTransform } from './expression/transform/sum.transform'
export { createVarianceTransform } from './expression/transform/variance.transform'
170 changes: 170 additions & 0 deletions src/function/matrix/diff.js
@@ -0,0 +1,170 @@
import { factory } from '../../utils/factory'
import { isInteger } from '../../utils/number'
import { isMatrix } from '../../utils/is'

const name = 'diff'
const dependencies = ['typed', 'matrix', 'subtract', 'number', 'bignumber']

export const createDiff = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, subtract, number, bignumber }) => {
/**
* Create a new matrix or array of the difference between elements of the given array
* The optional dim parameter lets you specify the dimension to evaluate the difference of
* If no dimension parameter is passed it is assumed as dimension 0
*
* Dimension is zero-based in javascript and one-based in the parser and can be a number or bignumber
* Arrays must be 'rectangular' meaning arrays like [1, 2]
* If something is passed as a matrix it will be returned as a matrix but other than that all matrices are converted to arrays
*
* Syntax:
*
* math.diff(arr)
* math.diff(arr, dim)
*
* Examples:
*
* const arr = [1, 2, 4, 7, 0]
* math.diff(arr) // returns [1, 2, 3, -7] (no dimension passed so 0 is assumed)
* math.diff(math.matrix(arr)) // returns math.matrix([1, 2, 3, -7])
*
* const arr = [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5], [9, 8, 7, 6, 4]]
* math.diff(arr) // returns [[0, 0, 0, 0, 0], [8, 6, 4, 2, -1]]
* math.diff(arr, 0) // returns [[0, 0, 0, 0, 0], [8, 6, 4, 2, -1]]
* math.diff(arr, 1) // returns [[1, 1, 1, 1], [1, 1, 1, 1], [-1, -1, -1, -2]]
* math.diff(arr, math.bignumber(1)) // returns [[1, 1, 1, 1], [1, 1, 1, 1], [-1, -1, -1, -2]]
*
* math.diff(arr, 2) // throws RangeError as arr is 2 dimensional not 3
* math.diff(arr, -1) // throws RangeError as negative dimensions are not allowed
*
* // These will all produce the same result
* math.diff([[1, 2], [3, 4]])
* math.diff([math.matrix([1, 2]), math.matrix([3, 4])])
* math.diff([[1, 2], math.matrix([3, 4])])
* math.diff([math.matrix([1, 2]), [3, 4]])
* // They do not produce the same result as math.diff(math.matrix([[1, 2], [3, 4]])) as this returns a matrix
*
* See Also:
*
* Subtract
* PartitionSelect
*
* @param {Array | Matrix} arr An array or matrix
* @param {number} dim Dimension
* @return {Array | Matrix} Difference between array elements in given dimension
*/
return typed(name, {
'Array | Matrix': function (arr) { // No dimension specified => assume dimension 0
if (isMatrix(arr)) {
return matrix(_diff(arr.toArray()))
} else {
return _diff(arr)
}
},
'Array | Matrix, number': function (arr, dim) {
if (!isInteger(dim)) throw RangeError('Dimension must be a whole number')
if (isMatrix(arr)) {
return matrix(_recursive(arr.toArray(), dim))
} else {
return _recursive(arr, dim)
}
},
'Array | Matrix, BigNumber': function (arr, dim) {
const maxInt = bignumber(Number.MAX_SAFE_INTEGER)
const minInt = bignumber(Number.MIN_SAFE_INTEGER)
if (dim > maxInt || dim < minInt) throw RangeError('The array does not have more than 2^53 dimensions')
if (!dim.isInt()) throw RangeError('Dimension must be a whole number')
if (isMatrix(arr)) {
return matrix(_recursive(arr.toArray(), number(dim)))
} else {
return _recursive(arr, number(dim))
}
}
})

/**
* Recursively find the correct dimension in the array/matrix
* Then Apply _diff to that dimension
*
* @param {Array} arr The array
* @param {number} dim Dimension
* @return {Array} resulting array
*/
function _recursive (arr, dim) {
if (isMatrix(arr)) {
arr = arr.toArray() // Makes sure arrays like [ matrix([0, 1]), matrix([1, 0]) ] are processed properly
}
if (!Array.isArray(arr)) {
throw RangeError('Array/Matrix does not have that many dimensions')
}
if (dim > 0) {
const result = []
arr.forEach(element => {
result.push(_recursive(element, dim - 1))
})
return result
} else if (dim === 0) {
return _diff(arr)
} else {
throw RangeError('Cannot have negative dimension')
}
}

/**
* Difference between elements in the array
*
* @param {Array} arr An array
* @return {Array} resulting array
*/
function _diff (arr) {
const result = []
const size = arr.length
if (size < 2) {
return arr
}
for (let i = 1; i < size; i++) {
result.push(_ElementDiff(arr[i - 1], arr[i]))
}
return result
}

/**
* Difference between 2 objects
*
* @param {Object} obj1 First object
* @param {Object} obj2 Second object
* @return {Array} resulting array
*/
function _ElementDiff (obj1, obj2) {
// Convert matrices to arrays
if (isMatrix(obj1)) obj1 = obj1.toArray()
if (isMatrix(obj2)) obj2 = obj2.toArray()

const obj1IsArray = Array.isArray(obj1)
const obj2IsArray = Array.isArray(obj2)
if (obj1IsArray && obj2IsArray) {
return _ArrayDiff(obj1, obj2)
}
if (!obj1IsArray && !obj2IsArray) {
return subtract(obj2, obj1) // Difference is (second - first) NOT (first - second)
}
throw TypeError('Cannot calculate difference between 1 array and 1 non-array')
}

/**
* Difference of elements in 2 arrays
*
* @param {Array} arr1 Array 1
* @param {Array} arr2 Array 2
* @return {Array} resulting array
*/
function _ArrayDiff (arr1, arr2) {
if (arr1.length !== arr2.length) {
throw RangeError('Not all sub-arrays have the same length')
}
const result = []
const size = arr1.length
for (let i = 0; i < size; i++) {
result.push(_ElementDiff(arr1[i], arr2[i]))
}
return result
}
})
36 changes: 36 additions & 0 deletions test/unit-tests/expression/transform/diff.transform.test.js
@@ -0,0 +1,36 @@
import assert from 'assert'
import math from '../../../../src/bundleAny'

const diff = math.expression.transform.diff

describe('diff.transform', function () {
it('Should use one-based indexing for dimensions with arrays', function () {
// With Dim = 1
assert.deepStrictEqual(diff([1, 2, 4, 7, 0], 1), [1, 2, 3, -7])
assert.deepStrictEqual(diff([1, 2, 4, 7, 0], math.bignumber(1)), [1, 2, 3, -7])

// Without Dim = 1
assert.deepStrictEqual(diff([1, 2, 4, 7, 0]), [1, 2, 3, -7])
})

it('Should use one-based indexing for dimensions with matrices', function () {
// With Dim = 1
assert.deepStrictEqual(diff(math.matrix([1, 2, 4, 7, 0]), 1), math.matrix([1, 2, 3, -7]))
assert.deepStrictEqual(diff(math.matrix([1, 2, 4, 7, 0]), math.bignumber(1)), math.matrix([1, 2, 3, -7]))

// Without Dim = 1
assert.deepStrictEqual(diff(math.matrix([1, 2, 4, 7, 0])), math.matrix([1, 2, 3, -7]))
})

it('should throw an error if the dimension is below the range for one based indices', function () {
assert.throws(function () { diff(math.matrix([1, 2, 4, 7, 0]), 0) }, Error)
})

it('should throw an error if the dimension is above the range for one based indices', function () {
assert.throws(function () { diff(math.matrix([1, 2, 4, 7, 0]), math.bignumber(0)) }, Error)
})

it('should work with the parser', function () {
assert.deepStrictEqual(math.evaluate('diff([1, 2, 4, 7, 0], 1)'), math.matrix([1, 2, 3, -7]))
})
})