Skip to content

Commit

Permalink
Merge 6691b99 into dc4e0c3
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Feb 25, 2019
2 parents dc4e0c3 + 6691b99 commit 45e4880
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 19 deletions.
69 changes: 51 additions & 18 deletions src/utils/stringify.ts
@@ -1,25 +1,58 @@
/** @hidden */
function stringifyOne<Ts>(value: Ts): string {
if (typeof value === 'string') return JSON.stringify(value);

const defaultRepr: string = `${value}`;
if (/^\[object (Object|Null|Undefined)\]$/.exec(defaultRepr) === null) return defaultRepr;
try {
return JSON.stringify(value, (k, v) => {
if (typeof v === 'bigint') {
return v.toString() + 'n';
} else {
return v;
}
});
} catch (err) {
// ignored: object cannot be stringified using JSON.stringify
function stringifyNumber(numValue: number) {
switch (numValue) {
case 0:
return 1 / numValue === Number.NEGATIVE_INFINITY ? '-0' : '0';
case Number.NEGATIVE_INFINITY:
return 'Number.NEGATIVE_INFINITY';
case Number.POSITIVE_INFINITY:
return 'Number.POSITIVE_INFINITY';
default:
return numValue === numValue ? String(numValue) : 'Number.NaN';
}
return defaultRepr;
}

/** @hidden */
export function stringify<Ts>(value: Ts): string {
if (Array.isArray(value)) return `[${value.map(stringify).join(',')}]`;
return stringifyOne(value);
switch (Object.prototype.toString.call(value)) {
case '[object Array]':
return `[${(value as any).map(stringify).join(',')}]`;
case '[object BigInt]':
return `${value}n`;
case '[object Boolean]':
return typeof value === 'boolean' ? JSON.stringify(value) : `new Boolean(${JSON.stringify(value)})`;
case '[object Map]':
return `new Map(${stringify(Array.from(value as any))})`;
case '[object Null]':
return `null`;
case '[object Number]':
return typeof value === 'number' ? stringifyNumber(value) : `new Number(${stringifyNumber(Number(value))})`;
case '[object Object]':
const defaultRepr: string = `${value}`;
if (defaultRepr !== '[object Object]') return defaultRepr;
try {
return (
'{' +
Object.keys(value)
.map(k => `${JSON.stringify(k)}:${stringify((value as any)[k])}`)
.join(',') +
'}'
);
} catch (err) {
if (err instanceof RangeError) return '[cyclic]';
return '[object Object]';
}
case '[object Set]':
return `new Set(${stringify(Array.from(value as any))})`;
case '[object String]':
return typeof value === 'string' ? JSON.stringify(value) : `new String(${JSON.stringify(value)})`;
case '[object Undefined]':
return `undefined`;
default:
try {
return `${value}`;
} catch {
return Object.prototype.toString.call(value);
}
}
}
71 changes: 70 additions & 1 deletion test/unit/utils/stringify.spec.ts
Expand Up @@ -4,13 +4,82 @@ import { stringify } from '../../../src/utils/stringify';

declare function BigInt(n: number | bigint | string): bigint;

const checkEqual = (a: any, b: any): boolean => {
try {
expect(a).toEqual(b);
return true;
} catch (err) {
return false;
}
};

describe('stringify', () => {
it('Should be able to stringify fc.anything()', () =>
fc.assert(fc.property(fc.anything(), a => typeof stringify(a) === 'string')));
it('Should be able to stringify fc.char16bits() (ie. possibly invalid strings)', () =>
fc.assert(fc.property(fc.char16bits(), a => typeof stringify(a) === 'string')));
if (typeof BigInt !== 'undefined') {
it('Should be able to stringify bigint in object correctly', () =>
fc.assert(fc.property(fc.bigInt(), b => stringify({ b }) === '{"b":"' + b + 'n"}')));
fc.assert(fc.property(fc.bigInt(), b => stringify({ b }) === '{"b":' + b + 'n}')));
}
it('Should be equivalent to JSON.stringify for JSON compliant objects', () =>
fc.assert(
fc.property(
fc.anything({ values: [fc.boolean(), fc.integer(), fc.double(), fc.fullUnicodeString(), fc.constant(null)] }),
obj => {
expect(stringify(obj)).toEqual(JSON.stringify(obj));
}
)
));
it('Should be readable from eval', () =>
fc.assert(
fc.property(fc.anything(), obj => {
expect(eval(`(function() { return ${stringify(obj)}; })()`)).toStrictEqual(obj);
})
));
it('Should stringify differently distinct objects', () =>
fc.assert(
fc.property(fc.anything(), fc.anything(), (a, b) => {
fc.pre(!checkEqual(a, b));
expect(stringify(a)).not.toEqual(stringify(b));
})
));
it('Should be able to stringify cyclic object', () => {
let cyclic: any = { a: 1, b: 2, c: 3 };
cyclic.b = cyclic;
const repr = stringify(cyclic);
expect(repr).toContain('"a"');
expect(repr).toContain('"b"');
expect(repr).toContain('"c"');
expect(repr).toContain('[cyclic]');
});
it('Should be able to stringify values', () => {
expect(stringify(null)).toEqual('null');
expect(stringify(undefined)).toEqual('undefined');
expect(stringify(false)).toEqual('false');
expect(stringify(42)).toEqual('42');
expect(stringify(-0)).toEqual('-0');
expect(stringify(Number.POSITIVE_INFINITY)).toEqual('Number.POSITIVE_INFINITY');
expect(stringify(Number.NEGATIVE_INFINITY)).toEqual('Number.NEGATIVE_INFINITY');
expect(stringify(Number.NaN)).toEqual('Number.NaN');
expect(stringify('Hello')).toEqual('"Hello"');
if (typeof BigInt !== 'undefined') {
expect(stringify(BigInt(42))).toEqual('42n');
}
});
it('Should be able to stringify boxed values', () => {
expect(stringify(new Boolean(false))).toEqual('new Boolean(false)');
expect(stringify(new Number(42))).toEqual('new Number(42)');
expect(stringify(new Number(-0))).toEqual('new Number(-0)');
expect(stringify(new Number(Number.POSITIVE_INFINITY))).toEqual('new Number(Number.POSITIVE_INFINITY)');
expect(stringify(new Number(Number.NEGATIVE_INFINITY))).toEqual('new Number(Number.NEGATIVE_INFINITY)');
expect(stringify(new Number(Number.NaN))).toEqual('new Number(Number.NaN)');
expect(stringify(new String('Hello'))).toEqual('new String("Hello")');
});
it('Should be able to stringify Set', () => {
expect(stringify(new Set([1, 2]))).toEqual('new Set([1,2])');
});
it('Should be able to stringify Map', () => {
expect(stringify(new Map([[1, 2]]))).toEqual('new Map([[1,2]])');
});
});

0 comments on commit 45e4880

Please sign in to comment.