diff --git a/src/compressor/number.ts b/src/compressor/number.ts index 37b1353..2a584e9 100644 --- a/src/compressor/number.ts +++ b/src/compressor/number.ts @@ -16,7 +16,7 @@ export function compressNumber( ) { let foundRef: string | undefined; - if(obj % 1 === 0) { + if(obj % 1 === 0 && obj.toString() !== obj.toExponential()) { // CHeck if the value is a small integer if(obj < INTEGER_SMALL_EXCLUSIVE_BOUND_UPPER && obj > INTEGER_SMALL_EXCLUSIVE_BOUND_LOWER) { writer.write(INTEGER_SMALL_TOKENS[obj + INTEGER_SMALL_TOKEN_ELEMENT_OFFSET]); diff --git a/src/constants.ts b/src/constants.ts index 149f829..0ce1830 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,7 @@ export const DATE_LOW_PRECISION = 100000; */ export const FLOAT_FULL_PRECISION_DELIMITER = ','; export const FLOAT_REDUCED_PRECISION_DELIMITER = '.'; +export const FLOAT_EXPONENT_DELIMITER = '@'; /** * Data type tokens diff --git a/src/util.ts b/src/util.ts index 9210121..0549c83 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,8 @@ import { - FLOAT_COMPRESSION_PRECISION, FLOAT_FULL_PRECISION_DELIMITER, FLOAT_REDUCED_PRECISION_DELIMITER + FLOAT_COMPRESSION_PRECISION, + FLOAT_EXPONENT_DELIMITER, + FLOAT_FULL_PRECISION_DELIMITER, + FLOAT_REDUCED_PRECISION_DELIMITER } from './constants'; const maxInteger = 2147483648; @@ -56,30 +59,54 @@ export function decompressInteger(compressedInteger: string): number { * Convert float to base62 string for integer and fraction */ export function compressFloat(float: number, fullPrecision: boolean = false): string { + let floatString = float.toString(); + const [mantissa, exponent] = floatString.split('e'); + + if (exponent) { + floatString = mantissa; + } + + let result; if(fullPrecision) { - const [integer, fraction] = float.toString().split('.'); + const [integer, fraction] = floatString.split('.'); const operator = integer === '-0' ? '-' : ''; - return `${operator}${compressInteger(parseInt(integer))}${FLOAT_FULL_PRECISION_DELIMITER}${fraction}`; + result = `${operator}${compressInteger(parseInt(integer))}${FLOAT_FULL_PRECISION_DELIMITER}${fraction}`; } else { + float = exponent ? parseFloat(floatString) : float; const integer = float >= maxInteger ? Math.floor(float) : float <= minInteger ? Math.ceil(float) : float << 0; const fraction = Math.round((FLOAT_COMPRESSION_PRECISION * (float % 1))); - return `${compressInteger(integer)}${FLOAT_REDUCED_PRECISION_DELIMITER}${compressInteger(fraction)}`; + result = `${compressInteger(integer)}${FLOAT_REDUCED_PRECISION_DELIMITER}${compressInteger(fraction)}`; } + + if (exponent) { + result = `${result}${FLOAT_EXPONENT_DELIMITER}${compressInteger(parseInt(exponent))}`; + } + + return result; } /** * Convert base62 integer and fraction to float */ export function decompressFloat(compressedFloat: string): number { - if(compressedFloat.indexOf(FLOAT_FULL_PRECISION_DELIMITER) > -1) { - const [integer, fraction] = compressedFloat.split(FLOAT_FULL_PRECISION_DELIMITER); + const [mantissa, exponent] = compressedFloat.split(FLOAT_EXPONENT_DELIMITER); + + let float; + if(mantissa.includes(FLOAT_FULL_PRECISION_DELIMITER)) { + const [integer, fraction] = mantissa.split(FLOAT_FULL_PRECISION_DELIMITER); const mult = integer === '-0' ? -1 : 1; const uncompressedInteger = decompressInteger(integer); - return mult * parseFloat(uncompressedInteger + '.' + fraction); + float = mult * parseFloat(uncompressedInteger + '.' + fraction); } else { - const [integer, fraction] = compressedFloat.split(FLOAT_REDUCED_PRECISION_DELIMITER); + const [integer, fraction] = mantissa.split(FLOAT_REDUCED_PRECISION_DELIMITER); const uncompressedInteger = decompressInteger(integer); const uncompressedFraction = decompressInteger(fraction); - return uncompressedInteger + uncompressedFraction / FLOAT_COMPRESSION_PRECISION; + float = uncompressedInteger + uncompressedFraction / FLOAT_COMPRESSION_PRECISION; } + + if (exponent) { + float = parseFloat(`${float}e${decompressInteger(exponent)}`); + } + + return float; } \ No newline at end of file diff --git a/test/complex/array.ts b/test/complex/array.ts index b73edaa..10a4d94 100644 --- a/test/complex/array.ts +++ b/test/complex/array.ts @@ -1,7 +1,6 @@ import { ARRAY_START_TOKEN, ARRAY_END_TOKEN, STRING_TOKEN, ARRAY_REPEAT_TOKEN, ARRAY_REPEAT_MANY_TOKEN, OBJECT_START_TOKEN, UNREFERENCED_STRING_TOKEN, OBJECT_END_TOKEN, TEMPLATE_OBJECT_START, TEMPLATE_OBJECT_END, TEMPLATE_OBJECT_FINAL, UNDEFINED_TOKEN, INTEGER_SMALL_TOKENS, INTEGER_SMALL_TOKEN_ELEMENT_OFFSET } from '../../src/constants'; -import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { TestCase } from './util'; +import { TestCase } from '../test-case'; const testCases = [ new TestCase('empty', diff --git a/test/complex/object.ts b/test/complex/object.ts index 2e970e3..1b85d02 100644 --- a/test/complex/object.ts +++ b/test/complex/object.ts @@ -1,13 +1,12 @@ -import { expect } from 'chai'; import { describe, it } from 'mocha'; import { - ARRAY_END_TOKEN, ARRAY_REPEAT_MANY_TOKEN, ARRAY_REPEAT_TOKEN, ARRAY_START_TOKEN, + ARRAY_END_TOKEN, ARRAY_START_TOKEN, INTEGER_SMALL_TOKEN_ELEMENT_OFFSET, INTEGER_SMALL_TOKENS, OBJECT_END_TOKEN, OBJECT_START_TOKEN, - STRING_TOKEN, TEMPLATE_OBJECT_END, TEMPLATE_OBJECT_FINAL, TEMPLATE_OBJECT_START, UNDEFINED_TOKEN, + STRING_TOKEN, TEMPLATE_OBJECT_END, TEMPLATE_OBJECT_FINAL, TEMPLATE_OBJECT_START, UNREFERENCED_STRING_TOKEN } from '../../src/constants'; -import { TestCase } from './util'; +import { TestCase } from '../test-case'; const testCases = [ new TestCase('empty', diff --git a/test/complex/util.ts b/test/complex/util.ts deleted file mode 100644 index c7cbc35..0000000 --- a/test/complex/util.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect } from 'chai'; -import { stringify, parse, parseIncremental } from '../../src'; - -export class TestCase { - constructor(public name: string, private obj: any, private tokens: string[]) {} - - compress() { - const compressed = stringify(this.obj); - expect(compressed).to.equal(this.tokens.join('')); - } - - decompress() { - const decompressed = parse(this.tokens.join('')); - expect(decompressed).to.deep.equal(this.obj); - } - - decompressIncremental() { - const all = this.tokens.join(''); - for(let i = 0; i < all.length; i++) { - for(let j = i; j < all.length; j++) { - const increment = parseIncremental(); - increment(all.slice(0, i)); - increment(all.slice(i, j)); - increment(all.slice(j)); - expect(increment(null)).to.deep.equal(this.obj); - } - } - } -} \ No newline at end of file diff --git a/test/full/array-homogenous.ts b/test/full/array-homogenous.ts index ed24d61..fdd24fa 100644 --- a/test/full/array-homogenous.ts +++ b/test/full/array-homogenous.ts @@ -96,6 +96,14 @@ describe('array-homogenous', function() { testPackUnpackHomogeneousArray(-15.55, ONE, 2, true); }); + it('floatExponentialOne', function() { + testPackUnpackHomogeneousArray(-1.552345411e+123, ONE, 2, true); + }); + + it('floatExponentialMany', function() { + testPackUnpackHomogeneousArray(-1.552345411e+123, MANY, 2, true); + }); + it('stringShortOne', function() { testPackUnpackHomogeneousArray('a', ONE); }); diff --git a/test/full/array-mixed.ts b/test/full/array-mixed.ts index 2276e1f..d22240e 100644 --- a/test/full/array-mixed.ts +++ b/test/full/array-mixed.ts @@ -8,6 +8,7 @@ describe('array-mixed', function() { null, undefined, 1029831209, + -1.1029831209e+123, { x: 123, y: { z: 'asd{f]s' }, z: [234, '{]324asd' ] }, -5, 0, @@ -16,7 +17,7 @@ describe('array-mixed', function() { 'faösodifjaosödfijasödofijasdöofijasodöfijasdoöfijasdoöfijsadoöfijsadfsdjfsadfhiarsl', "foaisdjfoas'dfasd'f'dfs'adfasdf'", 'aosdifjao"oijsdfioJ"sdfoij"', - ]); + ], 0, true); }); it('withRepititions', function() { @@ -28,6 +29,8 @@ describe('array-mixed', function() { { x: 123 }, undefined, 1029831209, + -1.1029831209e-123, + -1.1029831209e-123, 1, -5, { x: 123, y: { z: 'asd{f]s' }, z: [234, '{]324asd' ] }, @@ -48,6 +51,6 @@ describe('array-mixed', function() { 1029831209, { x: 123 }, 1029831209, - ]); + ], 0, true); }); }); diff --git a/test/full/number.ts b/test/full/number.ts new file mode 100644 index 0000000..4c7a113 --- /dev/null +++ b/test/full/number.ts @@ -0,0 +1,77 @@ +import {describe, it} from "mocha"; +import {TestCase} from "../test-case"; +import { + FLOAT_EXPONENT_DELIMITER, + FLOAT_FULL_PRECISION_DELIMITER, + FLOAT_REDUCED_PRECISION_DELIMITER, + FLOAT_TOKEN, + INTEGER_SMALL_TOKEN_ELEMENT_OFFSET, + INTEGER_SMALL_TOKENS, + UNREFERENCED_INTEGER_TOKEN +} from "../../lib/constants"; + + +const testCases = [ + new TestCase('small-integer', + 1, + [INTEGER_SMALL_TOKENS[INTEGER_SMALL_TOKEN_ELEMENT_OFFSET + 1]] + ), + new TestCase('big-integer', + 61, + [UNREFERENCED_INTEGER_TOKEN, 'z'] + ), + new TestCase('float', + 0.123, + [FLOAT_TOKEN, '0', FLOAT_REDUCED_PRECISION_DELIMITER, '1z'] + ), + new TestCase('float-negative', + -0.123, + [FLOAT_TOKEN, '0', FLOAT_REDUCED_PRECISION_DELIMITER, '-1z'] + ), + new TestCase('float-full-precision', + 0.1234567, + [FLOAT_TOKEN, '0', FLOAT_FULL_PRECISION_DELIMITER, '1234567'], + {fullPrecisionFloats: true} + ), + new TestCase('float-full-precision-negative', + -0.1234567, + [FLOAT_TOKEN, '-0', FLOAT_FULL_PRECISION_DELIMITER, '1234567'], + {fullPrecisionFloats: true} + ), + new TestCase('float-exp-positive', + 1.23e+123, + [FLOAT_TOKEN, '1', FLOAT_FULL_PRECISION_DELIMITER, '23', FLOAT_EXPONENT_DELIMITER, '1z'], + {fullPrecisionFloats: true} + ), + new TestCase('float-exp-negative', + 1.23e-123, + [FLOAT_TOKEN, '1', FLOAT_FULL_PRECISION_DELIMITER, '23', FLOAT_EXPONENT_DELIMITER, '-1z'], + {fullPrecisionFloats: true} + ), + new TestCase('float-exp-positive', + 1e+1, + [UNREFERENCED_INTEGER_TOKEN, 'A'], + ), + new TestCase('float-exp-negative', + 1e-1, + [FLOAT_TOKEN, '0', FLOAT_REDUCED_PRECISION_DELIMITER, '1c'], + ), +]; + +describe('Number', function() { + for(let i = 0; i < testCases.length; i++) { + const testCase = testCases[i]; + + describe(testCase.name, function() { + it('compress', function() { + testCase.compress(); + }); + it('decompress', function() { + testCase.decompress(); + }); + it('decompress-incremental', function() { + testCase.decompressIncremental(); + }); + }); + } +}) \ No newline at end of file diff --git a/test/full/object.ts b/test/full/object.ts index 276a074..062ff9e 100644 --- a/test/full/object.ts +++ b/test/full/object.ts @@ -12,7 +12,7 @@ describe('object', function() { }); it('mixed', function() { - testPackUnpack({ x: 1, y: 212301230, z: 'asdfioj{{', 'i': '', 'longkey': true, 'nope': undefined }); + testPackUnpack({ x: 1, y: 0.212301230e-123, z: 'asdfioj{{', 'i': '', 'longkey': true, 'nope': undefined }, 0, true); }); it('nested', function() { @@ -23,7 +23,7 @@ describe('object', function() { nope: undefined, float: 113123.432, nest: { - x: 1, y: 212301230, + x: 1, y: 0.212301230e-123322, float: 0.312, z: 'asdfioj{{', 'i': '', longerkey: true, @@ -35,13 +35,13 @@ describe('object', function() { }, array_nest: [ { - x: 1, y: 212301230, + x: 1, y: 0.212301230e-123322, z: 'asdfioj{{', 'i': '', longerkey: true, nope: undefined }, { - x: 1, y: 212301230, + x: 1, y: 0.212301230e-123322, z: 'asdfioj{{', 'i': '', longerkey: true, nope: undefined diff --git a/test/full/scalar.ts b/test/full/scalar.ts index dd7dde1..29eaf9a 100644 --- a/test/full/scalar.ts +++ b/test/full/scalar.ts @@ -155,6 +155,30 @@ describe('scalar', function() { testPackUnpack(-2147483649.63423, 2, true); }); + it('floatExponentialPositive', function() { + testPackUnpack(1.552345411e+123, 2, true); + }); + + it('floatSmallExponentialPositive', function() { + testPackUnpack(1.2e+10, 2, true); + }); + + it('floatExponentialNegative', function() { + testPackUnpack(1.552345411e-123, 2, true); + }); + + it('floatSmallExponentialNegative', function() { + testPackUnpack(1.2e-10, 2, true); + }); + + it('floatFullPrecisionExponentialPositive', function() { + testPackUnpack(1.552345411e+123, 2, true, {fullPrecisionFloats: true}); + }); + + it('floatFullPrecisionExponentialNegative', function() { + testPackUnpack(1.552345411e-123, 2, true, {fullPrecisionFloats: true}); + }); + it('stringShort', function() { testPackUnpack('a'); }); diff --git a/test/test-case.ts b/test/test-case.ts new file mode 100644 index 0000000..60f0c36 --- /dev/null +++ b/test/test-case.ts @@ -0,0 +1,35 @@ +import {CompressOptions} from "../lib"; +import {parse, parseIncremental, stringify} from "../src"; +import {expect} from "chai"; + +export class TestCase { + constructor( + public name: string, + private obj: any, + private tokens: string[], + private options: CompressOptions = {} + ) {} + + compress() { + const compressed = stringify(this.obj, this.options); + expect(compressed).to.equal(this.tokens.join('')); + } + + decompress() { + const decompressed = parse(this.tokens.join('')); + expect(decompressed).to.deep.equal(this.obj); + } + + decompressIncremental() { + const all = this.tokens.join(''); + for(let i = 0; i < all.length; i++) { + for(let j = i; j < all.length; j++) { + const increment = parseIncremental(); + increment(all.slice(0, i)); + increment(all.slice(i, j)); + increment(all.slice(j)); + expect(increment(null)).to.deep.equal(this.obj); + } + } + } +} \ No newline at end of file