@ezez/utils 2.1.0 → 4.0.0
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/CHANGELOG.md +37 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/omit.d.ts +1 -1
- package/dist/omit.d.ts.map +1 -1
- package/dist/omit.js.map +1 -1
- package/dist/replace.d.ts.map +1 -1
- package/dist/replace.js.map +1 -1
- package/dist/replaceDeep.d.ts +5 -1
- package/dist/replaceDeep.d.ts.map +1 -1
- package/dist/replaceDeep.js +3 -20
- package/dist/replaceDeep.js.map +1 -1
- package/dist/replaceDeepByFn.d.ts +7 -0
- package/dist/replaceDeepByFn.d.ts.map +1 -0
- package/dist/replaceDeepByFn.js +47 -0
- package/dist/replaceDeepByFn.js.map +1 -0
- package/dist/safe.d.ts.map +1 -1
- package/dist/safe.js.map +1 -1
- package/dist/sample.d.ts +3 -0
- package/dist/sample.d.ts.map +1 -0
- package/dist/sample.js +8 -0
- package/dist/sample.js.map +1 -0
- package/dist/samples.d.ts +3 -0
- package/dist/samples.d.ts.map +1 -0
- package/dist/samples.js +28 -0
- package/dist/samples.js.map +1 -0
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +56 -18
- package/dist/serialize.js.map +1 -1
- package/dist/shuffle.d.ts +3 -0
- package/dist/shuffle.d.ts.map +1 -0
- package/dist/shuffle.js +9 -0
- package/dist/shuffle.js.map +1 -0
- package/dist/toggle.d.ts +3 -0
- package/dist/toggle.d.ts.map +1 -0
- package/dist/toggle.js +15 -0
- package/dist/toggle.js.map +1 -0
- package/dist/trim.d.ts +3 -0
- package/dist/trim.d.ts.map +1 -0
- package/dist/trim.js +10 -0
- package/dist/trim.js.map +1 -0
- package/dist/trimEnd.d.ts +3 -0
- package/dist/trimEnd.d.ts.map +1 -0
- package/dist/trimEnd.js +12 -0
- package/dist/trimEnd.js.map +1 -0
- package/dist/trimStart.d.ts +3 -0
- package/dist/trimStart.d.ts.map +1 -0
- package/dist/trimStart.js +12 -0
- package/dist/trimStart.js.map +1 -0
- package/dist/utils/utils.d.ts +6 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +10 -0
- package/dist/utils/utils.js.map +1 -0
- package/dist/waitFor.d.ts +7 -1
- package/dist/waitFor.d.ts.map +1 -1
- package/dist/waitFor.js +35 -10
- package/dist/waitFor.js.map +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/functions/cap.html +12 -5
- package/docs/functions/capitalize.html +12 -5
- package/docs/functions/coalesce.html +12 -5
- package/docs/functions/compareArrays.html +12 -5
- package/docs/functions/compareProps.html +12 -5
- package/docs/functions/deserialize.html +12 -5
- package/docs/functions/ensureArray.html +12 -5
- package/docs/functions/ensureDate.html +12 -5
- package/docs/functions/ensureError.html +12 -5
- package/docs/functions/ensurePrefix.html +12 -5
- package/docs/functions/ensureSuffix.html +12 -5
- package/docs/functions/ensureTimestamp.html +12 -5
- package/docs/functions/escapeRegExp.html +12 -5
- package/docs/functions/formatDate.html +12 -5
- package/docs/functions/get.html +12 -5
- package/docs/functions/getMultiple.html +12 -5
- package/docs/functions/insertSeparator.html +12 -5
- package/docs/functions/isEmpty.html +12 -5
- package/docs/functions/isNumericString.html +12 -5
- package/docs/functions/isPlainObject.html +12 -5
- package/docs/functions/last.html +12 -5
- package/docs/functions/later-1.html +12 -5
- package/docs/functions/mapAsync.html +12 -5
- package/docs/functions/mapValues.html +12 -5
- package/docs/functions/match.html +12 -5
- package/docs/functions/merge.html +20 -13
- package/docs/functions/mostFrequent.html +12 -5
- package/docs/functions/noop.html +12 -5
- package/docs/functions/occurrences.html +12 -5
- package/docs/functions/omit.html +18 -7
- package/docs/functions/pick.html +13 -6
- package/docs/functions/pull.html +12 -5
- package/docs/functions/remove.html +12 -5
- package/docs/functions/removeCommonProperties.html +12 -5
- package/docs/functions/replace.html +12 -5
- package/docs/functions/replaceDeep.html +23 -7
- package/docs/functions/rethrow.html +12 -5
- package/docs/functions/round.html +12 -5
- package/docs/functions/safe.html +13 -6
- package/docs/functions/sample.html +149 -0
- package/docs/functions/samples.html +159 -0
- package/docs/functions/scale.html +12 -5
- package/docs/functions/seq.html +12 -5
- package/docs/functions/seqEarlyBreak.html +12 -5
- package/docs/functions/serialize.html +12 -5
- package/docs/functions/set.html +12 -5
- package/docs/functions/setImmutable.html +12 -5
- package/docs/functions/shuffle.html +149 -0
- package/docs/functions/sortBy.html +12 -5
- package/docs/functions/sortProps.html +12 -5
- package/docs/functions/stripPrefix.html +12 -5
- package/docs/functions/stripSuffix.html +12 -5
- package/docs/functions/throttle.html +12 -5
- package/docs/functions/toggle.html +154 -0
- package/docs/functions/trim.html +152 -0
- package/docs/functions/trimEnd.html +152 -0
- package/docs/functions/trimStart.html +152 -0
- package/docs/functions/truthy.html +12 -5
- package/docs/functions/unique.html +12 -5
- package/docs/functions/wait.html +12 -5
- package/docs/functions/waitFor.html +23 -18
- package/docs/functions/waitSync.html +12 -5
- package/docs/index.html +11 -4
- package/docs/interfaces/ComparePropsOptions.html +6 -6
- package/docs/interfaces/GetMultipleSource.html +12 -5
- package/docs/interfaces/GetSource.html +12 -5
- package/docs/interfaces/IsNumericStringOptions.html +9 -9
- package/docs/interfaces/OccurencesOptions.html +6 -6
- package/docs/interfaces/SetImmutableSource.html +12 -5
- package/docs/interfaces/SetSource.html +12 -5
- package/docs/interfaces/ThrottleOptions.html +7 -7
- package/docs/interfaces/ThrottledFunctionExtras.html +7 -7
- package/docs/modules.html +18 -4
- package/docs/pages/CHANGELOG.html +150 -61
- package/docs/pages/Introduction.html +11 -4
- package/docs/types/CustomDeserializers.html +12 -5
- package/docs/types/CustomSerializers.html +12 -5
- package/docs/types/Later.html +12 -5
- package/docs/types/MapValuesFn.html +12 -5
- package/docs/types/MatchCallback.html +12 -5
- package/docs/types/SeqEarlyBreaker.html +12 -5
- package/docs/types/SeqFn.html +12 -5
- package/docs/types/SeqFunctions.html +12 -5
- package/docs/types/SetImmutablePath.html +12 -5
- package/docs/types/ThrottledFunction.html +12 -5
- package/docs/variables/mapValuesUNSET.html +12 -5
- package/docs/variables/mergeUNSET.html +12 -5
- package/esm/index.d.ts +7 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +7 -0
- package/esm/index.js.map +1 -1
- package/esm/omit.d.ts +1 -1
- package/esm/omit.d.ts.map +1 -1
- package/esm/omit.js.map +1 -1
- package/esm/replace.d.ts.map +1 -1
- package/esm/replace.js.map +1 -1
- package/esm/replaceDeep.d.ts +5 -1
- package/esm/replaceDeep.d.ts.map +1 -1
- package/esm/replaceDeep.js +3 -20
- package/esm/replaceDeep.js.map +1 -1
- package/esm/replaceDeepByFn.d.ts +7 -0
- package/esm/replaceDeepByFn.d.ts.map +1 -0
- package/esm/replaceDeepByFn.js +44 -0
- package/esm/replaceDeepByFn.js.map +1 -0
- package/esm/safe.d.ts.map +1 -1
- package/esm/safe.js.map +1 -1
- package/esm/sample.d.ts +3 -0
- package/esm/sample.d.ts.map +1 -0
- package/esm/sample.js +5 -0
- package/esm/sample.js.map +1 -0
- package/esm/samples.d.ts +3 -0
- package/esm/samples.d.ts.map +1 -0
- package/esm/samples.js +25 -0
- package/esm/samples.js.map +1 -0
- package/esm/serialize.d.ts.map +1 -1
- package/esm/serialize.js +56 -18
- package/esm/serialize.js.map +1 -1
- package/esm/shuffle.d.ts +3 -0
- package/esm/shuffle.d.ts.map +1 -0
- package/esm/shuffle.js +6 -0
- package/esm/shuffle.js.map +1 -0
- package/esm/toggle.d.ts +3 -0
- package/esm/toggle.d.ts.map +1 -0
- package/esm/toggle.js +12 -0
- package/esm/toggle.js.map +1 -0
- package/esm/trim.d.ts +3 -0
- package/esm/trim.d.ts.map +1 -0
- package/esm/trim.js +7 -0
- package/esm/trim.js.map +1 -0
- package/esm/trimEnd.d.ts +3 -0
- package/esm/trimEnd.d.ts.map +1 -0
- package/esm/trimEnd.js +9 -0
- package/esm/trimEnd.js.map +1 -0
- package/esm/trimStart.d.ts +3 -0
- package/esm/trimStart.d.ts.map +1 -0
- package/esm/trimStart.js +9 -0
- package/esm/trimStart.js.map +1 -0
- package/esm/utils/utils.d.ts +6 -0
- package/esm/utils/utils.d.ts.map +1 -0
- package/esm/utils/utils.js +7 -0
- package/esm/utils/utils.js.map +1 -0
- package/esm/waitFor.d.ts +7 -1
- package/esm/waitFor.d.ts.map +1 -1
- package/esm/waitFor.js +35 -10
- package/esm/waitFor.js.map +1 -1
- package/package.json +4 -4
- package/pnpm-lock.yaml +1162 -986
- package/src/deserialize.spec.ts +12 -0
- package/src/index.ts +7 -0
- package/src/omit.ts +8 -2
- package/src/pick.ts +1 -1
- package/src/replace.ts +0 -1
- package/src/replaceDeep.spec.ts +91 -0
- package/src/replaceDeep.ts +22 -27
- package/src/replaceDeepByFn.spec.ts +162 -0
- package/src/replaceDeepByFn.ts +93 -0
- package/src/safe.ts +0 -1
- package/src/sample.spec.ts +31 -0
- package/src/sample.ts +11 -0
- package/src/samples.spec.ts +50 -0
- package/src/samples.ts +41 -0
- package/src/serialize.spec.ts +42 -0
- package/src/serialize.ts +65 -18
- package/src/shuffle.spec.ts +39 -0
- package/src/shuffle.ts +13 -0
- package/src/toggle.spec.ts +43 -0
- package/src/toggle.ts +22 -0
- package/src/trim.spec.ts +22 -0
- package/src/trim.ts +23 -0
- package/src/trimEnd.spec.ts +20 -0
- package/src/trimEnd.ts +22 -0
- package/src/trimStart.spec.ts +20 -0
- package/src/trimStart.ts +22 -0
- package/src/utils/utils.ts +11 -0
- package/src/waitFor.spec.ts +141 -0
- package/src/waitFor.ts +69 -18
package/src/serialize.spec.ts
CHANGED
|
@@ -80,4 +80,46 @@ describe("serialize", () => {
|
|
|
80
80
|
|
|
81
81
|
must(serialize(a)).equal(serialize(b));
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
it("allows serializing dates and other objects containing .toJSON", async () => {
|
|
85
|
+
const date = new Date(1714390008941);
|
|
86
|
+
must(serialize(date, {
|
|
87
|
+
D: (value) => {
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
return String(value.getTime());
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
},
|
|
93
|
+
})).equal(`"D:1714390008941"`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should avoid excessive calls to custom serializers", async () => {
|
|
97
|
+
const customSerializers: CustomSerializers = {
|
|
98
|
+
p: (value) => {
|
|
99
|
+
// console.log("Called with", typeof value, value);
|
|
100
|
+
if (value instanceof Person) {
|
|
101
|
+
return value.name;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const p1 = new Person("John");
|
|
108
|
+
const spy = jest.spyOn(customSerializers, "p");
|
|
109
|
+
|
|
110
|
+
serialize({
|
|
111
|
+
a: [p1],
|
|
112
|
+
b: 1,
|
|
113
|
+
c: {
|
|
114
|
+
d: true,
|
|
115
|
+
e: p1,
|
|
116
|
+
},
|
|
117
|
+
}, customSerializers);
|
|
118
|
+
expect(spy).toHaveBeenCalledTimes(4);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should not crash on unknown instances but serialize as plain object", async () => {
|
|
122
|
+
const p1 = new Person("John");
|
|
123
|
+
must(serialize(p1)).equal(`{"name":"s:John"}`);
|
|
124
|
+
});
|
|
83
125
|
});
|
package/src/serialize.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { sortProps } from "./sortProps.js";
|
|
2
|
+
import { replaceDeepByFn } from "./replaceDeepByFn.js";
|
|
3
|
+
import { isPlainObject } from "./isPlainObject.js";
|
|
4
|
+
import { DataWrapper } from "./utils/utils.js";
|
|
2
5
|
|
|
3
6
|
type CustomSerializers = {
|
|
4
7
|
[key: string]: (data: unknown) => (string | null);
|
|
@@ -33,6 +36,32 @@ type Options = {
|
|
|
33
36
|
* @param options - options
|
|
34
37
|
*/
|
|
35
38
|
const serialize = (data: unknown, customSerializers?: CustomSerializers, options?: Options) => { // eslint-disable-line max-lines-per-function,max-len
|
|
39
|
+
const sourceData = Object.keys(customSerializers ?? {}).length
|
|
40
|
+
? replaceDeepByFn(
|
|
41
|
+
data,
|
|
42
|
+
value => {
|
|
43
|
+
if (["string", "number", "bigint", "undefined", "boolean"].includes(typeof value)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (value === null || Array.isArray(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (isPlainObject(value)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const serializerKeys = Object.keys(customSerializers ?? {});
|
|
54
|
+
for (const key of serializerKeys) {
|
|
55
|
+
const serialized = customSerializers![key]!(value);
|
|
56
|
+
if (typeof serialized === "string") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
},
|
|
62
|
+
value => new DataWrapper(value),
|
|
63
|
+
)
|
|
64
|
+
: data;
|
|
36
65
|
const replacer = (_key: string, value: unknown) => { // eslint-disable-line max-statements
|
|
37
66
|
if (typeof value === "string") {
|
|
38
67
|
return `s:${value}`;
|
|
@@ -52,12 +81,22 @@ const serialize = (data: unknown, customSerializers?: CustomSerializers, options
|
|
|
52
81
|
if (value === null) {
|
|
53
82
|
return "l:";
|
|
54
83
|
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value as unknown[];
|
|
86
|
+
}
|
|
87
|
+
if (isPlainObject(value)) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
55
90
|
const serializerKeys = Object.keys(customSerializers ?? {});
|
|
56
91
|
for (const key of serializerKeys) {
|
|
57
|
-
const serialized = customSerializers![key]!(value);
|
|
92
|
+
const serialized = customSerializers![key]!(value instanceof DataWrapper ? value.data : value);
|
|
58
93
|
if (typeof serialized === "string") {
|
|
59
94
|
return `${key}:${serialized}`;
|
|
60
95
|
}
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
97
|
+
if (serialized !== null) {
|
|
98
|
+
throw new Error(`Custom serializer for key ${key} returned a non-string value: ${String(serialized)}`);
|
|
99
|
+
}
|
|
61
100
|
}
|
|
62
101
|
if (typeof value === "object") {
|
|
63
102
|
return value;
|
|
@@ -67,35 +106,43 @@ const serialize = (data: unknown, customSerializers?: CustomSerializers, options
|
|
|
67
106
|
};
|
|
68
107
|
|
|
69
108
|
if (
|
|
70
|
-
|
|
71
|
-
|| typeof
|
|
72
|
-
|| typeof
|
|
73
|
-
|| typeof
|
|
74
|
-
|| typeof
|
|
75
|
-
|| typeof
|
|
76
|
-
|| Array.isArray(
|
|
109
|
+
sourceData == null
|
|
110
|
+
|| typeof sourceData === "string"
|
|
111
|
+
|| typeof sourceData === "number"
|
|
112
|
+
|| typeof sourceData === "bigint"
|
|
113
|
+
|| typeof sourceData === "undefined"
|
|
114
|
+
|| typeof sourceData === "boolean"
|
|
115
|
+
|| Array.isArray(sourceData)
|
|
77
116
|
) {
|
|
78
|
-
return JSON.stringify(
|
|
117
|
+
return JSON.stringify(sourceData, replacer);
|
|
79
118
|
}
|
|
80
119
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
if (!isPlainObject(sourceData)) {
|
|
121
|
+
const serializerKeysGlobal = Object.keys(customSerializers ?? {});
|
|
122
|
+
for (const key of serializerKeysGlobal) {
|
|
123
|
+
const serialized = customSerializers![key]!(
|
|
124
|
+
sourceData instanceof DataWrapper ? sourceData.data : sourceData,
|
|
125
|
+
);
|
|
126
|
+
if (typeof serialized === "string") {
|
|
127
|
+
return `"${key}:${serialized}"`;
|
|
128
|
+
}
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
130
|
+
if (serialized !== null) {
|
|
131
|
+
throw new Error(`Custom serializer for key ${key} returned a non-string value: ${String(serialized)}`);
|
|
132
|
+
}
|
|
86
133
|
}
|
|
87
134
|
}
|
|
88
135
|
|
|
89
|
-
if (typeof
|
|
136
|
+
if (typeof sourceData === "object") {
|
|
90
137
|
return JSON.stringify(
|
|
91
138
|
options?.sortProps === false
|
|
92
|
-
?
|
|
93
|
-
: sortProps(
|
|
139
|
+
? sourceData
|
|
140
|
+
: sortProps(sourceData as Record<string, unknown>),
|
|
94
141
|
replacer,
|
|
95
142
|
);
|
|
96
143
|
}
|
|
97
144
|
|
|
98
|
-
throw new Error(`Unsupported data type: ${typeof
|
|
145
|
+
throw new Error(`Unsupported data type: ${typeof sourceData}`);
|
|
99
146
|
};
|
|
100
147
|
|
|
101
148
|
export type { CustomSerializers };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
2
|
+
import must from "must";
|
|
3
|
+
|
|
4
|
+
import { shuffle } from "./shuffle";
|
|
5
|
+
|
|
6
|
+
describe("shuffle", () => {
|
|
7
|
+
it("should shuffle the array", async () => {
|
|
8
|
+
for (let i = 0; i < 100; i++) {
|
|
9
|
+
{
|
|
10
|
+
const elements = [1, 2, 3];
|
|
11
|
+
|
|
12
|
+
const result = shuffle(elements);
|
|
13
|
+
must(result).have.length(3);
|
|
14
|
+
|
|
15
|
+
for (const item of result) {
|
|
16
|
+
must(elements).include(item);
|
|
17
|
+
}
|
|
18
|
+
must([...new Set(result)]).have.length(3);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should shuffle the array with duplicates", async () => {
|
|
24
|
+
for (let i = 0; i < 100; i++) {
|
|
25
|
+
{
|
|
26
|
+
const elements = [1, 2, 2, 3];
|
|
27
|
+
|
|
28
|
+
const result = shuffle(elements);
|
|
29
|
+
must(result).have.length(4);
|
|
30
|
+
|
|
31
|
+
for (const item of result) {
|
|
32
|
+
must(elements).include(item);
|
|
33
|
+
}
|
|
34
|
+
must([...new Set(result)]).have.length(3);
|
|
35
|
+
must(result.filter(r => r === 2).length).equal(2);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/shuffle.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { samples } from "./samples.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shuffles an array, returning a new array.
|
|
5
|
+
* @param array source array
|
|
6
|
+
*/
|
|
7
|
+
const shuffle = <T>(array: T[]): T[] => {
|
|
8
|
+
return samples(array, array.length, true);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
shuffle,
|
|
13
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { toggle } from "./toggle.js";
|
|
2
|
+
|
|
3
|
+
describe("toggle", () => {
|
|
4
|
+
it("adds the value if it doesn't exist", async () => {
|
|
5
|
+
must(toggle([1], 2)).eql([1, 2]);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("removes the value if it exists", async () => {
|
|
9
|
+
must(toggle([1, 2], 2)).eql([1]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("removes only one occurrence", async () => {
|
|
13
|
+
must(toggle([1, 2, 2], 2)).eql([1, 2]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("modifies the original array when adding", async () => {
|
|
17
|
+
const array = [1];
|
|
18
|
+
toggle(array, 2);
|
|
19
|
+
must(array).eql([1, 2]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("modifies the original array when removing", async () => {
|
|
23
|
+
const array = [1, 2];
|
|
24
|
+
toggle(array, 2);
|
|
25
|
+
must(array).eql([1]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns the original array", async () => {
|
|
29
|
+
const array = [1];
|
|
30
|
+
// adding
|
|
31
|
+
must(toggle(array, 2)).equal(array);
|
|
32
|
+
// removing
|
|
33
|
+
must(toggle(array, 2)).equal(array);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("adds NaN values correctly", async () => {
|
|
37
|
+
must(toggle([1], NaN)).eql([1, NaN]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("removes NaN values correctly", async () => {
|
|
41
|
+
must(toggle([1, NaN], NaN)).eql([1]);
|
|
42
|
+
});
|
|
43
|
+
});
|
package/src/toggle.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggles an item in an array. If it exists, remove it. If it doesn't exist, add it.
|
|
3
|
+
* If the array contains the given value more than once, this function will remove only one occurrence.
|
|
4
|
+
*
|
|
5
|
+
* @param array - source array
|
|
6
|
+
* @param item - item to toggle
|
|
7
|
+
*/
|
|
8
|
+
const toggle = <T>(array: T[], item: T): T[] => {
|
|
9
|
+
const index = array.findIndex((i) => Object.is(i, item));
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
11
|
+
if (index === -1) {
|
|
12
|
+
array.push(item);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
array.splice(index, 1);
|
|
16
|
+
}
|
|
17
|
+
return array;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
toggle,
|
|
22
|
+
};
|
package/src/trim.spec.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { trim } from "./trim";
|
|
2
|
+
|
|
3
|
+
describe("trim", () => {
|
|
4
|
+
it("should trim single character from both sides of the string", async () => {
|
|
5
|
+
must(trim("aaacataaa", "a")).equal("cat");
|
|
6
|
+
must(trim("abacteria", "a")).equal("bacteri");
|
|
7
|
+
must(trim("abc", "a")).equal("bc");
|
|
8
|
+
must(trim("abc", "c")).equal("ab");
|
|
9
|
+
must(trim("aajjja", "a")).equal("jjj");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should trim multiple characters as a whole", async () => {
|
|
13
|
+
must(trim("abopopab", "ab")).equal("opop");
|
|
14
|
+
must(trim("atitleb", "ab")).equal("atitleb");
|
|
15
|
+
must(trim("abtitleab", "ab")).equal("title");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("can trim into empty string", async () => {
|
|
19
|
+
must(trim("abc", "abc")).equal("");
|
|
20
|
+
must(trim("a", "a")).equal("");
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/trim.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { trimStart } from "./trimStart.js";
|
|
2
|
+
import { trimEnd } from "./trimEnd.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Removes given characters from both sides of the string.
|
|
6
|
+
* If you want to remove from one side of the string see {@link trimStart} and {@link trimEnd}.
|
|
7
|
+
*
|
|
8
|
+
* @param source - Source string.
|
|
9
|
+
* @param characters - Characters to remove, taken as a whole.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* trim("abcb", "ab"); // "cb"
|
|
13
|
+
* trim("!aaa!", "!"); // "aaa"
|
|
14
|
+
* trim("!aaa!!!", "!"); // "aaa"
|
|
15
|
+
* trim("!aaa!!!", "!!!"); // "!aaa"
|
|
16
|
+
*/
|
|
17
|
+
const trim = (source: string, characters: string) => {
|
|
18
|
+
return trimStart(trimEnd(source, characters), characters);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
trim,
|
|
23
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { trimEnd } from "./trimEnd";
|
|
2
|
+
|
|
3
|
+
describe("trimEnd", () => {
|
|
4
|
+
it("should trim single character from the end of the string", async () => {
|
|
5
|
+
must(trimEnd("abc?", "?")).equal("abc");
|
|
6
|
+
must(trimEnd("a? b?c???", "?")).equal("a? b?c");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should trim multiple characters as a whole", async () => {
|
|
10
|
+
must(trimEnd("abc", "bc")).equal("a");
|
|
11
|
+
|
|
12
|
+
must(trimEnd("abcc", "bc")).equal("abcc");
|
|
13
|
+
must(trimEnd("abcb", "bc")).equal("abcb");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("can trim into empty string", async () => {
|
|
17
|
+
must(trimEnd("abc", "abc")).equal("");
|
|
18
|
+
must(trimEnd("a", "a")).equal("");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/trimEnd.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes given characters from the end of the string.
|
|
3
|
+
* See also: {@link trim} and {@link trimStart}.
|
|
4
|
+
*
|
|
5
|
+
* @param source - Source string.
|
|
6
|
+
* @param characters - Characters to remove, taken as a whole.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* trimEnd("abcxzyz", "yz"); // "abcxz"
|
|
10
|
+
* trimEnd("!aaa!!", "!"); // "!aaa"
|
|
11
|
+
*/
|
|
12
|
+
const trimEnd = (source: string, characters: string) => {
|
|
13
|
+
let s = source;
|
|
14
|
+
while (s.endsWith(characters)) {
|
|
15
|
+
s = s.slice(0, -characters.length);
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
trimEnd,
|
|
22
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { trimStart } from "./trimStart";
|
|
2
|
+
|
|
3
|
+
describe("trimStart", () => {
|
|
4
|
+
it("should trim single character from the start of the string", async () => {
|
|
5
|
+
must(trimStart("?abc", "?")).equal("abc");
|
|
6
|
+
must(trimStart("aaabc", "a")).equal("bc");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("should trim multiple characters as a whole", async () => {
|
|
10
|
+
must(trimStart("abc", "ab")).equal("c");
|
|
11
|
+
|
|
12
|
+
must(trimStart("aabcc", "ab")).equal("aabcc");
|
|
13
|
+
must(trimStart("babcb", "ab")).equal("babcb");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should trim into empty string if needed", async () => {
|
|
17
|
+
must(trimStart("abc", "abc")).equal("");
|
|
18
|
+
must(trimStart("aaa", "a")).equal("");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/trimStart.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes given characters from the start of the string.
|
|
3
|
+
* See also: {@link trim} and {@link trimEnd}.
|
|
4
|
+
*
|
|
5
|
+
* @param source - Source string.
|
|
6
|
+
* @param characters - Characters to remove, taken as a whole.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* trimStart("abbcb", "ab"); // "bcb"
|
|
10
|
+
* trimStart("!!aaa!", "!"); // "aaa!"
|
|
11
|
+
*/
|
|
12
|
+
const trimStart = (source: string, characters: string) => {
|
|
13
|
+
let s = source;
|
|
14
|
+
while (s.startsWith(characters)) {
|
|
15
|
+
s = s.slice(characters.length);
|
|
16
|
+
}
|
|
17
|
+
return s;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
trimStart,
|
|
22
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
2
|
+
import must from "must";
|
|
3
|
+
|
|
4
|
+
import createSpy from "../test/createSpy";
|
|
5
|
+
|
|
6
|
+
import { waitFor } from "./waitFor";
|
|
7
|
+
|
|
8
|
+
describe("waitFor", () => {
|
|
9
|
+
it("calls function multiple times until it returns truthy value", async () => {
|
|
10
|
+
let calls = 0;
|
|
11
|
+
const spy = createSpy(() => {
|
|
12
|
+
calls++;
|
|
13
|
+
return calls === 3;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const start = Date.now();
|
|
17
|
+
await waitFor(spy);
|
|
18
|
+
|
|
19
|
+
must(calls).equal(3);
|
|
20
|
+
// waited for 3 tries, should be 100ms (0 for first try) in total
|
|
21
|
+
must(Date.now() - start).be.gte(100);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns value if check passes", async () => {
|
|
25
|
+
const x = await waitFor(() => 5);
|
|
26
|
+
must(x).equal(5);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns value if check passes in non-first try", async () => {
|
|
30
|
+
let calls = 0;
|
|
31
|
+
const spy = createSpy(() => {
|
|
32
|
+
calls++;
|
|
33
|
+
return calls === 3;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
37
|
+
const res = await waitFor(spy);
|
|
38
|
+
must(res).equal(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("allows to adjust interval", async () => {
|
|
42
|
+
let calls = 0;
|
|
43
|
+
const spy = createSpy(() => {
|
|
44
|
+
calls++;
|
|
45
|
+
return calls === 3;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
await waitFor(spy, { interval: 150 });
|
|
50
|
+
|
|
51
|
+
must(calls).equal(3);
|
|
52
|
+
must(Date.now() - start).be.gte(300);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("time outs after given time", async () => {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
57
|
+
await must(waitFor(() => null, {
|
|
58
|
+
interval: 40,
|
|
59
|
+
timeout: 300,
|
|
60
|
+
})).reject.to.instanceOf(Error);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("time outs after given amout of retries", async () => {
|
|
64
|
+
const spy = createSpy(() => false);
|
|
65
|
+
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
67
|
+
await must(waitFor(spy, {
|
|
68
|
+
interval: 40,
|
|
69
|
+
maxTries: 3,
|
|
70
|
+
})).reject.to.instanceOf(Error);
|
|
71
|
+
|
|
72
|
+
must(spy.__spy.calls).have.length(3);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("validates max tries", async () => {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
77
|
+
await must(waitFor(() => null, {
|
|
78
|
+
maxTries: 0,
|
|
79
|
+
})).reject.to.instanceOf(TypeError);
|
|
80
|
+
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
82
|
+
await must(waitFor(() => null, {
|
|
83
|
+
maxTries: -666,
|
|
84
|
+
})).reject.to.instanceOf(TypeError);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("crashes if check function crashes", async () => {
|
|
88
|
+
await waitFor(() => {
|
|
89
|
+
throw new Error(5);
|
|
90
|
+
}, 40).then(() => {
|
|
91
|
+
throw new Error("Should not resolve");
|
|
92
|
+
}, (e) => {
|
|
93
|
+
must(e).instanceOf(Error);
|
|
94
|
+
must(e.message).equal("[waitFor] check function threw an error");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("treats most falsy values as succeeded check", () => {
|
|
99
|
+
it("numeric zero", async () => {
|
|
100
|
+
const result = await waitFor(() => 0);
|
|
101
|
+
must(result).equal(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("empty string", async () => {
|
|
105
|
+
const result = await waitFor(() => "");
|
|
106
|
+
must(result).equal("");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("supports promises", () => {
|
|
111
|
+
it("truthy value", async () => {
|
|
112
|
+
const result = await waitFor(() => true);
|
|
113
|
+
must(result).be.true();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("by retrying if Promise returns false", async () => {
|
|
117
|
+
let calls = 0;
|
|
118
|
+
const spy = createSpy(() => {
|
|
119
|
+
calls++;
|
|
120
|
+
return calls === 3;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
124
|
+
const result = await waitFor(spy);
|
|
125
|
+
must(result).be.true();
|
|
126
|
+
must(calls).equal(3);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("by crashing if check function returns rejected promise", async () => {
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
return Promise.reject(new Error("oops"));
|
|
132
|
+
}, 40).then(() => {
|
|
133
|
+
throw new Error("Should not resolve");
|
|
134
|
+
}, (e) => {
|
|
135
|
+
must(e).instanceOf(Error);
|
|
136
|
+
must(e.message).equal("[waitFor] check function threw an error");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|