diff --git a/README.md b/README.md index 7cb7919..06d3dec 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ json-bigint [![Build Status](https://secure.travis-ci.org/sidorares/json-bigint.png)](http://travis-ci.org/sidorares/json-bigint) [![NPM](https://nodei.co/npm/json-bigint.png?downloads=true&stars=true)](https://nodei.co/npm/json-bigint/) -JSON.parse/stringify with bigints support. Based on Douglas Crockford [JSON.js](https://github.com/douglascrockford/JSON-js) package and [bignumber.js](https://github.com/MikeMcl/bignumber.js) library. +JSON.parse/stringify with bigints support. Based on Douglas Crockford [JSON.js](https://github.com/douglascrockford/JSON-js) package and [bignumber.js](https://github.com/MikeMcl/bignumber.js) library. + +Native `Bigint` was added to JS recently, so we added an option to leverage it instead of `bignumber.js`. However, the parsing with native `BigInt` is kept an option for backward compability. While most JSON parsers assume numeric values have same precision restrictions as IEEE 754 double, JSON specification _does not_ say anything about number precision. Any floating point number in decimal (optionally scientific) notation is valid JSON value. It's a good idea to serialize values which might fall out of IEEE 754 integer precision as strings in your JSON api, but `{ "value" : 9223372036854775807}`, for example, is still a valid RFC4627 JSON string, and in most JS runtimes the result of `JSON.parse` is this object: `{ value: 9223372036854776000 }` @@ -107,6 +109,61 @@ Default type: object, With option type: string ``` +#### options.useNativeBigInt, boolean, default false +Specifies if parser uses native BigInt instead of bignumber.js + +example: +```js +var JSONbig = require('json-bigint'); +var JSONbigNative = require('json-bigint')({"useNativeBigInt": true}); +var key = '{ "key": 993143214321423154315154321 }'; +console.log(`\n\nStoring the Number as native BigInt, instead of a BigNumber`); +console.log('Input:', key); +var normal = JSONbig.parse(key); +var nativeBigInt = JSONbigNative.parse(key); +console.log('Default type: %s, With option type: %s', typeof normal.key, typeof nativeBigInt.key); + +``` + +Output +``` +Storing the Number as native BigInt, instead of a BigNumber +Input: { "key": 993143214321423154315154321 } +Default type: object, With option type: bigint + +``` + +#### options.alwaysParseAsBig, boolean, default false +Specifies if all numbers should be stored as BigNumber. + +Note that this is a dangerous behavior as it breaks the default functionality of being able to convert back-and-forth without data type changes (as this will convert all Number to be-and-stay BigNumber) + +example: +```js +var JSONbig = require('json-bigint'); +var JSONbigAlways = require('json-bigint')({"alwaysParseAsBig": true}); +var key = '{ "key": 123 }'; // there is no need for BigNumber by default, but we're forcing it +console.log(`\n\nStoring the Number as a BigNumber, instead of a Number`); +console.log('Input:', key); +var normal = JSONbig.parse(key); +var always = JSONbigAlways.parse(key); +console.log('Default type: %s, With option type: %s', typeof normal.key, typeof always.key); + +``` + +Output +``` +Storing the Number as a BigNumber, instead of a Number +Input: { "key": 123 } +Default type: number, With option type: object + +``` + +If you want to force all numbers to be parsed as native `BigInt` +(you probably do! Otherwise any calulations become a real headache): +```js +var JSONbig = require('json-bigint')({"alwaysParseAsBig": true, "useNativeBigInt": true}); +``` ### Links: - [RFC4627: The application/json Media Type for JavaScript Object Notation (JSON)](http://www.ietf.org/rfc/rfc4627.txt) @@ -115,3 +172,20 @@ Default type: object, With option type: string - [What is JavaScript's Max Int? What's the highest Integer value a Number can go to without losing precision?](http://stackoverflow.com/questions/307179/what-is-javascripts-max-int-whats-the-highest-integer-value-a-number-can-go-t) - [Large numbers erroneously rounded in Javascript](http://stackoverflow.com/questions/1379934/large-numbers-erroneously-rounded-in-javascript) +### Note on native BigInt support + +#### Stringifying +Full support out-of-the-box, stringifies BigInts as pure numbers (no quotes, no `n`) + +#### Limitations +- Roundtrip operations + +`s === JSONbig.stringify(JSONbig.parse(s))` but + +`o !== JSONbig.parse(JSONbig.stringify(o))` + +when `o` has a value with something like `123n`. + +`JSONbig` stringify `123n` as `123`, which becomes `number` (aka `123` not `123n`) by default when being reparsed. + +There is currently no consistent way to deal with this issue, so we decided to leave it, handling this specific case is then up to users. \ No newline at end of file diff --git a/lib/parse.js b/lib/parse.js index 2941c81..8229130 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -76,7 +76,9 @@ var json_parse = function (options) { // Default options one can override by passing options to the parse() var _options = { "strict": false, // not being strict means do not generate syntax errors for "duplicate key" - "storeAsString": false // toggles whether the values should be stored as BigNumber (default) or a string + "storeAsString": false, // toggles whether the values should be stored as BigNumber (default) or a string + "alwaysParseAsBig": false, // toggles whether all numbers should be Big + "useNativeBigInt": false // toggles whether to use native BigInt instead of bignumber.js }; @@ -88,6 +90,8 @@ var json_parse = function (options) { if (options.storeAsString === true) { _options.storeAsString = true; } + _options.alwaysParseAsBig = options.alwaysParseAsBig === true ? options.alwaysParseAsBig : false + _options.useNativeBigInt = options.useNativeBigInt === true ? options.useNativeBigInt : false } @@ -174,8 +178,9 @@ var json_parse = function (options) { //if (number > 9007199254740992 || number < -9007199254740992) // Bignumber has stricter check: everything with length > 15 digits disallowed if (string.length > 15) - return (_options.storeAsString === true) ? string : new BigNumber(string); - return number; + return _options.storeAsString ? string : _options.useNativeBigInt ? BigInt(string) : new BigNumber(string); + else + return !_options.alwaysParseAsBig ? number : _options.useNativeBigInt ? BigInt(number) : new BigNumber(number); } }, @@ -191,12 +196,17 @@ var json_parse = function (options) { // When parsing for string values, we must look for " and \ characters. if (ch === '"') { + var startAt = at; while (next()) { if (ch === '"') { + if (at - 1 > startAt) + string += text.substring(startAt, at - 1); next(); return string; } if (ch === '\\') { + if (at - 1 > startAt) + string += text.substring(startAt, at - 1); next(); if (ch === 'u') { uffff = 0; @@ -213,8 +223,7 @@ var json_parse = function (options) { } else { break; } - } else { - string += ch; + startAt = at; } } } diff --git a/lib/stringify.js b/lib/stringify.js index ce0592e..3bd5269 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -249,6 +249,7 @@ var JSON = module.exports; case 'boolean': case 'null': + case 'bigint': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ae66fd5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,167 @@ +{ + "name": "json-bigint", + "version": "0.3.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "assertion-error": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", + "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", + "dev": true + }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, + "chai": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-1.9.2.tgz", + "integrity": "sha1-Pxog+CsLnXQ3V30k1vErGmnTtZA=", + "dev": true, + "requires": { + "assertion-error": "1.0.0", + "deep-eql": "0.1.3" + } + }, + "commander": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + } + }, + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", + "dev": true + }, + "glob": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "dev": true, + "requires": { + "graceful-fs": "~2.0.0", + "inherits": "2", + "minimatch": "~0.2.11" + } + }, + "graceful-fs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "dev": true + }, + "growl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "mkdirp": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "dev": true + }, + "mocha": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.20.1.tgz", + "integrity": "sha1-80ODLZ/gx9l8ZPxwRI9RNt+f7Vs=", + "dev": true, + "requires": { + "commander": "2.0.0", + "debug": "*", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.x", + "jade": "0.26.3", + "mkdirp": "0.3.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 70cdae3..6b0f5d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-bigint", - "version": "0.3.0", + "version": "0.4.0", "description": "JSON.parse with bigints support", "main": "index.js", "scripts": { @@ -26,4 +26,4 @@ "chai": "~1.9.1", "mocha": "~1.20.1" } -} +} \ No newline at end of file diff --git a/test/bigint-parse-test.js b/test/bigint-parse-test.js new file mode 100644 index 0000000..e9755ff --- /dev/null +++ b/test/bigint-parse-test.js @@ -0,0 +1,59 @@ +var mocha = require('mocha') + , assert = require('chai').assert + , expect = require('chai').expect + , BigNumber = require('bignumber.js') + ; + +describe("Testing native BigInt support: parse", function () { + if (typeof (BigInt) === 'undefined') { + console.log('No native BigInt'); + return; + } + var input = '{"big":92233720368547758070,"small":123}'; + + it("Should show JSONbig does support parsing native BigInt", function (done) { + var JSONbig = require('../index')({ + "useNativeBigInt": true + }); + var obj = JSONbig.parse(input); + expect(obj.small, "small int").to.equal(123); + expect(obj.big.toString(), "big int").to.equal("92233720368547758070"); + expect(typeof obj.big, "big int").to.equal('bigint'); + done(); + }); + + it("Should show JSONbig does support forced parsing to native BigInt", function (done) { + var JSONbig = require('../index')({ + "alwaysParseAsBig": true, + "useNativeBigInt": true + }); + var obj = JSONbig.parse(input); + expect(obj.big.toString(), "big int").to.equal("92233720368547758070"); + expect(typeof obj.big, "big int").to.equal('bigint'); + expect(obj.small.toString(), "small int").to.equal("123"); + expect(typeof obj.small, "small int").to.equal('bigint'); + done(); + }); + + + it("Should show JSONbig does support native Bigint parse/stringify roundtrip", function (done) { + var JSONbig = require('../index')({ + "useNativeBigInt": true + }); + var obj = JSONbig.parse(input); + var output = JSONbig.stringify(obj); + expect(output).to.equal(input); + done(); + }); + + it("Should show JSONbig does support native Bigint parse/stringify roundtrip when BigInt is forced", function (done) { + var JSONbig = require('../index')({ + "alwaysParseAsBig": true, + "useNativeBigInt": true + }); + var obj = JSONbig.parse(input); + var output = JSONbig.stringify(obj); + expect(output).to.equal(input); + done(); + }); +}); \ No newline at end of file diff --git a/test/bigint-stringify-test.js b/test/bigint-stringify-test.js new file mode 100644 index 0000000..bef4670 --- /dev/null +++ b/test/bigint-stringify-test.js @@ -0,0 +1,36 @@ +var mocha = require('mocha') + , assert = require('chai').assert + , expect = require('chai').expect + , BigNumber = require('bignumber.js') + ; + +describe("Testing native BigInt support: stringify", function () { + if (typeof (BigInt) === 'undefined') { + console.log('No native BigInt'); + return; + } + it("Should show JSONbig can stringify native BigInt", function (done) { + var JSONbig = require('../index'); + var obj = { + // We cannot use n-literals - otherwise older NodeJS versions fail on this test + big: eval("123456789012345678901234567890n"), + small: -42, + bigConstructed: BigInt(1), + smallConstructed: Number(2), + }; + expect(obj.small.toString(), "string from small int").to.equal("-42"); + expect(obj.big.toString(), "string from big int").to.equal("123456789012345678901234567890"); + expect(typeof obj.big, "typeof big int").to.equal('bigint'); + + var output = JSONbig.stringify(obj); + expect(output).to.equal( + '{' + + '"big":123456789012345678901234567890,' + + '"small":-42,' + + '"bigConstructed":1,' + + '"smallConstructed":2' + + '}' + ); + done(); + }); +}); \ No newline at end of file diff --git a/test/bigint-test.js b/test/bigint-test.js index 5f5e1a3..046aa5b 100644 --- a/test/bigint-test.js +++ b/test/bigint-test.js @@ -17,7 +17,7 @@ describe("Testing bigint support", function(){ done(); }); - it("Should show JSNbig does support bigint parse/stringify roundtrip", function(done){ + it("Should show JSONbig does support bigint parse/stringify roundtrip", function(done){ var JSONbig = require('../index'); var obj = JSONbig.parse(input); expect(obj.small.toString(), "string from small int").to.equal("123");