@digitalwalletcorp/utils 0.1.0 → 0.4.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 (104) hide show
  1. package/README.md +13 -12
  2. package/lib/crypto-util-DFJqfmuw.js +46 -0
  3. package/lib/crypto-util-DFJqfmuw.js.map +1 -0
  4. package/lib/crypto-util-DeZSGetI.cjs +2 -0
  5. package/lib/crypto-util-DeZSGetI.cjs.map +1 -0
  6. package/lib/crypto-util.cjs +2 -0
  7. package/lib/crypto-util.cjs.map +1 -0
  8. package/lib/crypto-util.d.cts +28 -0
  9. package/lib/crypto-util.d.ts +28 -0
  10. package/lib/crypto-util.js +7 -0
  11. package/lib/crypto-util.js.map +1 -0
  12. package/lib/date-util-CG7L8nSP.js +184 -0
  13. package/lib/date-util-CG7L8nSP.js.map +1 -0
  14. package/lib/date-util-DopNKy3u.cjs +2 -0
  15. package/lib/date-util-DopNKy3u.cjs.map +1 -0
  16. package/lib/date-util.cjs +2 -0
  17. package/lib/date-util.cjs.map +1 -0
  18. package/lib/date-util.d.cts +231 -0
  19. package/lib/date-util.d.ts +231 -0
  20. package/lib/date-util.js +23 -0
  21. package/lib/date-util.js.map +1 -0
  22. package/lib/decimal.cjs +2 -0
  23. package/lib/decimal.cjs.map +1 -0
  24. package/lib/decimal.d.cts +197 -0
  25. package/lib/decimal.d.ts +197 -0
  26. package/lib/decimal.js +227 -0
  27. package/lib/decimal.js.map +1 -0
  28. package/lib/file-util-D09BvDYY.cjs +2 -0
  29. package/lib/file-util-D09BvDYY.cjs.map +1 -0
  30. package/lib/file-util-H-c_eQcd.js +140 -0
  31. package/lib/file-util-H-c_eQcd.js.map +1 -0
  32. package/lib/file-util.cjs +2 -0
  33. package/lib/file-util.cjs.map +1 -0
  34. package/lib/file-util.d.cts +119 -0
  35. package/lib/file-util.d.ts +119 -0
  36. package/lib/file-util.js +18 -0
  37. package/lib/file-util.js.map +1 -0
  38. package/lib/html-util-Bxn5YDSK.cjs +2 -0
  39. package/lib/html-util-Bxn5YDSK.cjs.map +1 -0
  40. package/lib/html-util-CgMIuOsf.js +49 -0
  41. package/lib/html-util-CgMIuOsf.js.map +1 -0
  42. package/lib/html-util.cjs +2 -0
  43. package/lib/html-util.cjs.map +1 -0
  44. package/lib/html-util.d.cts +71 -0
  45. package/lib/html-util.d.ts +71 -0
  46. package/lib/html-util.js +8 -0
  47. package/lib/html-util.js.map +1 -0
  48. package/lib/index.cjs +2 -0
  49. package/lib/index.cjs.map +1 -0
  50. package/lib/index.d.cts +9 -0
  51. package/lib/index.d.ts +8 -0
  52. package/lib/index.js +21 -38
  53. package/lib/index.js.map +1 -1
  54. package/lib/json-util-BvrFYh5E.cjs +3 -0
  55. package/lib/json-util-BvrFYh5E.cjs.map +1 -0
  56. package/lib/json-util-YX7xSF6R.js +136 -0
  57. package/lib/json-util-YX7xSF6R.js.map +1 -0
  58. package/lib/json-util.cjs +2 -0
  59. package/lib/json-util.cjs.map +1 -0
  60. package/lib/json-util.d.cts +58 -0
  61. package/lib/json-util.d.ts +58 -0
  62. package/lib/json-util.js +8 -0
  63. package/lib/json-util.js.map +1 -0
  64. package/lib/number-util-BA4KVFyF.js +198 -0
  65. package/lib/number-util-BA4KVFyF.js.map +1 -0
  66. package/lib/number-util-BaSf1wWH.cjs +2 -0
  67. package/lib/number-util-BaSf1wWH.cjs.map +1 -0
  68. package/lib/number-util.cjs +2 -0
  69. package/lib/number-util.cjs.map +1 -0
  70. package/lib/number-util.d.cts +237 -0
  71. package/lib/number-util.d.ts +65 -51
  72. package/lib/number-util.js +23 -495
  73. package/lib/number-util.js.map +1 -1
  74. package/lib/object-util-DhGi-PmA.js +204 -0
  75. package/lib/object-util-DhGi-PmA.js.map +1 -0
  76. package/lib/object-util-Z0Ah5J8A.cjs +2 -0
  77. package/lib/object-util-Z0Ah5J8A.cjs.map +1 -0
  78. package/lib/object-util.cjs +2 -0
  79. package/lib/object-util.cjs.map +1 -0
  80. package/lib/object-util.d.cts +157 -0
  81. package/lib/object-util.d.ts +157 -0
  82. package/lib/object-util.js +16 -0
  83. package/lib/object-util.js.map +1 -0
  84. package/lib/string-util-Cw0VgbIH.js +52 -0
  85. package/lib/string-util-Cw0VgbIH.js.map +1 -0
  86. package/lib/string-util-w2I5EYuY.cjs +2 -0
  87. package/lib/string-util-w2I5EYuY.cjs.map +1 -0
  88. package/lib/string-util.cjs +2 -0
  89. package/lib/string-util.cjs.map +1 -0
  90. package/lib/string-util.d.cts +51 -0
  91. package/lib/string-util.d.ts +51 -0
  92. package/lib/string-util.js +8 -0
  93. package/lib/string-util.js.map +1 -0
  94. package/package.json +59 -16
  95. package/src/crypto-util.ts +84 -0
  96. package/src/date-util.ts +448 -0
  97. package/src/decimal.ts +294 -0
  98. package/src/file-util.ts +281 -0
  99. package/src/html-util.ts +196 -0
  100. package/src/index.ts +8 -0
  101. package/src/json-util.ts +271 -0
  102. package/src/number-util.ts +141 -194
  103. package/src/object-util.ts +482 -0
  104. package/src/string-util.ts +115 -0
@@ -0,0 +1,482 @@
1
+ import * as JsonUtil from './json-util';
2
+
3
+ /** コンストラクタの型 */
4
+ export type Constructor<T> = new (...args: any[]) => T;
5
+
6
+ /**
7
+ * マージのオプション
8
+ */
9
+ export interface MergeOptions {
10
+ /**
11
+ * 配列のマージ方法。既定は 'replace'
12
+ * ├ replace source側の配列の複製で置き換える
13
+ * ├ concat original側の配列にsource側の要素の複製を連結する
14
+ * └ indexMerge 添字ごとにマージする。source側より長い部分はoriginal側の要素が残る
15
+ */
16
+ arrays?: 'replace' | 'concat' | 'indexMerge';
17
+ }
18
+
19
+ interface MergeContext {
20
+ arrays: NonNullable<MergeOptions['arrays']>;
21
+ /** trueの場合、original側が自身のプロパティとして持つキーだけをマージの対象にする */
22
+ existingKeysOnly: boolean;
23
+ /** source側のオブジェクトと、それをマージした先のオブジェクトの対応。循環参照・共有参照を結果に保つために使う */
24
+ merged: WeakMap<object, any>;
25
+ }
26
+
27
+ const ARRAY_INDEX_KEY = /^(.+)\[(\d+)\]$/;
28
+
29
+ const isPlainObject = (value: unknown): value is Record<PropertyKey, any> => {
30
+ if (typeof value !== 'object' || value === null) {
31
+ return false;
32
+ }
33
+ const prototype = Object.getPrototypeOf(value);
34
+ return prototype === Object.prototype || prototype === null;
35
+ };
36
+
37
+ const functionToString = (fn: (...args: any[]) => any): string => fn.name ? `[Function: ${fn.name}]` : '[Function (anonymous)]';
38
+
39
+ /**
40
+ * 指定したクラスのインスタンスを生成する
41
+ *
42
+ * @param {Constructor<T>} creator クラス
43
+ * @param {any[]} args コンストラクタの引数
44
+ * @returns {T}
45
+ */
46
+ export function create<T>(creator: Constructor<T>, ...args: any[]): T {
47
+ return new creator(...args);
48
+ }
49
+
50
+ /**
51
+ * オブジェクトに階層構造で値を設定する
52
+ * 引数の最後の要素が値、それ以外がキーの階層である。キーが1つもない場合は何もしない
53
+ * 途中の階層が存在しない、またはオブジェクトでない場合は、新しいオブジェクトで置き換えて先へ進む
54
+ * 設定先がすでに配列の場合は、値を置き換えずに配列へ追加する。値が配列の場合はその要素を追加する
55
+ *
56
+ * @example
57
+ * const obj = {};
58
+ * appendObjectHierarchy(obj, 'a', 'b', 'c', 100); // { a: { b: { c: 100 } } }
59
+ * appendObjectHierarchy(obj, 'a', 'b', 'd', [1]); // { a: { b: { c: 100, d: [1] } } }
60
+ * appendObjectHierarchy(obj, 'a', 'b', 'd', [2]); // { a: { b: { c: 100, d: [1, 2] } } }
61
+ *
62
+ * @param {object} obj
63
+ * @param {any[]} args キーの階層と、最後に設定する値
64
+ */
65
+ export function appendObjectHierarchy(obj: Record<PropertyKey, any>, ...args: any[]): void {
66
+ if (args.length < 2) {
67
+ return;
68
+ }
69
+ const [key, ...rest] = args;
70
+ if (rest.length === 1) {
71
+ const value = rest[0];
72
+ if (Array.isArray(obj[key])) {
73
+ obj[key].push(...Array.isArray(value) ? value : [value]);
74
+ } else {
75
+ obj[key] = value;
76
+ }
77
+ return;
78
+ }
79
+ if (typeof obj[key] !== 'object' || obj[key] === null) {
80
+ obj[key] = {};
81
+ }
82
+ appendObjectHierarchy(obj[key], ...rest);
83
+ }
84
+
85
+ /**
86
+ * オブジェクトの階層キーをドット(.)で繋いで指定して値を取得する
87
+ * 配列の要素は [n] で指定する
88
+ * 該当するキーがない場合、または途中の値が null / undefined の場合は undefined を返す
89
+ *
90
+ * @example
91
+ * getValue({ a: { b: [1, 2, 3] } }, 'a.b[1]'); // 2
92
+ *
93
+ * @param {any} obj
94
+ * @param {string} key
95
+ * @returns {T | undefined}
96
+ */
97
+ export function getValue<T = any>(obj: any, key: string): T | undefined {
98
+ let current = obj;
99
+ for (const segment of key.split('.')) {
100
+ if (current == null) {
101
+ return undefined;
102
+ }
103
+ const matcher = ARRAY_INDEX_KEY.exec(segment);
104
+ if (matcher && Array.isArray(current[matcher[1]])) {
105
+ current = current[matcher[1]][Number(matcher[2])];
106
+ } else {
107
+ current = current[segment];
108
+ }
109
+ }
110
+ return current;
111
+ }
112
+
113
+ const cloneDeep = (value: any, seen: WeakMap<object, any>): any => {
114
+ if (typeof value !== 'object' || value === null) {
115
+ // プリミティブと関数はそのまま返す
116
+ return value;
117
+ }
118
+ if (seen.has(value)) {
119
+ return seen.get(value);
120
+ }
121
+ if (value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof Error) {
122
+ // 中身を列挙できない、または複製する意味のないオブジェクトは参照のまま返す
123
+ return value;
124
+ }
125
+ if (value instanceof Date) {
126
+ return new Date(value.getTime());
127
+ }
128
+ if (value instanceof RegExp) {
129
+ const clone = new RegExp(value.source, value.flags);
130
+ clone.lastIndex = value.lastIndex;
131
+ return clone;
132
+ }
133
+ if (value instanceof ArrayBuffer) {
134
+ return value.slice(0);
135
+ }
136
+ if (ArrayBuffer.isView(value)) {
137
+ if (value instanceof DataView) {
138
+ return new DataView(value.buffer.slice(0), value.byteOffset, value.byteLength);
139
+ }
140
+ return new (value.constructor as new (source: ArrayBufferView) => ArrayBufferView)(value);
141
+ }
142
+ if (typeof globalThis.Node !== 'undefined' && value instanceof globalThis.Node) {
143
+ const clone = value.cloneNode(true);
144
+ seen.set(value, clone);
145
+ return clone;
146
+ }
147
+ if (value instanceof Map) {
148
+ const clone = new Map();
149
+ seen.set(value, clone);
150
+ for (const [k, v] of value) {
151
+ clone.set(cloneDeep(k, seen), cloneDeep(v, seen));
152
+ }
153
+ return clone;
154
+ }
155
+ if (value instanceof Set) {
156
+ const clone = new Set();
157
+ seen.set(value, clone);
158
+ for (const v of value) {
159
+ clone.add(cloneDeep(v, seen));
160
+ }
161
+ return clone;
162
+ }
163
+ if (Array.isArray(value)) {
164
+ const clone: any[] = [];
165
+ seen.set(value, clone);
166
+ for (let i = 0; i < value.length; i++) {
167
+ clone[i] = cloneDeep(value[i], seen);
168
+ }
169
+ return clone;
170
+ }
171
+ // プレーンオブジェクトとクラスのインスタンス。プロトタイプを引き継ぎ、列挙可能な自身のプロパティを複製する
172
+ const clone = Object.create(Object.getPrototypeOf(value));
173
+ seen.set(value, clone);
174
+ for (const key of Reflect.ownKeys(value)) {
175
+ if (Object.prototype.propertyIsEnumerable.call(value, key)) {
176
+ clone[key] = cloneDeep(value[key], seen);
177
+ }
178
+ }
179
+ return clone;
180
+ };
181
+
182
+ /**
183
+ * 値を深く複製する
184
+ * ・プリミティブと関数はそのまま返す
185
+ * ・Date / RegExp / Map / Set / 配列 / ArrayBuffer / 型付き配列は複製する
186
+ * ・プレーンオブジェクトとクラスのインスタンスは、プロトタイプを引き継いで列挙可能な自身のプロパティを複製する
187
+ * ・DOMのNodeは cloneNode(true) で複製する
188
+ * ・WeakMap / WeakSet / Promise / Error は参照のまま返す
189
+ * ・循環参照と、同じオブジェクトを複数箇所から参照している構造は、複製後も保たれる
190
+ *
191
+ * @param {T} value
192
+ * @returns {T}
193
+ */
194
+ export function cloneObject<T>(value: T): T {
195
+ return cloneDeep(value, new WeakMap());
196
+ }
197
+
198
+ const mergeValue = (target: any, source: any, context: MergeContext): any => {
199
+ if (typeof source !== 'object' || source === null) {
200
+ // プリミティブ・関数・nullはsource側の値で置き換える
201
+ return source;
202
+ }
203
+ if (context.merged.has(source)) {
204
+ return context.merged.get(source);
205
+ }
206
+ if (isPlainObject(source) && isPlainObject(target)) {
207
+ mergeInto(target, source, context);
208
+ return target;
209
+ }
210
+ if (Array.isArray(source) && Array.isArray(target) && context.arrays !== 'replace') {
211
+ context.merged.set(source, target);
212
+ if (context.arrays === 'concat') {
213
+ for (const item of source) {
214
+ target.push(cloneObject(item));
215
+ }
216
+ } else {
217
+ for (let i = 0; i < source.length; i++) {
218
+ if (source[i] !== undefined) {
219
+ target[i] = mergeValue(target[i], source[i], context);
220
+ }
221
+ }
222
+ }
223
+ return target;
224
+ }
225
+ const clone = cloneObject(source);
226
+ context.merged.set(source, clone);
227
+ return clone;
228
+ };
229
+
230
+ const mergeInto = (target: Record<PropertyKey, any>, source: Record<PropertyKey, any>, context: MergeContext): void => {
231
+ context.merged.set(source, target);
232
+ for (const key of Object.keys(source)) {
233
+ if (source[key] === undefined) {
234
+ // undefinedは「指定なし」とみなしてoriginal側の値を残す
235
+ continue;
236
+ }
237
+ if (context.existingKeysOnly && !Object.hasOwn(target, key)) {
238
+ continue;
239
+ }
240
+ target[key] = mergeValue(target[key], source[key], context);
241
+ }
242
+ };
243
+
244
+ const mergeWithContext = <T extends object>(original: T, source: object | null | undefined, options: MergeOptions | undefined, existingKeysOnly: boolean): T => {
245
+ if (!isPlainObject(source)) {
246
+ return original;
247
+ }
248
+ mergeInto(original, source, {
249
+ arrays: options?.arrays ?? 'replace',
250
+ existingKeysOnly: existingKeysOnly,
251
+ merged: new WeakMap()
252
+ });
253
+ return original;
254
+ };
255
+
256
+ /**
257
+ * originalにsourceのプロパティを深くマージする(破壊)
258
+ * ・original側とsource側が両方プレーンオブジェクトの場合は再帰的にマージする
259
+ * ・source側が undefined のプロパティは「指定なし」とみなし、original側の値を残す
260
+ * ・それ以外はsource側の値の深い複製で置き換える。null は null を設定する
261
+ * ・配列は既定でsource側の複製で置き換える。options.arrays で連結・添字ごとのマージに変更できる
262
+ * ・source側の循環参照と、同じオブジェクトを複数箇所から参照している構造は、結果にも保たれる
263
+ * ・sourceが null / undefined / プレーンオブジェクト以外の場合は何もしない
264
+ *
265
+ * @param {T} original マージ先。この引数が書き換えられる
266
+ * @param {object | null | undefined} source マージするプロパティを持つオブジェクト
267
+ * @param {MergeOptions} [options]
268
+ * @returns {T} original
269
+ */
270
+ export function merge<T extends object>(original: T, source: object | null | undefined, options?: MergeOptions): T {
271
+ return mergeWithContext(original, source, options, false);
272
+ }
273
+
274
+ /**
275
+ * originalの複製にsourceのプロパティを深くマージした新しいオブジェクトを返す(非破壊)
276
+ * マージの規則は merge と同じ
277
+ *
278
+ * @param {T} original マージ元。この引数は書き換えない
279
+ * @param {object | null | undefined} source マージするプロパティを持つオブジェクト
280
+ * @param {MergeOptions} [options]
281
+ * @returns {T} マージした新しいオブジェクト
282
+ */
283
+ export function cloneMerge<T extends object>(original: T, source: object | null | undefined, options?: MergeOptions): T {
284
+ return mergeWithContext(cloneObject(original), source, options, false);
285
+ }
286
+
287
+ /**
288
+ * originalが自身のプロパティとして持つキーに限定して、sourceのプロパティを深くマージする(破壊)
289
+ * 値が undefined でもキーがあれば対象になる。入れ子のプレーンオブジェクトにも同じ限定を適用する
290
+ * original側が null やプリミティブで形を持たない場合は、source側の複製を設定する
291
+ * そのほかの規則は merge と同じ
292
+ *
293
+ * @param {T} original マージ先。この引数が書き換えられる
294
+ * @param {object | null | undefined} source マージするプロパティを持つオブジェクト
295
+ * @param {MergeOptions} [options]
296
+ * @returns {T} original
297
+ */
298
+ export function transfer<T extends object>(original: T, source: object | null | undefined, options?: MergeOptions): T {
299
+ return mergeWithContext(original, source, options, true);
300
+ }
301
+
302
+ /**
303
+ * originalの複製に対して、originalが持つキーに限定してsourceのプロパティを深くマージした新しいオブジェクトを返す(非破壊)
304
+ * マージの規則は transfer と同じ
305
+ *
306
+ * @param {T} original マージ元。この引数は書き換えない
307
+ * @param {object | null | undefined} source マージするプロパティを持つオブジェクト
308
+ * @param {MergeOptions} [options]
309
+ * @returns {T} マージした新しいオブジェクト
310
+ */
311
+ export function cloneTransfer<T extends object>(original: T, source: object | null | undefined, options?: MergeOptions): T {
312
+ return mergeWithContext(cloneObject(original), source, options, true);
313
+ }
314
+
315
+ const decodeBinary = (value: ArrayBuffer | ArrayBufferView): string => new TextDecoder().decode(value as any);
316
+
317
+ const mapToObject = (map: Map<any, any>): Record<string, any> => {
318
+ const obj: Record<string, any> = {};
319
+ for (const [key, value] of map) {
320
+ // キーはプロパティ名になるため、オブジェクトのキーは改行を入れずに1行のJSONにする
321
+ obj[typeof key === 'object' && key !== null ? JsonUtil.stringify(key, toStringReplacer, undefined, { circular: 'replace' }) : toString(key)] = value;
322
+ }
323
+ return obj;
324
+ };
325
+
326
+ /**
327
+ * JSON化できない値を人が読める形に変換するreplacer。入れ子の値にも適用される
328
+ */
329
+ function toStringReplacer(this: any, _key: string, value: any): any {
330
+ if (typeof value === 'function') {
331
+ return functionToString(value);
332
+ }
333
+ if (typeof value !== 'object' || value === null) {
334
+ return value;
335
+ }
336
+ if (value instanceof Error) {
337
+ return String(value);
338
+ }
339
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
340
+ return decodeBinary(value);
341
+ }
342
+ if (value instanceof Map) {
343
+ return mapToObject(value);
344
+ }
345
+ if (value instanceof Set) {
346
+ return Array.from(value);
347
+ }
348
+ return value;
349
+ }
350
+
351
+ /**
352
+ * 値を人が読める文字列にする。値の中身は変えない
353
+ * ・null / undefined は 'null' / 'undefined'
354
+ * ・文字列はそのまま
355
+ * ・number / boolean / bigint / symbol は String(value)
356
+ * ・関数は '[Function: name]'
357
+ * ・Error は 'name: message'。スタックは含めない
358
+ * ・ArrayBuffer / 型付き配列はUTF-8の文字列として復号する
359
+ * ・toJSON を持つオブジェクト(Dateなど)は toJSON() の結果を変換する
360
+ * ・Map は { key: value } のオブジェクト、Set は要素の配列としてJSON化する。Mapのキーもこの規則で文字列にする
361
+ * ・それ以外のオブジェクトと配列はインデント2のJSON。循環参照は '[Circular]' になる
362
+ * Error / 関数 / Map / Set / bigint の変換は、オブジェクトの中に入っている値にも適用する
363
+ *
364
+ * @param {any} value
365
+ * @returns {string}
366
+ */
367
+ export function toString(value: any): string {
368
+ if (value == null) {
369
+ return String(value);
370
+ }
371
+ switch (typeof value) {
372
+ case 'string':
373
+ return value;
374
+ case 'number':
375
+ case 'boolean':
376
+ case 'bigint':
377
+ case 'symbol':
378
+ return String(value);
379
+ case 'function':
380
+ return functionToString(value);
381
+ default:
382
+ }
383
+ if (value instanceof Error) {
384
+ return String(value);
385
+ }
386
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
387
+ return decodeBinary(value);
388
+ }
389
+ if (typeof value.toJSON === 'function') {
390
+ return toString(value.toJSON());
391
+ }
392
+ if (value instanceof Map) {
393
+ return toString(mapToObject(value));
394
+ }
395
+ if (value instanceof Set) {
396
+ return toString(Array.from(value));
397
+ }
398
+ return JsonUtil.stringify(value, toStringReplacer, 2, { circular: 'replace' });
399
+ }
400
+
401
+ /**
402
+ * 値の同一性を比較するための文字列を作るreplacer
403
+ * toStringReplacer と同じ変換を行ったうえで、プレーンオブジェクトはプロパティを名前順に並べ替える
404
+ */
405
+ function stableReplacer(this: any, key: string, value: any): any {
406
+ const converted = toStringReplacer.call(this, key, value);
407
+ if (!isPlainObject(converted)) {
408
+ return converted;
409
+ }
410
+ const sorted: Record<string, any> = {};
411
+ for (const name of Object.keys(converted).sort()) {
412
+ sorted[name] = converted[name];
413
+ }
414
+ return sorted;
415
+ }
416
+
417
+ /**
418
+ * 値の同一性を比較するための文字列にする
419
+ * オブジェクトは改行なしのJSONで、プレーンオブジェクトのプロパティは名前順になる。配列の順序は区別する
420
+ */
421
+ const toStableString = (value: any): string => {
422
+ return typeof value === 'object' && value !== null ? JsonUtil.stringify(value, stableReplacer, undefined, { circular: 'replace' }) : String(value);
423
+ };
424
+
425
+ const FNV_OFFSET_BASIS = 2166136261;
426
+ const FNV_PRIME = 16777619;
427
+
428
+ /**
429
+ * 値のハッシュ値を返す
430
+ * 値を toStableString で文字列にし、そのUTF-8のバイト列をFNV-1a(32bit)でハッシュ化する
431
+ * distinct で同じとみなされる値は同じハッシュ値になる
432
+ *
433
+ * @param {any} value
434
+ * @returns {number} 0 〜 4294967295 の整数
435
+ */
436
+ export function hashCode(value: any): number {
437
+ let hash = FNV_OFFSET_BASIS;
438
+ for (const byte of new TextEncoder().encode(toStableString(value))) {
439
+ hash = Math.imul(hash ^ byte, FNV_PRIME);
440
+ }
441
+ return hash >>> 0;
442
+ }
443
+
444
+ /**
445
+ * 配列から重複する要素を除いた新しい配列を返す
446
+ * ・null / undefined の要素は除く
447
+ * ・オブジェクトはJSON化した文字列で比較する。プロパティの順序は区別せず、配列の順序は区別する(hashCode と同じ文字列表現)
448
+ * ・プリミティブは型も区別する。'1' と 1 は別の要素として残る
449
+ *
450
+ * @example
451
+ * distinct([1, 3, 3, 2, 1]); // [1, 3, 2]
452
+ * distinct([{ id: 1, name: 'a' }, { id: 1, name: 'b' }], { distinct: a => a.id }); // [{ id: 1, name: 'a' }]
453
+ *
454
+ * @param {T[]} array
455
+ * @param {*} [options]
456
+ * ├ distinct 重複判定に使う値を返す関数。戻り値が null / undefined の要素は除く
457
+ * └ exclude trueを返した要素は除く
458
+ * @returns {T[]}
459
+ */
460
+ export function distinct<T>(array: T[], options?: {
461
+ distinct?: (a: T) => any;
462
+ exclude?: (a: T) => boolean;
463
+ }): T[] {
464
+ const result: T[] = [];
465
+ const keys = new Set<string>();
466
+ for (const value of array) {
467
+ if (value == null || options?.exclude?.(value)) {
468
+ continue;
469
+ }
470
+ const target = options?.distinct ? options.distinct(value) : value;
471
+ if (target == null) {
472
+ continue;
473
+ }
474
+ // 文字列表現が同じになる '1' と 1 を区別するため、プリミティブは型を付けてキーにする
475
+ const key = typeof target === 'object' ? toStableString(target) : `${typeof target}:${String(target)}`;
476
+ if (!keys.has(key)) {
477
+ keys.add(key);
478
+ result.push(value);
479
+ }
480
+ }
481
+ return result;
482
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * URLのクエリに指定できる値
3
+ */
4
+ export type QueryValue = string | number | boolean | undefined;
5
+
6
+ /**
7
+ * URLのクエリをキーと値の組で表したもの
8
+ */
9
+ export type QueryObject = Record<string, QueryValue | QueryValue[]>;
10
+
11
+ /**
12
+ * 文字列の前後の空白を取り除く
13
+ * null, undefined, 空文字の場合はそのまま元の値を返す
14
+ *
15
+ * @param {string} str
16
+ * @returns {string}
17
+ */
18
+ export function trim(str: string): string {
19
+ if (!str) {
20
+ return str;
21
+ }
22
+ return str.trim();
23
+ }
24
+
25
+ /**
26
+ * 文字列の先頭が英小文字の場合、先頭文字を大文字にする
27
+ * null, undefined, 空文字の場合はそのまま元の値を返す
28
+ *
29
+ * @param {string} str
30
+ * @returns {string}
31
+ */
32
+ export function capitalize(str: string): string {
33
+ if (!str) {
34
+ return str;
35
+ }
36
+ return str.replace(/^[a-z]/, c => c.toUpperCase());
37
+ }
38
+
39
+ /**
40
+ * 文字列内の {0} {1} の部分を、配列の添字に該当するパラメータで置換する
41
+ *
42
+ * @param {string} str 元の文字列
43
+ * @param {(string | number | boolean | symbol)[]} [replacements] 置換パラメータ
44
+ * @returns {string}
45
+ */
46
+ export function messageFormat(str: string, replacements?: (string | number | boolean | symbol)[]): string {
47
+ if (!str || !replacements || !replacements.length) {
48
+ return str;
49
+ }
50
+ let buf = str;
51
+ for (let i = 0; i < replacements.length; i++) {
52
+ const replacement = replacements[i];
53
+ const value = typeof replacement === 'symbol'
54
+ ? String(replacement.description)
55
+ : String(replacement);
56
+ buf = buf.replace(new RegExp(`\\{${i}\\}`, 'g'), value);
57
+ }
58
+ return buf;
59
+ }
60
+
61
+ /**
62
+ * base, segments, queryを連結してURLを生成する
63
+ *
64
+ * @param {*} options
65
+ * ├ base {string} https://example.com のようなプロトコル付きURL
66
+ * ├ segments {(string | number)[]} セグメント
67
+ * baseが未指定で配列の先頭要素がスラッシュで始まる場合、生成されるパスはスラッシュで始まる絶対パスを示す
68
+ * baseが未指定で配列の先頭要素がスラッシュで始まらない場合、生成されるパスはスラッシュで始まらない相対パスを示す
69
+ * baseが指定されている場合は、先頭要素のスラッシュの有無に関わらずbaseに続くURLとして生成する。URLクラスの挙動とは異なる
70
+ * ├ query {string | QueryObject}
71
+ * 文字列: ?date=2022-11-02&param=abc のようなクエリ。先頭に?がない場合は補完する。エンコードはしない
72
+ * キーと値の組: { date: '2022-11-02', param: 'abc' }。値が配列の場合は同じキーで複数指定する
73
+ * @returns {string}
74
+ */
75
+ export function buildUrl(options: {
76
+ base?: string;
77
+ segments?: (string | number)[];
78
+ query?: string | QueryObject;
79
+ }): string {
80
+ const base = options.base?.replace(/\/+$/, '') ?? '';
81
+ const segments = options.segments?.map((seg, idx) => {
82
+ let normalizedSegment = String(seg);
83
+ if (!options.base && idx === 0) {
84
+ // 絶対パスか相対パスかを表すため、baseがない場合の先頭要素だけはスラッシュを1つ残す
85
+ normalizedSegment = normalizedSegment.replace(/^\/+/g, '/');
86
+ } else {
87
+ normalizedSegment = normalizedSegment.replace(/^\/+/g, '');
88
+ }
89
+ return normalizedSegment.replace(/\/+$/g, '');
90
+ }) ?? [];
91
+ const path = [base, ...segments].filter(Boolean).join('/');
92
+
93
+ let qs = '';
94
+ if (options.query) {
95
+ if (typeof options.query === 'string') {
96
+ qs = options.query.startsWith('?') ? options.query : `?${options.query}`;
97
+ } else {
98
+ const params = new URLSearchParams();
99
+ for (const [key, value] of Object.entries(options.query)) {
100
+ if (Array.isArray(value)) {
101
+ for (const v of value) {
102
+ params.append(key, String(v));
103
+ }
104
+ } else if (value != null) {
105
+ params.append(key, String(value));
106
+ }
107
+ }
108
+ const strParams = params.toString();
109
+ if (strParams) {
110
+ qs = `?${strParams}`;
111
+ }
112
+ }
113
+ }
114
+ return `${path}${qs}`;
115
+ }