diff --git a/src/expression/node/SymbolNode.js b/src/expression/node/SymbolNode.js index 36391c751e..3f04801d3a 100644 --- a/src/expression/node/SymbolNode.js +++ b/src/expression/node/SymbolNode.js @@ -124,7 +124,14 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m * @override */ _toString (options) { - return this.name + // This is a conservative approximation of the characters that can be used + // without quoting - we err on the side of quoting. Based on + // isValidLatinOrGreek from parse.js. + if (/^[a-zA-Z_$\u00C0-\u02AF\u0370-\u03FF\u2100-\u214F]*$/.test(this.name)) { + return this.name + } else { + return '`' + this.name.replace(/`/g, '\\`') + '`' + } } /** diff --git a/src/expression/parse.js b/src/expression/parse.js index 7961e0ba0c..90ef28692c 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -131,6 +131,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ '"': true, '\'': true, ';': true, + '`': true, '+': true, '-': true, @@ -1335,10 +1336,14 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ let node, name if (state.tokenType === TOKENTYPE.SYMBOL || - (state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS)) { - name = state.token - - getToken(state) + (state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS) || + state.token === '`') { + if (state.token === '`') { + name = parseQuotedSymbolToken(state) + } else { + name = state.token + getToken(state) + } if (hasOwnProperty(CONSTANTS, name)) { // true, false, null, ... node = new ConstantNode(CONSTANTS[name]) @@ -1356,6 +1361,28 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ return parseString(state) } + function parseQuotedSymbolToken (state) { + let name = '' + + while (currentCharacter(state) !== '' && currentCharacter(state) !== '`') { + if (currentCharacter(state) === '\\') { + // escape character for closing ` - skip it + next(state) + } + + name += currentCharacter(state) + next(state) + } + + getToken(state) + if (state.token !== '`') { + throw createSyntaxError(state, 'End of symbol ` expected') + } + getToken(state) + + return name + } + /** * parse accessors: * - function invocation in round brackets (...), for example sqrt(2) diff --git a/src/type/unit/Unit.js b/src/type/unit/Unit.js index 1d1eb8da77..f443b7401f 100644 --- a/src/type/unit/Unit.js +++ b/src/type/unit/Unit.js @@ -759,12 +759,12 @@ export const createUnitClass = /* #__PURE__ */ factory(name, dependencies, ({ } /** - * Return the numeric value of this unit if it is dimensionless, has a value, and config.predictable == false; or the original unit otherwise + * Return the numeric value of this unit if it is dimensionless, and has a value; or the original unit otherwise * @param {Unit} unit * @returns {number | Fraction | BigNumber | Unit} The numeric value of the unit if conditions are met, or the original unit otherwise */ function getNumericIfUnitless (unit) { - if (unit.equalBase(BASE_UNITS.NONE) && unit.value !== null && !config.predictable) { + if (unit.equalBase(BASE_UNITS.NONE) && unit.value !== null) { return unit.value } else { return unit diff --git a/test/unit-tests/expression/node/SymbolNode.test.js b/test/unit-tests/expression/node/SymbolNode.test.js index 2f3efd546d..62d0f5cb5a 100644 --- a/test/unit-tests/expression/node/SymbolNode.test.js +++ b/test/unit-tests/expression/node/SymbolNode.test.js @@ -123,6 +123,12 @@ describe('SymbolNode', function () { assert.strictEqual(s.toString(), 'foo') }) + it('should quote stringified SymbolNodes if needed', function () { + assert.strictEqual(new SymbolNode('foo bar').toString(), '`foo bar`') + assert.strictEqual(new SymbolNode('foo_bar').toString(), 'foo_bar') + assert.strictEqual(new SymbolNode('foo`').toString(), '`foo\\``') + }) + it('should stringify a SymbolNode with custom toString', function () { // Also checks if the custom functions get passed on to the children const customFunction = function (node, options) { diff --git a/test/unit-tests/expression/parse.test.js b/test/unit-tests/expression/parse.test.js index 2bd7641926..2671f02d25 100644 --- a/test/unit-tests/expression/parse.test.js +++ b/test/unit-tests/expression/parse.test.js @@ -523,6 +523,38 @@ describe('parse', function () { }) }) + describe('symbol', function () { + it('should parse bare symbols', function () { + const scope = { foo: 3, b_a_r: 4 } + assert.deepStrictEqual(parseAndEval('foo*b_a_r', scope), 12) + }) + + it('should parse quoted symbols', function () { + const scope = { foo: 3, b_a_r: 4 } + assert.deepStrictEqual(parseAndEval('`foo`*`b_a_r`', scope), 12) + }) + + it('should parse quoted symbols containing arbitrary characters', function () { + const scope = { '-f o o-': 3, '/b a r/': 4 } + assert.deepStrictEqual(parseAndEval('`-f o o-`*`/b a r/`', scope), 12) + }) + + it('should allow closing backquote to be escaped', function () { + const scope = { 'foo`': 3, 'b`ar': 4 } + assert.deepStrictEqual(parseAndEval('`foo\\``*`b\\`ar`', scope), 12) + }) + + it('should allow quoted symbols to be used as function names', function () { + const scope = { '-f o o-': () => 3, bar: 4 } + assert.deepStrictEqual(parseAndEval('`-f o o-`()*bar', scope), 12) + }) + + it('should allow quoted symbol to be indexed', function () { + const scope = { '-f o o-': [1, 2] } + assert.deepStrictEqual(parseAndEval('`-f o o-`[2]', scope), 2) + }) + }) + describe('unit', function () { it('should parse units', function () { assert.deepStrictEqual(parseAndEval('5cm'), new Unit(5, 'cm')) diff --git a/test/unit-tests/type/unit/Unit.test.js b/test/unit-tests/type/unit/Unit.test.js index db6822e075..d60619a214 100644 --- a/test/unit-tests/type/unit/Unit.test.js +++ b/test/unit-tests/type/unit/Unit.test.js @@ -556,34 +556,7 @@ describe('Unit', function () { assert.strictEqual(unit1.toString(), '1 N') }) - it('should simplify units when they cancel out with {predictable: true}', function () { - const math2 = math.create({ predictable: true }) - const unit1 = new math2.Unit(2, 'Hz') - const unit2 = new math2.Unit(2, 's') - const unit3 = math2.multiply(unit1, unit2) - assert.strictEqual(unit3.toString(), '4') - assert.strictEqual(unit3.simplify().units.length, 0) - - const nounit = math2.evaluate('40m * 40N / (40J)') - assert.strictEqual(nounit.toString(), '40') - assert.strictEqual(nounit.simplify().units.length, 0) - - const a = math2.unit('3 s^-1') - const b = math2.unit('4 s') - assert.strictEqual(math2.multiply(a, b).type, 'Unit') - - const c = math2.unit('8.314 J / mol / K') - assert.strictEqual(math2.pow(c, 0).type, 'Unit') - - const d = math2.unit('60 minute') - const e = math2.unit('1 s') - assert.strictEqual(math2.divide(d, e).type, 'Unit') - }) - - it('should convert units to appropriate _numeric_ values when they cancel out with {predictable: false}', function () { - const origConfig = math.config() - math.config({ predictable: false }) - + it('should convert units to appropriate _numeric_ values when they cancel out', function () { assert.strictEqual(typeof (math.evaluate('40 m * 40 N / (40 J)')), 'number') let bigunit = math.unit(math.bignumber(1), 'km') @@ -608,8 +581,6 @@ describe('Unit', function () { const d = math.unit('60 minute') const e = math.unit('1 s') assert.strictEqual(typeof (math.divide(d, e)), 'number') - - math.config(origConfig) }) it('should simplify units according to chosen unit system', function () {