Comparing version 0.3.0 to 0.4.0
# Base93 | ||
***Base93*** is a binary-to-text encoding schemes that represent binary data in an ASCII string format. | ||
**Base93** is a binary-to-text encoding schemes that represent binary data in an ASCII string format. | ||
It is based on Joachim Henke's [Base91](http://base91.sourceforge.net/) with [small modification](https://github.com/ticlo/jsonesc/commit/df4516616b6088ed2c07e8986094b25afb0a45cb#diff-e5daa9e13f272067963c7e68fd9afd3d) to optimize JSON encoding. | ||
It is based on Joachim Henke's [BasE91](http://base91.sourceforge.net/) with [small modification](https://github.com/ticlo/jsonesc/commit/df4516616b6088ed2c07e8986094b25afb0a45cb#diff-e5daa9e13f272067963c7e68fd9afd3d) to optimize JSON encoding. | ||
## Base93 vs Base91 vs Base64 | ||
## Base93 vs BasE91 vs Base64 | ||
@@ -12,6 +12,6 @@ ||Characters|JSON Encoded Size / Binary Size| | ||
|Base93| \ " are not used|1.22551| | ||
|Base91| \ - ' space are not used|1.23897*| | ||
|BasE91| \ - ' space are not used|1.23897*| | ||
|Base64|A-Z a-Z 0-9 + /|1.33333| | ||
\* Base91's original encoding ratio is 1.22974, but in JSON it's a little bit worse since " will be encoded to \". | ||
\* BasE91's original encoding ratio is 1.22974, but in JSON it's a little bit worse since " will be encoded to \\". | ||
@@ -18,0 +18,0 @@ ## Source Code |
@@ -56,2 +56,4 @@ const JsonEsc = require('../dist').default; | ||
benchmark("JsonEsc encode", ()=>JsonEsc.stringify(sample)); | ||
benchmark("JsonEsc sorted encode", ()=>JsonEsc.stringify(sample, undefined, true)); | ||
benchmark("JsonEsc sorted indent encode", ()=>JsonEsc.stringify(sample, 1, true)); | ||
benchmark("MsgPack encode", ()=>MsgPack.encode(sample)); | ||
@@ -58,0 +60,0 @@ benchmark("BSON encode", ()=>bson.serialize(bsonSample)); |
@@ -63,9 +63,9 @@ "use strict"; | ||
static decode(str, offset = 0, length = -1) { | ||
let len = offset + length; | ||
if (length < 0 || len > str.length) { | ||
len = str.length; | ||
let end = offset + length; | ||
if (length < 0 || end > str.length) { | ||
end = str.length; | ||
} | ||
let output = new Array(Math.ceil((len - offset) * 7 / 8)); | ||
let output = new Array(Math.ceil((end - offset) * 7 / 8)); | ||
let dbq = 0, dn = 0, dv = -1, current = 0; | ||
for (let i = offset; i < len; ++i) { | ||
for (let i = offset; i < end; ++i) { | ||
let code = str.charCodeAt(i); | ||
@@ -72,0 +72,0 @@ if (code > 126) |
@@ -13,3 +13,3 @@ "use strict"; | ||
if (type && encoder) { | ||
this._encodeTable.set(type.prototype, encoder); | ||
this._encodeTable.set(type, encoder); | ||
} | ||
@@ -23,3 +23,3 @@ if (decoder) { | ||
let prefixLen = prefix.length; | ||
this._encodeTable.set(type.prototype, (self) => `${prefix}${encoder(self)}`); | ||
this._encodeTable.set(type, (self) => `${prefix}${encoder(self)}`); | ||
this._decodeTable[key] = (str) => decoder(str.substr(prefixLen)); | ||
@@ -67,3 +67,3 @@ } | ||
if (value) { | ||
let encoder = this._encodeTable.get(value.__proto__); | ||
let encoder = this._encodeTable.get(value.constructor); | ||
if (encoder) { | ||
@@ -83,6 +83,91 @@ return encoder(value); | ||
} | ||
stringifySorted(input, space) { | ||
let spacesCached = 0; | ||
let colon = ':'; | ||
let getSpacer = (level) => ''; | ||
if (space >= 0) { | ||
colon = ': '; | ||
let spaces = ['\n']; | ||
getSpacer = (level) => { | ||
if (level < spacesCached) { | ||
return spaces[level]; | ||
} | ||
return spaces[level] = '\n'.padEnd((level - 1) * space + 1); | ||
}; | ||
} | ||
let encodeValue = (value, level) => { | ||
switch (typeof value) { | ||
case 'number': { | ||
if (value !== value) { | ||
return '"\\u001bNaN"'; | ||
} | ||
if (value === Infinity) { | ||
return '"\\u001bInf"'; | ||
} | ||
if (value === -Infinity) { | ||
return '"\\u001b-Inf"'; | ||
} | ||
return value; | ||
} | ||
case 'object': { | ||
if (value) { | ||
switch (value.constructor) { | ||
case Object: { | ||
let keys = Object.keys(value); | ||
keys.sort(); | ||
let items = []; | ||
for (let key of keys) { | ||
let val = value[key]; | ||
if (val !== undefined) { | ||
items.push(`${JSON.stringify(key)}${colon}${encodeValue(val, level + 1)}`); | ||
} | ||
} | ||
if (items.length === 0) { | ||
return '{}'; | ||
} | ||
else { | ||
return `{${getSpacer(level + 1)}${items.join(`,${getSpacer(level + 1)}`)}${getSpacer(level)}}`; | ||
} | ||
} | ||
case Array: { | ||
let items = []; | ||
for (let val of value) { | ||
if (val !== undefined) { | ||
items.push(`${encodeValue(val, level + 1)}`); | ||
} | ||
else { | ||
items.push('null'); | ||
} | ||
} | ||
if (items.length === 0) { | ||
return '[]'; | ||
} | ||
else { | ||
return `[${getSpacer(level + 1)}${items.join(`,${getSpacer(level + 1)}`)}${getSpacer(level)}]`; | ||
} | ||
} | ||
default: { | ||
let encoder = this._encodeTable.get(value.constructor); | ||
if (encoder) { | ||
return JSON.stringify(encoder(value)); | ||
} | ||
} | ||
} | ||
} | ||
return 'null'; | ||
} | ||
default: { | ||
return JSON.stringify(value); | ||
} | ||
} | ||
}; | ||
return encodeValue(input, 0); | ||
} | ||
static parse(str) { | ||
return JsonEsc.defaultEncoder.parse(str); | ||
} | ||
static stringify(input, space) { | ||
static stringify(input, space, sortKeys = false) { | ||
if (sortKeys === true) { | ||
return JsonEsc.defaultEncoder.stringifySorted(input, space); | ||
} | ||
let dateToJSON = Date.prototype.toJSON; | ||
@@ -89,0 +174,0 @@ delete Date.prototype.toJSON; |
{ | ||
"name": "jsonesc", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "Json Escape", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -17,3 +17,3 @@ # Json Escape | ||
new Uint8Array([1,2,3,4]) | ||
] ); | ||
], 1); | ||
// returns: | ||
@@ -31,3 +31,3 @@ [ | ||
JsonEsc allows additional data types to be serialized in JSON, such as binary date (Uint8Array) and Date, while still keeps the verbose nature of JSON.<br> | ||
This makes JsonEsc much easier to debug and trouble shoot than binary formats like BSON and MsgPack | ||
The output string is still a 100% valid JSON, and compatible with any JSON editing/parsing tool or library. This makes JsonEsc much easier to debug and trouble shoot than binary formats like BSON and MsgPack | ||
@@ -39,8 +39,7 @@ ### Performance | ||
#### benchmark result | ||
Benchmark on Chrome 67 with [sample data](https://github.com/ticlo/jsonesc/blob/master/benchmark/sample-data.js) | ||
Benchmark with [sample data](https://github.com/ticlo/jsonesc/blob/master/benchmark/sample-data.js) on Chrome 67, Firefox59, Edge 42 | ||
Chrome 67, Firefox59, Edge 42 <br> | ||
[Time are all in ms, smaller is better](https://github.com/ticlo/jsonesc/blob/master/benchmark/benchmark.js) | ||
||Encode<br>Chrome|Decode<br>Chrome|Encode<br>Firefox|Decode<br>Firefox|Encode<br>Edge|Decode<br>Edge| | ||
||Chrome<br>Encode|Chrome<br>Decode|Firefox<br>Encode|Firefox<br>Decode|Edge<br>Encode|Edge<br>Decode| | ||
|:----:|:----:|:----:|:----:|:----:|:----:|:----:| | ||
@@ -51,2 +50,9 @@ |JsonEsc|***0.1161***|0.1606|***0.1394***|***0.1553***|***0.0899***|***0.0753***| | ||
## API | ||
```typescript | ||
JsonEsc.stringify(inpt:any, space?:number, sortKeys?:boolean = false); | ||
JsonEsc.parse(inpt:string); | ||
``` | ||
## Custom Types | ||
@@ -80,3 +86,3 @@ | ||
But if you still prefer Base64, just use these lines to register Uint8Array with a base64 encoder/decoder | ||
If you still prefer Base64, just use these lines to register Uint8Array with a base64 encoder/decoder | ||
@@ -83,0 +89,0 @@ ```javascript |
@@ -65,10 +65,10 @@ // last character is not same as original base93 | ||
static decode(str: string, offset: number = 0, length: number = -1): number[] { | ||
let len = offset + length; | ||
if (length < 0 || len > str.length) { | ||
len = str.length; | ||
let end = offset + length; | ||
if (length < 0 || end > str.length) { | ||
end = str.length; | ||
} | ||
let output: number[] = new Array(Math.ceil((len - offset) * 7 / 8)); | ||
let output: number[] = new Array(Math.ceil((end - offset) * 7 / 8)); | ||
let dbq = 0, dn = 0, dv = -1, current = 0; | ||
for (let i = offset; i < len; ++i) { | ||
for (let i = offset; i < end; ++i) { | ||
let code = str.charCodeAt(i); | ||
@@ -75,0 +75,0 @@ if (code > 126) continue; |
@@ -13,7 +13,7 @@ import * as Codec from './codec'; | ||
registerRaw(key: string, type: { prototype: object }, | ||
registerRaw(key: string, type: object, | ||
encoder: (self: object) => string, | ||
decoder: (str: string) => object) { | ||
if (type && encoder) { | ||
this._encodeTable.set(type.prototype, encoder); | ||
this._encodeTable.set(type, encoder); | ||
} | ||
@@ -24,3 +24,3 @@ if (decoder) { | ||
} | ||
register(key: string, type: { prototype: object }, | ||
register(key: string, type: object, | ||
encoder: (self: object) => string, | ||
@@ -30,3 +30,3 @@ decoder: (str: string) => object) { | ||
let prefixLen = prefix.length; | ||
this._encodeTable.set(type.prototype, (self: object) => `${prefix}${encoder(self)}`); | ||
this._encodeTable.set(type, (self: object) => `${prefix}${encoder(self)}`); | ||
this._decodeTable[key] = (str: string) => decoder(str.substr(prefixLen)); | ||
@@ -76,3 +76,3 @@ } | ||
if (value) { | ||
let encoder = this._encodeTable.get(value.__proto__); | ||
let encoder = this._encodeTable.get(value.constructor); | ||
if (encoder) { | ||
@@ -91,6 +91,88 @@ return encoder(value); | ||
stringify(input: any, space?: string | number): string { | ||
stringify(input: any, space?: number): string { | ||
return JSON.stringify(input, (key: string, value: any) => this.replacer(key, value), space); | ||
} | ||
stringifySorted(input: any, space?: number): string { | ||
let spacesCached = 0; | ||
let colon = ':'; | ||
let getSpacer = (level: number) => ''; | ||
if (space >= 0) { | ||
colon = ': '; | ||
let spaces: string[] = ['\n']; | ||
getSpacer = (level: number) => { | ||
if (level < spacesCached) { | ||
return spaces[level]; | ||
} | ||
return spaces[level] = '\n'.padEnd((level - 1) * space + 1); | ||
}; | ||
} | ||
let encodeValue = (value: any, level: number) => { | ||
switch (typeof value) { | ||
case 'number': { | ||
if (value !== value) { | ||
return '"\\u001bNaN"'; | ||
} | ||
if (value === Infinity) { | ||
return '"\\u001bInf"'; | ||
} | ||
if (value === -Infinity) { | ||
return '"\\u001b-Inf"'; | ||
} | ||
return value; | ||
} | ||
case 'object': { | ||
if (value) { | ||
switch (value.constructor) { | ||
case Object: { | ||
let keys = Object.keys(value); | ||
keys.sort(); | ||
let items: string[] = []; | ||
for (let key of keys) { | ||
let val = value[key]; | ||
if (val !== undefined) { | ||
items.push(`${JSON.stringify(key)}${colon}${encodeValue(val, level + 1)}`); | ||
} | ||
} | ||
if (items.length === 0) { | ||
return '{}'; | ||
} else { | ||
return `{${getSpacer(level + 1)}${items.join(`,${getSpacer(level + 1)}`)}${getSpacer(level)}}`; | ||
} | ||
} | ||
case Array: { | ||
let items: string[] = []; | ||
for (let val of value) { | ||
if (val !== undefined) { | ||
items.push(`${encodeValue(val, level + 1)}`); | ||
} else { | ||
items.push('null'); | ||
} | ||
} | ||
if (items.length === 0) { | ||
return '[]'; | ||
} else { | ||
return `[${getSpacer(level + 1)}${items.join(`,${getSpacer(level + 1)}`)}${getSpacer(level)}]`; | ||
} | ||
} | ||
default: { | ||
let encoder = this._encodeTable.get(value.constructor); | ||
if (encoder) { | ||
return JSON.stringify(encoder(value)); | ||
} | ||
} | ||
} | ||
} | ||
return 'null'; | ||
} | ||
default: { | ||
return JSON.stringify(value); | ||
} | ||
} | ||
}; | ||
return encodeValue(input, 0); | ||
} | ||
private static defaultEncoder: JsonEsc = new JsonEsc(); | ||
@@ -100,3 +182,6 @@ static parse(str: string): any { | ||
} | ||
static stringify(input: any, space?: string | number): string { | ||
static stringify(input: any, space?: number, sortKeys: boolean = false): string { | ||
if (sortKeys === true) { | ||
return JsonEsc.defaultEncoder.stringifySorted(input, space); | ||
} | ||
let dateToJSON = Date.prototype.toJSON; | ||
@@ -103,0 +188,0 @@ delete Date.prototype.toJSON; |
import JsonEsc from '../dist/index'; | ||
import { assert } from 'chai'; | ||
const nanStr = '"\\u001bNaN"'; | ||
const infStr = '"\\u001bInf"'; | ||
const ninfStr = '"\\u001b-Inf"'; | ||
describe('esc', () => { | ||
it('numbers', () => { | ||
let nanStr = '"\\u001bNaN"'; | ||
let infStr = '"\\u001bInf"'; | ||
let ninfStr = '"\\u001b-Inf"'; | ||
@@ -48,1 +49,19 @@ assert.isNaN(JsonEsc.parse(nanStr), 'decode NaN'); | ||
}); | ||
describe('sorted', () => { | ||
it('basic', () => { | ||
assert.equal(JsonEsc.stringify(NaN, undefined, true), nanStr, 'encode NaN'); | ||
assert.equal(JsonEsc.stringify(1, undefined, true), '1', 'encode 1'); | ||
assert.equal(JsonEsc.stringify("", undefined, true), '""', 'encode string'); | ||
assert.equal(JsonEsc.stringify({}, undefined, true), '{}', 'blank Object'); | ||
assert.equal(JsonEsc.stringify([], undefined, true), '[]', 'blank array'); | ||
assert.equal(JsonEsc.stringify({ c: 1, a: 2, b: 3 }, undefined, true), '{"a":2,"b":3,"c":1}'); | ||
assert.equal(JsonEsc.stringify({ c: 1, a: 2, b: 3 }, 1, true), `{ | ||
"a": 2, | ||
"b": 3, | ||
"c": 1 | ||
}`); | ||
}); | ||
}); |
@@ -7,3 +7,3 @@ { | ||
"lib": [ | ||
"es6" | ||
"es2017" | ||
], | ||
@@ -13,2 +13,3 @@ "alwaysStrict": true, | ||
"sourceMap": true, | ||
"declaration": true, | ||
"outDir": "dist" | ||
@@ -15,0 +16,0 @@ }, |
81731
26
1271
97