From 3720fc5b77189762f150fdcec460767fc62de260 Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Tue, 20 May 2025 11:41:15 +0200 Subject: [PATCH] fix: support non-primitive types --- src/compile.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ src/functions.ts | 21 +++++++++++++-------- src/is.ts | 17 ++++++++++++----- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/compile.test.ts b/src/compile.test.ts index eac84ad..ae8eadd 100644 --- a/src/compile.test.ts +++ b/src/compile.test.ts @@ -207,6 +207,46 @@ describe('customization', () => { expect(go({ a: 2 }, ['aboutEq', ['get', 'a'], 2], options)).toEqual(true) expect(go({ a: 2 }, ['aboutEq', ['get', 'a'], '2'], options)).toEqual(true) }) + + test('should use valueOf() in case of non-primitive types in all operators', () => { + class BigNumber { + public _value: string + + constructor(public value: string) { + this._value = value + } + + valueOf: () => number = () => Number.parseFloat(this.value) + } + + const three = new BigNumber('3') + const six = new BigNumber('6') + const two = new BigNumber('2') + const anotherTwo = new BigNumber('2') + + // Test whether we can use our BigNumber.valueOf() + // @ts-ignore + expect(two + three).toEqual(5) + + expect(go(null, ['eq', three, two] as JSONQuery)).toEqual(false) + expect(go(null, ['eq', two, anotherTwo] as JSONQuery)).toEqual(true) + expect(go(null, ['ne', three, two] as JSONQuery)).toEqual(true) + expect(go(null, ['ne', two, anotherTwo] as JSONQuery)).toEqual(false) + + expect(go(null, ['gt', three, two] as JSONQuery)).toEqual(true) + expect(go(null, ['gte', three, two] as JSONQuery)).toEqual(true) + expect(go(null, ['lt', three, two] as JSONQuery)).toEqual(false) + expect(go(null, ['lte', three, two] as JSONQuery)).toEqual(false) + + expect(go(null, ['add', three, two] as JSONQuery)).toEqual(5) + expect(go(null, ['subtract', three, two] as JSONQuery)).toEqual(1) + expect(go(null, ['multiply', three, two] as JSONQuery)).toEqual(6) + expect(go(null, ['divide', six, two] as JSONQuery)).toEqual(3) + expect(go(null, ['pow', three, two] as JSONQuery)).toEqual(9) + expect(go(null, ['mod', three, two] as JSONQuery)).toEqual(1) + + expect(go([three, two], ['sort'] as JSONQuery)).toEqual([two, three]) + }) }) test('should validate the compile test-suite against its JSON schema', () => { diff --git a/src/functions.ts b/src/functions.ts index 9d235b2..b94c5ac 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -1,5 +1,5 @@ import { compile } from './compile' -import { isArray, isEqual } from './is' +import { getValueOf, isArray, isEqual, typeOf } from './is' import type { Entry, FunctionBuilder, @@ -31,12 +31,12 @@ const sortableTypes = { boolean: 0, number: 1, string: 2 } const otherTypes = 3 const gt = (a: unknown, b: unknown) => - typeof a === typeof b && (typeof a) in sortableTypes ? a > b : false + typeOf(a) === typeOf(b) && typeOf(a) in sortableTypes ? a > b : false const gte = (a: unknown, b: unknown) => isEqual(a, b) || gt(a, b) const lt = (a: unknown, b: unknown) => - typeof a === typeof b && (typeof a) in sortableTypes ? a < b : false + typeOf(a) === typeOf(b) && typeOf(a) in sortableTypes ? a < b : false const lte = (a: unknown, b: unknown) => isEqual(a, b) || lt(a, b) @@ -143,18 +143,23 @@ export const functions: FunctionBuildersMap = { function compare(itemA: unknown, itemB: unknown) { const a = getter(itemA) const b = getter(itemB) + const typeA = typeOf(a) + const typeB = typeOf(b) // Order mixed types - if (typeof a !== typeof b) { - const aIndex = sortableTypes[typeof a] ?? otherTypes - const bIndex = sortableTypes[typeof b] ?? otherTypes + if (typeA !== typeB) { + const aIndex = sortableTypes[typeA] ?? otherTypes + const bIndex = sortableTypes[typeB] ?? otherTypes return aIndex > bIndex ? sign : aIndex < bIndex ? -sign : 0 } // Order two numbers, two strings, or two booleans - if ((typeof a) in sortableTypes) { - return a > b ? sign : a < b ? -sign : 0 + if (typeA in sortableTypes) { + const _a = getValueOf(a) + const _b = getValueOf(b) + + return _a > _b ? sign : _a < _b ? -sign : 0 } // Leave arrays, objects, and unknown types ordered as is diff --git a/src/is.ts b/src/is.ts index 8f6a70f..91d95dd 100644 --- a/src/is.ts +++ b/src/is.ts @@ -1,21 +1,28 @@ export const isArray = (value: unknown): value is T[] => Array.isArray(value) export const isObject = (value: unknown): value is object => - value !== null && typeof value === 'object' && !isArray(value) + value !== null && typeof value === 'object' && value.constructor === Object export const isString = (value: unknown): value is string => typeof value === 'string' // source: https://stackoverflow.com/a/77278013/1262753 export const isEqual = (a: T, b: T): boolean => { - if (a === b) { + const _a = getValueOf(a) + const _b = getValueOf(b) + + if (_a === _b) { return true } - const bothObject = a !== null && b !== null && typeof a === 'object' && typeof b === 'object' + const bothObject = (isObject(_a) || isArray(_a)) && (isObject(_b) || isArray(_b)) return ( bothObject && - Object.keys(a).length === Object.keys(b).length && - Object.entries(a).every(([k, v]) => isEqual(v, b[k as keyof T])) + Object.keys(_a).length === Object.keys(_b).length && + Object.entries(_a).every(([k, v]) => isEqual(v, _b[k])) ) } + +export const typeOf = (value: unknown): string => typeof getValueOf(value) + +export const getValueOf = (value: unknown): unknown => (value ? value.valueOf() : value)