@simplysm/core-common 13.0.69 → 13.0.71

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 (151) hide show
  1. package/README.md +66 -267
  2. package/dist/common.types.d.ts +14 -14
  3. package/dist/errors/argument-error.d.ts +10 -10
  4. package/dist/errors/argument-error.d.ts.map +1 -1
  5. package/dist/errors/argument-error.js +2 -2
  6. package/dist/errors/argument-error.js.map +1 -1
  7. package/dist/errors/not-implemented-error.d.ts +8 -8
  8. package/dist/errors/not-implemented-error.js +2 -2
  9. package/dist/errors/not-implemented-error.js.map +1 -1
  10. package/dist/errors/sd-error.d.ts +10 -10
  11. package/dist/errors/sd-error.d.ts.map +1 -1
  12. package/dist/errors/timeout-error.d.ts +10 -10
  13. package/dist/errors/timeout-error.js +3 -3
  14. package/dist/errors/timeout-error.js.map +1 -1
  15. package/dist/extensions/arr-ext.d.ts +2 -2
  16. package/dist/extensions/arr-ext.helpers.d.ts +8 -8
  17. package/dist/extensions/arr-ext.helpers.js +1 -1
  18. package/dist/extensions/arr-ext.helpers.js.map +1 -1
  19. package/dist/extensions/arr-ext.js +13 -13
  20. package/dist/extensions/arr-ext.js.map +1 -1
  21. package/dist/extensions/arr-ext.types.d.ts +57 -57
  22. package/dist/extensions/arr-ext.types.d.ts.map +1 -1
  23. package/dist/extensions/map-ext.d.ts +16 -16
  24. package/dist/extensions/set-ext.d.ts +11 -11
  25. package/dist/features/debounce-queue.d.ts +17 -15
  26. package/dist/features/debounce-queue.d.ts.map +1 -1
  27. package/dist/features/debounce-queue.js +6 -6
  28. package/dist/features/debounce-queue.js.map +1 -1
  29. package/dist/features/event-emitter.d.ts +20 -20
  30. package/dist/features/event-emitter.js +17 -17
  31. package/dist/features/serial-queue.d.ts +11 -11
  32. package/dist/features/serial-queue.js +5 -5
  33. package/dist/features/serial-queue.js.map +1 -1
  34. package/dist/globals.d.ts +4 -4
  35. package/dist/types/date-only.d.ts +64 -64
  36. package/dist/types/date-only.d.ts.map +1 -1
  37. package/dist/types/date-only.js +63 -63
  38. package/dist/types/date-time.d.ts +37 -37
  39. package/dist/types/date-time.d.ts.map +1 -1
  40. package/dist/types/date-time.js +54 -37
  41. package/dist/types/date-time.js.map +1 -1
  42. package/dist/types/lazy-gc-map.d.ts +26 -26
  43. package/dist/types/lazy-gc-map.d.ts.map +1 -1
  44. package/dist/types/lazy-gc-map.js +26 -26
  45. package/dist/types/lazy-gc-map.js.map +1 -1
  46. package/dist/types/time.d.ts +25 -25
  47. package/dist/types/time.d.ts.map +1 -1
  48. package/dist/types/time.js +25 -25
  49. package/dist/types/time.js.map +1 -1
  50. package/dist/types/uuid.d.ts +11 -11
  51. package/dist/types/uuid.d.ts.map +1 -1
  52. package/dist/types/uuid.js +12 -12
  53. package/dist/types/uuid.js.map +1 -1
  54. package/dist/utils/bytes.d.ts +17 -17
  55. package/dist/utils/bytes.js +4 -4
  56. package/dist/utils/bytes.js.map +1 -1
  57. package/dist/utils/date-format.d.ts +45 -45
  58. package/dist/utils/date-format.js +1 -1
  59. package/dist/utils/date-format.js.map +1 -1
  60. package/dist/utils/error.d.ts +4 -4
  61. package/dist/utils/json.d.ts +17 -17
  62. package/dist/utils/json.js +3 -3
  63. package/dist/utils/json.js.map +1 -1
  64. package/dist/utils/num.d.ts +23 -23
  65. package/dist/utils/obj.d.ts +111 -111
  66. package/dist/utils/obj.d.ts.map +1 -1
  67. package/dist/utils/obj.js +3 -3
  68. package/dist/utils/obj.js.map +1 -1
  69. package/dist/utils/path.d.ts +10 -10
  70. package/dist/utils/primitive.d.ts +5 -5
  71. package/dist/utils/primitive.js +1 -1
  72. package/dist/utils/primitive.js.map +1 -1
  73. package/dist/utils/str.d.ts +46 -46
  74. package/dist/utils/str.d.ts.map +1 -1
  75. package/dist/utils/str.js +5 -5
  76. package/dist/utils/str.js.map +1 -1
  77. package/dist/utils/template-strings.d.ts +26 -26
  78. package/dist/utils/transferable.d.ts +18 -18
  79. package/dist/utils/transferable.js +1 -1
  80. package/dist/utils/transferable.js.map +1 -1
  81. package/dist/utils/wait.d.ts +9 -9
  82. package/dist/utils/xml.d.ts +13 -13
  83. package/dist/utils/xml.d.ts.map +1 -1
  84. package/dist/utils/xml.js +1 -0
  85. package/dist/utils/xml.js.map +1 -1
  86. package/dist/zip/sd-zip.d.ts +22 -22
  87. package/dist/zip/sd-zip.js +16 -16
  88. package/package.json +4 -4
  89. package/src/common.types.ts +17 -17
  90. package/src/errors/argument-error.ts +15 -15
  91. package/src/errors/not-implemented-error.ts +9 -9
  92. package/src/errors/sd-error.ts +12 -12
  93. package/src/errors/timeout-error.ts +12 -12
  94. package/src/extensions/arr-ext.helpers.ts +10 -10
  95. package/src/extensions/arr-ext.ts +57 -57
  96. package/src/extensions/arr-ext.types.ts +59 -59
  97. package/src/extensions/map-ext.ts +16 -16
  98. package/src/extensions/set-ext.ts +11 -11
  99. package/src/features/debounce-queue.ts +21 -19
  100. package/src/features/event-emitter.ts +25 -25
  101. package/src/features/serial-queue.ts +13 -13
  102. package/src/globals.ts +4 -4
  103. package/src/index.ts +1 -1
  104. package/src/types/date-only.ts +83 -83
  105. package/src/types/date-time.ts +64 -44
  106. package/src/types/lazy-gc-map.ts +45 -45
  107. package/src/types/time.ts +34 -34
  108. package/src/types/uuid.ts +17 -17
  109. package/src/utils/bytes.ts +35 -35
  110. package/src/utils/date-format.ts +65 -65
  111. package/src/utils/error.ts +4 -4
  112. package/src/utils/json.ts +39 -39
  113. package/src/utils/num.ts +23 -23
  114. package/src/utils/obj.ts +138 -138
  115. package/src/utils/path.ts +10 -10
  116. package/src/utils/primitive.ts +6 -6
  117. package/src/utils/str.ts +260 -261
  118. package/src/utils/template-strings.ts +29 -29
  119. package/src/utils/transferable.ts +284 -284
  120. package/src/utils/wait.ts +10 -10
  121. package/src/utils/xml.ts +20 -19
  122. package/src/zip/sd-zip.ts +25 -25
  123. package/tests/errors/errors.spec.ts +80 -0
  124. package/tests/extensions/array-extension.spec.ts +796 -0
  125. package/tests/extensions/map-extension.spec.ts +147 -0
  126. package/tests/extensions/set-extension.spec.ts +74 -0
  127. package/tests/types/date-only.spec.ts +638 -0
  128. package/tests/types/date-time.spec.ts +391 -0
  129. package/tests/types/lazy-gc-map.spec.ts +692 -0
  130. package/tests/types/time.spec.ts +559 -0
  131. package/tests/types/uuid.spec.ts +74 -0
  132. package/tests/utils/bytes-utils.spec.ts +230 -0
  133. package/tests/utils/date-format.spec.ts +373 -0
  134. package/tests/utils/debounce-queue.spec.ts +272 -0
  135. package/tests/utils/json.spec.ts +486 -0
  136. package/tests/utils/number.spec.ts +157 -0
  137. package/tests/utils/object.spec.ts +829 -0
  138. package/tests/utils/path.spec.ts +78 -0
  139. package/tests/utils/primitive.spec.ts +43 -0
  140. package/tests/utils/sd-event-emitter.spec.ts +216 -0
  141. package/tests/utils/serial-queue.spec.ts +365 -0
  142. package/tests/utils/string.spec.ts +281 -0
  143. package/tests/utils/template-strings.spec.ts +57 -0
  144. package/tests/utils/transferable.spec.ts +703 -0
  145. package/tests/utils/wait.spec.ts +145 -0
  146. package/tests/utils/xml.spec.ts +146 -0
  147. package/tests/zip/sd-zip.spec.ts +238 -0
  148. package/docs/extensions.md +0 -503
  149. package/docs/features.md +0 -109
  150. package/docs/types.md +0 -486
  151. package/docs/utils.md +0 -780
@@ -0,0 +1,486 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ jsonStringify as stringify,
4
+ jsonParse as parse,
5
+ DateTime,
6
+ DateOnly,
7
+ Time,
8
+ Uuid,
9
+ } from "@simplysm/core-common";
10
+
11
+ describe("JsonConvert", () => {
12
+ //#region stringify
13
+
14
+ describe("stringify()", () => {
15
+ it("Serializes primitive values", () => {
16
+ expect(stringify(42)).toBe("42");
17
+ expect(stringify("hello")).toBe('"hello"');
18
+ expect(stringify(true)).toBe("true");
19
+ expect(stringify(null)).toBe("null");
20
+ });
21
+
22
+ it("Serializes Date with __type__", () => {
23
+ const date = new Date("2024-03-15T10:30:00.000Z");
24
+ const json = stringify(date);
25
+ const parsed = JSON.parse(json);
26
+
27
+ expect(parsed.__type__).toBe("Date");
28
+ expect(parsed.data).toBe("2024-03-15T10:30:00.000Z");
29
+ });
30
+
31
+ it("Serializes DateTime with __type__", () => {
32
+ const dt = new DateTime(2024, 3, 15, 10, 30);
33
+ const json = stringify(dt);
34
+ const parsed = JSON.parse(json);
35
+
36
+ expect(parsed.__type__).toBe("DateTime");
37
+ expect(typeof parsed.data).toBe("string");
38
+ });
39
+
40
+ it("Serializes DateOnly with __type__", () => {
41
+ const d = new DateOnly(2024, 3, 15);
42
+ const json = stringify(d);
43
+ const parsed = JSON.parse(json);
44
+
45
+ expect(parsed.__type__).toBe("DateOnly");
46
+ });
47
+
48
+ it("Serializes Time with __type__", () => {
49
+ const t = new Time(10, 30, 45);
50
+ const json = stringify(t);
51
+ const parsed = JSON.parse(json);
52
+
53
+ expect(parsed.__type__).toBe("Time");
54
+ });
55
+
56
+ it("Serializes Uuid with __type__", () => {
57
+ const uuid = new Uuid("12345678-9abc-def0-1234-56789abcdef0");
58
+ const json = stringify(uuid);
59
+ const parsed = JSON.parse(json);
60
+
61
+ expect(parsed.__type__).toBe("Uuid");
62
+ expect(parsed.data).toBe("12345678-9abc-def0-1234-56789abcdef0");
63
+ });
64
+
65
+ it("Serializes Set with __type__", () => {
66
+ const set = new Set([1, 2, 3]);
67
+ const json = stringify(set);
68
+ const parsed = JSON.parse(json);
69
+
70
+ expect(parsed.__type__).toBe("Set");
71
+ expect(parsed.data).toEqual([1, 2, 3]);
72
+ });
73
+
74
+ it("Serializes Map with __type__", () => {
75
+ const map = new Map([
76
+ ["a", 1],
77
+ ["b", 2],
78
+ ]);
79
+ const json = stringify(map);
80
+ const parsed = JSON.parse(json);
81
+
82
+ expect(parsed.__type__).toBe("Map");
83
+ expect(parsed.data).toEqual([
84
+ ["a", 1],
85
+ ["b", 2],
86
+ ]);
87
+ });
88
+
89
+ it("Serializes Error with __type__", () => {
90
+ const error = new Error("test error");
91
+ const json = stringify(error);
92
+ const parsed = JSON.parse(json);
93
+
94
+ expect(parsed.__type__).toBe("Error");
95
+ expect(parsed.data.message).toBe("test error");
96
+ expect(parsed.data.name).toBe("Error");
97
+ });
98
+
99
+ it("Serializes Error extended properties (code, detail, cause)", () => {
100
+ const cause = new Error("cause error");
101
+ const error = new Error("test error") as Error & { code: string; detail: object };
102
+ error.code = "ERR_CODE";
103
+ error.detail = { key: "value" };
104
+ (error as Error & { cause: Error }).cause = cause;
105
+
106
+ const json = stringify(error);
107
+ const parsed = JSON.parse(json);
108
+
109
+ expect(parsed.__type__).toBe("Error");
110
+ expect(parsed.data.message).toBe("test error");
111
+ expect(parsed.data.code).toBe("ERR_CODE");
112
+ expect(parsed.data.detail).toEqual({ key: "value" });
113
+ expect(parsed.data.cause.__type__).toBe("Error");
114
+ expect(parsed.data.cause.data.message).toBe("cause error");
115
+ });
116
+
117
+ it("Hides Uint8Array with redactBytes option", () => {
118
+ const obj = { data: new TextEncoder().encode("hello") };
119
+ const json = stringify(obj, { redactBytes: true });
120
+ const parsed = JSON.parse(json);
121
+
122
+ expect(parsed.data.data).toBe("__hidden__");
123
+ });
124
+
125
+ it("Indents with space option", () => {
126
+ const obj = { a: 1 };
127
+ const json = stringify(obj, { space: 2 });
128
+
129
+ expect(json).toBe('{\n "a": 1\n}');
130
+ });
131
+
132
+ it("Transforms values with replacer option", () => {
133
+ const obj = { a: 1, b: 2, c: 3 };
134
+ const json = stringify(obj, {
135
+ replacer: (key, value) => {
136
+ if (key === "b") return undefined;
137
+ return value;
138
+ },
139
+ });
140
+ const parsed = JSON.parse(json);
141
+
142
+ expect(parsed.a).toBe(1);
143
+ expect(parsed.b).toBeUndefined();
144
+ expect(parsed.c).toBe(3);
145
+ });
146
+
147
+ it("No race condition on concurrent calls", async () => {
148
+ // Serialize complex objects with Date simultaneously
149
+ const createTestObject = (id: number) => ({
150
+ id,
151
+ date: new Date(`2024-0${(id % 9) + 1}-15T10:30:00.000Z`),
152
+ nested: {
153
+ innerDate: new Date(`2024-0${(id % 9) + 1}-20T15:45:00.000Z`),
154
+ },
155
+ array: [new Date(`2024-0${(id % 9) + 1}-25T08:00:00.000Z`)],
156
+ });
157
+
158
+ // 100 concurrent calls
159
+ const promises = Array.from({ length: 100 }, (_, i) =>
160
+ Promise.resolve().then(() => {
161
+ const obj = createTestObject(i);
162
+ const json = stringify(obj);
163
+ const parsed = parse<typeof obj>(json);
164
+
165
+ // Verify all Dates restored correctly
166
+ expect(parsed.date).toBeInstanceOf(Date);
167
+ expect(parsed.nested.innerDate).toBeInstanceOf(Date);
168
+ expect(parsed.array[0]).toBeInstanceOf(Date);
169
+
170
+ return { id: i, success: true };
171
+ }),
172
+ );
173
+
174
+ const results = await Promise.all(promises);
175
+
176
+ // Verify all calls succeeded
177
+ expect(results.every((r) => r.success)).toBe(true);
178
+ });
179
+
180
+ it("Correctly serializes nested Date objects", () => {
181
+ const obj = {
182
+ level1: {
183
+ level2: {
184
+ level3: {
185
+ date: new Date("2024-06-15T12:00:00.000Z"),
186
+ },
187
+ },
188
+ },
189
+ dates: [new Date("2024-01-01T00:00:00.000Z"), new Date("2024-12-31T23:59:59.000Z")],
190
+ };
191
+
192
+ const json = stringify(obj);
193
+ const parsed = parse<typeof obj>(json);
194
+
195
+ expect(parsed.level1.level2.level3.date).toBeInstanceOf(Date);
196
+ expect(parsed.level1.level2.level3.date.toISOString()).toBe("2024-06-15T12:00:00.000Z");
197
+ expect(parsed.dates[0]).toBeInstanceOf(Date);
198
+ expect(parsed.dates[1]).toBeInstanceOf(Date);
199
+ });
200
+
201
+ it("Does not modify Date.prototype.toJSON", () => {
202
+ const originalToJSON = Date.prototype.toJSON;
203
+
204
+ // Call stringify
205
+ const date = new Date("2024-03-15T10:30:00.000Z");
206
+ stringify({ date });
207
+
208
+ // Verify toJSON not changed
209
+ expect(Date.prototype.toJSON).toBe(originalToJSON);
210
+ });
211
+
212
+ it("Circular reference object throws TypeError", () => {
213
+ const obj: Record<string, unknown> = { name: "test" };
214
+ obj["self"] = obj; // circular reference
215
+
216
+ expect(() => stringify(obj)).toThrow(TypeError);
217
+ expect(() => stringify(obj)).toThrow("Converting circular structure to JSON");
218
+ });
219
+
220
+ it("Detects circular reference in array", () => {
221
+ const arr: unknown[] = [1, 2];
222
+ arr.push(arr); // array circular reference
223
+
224
+ expect(() => stringify(arr)).toThrow("Converting circular structure to JSON");
225
+ });
226
+
227
+ it("Serializes custom object with toJSON method", () => {
228
+ const obj = {
229
+ amount: 100,
230
+ toJSON() {
231
+ return `$${this.amount}`;
232
+ },
233
+ };
234
+
235
+ const json = stringify(obj);
236
+ expect(json).toBe('"$100"');
237
+ });
238
+
239
+ it("Recursively processes toJSON if returning object", () => {
240
+ const obj = {
241
+ data: "test",
242
+ toJSON() {
243
+ return { converted: true, date: new Date("2024-01-01T00:00:00.000Z") };
244
+ },
245
+ };
246
+
247
+ const json = stringify(obj);
248
+ const parsed = JSON.parse(json);
249
+
250
+ expect(parsed.converted).toBe(true);
251
+ expect(parsed.date.__type__).toBe("Date");
252
+ });
253
+
254
+ it("Serializes getter properties", () => {
255
+ const obj = {
256
+ _value: 10,
257
+ get computed() {
258
+ return this._value * 2;
259
+ },
260
+ };
261
+
262
+ const json = stringify(obj);
263
+ const parsed = JSON.parse(json);
264
+
265
+ expect(parsed._value).toBe(10);
266
+ expect(parsed.computed).toBe(20);
267
+ });
268
+
269
+ it("Serializes empty objects and arrays", () => {
270
+ expect(stringify({})).toBe("{}");
271
+ expect(stringify([])).toBe("[]");
272
+ expect(stringify({ arr: [], obj: {} })).toBe('{"arr":[],"obj":{}}');
273
+ });
274
+
275
+ it("Excludes properties with undefined value", () => {
276
+ const obj = { a: 1, b: undefined, c: 3 };
277
+ const json = stringify(obj);
278
+ const parsed = JSON.parse(json);
279
+
280
+ expect(parsed.a).toBe(1);
281
+ expect("b" in parsed).toBe(false);
282
+ expect(parsed.c).toBe(3);
283
+ });
284
+
285
+ it("Performance completes in reasonable time", () => {
286
+ // Create complex test object
287
+ const createTestObject = () => ({
288
+ id: 1,
289
+ name: "test",
290
+ date: new Date(),
291
+ nested: {
292
+ array: [1, 2, 3, new Date(), { deep: true }],
293
+ map: new Map([["a", 1]]),
294
+ set: new Set([1, 2, 3]),
295
+ },
296
+ });
297
+
298
+ const iterations = 1000;
299
+ const testObj = createTestObject();
300
+
301
+ // Measure jsonStringify performance
302
+ const startCustom = performance.now();
303
+ for (let i = 0; i < iterations; i++) {
304
+ stringify(testObj);
305
+ }
306
+ const customTime = performance.now() - startCustom;
307
+
308
+ // 1000 serializations should complete within 100ms
309
+ // (includes custom type handling, circular reference detection, etc)
310
+ expect(customTime).toBeLessThan(100);
311
+ });
312
+ });
313
+
314
+ //#endregion
315
+
316
+ //#region parse
317
+
318
+ describe("parse()", () => {
319
+ it("Deserializes primitive values", () => {
320
+ expect(parse("42")).toBe(42);
321
+ expect(parse('"hello"')).toBe("hello");
322
+ expect(parse("true")).toBe(true);
323
+ });
324
+
325
+ it("Converts null to undefined", () => {
326
+ expect(parse("null")).toBe(undefined);
327
+ });
328
+
329
+ it("Restores Date", () => {
330
+ const json = '{"__type__":"Date","data":"2024-03-15T10:30:00.000Z"}';
331
+ const result = parse(json);
332
+
333
+ expect(result).toBeInstanceOf(Date);
334
+ expect((result as Date).toISOString()).toBe("2024-03-15T10:30:00.000Z");
335
+ });
336
+
337
+ it("Restores DateTime", () => {
338
+ const dt = new DateTime(2024, 3, 15, 10, 30);
339
+ const json = stringify(dt);
340
+ const result = parse(json);
341
+
342
+ expect(result).toBeInstanceOf(DateTime);
343
+ expect((result as DateTime).year).toBe(2024);
344
+ expect((result as DateTime).month).toBe(3);
345
+ expect((result as DateTime).day).toBe(15);
346
+ });
347
+
348
+ it("Restores DateOnly", () => {
349
+ const d = new DateOnly(2024, 3, 15);
350
+ const json = stringify(d);
351
+ const result = parse(json);
352
+
353
+ expect(result).toBeInstanceOf(DateOnly);
354
+ expect((result as DateOnly).year).toBe(2024);
355
+ });
356
+
357
+ it("Restores Time", () => {
358
+ const t = new Time(10, 30, 45);
359
+ const json = stringify(t);
360
+ const result = parse(json);
361
+
362
+ expect(result).toBeInstanceOf(Time);
363
+ expect((result as Time).hour).toBe(10);
364
+ });
365
+
366
+ it("Restores Uuid", () => {
367
+ const json = '{"__type__":"Uuid","data":"12345678-9abc-def0-1234-56789abcdef0"}';
368
+ const result = parse(json);
369
+
370
+ expect(result).toBeInstanceOf(Uuid);
371
+ expect((result as Uuid).toString()).toBe("12345678-9abc-def0-1234-56789abcdef0");
372
+ });
373
+
374
+ it("Restores Set", () => {
375
+ const json = '{"__type__":"Set","data":[1,2,3]}';
376
+ const result = parse(json);
377
+
378
+ expect(result).toBeInstanceOf(Set);
379
+ expect(Array.from(result as Set<number>)).toEqual([1, 2, 3]);
380
+ });
381
+
382
+ it("Restores Map", () => {
383
+ const json = '{"__type__":"Map","data":[["a",1],["b",2]]}';
384
+ const result = parse(json);
385
+
386
+ expect(result).toBeInstanceOf(Map);
387
+ expect((result as Map<string, number>).get("a")).toBe(1);
388
+ });
389
+
390
+ it("Restores Error (with extended properties)", () => {
391
+ const cause = new Error("cause error");
392
+ const error = new Error("test error") as Error & {
393
+ code: string;
394
+ detail: object;
395
+ cause: Error;
396
+ };
397
+ error.code = "ERR_CODE";
398
+ error.detail = { key: "value" };
399
+ error.cause = cause;
400
+
401
+ const json = stringify(error);
402
+ const result = parse<typeof error>(json);
403
+
404
+ expect(result).toBeInstanceOf(Error);
405
+ expect(result.message).toBe("test error");
406
+ expect(result.code).toBe("ERR_CODE");
407
+ expect(result.detail).toEqual({ key: "value" });
408
+ expect(result.cause).toBeInstanceOf(Error);
409
+ expect(result.cause.message).toBe("cause error");
410
+ });
411
+
412
+ it("Restores Uint8Array", () => {
413
+ const json = '{"__type__":"Uint8Array","data":"68656c6c6f"}';
414
+ const result = parse(json);
415
+
416
+ expect(result).toBeInstanceOf(Uint8Array);
417
+ expect(new TextDecoder().decode(result as Uint8Array)).toBe("hello");
418
+ });
419
+
420
+ it("stringify/parse round-trip", () => {
421
+ const original = {
422
+ date: new Date("2024-03-15T10:30:00.000Z"),
423
+ dateTime: new DateTime(2024, 3, 15),
424
+ dateOnly: new DateOnly(2024, 3, 15),
425
+ time: new Time(10, 30),
426
+ uuid: new Uuid("12345678-9abc-def0-1234-56789abcdef0"),
427
+ set: new Set([1, 2, 3]),
428
+ map: new Map([["a", 1]]),
429
+ bytes: new TextEncoder().encode("hello"),
430
+ };
431
+
432
+ const json = stringify(original);
433
+ const result = parse<typeof original>(json);
434
+
435
+ expect(result.date).toBeInstanceOf(Date);
436
+ expect(result.dateTime).toBeInstanceOf(DateTime);
437
+ expect(result.dateOnly).toBeInstanceOf(DateOnly);
438
+ expect(result.time).toBeInstanceOf(Time);
439
+ expect(result.uuid).toBeInstanceOf(Uuid);
440
+ expect(result.set).toBeInstanceOf(Set);
441
+ expect(result.map).toBeInstanceOf(Map);
442
+ expect(result.bytes).toBeInstanceOf(Uint8Array);
443
+ });
444
+
445
+ it("Data serialized with redactBytes throws error on parse", () => {
446
+ const obj = { data: new TextEncoder().encode("hello") };
447
+ const json = stringify(obj, { redactBytes: true });
448
+
449
+ // "__hidden__" is data serialized with redactBytes option, so parse throws error
450
+ expect(() => parse<typeof obj>(json)).toThrow(
451
+ "Uint8Array serialized with redactBytes option cannot be restored via parse",
452
+ );
453
+ });
454
+
455
+ it("Invalid JSON throws error", () => {
456
+ expect(() => parse("invalid json")).toThrow("JSON parsing error");
457
+ });
458
+
459
+ it("In DEV mode, full JSON included in error message", () => {
460
+ const longJson = "x".repeat(2000);
461
+
462
+ try {
463
+ parse(longJson);
464
+ expect.fail("Error should be thrown");
465
+ } catch (err) {
466
+ const message = (err as Error).message;
467
+ // In DEV mode, full JSON included
468
+ expect(message).toContain(longJson);
469
+ }
470
+ });
471
+
472
+ it("Error message includes JSON content", () => {
473
+ const shortJson = "invalid";
474
+
475
+ try {
476
+ parse(shortJson);
477
+ expect.fail("Error should be thrown");
478
+ } catch (err) {
479
+ const message = (err as Error).message;
480
+ expect(message).toContain("invalid");
481
+ }
482
+ });
483
+ });
484
+
485
+ //#endregion
486
+ });
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ numParseInt as parseInt,
4
+ numParseRoundedInt as parseRoundedInt,
5
+ numParseFloat as parseFloat,
6
+ numFormat as format,
7
+ } from "@simplysm/core-common";
8
+
9
+ describe("number utils", () => {
10
+ //#region numParseInt
11
+
12
+ describe("numParseInt()", () => {
13
+ it("Parses numeric string to integer", () => {
14
+ expect(parseInt("123")).toBe(123);
15
+ });
16
+
17
+ it("Parses negative string", () => {
18
+ expect(parseInt("-123")).toBe(-123);
19
+ });
20
+
21
+ it("Floating string returns only integer part", () => {
22
+ expect(parseInt("123.45")).toBe(123);
23
+ });
24
+
25
+ it("Number type discards decimal places", () => {
26
+ expect(parseInt(123.7)).toBe(123);
27
+ expect(parseInt(123.3)).toBe(123);
28
+ });
29
+
30
+ it("Removes non-numeric characters then parses", () => {
31
+ expect(parseInt("$1,234")).toBe(1234);
32
+ expect(parseInt("abc123")).toBe(123);
33
+ });
34
+
35
+ it("Handles minus sign between characters", () => {
36
+ expect(parseInt("abc-123def")).toBe(-123);
37
+ expect(parseInt("abc-456def")).toBe(-456);
38
+ });
39
+
40
+ it("Empty string returns undefined", () => {
41
+ expect(parseInt("")).toBe(undefined);
42
+ });
43
+
44
+ it("String with no digits returns undefined", () => {
45
+ expect(parseInt("abc")).toBe(undefined);
46
+ });
47
+
48
+ it("Non-string type returns undefined", () => {
49
+ expect(parseInt(null)).toBe(undefined);
50
+ expect(parseInt(undefined)).toBe(undefined);
51
+ expect(parseInt({})).toBe(undefined);
52
+ });
53
+ });
54
+
55
+ //#endregion
56
+
57
+ //#region numParseRoundedInt
58
+
59
+ describe("numParseRoundedInt()", () => {
60
+ it("Rounds floating string to integer", () => {
61
+ expect(parseRoundedInt("123.5")).toBe(124);
62
+ expect(parseRoundedInt("123.4")).toBe(123);
63
+ });
64
+
65
+ it("Integer string returned as-is", () => {
66
+ expect(parseRoundedInt("123")).toBe(123);
67
+ });
68
+
69
+ it("Rounds number type", () => {
70
+ expect(parseRoundedInt(123.7)).toBe(124);
71
+ });
72
+
73
+ it("Returns undefined if not parseable", () => {
74
+ expect(parseRoundedInt("abc")).toBe(undefined);
75
+ });
76
+ });
77
+
78
+ //#endregion
79
+
80
+ //#region numParseFloat
81
+
82
+ describe("numParseFloat()", () => {
83
+ it("Parses floating string", () => {
84
+ expect(parseFloat("123.45")).toBe(123.45);
85
+ });
86
+
87
+ it("Parses integer string as float", () => {
88
+ expect(parseFloat("123")).toBe(123);
89
+ });
90
+
91
+ it("Parses negative floating string", () => {
92
+ expect(parseFloat("-123.45")).toBe(-123.45);
93
+ });
94
+
95
+ it("Number type returned as-is", () => {
96
+ expect(parseFloat(123.45)).toBe(123.45);
97
+ });
98
+
99
+ it("Removes non-numeric characters then parses", () => {
100
+ expect(parseFloat("$1,234.56")).toBe(1234.56);
101
+ });
102
+
103
+ it("Empty string returns undefined", () => {
104
+ expect(parseFloat("")).toBe(undefined);
105
+ });
106
+
107
+ it("String with no digits returns undefined", () => {
108
+ expect(parseFloat("abc")).toBe(undefined);
109
+ });
110
+
111
+ it("Non-string type returns undefined", () => {
112
+ expect(parseFloat(null)).toBe(undefined);
113
+ expect(parseFloat(undefined)).toBe(undefined);
114
+ });
115
+ });
116
+
117
+ //#endregion
118
+
119
+ //#region numFormat
120
+
121
+ describe("numFormat()", () => {
122
+ it("Applies thousands separator", () => {
123
+ expect(format(1234567)).toBe("1,234,567");
124
+ });
125
+
126
+ it("Applies thousands separator to float", () => {
127
+ const result = format(1234567.89);
128
+ expect(result).toContain("1,234,567");
129
+ });
130
+
131
+ it("Specifies maximum decimal places", () => {
132
+ expect(format(123.456, { max: 2 })).toBe("123.46");
133
+ });
134
+
135
+ it("Specifies minimum decimal places", () => {
136
+ expect(format(123, { min: 2 })).toBe("123.00");
137
+ });
138
+
139
+ it("Specifies both max and min decimal places", () => {
140
+ expect(format(123.4, { max: 3, min: 2 })).toBe("123.40");
141
+ });
142
+
143
+ it("Undefined returns undefined", () => {
144
+ expect(format(undefined)).toBe(undefined);
145
+ });
146
+
147
+ it("Formats zero", () => {
148
+ expect(format(0)).toBe("0");
149
+ });
150
+
151
+ it("Formats negative number", () => {
152
+ expect(format(-1234567)).toBe("-1,234,567");
153
+ });
154
+ });
155
+
156
+ //#endregion
157
+ });