@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.
Files changed (236) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/index.d.ts +7 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +7 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/omit.d.ts +1 -1
  7. package/dist/omit.d.ts.map +1 -1
  8. package/dist/omit.js.map +1 -1
  9. package/dist/replace.d.ts.map +1 -1
  10. package/dist/replace.js.map +1 -1
  11. package/dist/replaceDeep.d.ts +5 -1
  12. package/dist/replaceDeep.d.ts.map +1 -1
  13. package/dist/replaceDeep.js +3 -20
  14. package/dist/replaceDeep.js.map +1 -1
  15. package/dist/replaceDeepByFn.d.ts +7 -0
  16. package/dist/replaceDeepByFn.d.ts.map +1 -0
  17. package/dist/replaceDeepByFn.js +47 -0
  18. package/dist/replaceDeepByFn.js.map +1 -0
  19. package/dist/safe.d.ts.map +1 -1
  20. package/dist/safe.js.map +1 -1
  21. package/dist/sample.d.ts +3 -0
  22. package/dist/sample.d.ts.map +1 -0
  23. package/dist/sample.js +8 -0
  24. package/dist/sample.js.map +1 -0
  25. package/dist/samples.d.ts +3 -0
  26. package/dist/samples.d.ts.map +1 -0
  27. package/dist/samples.js +28 -0
  28. package/dist/samples.js.map +1 -0
  29. package/dist/serialize.d.ts.map +1 -1
  30. package/dist/serialize.js +56 -18
  31. package/dist/serialize.js.map +1 -1
  32. package/dist/shuffle.d.ts +3 -0
  33. package/dist/shuffle.d.ts.map +1 -0
  34. package/dist/shuffle.js +9 -0
  35. package/dist/shuffle.js.map +1 -0
  36. package/dist/toggle.d.ts +3 -0
  37. package/dist/toggle.d.ts.map +1 -0
  38. package/dist/toggle.js +15 -0
  39. package/dist/toggle.js.map +1 -0
  40. package/dist/trim.d.ts +3 -0
  41. package/dist/trim.d.ts.map +1 -0
  42. package/dist/trim.js +10 -0
  43. package/dist/trim.js.map +1 -0
  44. package/dist/trimEnd.d.ts +3 -0
  45. package/dist/trimEnd.d.ts.map +1 -0
  46. package/dist/trimEnd.js +12 -0
  47. package/dist/trimEnd.js.map +1 -0
  48. package/dist/trimStart.d.ts +3 -0
  49. package/dist/trimStart.d.ts.map +1 -0
  50. package/dist/trimStart.js +12 -0
  51. package/dist/trimStart.js.map +1 -0
  52. package/dist/utils/utils.d.ts +6 -0
  53. package/dist/utils/utils.d.ts.map +1 -0
  54. package/dist/utils/utils.js +10 -0
  55. package/dist/utils/utils.js.map +1 -0
  56. package/dist/waitFor.d.ts +7 -1
  57. package/dist/waitFor.d.ts.map +1 -1
  58. package/dist/waitFor.js +35 -10
  59. package/dist/waitFor.js.map +1 -1
  60. package/docs/assets/search.js +1 -1
  61. package/docs/functions/cap.html +12 -5
  62. package/docs/functions/capitalize.html +12 -5
  63. package/docs/functions/coalesce.html +12 -5
  64. package/docs/functions/compareArrays.html +12 -5
  65. package/docs/functions/compareProps.html +12 -5
  66. package/docs/functions/deserialize.html +12 -5
  67. package/docs/functions/ensureArray.html +12 -5
  68. package/docs/functions/ensureDate.html +12 -5
  69. package/docs/functions/ensureError.html +12 -5
  70. package/docs/functions/ensurePrefix.html +12 -5
  71. package/docs/functions/ensureSuffix.html +12 -5
  72. package/docs/functions/ensureTimestamp.html +12 -5
  73. package/docs/functions/escapeRegExp.html +12 -5
  74. package/docs/functions/formatDate.html +12 -5
  75. package/docs/functions/get.html +12 -5
  76. package/docs/functions/getMultiple.html +12 -5
  77. package/docs/functions/insertSeparator.html +12 -5
  78. package/docs/functions/isEmpty.html +12 -5
  79. package/docs/functions/isNumericString.html +12 -5
  80. package/docs/functions/isPlainObject.html +12 -5
  81. package/docs/functions/last.html +12 -5
  82. package/docs/functions/later-1.html +12 -5
  83. package/docs/functions/mapAsync.html +12 -5
  84. package/docs/functions/mapValues.html +12 -5
  85. package/docs/functions/match.html +12 -5
  86. package/docs/functions/merge.html +20 -13
  87. package/docs/functions/mostFrequent.html +12 -5
  88. package/docs/functions/noop.html +12 -5
  89. package/docs/functions/occurrences.html +12 -5
  90. package/docs/functions/omit.html +18 -7
  91. package/docs/functions/pick.html +13 -6
  92. package/docs/functions/pull.html +12 -5
  93. package/docs/functions/remove.html +12 -5
  94. package/docs/functions/removeCommonProperties.html +12 -5
  95. package/docs/functions/replace.html +12 -5
  96. package/docs/functions/replaceDeep.html +23 -7
  97. package/docs/functions/rethrow.html +12 -5
  98. package/docs/functions/round.html +12 -5
  99. package/docs/functions/safe.html +13 -6
  100. package/docs/functions/sample.html +149 -0
  101. package/docs/functions/samples.html +159 -0
  102. package/docs/functions/scale.html +12 -5
  103. package/docs/functions/seq.html +12 -5
  104. package/docs/functions/seqEarlyBreak.html +12 -5
  105. package/docs/functions/serialize.html +12 -5
  106. package/docs/functions/set.html +12 -5
  107. package/docs/functions/setImmutable.html +12 -5
  108. package/docs/functions/shuffle.html +149 -0
  109. package/docs/functions/sortBy.html +12 -5
  110. package/docs/functions/sortProps.html +12 -5
  111. package/docs/functions/stripPrefix.html +12 -5
  112. package/docs/functions/stripSuffix.html +12 -5
  113. package/docs/functions/throttle.html +12 -5
  114. package/docs/functions/toggle.html +154 -0
  115. package/docs/functions/trim.html +152 -0
  116. package/docs/functions/trimEnd.html +152 -0
  117. package/docs/functions/trimStart.html +152 -0
  118. package/docs/functions/truthy.html +12 -5
  119. package/docs/functions/unique.html +12 -5
  120. package/docs/functions/wait.html +12 -5
  121. package/docs/functions/waitFor.html +23 -18
  122. package/docs/functions/waitSync.html +12 -5
  123. package/docs/index.html +11 -4
  124. package/docs/interfaces/ComparePropsOptions.html +6 -6
  125. package/docs/interfaces/GetMultipleSource.html +12 -5
  126. package/docs/interfaces/GetSource.html +12 -5
  127. package/docs/interfaces/IsNumericStringOptions.html +9 -9
  128. package/docs/interfaces/OccurencesOptions.html +6 -6
  129. package/docs/interfaces/SetImmutableSource.html +12 -5
  130. package/docs/interfaces/SetSource.html +12 -5
  131. package/docs/interfaces/ThrottleOptions.html +7 -7
  132. package/docs/interfaces/ThrottledFunctionExtras.html +7 -7
  133. package/docs/modules.html +18 -4
  134. package/docs/pages/CHANGELOG.html +150 -61
  135. package/docs/pages/Introduction.html +11 -4
  136. package/docs/types/CustomDeserializers.html +12 -5
  137. package/docs/types/CustomSerializers.html +12 -5
  138. package/docs/types/Later.html +12 -5
  139. package/docs/types/MapValuesFn.html +12 -5
  140. package/docs/types/MatchCallback.html +12 -5
  141. package/docs/types/SeqEarlyBreaker.html +12 -5
  142. package/docs/types/SeqFn.html +12 -5
  143. package/docs/types/SeqFunctions.html +12 -5
  144. package/docs/types/SetImmutablePath.html +12 -5
  145. package/docs/types/ThrottledFunction.html +12 -5
  146. package/docs/variables/mapValuesUNSET.html +12 -5
  147. package/docs/variables/mergeUNSET.html +12 -5
  148. package/esm/index.d.ts +7 -0
  149. package/esm/index.d.ts.map +1 -1
  150. package/esm/index.js +7 -0
  151. package/esm/index.js.map +1 -1
  152. package/esm/omit.d.ts +1 -1
  153. package/esm/omit.d.ts.map +1 -1
  154. package/esm/omit.js.map +1 -1
  155. package/esm/replace.d.ts.map +1 -1
  156. package/esm/replace.js.map +1 -1
  157. package/esm/replaceDeep.d.ts +5 -1
  158. package/esm/replaceDeep.d.ts.map +1 -1
  159. package/esm/replaceDeep.js +3 -20
  160. package/esm/replaceDeep.js.map +1 -1
  161. package/esm/replaceDeepByFn.d.ts +7 -0
  162. package/esm/replaceDeepByFn.d.ts.map +1 -0
  163. package/esm/replaceDeepByFn.js +44 -0
  164. package/esm/replaceDeepByFn.js.map +1 -0
  165. package/esm/safe.d.ts.map +1 -1
  166. package/esm/safe.js.map +1 -1
  167. package/esm/sample.d.ts +3 -0
  168. package/esm/sample.d.ts.map +1 -0
  169. package/esm/sample.js +5 -0
  170. package/esm/sample.js.map +1 -0
  171. package/esm/samples.d.ts +3 -0
  172. package/esm/samples.d.ts.map +1 -0
  173. package/esm/samples.js +25 -0
  174. package/esm/samples.js.map +1 -0
  175. package/esm/serialize.d.ts.map +1 -1
  176. package/esm/serialize.js +56 -18
  177. package/esm/serialize.js.map +1 -1
  178. package/esm/shuffle.d.ts +3 -0
  179. package/esm/shuffle.d.ts.map +1 -0
  180. package/esm/shuffle.js +6 -0
  181. package/esm/shuffle.js.map +1 -0
  182. package/esm/toggle.d.ts +3 -0
  183. package/esm/toggle.d.ts.map +1 -0
  184. package/esm/toggle.js +12 -0
  185. package/esm/toggle.js.map +1 -0
  186. package/esm/trim.d.ts +3 -0
  187. package/esm/trim.d.ts.map +1 -0
  188. package/esm/trim.js +7 -0
  189. package/esm/trim.js.map +1 -0
  190. package/esm/trimEnd.d.ts +3 -0
  191. package/esm/trimEnd.d.ts.map +1 -0
  192. package/esm/trimEnd.js +9 -0
  193. package/esm/trimEnd.js.map +1 -0
  194. package/esm/trimStart.d.ts +3 -0
  195. package/esm/trimStart.d.ts.map +1 -0
  196. package/esm/trimStart.js +9 -0
  197. package/esm/trimStart.js.map +1 -0
  198. package/esm/utils/utils.d.ts +6 -0
  199. package/esm/utils/utils.d.ts.map +1 -0
  200. package/esm/utils/utils.js +7 -0
  201. package/esm/utils/utils.js.map +1 -0
  202. package/esm/waitFor.d.ts +7 -1
  203. package/esm/waitFor.d.ts.map +1 -1
  204. package/esm/waitFor.js +35 -10
  205. package/esm/waitFor.js.map +1 -1
  206. package/package.json +4 -4
  207. package/pnpm-lock.yaml +1162 -986
  208. package/src/deserialize.spec.ts +12 -0
  209. package/src/index.ts +7 -0
  210. package/src/omit.ts +8 -2
  211. package/src/pick.ts +1 -1
  212. package/src/replace.ts +0 -1
  213. package/src/replaceDeep.spec.ts +91 -0
  214. package/src/replaceDeep.ts +22 -27
  215. package/src/replaceDeepByFn.spec.ts +162 -0
  216. package/src/replaceDeepByFn.ts +93 -0
  217. package/src/safe.ts +0 -1
  218. package/src/sample.spec.ts +31 -0
  219. package/src/sample.ts +11 -0
  220. package/src/samples.spec.ts +50 -0
  221. package/src/samples.ts +41 -0
  222. package/src/serialize.spec.ts +42 -0
  223. package/src/serialize.ts +65 -18
  224. package/src/shuffle.spec.ts +39 -0
  225. package/src/shuffle.ts +13 -0
  226. package/src/toggle.spec.ts +43 -0
  227. package/src/toggle.ts +22 -0
  228. package/src/trim.spec.ts +22 -0
  229. package/src/trim.ts +23 -0
  230. package/src/trimEnd.spec.ts +20 -0
  231. package/src/trimEnd.ts +22 -0
  232. package/src/trimStart.spec.ts +20 -0
  233. package/src/trimStart.ts +22 -0
  234. package/src/utils/utils.ts +11 -0
  235. package/src/waitFor.spec.ts +141 -0
  236. package/src/waitFor.ts +69 -18
@@ -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 new object with copied all properties without these specified.
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 copied given properties from source object.
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
  };
@@ -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
  });
@@ -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
- * It mutates the `source` object/array!
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
- if (Object.is(source, search)) {
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,11 @@
1
+ /**
2
+ * Picks a random element from an array.
3
+ * @param array - source array
4
+ */
5
+ const sample = <T>(array: T[]): T => {
6
+ return array[Math.floor(Math.random() * array.length)]!;
7
+ };
8
+
9
+ export {
10
+ sample,
11
+ };
@@ -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
+ };