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

fix: #2880 not possible to map cube root cbrt #2966

Merged
merged 2 commits into from
Jun 8, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# unpublished changes since 11.8.0

- Fix #2880: not possible to map cube root `cbrt`.
- Fix #2938: make the syntax description of all functions consistent in the
docs (#2941). Thanks @dvd101x.
- Fix #2954: improve the TypeScript definitions the return type of functions
Expand Down
17 changes: 4 additions & 13 deletions src/expression/transform/filter.transform.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { applyCallback } from '../../utils/applyCallback.js'
import { filter, filterRegExp } from '../../utils/array.js'
import { maxArgumentCount } from '../../utils/function.js'
import { compileInlineExpression } from './utils/compileInlineExpression.js'
import { factory } from '../../utils/factory.js'
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { compileInlineExpression } from './utils/compileInlineExpression.js'

const name = 'filter'
const dependencies = ['typed']
Expand Down Expand Up @@ -65,17 +65,8 @@ export const createFilterTransform = /* #__PURE__ */ factory(name, dependencies,
* @private
*/
function _filter (x, callback) {
// figure out what number of arguments the callback function expects
const args = maxArgumentCount(callback)

return filter(x, function (value, index, array) {
// invoke the callback function with the right number of arguments
if (args === 1) {
return callback(value)
} else if (args === 2) {
return callback(value, [index + 1])
} else { // 3 or -1
return callback(value, [index + 1], array)
}
return applyCallback(callback, value, [index + 1], array, 'filter')
})
}
15 changes: 3 additions & 12 deletions src/expression/transform/forEach.transform.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { maxArgumentCount } from '../../utils/function.js'
import { applyCallback } from '../../utils/applyCallback.js'
import { forEach } from '../../utils/array.js'
import { factory } from '../../utils/factory.js'
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { compileInlineExpression } from './utils/compileInlineExpression.js'

const name = 'forEach'
Expand Down Expand Up @@ -38,9 +38,6 @@ export const createForEachTransform = /* #__PURE__ */ factory(name, dependencies
// one-based version of forEach
const _forEach = typed('forEach', {
'Array | Matrix, function': function (array, callback) {
// figure out what number of arguments the callback function expects
const args = maxArgumentCount(callback)

const recurse = function (value, index) {
if (Array.isArray(value)) {
forEach(value, function (child, i) {
Expand All @@ -49,13 +46,7 @@ export const createForEachTransform = /* #__PURE__ */ factory(name, dependencies
})
} else {
// invoke the callback function with the right number of arguments
if (args === 1) {
callback(value)
} else if (args === 2) {
callback(value, index)
} else { // 3 or -1
callback(value, index, array)
}
return applyCallback(callback, value, index, array, 'forEach')
}
}
recurse(array.valueOf(), []) // pass Array
Expand Down
17 changes: 4 additions & 13 deletions src/expression/transform/map.transform.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { maxArgumentCount } from '../../utils/function.js'
import { applyCallback } from '../../utils/applyCallback.js'
import { map } from '../../utils/array.js'
import { factory } from '../../utils/factory.js'
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { compileInlineExpression } from './utils/compileInlineExpression.js'

const name = 'map'
Expand Down Expand Up @@ -50,17 +50,14 @@ export const createMapTransform = /* #__PURE__ */ factory(name, dependencies, ({
}, { isTransformFunction: true })

/**
* Map for a multi dimensional array. One-based indexes
* Map for a multidimensional array. One-based indexes
* @param {Array} array
* @param {function} callback
* @param {Array} orig
* @return {Array}
* @private
*/
function _map (array, callback, orig) {
// figure out what number of arguments the callback function expects
const argsCount = maxArgumentCount(callback)

function recurse (value, index) {
if (Array.isArray(value)) {
return map(value, function (child, i) {
Expand All @@ -69,13 +66,7 @@ function _map (array, callback, orig) {
})
} else {
// invoke the (typed) callback function with the right number of arguments
if (argsCount === 1) {
return callback(value)
} else if (argsCount === 2) {
return callback(value, index)
} else { // 3 or -1
return callback(value, index, orig)
}
return applyCallback(callback, value, index, orig, 'map')
}
}

Expand Down
13 changes: 2 additions & 11 deletions src/function/matrix/filter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { applyCallback } from '../../utils/applyCallback.js'
import { filter, filterRegExp } from '../../utils/array.js'
import { maxArgumentCount } from '../../utils/function.js'
import { factory } from '../../utils/factory.js'

const name = 'filter'
Expand Down Expand Up @@ -58,17 +58,8 @@ export const createFilter = /* #__PURE__ */ factory(name, dependencies, ({ typed
* @private
*/
function _filterCallback (x, callback) {
// figure out what number of arguments the callback function expects
const args = maxArgumentCount(callback)

return filter(x, function (value, index, array) {
// invoke the callback function with the right number of arguments
if (args === 1) {
return callback(value)
} else if (args === 2) {
return callback(value, [index])
} else { // 3 or -1
return callback(value, [index], array)
}
return applyCallback(callback, value, [index], array, 'filter')
})
}
15 changes: 3 additions & 12 deletions src/function/matrix/forEach.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { maxArgumentCount } from '../../utils/function.js'
import { applyCallback } from '../../utils/applyCallback.js'
import { forEach as forEachArray } from '../../utils/array.js'
import { factory } from '../../utils/factory.js'

Expand Down Expand Up @@ -39,15 +39,12 @@ export const createForEach = /* #__PURE__ */ factory(name, dependencies, ({ type
})

/**
* forEach for a multi dimensional array
* forEach for a multidimensional array
* @param {Array} array
* @param {Function} callback
* @private
*/
function _forEach (array, callback) {
// figure out what number of arguments the callback function expects
const args = maxArgumentCount(callback)

const recurse = function (value, index) {
if (Array.isArray(value)) {
forEachArray(value, function (child, i) {
Expand All @@ -56,13 +53,7 @@ function _forEach (array, callback) {
})
} else {
// invoke the callback function with the right number of arguments
if (args === 1) {
callback(value)
} else if (args === 2) {
callback(value, index)
} else { // 3 or -1
callback(value, index, array)
}
return applyCallback(callback, value, index, array, 'forEach')
}
}
recurse(array, [])
Expand Down
46 changes: 6 additions & 40 deletions src/function/matrix/map.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { maxArgumentCount } from '../../utils/function.js'
import { applyCallback } from '../../utils/applyCallback.js'
import { factory } from '../../utils/factory.js'

const name = 'map'
Expand Down Expand Up @@ -26,14 +26,9 @@ export const createMap = /* #__PURE__ */ factory(name, dependencies, ({ typed })
* return value * value
* }) // returns [1, 4, 9]
*
* // The calling convention for the callback can cause subtleties:
* math.map([1, 2, 3], math.format)
* // throws TypeError: map attempted to call 'format(1,[0])' but argument 2 of type Array does not match expected type number or function or Object or string or boolean
* // [This happens because `format` _can_ take a second argument,
* // but its semantics don't match that of the 2nd argument `map` provides]
*
* // To avoid this error, use a function that takes exactly the
* // desired arguments:
* // The callback is normally called with three arguments:
* // callback(value, index, Array)
* // If you want to call with only one argument, use:
* math.map([1, 2, 3], x => math.format(x)) // returns ['1', '2', '3']
*
* See also:
Expand Down Expand Up @@ -63,44 +58,15 @@ export const createMap = /* #__PURE__ */ factory(name, dependencies, ({ typed })
* @private
*/
function _map (array, callback) {
// figure out what number of arguments the callback function expects
const args = maxArgumentCount(callback)

const recurse = function (value, index) {
if (Array.isArray(value)) {
return value.map(function (child, i) {
// we create a copy of the index array and append the new index value
return recurse(child, index.concat(i))
})
} else {
try {
// invoke the callback function with the right number of arguments
if (args === 1) {
return callback(value)
} else if (args === 2) {
return callback(value, index)
} else { // 3 or -1
return callback(value, index, array)
}
} catch (err) {
// But maybe the arguments still weren't right
if (err instanceof TypeError &&
'data' in err &&
err.data.category === 'wrongType') {
let newmsg = `map attempted to call '${err.data.fn}(${value}`
const indexString = JSON.stringify(index)
if (args === 2) {
newmsg += ',' + indexString
} else if (args !== 1) {
newmsg += `,${indexString},${array}`
}
newmsg += `)' but argument ${err.data.index + 1} of type `
newmsg += `${err.data.actual} does not match expected type `
newmsg += err.data.expected.join(' or ')
throw new TypeError(newmsg)
}
throw err
}
// invoke the callback function with the right number of arguments
return applyCallback(callback, value, index, array, 'map')
}
}

Expand Down
67 changes: 67 additions & 0 deletions src/utils/applyCallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import typed from 'typed-function'
import { typeOf as _typeOf } from './is.js'

/**
* Invoke a callback for functions like map and filter with a matching number of arguments
* @param {function} callback
* @param {any} value
* @param {number | number[]} index
* @param {Array} array
* @param {string} mappingFnName The name of the function that is invoking these callbacks, for example "map" or "filter"
* @returns {*}
*/
export function applyCallback (callback, value, index, array, mappingFnName) {
if (typed.isTypedFunction(callback)) {
// invoke the typed callback function with the matching number of arguments only

const args3 = [value, index, array]
const signature3 = typed.resolve(callback, args3)
if (signature3) {
return tryWithArgs(signature3.implementation, args3)
}

const args2 = [value, index]
const signature2 = typed.resolve(callback, args2)
if (signature2) {
return tryWithArgs(signature2.implementation, args2)
}

const args1 = [value]
const signature1 = typed.resolve(callback, args1)
if (signature1) {
return tryWithArgs(signature1.implementation, args1)
}

// fallback (will throw an exception)
return tryWithArgs(callback, args3)
} else {
// A regular JavaScript function
return callback(value, index, array)
}

/**
* @param {function} signature The selected signature of the typed-function
* @param {Array} args List with arguments to apply to the selected signature
* @returns {*} Returns the return value of the invoked signature
* @throws {TypeError} Throws an error when no matching signature was found
*/
function tryWithArgs (signature, args) {
try {
return signature.apply(signature, args)
} catch (err) {
// Enrich the error message so the user understands that it took place inside the callback function
if (err instanceof TypeError && err.data?.category === 'wrongType') {
const argsDesc = []
argsDesc.push(`value: ${_typeOf(value)}`)
if (args.length >= 2) { argsDesc.push(`index: ${_typeOf(index)}`) }
if (args.length >= 3) { argsDesc.push(`array: ${_typeOf(array)}`) }

throw new TypeError(`Function ${mappingFnName} cannot apply callback arguments ` +
`${callback.name}(${argsDesc.join(', ')}) at index ${JSON.stringify(index)}`)
} else {
throw new TypeError(`Function ${mappingFnName} cannot apply callback arguments ` +
`to function ${callback.name}: ${err.message}`)
}
}
}
}
23 changes: 19 additions & 4 deletions test/unit-tests/function/matrix/map.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ describe('map', function () {
assert.deepStrictEqual(output, [3, 4, 5])
})

it('should invoke a typed function with correct number of arguments (4)', function () {
// cbrt has a syntax cbrt(x, allRoots), but it should invoke cbrt(x) here
assert.deepStrictEqual(math.map([1, 8, 27], math.cbrt), [1, 2, 3])
})

it('should invoke a typed function with correct number of arguments (5)', function () {
// cbrt has a syntax cbrt(x, allRoots), but it should invoke cbrt(x) here
assert.deepStrictEqual(math.map([1, 8, 27], math.format), ['1', '8', '27'])
})

it('should throw an error if called with unsupported type', function () {
assert.throws(function () { math.map(1, function () {}) })
assert.throws(function () { math.map('arr', function () {}) })
Expand All @@ -72,10 +82,15 @@ describe('map', function () {
assert.throws(function () { math.map([1, 2, 3]) })
})

it('should throw an error if the callback argument types are incorrect',
function () {
assert.throws(() => math.map([1, 2, 3], math.format), TypeError)
})
it('should throw an error if the callback argument types are incorrect (1)', function () {
assert.throws(() => math.map([1, 2, 3], math.equalText),
/Function map cannot apply callback arguments to function equalText: Unexpected type of argument in function compareText \(expected: string or Array or Matrix, actual: number, index: 0\)/)
})

it('should throw an error if the callback argument types are incorrect (2)', function () {
assert.throws(() => math.map([math.sin, 2, 3], math.sqrt),
/TypeError: Function map cannot apply callback arguments sqrt\(value: function, index: Array, array: Array\) at index \[0]/)
})

it('should operate from the parser', function () {
assert.deepStrictEqual(
Expand Down