Skip to content

Commit

Permalink
feat(utils): improve toStringKey
Browse files Browse the repository at this point in the history
  • Loading branch information
artalar committed Oct 8, 2024
1 parent eedb14d commit 9f9a7e8
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 17 deletions.
24 changes: 21 additions & 3 deletions packages/utils/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,28 @@ test('toStringKey', () => {

const obj: Record<string, any> = {}
obj.obj = obj
obj.one = { two: { CLASS } }
obj.list = [undefined, false, true, 0, '0', Symbol('0'), Symbol.for('0'), 0n, () => 0, new Map([['key', 'val']])]
obj.class = { CLASS, class: { CLASS } }
obj.list = [
Object.create(null),
undefined,
false,
true,
0,
'0',
Symbol('0'),
Symbol.for('0'),
0n,
() => 0,
new Map([['key', 'val']]),
Object.assign(new Date(0), {
toString(this: Date) {
return this.toISOString()
},
}),
/regexp/,
]

const target = `[object Object][object Array][string]list[object Array][number]1[number]2[number]3[object Map][object Array][string]key[string]val[object Array][string]obj[object Object#1][object Array][string]one[object Object][object Array][string]two[object Object][object Array][string]CLASS[object AbortController#12]`
const target = `[reatom·Object#1][reatom·Array#2][reatom·string]class[reatom·Object#3][reatom·Array#4][reatom·string]class[reatom·Object#5][reatom·Array#6][reatom·string]CLASS[reatom·AbortController#7][reatom·Array#8][reatom·string]CLASS[reatom·AbortController#7][reatom·Array#9][reatom·string]list[reatom·Array#10][reatom·Object#11][reatom·undefined]undefined[reatom·boolean]false[reatom·boolean]true[reatom·number]0[reatom·string]0[reatom·Symbol]0[reatom·Symbol]0[reatom·bigint]0[reatom·Function#12][reatom·Map#13][reatom·Array#14][reatom·string]key[reatom·string]val[reatom·object]1970-01-01T00:00:00.000Z[reatom·object]/regexp/[reatom·Array#15][reatom·string]obj[reatom·Object#1]`

let i = 1
const unmock = mockRandom(() => i++)
Expand Down
35 changes: 21 additions & 14 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,38 +181,45 @@ export const nonNullable = <T>(value: T, message?: string): NonNullable<T> => {
}

const { toString } = Object.prototype
const { toString: toStringArray } = []
const visited = new WeakMap<{}, string>()
let toStringKeyIdx = 0
/** Stringify any kind of data with some sort of stability.
* Support: an object keys sorting, `Map`, `Set`, circular references, custom classes, functions and symbols.
* The optional `immutable` could memoize the result for complex objects if you think it will never change
*/
export const toStringKey = (thing: any, immutable = true): string => {
var tag = typeof thing
var isNominal = tag === 'function' || tag === 'symbol'

if (!isNominal && (tag !== 'object' || thing === null || thing instanceof Date || thing instanceof RegExp)) {
if (tag === 'symbol') return `[reatom Symbol]${thing.description || 'symbol'}`

if (tag !== 'function' && (tag !== 'object' || thing === null || thing instanceof Date || thing instanceof RegExp)) {
return `[reatom ${tag}]` + thing
}

if (visited.has(thing)) return visited.get(thing)!

var name = Reflect.getPrototypeOf(thing)?.constructor.name || toString.call(thing).slice(8, -1)
// get a unique prefix for each type to separate same array / map
var result = thing.toString()
var unique = `[reatom ${result.slice(7, -1)}#${++toStringKeyIdx}]`
// thing could be a circular or not stringifiable object from a userspace
try {
visited.set(thing, unique)
} catch {
return `[reatom ${result}]`
var result = `[reatom ${name}#${random()}]`
if (tag === 'function') {
visited.set(thing, (result += thing.name))
return result
}

if (isNominal || (thing.constructor !== Object && Symbol.iterator in thing === false)) {
return unique
visited.set(thing, result)

var proto = Reflect.getPrototypeOf(thing)
if (
proto &&
Reflect.getPrototypeOf(proto) &&
thing.toString !== toStringArray &&
Symbol.iterator in thing === false
) {
return result
}

for (let item of Symbol.iterator in thing ? thing : Object.entries(thing).sort(([a], [b]) => a.localeCompare(b)))
result += toStringKey(item, immutable)
var iterator = Symbol.iterator in thing ? thing : Object.entries(thing).sort(([a], [b]) => a.localeCompare(b))
for (let item of iterator) result += toStringKey(item, immutable)

if (immutable) {
visited.set(thing, result)
Expand Down

0 comments on commit 9f9a7e8

Please sign in to comment.