Skip to content
Draft
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
9 changes: 8 additions & 1 deletion src/expression/node/SymbolNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '\\`') + '`'
}
}

/**
Expand Down
35 changes: 31 additions & 4 deletions src/expression/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
'"': true,
'\'': true,
';': true,
'`': true,

'+': true,
'-': true,
Expand Down Expand Up @@ -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])
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/type/unit/Unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions test/unit-tests/expression/node/SymbolNode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions test/unit-tests/expression/parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
31 changes: 1 addition & 30 deletions test/unit-tests/type/unit/Unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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 () {
Expand Down