@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/deserialize.spec.ts
CHANGED
|
@@ -66,4 +66,16 @@ describe("deserialize", () => {
|
|
|
66
66
|
it("throws on unknown data type", () => {
|
|
67
67
|
must(() => deserialize(`"v:test"`)).throw("Unsupported data type: v");
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it("supports deserializers with things like Date", async () => {
|
|
71
|
+
const customSerializers: CustomDeserializers = {
|
|
72
|
+
D: (value) => {
|
|
73
|
+
return new Date(Number(value));
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const result = deserialize(`"D:1714398481437"`, customSerializers);
|
|
78
|
+
must(result).be.instanceof(Date);
|
|
79
|
+
must(result.getTime()).equal(1714398481437);
|
|
80
|
+
});
|
|
69
81
|
});
|
package/src/index.ts
CHANGED
|
@@ -38,16 +38,23 @@ export * from "./replaceDeep.js";
|
|
|
38
38
|
export * from "./rethrow.js";
|
|
39
39
|
export * from "./round.js";
|
|
40
40
|
export * from "./safe.js";
|
|
41
|
+
export * from "./sample.js";
|
|
42
|
+
export * from "./samples.js";
|
|
41
43
|
export * from "./scale.js";
|
|
42
44
|
export * from "./seq.js";
|
|
43
45
|
export * from "./serialize.js";
|
|
44
46
|
export * from "./set.js";
|
|
45
47
|
export * from "./setImmutable.js";
|
|
48
|
+
export * from "./shuffle.js";
|
|
46
49
|
export * from "./sortBy.js";
|
|
47
50
|
export * from "./sortProps.js";
|
|
48
51
|
export * from "./stripPrefix.js";
|
|
49
52
|
export * from "./stripSuffix.js";
|
|
50
53
|
export * from "./throttle.js";
|
|
54
|
+
export * from "./toggle.js";
|
|
55
|
+
export * from "./trim.js";
|
|
56
|
+
export * from "./trimEnd.js";
|
|
57
|
+
export * from "./trimStart.js";
|
|
51
58
|
export * from "./truthy.js";
|
|
52
59
|
export * from "./unique.js";
|
|
53
60
|
export * from "./wait.js";
|
package/src/omit.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// TODO verify & maybe fix typings when object is an Array
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Returns
|
|
4
|
+
* Returns a cloned source object but without specified properties.
|
|
5
|
+
*
|
|
6
|
+
* TypeScript tip: if you want to omit properties that TS think does not exist in the object, call the function like
|
|
7
|
+
* that:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* omit<Record<string, unknown>>(source, ["property"]);
|
|
10
|
+
* ```
|
|
5
11
|
*
|
|
6
12
|
* @param {Object} object - source object
|
|
7
13
|
* @param {Array.<string>} props - properties to skip
|
|
@@ -13,7 +19,7 @@
|
|
|
13
19
|
* // { 1: "world" }
|
|
14
20
|
* @returns {Object} - new object without given properties
|
|
15
21
|
*/
|
|
16
|
-
const omit = <T extends object, K extends keyof T>(
|
|
22
|
+
const omit = <T extends object, K extends keyof T = keyof T>(
|
|
17
23
|
object: T | null, props: K[],
|
|
18
24
|
): T extends null ? { [key: string]: never } : Omit<T, K> => {
|
|
19
25
|
if (!object || (typeof object !== "object" && typeof object !== "function")) {
|
package/src/pick.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// TODO verify & maybe fix typings when object is an Array
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Returns new object with
|
|
4
|
+
* Returns a new object with given properties copied from a source object.
|
|
5
5
|
*
|
|
6
6
|
* @param {Object} object - source object
|
|
7
7
|
* @param {Array.<string>} props - properties to copy
|
package/src/replace.ts
CHANGED
|
@@ -12,7 +12,6 @@ const replace = (source: string, replaceMap: Record<string, string>) => {
|
|
|
12
12
|
if (keys.length === 0) {
|
|
13
13
|
return source;
|
|
14
14
|
}
|
|
15
|
-
/* eslint-enable max-len */
|
|
16
15
|
const regex = new RegExp(keys.map(escapeRegExp).join("|"), "g");
|
|
17
16
|
return source.replace(regex, (matched) => replaceMap[matched]!);
|
|
18
17
|
};
|
package/src/replaceDeep.spec.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { replaceDeep } from "./replaceDeep";
|
|
2
2
|
|
|
3
|
+
class MyClass {
|
|
4
|
+
public value: number;
|
|
5
|
+
|
|
6
|
+
public constructor(value: number) {
|
|
7
|
+
this.value = value;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
describe("replaceDeep", () => {
|
|
4
12
|
it("should replace given value in a deep object", () => {
|
|
5
13
|
const source = [
|
|
@@ -68,4 +76,87 @@ describe("replaceDeep", () => {
|
|
|
68
76
|
b: 123,
|
|
69
77
|
});
|
|
70
78
|
});
|
|
79
|
+
|
|
80
|
+
it("should allow to replace nils", async () => {
|
|
81
|
+
must(replaceDeep(null, null, 300)).equal(300);
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
|
83
|
+
must(replaceDeep(undefined, undefined, 300)).equal(300);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not mutate original object", () => {
|
|
87
|
+
const source = {
|
|
88
|
+
a: 1,
|
|
89
|
+
b: {
|
|
90
|
+
c: 2,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const result = replaceDeep(source, 2, 3);
|
|
94
|
+
must(result).not.equal(source);
|
|
95
|
+
must(result.b).not.equal(source.b);
|
|
96
|
+
must(result).eql({
|
|
97
|
+
a: 1,
|
|
98
|
+
b: {
|
|
99
|
+
c: 3,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("does not mutate original array", async () => {
|
|
105
|
+
const source = [1, 2, 3, [2, 3]];
|
|
106
|
+
const result = replaceDeep(source, 2, 4);
|
|
107
|
+
must(result).not.equal(source);
|
|
108
|
+
must(result[3]).not.equal(source[3]);
|
|
109
|
+
must(result).eql([1, 4, 3, [4, 3]]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("mutates original object when enabled", () => {
|
|
113
|
+
const source = {
|
|
114
|
+
a: 1,
|
|
115
|
+
b: {
|
|
116
|
+
c: 2,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const result = replaceDeep(source, 2, 3, { mutate: true });
|
|
120
|
+
must(result).equal(source);
|
|
121
|
+
must(result.b).equal(source.b);
|
|
122
|
+
must(result).eql({
|
|
123
|
+
a: 1,
|
|
124
|
+
b: {
|
|
125
|
+
c: 3,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("mutates original array when enabled", async () => {
|
|
131
|
+
const source = [1, 2, 3, [2, 3]];
|
|
132
|
+
const result = replaceDeep(source, 2, 4, { mutate: true });
|
|
133
|
+
must(result).equal(source);
|
|
134
|
+
must(result[3]).equal(source[3]);
|
|
135
|
+
must(result).eql([1, 4, 3, [4, 3]]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("does not get into instances by default", async () => {
|
|
139
|
+
const source = new MyClass(100);
|
|
140
|
+
const result = replaceDeep(source, 100, 200);
|
|
141
|
+
must(result).equal(source);
|
|
142
|
+
must(source.value).equal(100);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("gets into instances when allowed", async () => {
|
|
146
|
+
const source = new MyClass(100);
|
|
147
|
+
const result = replaceDeep(source, 100, 200, {
|
|
148
|
+
replaceInstancesProps: true,
|
|
149
|
+
mutate: true,
|
|
150
|
+
});
|
|
151
|
+
must(result).equal(source);
|
|
152
|
+
must(source.value).equal(200);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("requires `mutate` option if `replaceInstancesProps` is defined", async () => {
|
|
156
|
+
const source = new MyClass(100);
|
|
157
|
+
must(() => replaceDeep(source, 100, 200, {
|
|
158
|
+
replaceInstancesProps: true,
|
|
159
|
+
mutate: false,
|
|
160
|
+
})).throw("`replaceInstancesProps` option requires `mutate` to be enabled");
|
|
161
|
+
});
|
|
71
162
|
});
|
package/src/replaceDeep.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
import { replaceDeepByFn } from "./replaceDeepByFn.js";
|
|
2
|
+
|
|
3
|
+
type Options = {
|
|
4
|
+
/**
|
|
5
|
+
* If true, the source objects and arrays will be mutated. Default is false.
|
|
6
|
+
*/
|
|
7
|
+
mutate?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* If true, the function will go into instances for replacement. Otherwise, it will only replace properties of plain
|
|
10
|
+
* objects. Default is false.
|
|
11
|
+
* Warning: This option requires `mutate` to be enabled, because we can't clone instances.
|
|
12
|
+
*/
|
|
13
|
+
replaceInstancesProps?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
1
16
|
/**
|
|
2
17
|
* Replaces all occurrences of `search` with `value` in `source` object/array. Comparison is done with `Object.is`.
|
|
3
18
|
* If `source` is exactly the `search` a `value` will be returned. It does not do a substring replacements.
|
|
4
19
|
*
|
|
5
|
-
*
|
|
20
|
+
* Warnings:
|
|
21
|
+
* - By default, it does not mutate the `source`/deep objects/arrays, but it can be enabled with `mutate` option.
|
|
22
|
+
* - By default, it does not go into instances for replacement, only plain objects.
|
|
23
|
+
* - If your instances are cross-referenced, you may end up in an infinite loop.
|
|
6
24
|
*
|
|
7
25
|
* TypeScript users: This is way too dynamic to type properly, therefore, typing assumes the most basic form of
|
|
8
26
|
* replacement where search and value are of the same type. If that's not the case for you - you'll have to typecast.
|
|
@@ -10,33 +28,10 @@
|
|
|
10
28
|
* @param source - source object/array/value
|
|
11
29
|
* @param search - value to search for
|
|
12
30
|
* @param value - value to replace with
|
|
31
|
+
* @param options - optional options
|
|
13
32
|
*/
|
|
14
|
-
const replaceDeep = <T>(source: T, search: unknown, value: unknown): T => {
|
|
15
|
-
|
|
16
|
-
return value as T;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (source == null) {
|
|
20
|
-
return source;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (typeof source === "object") {
|
|
24
|
-
if (Array.isArray(source)) {
|
|
25
|
-
for (let i = 0; i < source.length; i++) {
|
|
26
|
-
// eslint-disable-next-line no-param-reassign,@typescript-eslint/no-unsafe-assignment
|
|
27
|
-
source[i] = replaceDeep(source[i], search, value);
|
|
28
|
-
}
|
|
29
|
-
return source;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return Object.keys(source).reduce<Record<string, unknown>>((acc, key) => {
|
|
33
|
-
// eslint-disable-next-line no-param-reassign
|
|
34
|
-
acc[key] = replaceDeep((source as Record<string, unknown>)[key], search, value);
|
|
35
|
-
return acc;
|
|
36
|
-
}, {}) as T;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return source;
|
|
33
|
+
const replaceDeep = <T>(source: T, search: unknown, value: unknown, options?: Options): T => {
|
|
34
|
+
return replaceDeepByFn(source, (v) => Object.is(v, search), () => value, options);
|
|
40
35
|
};
|
|
41
36
|
|
|
42
37
|
export {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { replaceDeepByFn } from "./replaceDeepByFn";
|
|
2
|
+
|
|
3
|
+
class MyClass {
|
|
4
|
+
public value: number;
|
|
5
|
+
|
|
6
|
+
public constructor(value: number) {
|
|
7
|
+
this.value = value;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("replaceDeepByFn", () => {
|
|
12
|
+
it("should replace given value in a deep object", () => {
|
|
13
|
+
const source = [
|
|
14
|
+
99,
|
|
15
|
+
100,
|
|
16
|
+
{
|
|
17
|
+
favouriteBook: {
|
|
18
|
+
title: "The Ring of The Lord",
|
|
19
|
+
price: 100,
|
|
20
|
+
},
|
|
21
|
+
otherBooks: [
|
|
22
|
+
{
|
|
23
|
+
title: "Parry Hotter",
|
|
24
|
+
price: 50,
|
|
25
|
+
tag: "100",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: "The Hobbyte 100",
|
|
29
|
+
price: [100],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
must(replaceDeepByFn(source, v => v === 100, () => 200)).eql([
|
|
36
|
+
99,
|
|
37
|
+
200,
|
|
38
|
+
{
|
|
39
|
+
favouriteBook: {
|
|
40
|
+
title: "The Ring of The Lord",
|
|
41
|
+
price: 200,
|
|
42
|
+
},
|
|
43
|
+
otherBooks: [
|
|
44
|
+
{
|
|
45
|
+
title: "Parry Hotter",
|
|
46
|
+
price: 50,
|
|
47
|
+
tag: "100",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: "The Hobbyte 100",
|
|
51
|
+
price: [200],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should leave primitives as-is unless they equal to the search value", () => {
|
|
59
|
+
must(replaceDeepByFn(100, v => v === 200, () => 300)).equal(100);
|
|
60
|
+
must(replaceDeepByFn(200, v => v === 200, () => 300)).equal(300);
|
|
61
|
+
must(replaceDeepByFn("100", v => v === 200, () => 300)).equal("100");
|
|
62
|
+
// ESLINT BUG: (see replaceDeep.spec.ts)
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
|
64
|
+
must(replaceDeepByFn(undefined, v => v === 200, () => 300)).equal(undefined);
|
|
65
|
+
must(replaceDeepByFn(null, v => v === 200, () => 300)).equal(null);
|
|
66
|
+
must(replaceDeepByFn(true, v => v === 200, () => 300)).equal(true);
|
|
67
|
+
must(replaceDeepByFn(666n, v => v === 200, () => 300)).equal(666n);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should work with nans", async () => {
|
|
71
|
+
must(replaceDeepByFn({
|
|
72
|
+
a: NaN,
|
|
73
|
+
b: 123,
|
|
74
|
+
}, v => Number.isNaN(v), () => 300)).eql({
|
|
75
|
+
a: 300,
|
|
76
|
+
b: 123,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should allow to replace nils", async () => {
|
|
81
|
+
must(replaceDeepByFn(null, v => v === null, () => 300)).equal(300);
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
|
83
|
+
must(replaceDeepByFn(undefined, v => v === undefined, () => 300)).equal(300);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not mutate the original object", () => {
|
|
87
|
+
const source = {
|
|
88
|
+
a: 1,
|
|
89
|
+
b: {
|
|
90
|
+
c: 2,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const result = replaceDeepByFn(source, v => v === 2, () => 3);
|
|
94
|
+
must(result).not.equal(source);
|
|
95
|
+
must(result.b).not.equal(source.b);
|
|
96
|
+
must(result).eql({
|
|
97
|
+
a: 1,
|
|
98
|
+
b: {
|
|
99
|
+
c: 3,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("does not mutate original array", async () => {
|
|
105
|
+
const source = [1, 2, 3, [2, 3]];
|
|
106
|
+
const result = replaceDeepByFn(source, v => v === 2, () => 4);
|
|
107
|
+
must(result).not.equal(source);
|
|
108
|
+
must(result[3]).not.equal(source[3]);
|
|
109
|
+
must(result).eql([1, 4, 3, [4, 3]]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("mutates the original object when enabled", () => {
|
|
113
|
+
const source = {
|
|
114
|
+
a: 1,
|
|
115
|
+
b: {
|
|
116
|
+
c: 2,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const result = replaceDeepByFn(source, v => v === 2, () => 3, { mutate: true });
|
|
120
|
+
must(result).equal(source);
|
|
121
|
+
must(result.b).equal(source.b);
|
|
122
|
+
must(result).eql({
|
|
123
|
+
a: 1,
|
|
124
|
+
b: {
|
|
125
|
+
c: 3,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("mutates original array when enabled", async () => {
|
|
131
|
+
const source = [1, 2, 3, [2, 3]];
|
|
132
|
+
const result = replaceDeepByFn(source, v => v === 2, () => 4, { mutate: true });
|
|
133
|
+
must(result).equal(source);
|
|
134
|
+
must(result[3]).equal(source[3]);
|
|
135
|
+
must(result).eql([1, 4, 3, [4, 3]]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("does not get into instances by default", async () => {
|
|
139
|
+
const source = new MyClass(100);
|
|
140
|
+
const result = replaceDeepByFn(source, v => v === 100, () => 200);
|
|
141
|
+
must(result).equal(source);
|
|
142
|
+
must(source.value).equal(100);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("gets into instances when allowed", async () => {
|
|
146
|
+
const source = new MyClass(100);
|
|
147
|
+
const result = replaceDeepByFn(source, v => v === 100, () => 200, {
|
|
148
|
+
replaceInstancesProps: true,
|
|
149
|
+
mutate: true,
|
|
150
|
+
});
|
|
151
|
+
must(result).equal(source);
|
|
152
|
+
must(source.value).equal(200);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("requires `mutate` option if `replaceInstancesProps` is defined", async () => {
|
|
156
|
+
const source = new MyClass(100);
|
|
157
|
+
must(() => replaceDeepByFn(source, v => v === 100, () => 200, {
|
|
158
|
+
replaceInstancesProps: true,
|
|
159
|
+
mutate: false,
|
|
160
|
+
})).throw("`replaceInstancesProps` option requires `mutate` to be enabled");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { isPlainObject } from "./isPlainObject";
|
|
2
|
+
|
|
3
|
+
type Options = {
|
|
4
|
+
/**
|
|
5
|
+
* If true, the source objects and arrays will be mutated. Default is false.
|
|
6
|
+
*/
|
|
7
|
+
mutate?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* If true, the function will go into instances for replacement. Otherwise, it will only replace properties of plain
|
|
10
|
+
* objects. Default is false.
|
|
11
|
+
* Warning: This option requires `mutate` to be enabled, because we can't clone instances.
|
|
12
|
+
*/
|
|
13
|
+
replaceInstancesProps?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Replaces all occurrences of `search` with `value` in `source` object/array. You do comparison by yourself by providing a callback function.
|
|
18
|
+
*
|
|
19
|
+
* Warnings:
|
|
20
|
+
* - By default, it does not mutate the `source`/deep objects/arrays, but it can be enabled with `mutate` option.
|
|
21
|
+
* - By default, it does not go into instances for replacement, only plain objects.
|
|
22
|
+
* - If your instances are cross-referenced, you may end up in an infinite loop.
|
|
23
|
+
*
|
|
24
|
+
* TypeScript users: This is way too dynamic to type properly, therefore, typing assumes the most basic form of
|
|
25
|
+
* replacement where search and value are of the same type. If that's not the case for you - you'll have to typecast.
|
|
26
|
+
*
|
|
27
|
+
* @param source - source object/array/value
|
|
28
|
+
* @param search - value to search for
|
|
29
|
+
* @param replaceWith - value to replace with
|
|
30
|
+
* @param options - optional options
|
|
31
|
+
*/
|
|
32
|
+
const replaceDeepByFn = <T>( // eslint-disable-line max-statements
|
|
33
|
+
source: T, search: (value: unknown) => boolean, replaceWith: (value: unknown) => unknown, options?: Options,
|
|
34
|
+
): T => {
|
|
35
|
+
if (options?.replaceInstancesProps && !options.mutate) {
|
|
36
|
+
throw new Error("`replaceInstancesProps` option requires `mutate` to be enabled");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const searchResult = search(source);
|
|
40
|
+
|
|
41
|
+
if (typeof searchResult !== "boolean") {
|
|
42
|
+
throw new Error("search function must return a boolean");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (searchResult) {
|
|
46
|
+
return replaceWith(source) as T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (source == null) {
|
|
50
|
+
return source;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof source === "object") {
|
|
54
|
+
if (Array.isArray(source)) {
|
|
55
|
+
if (options?.mutate) {
|
|
56
|
+
for (let i = 0; i < source.length; i++) {
|
|
57
|
+
// eslint-disable-next-line no-param-reassign,@typescript-eslint/no-unsafe-assignment
|
|
58
|
+
source[i] = replaceDeepByFn(source[i], search, replaceWith, options);
|
|
59
|
+
}
|
|
60
|
+
return source;
|
|
61
|
+
}
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
63
|
+
return source.map((item) => replaceDeepByFn(item, search, replaceWith, options)) as unknown as T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options?.mutate) {
|
|
67
|
+
if (isPlainObject(source) || options.replaceInstancesProps) {
|
|
68
|
+
for (const key of Object.keys(source)) {
|
|
69
|
+
// eslint-disable-next-line no-param-reassign
|
|
70
|
+
(source as Record<string, unknown>)[key] = replaceDeepByFn(
|
|
71
|
+
(source as Record<string, unknown>)[key], search, replaceWith, options,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return source;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isPlainObject(source)) {
|
|
80
|
+
return Object.keys(source).reduce<Record<string, unknown>>((acc, key) => {
|
|
81
|
+
// eslint-disable-next-line no-param-reassign
|
|
82
|
+
acc[key] = replaceDeepByFn((source as Record<string, unknown>)[key], search, replaceWith, options);
|
|
83
|
+
return acc;
|
|
84
|
+
}, {}) as T;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return source;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
replaceDeepByFn,
|
|
93
|
+
};
|
package/src/safe.ts
CHANGED
|
@@ -11,7 +11,6 @@ function safe<T, Y>(fn: () => T, def: Y): T | Y;
|
|
|
11
11
|
* safe(() => trySomethingComplicated(), defaultValue); // if trySomethingComplicated throws, defaultValue will be returned
|
|
12
12
|
*/
|
|
13
13
|
function safe<T, Y>(fn: () => T, def?: Y) { // eslint-disable-line func-style
|
|
14
|
-
/* eslint-enable max-len */
|
|
15
14
|
try {
|
|
16
15
|
return fn();
|
|
17
16
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
2
|
+
import must from "must";
|
|
3
|
+
|
|
4
|
+
import { sample } from "./sample";
|
|
5
|
+
|
|
6
|
+
describe("sample", () => {
|
|
7
|
+
it("should return any random item of an array", async () => {
|
|
8
|
+
const array = [1, 2, 3, 4, 5];
|
|
9
|
+
const pickSet = new Set<number>();
|
|
10
|
+
for (let i = 0; i < 1000; i++) {
|
|
11
|
+
const item = sample(array);
|
|
12
|
+
must(array).include(item);
|
|
13
|
+
pickSet.add(item);
|
|
14
|
+
}
|
|
15
|
+
must(pickSet.size).equal(5);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should work with 1 item array", async () => {
|
|
19
|
+
const array = [1];
|
|
20
|
+
for (let i = 0; i < 1000; i++) {
|
|
21
|
+
const item = sample(array);
|
|
22
|
+
must(item).equal(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should work with 0 items array", async () => {
|
|
27
|
+
const array: never[] = [];
|
|
28
|
+
const item = sample(array);
|
|
29
|
+
must(item).equal(undefined);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/src/sample.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
2
|
+
import must from "must";
|
|
3
|
+
|
|
4
|
+
import { samples } from "./samples";
|
|
5
|
+
|
|
6
|
+
describe("samples", () => {
|
|
7
|
+
it("must pick random elements", async () => {
|
|
8
|
+
const elements = [1, 2, 3];
|
|
9
|
+
|
|
10
|
+
const result = samples(elements, 2);
|
|
11
|
+
must(result).have.length(2);
|
|
12
|
+
|
|
13
|
+
for (const item of result) {
|
|
14
|
+
must(elements).include(item);
|
|
15
|
+
}
|
|
16
|
+
must([...new Set(result)]).have.length(2);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should be able to pick one element", async () => {
|
|
20
|
+
const elements = [1, 2, 3];
|
|
21
|
+
const result = samples(elements, 1);
|
|
22
|
+
must(result).have.length(1);
|
|
23
|
+
must(elements).include(result[0]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should return empty array if no elements to pick", async () => {
|
|
27
|
+
const elements = [1, 2, 3];
|
|
28
|
+
const result = samples(elements, 0);
|
|
29
|
+
must(result).be.empty();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should return all elements if elements to pick is greater than array length", async () => {
|
|
33
|
+
const elements = [1, 2, 3];
|
|
34
|
+
const result = samples(elements, 5);
|
|
35
|
+
must(result).have.length(3);
|
|
36
|
+
must(result).eql(elements);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return all elements if elements to pick is equal to array length", async () => {
|
|
40
|
+
const elements = [1, 2, 3];
|
|
41
|
+
const result = samples(elements, 3);
|
|
42
|
+
must(result).have.length(3);
|
|
43
|
+
must(result).eql(elements);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should throw if elements to pick is negative", async () => {
|
|
47
|
+
const elements = [1, 2, 3];
|
|
48
|
+
must(() => samples(elements, -1)).throw();
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/samples.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picks a given number of random elements from the array. It won't pick the same element twice (unless the array has
|
|
3
|
+
* duplicates).
|
|
4
|
+
*
|
|
5
|
+
* If the number of elements to pick is greater than the array length, it will return the original array.
|
|
6
|
+
*
|
|
7
|
+
* @param array - source array
|
|
8
|
+
* @param elementsToPick - number of elements to pick
|
|
9
|
+
* @param allowShuffle - if true, it will shuffle the values if elementsToPick is greater or equal to array length instead of returning the original array
|
|
10
|
+
*/
|
|
11
|
+
const samples = <T>(array: T[], elementsToPick: number, allowShuffle = false): T[] => { // eslint-disable-line max-statements,max-len
|
|
12
|
+
if (elementsToPick < 0) {
|
|
13
|
+
throw new Error("elementsToPick must be a positive number");
|
|
14
|
+
}
|
|
15
|
+
if (elementsToPick === 0) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!allowShuffle && elementsToPick >= array.length) {
|
|
20
|
+
return array;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const keys = Object.keys(array);
|
|
24
|
+
let picked = 0;
|
|
25
|
+
const result: T[] = [];
|
|
26
|
+
|
|
27
|
+
while (picked < elementsToPick) {
|
|
28
|
+
const indexOfKey = Math.floor(Math.random() * keys.length);
|
|
29
|
+
const indexOfArray = Number(keys[indexOfKey]);
|
|
30
|
+
const element = array[indexOfArray]!;
|
|
31
|
+
result.push(element);
|
|
32
|
+
keys.splice(indexOfKey, 1);
|
|
33
|
+
picked++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
samples,
|
|
41
|
+
};
|