Skip to content

Commit

Permalink
feat: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
haoziqaq committed Nov 3, 2024
1 parent 178facd commit 81e06c8
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 14 deletions.
16 changes: 8 additions & 8 deletions src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
isRegExp,
isTypedArray,
isWeakMap,
isError,
isDOMException,
} from './general'

export function mergeWith<TObject extends Record<string, any>, TSource extends Record<string, any>>(
Expand Down Expand Up @@ -111,7 +113,7 @@ export function cloneDeepWith<T>(value: T, fn: (value: any) => any): T {
return newConstructor(value, value.valueOf())
}

if (isWeakMap(value) || isWeakSet(value)) {
if (isWeakMap(value) || isWeakSet(value) || isError(value) || isDOMException(value)) {
return {}
}

Expand Down Expand Up @@ -139,15 +141,13 @@ export function cloneDeepWith<T>(value: T, fn: (value: any) => any): T {
}

if (isPlainObject(value)) {
const result: Record<string, any> = Object.create(Reflect.getPrototypeOf(value))
const result: Record<string | symbol, any> = Object.create(Reflect.getPrototypeOf(value))
cache.set(value, result)

// eslint-disable-next-line no-restricted-syntax
for (const key in value) {
if (hasOwn(value, key)) {
result[key] = baseCloneDeep(value[key], cache)
}
}
const ownKeys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)]
ownKeys.forEach((key) => {
result[key] = baseCloneDeep(value[key as keyof typeof value], cache)
})

return result
}
Expand Down
137 changes: 136 additions & 1 deletion src/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export function isSymbol(val: unknown): val is symbol {
return typeof val === 'symbol'
}

export function isError(val: unknown): val is Error {
return toRawType(val) === 'Error'
}

export function isDOMException(val: unknown): val is DOMException {
return toRawType(val) === 'DOMException'
}

export function isNumeric(val: unknown): val is number | string {
return isNumber(val) || (isString(val) && /^[-+]?\d+$/.test(val))
}
Expand Down Expand Up @@ -64,7 +72,20 @@ export function isArrayBuffer(val: unknown): val is ArrayBuffer {
return toRawType(val) === 'ArrayBuffer'
}

export function isTypedArray(val: unknown) {
export function isTypedArray(
val: unknown,
): val is
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array {
return [
'Int8Array',
'Uint8Array',
Expand Down Expand Up @@ -146,3 +167,117 @@ export function getGlobalThis() {
export function isNonEmptyArray(val: unknown): val is Array<any> {
return isArray(val) && !!val.length
}

export function isEqualWith(value: any, other: any, fn: (value: any, other: any) => any): boolean {
const cache = new WeakMap()

function baseIsEqual(value: any, other: any, cache: WeakMap<any, any>): boolean {
const customEqual = fn(value, other)

if (customEqual === true) {
return true
}

if (value === other) {
return true
}

// eslint-disable-next-line no-self-compare
if (value !== value && other !== other) {
return true
}

if (!isObject(value) || !isObject(other)) {
return value === other
}

if (value.constructor !== other.constructor) {
return false
}

if (isDate(value) && isDate(other)) {
return value.getTime() === other.getTime()
}

if (isRegExp(value) && isRegExp(other)) {
return value.source === other.source && value.flags === other.flags
}

if (isError(value) && isError(other)) {
return value.name === other.name && value.message === other.message && value.cause === other.cause
}

if (isDOMException(value) && isDOMException(other)) {
return value.name === other.name && value.message === other.message
}

if ((isTypedArray(value) && isTypedArray(other)) || (isDataView(value) && isDataView(other))) {
if (value.byteLength !== other.byteLength) {
return false
}

const valueTypedArray = new Uint8Array(value.buffer)
const otherTypedArray = new Uint8Array(other.buffer)

return valueTypedArray.every((v, i) => v === otherTypedArray[i])
}

if (isArrayBuffer(value) && isArrayBuffer(other)) {
if (value.byteLength !== other.byteLength) {
return false
}

const valueTypedArray = new Uint8Array(value)
const otherTypedArray = new Uint8Array(other)

return valueTypedArray.every((v, i) => v === otherTypedArray[i])
}

if (cache.get(value) === other && cache.get(other) === value) {
return true
}

cache.set(value, other)
cache.set(other, value)

if ((isMap(value) && isMap(other)) || (isSet(value) && isSet(other))) {
if (value.size !== other.size) {
return false
}

const valueArray = [...value]
const otherArray = [...other]

return valueArray.every((v, i) => baseIsEqual(v, otherArray[i], cache))
}

if (isArray(value) && isArray(other)) {
if (value.length !== other.length) {
return false
}

return value.every((v, i) => baseIsEqual(v, other[i], cache))
}

if (isPlainObject(value) && isPlainObject(other)) {
const valueOwnKeys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)]
const otherOwnKeys = [...Object.keys(other), ...Object.getOwnPropertySymbols(other)]

if (valueOwnKeys.length !== otherOwnKeys.length) {
return false
}

return valueOwnKeys.every((k) =>
baseIsEqual(value[k as keyof typeof value], other[k as keyof typeof other], cache),
)
}

return false
}

return baseIsEqual(value, other, cache)
}

export function isEqual(value: any, other: any): boolean {
return isEqualWith(value, other, () => undefined)
}
6 changes: 1 addition & 5 deletions src/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,7 @@ export function sumHash(value: any): string {
.reduce((hash, key) => baseSumHash(hash, value[key], key, seen), hash)

if (isFunction(value.valueOf)) {
try {
return sum(hash, String(value.valueOf()))
} catch (err) {
return sum(hash, `[valueOf exception]${(err as Error).message}`)
}
return sum(hash, String(value.valueOf()))
}

return hash
Expand Down
14 changes: 14 additions & 0 deletions tests/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,20 @@ describe('cloneDeep', () => {
expect(value).not.toBe(result)
})

it('Error', () => {
const value = new Error()
const result = cloneDeep(value)
expect(result).toEqual({})
expect(value).not.toBe(result)
})

it('DOMException', () => {
const value = new DOMException()
const result = cloneDeep(value)
expect(result).toEqual({})
expect(value).not.toBe(result)
})

it('class instance (prototype)', () => {
class Person {
name = 1
Expand Down
84 changes: 84 additions & 0 deletions tests/general.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
isWeakSet,
isTypedArray,
isDataView,
isEqual,
isEqualWith,
getGlobalThis,
} from '../src'

Expand Down Expand Up @@ -268,3 +270,85 @@ it('getGlobalThis', () => {
expect(getGlobalThis()).toBe(window)
expect(getGlobalThis()).toBe(self)
})

it('isEqual', () => {
expect(isEqual('123', '123')).toBe(true)
expect(isEqual('123', '1234')).toBe(false)
expect(isEqual(1, 1)).toBe(true)
expect(isEqual(1, 2)).toBe(false)
expect(isEqual(true, true)).toBe(true)
expect(isEqual(true, false)).toBe(false)
expect(isEqual(NaN, NaN)).toBe(true)
expect(isEqual(/abc/, /abc/)).toBe(true)
expect(isEqual(/abc/g, /abc/)).toBe(false)
expect(isEqual(/abc/, /abcd/)).toBe(false)
expect(isEqual(Symbol('test'), Symbol('test'))).toBe(false)
expect(isEqual(new WeakMap(), new WeakMap())).toBe(false)
expect(isEqual(new WeakSet(), new WeakSet())).toBe(false)
expect(isEqual(new Date('2024/11/03'), new Date('2024/11/03'))).toBe(true)
expect(isEqual(new Date('2024/11/03'), new Date('2024/11/04'))).toBe(false)
expect(isEqual(new Error('message'), new Error('message'))).toBe(true)
expect(isEqual(new Error('message'), new Error('mess'))).toBe(false)
expect(isEqual(new DOMException('message'), new DOMException('message'))).toBe(true)
expect(isEqual(new DOMException('message'), new DOMException('mess'))).toBe(false)
expect(
isEqual(
() => {},
() => {},
),
).toBe(false)

class A {}
class B {}

expect(isEqual(new A(), new A())).toBe(true)
expect(isEqual(new A(), new B())).toBe(false)

expect(isEqual(new Set([1]), new Set([1]))).toBe(true)
expect(isEqual(new Set([1]), new Set([]))).toBe(false)
expect(isEqual(new Set([{ n: 1 }]), new Set([{ n: 1 }]))).toBe(true)
expect(isEqual(new Set([{ n: 1 }]), new Set([{ n: 2 }]))).toBe(false)

expect(isEqual(new Map([['a', 1]]), new Map([['a', 1]]))).toBe(true)
expect(isEqual(new Map([['a', 1]]), new Map())).toBe(false)
expect(isEqual(new Map([['a', 1]]), new Map([['a', 2]]))).toBe(false)
expect(isEqual(new Map([['a', { n: 1 }]]), new Map([['a', { n: 1 }]]))).toBe(true)
expect(isEqual(new Map([[{ n: 1 }, { n: 1 }]]), new Map([[{ n: 1 }, { n: 1 }]]))).toBe(true)
expect(isEqual(new Map([[{ n: 1 }, { n: 1 }]]), new Map([[{ n: 2 }, { n: 1 }]]))).toBe(false)

expect(isEqual(new Int8Array(8), new Int8Array(8))).toBe(true)
expect(isEqual(new Int8Array(8), new Int8Array(10))).toBe(false)
expect(isEqual(new ArrayBuffer(8), new ArrayBuffer(8))).toBe(true)
expect(isEqual(new ArrayBuffer(8), new ArrayBuffer(10))).toBe(false)
expect(isEqual(new TextEncoder().encode('123').buffer, new TextEncoder().encode('123').buffer)).toBe(true)
expect(isEqual(new TextEncoder().encode('123').buffer, new TextEncoder().encode('1234').buffer)).toBe(false)

expect(isEqual({ n: 1 }, { n: 1 })).toBe(true)
expect(isEqual({ n: 1 }, { n: 2 })).toBe(false)
expect(isEqual({ n: 1, x: [1] }, { n: 1, x: [1] })).toBe(true)
expect(isEqual({ n: 1, x: [1] }, { n: 1, x: [1, 2] })).toBe(false)
expect(isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } })).toBe(true)
expect(isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } }, a1: {} })).toBe(false)
expect(isEqual([{ a: { b: { c: 1 } } }], [{ a: { b: { c: 1 } } }])).toBe(true)

const a: Record<string, any> = { n: 1 }
a.self = a
const b: Record<string, any> = { n: 1 }
b.self = b
a.x = b
b.x = a

expect(isEqual(a, b)).toBe(true)
})

it('isEqualWith', () => {
expect(isEqualWith({}, [], (a, b) => typeof a === typeof b)).toBe(true)
expect(isEqualWith([1, 2, 3], [1, 2, 4], (a, b) => a.length === b.length)).toBe(true)
expect(
isEqualWith(
() => {},
() => {},
(a, b) => isFunction(a) === isFunction(b),
),
).toBe(true)
})
2 changes: 2 additions & 0 deletions tests/math.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ it('sumHash', () => {
expect(sumHash(undefined)).toBe('29172c1a')
expect(sumHash('123')).toBe('1a3a267c')
expect(sumHash(123)).toBe('64a57068')
expect(sumHash({ n: 1 })).toBe('66b13e4a')
expect(sumHash(Object.create({ n: 1 }))).toBe('59322f29')
expect(sumHash([1, 2, 3])).toBe('352dd8ea')
expect(sumHash({ a: '123' })).toBe('b1c920ac')
expect(
Expand Down

0 comments on commit 81e06c8

Please sign in to comment.