Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle exponential floats #16

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/compressor/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 36 additions & 9 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
3 changes: 1 addition & 2 deletions test/complex/array.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
7 changes: 3 additions & 4 deletions test/complex/object.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
29 changes: 0 additions & 29 deletions test/complex/util.ts

This file was deleted.

8 changes: 8 additions & 0 deletions test/full/array-homogenous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Comment on lines +99 to +106
Copy link
Owner

@jgranstrom jgranstrom Mar 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another couple of test cases that would be worth adding is like 1e1 and 1e-1 for numbers that would generally not be expressed with this notation, but still could be.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Added them in test/full/number.ts

it('stringShortOne', function() {
testPackUnpackHomogeneousArray('a', ONE);
});
Expand Down
7 changes: 5 additions & 2 deletions test/full/array-mixed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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() {
Expand All @@ -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' ] },
Expand All @@ -48,6 +51,6 @@ describe('array-mixed', function() {
1029831209,
{ x: 123 },
1029831209,
]);
], 0, true);
});
});
77 changes: 77 additions & 0 deletions test/full/number.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
}
})
8 changes: 4 additions & 4 deletions test/full/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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,
Expand All @@ -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
Expand Down
24 changes: 24 additions & 0 deletions test/full/scalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
35 changes: 35 additions & 0 deletions test/test-case.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}