@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,230 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ ArgumentError,
4
+ bytesConcat as concat,
5
+ bytesToHex as toHex,
6
+ bytesFromHex as fromHex,
7
+ bytesToBase64 as toBase64,
8
+ bytesFromBase64 as fromBase64,
9
+ } from "@simplysm/core-common";
10
+
11
+ describe("BytesUtils", () => {
12
+ //#region concat
13
+
14
+ describe("concat()", () => {
15
+ it("Concatenates multiple Uint8Arrays", () => {
16
+ const arr1 = new Uint8Array([1, 2, 3]);
17
+ const arr2 = new Uint8Array([4, 5]);
18
+ const arr3 = new Uint8Array([6, 7, 8, 9]);
19
+
20
+ const result = concat([arr1, arr2, arr3]);
21
+
22
+ expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
23
+ });
24
+
25
+ it("Handles empty array", () => {
26
+ const result = concat([]);
27
+
28
+ expect(result).toEqual(new Uint8Array([]));
29
+ expect(result.length).toBe(0);
30
+ });
31
+
32
+ it("Handles single array", () => {
33
+ const arr = new Uint8Array([1, 2, 3]);
34
+
35
+ const result = concat([arr]);
36
+
37
+ expect(result).toEqual(new Uint8Array([1, 2, 3]));
38
+ });
39
+
40
+ it("Handles empty Uint8Array in array", () => {
41
+ const arr1 = new Uint8Array([1, 2]);
42
+ const arr2 = new Uint8Array([]);
43
+ const arr3 = new Uint8Array([3, 4]);
44
+
45
+ const result = concat([arr1, arr2, arr3]);
46
+
47
+ expect(result).toEqual(new Uint8Array([1, 2, 3, 4]));
48
+ });
49
+ });
50
+
51
+ //#endregion
52
+
53
+ //#region toHex/fromHex
54
+
55
+ describe("toHex()", () => {
56
+ it("Converts Uint8Array to hex string", () => {
57
+ const bytes = new Uint8Array([0, 1, 15, 16, 255]);
58
+
59
+ const result = toHex(bytes);
60
+
61
+ expect(result).toBe("00010f10ff");
62
+ });
63
+
64
+ it("Handles empty array", () => {
65
+ const result = toHex(new Uint8Array([]));
66
+
67
+ expect(result).toBe("");
68
+ });
69
+
70
+ it("Handles single byte", () => {
71
+ expect(toHex(new Uint8Array([0]))).toBe("00");
72
+ expect(toHex(new Uint8Array([255]))).toBe("ff");
73
+ });
74
+ });
75
+
76
+ describe("fromHex()", () => {
77
+ it("Converts hex string to Uint8Array", () => {
78
+ const result = fromHex("00010f10ff");
79
+
80
+ expect(result).toEqual(new Uint8Array([0, 1, 15, 16, 255]));
81
+ });
82
+
83
+ it("Handles empty string", () => {
84
+ const result = fromHex("");
85
+
86
+ expect(result).toEqual(new Uint8Array([]));
87
+ });
88
+
89
+ it("Handles uppercase hex", () => {
90
+ const result = fromHex("FF0A");
91
+
92
+ expect(result).toEqual(new Uint8Array([255, 10]));
93
+ });
94
+
95
+ it("Throws error for odd-length string", () => {
96
+ expect(() => fromHex("abc")).toThrow(ArgumentError);
97
+ expect(() => fromHex("a")).toThrow(ArgumentError);
98
+ expect(() => fromHex("12345")).toThrow(ArgumentError);
99
+ });
100
+
101
+ it("Throws error for invalid hex characters", () => {
102
+ expect(() => fromHex("zz")).toThrow(ArgumentError);
103
+ expect(() => fromHex("gh")).toThrow(ArgumentError);
104
+ expect(() => fromHex("12g4")).toThrow(ArgumentError);
105
+ });
106
+ });
107
+
108
+ describe("toHex/fromHex round-trip conversion", () => {
109
+ it("Round-trip conversion matches", () => {
110
+ const original = new Uint8Array([0, 127, 128, 255, 1, 2, 3]);
111
+
112
+ const hex = toHex(original);
113
+ const restored = fromHex(hex);
114
+
115
+ expect(restored).toEqual(original);
116
+ });
117
+
118
+ it("Round-trip conversion matches all byte values (0-255)", () => {
119
+ const original = new Uint8Array(256);
120
+ for (let i = 0; i < 256; i++) {
121
+ original[i] = i;
122
+ }
123
+
124
+ const hex = toHex(original);
125
+ const restored = fromHex(hex);
126
+
127
+ expect(restored).toEqual(original);
128
+ });
129
+ });
130
+
131
+ //#endregion
132
+
133
+ //#region toBase64/fromBase64
134
+
135
+ describe("toBase64()", () => {
136
+ it("Handles empty array", () => {
137
+ expect(toBase64(new Uint8Array([]))).toBe("");
138
+ });
139
+
140
+ it("Converts general data", () => {
141
+ expect(toBase64(new Uint8Array([72, 101, 108, 108, 111]))).toBe("SGVsbG8=");
142
+ });
143
+
144
+ it("Handles large data (1MB) without stack overflow", () => {
145
+ const data = new Uint8Array(1024 * 1024);
146
+ expect(() => toBase64(data)).not.toThrow();
147
+ });
148
+
149
+ it("Handles case with no padding needed", () => {
150
+ // Multiple of 3 length - no padding
151
+ expect(toBase64(new Uint8Array([1, 2, 3]))).toBe("AQID");
152
+ });
153
+
154
+ it("Handles case with single padding needed", () => {
155
+ // Remainder 2 when divided by 3 - 1 padding
156
+ expect(toBase64(new Uint8Array([1, 2]))).toBe("AQI=");
157
+ });
158
+
159
+ it("Handles case with double padding needed", () => {
160
+ // Remainder 1 when divided by 3 - 2 padding
161
+ expect(toBase64(new Uint8Array([1]))).toBe("AQ==");
162
+ });
163
+ });
164
+
165
+ describe("fromBase64()", () => {
166
+ it("Handles empty string", () => {
167
+ expect(fromBase64("")).toEqual(new Uint8Array([]));
168
+ });
169
+
170
+ it("Converts general data", () => {
171
+ expect(fromBase64("SGVsbG8=")).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
172
+ });
173
+
174
+ it("Throws error for invalid base64 characters", () => {
175
+ expect(() => fromBase64("!!invalid!!")).toThrow(ArgumentError);
176
+ });
177
+
178
+ it("Throws error for invalid base64 length (remainder 1)", () => {
179
+ expect(() => fromBase64("A")).toThrow(ArgumentError);
180
+ expect(() => fromBase64("AAAAA")).toThrow(ArgumentError);
181
+ });
182
+
183
+ it("Handles base64 without padding", () => {
184
+ expect(fromBase64("AQID")).toEqual(new Uint8Array([1, 2, 3]));
185
+ });
186
+
187
+ it("Handles base64 with whitespace", () => {
188
+ expect(fromBase64("SGVs bG8=")).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
189
+ });
190
+ });
191
+
192
+ describe("toBase64/fromBase64 round-trip conversion", () => {
193
+ it("Round-trip conversion matches", () => {
194
+ const original = new Uint8Array([0, 127, 128, 255, 1, 2, 3]);
195
+
196
+ const base64 = toBase64(original);
197
+ const restored = fromBase64(base64);
198
+
199
+ expect(restored).toEqual(original);
200
+ });
201
+
202
+ it("Round-trip conversion matches all byte values (0-255)", () => {
203
+ const original = new Uint8Array(256);
204
+ for (let i = 0; i < 256; i++) {
205
+ original[i] = i;
206
+ }
207
+
208
+ const base64 = toBase64(original);
209
+ const restored = fromBase64(base64);
210
+
211
+ expect(restored).toEqual(original);
212
+ });
213
+
214
+ it("Round-trip conversion matches various lengths (1-10 bytes)", () => {
215
+ for (let len = 1; len <= 10; len++) {
216
+ const original = new Uint8Array(len);
217
+ for (let i = 0; i < len; i++) {
218
+ original[i] = (i * 37 + 13) % 256;
219
+ }
220
+
221
+ const base64 = toBase64(original);
222
+ const restored = fromBase64(base64);
223
+
224
+ expect(restored).toEqual(original);
225
+ }
226
+ });
227
+ });
228
+
229
+ //#endregion
230
+ });
@@ -0,0 +1,373 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { formatDate, normalizeMonth } from "@simplysm/core-common";
3
+
4
+ describe("formatDateTime", () => {
5
+ //#region Year pattern
6
+
7
+ describe("Year pattern", () => {
8
+ it("outputs 4-digit year with yyyy format", () => {
9
+ expect(formatDate("yyyy", { year: 2025 })).toBe("2025");
10
+ });
11
+
12
+ it("outputs 2-digit year with yy format", () => {
13
+ expect(formatDate("yy", { year: 2025 })).toBe("25");
14
+ expect(formatDate("yy", { year: 2000 })).toBe("00");
15
+ expect(formatDate("yy", { year: 1999 })).toBe("99");
16
+ });
17
+ });
18
+
19
+ //#endregion
20
+
21
+ //#region Month pattern
22
+
23
+ describe("Month pattern", () => {
24
+ it("outputs 2-digit month with MM format", () => {
25
+ expect(formatDate("MM", { month: 1 })).toBe("01");
26
+ expect(formatDate("MM", { month: 9 })).toBe("09");
27
+ expect(formatDate("MM", { month: 12 })).toBe("12");
28
+ });
29
+
30
+ it("outputs month without padding with M format", () => {
31
+ expect(formatDate("M", { month: 1 })).toBe("1");
32
+ expect(formatDate("M", { month: 9 })).toBe("9");
33
+ expect(formatDate("M", { month: 12 })).toBe("12");
34
+ });
35
+ });
36
+
37
+ //#endregion
38
+
39
+ //#region Day pattern
40
+
41
+ describe("Day pattern", () => {
42
+ it("outputs 2-digit day with dd format", () => {
43
+ expect(formatDate("dd", { day: 1 })).toBe("01");
44
+ expect(formatDate("dd", { day: 9 })).toBe("09");
45
+ expect(formatDate("dd", { day: 31 })).toBe("31");
46
+ });
47
+
48
+ it("outputs day without padding with d format", () => {
49
+ expect(formatDate("d", { day: 1 })).toBe("1");
50
+ expect(formatDate("d", { day: 9 })).toBe("9");
51
+ expect(formatDate("d", { day: 31 })).toBe("31");
52
+ });
53
+ });
54
+
55
+ //#endregion
56
+
57
+ //#region Day of week pattern
58
+
59
+ describe("Day of week pattern", () => {
60
+ it("outputs day of week in Korean with ddd format", () => {
61
+ // 2025-01-18 is Saturday
62
+ expect(formatDate("ddd", { year: 2025, month: 1, day: 18 })).toBe("토");
63
+ // 2025-01-19 is Sunday
64
+ expect(formatDate("ddd", { year: 2025, month: 1, day: 19 })).toBe("일");
65
+ // 2025-01-20 is Monday
66
+ expect(formatDate("ddd", { year: 2025, month: 1, day: 20 })).toBe("월");
67
+ });
68
+ });
69
+
70
+ //#endregion
71
+
72
+ //#region Hour pattern
73
+
74
+ describe("Hour pattern", () => {
75
+ it("outputs 12-hour format with padding using hh format", () => {
76
+ expect(formatDate("hh", { hour: 0 })).toBe("12");
77
+ expect(formatDate("hh", { hour: 1 })).toBe("01");
78
+ expect(formatDate("hh", { hour: 12 })).toBe("12");
79
+ expect(formatDate("hh", { hour: 13 })).toBe("01");
80
+ });
81
+
82
+ it("outputs 12-hour format without padding using h format", () => {
83
+ expect(formatDate("h", { hour: 0 })).toBe("12");
84
+ expect(formatDate("h", { hour: 1 })).toBe("1");
85
+ expect(formatDate("h", { hour: 9 })).toBe("9");
86
+ expect(formatDate("h", { hour: 10 })).toBe("10");
87
+ expect(formatDate("h", { hour: 12 })).toBe("12");
88
+ expect(formatDate("h", { hour: 13 })).toBe("1");
89
+ expect(formatDate("h", { hour: 23 })).toBe("11");
90
+ });
91
+
92
+ it("outputs 24-hour format with padding using HH format", () => {
93
+ expect(formatDate("HH", { hour: 0 })).toBe("00");
94
+ expect(formatDate("HH", { hour: 9 })).toBe("09");
95
+ expect(formatDate("HH", { hour: 23 })).toBe("23");
96
+ });
97
+
98
+ it("outputs 24-hour format without padding using H format", () => {
99
+ expect(formatDate("H", { hour: 0 })).toBe("0");
100
+ expect(formatDate("H", { hour: 9 })).toBe("9");
101
+ expect(formatDate("H", { hour: 23 })).toBe("23");
102
+ });
103
+
104
+ it("outputs AM/PM with tt format", () => {
105
+ expect(formatDate("tt", { hour: 0 })).toBe("AM");
106
+ expect(formatDate("tt", { hour: 11 })).toBe("AM");
107
+ expect(formatDate("tt", { hour: 12 })).toBe("PM");
108
+ expect(formatDate("tt", { hour: 23 })).toBe("PM");
109
+ });
110
+ });
111
+
112
+ //#endregion
113
+
114
+ //#region Minute pattern
115
+
116
+ describe("Minute pattern", () => {
117
+ it("outputs 2-digit minute with mm format", () => {
118
+ expect(formatDate("mm", { minute: 0 })).toBe("00");
119
+ expect(formatDate("mm", { minute: 5 })).toBe("05");
120
+ expect(formatDate("mm", { minute: 59 })).toBe("59");
121
+ });
122
+
123
+ it("outputs minute without padding with m format", () => {
124
+ expect(formatDate("m", { minute: 0 })).toBe("0");
125
+ expect(formatDate("m", { minute: 5 })).toBe("5");
126
+ expect(formatDate("m", { minute: 59 })).toBe("59");
127
+ });
128
+ });
129
+
130
+ //#endregion
131
+
132
+ //#region Second pattern
133
+
134
+ describe("Second pattern", () => {
135
+ it("outputs 2-digit second with ss format", () => {
136
+ expect(formatDate("ss", { second: 0 })).toBe("00");
137
+ expect(formatDate("ss", { second: 5 })).toBe("05");
138
+ expect(formatDate("ss", { second: 59 })).toBe("59");
139
+ });
140
+
141
+ it("outputs second without padding with s format", () => {
142
+ expect(formatDate("s", { second: 0 })).toBe("0");
143
+ expect(formatDate("s", { second: 5 })).toBe("5");
144
+ expect(formatDate("s", { second: 59 })).toBe("59");
145
+ });
146
+ });
147
+
148
+ //#endregion
149
+
150
+ //#region Millisecond pattern
151
+
152
+ describe("Millisecond pattern", () => {
153
+ it("outputs 3-digit millisecond with fff format", () => {
154
+ expect(formatDate("fff", { millisecond: 0 })).toBe("000");
155
+ expect(formatDate("fff", { millisecond: 5 })).toBe("005");
156
+ expect(formatDate("fff", { millisecond: 50 })).toBe("050");
157
+ expect(formatDate("fff", { millisecond: 500 })).toBe("500");
158
+ expect(formatDate("fff", { millisecond: 999 })).toBe("999");
159
+ });
160
+
161
+ it("outputs 2-digit millisecond with ff format", () => {
162
+ expect(formatDate("ff", { millisecond: 0 })).toBe("00");
163
+ expect(formatDate("ff", { millisecond: 5 })).toBe("00");
164
+ expect(formatDate("ff", { millisecond: 50 })).toBe("05");
165
+ expect(formatDate("ff", { millisecond: 500 })).toBe("50");
166
+ expect(formatDate("ff", { millisecond: 999 })).toBe("99");
167
+ });
168
+
169
+ it("outputs 1-digit millisecond with f format", () => {
170
+ expect(formatDate("f", { millisecond: 0 })).toBe("0");
171
+ expect(formatDate("f", { millisecond: 5 })).toBe("0");
172
+ expect(formatDate("f", { millisecond: 100 })).toBe("1");
173
+ expect(formatDate("f", { millisecond: 500 })).toBe("5");
174
+ expect(formatDate("f", { millisecond: 999 })).toBe("9");
175
+ });
176
+ });
177
+
178
+ //#endregion
179
+
180
+ //#region Timezone pattern
181
+
182
+ describe("Timezone pattern", () => {
183
+ describe("Positive offset (East)", () => {
184
+ it("outputs +HH:mm format with zzz format", () => {
185
+ // UTC+9 (540 minutes)
186
+ expect(formatDate("zzz", { timezoneOffsetMinutes: 540 })).toBe("+09:00");
187
+ // UTC+5:30 (330 minutes)
188
+ expect(formatDate("zzz", { timezoneOffsetMinutes: 330 })).toBe("+05:30");
189
+ });
190
+
191
+ it("outputs +HH format with zz format", () => {
192
+ expect(formatDate("zz", { timezoneOffsetMinutes: 540 })).toBe("+09");
193
+ expect(formatDate("zz", { timezoneOffsetMinutes: 60 })).toBe("+01");
194
+ });
195
+
196
+ it("outputs +H format without padding with z format", () => {
197
+ expect(formatDate("z", { timezoneOffsetMinutes: 540 })).toBe("+9");
198
+ expect(formatDate("z", { timezoneOffsetMinutes: 60 })).toBe("+1");
199
+ expect(formatDate("z", { timezoneOffsetMinutes: 600 })).toBe("+10");
200
+ });
201
+ });
202
+
203
+ describe("Negative offset (West)", () => {
204
+ it("outputs -HH:mm format with zzz format", () => {
205
+ // UTC-5 (-300 minutes) - Integer hour offset
206
+ expect(formatDate("zzz", { timezoneOffsetMinutes: -300 })).toBe("-05:00");
207
+ // UTC-8 (-480 minutes) - Integer hour offset
208
+ expect(formatDate("zzz", { timezoneOffsetMinutes: -480 })).toBe("-08:00");
209
+ // UTC-3:30 (-210 minutes) - Newfoundland Standard Time
210
+ expect(formatDate("zzz", { timezoneOffsetMinutes: -210 })).toBe("-03:30");
211
+ // UTC-9:30 (-570 minutes) - Marquesas Islands
212
+ expect(formatDate("zzz", { timezoneOffsetMinutes: -570 })).toBe("-09:30");
213
+ });
214
+
215
+ it("outputs -HH format with zz format", () => {
216
+ expect(formatDate("zz", { timezoneOffsetMinutes: -300 })).toBe("-05");
217
+ expect(formatDate("zz", { timezoneOffsetMinutes: -60 })).toBe("-01");
218
+ });
219
+
220
+ it("outputs -H format without padding with z format", () => {
221
+ expect(formatDate("z", { timezoneOffsetMinutes: -300 })).toBe("-5");
222
+ expect(formatDate("z", { timezoneOffsetMinutes: -60 })).toBe("-1");
223
+ expect(formatDate("z", { timezoneOffsetMinutes: -720 })).toBe("-12");
224
+ });
225
+ });
226
+
227
+ describe("UTC (0 offset)", () => {
228
+ it("outputs +00:00 with zzz format", () => {
229
+ expect(formatDate("zzz", { timezoneOffsetMinutes: 0 })).toBe("+00:00");
230
+ });
231
+
232
+ it("outputs +00 with zz format", () => {
233
+ expect(formatDate("zz", { timezoneOffsetMinutes: 0 })).toBe("+00");
234
+ });
235
+
236
+ it("outputs +0 with z format", () => {
237
+ expect(formatDate("z", { timezoneOffsetMinutes: 0 })).toBe("+0");
238
+ });
239
+ });
240
+ });
241
+
242
+ //#endregion
243
+
244
+ //#endregion
245
+
246
+ //#region Complex format
247
+
248
+ describe("Complex format", () => {
249
+ it("handles full date/time format", () => {
250
+ const result = formatDate("yyyy-MM-dd HH:mm:ss.fff", {
251
+ year: 2025,
252
+ month: 1,
253
+ day: 18,
254
+ hour: 14,
255
+ minute: 30,
256
+ second: 45,
257
+ millisecond: 123,
258
+ });
259
+ expect(result).toBe("2025-01-18 14:30:45.123");
260
+ });
261
+
262
+ it("handles 12-hour format", () => {
263
+ const result = formatDate("yyyy-MM-dd tt h:mm:ss", {
264
+ year: 2025,
265
+ month: 1,
266
+ day: 18,
267
+ hour: 14,
268
+ minute: 5,
269
+ second: 9,
270
+ });
271
+ expect(result).toBe("2025-01-18 PM 2:05:09");
272
+ });
273
+
274
+ it("handles format with timezone", () => {
275
+ const result = formatDate("yyyy-MM-ddTHH:mm:sszzz", {
276
+ year: 2025,
277
+ month: 1,
278
+ day: 18,
279
+ hour: 14,
280
+ minute: 30,
281
+ second: 0,
282
+ timezoneOffsetMinutes: 540,
283
+ });
284
+ expect(result).toBe("2025-01-18T14:30:00+09:00");
285
+ });
286
+ });
287
+
288
+ //#endregion
289
+ });
290
+
291
+ describe("normalizeMonth", () => {
292
+ //#region Normal range
293
+
294
+ describe("Normal range (1-12)", () => {
295
+ it("returns unchanged if month is within 1-12 range", () => {
296
+ expect(normalizeMonth(2025, 1, 15)).toEqual({ year: 2025, month: 1, day: 15 });
297
+ expect(normalizeMonth(2025, 6, 15)).toEqual({ year: 2025, month: 6, day: 15 });
298
+ expect(normalizeMonth(2025, 12, 15)).toEqual({ year: 2025, month: 12, day: 15 });
299
+ });
300
+ });
301
+
302
+ //#endregion
303
+
304
+ //#region Month overflow
305
+
306
+ describe("Month overflow (13 or more)", () => {
307
+ it("month 13 becomes January next year", () => {
308
+ expect(normalizeMonth(2025, 13, 15)).toEqual({ year: 2026, month: 1, day: 15 });
309
+ });
310
+
311
+ it("month 14 becomes February next year", () => {
312
+ expect(normalizeMonth(2025, 14, 15)).toEqual({ year: 2026, month: 2, day: 15 });
313
+ });
314
+
315
+ it("month 25 becomes January 2 years later", () => {
316
+ expect(normalizeMonth(2025, 25, 15)).toEqual({ year: 2027, month: 1, day: 15 });
317
+ });
318
+
319
+ it("month 24 becomes December next year", () => {
320
+ expect(normalizeMonth(2025, 24, 15)).toEqual({ year: 2026, month: 12, day: 15 });
321
+ });
322
+ });
323
+
324
+ //#endregion
325
+
326
+ //#region Month underflow
327
+
328
+ describe("Month underflow (0 or less)", () => {
329
+ it("month 0 becomes December previous year", () => {
330
+ expect(normalizeMonth(2025, 0, 15)).toEqual({ year: 2024, month: 12, day: 15 });
331
+ });
332
+
333
+ it("month -1 becomes November previous year", () => {
334
+ expect(normalizeMonth(2025, -1, 15)).toEqual({ year: 2024, month: 11, day: 15 });
335
+ });
336
+
337
+ it("month -11 becomes January previous year", () => {
338
+ expect(normalizeMonth(2025, -11, 15)).toEqual({ year: 2024, month: 1, day: 15 });
339
+ });
340
+
341
+ it("month -12 becomes December 2 years ago", () => {
342
+ expect(normalizeMonth(2025, -12, 15)).toEqual({ year: 2023, month: 12, day: 15 });
343
+ });
344
+
345
+ it("month -13 becomes November 2 years ago", () => {
346
+ expect(normalizeMonth(2025, -13, 15)).toEqual({ year: 2023, month: 11, day: 15 });
347
+ });
348
+ });
349
+
350
+ //#endregion
351
+
352
+ //#region Day adjustment
353
+
354
+ describe("Day adjustment (target month's last day)", () => {
355
+ it("day 31 adjusted to 28 when changing to February (non-leap year)", () => {
356
+ expect(normalizeMonth(2025, 2, 31)).toEqual({ year: 2025, month: 2, day: 28 });
357
+ });
358
+
359
+ it("day 31 adjusted to 29 when changing to February (leap year)", () => {
360
+ expect(normalizeMonth(2024, 2, 31)).toEqual({ year: 2024, month: 2, day: 29 });
361
+ });
362
+
363
+ it("day 31 adjusted to 30 when changing to April", () => {
364
+ expect(normalizeMonth(2025, 4, 31)).toEqual({ year: 2025, month: 4, day: 30 });
365
+ });
366
+
367
+ it("day unchanged if less than target month's day count", () => {
368
+ expect(normalizeMonth(2025, 3, 15)).toEqual({ year: 2025, month: 3, day: 15 });
369
+ });
370
+ });
371
+
372
+ //#endregion
373
+ });