Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong committed Sep 26, 2020
2 parents d82fc39 + f5d843b commit 1d0ce02
Show file tree
Hide file tree
Showing 14 changed files with 249 additions and 5 deletions.
34 changes: 30 additions & 4 deletions docs/expressions/syntax.md
Expand Up @@ -283,6 +283,32 @@ const ans = math.evaluate('0.1 + 0.2') // 0.30000000000000004
math.format(ans, {precision: 14}) // "0.3"
```

Numbers can be expressed as binary, octal, and hexadecimal literals:

```js
math.evaluate('0b11') // 3
math.evaluate('0o77') // 63
math.evaluate('0xff') // 255
```

Non-decimal literals are parsed as 32 bit signed integers:

```js
math.evaluate('0xffffffff') // -1
math.evaluate('0xfffffffff') // SyntaxError: String "0xfffffffff" is out of range
```

Numbers can be formatted as binary, octal, and hex strings using the functions
`bin`, `oct`, and `hex`:

```js
math.evaluate('bin(3)') // '0b11'
math.evaluate('oct(63)') // '0o77'
math.evaluate('hex(255)') // '0xff'
math.evaluate('hex(-1)') // '0xffffffff'
math.evaluate('hex(2.3)') // Error: Value must be an integer
math.evaluate('hex(2^35)') // Error: Value must be in range [-2^31, 2^31-1]
```

### BigNumbers

Expand Down Expand Up @@ -397,7 +423,7 @@ using function `string`.

When setting the value of a character in a string, the character that has been
set is returned. Likewise, when a range of characters is set, that range of
characters is returned.
characters is returned.


```js
Expand Down Expand Up @@ -499,7 +525,7 @@ parser.evaluate('c[end - 1 : -1 : 2]') // Matrix, [8, 7, 6]
## Objects

Objects in math.js work the same as in languages like JavaScript and Python.
An object is enclosed by square brackets `{`, `}`, and contains a set of
An object is enclosed by square brackets `{`, `}`, and contains a set of
comma separated key/value pairs. Keys and values are separated by a colon `:`.
Keys can be a symbol like `prop` or a string like `"prop"`.

Expand All @@ -514,7 +540,7 @@ Objects can contain objects:
math.evaluate('{a: 2, b: {c: 3, d: 4}}') // {a: 2, b: {c: 3, d: 4}}
```

Object properties can be retrieved or replaced using dot notation or bracket
Object properties can be retrieved or replaced using dot notation or bracket
notation. Unlike JavaScript, when setting a property value, the whole object
is returned, not the property value

Expand Down Expand Up @@ -590,7 +616,7 @@ The behavior of implicit multiplication can be summarized by these operator prec

Implicit multiplication is tricky as there can appear to be ambiguity in how an expression will be evaluated. Experience has shown that the above rules most closely match user intent when entering expressions that could be interpreted different ways. It's also possible that these rules could be tweaked in future major releases. Use implicit multiplication carefully. If you don't like the uncertainty introduced by implicit multiplication, use explicit `*` operators and parentheses to ensure your expression is evaluated the way you intend.

Here are some more examples using implicit multiplication:
Here are some more examples using implicit multiplication:

Expression | Evaluated as | Result
--------------- | ------------------- | ------------------
Expand Down
6 changes: 6 additions & 0 deletions src/expression/embeddedDocs/embeddedDocs.js
Expand Up @@ -9,6 +9,9 @@ import { isNegativeDocs } from './function/utils/isNegative'
import { isIntegerDocs } from './function/utils/isInteger'
import { isNaNDocs } from './function/utils/isNaN'
import { formatDocs } from './function/utils/format'
import { binDocs } from './function/utils/bin'
import { octDocs } from './function/utils/oct'
import { hexDocs } from './function/utils/hex'
import { cloneDocs } from './function/utils/clone'
import { toDocs } from './function/units/to'
import { tanhDocs } from './function/trigonometry/tanh'
Expand Down Expand Up @@ -519,6 +522,9 @@ export const embeddedDocs = {
// functions - utils
clone: cloneDocs,
format: formatDocs,
bin: binDocs,
oct: octDocs,
hex: hexDocs,
isNaN: isNaNDocs,
isInteger: isIntegerDocs,
isNegative: isNegativeDocs,
Expand Down
12 changes: 12 additions & 0 deletions src/expression/embeddedDocs/function/utils/bin.js
@@ -0,0 +1,12 @@
export const binDocs = {
name: 'bin',
category: 'Utils',
syntax: [
'bin(value)'
],
description: 'Format a number as binary',
examples: [
'bin(2)'
],
seealso: ['oct', 'hex']
}
12 changes: 12 additions & 0 deletions src/expression/embeddedDocs/function/utils/hex.js
@@ -0,0 +1,12 @@
export const hexDocs = {
name: 'hex',
category: 'Utils',
syntax: [
'hex(value)'
],
description: 'Format a number as hexadecimal',
examples: [
'hex(240)'
],
seealso: ['bin', 'oct']
}
12 changes: 12 additions & 0 deletions src/expression/embeddedDocs/function/utils/oct.js
@@ -0,0 +1,12 @@
export const octDocs = {
name: 'oct',
category: 'Utils',
syntax: [
'oct(value)'
],
description: 'Format a number as octal',
examples: [
'oct(56)'
],
seealso: ['bin', 'hex']
}
25 changes: 25 additions & 0 deletions src/expression/parse.js
Expand Up @@ -321,6 +321,20 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
if (parse.isDigitDot(c1)) {
state.tokenType = TOKENTYPE.NUMBER

// check for binary, octal, or hex
const c2 = currentString(state, 2)
if (c2 === '0b' || c2 === '0o' || c2 === '0x') {
state.token += currentCharacter(state)
next(state)
state.token += currentCharacter(state)
next(state)
while (parse.isHexDigit(currentCharacter(state))) {
state.token += currentCharacter(state)
next(state)
}
return
}

// get number, can have a single dot
if (currentCharacter(state) === '.') {
state.token += currentCharacter(state)
Expand Down Expand Up @@ -522,6 +536,17 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
return (c >= '0' && c <= '9')
}

/**
* checks if the given char c is a hex digit
* @param {string} c a string with one character
* @return {boolean}
*/
parse.isHexDigit = function isHexDigit (c) {
return ((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))
}

/**
* Start of the parse levels below, in order of precedence
* @return {Node} node
Expand Down
3 changes: 3 additions & 0 deletions src/factoriesAny.js
Expand Up @@ -89,6 +89,9 @@ export { createErf } from './function/special/erf'
export { createMode } from './function/statistics/mode'
export { createProd } from './function/statistics/prod'
export { createFormat } from './function/string/format'
export { createBin } from './function/string/bin'
export { createOct } from './function/string/oct'
export { createHex } from './function/string/hex'
export { createPrint } from './function/string/print'
export { createTo } from './function/unit/to'
export { createIsPrime } from './function/utils/isPrime'
Expand Down
29 changes: 29 additions & 0 deletions src/function/string/baseUtils.js
@@ -0,0 +1,29 @@
import { factory } from '../../utils/factory'
import { isInteger } from '../../utils/number'

function baseFormatter (base) {
const prefixes = { 2: '0b', 8: '0o', 16: '0x' }
const prefix = prefixes[base]
return function (n) {
if (n > 2 ** 31 - 1 || n < -(2 ** 31)) {
throw new Error('Value must be in range [-2^31, 2^31-1]')
}
if (!isInteger(n)) {
throw new Error('Value must be an integer')
}
if (n < 0) {
n = n + 2 ** 32
}
return `${prefix}${n.toString(base)}`
}
}

const dependencies = ['typed']

export function createBaseFormatterFactory (name, base) {
return factory(name, dependencies, ({ typed }) => {
return typed(name, {
number: baseFormatter(base)
})
})
}
23 changes: 23 additions & 0 deletions src/function/string/bin.js
@@ -0,0 +1,23 @@
import { createBaseFormatterFactory } from './baseUtils'

/**
* Format a number as binary.
*
* Syntax:
*
* math.bin(value)
*
* Examples:
*
* //the following outputs "0b10"
* math.bin(2)
*
* See also:
*
* oct
* hex
*
* @param {number} value Value to be stringified
* @return {string} The formatted value
*/
export const createBin = createBaseFormatterFactory('bin', 2)
23 changes: 23 additions & 0 deletions src/function/string/hex.js
@@ -0,0 +1,23 @@
import { createBaseFormatterFactory } from './baseUtils'

/**
* Format a number as hexadecimal.
*
* Syntax:
*
* math.hex(value)
*
* Examples:
*
* //the following outputs "0xF0"
* math.hex(240)
*
* See also:
*
* oct
* bin
*
* @param {number} value Value to be stringified
* @return {string} The formatted value
*/
export const createHex = createBaseFormatterFactory('hex', 16)
24 changes: 24 additions & 0 deletions src/function/string/oct.js
@@ -0,0 +1,24 @@
import { createBaseFormatterFactory } from './baseUtils'

/**
* Format a number as octal.
*
* Syntax:
*
* math.oct(value)
*
* Examples:
*
* //the following outputs "0o70"
* math.oct(56)
*
* See also:
*
* bin
* hex
*
* @param {number} value Value to be stringified
* @return {string} The formatted value
*/

export const createOct = createBaseFormatterFactory('oct', 8)
10 changes: 9 additions & 1 deletion src/type/number.js
Expand Up @@ -41,10 +41,18 @@ export const createNumber = /* #__PURE__ */ factory(name, dependencies, ({ typed

string: function (x) {
if (x === 'NaN') return NaN
const num = Number(x)
let num = Number(x)
if (isNaN(num)) {
throw new SyntaxError('String "' + x + '" is no valid number')
}
if (['0b', '0o', '0x'].includes(x.substring(0, 2))) {
if (num > 2 ** 32 - 1) {
throw new SyntaxError(`String "${x}" is out of range`)
}
if (num & 0x80000000) {
num = -1 * ~(num - 1)
}
}
return num
},

Expand Down
24 changes: 24 additions & 0 deletions test/unit-tests/expression/parse.test.js
Expand Up @@ -243,6 +243,21 @@ describe('parse', function () {
assert.strictEqual(parseAndEval('300e-2'), 3)
assert.strictEqual(parseAndEval('300E-2'), 3)
assert.strictEqual(parseAndEval('3.2e2'), 320)

assert.strictEqual(parseAndEval('0b0'), 0)
assert.strictEqual(parseAndEval('0o0'), 0)
assert.strictEqual(parseAndEval('0x0'), 0)
assert.strictEqual(parseAndEval('0b01'), 0b1)
assert.strictEqual(parseAndEval('0o01234567'), 0o01234567)
assert.strictEqual(parseAndEval('0xabcdef'), 0xabcdef)
assert.strictEqual(parseAndEval('0x3456789'), 0x3456789)
assert.strictEqual(parseAndEval('0xABCDEF'), 0xabcdef)
assert.strictEqual(parseAndEval('0xffffffff'), -1)
assert.strictEqual(parseAndEval('0xfffffffe'), -2)
assert.strictEqual(parseAndEval('0o37777777777'), -1)
assert.strictEqual(parseAndEval('0b11111111111111111111111111111111'), -1)
assert.strictEqual(parseAndEval('0b11111111111111111111111111111110'), -2)
assert.strictEqual(parseAndEval('0x7fffffff'), 2 ** 31 - 1)
})

it('should parse a number followed by e', function () {
Expand All @@ -264,6 +279,15 @@ describe('parse', function () {
assert.throws(function () { parseAndEval('-3e-.5') }, /Digit expected, got "."/)

assert.throws(function () { parseAndEval('2e+a') }, /Digit expected, got "a"/)

assert.throws(function () { parseAndEval('0b') }, SyntaxError)
assert.throws(function () { parseAndEval('0o') }, SyntaxError)
assert.throws(function () { parseAndEval('0x') }, SyntaxError)
assert.throws(function () { parseAndEval('0b2') }, SyntaxError)
assert.throws(function () { parseAndEval('0o8') }, SyntaxError)
assert.throws(function () { parseAndEval('0xg') }, SyntaxError)
assert.throws(function () { parseAndEval('0x12.3') }, SyntaxError)
assert.throws(function () { parseAndEval('0x100000000') }, SyntaxError)
})
})

Expand Down
17 changes: 17 additions & 0 deletions test/unit-tests/function/string/format.test.js
Expand Up @@ -172,6 +172,23 @@ describe('format', function () {
})
})

describe('base formatting', function () {
it('should format in binary, octal, and hexadecimal', function () {
assert.strictEqual(math.bin(0b10), '0b10')
assert.strictEqual(math.hex(-1), '0xffffffff')
assert.strictEqual(math.oct(0o70), '0o70')
assert.strictEqual(math.hex(0xf0), '0xf0')
assert.strictEqual(math.hex(0x7fffffff), '0x7fffffff')
})
it('should throw an error for invalid values', function () {
assert.throws(function () { math.bin(1.5) }, Error, 'Value must be an integer')
assert.throws(function () { math.oct(1.5) }, Error, 'Value must be an integer')
assert.throws(function () { math.hex(1.5) }, Error, 'Value must be an integer')
assert.throws(function () { math.hex(-(2 ** 31 + 1)) }, Error, 'Value must be in range [-2^31, 2^31-1]')
assert.throws(function () { math.hex(2 ** 31) }, Error, 'Value must be in range [-2^31, 2^31-1]')
})
})

describe('bignumber', function () {
const bigmath = math.create({ precision: 20 }) // ensure the precision is 20 digits

Expand Down

0 comments on commit 1d0ce02

Please sign in to comment.