From 28b7a02f01c6f4d4512c7f967ff9671e732d1220 Mon Sep 17 00:00:00 2001 From: josdejong Date: Sun, 3 May 2020 15:41:03 +0200 Subject: [PATCH] Fix #1834: value `Infinity` cannot be serialized and deserialized --- HISTORY.md | 7 +++++++ docs/core/serialization.md | 14 ++++++++++++- examples/browser/webworkers/worker.js | 2 +- examples/serialization.js | 5 +++-- src/factoriesAny.js | 1 + src/factoriesNumber.js | 1 + src/json/replacer.js | 30 +++++++++++++++++++++++++++ src/type/number.js | 9 ++++++++ test/unit-tests/json/replacer.test.js | 24 +++++++++++++++++++++ test/unit-tests/json/reviver.test.js | 8 +++++++ 10 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 src/json/replacer.js diff --git a/HISTORY.md b/HISTORY.md index 3790253bd2..7b61349365 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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). diff --git a/docs/core/serialization.md b/docs/core/serialization.md index 81751c1701..368626e1d7 100644 --- a/docs/core/serialization.md +++ b/docs/core/serialization.md @@ -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: diff --git a/examples/browser/webworkers/worker.js b/examples/browser/webworkers/worker.js index 708268240d..4abd330c20 100644 --- a/examples/browser/webworkers/worker.js +++ b/examples/browser/webworkers/worker.js @@ -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 } diff --git a/examples/serialization.js b/examples/serialization.js index f2c2e43e3f..a7a52bbd67 100644 --- a/examples/serialization.js +++ b/examples/serialization.js @@ -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} diff --git a/src/factoriesAny.js b/src/factoriesAny.js index b144bbc976..3ffe7ed139 100644 --- a/src/factoriesAny.js +++ b/src/factoriesAny.js @@ -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, diff --git a/src/factoriesNumber.js b/src/factoriesNumber.js index 6bda89954b..8430603a83 100644 --- a/src/factoriesNumber.js +++ b/src/factoriesNumber.js @@ -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) { diff --git a/src/json/replacer.js b/src/json/replacer.js new file mode 100644 index 0000000000..caa9728ffe --- /dev/null +++ b/src/json/replacer.js @@ -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 + } +}) diff --git a/src/type/number.js b/src/type/number.js index e3d02f09de..1d08482742 100644 --- a/src/type/number.js +++ b/src/type/number.js @@ -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 }) diff --git a/test/unit-tests/json/replacer.test.js b/test/unit-tests/json/replacer.test.js index 430e9f29fd..8a96589e8a 100644 --- a/test/unit-tests/json/replacer.test.js +++ b/test/unit-tests/json/replacer.test.js @@ -1,11 +1,20 @@ 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 () { @@ -13,6 +22,7 @@ describe('replacer', function () { 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 () { @@ -20,6 +30,7 @@ describe('replacer', function () { 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 () { @@ -27,12 +38,14 @@ describe('replacer', function () { 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 () { @@ -42,18 +55,21 @@ 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 () { @@ -61,6 +77,7 @@ describe('replacer', function () { 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 () { @@ -68,12 +85,14 @@ describe('replacer', function () { 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 () { @@ -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 () { @@ -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 () { @@ -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)) }) }) diff --git a/test/unit-tests/json/reviver.test.js b/test/unit-tests/json/reviver.test.js index 0f0d6d9816..5c4b8becf6 100644 --- a/test/unit-tests/json/reviver.test.js +++ b/test/unit-tests/json/reviver.test.js @@ -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)