Skip to content

Commit

Permalink
Fix #1834: value Infinity cannot be serialized and deserialized
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong committed May 3, 2020
1 parent ccb1323 commit 28b7a02
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 4 deletions.
7 changes: 7 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# History


# not yet published, version 6.6.5

- Fix #1834: value `Infinity` cannot be serialized and deserialized.
This is solved now with a new `math.replacer` function used as
`JSON.stringify(value, math.replacer)`.


# 2020-04-15, version 6.6.4

- Fix published files containing Windows line endings (CRLF instead of LF).
Expand Down
14 changes: 13 additions & 1 deletion docs/core/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,23 @@ function:

```js
const x = math.complex('2 + 3i')
const str = JSON.stringify(x)
const str = JSON.stringify(x, math.json.replacer)
console.log(str)
// outputs a string '{"mathjs":"Complex","re":2,"im":3}'
```

> IMPORTANT: in most cases works, serialization correctly without
> passing the `math.json.replacer` function as second argument. This is because
> in most cases we can rely on the default behavior of JSON.stringify, which
> uses the `.toJSON` method on classes like `Unit` and `Complex` to correctly
> serialize them. However, there are a few special cases like the
> number `Infinity` which does require the replacer function in order to be
> serialized without losing information: without it, `Infinity` will be
> serialized as `"null"` and cannot be deserialized correctly.
>
> So, it's best to always pass the `math.json.replacer` function to prevent
> weird edge cases.
In order to deserialize a string, containing math.js data types, `JSON.parse`
can be used. In order to recognize the data types of math.js, `JSON.parse` must
be called with the reviver function of math.js:
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/webworkers/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ self.addEventListener('message', function (event) {
// build a response
const response = {
id: request.id,
result: result,
result: self.math.format(result),
err: err
}

Expand Down
5 changes: 3 additions & 2 deletions examples/serialization.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// load math.js (using node.js)
const { complex, reviver, typeOf } = require('..')
const { complex, replacer, reviver, typeOf } = require('..')

// serialize a math.js data type into a JSON string
// the replacer function is needed to correctly stringify a value like Infinity
const x = complex('2+3i')
const str1 = JSON.stringify(x)
const str1 = JSON.stringify(x, replacer)
console.log(str1)
// outputs {"mathjs":"Complex","re":2,"im":3}

Expand Down
1 change: 1 addition & 0 deletions src/factoriesAny.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export { createSimplify } from './function/algebra/simplify'
export { createDerivative } from './function/algebra/derivative'
export { createRationalize } from './function/algebra/rationalize'
export { createReviver } from './json/reviver'
export { createReplacer } from './json/replacer'
export {
createE,
createUppercaseE,
Expand Down
1 change: 1 addition & 0 deletions src/factoriesNumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export { createNumeric } from './function/utils/numeric'

// json
export { createReviver } from './json/reviver'
export { createReplacer } from './json/replacer'

// helper function to create a factory function for a function which only needs typed-function
function createNumberFactory (name, fn) {
Expand Down
30 changes: 30 additions & 0 deletions src/json/replacer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { factory } from '../utils/factory'

const name = 'replacer'
const dependencies = []

export const createReplacer = /* #__PURE__ */ factory(name, dependencies, ({ }) => {
/**
* Stringify data types into their JSON representation.
* Most data types can be serialized using their `.toJSON` method,
* but not all, for example the number `Infinity`. For these cases you have
* to use the replacer. Example usage:
*
* JSON.stringify([2, Infinity], math.replacer)
*
* @param {string} key
* @param {*} value
* @returns {*} Returns the replaced object
*/
return function replacer (key, value) {
// the numeric values Infinitiy, -Infinity, and NaN cannot be serialized to JSON
if (typeof value === 'number' && (!isFinite(value) || isNaN(value))) {
return {
mathjs: 'number',
value: String(value)
}
}

return value
}
})
9 changes: 9 additions & 0 deletions src/type/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,14 @@ export const createNumber = /* #__PURE__ */ factory(name, dependencies, ({ typed
}
})

// reviver function to parse a JSON object like:
//
// {"mathjs":"number","value":"2.3"}
//
// into a number 2.3
number.fromJSON = function (json) {
return parseFloat(json.value)
}

return number
})
24 changes: 24 additions & 0 deletions test/unit-tests/json/replacer.test.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,51 @@
import assert from 'assert'
import math from '../../../src/bundleAny'
const replacer = math.replacer

describe('replacer', function () {
it('should stringify generic JSON', function () {
const data = { foo: [1, 2, 3], bar: null, baz: 'str' }
const json = '{"foo":[1,2,3],"bar":null,"baz":"str"}'
assert.deepStrictEqual(JSON.stringify(data), json)
assert.deepStrictEqual(JSON.stringify(data, replacer), json)
})

it('should stringify a number with special values like Infinity', function () {
assert.deepStrictEqual(JSON.stringify(2.3, replacer), '2.3')
assert.deepStrictEqual(JSON.stringify(Infinity, replacer), '{"mathjs":"number","value":"Infinity"}')
assert.deepStrictEqual(JSON.stringify(-Infinity, replacer), '{"mathjs":"number","value":"-Infinity"}')
assert.deepStrictEqual(JSON.stringify(NaN, replacer), '{"mathjs":"number","value":"NaN"}')
})

it('should stringify a Complex number', function () {
const c = new math.Complex(2, 4)
const json = '{"mathjs":"Complex","re":2,"im":4}'

assert.deepStrictEqual(JSON.stringify(c), json)
assert.deepStrictEqual(JSON.stringify(c, replacer), json)
})

it('should stringify a BigNumber', function () {
const b = new math.BigNumber(5)
const json = '{"mathjs":"BigNumber","value":"5"}'

assert.deepStrictEqual(JSON.stringify(b), json)
assert.deepStrictEqual(JSON.stringify(b, replacer), json)
})

it('should stringify a Fraction', function () {
const b = new math.Fraction(0.375)
const json = '{"mathjs":"Fraction","n":3,"d":8}'

assert.deepStrictEqual(JSON.stringify(b), json)
assert.deepStrictEqual(JSON.stringify(b, replacer), json)
})

it('should stringify a Range', function () {
const r = new math.Range(2, 10)
const json = '{"mathjs":"Range","start":2,"end":10,"step":1}'
assert.deepStrictEqual(JSON.stringify(r), json)
assert.deepStrictEqual(JSON.stringify(r, replacer), json)
})

it('should stringify an Index', function () {
Expand All @@ -42,38 +55,44 @@ describe('replacer', function () {
'{"mathjs":"ImmutableDenseMatrix","data":[2],"size":[1]}' +
']}'
assert.deepStrictEqual(JSON.stringify(i), json)
assert.deepStrictEqual(JSON.stringify(i, replacer), json)
})

it('should stringify a Range (2)', function () {
const r = new math.Range(2, 10, 2)
const json = '{"mathjs":"Range","start":2,"end":10,"step":2}'
assert.deepStrictEqual(JSON.stringify(r), json)
assert.deepStrictEqual(JSON.stringify(r, replacer), json)
})

it('should stringify a Unit', function () {
const u = new math.Unit(5, 'cm')
const json = '{"mathjs":"Unit","value":5,"unit":"cm","fixPrefix":false}'
assert.deepStrictEqual(JSON.stringify(u), json)
assert.deepStrictEqual(JSON.stringify(u, replacer), json)
})

it('should stringify a Matrix, dense', function () {
const m = math.matrix([[1, 2], [3, 4]], 'dense')
const json = '{"mathjs":"DenseMatrix","data":[[1,2],[3,4]],"size":[2,2]}'

assert.deepStrictEqual(JSON.stringify(m), json)
assert.deepStrictEqual(JSON.stringify(m, replacer), json)
})

it('should stringify a Matrix, sparse', function () {
const m = math.matrix([[1, 2], [3, 4]], 'sparse')
const json = '{"mathjs":"SparseMatrix","values":[1,3,2,4],"index":[0,1,0,1],"ptr":[0,2,4],"size":[2,2]}'

assert.deepStrictEqual(JSON.stringify(m), json)
assert.deepStrictEqual(JSON.stringify(m, replacer), json)
})

it('should stringify a ResultSet', function () {
const r = new math.ResultSet([1, 2, new math.Complex(3, 4)])
const json = '{"mathjs":"ResultSet","entries":[1,2,{"mathjs":"Complex","re":3,"im":4}]}'
assert.deepStrictEqual(JSON.stringify(r), json)
assert.deepStrictEqual(JSON.stringify(r, replacer), json)
})

it('should stringify a Matrix containing a complex number, dense', function () {
Expand All @@ -82,6 +101,7 @@ describe('replacer', function () {
const json = '{"mathjs":"DenseMatrix","data":[[1,2],[3,{"mathjs":"Complex","re":4,"im":5}]],"size":[2,2]}'

assert.deepStrictEqual(JSON.stringify(m), json)
assert.deepStrictEqual(JSON.stringify(m, replacer), json)
})

it('should stringify a Matrix containing a complex number, sparse', function () {
Expand All @@ -90,12 +110,14 @@ describe('replacer', function () {
const json = '{"mathjs":"SparseMatrix","values":[1,3,2,{"mathjs":"Complex","re":4,"im":5}],"index":[0,1,0,1],"ptr":[0,2,4],"size":[2,2]}'

assert.deepStrictEqual(JSON.stringify(m), json)
assert.deepStrictEqual(JSON.stringify(m, replacer), json)
})

it('should stringify a Chain', function () {
const c = math.chain(2.3)
const json = '{"mathjs":"Chain","value":2.3}'
assert.deepStrictEqual(JSON.stringify(c), json)
assert.deepStrictEqual(JSON.stringify(c, replacer), json)
})

it('should stringify a node tree', function () {
Expand Down Expand Up @@ -139,11 +161,13 @@ describe('replacer', function () {
}

assert.deepStrictEqual(JSON.parse(JSON.stringify(node)), json)
assert.deepStrictEqual(JSON.parse(JSON.stringify(node, replacer)), json)
})

it('should stringify Help', function () {
const h = new math.Help({ name: 'foo', description: 'bar' })
const json = '{"mathjs":"Help","name":"foo","description":"bar"}'
assert.deepStrictEqual(JSON.parse(JSON.stringify(h)), JSON.parse(json))
assert.deepStrictEqual(JSON.parse(JSON.stringify(h, replacer)), JSON.parse(json))
})
})
8 changes: 8 additions & 0 deletions test/unit-tests/json/reviver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ describe('reviver', function () {
assert.deepStrictEqual(JSON.parse(json, reviver), data)
})

it('should parse a stringified numbers', function () {
assert.strictEqual(JSON.parse('2.3' , reviver), 2.3)
assert.strictEqual(JSON.parse('{"mathjs":"number","value":"2.3"}', reviver), 2.3)
assert.strictEqual(JSON.parse('{"mathjs":"number","value":"Infinity"}', reviver), Infinity)
assert.strictEqual(JSON.parse('{"mathjs":"number","value":"-Infinity"}', reviver), -Infinity)
assert.strictEqual(JSON.parse('{"mathjs":"number","value":"NaN"}', reviver), NaN)
})

it('should parse a stringified complex number', function () {
const json = '{"mathjs":"Complex","re":2,"im":4}'
const c = new math.Complex(2, 4)
Expand Down

0 comments on commit 28b7a02

Please sign in to comment.