mirror of
https://github.com/josdejong/mathjs.git
synced 2026-01-18 14:59:29 +00:00
* chore: Rename `apply` to `mapSlices` This renaming conforms with the Julia name for the function formerly called `apply`, and allows it to be called from the expression parser. The previous name `apply` is kept as an alias for `mapSlices`, for backward compatibility. This commit implements an `alias` metadata property for function factories to facilitate the `apply` alias for `mapSlices`. As a separate bonus, this PR corrects several typos in function docs and removes now-passing doc tests from the list of "known failing" doc tests to get down to 45 known failures and 136 total issues in doc tests. (Most of the excess of 136 as compared to 45 are just due to roundoff error/slight inaccuracy of what the documentation claims the result will be and the actual result returned by mathjs. When the 45 are eliminated, a reasonable numeric tolerance can be decided on for doc testing and then the doc tests can be made binding rather than advisory. * refactor: changes per PR review --------- Co-authored-by: Jos de Jong <wjosdejong@gmail.com>
447 lines
12 KiB
JavaScript
447 lines
12 KiB
JavaScript
import assert from 'node:assert'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { approxEqual, approxDeepEqual } from '../../tools/approx.js'
|
|
import { collectDocs } from '../../tools/docgenerator.js'
|
|
import { create, all } from '../../lib/esm/index.js'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const math = create(all)
|
|
const debug = process.argv.includes('--debug-docs')
|
|
|
|
function extractExpectation (comment, optional = false) {
|
|
if (comment === '') return undefined
|
|
const returnsParts = comment.split('eturns').map(s => s.trim())
|
|
if (returnsParts.length > 1) return extractValue(returnsParts[1])
|
|
const outputsParts = comment.split('utputs')
|
|
if (outputsParts.length > 1) {
|
|
let output = outputsParts[1]
|
|
if (output[0] === ':') output = output.substring(1)
|
|
return extractValue(output.trim())
|
|
}
|
|
// None of the usual flags; if we need a value,
|
|
// assume the whole comment is the desired value. Otherwise return undefined
|
|
if (optional) return undefined
|
|
return extractValue(comment)
|
|
}
|
|
|
|
function extractValue (spec) {
|
|
// First check for a leading keyword:
|
|
const words = spec.split(' ')
|
|
// If the last word end in 'i' and the value is not labeled as complex,
|
|
// label it for the comment writer:
|
|
if (words[words.length - 1].substr(-1) === 'i' && words[0] !== 'Complex') {
|
|
words.unshift('Complex')
|
|
}
|
|
// Collapse 'Dense Matrix' into 'DenseMatrix'
|
|
if (words[0] === 'Dense' && words[1] === 'Matrix') {
|
|
words.shift()
|
|
words[0] = 'DenseMatrix'
|
|
}
|
|
const keywords = {
|
|
number: 'Number(_)',
|
|
BigNumber: 'math.bignumber(_)',
|
|
Fraction: 'math.fraction(_)',
|
|
Complex: "math.complex('_')",
|
|
Unit: "math.unit('_')",
|
|
Array: '_',
|
|
Matrix: 'math.matrix(_)',
|
|
DenseMatrix: "math.matrix(_, 'dense')",
|
|
string: '_',
|
|
Node: 'math.parse(_)',
|
|
throws: "'_'"
|
|
}
|
|
if (words[0] in keywords) {
|
|
const template = keywords[words[0]]
|
|
const spot = template.indexOf('_')
|
|
let filler = words.slice(1).join(' ')
|
|
if (words[0] === 'Complex') { // a bit of a hack here :(
|
|
filler = words.slice(1).join('')
|
|
}
|
|
spec = template.substring(0, spot) + filler + template.substr(spot + 1)
|
|
}
|
|
if (spec.substring(0, 7) === 'matrix(') {
|
|
spec = 'math.' + spec // More hackery :(
|
|
}
|
|
let value
|
|
try {
|
|
value = eval(spec) // eslint-disable-line no-eval
|
|
} catch (err) {
|
|
if (spec[0] === '[') {
|
|
// maybe it was an array with mathjs expressions in it
|
|
try {
|
|
value = math.evaluate(spec).toArray()
|
|
} catch (newError) {
|
|
value = spec
|
|
}
|
|
} else if (err instanceof SyntaxError || err instanceof ReferenceError) {
|
|
value = spec
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
if (words[0] === 'Node') { // and even more :(
|
|
delete value.comment
|
|
}
|
|
return value
|
|
}
|
|
|
|
const knownProblems = new Set([
|
|
'setUnion', 'unequal', 'equal', 'deepEqual', 'compareNatural', 'randomInt',
|
|
'random', 'pickRandom', 'kldivergence',
|
|
'parser', 'compile', 're', 'im',
|
|
'subset', 'squeeze', 'rotationMatrix',
|
|
'rotate', 'reshape', 'partitionSelect', 'matrixFromFunction',
|
|
'matrixFromColumns', 'getMatrixDataType', 'eigs', 'diff',
|
|
'nthRoots', 'nthRoot',
|
|
'mod', 'floor', 'fix', 'expm1', 'exp',
|
|
'ceil', 'cbrt', 'add', 'slu',
|
|
'rationalize', 'qr', 'lusolve', 'lup', 'derivative',
|
|
'symbolicEqual', 'schur', 'sylvester', 'freqz', 'round'
|
|
])
|
|
|
|
let issueCount = 0
|
|
|
|
function maybeCheckExpectation (name, expected, expectedFrom, got, gotFrom) {
|
|
if (knownProblems.has(name)) {
|
|
try {
|
|
checkExpectation(expected, got)
|
|
} catch (err) {
|
|
issueCount++
|
|
if (debug) {
|
|
console.log(
|
|
`PLEASE RESOLVE: '${gotFrom}' was supposed to '${expectedFrom}'`)
|
|
console.log(' but', err.toString())
|
|
}
|
|
}
|
|
} else {
|
|
checkExpectation(expected, got)
|
|
}
|
|
}
|
|
|
|
function checkExpectation (want, got) {
|
|
if (Array.isArray(want)) {
|
|
if (!Array.isArray(got)) {
|
|
got = want.valueOf()
|
|
}
|
|
return approxDeepEqual(got, want, 1e-9)
|
|
}
|
|
if (want instanceof math.Unit && got instanceof math.Unit) {
|
|
if (got.fixPrefix !== want.fixPrefix) {
|
|
issueCount++
|
|
if (debug) {
|
|
console.log(' Note: Ignoring different fixPrefix in Unit comparison')
|
|
}
|
|
got.fixPrefix = want.fixPrefix
|
|
}
|
|
return approxDeepEqual(got, want, 1e-9)
|
|
}
|
|
if (want instanceof math.Complex && got instanceof math.Complex) {
|
|
return approxDeepEqual(got, want, 1e-9)
|
|
}
|
|
if (typeof want === 'number' && typeof got === 'number' && want !== got) {
|
|
issueCount++
|
|
if (debug) {
|
|
console.log(` Note: return value ${got} not exactly as expected: ${want}`)
|
|
}
|
|
return approxEqual(got, want, 1e-9)
|
|
}
|
|
if (
|
|
typeof want === 'string' &&
|
|
typeof got === 'string' &&
|
|
want.endsWith('Error') &&
|
|
got.startsWith(want)
|
|
) {
|
|
return true // we obtained the expected error type
|
|
}
|
|
if (typeof want !== 'undefined') {
|
|
return approxDeepEqual(got, want)
|
|
} else {
|
|
// don't check if we don't know what the result is supposed to be
|
|
}
|
|
}
|
|
|
|
const OKundocumented = new Set([
|
|
'apply', // deprecated backwards-compatibility synonym of mapSlices
|
|
'addScalar', 'subtractScalar', 'divideScalar', 'multiplyScalar', 'equalScalar',
|
|
'docs', 'FibonacciHeap',
|
|
'IndexError', 'DimensionError', 'ArgumentsError'
|
|
])
|
|
|
|
const knownUndocumented = new Set([
|
|
'all',
|
|
'isNumber',
|
|
'isComplex',
|
|
'isBigNumber',
|
|
'isBigInt',
|
|
'isFraction',
|
|
'isUnit',
|
|
'isString',
|
|
'isArray',
|
|
'isMatrix',
|
|
'isCollection',
|
|
'isDenseMatrix',
|
|
'isSparseMatrix',
|
|
'isRange',
|
|
'isIndex',
|
|
'isBoolean',
|
|
'isResultSet',
|
|
'isHelp',
|
|
'isFunction',
|
|
'isDate',
|
|
'isRegExp',
|
|
'isObject',
|
|
'isMap',
|
|
'isPartitionedMap',
|
|
'isObjectWrappingMap',
|
|
'isNull',
|
|
'isUndefined',
|
|
'isAccessorNode',
|
|
'isArrayNode',
|
|
'isAssignmentNode',
|
|
'isBlockNode',
|
|
'isConditionalNode',
|
|
'isConstantNode',
|
|
'isFunctionAssignmentNode',
|
|
'isFunctionNode',
|
|
'isIndexNode',
|
|
'isNode',
|
|
'isObjectNode',
|
|
'isOperatorNode',
|
|
'isParenthesisNode',
|
|
'isRangeNode',
|
|
'isRelationalNode',
|
|
'isSymbolNode',
|
|
'isChain',
|
|
'on',
|
|
'off',
|
|
'once',
|
|
'emit',
|
|
'config',
|
|
'expression',
|
|
'import',
|
|
'create',
|
|
'factory',
|
|
'AccessorNode',
|
|
'ArrayNode',
|
|
'AssignmentNode',
|
|
'atomicMass',
|
|
'avogadro',
|
|
'BigNumber',
|
|
'bignumber',
|
|
'BlockNode',
|
|
'bohrMagneton',
|
|
'bohrRadius',
|
|
'boltzmann',
|
|
'boolean',
|
|
'chain',
|
|
'Chain',
|
|
'classicalElectronRadius',
|
|
'complex',
|
|
'Complex',
|
|
'ConditionalNode',
|
|
'conductanceQuantum',
|
|
'ConstantNode',
|
|
'coulomb',
|
|
'createUnit',
|
|
'DenseMatrix',
|
|
'deuteronMass',
|
|
'e',
|
|
'efimovFactor',
|
|
'electricConstant',
|
|
'electronMass',
|
|
'elementaryCharge',
|
|
'false',
|
|
'faraday',
|
|
'fermiCoupling',
|
|
'fineStructure',
|
|
'firstRadiation',
|
|
'fraction',
|
|
'Fraction',
|
|
'FunctionAssignmentNode',
|
|
'FunctionNode',
|
|
'gasConstant',
|
|
'gravitationConstant',
|
|
'gravity',
|
|
'hartreeEnergy',
|
|
'Help',
|
|
'i',
|
|
'ImmutableDenseMatrix',
|
|
'index',
|
|
'Index',
|
|
'IndexNode',
|
|
'Infinity',
|
|
'inverseConductanceQuantum',
|
|
'klitzing',
|
|
'LN10',
|
|
'LN2',
|
|
'LOG10E',
|
|
'LOG2E',
|
|
'loschmidt',
|
|
'magneticConstant',
|
|
'magneticFluxQuantum',
|
|
'matrix',
|
|
'Matrix',
|
|
'molarMass',
|
|
'molarMassC12',
|
|
'molarPlanckConstant',
|
|
'molarVolume',
|
|
'NaN',
|
|
'neutronMass',
|
|
'Node',
|
|
'nuclearMagneton',
|
|
'null',
|
|
'number',
|
|
'bigint',
|
|
'ObjectNode',
|
|
'OperatorNode',
|
|
'ParenthesisNode',
|
|
'parse',
|
|
'Parser',
|
|
'phi',
|
|
'pi',
|
|
'planckCharge',
|
|
'planckConstant',
|
|
'planckLength',
|
|
'planckMass',
|
|
'planckTemperature',
|
|
'planckTime',
|
|
'protonMass',
|
|
'quantumOfCirculation',
|
|
'Range',
|
|
'RangeNode',
|
|
'reducedPlanckConstant',
|
|
'RelationalNode',
|
|
'replacer',
|
|
'ResultSet',
|
|
'reviver',
|
|
'rydberg',
|
|
'SQRT1_2',
|
|
'SQRT2',
|
|
'sackurTetrode',
|
|
'secondRadiation',
|
|
'Spa',
|
|
'sparse',
|
|
'SparseMatrix',
|
|
'speedOfLight',
|
|
'splitUnit',
|
|
'stefanBoltzmann',
|
|
'string',
|
|
'SymbolNode',
|
|
'tau',
|
|
'thomsonCrossSection',
|
|
'true',
|
|
'typed',
|
|
'Unit',
|
|
'unit',
|
|
'E',
|
|
'PI',
|
|
'vacuumImpedance',
|
|
'version',
|
|
'weakMixingAngle',
|
|
'wienDisplacement'
|
|
])
|
|
|
|
describe('Testing examples from (jsdoc) comments', function () {
|
|
const allNames = Object.keys(math)
|
|
const srcPath = path.resolve(__dirname, '../../src') + '/'
|
|
const allDocs = collectDocs(allNames, srcPath)
|
|
|
|
it("should cover all names (but doesn't yet)", function () {
|
|
const documented = new Set(Object.keys(allDocs))
|
|
const badUndocumented = allNames.filter(name => {
|
|
return !(documented.has(name) ||
|
|
OKundocumented.has(name) ||
|
|
knownUndocumented.has(name) ||
|
|
name.substr(0, 1) === '_' ||
|
|
name.substr(-12) === 'Dependencies' ||
|
|
name.substr(0, 6) === 'create'
|
|
)
|
|
})
|
|
assert.deepEqual(badUndocumented, [])
|
|
})
|
|
const byCategory = {}
|
|
for (const fun of Object.values(allDocs)) {
|
|
if (!(fun.category in byCategory)) {
|
|
byCategory[fun.category] = []
|
|
}
|
|
byCategory[fun.category].push(fun.doc)
|
|
}
|
|
for (const category in byCategory) {
|
|
describe('category: ' + category, function () {
|
|
for (const doc of byCategory[category]) {
|
|
it('satisfies ' + doc.name, function () {
|
|
if (debug) {
|
|
console.log(` Testing ${doc.name} ...`) // can remove once no known failures; for now it clarifies "PLEASE RESOLVE"
|
|
}
|
|
const lines = doc.examples
|
|
lines.push('//') // modifies doc but OK for test
|
|
let accumulation = ''
|
|
let expectation
|
|
let expectationFrom = ''
|
|
for (const line of lines) {
|
|
if (line.includes('//')) {
|
|
let parts = line.split('//')
|
|
if (parts[0] && !parts[0].trim()) {
|
|
// Indented comment, unusual in examples
|
|
// assume this is a comment within some code to evaluate
|
|
// i.e., ignore it
|
|
continue
|
|
}
|
|
// Comment specifying a future value or the return of prior code
|
|
parts = parts.map(s => s.trim())
|
|
if (parts[0] !== '') {
|
|
if (accumulation) { accumulation += '\n' }
|
|
accumulation += parts[0]
|
|
}
|
|
if (accumulation !== '' && expectation === undefined) {
|
|
expectationFrom = parts[1]
|
|
expectation = extractExpectation(expectationFrom)
|
|
parts[1] = ''
|
|
}
|
|
if (accumulation && !accumulation.includes('console.log(')) {
|
|
// note: we ignore examples that contain a console.log to keep the output of the tests clean
|
|
let value
|
|
try {
|
|
value = eval(accumulation) // eslint-disable-line no-eval
|
|
} catch (err) {
|
|
value = err.toString()
|
|
}
|
|
maybeCheckExpectation(
|
|
doc.name, expectation, expectationFrom, value, accumulation)
|
|
accumulation = ''
|
|
}
|
|
expectationFrom = parts[1]
|
|
expectation = extractExpectation(expectationFrom, 'requireSignal')
|
|
} else {
|
|
if (line !== '') {
|
|
if (accumulation) { accumulation += '\n' }
|
|
accumulation += line
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
after(function () {
|
|
if (debug) {
|
|
if (knownProblems.size > 0) {
|
|
console.log(`\nWARNING: ${knownProblems.size} known errors converted ` +
|
|
'to PLEASE RESOLVE warnings.')
|
|
}
|
|
if (knownUndocumented.size > 0) {
|
|
console.log(`\nWARNING: ${knownUndocumented.size} symbols in math are known to ` +
|
|
'be undocumented; PLEASE EXTEND the documentation.')
|
|
}
|
|
}
|
|
|
|
if (issueCount > 0) {
|
|
console.log(`\nWARNING: ${issueCount} issues found in the JSDoc comments.` + (!debug
|
|
? ' Run the tests again with "npm run test:node -- --debug-docs" to see detailed information'
|
|
: ''))
|
|
}
|
|
})
|
|
})
|