@heliosgraphics/utils 6.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/classnames.spec.ts +17 -0
- package/classnames.ts +21 -0
- package/clipboard.spec.ts +40 -0
- package/clipboard.ts +10 -0
- package/colors.spec.ts +40 -0
- package/colors.ts +33 -0
- package/debounce.spec.ts +101 -0
- package/debounce.ts +15 -0
- package/equals.spec.ts +168 -0
- package/equals.ts +120 -0
- package/index.ts +20 -0
- package/package.json +14 -0
- package/sanitize.spec.ts +558 -0
- package/sanitize.ts +219 -0
- package/sleep.spec.ts +59 -0
- package/sleep.ts +1 -0
- package/slug.spec.ts +15 -0
- package/slug.ts +14 -0
- package/strings.spec.ts +77 -0
- package/strings.ts +46 -0
- package/throttle.spec.ts +142 -0
- package/throttle.ts +17 -0
- package/tsconfig.json +40 -0
- package/uuid.spec.ts +32 -0
- package/uuid.ts +14 -0
- package/validations.spec.ts +26 -0
- package/validations.ts +19 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Helios Graphics
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { it, describe, expect } from "vitest"
|
|
2
|
+
import { getClasses } from "./classnames"
|
|
3
|
+
|
|
4
|
+
describe("classnames", () => {
|
|
5
|
+
describe("getClasses", () => {
|
|
6
|
+
it("returns valid with string", () => expect(getClasses("burn")).toEqual("burn"))
|
|
7
|
+
it("returns conditional object", () =>
|
|
8
|
+
expect(getClasses("burn", { "burn--alternative": true })).toEqual("burn burn--alternative"))
|
|
9
|
+
it("returns valid with invalid", () =>
|
|
10
|
+
expect(
|
|
11
|
+
getClasses("burn", null, "burn", undefined, {
|
|
12
|
+
"burn--active": false,
|
|
13
|
+
"burn--loading": true,
|
|
14
|
+
}),
|
|
15
|
+
).toEqual("burn burn--loading"))
|
|
16
|
+
})
|
|
17
|
+
})
|
package/classnames.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const getClasses = (...args: Array<unknown>): string => {
|
|
2
|
+
const classNames: Set<string> = new Set()
|
|
3
|
+
|
|
4
|
+
for (const item of args) {
|
|
5
|
+
const itemType = typeof item
|
|
6
|
+
const isValidString: boolean = itemType === "string" && (item as string).length > 0
|
|
7
|
+
const isValidObject: boolean = itemType === "object" && item !== null
|
|
8
|
+
|
|
9
|
+
if (isValidString) {
|
|
10
|
+
classNames.add(item as string)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (isValidObject) {
|
|
14
|
+
for (const [key, value] of Object.entries(item as Record<string, unknown>)) {
|
|
15
|
+
if (value) classNames.add(key)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return Array.from(classNames).join(" ")
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest"
|
|
2
|
+
import { copyValue } from "./clipboard"
|
|
3
|
+
|
|
4
|
+
const mockDocument = {
|
|
5
|
+
createElement: vi.fn(),
|
|
6
|
+
execCommand: vi.fn(),
|
|
7
|
+
body: {
|
|
8
|
+
appendChild: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
} as unknown as Document
|
|
11
|
+
|
|
12
|
+
global.document = mockDocument
|
|
13
|
+
|
|
14
|
+
describe("copyValue", () => {
|
|
15
|
+
const TEXT_STRING = "Test text" as const
|
|
16
|
+
|
|
17
|
+
it("copies text to the clipboard", () => {
|
|
18
|
+
const textAreaMock: {
|
|
19
|
+
value: string
|
|
20
|
+
select: () => void
|
|
21
|
+
remove: () => void
|
|
22
|
+
} = {
|
|
23
|
+
value: "",
|
|
24
|
+
select: vi.fn(),
|
|
25
|
+
remove: vi.fn(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
document.createElement = vi.fn().mockReturnValue(textAreaMock)
|
|
29
|
+
document.body.appendChild = vi.fn().mockReturnValue(textAreaMock)
|
|
30
|
+
|
|
31
|
+
copyValue(TEXT_STRING)
|
|
32
|
+
|
|
33
|
+
expect(document.createElement).toHaveBeenCalledWith("textarea")
|
|
34
|
+
expect(document.body.appendChild).toHaveBeenCalled()
|
|
35
|
+
expect(document.execCommand).toHaveBeenCalledWith("copy", false)
|
|
36
|
+
expect(textAreaMock.value).toBe(TEXT_STRING)
|
|
37
|
+
expect(textAreaMock.select).toHaveBeenCalled()
|
|
38
|
+
expect(textAreaMock.remove).toHaveBeenCalled()
|
|
39
|
+
})
|
|
40
|
+
})
|
package/clipboard.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const copyValue = (text: string): void => {
|
|
2
|
+
const input: HTMLTextAreaElement = document.createElement("textarea")
|
|
3
|
+
|
|
4
|
+
document.body.appendChild(input)
|
|
5
|
+
input.value = text
|
|
6
|
+
input.select()
|
|
7
|
+
document.execCommand("copy", false)
|
|
8
|
+
|
|
9
|
+
return input.remove()
|
|
10
|
+
}
|
package/colors.spec.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { it, describe, expect } from "vitest"
|
|
2
|
+
import { rgbToHex, hexToRgb, DEFAULT_PROFILE_RGB } from "./colors"
|
|
3
|
+
|
|
4
|
+
describe("colors", () => {
|
|
5
|
+
describe("hexToRgb", () => {
|
|
6
|
+
it("converts hex to rgb", () => expect(hexToRgb("#0c2c78")).toEqual([12, 44, 120]))
|
|
7
|
+
it("converts hex without #", () => expect(hexToRgb("0c2c78")).toEqual([12, 44, 120]))
|
|
8
|
+
it("converts uppercase hex", () => expect(hexToRgb("#0C2C78")).toEqual([12, 44, 120]))
|
|
9
|
+
it("converts short hex", () => expect(hexToRgb("#fff")).toEqual([0, 15, 255]))
|
|
10
|
+
it("converts black", () => expect(hexToRgb("#000000")).toEqual([0, 0, 0]))
|
|
11
|
+
it("converts white", () => expect(hexToRgb("#ffffff")).toEqual([255, 255, 255]))
|
|
12
|
+
it("returns default for 0", () => expect(hexToRgb(0 as unknown as string)).toEqual(DEFAULT_PROFILE_RGB))
|
|
13
|
+
it("returns default for undefined", () => expect(hexToRgb(undefined)).toEqual(DEFAULT_PROFILE_RGB))
|
|
14
|
+
it("returns default for null", () => expect(hexToRgb(null)).toEqual(DEFAULT_PROFILE_RGB))
|
|
15
|
+
it("returns default for empty string", () => expect(hexToRgb("")).toEqual(DEFAULT_PROFILE_RGB))
|
|
16
|
+
it("returns default for invalid hex", () => expect(hexToRgb("gggggg")).toEqual([0, 0, 0]))
|
|
17
|
+
it("returns default for non-string", () => expect(hexToRgb(123 as unknown as string)).toEqual(DEFAULT_PROFILE_RGB))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe("rgbToHex", () => {
|
|
21
|
+
it("converts rgb to hex", () => expect(rgbToHex(12, 44, 120)).toEqual("#0c2c78"))
|
|
22
|
+
it("converts string to hex", () =>
|
|
23
|
+
expect(rgbToHex("12" as unknown as number, "44" as unknown as number, "120" as unknown as number)).toEqual(
|
|
24
|
+
"#0c2c78",
|
|
25
|
+
))
|
|
26
|
+
it("converts null to hex", () => expect(rgbToHex(null as unknown as number, 44, 120)).toEqual("#002c78"))
|
|
27
|
+
it("returns undefined to hex", () => expect(rgbToHex(12, undefined as unknown as number, 120)).toEqual("#0cff78"))
|
|
28
|
+
it("returns FF for negative", () => expect(rgbToHex(-1, -12, 120)).toEqual("#ffff78"))
|
|
29
|
+
it("converts zero values", () => expect(rgbToHex(0, 0, 0)).toEqual("#000000"))
|
|
30
|
+
it("converts max values", () => expect(rgbToHex(255, 255, 255)).toEqual("#ffffff"))
|
|
31
|
+
it("returns FF for over 255", () => expect(rgbToHex(300, 256, 120)).toEqual("#ffff78"))
|
|
32
|
+
it("returns FF for invalid string", () => expect(rgbToHex("abc" as unknown as number, 44, 120)).toEqual("#ff2c78"))
|
|
33
|
+
it("handles fractional numbers", () =>
|
|
34
|
+
expect(rgbToHex(12.9, 44.1, 120.5)).toEqual("#c.e6666666666682c.19999999999a78.8"))
|
|
35
|
+
it("uses defaults when no params", () => expect(rgbToHex()).toEqual("#ffffff"))
|
|
36
|
+
it("handles mixed valid/invalid", () =>
|
|
37
|
+
expect(rgbToHex(12, "invalid" as unknown as number, 120)).toEqual("#0cff78"))
|
|
38
|
+
it("converts single digit to padded hex", () => expect(rgbToHex(1, 2, 3)).toEqual("#010203"))
|
|
39
|
+
})
|
|
40
|
+
})
|
package/colors.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type TypeRGB = [number, number, number]
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PROFILE_RGB: TypeRGB = [199, 201, 209] as const
|
|
4
|
+
|
|
5
|
+
export const hexToRgb = (hex?: string | null): TypeRGB => {
|
|
6
|
+
const isValid: boolean = !!hex && typeof hex === "string"
|
|
7
|
+
|
|
8
|
+
if (!isValid || !hex) return DEFAULT_PROFILE_RGB
|
|
9
|
+
|
|
10
|
+
hex = hex.replace(/^#/, "")
|
|
11
|
+
|
|
12
|
+
const bigint = parseInt(hex, 16)
|
|
13
|
+
const r: number = (bigint >> 16) & 255
|
|
14
|
+
const g: number = (bigint >> 8) & 255
|
|
15
|
+
const b: number = bigint & 255
|
|
16
|
+
|
|
17
|
+
return [r, g, b]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const rgbToHex = (r: number | string = 255, g: number | string = 255, b: number | string = 255): string => {
|
|
21
|
+
const _toHex = (c: unknown): string => {
|
|
22
|
+
const value: number = Number(c)
|
|
23
|
+
const isInvalid: boolean = isNaN(value) || value < 0 || value > 255
|
|
24
|
+
|
|
25
|
+
if (isInvalid) return "ff"
|
|
26
|
+
|
|
27
|
+
const hex = value.toString(16)
|
|
28
|
+
|
|
29
|
+
return hex.length === 1 ? `0${hex}` : hex
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `#${_toHex(r)}${_toHex(g)}${_toHex(b)}`
|
|
33
|
+
}
|
package/debounce.spec.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest"
|
|
2
|
+
import { debounce } from "./debounce"
|
|
3
|
+
import { sleep } from "./sleep"
|
|
4
|
+
|
|
5
|
+
describe("debounce", () => {
|
|
6
|
+
it("calls the callback after the specified time", async () => {
|
|
7
|
+
const callback = vi.fn()
|
|
8
|
+
const debouncedFunction = debounce(callback, 50)
|
|
9
|
+
|
|
10
|
+
debouncedFunction()
|
|
11
|
+
|
|
12
|
+
await sleep(25)
|
|
13
|
+
expect(callback).not.toHaveBeenCalled()
|
|
14
|
+
|
|
15
|
+
await sleep(30)
|
|
16
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("does not call the callback if the function is called again within the wait time", async () => {
|
|
20
|
+
const callback = vi.fn()
|
|
21
|
+
const debouncedFunction = debounce(callback, 50)
|
|
22
|
+
|
|
23
|
+
debouncedFunction()
|
|
24
|
+
await sleep(25)
|
|
25
|
+
|
|
26
|
+
debouncedFunction()
|
|
27
|
+
await sleep(25)
|
|
28
|
+
|
|
29
|
+
expect(callback).not.toHaveBeenCalled()
|
|
30
|
+
|
|
31
|
+
await sleep(30)
|
|
32
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("preserves function arguments", async () => {
|
|
36
|
+
const callback = vi.fn()
|
|
37
|
+
const debouncedFunction = debounce(callback, 50)
|
|
38
|
+
|
|
39
|
+
debouncedFunction("arg1", "arg2", 123)
|
|
40
|
+
|
|
41
|
+
await sleep(60)
|
|
42
|
+
expect(callback).toHaveBeenCalledWith("arg1", "arg2", 123)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("uses the latest arguments when called multiple times", async () => {
|
|
46
|
+
const callback = vi.fn()
|
|
47
|
+
const debouncedFunction = debounce(callback, 50)
|
|
48
|
+
|
|
49
|
+
debouncedFunction("first")
|
|
50
|
+
await sleep(25)
|
|
51
|
+
|
|
52
|
+
debouncedFunction("second")
|
|
53
|
+
await sleep(60)
|
|
54
|
+
|
|
55
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
56
|
+
expect(callback).toHaveBeenCalledWith("second")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("works with functions that have specific parameter types", async () => {
|
|
60
|
+
const callback = vi.fn((str: string, num: number) => str + num)
|
|
61
|
+
const debouncedFunction = debounce(callback, 50)
|
|
62
|
+
|
|
63
|
+
debouncedFunction("test", 42)
|
|
64
|
+
|
|
65
|
+
await sleep(60)
|
|
66
|
+
expect(callback).toHaveBeenCalledWith("test", 42)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("works with functions that return values", async () => {
|
|
70
|
+
const callback = vi.fn((x: number) => x * 2)
|
|
71
|
+
const debouncedFunction = debounce(callback, 50)
|
|
72
|
+
|
|
73
|
+
debouncedFunction(5)
|
|
74
|
+
|
|
75
|
+
await sleep(60)
|
|
76
|
+
expect(callback).toHaveBeenCalledWith(5)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("works with zero wait time", async () => {
|
|
80
|
+
const callback = vi.fn()
|
|
81
|
+
const debouncedFunction = debounce(callback, 0)
|
|
82
|
+
|
|
83
|
+
debouncedFunction()
|
|
84
|
+
|
|
85
|
+
await sleep(1)
|
|
86
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("can be called after completion", async () => {
|
|
90
|
+
const callback = vi.fn()
|
|
91
|
+
const debouncedFunction = debounce(callback, 50)
|
|
92
|
+
|
|
93
|
+
debouncedFunction()
|
|
94
|
+
await sleep(60)
|
|
95
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
96
|
+
|
|
97
|
+
debouncedFunction()
|
|
98
|
+
await sleep(60)
|
|
99
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
100
|
+
})
|
|
101
|
+
})
|
package/debounce.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export const debounce = <T extends (...args: Array<any>) => any>(
|
|
3
|
+
callback: T,
|
|
4
|
+
wait: number,
|
|
5
|
+
): ((...args: Parameters<T>) => void) => {
|
|
6
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
7
|
+
|
|
8
|
+
return (...args: Parameters<T>): void => {
|
|
9
|
+
globalThis.clearTimeout(timeoutId)
|
|
10
|
+
|
|
11
|
+
timeoutId = globalThis.setTimeout(() => {
|
|
12
|
+
callback.apply(null, args)
|
|
13
|
+
}, wait)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/equals.spec.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { getIsEqual, getAreSetsEqual } from "./equals"
|
|
2
|
+
import { describe, it, expect } from "vitest"
|
|
3
|
+
|
|
4
|
+
describe("getIsEqual", () => {
|
|
5
|
+
it("primitives", () => {
|
|
6
|
+
expect(getIsEqual(1, 1)).toBe(true)
|
|
7
|
+
expect(getIsEqual("hello", "hello")).toBe(true)
|
|
8
|
+
expect(getIsEqual(" ", " ")).toBe(true)
|
|
9
|
+
expect(getIsEqual("❤️", "❤️")).toBe(true)
|
|
10
|
+
expect(getIsEqual(true, true)).toBe(true)
|
|
11
|
+
expect(getIsEqual(null, null)).toBe(true)
|
|
12
|
+
expect(getIsEqual(undefined, undefined)).toBe(true)
|
|
13
|
+
expect(getIsEqual(1, 2)).toBe(false)
|
|
14
|
+
expect(getIsEqual("hello", "world")).toBe(false)
|
|
15
|
+
expect(getIsEqual(true, false)).toBe(false)
|
|
16
|
+
expect(getIsEqual(null, undefined)).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("arrays", () => {
|
|
20
|
+
expect(getIsEqual([], [])).toBe(true)
|
|
21
|
+
expect(getIsEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
|
22
|
+
expect(getIsEqual([1, [2, 3]], [1, [2, 3]])).toBe(true)
|
|
23
|
+
expect(getIsEqual([1, 2], [1, 2, 3])).toBe(false)
|
|
24
|
+
expect(getIsEqual([1, 2, 3], [1, 3, 2])).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("objects", () => {
|
|
28
|
+
expect(getIsEqual({}, {})).toBe(true)
|
|
29
|
+
expect(getIsEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
|
|
30
|
+
expect(getIsEqual({ a: { b: 2 } }, { a: { b: 2 } })).toBe(true)
|
|
31
|
+
expect(getIsEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
|
|
32
|
+
expect(getIsEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false)
|
|
33
|
+
expect(getIsEqual({ a: 1 }, { a: 2 })).toBe(false)
|
|
34
|
+
|
|
35
|
+
const object1 = { a: 1, b: 2 }
|
|
36
|
+
expect(getIsEqual(object1, null)).toBeFalsy()
|
|
37
|
+
expect(getIsEqual(null, object1)).toBeFalsy()
|
|
38
|
+
expect(getIsEqual(undefined, undefined)).toBeTruthy()
|
|
39
|
+
|
|
40
|
+
const object3 = { a: 1, b: 2 }
|
|
41
|
+
const object4 = { b: 2, a: 1 }
|
|
42
|
+
expect(getIsEqual(object3, object4)).toBeTruthy()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("dates", () => {
|
|
46
|
+
const date1 = new Date("2024-01-01")
|
|
47
|
+
const date2 = new Date("2024-01-01")
|
|
48
|
+
const date3 = new Date("2024-01-02")
|
|
49
|
+
|
|
50
|
+
expect(getIsEqual(date1, date2)).toBe(true)
|
|
51
|
+
expect(getIsEqual(date1, date3)).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("sets", () => {
|
|
55
|
+
const basicSet1 = new Set(["hello", 2, 3])
|
|
56
|
+
const basicSet2 = new Set(["hello", 2, 3])
|
|
57
|
+
const basicSet3 = new Set(["hello", 2, "3"])
|
|
58
|
+
|
|
59
|
+
expect(getIsEqual(basicSet1, basicSet2)).toBe(true)
|
|
60
|
+
expect(getIsEqual(basicSet1, basicSet3)).toBe(false)
|
|
61
|
+
|
|
62
|
+
const objectSet1 = new Set([{ a: 1 }, { b: 2 }])
|
|
63
|
+
const objectSet2 = new Set([{ a: 1 }, { b: 2 }])
|
|
64
|
+
const objectSet3 = new Set([{ a: 1 }, { b: 3 }])
|
|
65
|
+
|
|
66
|
+
expect(getAreSetsEqual(objectSet1, objectSet2)).toBe(true)
|
|
67
|
+
expect(getAreSetsEqual(objectSet1, objectSet3)).toBe(false)
|
|
68
|
+
|
|
69
|
+
const nestedSet1 = new Set([{ a: { b: 2 } }, [1, 2, 3]])
|
|
70
|
+
const nestedSet2 = new Set([{ a: { b: 2 } }, [1, 2, 3]])
|
|
71
|
+
const nestedSet3 = new Set([{ a: { b: 3 } }, [1, 2, 3]])
|
|
72
|
+
|
|
73
|
+
expect(getAreSetsEqual(nestedSet1, nestedSet2)).toBe(true)
|
|
74
|
+
expect(getAreSetsEqual(nestedSet1, nestedSet3)).toBe(false)
|
|
75
|
+
|
|
76
|
+
const emptySet1 = new Set()
|
|
77
|
+
const emptySet2 = new Set()
|
|
78
|
+
|
|
79
|
+
expect(getAreSetsEqual(emptySet1, emptySet2)).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("maps", () => {
|
|
83
|
+
const map1 = new Map([
|
|
84
|
+
["a", 1],
|
|
85
|
+
["b", 2],
|
|
86
|
+
])
|
|
87
|
+
const map2 = new Map([
|
|
88
|
+
["a", 1],
|
|
89
|
+
["b", 2],
|
|
90
|
+
])
|
|
91
|
+
const map3 = new Map([
|
|
92
|
+
["a", 1],
|
|
93
|
+
["b", 3],
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
expect(getIsEqual(map1, map2)).toBe(true)
|
|
97
|
+
expect(getIsEqual(map1, map3)).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("circular references", () => {
|
|
101
|
+
const obj1: Record<string, unknown> = { a: 1 }
|
|
102
|
+
const obj2: Record<string, unknown> = { a: 1 }
|
|
103
|
+
|
|
104
|
+
obj1["self"] = obj1
|
|
105
|
+
obj2["self"] = obj2
|
|
106
|
+
|
|
107
|
+
expect(getIsEqual(obj1, obj2)).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("circular arrays", () => {
|
|
111
|
+
const arr1: Array<unknown> = [1]
|
|
112
|
+
const arr2: Array<unknown> = [1]
|
|
113
|
+
arr1.push(arr1)
|
|
114
|
+
arr2.push(arr2)
|
|
115
|
+
expect(getIsEqual(arr1, arr2)).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("symbols are unique", () => {
|
|
119
|
+
const symbol1 = Symbol("foo")
|
|
120
|
+
const symbol2 = Symbol("foo")
|
|
121
|
+
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
expect(getIsEqual(symbol1, symbol2 as any)).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("bigint", () => {
|
|
127
|
+
expect(getIsEqual(42n, 42n)).toBe(true)
|
|
128
|
+
expect(getIsEqual(42n, 43n)).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("regexps", () => {
|
|
132
|
+
const regex1 = /hello/gi
|
|
133
|
+
const regex2 = /hello/gi
|
|
134
|
+
const regex3 = /world/gi
|
|
135
|
+
|
|
136
|
+
expect(getIsEqual(regex1, regex2)).toBe(true)
|
|
137
|
+
expect(getIsEqual(regex1, regex3)).toBe(false)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("errors", () => {
|
|
141
|
+
const error1 = new Error("test message")
|
|
142
|
+
const error2 = new Error("test message")
|
|
143
|
+
const error3 = new Error("different message")
|
|
144
|
+
|
|
145
|
+
// Errors with same message but different stack traces are not equal
|
|
146
|
+
expect(getIsEqual(error1, error2)).toBe(false)
|
|
147
|
+
expect(getIsEqual(error1, error3)).toBe(false)
|
|
148
|
+
|
|
149
|
+
// Same error instance is equal to itself
|
|
150
|
+
expect(getIsEqual(error1, error1)).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("buffers", () => {
|
|
154
|
+
const buffer1 = Buffer.from("hello")
|
|
155
|
+
const buffer2 = Buffer.from("hello")
|
|
156
|
+
const buffer3 = Buffer.from("world")
|
|
157
|
+
|
|
158
|
+
expect(getIsEqual(buffer1, buffer2)).toBe(true)
|
|
159
|
+
expect(getIsEqual(buffer1, buffer3)).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("duplicate array check branch", () => {
|
|
163
|
+
const arr1 = [1, 2, 3]
|
|
164
|
+
const arr2 = [1, 2, 3]
|
|
165
|
+
|
|
166
|
+
expect(getIsEqual(arr1, arr2)).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
})
|
package/equals.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
type FracturesPrimitive = string | number | boolean | null | undefined | bigint | symbol
|
|
2
|
+
type FracturesComparable = FracturesPrimitive | object | Array<unknown> | Set<unknown> | Map<unknown, unknown>
|
|
3
|
+
|
|
4
|
+
export const _getAreMapsEqual = <K, V>(
|
|
5
|
+
mapA: Map<K, V>,
|
|
6
|
+
mapB: Map<K, V>,
|
|
7
|
+
seen = new WeakMap<object, object>(),
|
|
8
|
+
): boolean => {
|
|
9
|
+
if (mapA.size !== mapB.size) return false
|
|
10
|
+
if (mapA === mapB) return true
|
|
11
|
+
|
|
12
|
+
const entriesA = Array.from(mapA.entries())
|
|
13
|
+
const entriesB = new Map(mapB)
|
|
14
|
+
|
|
15
|
+
return entriesA.every(
|
|
16
|
+
([key, value]) =>
|
|
17
|
+
entriesB.has(key) && _getIsEqual(value as FracturesComparable, entriesB.get(key) as FracturesComparable, seen),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getAreDatesEqual = (a: Date, b: Date): boolean => a.getTime() === b.getTime()
|
|
22
|
+
export const getAreRegExpsEqual = (a: RegExp, b: RegExp): boolean => a.toString() === b.toString()
|
|
23
|
+
export const getAreErrorsEqual = (a: Error, b: Error): boolean =>
|
|
24
|
+
a.message === b.message && a.name === b.name && a.stack === b.stack
|
|
25
|
+
export const getAreBuffersEqual = (a: Buffer, b: Buffer): boolean => a.length === b.length && Buffer.compare(a, b) === 0
|
|
26
|
+
|
|
27
|
+
export const getAreSetsEqual = <T>(setA: Set<T>, setB: Set<T>): boolean => {
|
|
28
|
+
if (setA.size !== setB.size) return false
|
|
29
|
+
if (setA === setB) return true
|
|
30
|
+
if (setA.size === 0) return true
|
|
31
|
+
|
|
32
|
+
const arrA = Array.from(setA)
|
|
33
|
+
const arrB = Array.from(setB)
|
|
34
|
+
const matched = new Array(arrB.length).fill(false)
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < arrA.length; i++) {
|
|
37
|
+
let foundMatch = false
|
|
38
|
+
|
|
39
|
+
for (let j = 0; j < arrB.length; j++) {
|
|
40
|
+
if (!matched[j] && _getIsEqual(arrA[i] as FracturesComparable, arrB[j] as FracturesComparable)) {
|
|
41
|
+
matched[j] = true
|
|
42
|
+
foundMatch = true
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!foundMatch) return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const _getIsEqual = <T extends FracturesComparable>(
|
|
54
|
+
a: T,
|
|
55
|
+
b: T,
|
|
56
|
+
seen = new WeakMap<object, object>(),
|
|
57
|
+
): boolean => {
|
|
58
|
+
if (a === b) return true
|
|
59
|
+
if (a === null || b === null) return a === b
|
|
60
|
+
|
|
61
|
+
if (typeof a !== "object" && typeof b !== "object") {
|
|
62
|
+
return a === b
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Object.is(a, b)) return true
|
|
70
|
+
|
|
71
|
+
const typeA = typeof a
|
|
72
|
+
const typeB = typeof b
|
|
73
|
+
|
|
74
|
+
if (typeA !== typeB) return false
|
|
75
|
+
if (typeA !== "object") return false
|
|
76
|
+
|
|
77
|
+
const seenA = seen.get(a as object)
|
|
78
|
+
if (seenA) return seenA === b
|
|
79
|
+
|
|
80
|
+
seen.set(a as object, b as object)
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
83
|
+
if (a.length !== b.length) return false
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < a.length; i++) {
|
|
86
|
+
if (!_getIsEqual(a[i], b[i], seen)) return false
|
|
87
|
+
}
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (a instanceof Map && b instanceof Map) return _getAreMapsEqual(a, b, seen)
|
|
92
|
+
if (a instanceof Set && b instanceof Set) return getAreSetsEqual(a, b)
|
|
93
|
+
if (a instanceof Date && b instanceof Date) return getAreDatesEqual(a, b)
|
|
94
|
+
if (a instanceof Error && b instanceof Error) return getAreErrorsEqual(a, b)
|
|
95
|
+
if (a instanceof RegExp && b instanceof RegExp) return getAreRegExpsEqual(a, b)
|
|
96
|
+
if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) return getAreBuffersEqual(a, b)
|
|
97
|
+
|
|
98
|
+
const keysA = [...Object.keys(a as object), ...Object.getOwnPropertySymbols(a as object)]
|
|
99
|
+
const keysB = [...Object.keys(b as object), ...Object.getOwnPropertySymbols(b as object)]
|
|
100
|
+
|
|
101
|
+
if (keysA.length !== keysB.length) return false
|
|
102
|
+
|
|
103
|
+
return keysA.every(
|
|
104
|
+
(key) =>
|
|
105
|
+
Object.prototype.hasOwnProperty.call(b, key) &&
|
|
106
|
+
_getIsEqual(
|
|
107
|
+
(a as Record<string | symbol, unknown>)[key] as FracturesComparable,
|
|
108
|
+
(b as Record<string | symbol, unknown>)[key] as FracturesComparable,
|
|
109
|
+
seen,
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const getIsEqual = <T extends FracturesComparable>(a: T, b: T): boolean => {
|
|
115
|
+
return _getIsEqual(a, b)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const getAreMapsEqual = <T extends FracturesComparable>(a: T, b: T): boolean => {
|
|
119
|
+
return _getAreMapsEqual(a as Map<unknown, unknown>, b as Map<unknown, unknown>)
|
|
120
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { getClasses } from "./classnames"
|
|
2
|
+
export { copyValue } from "./clipboard"
|
|
3
|
+
export { hexToRgb, rgbToHex } from "./colors"
|
|
4
|
+
export { debounce } from "./debounce"
|
|
5
|
+
export {
|
|
6
|
+
getIsEqual,
|
|
7
|
+
getAreMapsEqual,
|
|
8
|
+
getAreSetsEqual,
|
|
9
|
+
getAreDatesEqual,
|
|
10
|
+
getAreErrorsEqual,
|
|
11
|
+
getAreRegExpsEqual,
|
|
12
|
+
getAreBuffersEqual,
|
|
13
|
+
} from "./equals"
|
|
14
|
+
export { sleep } from "./sleep"
|
|
15
|
+
export { getSlug } from "./slug"
|
|
16
|
+
export { removeMarkdown, removeNewlines, ellipsis } from "./strings"
|
|
17
|
+
export { sanitizeText } from "./sanitize"
|
|
18
|
+
export { throttle } from "./throttle"
|
|
19
|
+
export { isUUID, getUUID } from "./uuid"
|
|
20
|
+
export { validateUrl, validateEmail } from "./validations"
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heliosgraphics/utils",
|
|
3
|
+
"version": "6.0.0-alpha.9",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": "Chris Puska <chris@puska.org>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"description": "Universal Javascript Helpers",
|
|
9
|
+
"main": "index.ts",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@types/node": "^24"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {}
|
|
14
|
+
}
|