@zairakai/js-utils 1.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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/dist/arrays.cjs +210 -0
  4. package/dist/arrays.d.cts +119 -0
  5. package/dist/arrays.d.ts +119 -0
  6. package/dist/arrays.js +32 -0
  7. package/dist/chunk-27YHP2CK.js +407 -0
  8. package/dist/chunk-3WNRYKPG.js +37 -0
  9. package/dist/chunk-42CHLXT7.js +214 -0
  10. package/dist/chunk-6F4PWJZI.js +0 -0
  11. package/dist/chunk-7SXRFZBB.js +173 -0
  12. package/dist/chunk-F6RSTW65.js +156 -0
  13. package/dist/chunk-G7ZJ23DW.js +253 -0
  14. package/dist/chunk-IPP7PA6H.js +136 -0
  15. package/dist/chunk-LDSWHSRX.js +96 -0
  16. package/dist/chunk-TY75OOIQ.js +700 -0
  17. package/dist/chunk-W6JEMFAF.js +54 -0
  18. package/dist/chunk-XEJLBAXE.js +164 -0
  19. package/dist/chunk-Z7G3SIQH.js +270 -0
  20. package/dist/chunk-ZJPKS2MQ.js +101 -0
  21. package/dist/collections.cjs +797 -0
  22. package/dist/collections.d.cts +353 -0
  23. package/dist/collections.d.ts +353 -0
  24. package/dist/collections.js +17 -0
  25. package/dist/datetime.cjs +80 -0
  26. package/dist/datetime.d.cts +75 -0
  27. package/dist/datetime.d.ts +75 -0
  28. package/dist/datetime.js +24 -0
  29. package/dist/equals.cjs +121 -0
  30. package/dist/equals.d.cts +24 -0
  31. package/dist/equals.d.ts +24 -0
  32. package/dist/equals.js +8 -0
  33. package/dist/formatters.cjs +201 -0
  34. package/dist/formatters.d.cts +180 -0
  35. package/dist/formatters.d.ts +180 -0
  36. package/dist/formatters.js +48 -0
  37. package/dist/index.cjs +2906 -0
  38. package/dist/index.d.cts +120 -0
  39. package/dist/index.d.ts +120 -0
  40. package/dist/index.js +348 -0
  41. package/dist/number.cjs +279 -0
  42. package/dist/number.d.cts +177 -0
  43. package/dist/number.d.ts +177 -0
  44. package/dist/number.js +10 -0
  45. package/dist/obj.cjs +427 -0
  46. package/dist/obj.d.cts +177 -0
  47. package/dist/obj.d.ts +177 -0
  48. package/dist/obj.js +12 -0
  49. package/dist/php-arrays.cjs +954 -0
  50. package/dist/php-arrays.d.cts +256 -0
  51. package/dist/php-arrays.d.ts +256 -0
  52. package/dist/php-arrays.js +70 -0
  53. package/dist/runtime.cjs +134 -0
  54. package/dist/runtime.d.cts +90 -0
  55. package/dist/runtime.d.ts +90 -0
  56. package/dist/runtime.js +24 -0
  57. package/dist/schemas.cjs +86 -0
  58. package/dist/schemas.d.cts +108 -0
  59. package/dist/schemas.d.ts +108 -0
  60. package/dist/schemas.js +22 -0
  61. package/dist/str.cjs +499 -0
  62. package/dist/str.d.cts +282 -0
  63. package/dist/str.d.ts +282 -0
  64. package/dist/str.js +11 -0
  65. package/dist/types.cjs +18 -0
  66. package/dist/types.d.cts +13 -0
  67. package/dist/types.d.ts +13 -0
  68. package/dist/types.js +1 -0
  69. package/dist/validator.cjs +251 -0
  70. package/dist/validator.d.cts +99 -0
  71. package/dist/validator.d.ts +99 -0
  72. package/dist/validator.js +11 -0
  73. package/dist/validators.cjs +217 -0
  74. package/dist/validators.d.cts +216 -0
  75. package/dist/validators.d.ts +216 -0
  76. package/dist/validators.js +64 -0
  77. package/package.json +180 -0
  78. package/src/arrays.ts +316 -0
  79. package/src/collections.ts +866 -0
  80. package/src/datetime.ts +103 -0
  81. package/src/equals.ts +134 -0
  82. package/src/formatters.ts +342 -0
  83. package/src/index.ts +36 -0
  84. package/src/number.ts +281 -0
  85. package/src/obj.ts +303 -0
  86. package/src/php-arrays.ts +445 -0
  87. package/src/pipe.ts +29 -0
  88. package/src/runtime.ts +194 -0
  89. package/src/schemas.ts +136 -0
  90. package/src/str.ts +438 -0
  91. package/src/types.ts +13 -0
  92. package/src/validator.ts +157 -0
  93. package/src/validators.ts +359 -0
@@ -0,0 +1,866 @@
1
+ /**
2
+ * Collection utilities for JavaScript arrays and objects
3
+ * Inspired by Laravel Collections with modern JavaScript features
4
+ */
5
+
6
+ import { isEqual } from './equals.js'
7
+
8
+ // Type definitions
9
+ /**
10
+ * Generic callback type for collection operations.
11
+ */
12
+ export type Callback<T, R> = (item: T, index: number) => R
13
+
14
+ /**
15
+ * Predicate type for collection filtering and validation.
16
+ */
17
+ export type Predicate<T> = (item: T, index: number) => boolean
18
+
19
+ /**
20
+ * Enhanced Array class with Laravel Collection-like methods.
21
+ * Extends the native Array class with additional utility methods for manipulation,
22
+ * validation, and statistical analysis.
23
+ */
24
+ export class EnhancedArray<T> extends Array<T> {
25
+ protected originalItems: T[]
26
+
27
+ /**
28
+ * Create a new EnhancedArray instance.
29
+ *
30
+ * @param {...T[]} items The items to initialize the collection with
31
+ */
32
+ constructor(...items: T[]) {
33
+ super(...items)
34
+ Object.setPrototypeOf(this, EnhancedArray.prototype)
35
+ this.originalItems = structuredClone(items)
36
+ }
37
+
38
+ /**
39
+ * Determine if the collection has been modified since initialization or last sync.
40
+ *
41
+ * @returns {boolean} True if modified, false otherwise
42
+ */
43
+ isDirty(): boolean {
44
+ return !isEqual(this.originalItems, Array.from(this))
45
+ }
46
+
47
+ /**
48
+ * Determine if the collection is clean (not modified).
49
+ *
50
+ * @returns {boolean} True if not modified, false otherwise
51
+ */
52
+ isClean(): boolean {
53
+ return !this.isDirty()
54
+ }
55
+
56
+ /**
57
+ * Sync the original items with current items to mark the collection as clean.
58
+ *
59
+ * @returns {this} The collection instance
60
+ */
61
+ syncOriginal(): this {
62
+ this.originalItems = structuredClone(Array.from(this))
63
+ return this
64
+ }
65
+
66
+ /**
67
+ * Compare with another array or collection for equality.
68
+ *
69
+ * @param {T[] | EnhancedArray<T>} other The array or collection to compare with
70
+ * @returns {boolean} True if equivalent, false otherwise
71
+ */
72
+ isEquivalent(other: T[] | EnhancedArray<T>): boolean {
73
+ return isEqual(Array.from(this), Array.from(other))
74
+ }
75
+
76
+ /**
77
+ * Create an EnhancedArray from an iterable or array-like object.
78
+ *
79
+ * @param {Iterable<T> | ArrayLike<T>} items The items to convert
80
+ * @returns {EnhancedArray<T>} A new EnhancedArray instance
81
+ */
82
+ static from<T>(items: Iterable<T> | ArrayLike<T>): EnhancedArray<T> {
83
+ return new EnhancedArray(...Array.from(items))
84
+ }
85
+
86
+ /**
87
+ * Create an EnhancedArray from a variable number of arguments.
88
+ *
89
+ * @param {...T[]} items The items to include
90
+ * @returns {EnhancedArray<T>} A new EnhancedArray instance
91
+ */
92
+ static of<T>(...items: T[]): EnhancedArray<T> {
93
+ return new EnhancedArray(...items)
94
+ }
95
+
96
+ // Laravel Collection inspired methods
97
+ /**
98
+ * Create a new collection instance from the current items.
99
+ *
100
+ * @returns {EnhancedArray<T>} A new EnhancedArray instance
101
+ */
102
+ collect(): EnhancedArray<T> {
103
+ return new EnhancedArray(...this)
104
+ }
105
+
106
+ /**
107
+ * Pluck an array of values from an array of objects.
108
+ *
109
+ * @param {K} key The key to pluck
110
+ * @returns {EnhancedArray<T[K]>} A new collection of plucked values
111
+ */
112
+ pluck<K extends keyof T>(key: K): EnhancedArray<T[K]> {
113
+ return new EnhancedArray(
114
+ ...(this.map((item) => (item && 'object' === typeof item ? item[key] : undefined)).filter(
115
+ (val) => val !== undefined
116
+ ) as T[K][])
117
+ )
118
+ }
119
+
120
+ /**
121
+ * Group the collection's items by a given key or callback.
122
+ *
123
+ * @param {K | Callback<T, string | number>} key The key or callback to group by
124
+ * @returns {Record<string, EnhancedArray<T>>} A record of grouped collections
125
+ */
126
+ groupBy<K extends keyof T>(key: K | Callback<T, string | number>): Record<string, EnhancedArray<T>> {
127
+ const groups: Record<string, EnhancedArray<T>> = {}
128
+
129
+ for (let i = 0; i < this.length; i++) {
130
+ const item = this[i]
131
+ const groupKey =
132
+ 'function' === typeof key ? String(key(item, i)) : String(item && 'object' === typeof item ? item[key] : item)
133
+
134
+ if (!groups[groupKey]) {
135
+ groups[groupKey] = new EnhancedArray<T>()
136
+ }
137
+ groups[groupKey].push(item)
138
+ }
139
+
140
+ return groups
141
+ }
142
+
143
+ /**
144
+ * Return only unique items from the collection.
145
+ *
146
+ * @param {K} [key] Optional key to determine uniqueness for objects
147
+ * @returns {EnhancedArray<T>} A new collection of unique items
148
+ */
149
+ unique<K extends keyof T>(key?: K): EnhancedArray<T> {
150
+ if (!key) {
151
+ return new EnhancedArray(...new Set(this))
152
+ }
153
+
154
+ const seen = new Set()
155
+ return new EnhancedArray(
156
+ ...this.filter((item) => {
157
+ const value = item && 'object' === typeof item ? item[key] : item
158
+ if (seen.has(value)) {
159
+ return false
160
+ }
161
+ seen.add(value)
162
+ return true
163
+ })
164
+ )
165
+ }
166
+
167
+ /**
168
+ * Chunk the collection into smaller collections of a given size.
169
+ *
170
+ * @param {number} size The size of each chunk
171
+ * @returns {EnhancedArray<EnhancedArray<T>>} A collection of chunked collections
172
+ */
173
+ chunk(size: number): EnhancedArray<EnhancedArray<T>> {
174
+ const chunks = new EnhancedArray<EnhancedArray<T>>()
175
+ for (let i = 0; i < this.length; i += size) {
176
+ chunks.push(new EnhancedArray(...this.slice(i, i + size)))
177
+ }
178
+ return chunks
179
+ }
180
+
181
+ // Advanced manipulation methods
182
+ /**
183
+ * Flatten a multi-dimensional collection into a single level.
184
+ *
185
+ * @param {number} [depth=Infinity] The depth to flatten to
186
+ * @returns {EnhancedArray<unknown>} A new flattened collection
187
+ */
188
+ deepFlatten(depth: number = Infinity): EnhancedArray<unknown> {
189
+ const flatten = (arr: unknown[], currentDepth: number): unknown[] => {
190
+ const result: unknown[] = []
191
+
192
+ for (const item of arr) {
193
+ if (Array.isArray(item) && 0 < currentDepth) {
194
+ result.push(...flatten(item, currentDepth - 1))
195
+ } else {
196
+ result.push(item)
197
+ }
198
+ }
199
+
200
+ return result
201
+ }
202
+
203
+ return new EnhancedArray(...flatten(this, depth))
204
+ }
205
+
206
+ /**
207
+ * Rotate the collection by a given number of positions.
208
+ *
209
+ * @param {number} [positions=1] The number of positions to rotate
210
+ * @returns {EnhancedArray<T>} A new rotated collection
211
+ */
212
+ rotate(positions: number = 1): EnhancedArray<T> {
213
+ if (0 === this.length) {
214
+ return new EnhancedArray<T>()
215
+ }
216
+
217
+ const count = this.length
218
+ positions = positions % count
219
+
220
+ if (0 === positions) {
221
+ return this.collect()
222
+ }
223
+ if (0 > positions) {
224
+ positions = count + positions
225
+ }
226
+
227
+ return new EnhancedArray(...this.slice(positions), ...this.slice(0, positions))
228
+ }
229
+
230
+ /**
231
+ * Chunk the collection as long as the given predicate returns true.
232
+ *
233
+ * @param {Predicate<T>} predicate The predicate to check
234
+ * @returns {EnhancedArray<EnhancedArray<T>>} A collection of chunked collections
235
+ */
236
+ chunkWhile(predicate: Predicate<T>): EnhancedArray<EnhancedArray<T>> {
237
+ const chunks = new EnhancedArray<EnhancedArray<T>>()
238
+ let currentChunk = new EnhancedArray<T>()
239
+
240
+ for (let i = 0; i < this.length; i++) {
241
+ const item = this[i]
242
+
243
+ if (0 < currentChunk.length && !predicate(item, i)) {
244
+ chunks.push(currentChunk)
245
+ currentChunk = new EnhancedArray<T>()
246
+ }
247
+
248
+ currentChunk.push(item)
249
+ }
250
+
251
+ if (0 < currentChunk.length) {
252
+ chunks.push(currentChunk)
253
+ }
254
+
255
+ return chunks
256
+ }
257
+
258
+ /**
259
+ * Transpose the collection (swap rows and columns).
260
+ *
261
+ * @returns {EnhancedArray<EnhancedArray<unknown>>} A new transposed collection
262
+ */
263
+ transpose(): EnhancedArray<EnhancedArray<unknown>> {
264
+ if (0 === this.length) {
265
+ return new EnhancedArray()
266
+ }
267
+
268
+ const maxLength = Math.max(...this.map((row) => (Array.isArray(row) ? row.length : 1)))
269
+
270
+ const result = new EnhancedArray<EnhancedArray<unknown>>()
271
+
272
+ for (let i = 0; i < maxLength; i++) {
273
+ const column = new EnhancedArray(
274
+ ...this.map((row) => {
275
+ if (Array.isArray(row)) {
276
+ return row[i] ?? null
277
+ }
278
+ return 0 === i ? row : null
279
+ })
280
+ )
281
+
282
+ result.push(column)
283
+ }
284
+
285
+ return result
286
+ }
287
+
288
+ // Validation methods
289
+ /**
290
+ * Check if the collection is sorted.
291
+ *
292
+ * @param {Function} [compareFn] Optional comparison function
293
+ * @returns {boolean} True if sorted, false otherwise
294
+ */
295
+ isSorted(compareFn?: (a: T, b: T) => number): boolean {
296
+ if (1 >= this.length) {
297
+ return true
298
+ }
299
+
300
+ for (let i = 1; i < this.length; i++) {
301
+ const prev = this[i - 1]
302
+ const current = this[i]
303
+
304
+ if (compareFn) {
305
+ if (0 < compareFn(prev, current)) {
306
+ return false
307
+ }
308
+ } else {
309
+ if (prev > current) {
310
+ return false
311
+ }
312
+ }
313
+ }
314
+
315
+ return true
316
+ }
317
+
318
+ /**
319
+ * Check if the collection contains duplicate items.
320
+ *
321
+ * @param {K} [key] Optional key to check for uniqueness in objects
322
+ * @returns {boolean} True if duplicates found, false otherwise
323
+ */
324
+ hasDuplicates<K extends keyof T>(key?: K): boolean {
325
+ if (key) {
326
+ const seen = new Set()
327
+ for (const item of this) {
328
+ const value = item && 'object' === typeof item ? item[key] : item
329
+ if (seen.has(value)) {
330
+ return true
331
+ }
332
+ seen.add(value)
333
+ }
334
+ return false
335
+ }
336
+
337
+ return this.length !== new Set(this).size
338
+ }
339
+
340
+ // Statistical methods
341
+ /**
342
+ * Calculate the median value of the collection.
343
+ *
344
+ * @param {Callback<T, number>} [accessor] Optional accessor for non-numeric items
345
+ * @returns {number | null} The median value or null if collection is empty
346
+ */
347
+ median(accessor?: Callback<T, number>): number | null {
348
+ if (0 === this.length) {
349
+ return null
350
+ }
351
+
352
+ const values = accessor ? this.map(accessor) : (this as unknown as number[])
353
+ const sorted = [...values].sort((a, b) => (a as number) - (b as number))
354
+ const count = sorted.length
355
+
356
+ if (0 === count % 2) {
357
+ return ((sorted[count / 2 - 1] as number) + (sorted[count / 2] as number)) / 2
358
+ }
359
+
360
+ return sorted[Math.floor(count / 2)] as number
361
+ }
362
+
363
+ /**
364
+ * Calculate the mode (most frequent values) of the collection.
365
+ *
366
+ * @param {Callback<T, unknown>} [accessor] Optional accessor for the values
367
+ * @returns {EnhancedArray<unknown>} A collection of mode values
368
+ */
369
+ mode(accessor?: Callback<T, unknown>): EnhancedArray<unknown> {
370
+ if (0 === this.length) {
371
+ return new EnhancedArray()
372
+ }
373
+
374
+ const frequencies = this.frequencies(accessor)
375
+ const maxFrequency = Math.max(...Object.values(frequencies))
376
+
377
+ return new EnhancedArray(...Object.keys(frequencies).filter((key) => frequencies[key] === maxFrequency))
378
+ }
379
+
380
+ /**
381
+ * Calculate the standard deviation of the collection.
382
+ *
383
+ * @param {Callback<T, number>} [accessor] Optional accessor for non-numeric items
384
+ * @returns {number} The standard deviation
385
+ */
386
+ standardDeviation(accessor?: Callback<T, number>): number {
387
+ if (0 === this.length) {
388
+ return 0
389
+ }
390
+
391
+ const values = accessor ? this.map(accessor) : (this as unknown as number[])
392
+ const mean = (values as number[]).reduce((sum, val) => (sum as number) + (val as number), 0) / values.length
393
+ const sumSquaredDifferences = (values as number[]).reduce(
394
+ (sum, val) => (sum as number) + Math.pow((val as number) - mean, 2),
395
+ 0
396
+ )
397
+
398
+ return Math.sqrt((sumSquaredDifferences as number) / values.length)
399
+ }
400
+
401
+ /**
402
+ * Calculate a specific percentile of the collection.
403
+ *
404
+ * @param {number} percentile The percentile to calculate (0-100)
405
+ * @param {Callback<T, number>} [accessor] Optional accessor for non-numeric items
406
+ * @returns {number | null} The percentile value or null if collection is empty
407
+ */
408
+ percentile(percentile: number, accessor?: Callback<T, number>): number | null {
409
+ if (0 === this.length) {
410
+ return null
411
+ }
412
+
413
+ const values = accessor ? this.map(accessor) : (this as unknown as number[])
414
+ const sorted = [...values].sort((a, b) => (a as number) - (b as number))
415
+ const index = (percentile / 100) * (sorted.length - 1)
416
+ const lower = Math.floor(index)
417
+ const upper = Math.ceil(index)
418
+
419
+ if (lower === upper) {
420
+ return sorted[lower] as number
421
+ }
422
+
423
+ const weight = index - lower
424
+ return (sorted[lower] as number) * (1 - weight) + (sorted[upper] as number) * weight
425
+ }
426
+
427
+ /**
428
+ * Calculate the frequencies of items in the collection.
429
+ *
430
+ * @param {Callback<T, unknown>} [accessor] Optional accessor for the values
431
+ * @returns {Record<string, number>} A record of values and their frequencies
432
+ */
433
+ frequencies(accessor?: Callback<T, unknown>): Record<string, number> {
434
+ const frequencies: Record<string, number> = {}
435
+
436
+ for (let i = 0; i < this.length; i++) {
437
+ const item = this[i]
438
+ const key = accessor ? String(accessor(item, i)) : String(item)
439
+ frequencies[key] = (frequencies[key] || 0) + 1
440
+ }
441
+
442
+ return frequencies
443
+ }
444
+
445
+ // Advanced operations
446
+ /**
447
+ * Calculate the Cartesian product of the collection and other arrays.
448
+ *
449
+ * @param {...U[][]} arrays The arrays to calculate the product with
450
+ * @returns {EnhancedArray<[T, ...U[]]>} A collection of product combinations
451
+ */
452
+ cartesian<U>(...arrays: U[][]): EnhancedArray<[T, ...U[]]> {
453
+ const result = new EnhancedArray<[T, ...U[]]>()
454
+
455
+ const allArrays = [this, ...arrays]
456
+
457
+ function* cartesianProduct(arrays: unknown[][], index = 0, current: unknown[] = []): Generator<unknown[]> {
458
+ if (index === arrays.length) {
459
+ yield [...current]
460
+ return
461
+ }
462
+
463
+ for (const item of arrays[index]) {
464
+ yield* cartesianProduct(arrays, index + 1, [...current, item])
465
+ }
466
+ }
467
+
468
+ for (const combination of cartesianProduct(allArrays)) {
469
+ result.push(combination as [T, ...U[]])
470
+ }
471
+
472
+ return result
473
+ }
474
+
475
+ /**
476
+ * Interleave the collection with other arrays.
477
+ *
478
+ * @param {...U[][]} arrays The arrays to interleave with
479
+ * @returns {EnhancedArray<T | U>} A new interleaved collection
480
+ */
481
+ interleave<U>(...arrays: U[][]): EnhancedArray<T | U> {
482
+ const result = new EnhancedArray<T | U>()
483
+ const allArrays = [this, ...arrays]
484
+ const maxLength = Math.max(...allArrays.map((arr) => arr.length))
485
+
486
+ for (let i = 0; i < maxLength; i++) {
487
+ for (const array of allArrays) {
488
+ if (i < array.length) {
489
+ result.push(array[i])
490
+ }
491
+ }
492
+ }
493
+
494
+ return result
495
+ }
496
+
497
+ /**
498
+ * Get a sliding window of items from the collection.
499
+ *
500
+ * @param {number} [size=2] The size of the window
501
+ * @param {number} [step=1] The step between windows
502
+ * @returns {EnhancedArray<EnhancedArray<T>>} A collection of window collections
503
+ */
504
+ sliding(size: number = 2, step: number = 1): EnhancedArray<EnhancedArray<T>> {
505
+ if (0 >= size || 0 >= step) {
506
+ return new EnhancedArray()
507
+ }
508
+
509
+ const windows = new EnhancedArray<EnhancedArray<T>>()
510
+
511
+ for (let i = 0; i <= this.length - size; i += step) {
512
+ windows.push(new EnhancedArray(...this.slice(i, i + size)))
513
+ }
514
+
515
+ return windows
516
+ }
517
+
518
+ // Navigation macros
519
+ /**
520
+ * Get the item at the given index.
521
+ *
522
+ * @param {number} index The index to retrieve
523
+ * @returns {T | undefined} The item at the index or undefined
524
+ */
525
+ at(index: number): T | undefined {
526
+ return this[index]
527
+ }
528
+
529
+ /**
530
+ * Get the item before the given item.
531
+ *
532
+ * @param {T} item The item to search for
533
+ * @returns {T | undefined} The item before or undefined
534
+ */
535
+ before(item: T): T | undefined {
536
+ const index = this.indexOf(item)
537
+ return 0 < index ? this[index - 1] : undefined
538
+ }
539
+
540
+ /**
541
+ * Get the item after the given item.
542
+ *
543
+ * @param {T} item The item to search for
544
+ * @returns {T | undefined} The item after or undefined
545
+ */
546
+ after(item: T): T | undefined {
547
+ const index = this.indexOf(item)
548
+ return 0 <= index && index < this.length - 1 ? this[index + 1] : undefined
549
+ }
550
+
551
+ // Transformation macros
552
+ /**
553
+ * Chunk the collection by a given key or callback when its value changes.
554
+ *
555
+ * @param {K | Callback<T, unknown>} key The key or callback to chunk by
556
+ * @returns {EnhancedArray<EnhancedArray<T>>} A collection of chunked collections
557
+ */
558
+ chunkBy<K extends keyof T>(key: K | Callback<T, unknown>): EnhancedArray<EnhancedArray<T>> {
559
+ if (0 === this.length) {
560
+ return new EnhancedArray()
561
+ }
562
+
563
+ const chunks = new EnhancedArray<EnhancedArray<T>>()
564
+ let currentChunk = new EnhancedArray<T>()
565
+ let lastValue: unknown = null
566
+
567
+ for (let i = 0; i < this.length; i++) {
568
+ const item = this[i]
569
+ const currentValue =
570
+ 'function' === typeof key ? key(item, i) : item && 'object' === typeof item ? item[key] : item
571
+
572
+ if (0 === i || currentValue === lastValue) {
573
+ currentChunk.push(item)
574
+ } else {
575
+ chunks.push(currentChunk)
576
+ currentChunk = new EnhancedArray<T>(item)
577
+ }
578
+
579
+ lastValue = currentValue
580
+ }
581
+
582
+ if (0 < currentChunk.length) {
583
+ chunks.push(currentChunk)
584
+ }
585
+
586
+ return chunks
587
+ }
588
+
589
+ /**
590
+ * Map and filter the collection simultaneously.
591
+ * Items for which the callback returns null or undefined are removed.
592
+ *
593
+ * @param {Function} callback The callback to map items
594
+ * @returns {EnhancedArray<U>} A new mapped and filtered collection
595
+ */
596
+ filterMap<U>(callback: (item: T, index: number) => U | null | undefined): EnhancedArray<U> {
597
+ const result = new EnhancedArray<U>()
598
+
599
+ for (let i = 0; i < this.length; i++) {
600
+ const mapped = callback(this[i], i)
601
+ if (null !== mapped && mapped !== undefined) {
602
+ result.push(mapped)
603
+ }
604
+ }
605
+
606
+ return result
607
+ }
608
+
609
+ /**
610
+ * Extract a subset of properties from each item in the collection.
611
+ *
612
+ * @param {K[]} keys The keys to extract
613
+ * @returns {EnhancedArray<Partial<T>>} A new collection of partial items
614
+ */
615
+ extract<K extends keyof T>(keys: K[]): EnhancedArray<Partial<T>> {
616
+ return new EnhancedArray(
617
+ ...this.map((item) => {
618
+ if (!item || 'object' !== typeof item) {
619
+ return {} as Partial<T>
620
+ }
621
+
622
+ const extracted: Partial<T> = {}
623
+ for (const key of keys) {
624
+ if (key in item) {
625
+ extracted[key] = item[key]
626
+ }
627
+ }
628
+ return extracted
629
+ })
630
+ )
631
+ }
632
+
633
+ // Utility macros
634
+ /**
635
+ * Get the first item or push a default item if the collection is empty.
636
+ *
637
+ * @param {T} item The default item to push if empty
638
+ * @returns {T} The first item or the pushed item
639
+ */
640
+ firstOrPush(item: T): T {
641
+ if (0 === this.length) {
642
+ this.push(item)
643
+ return item
644
+ }
645
+ return this[0]
646
+ }
647
+
648
+ /**
649
+ * Get every n-th item from the collection.
650
+ *
651
+ * @param {number} n The step size
652
+ * @returns {EnhancedArray<T>} A new collection of every n-th item
653
+ */
654
+ getNth(n: number): EnhancedArray<T> {
655
+ const result = new EnhancedArray<T>()
656
+ for (let i = n - 1; i < this.length; i += n) {
657
+ result.push(this[i])
658
+ }
659
+ return result
660
+ }
661
+
662
+ /**
663
+ * Get a random item from the collection based on weights.
664
+ *
665
+ * @param {number[]} [weights] Optional weights for each item
666
+ * @returns {T | undefined} A random item or undefined if collection is empty
667
+ */
668
+ weightedRandom(weights?: number[]): T | undefined {
669
+ if (0 === this.length) {
670
+ return undefined
671
+ }
672
+
673
+ const effectiveWeights = weights ?? new Array(this.length).fill(1)
674
+ if (effectiveWeights.length !== this.length) {
675
+ throw new Error('Weights array must have same length as collection')
676
+ }
677
+
678
+ const totalWeight = effectiveWeights.reduce((sum, weight) => sum + weight, 0)
679
+ const random = Math.random() * totalWeight
680
+
681
+ let currentWeight = 0
682
+ for (let i = 0; i < this.length; i++) {
683
+ currentWeight += effectiveWeights[i]
684
+ if (random <= currentWeight) {
685
+ return this[i]
686
+ }
687
+ }
688
+
689
+ return this[this.length - 1]
690
+ }
691
+
692
+ /**
693
+ * Paginate the collection items.
694
+ *
695
+ * @param {number} perPage Items per page
696
+ * @param {number} [page=1] Current page number
697
+ * @returns {Object} Pagination results
698
+ */
699
+ paginate(
700
+ perPage: number,
701
+ page: number = 1
702
+ ): {
703
+ data: EnhancedArray<T>
704
+ total: number
705
+ perPage: number
706
+ currentPage: number
707
+ lastPage: number
708
+ from: number
709
+ to: number
710
+ } {
711
+ const total = this.length
712
+ const lastPage = Math.ceil(total / perPage)
713
+ const currentPage = Math.max(1, Math.min(page, lastPage))
714
+ const from = (currentPage - 1) * perPage
715
+ const to = Math.min(from + perPage, total)
716
+
717
+ return {
718
+ data: new EnhancedArray(...this.slice(from, to)),
719
+ total,
720
+ perPage,
721
+ currentPage,
722
+ lastPage,
723
+ from: from + 1,
724
+ to,
725
+ }
726
+ }
727
+
728
+ /**
729
+ * Recursively convert all nested arrays to EnhancedArray instances.
730
+ *
731
+ * @returns {EnhancedArray<unknown>} A new collection with recursive EnhancedArrays
732
+ */
733
+ recursive(): EnhancedArray<unknown> {
734
+ const convert = (item: unknown): unknown => {
735
+ if (Array.isArray(item)) {
736
+ return new EnhancedArray(...item.map(convert))
737
+ }
738
+ if (item && 'object' === typeof item && item.constructor === Object) {
739
+ const converted: Record<string, unknown> = {}
740
+ for (const [key, value] of Object.entries(item)) {
741
+ converted[key] = convert(value)
742
+ }
743
+ return converted
744
+ }
745
+ return item
746
+ }
747
+
748
+ return new EnhancedArray(...this.map(convert))
749
+ }
750
+ }
751
+
752
+ // Utility functions for objects and maps
753
+ /**
754
+ * Convert a plain object to a Map.
755
+ *
756
+ * @param {Record<string, T>} obj The object to convert
757
+ * @returns {Map<string, T>} A new Map instance
758
+ */
759
+ export const objectToMap = <T>(obj: Record<string, T>): Map<string, T> => {
760
+ return new Map(Object.entries(obj))
761
+ }
762
+
763
+ /**
764
+ * Convert a Map to a plain object.
765
+ *
766
+ * @param {Map<string, T>} map The Map to convert
767
+ * @returns {Record<string, T>} A new plain object
768
+ */
769
+ export const mapToObject = <T>(map: Map<string, T>): Record<string, T> => {
770
+ return Object.fromEntries(map)
771
+ }
772
+
773
+ /**
774
+ * Enhanced Map class with additional collection-like methods.
775
+ * Extends the native Map class.
776
+ */
777
+ export class EnhancedMap<K, V> extends Map<K, V> {
778
+ /**
779
+ * Create an EnhancedMap from a plain object.
780
+ *
781
+ * @param {Record<string, V>} obj The object to convert
782
+ * @returns {EnhancedMap<string, V>} A new EnhancedMap instance
783
+ */
784
+ static fromObject<V>(obj: Record<string, V>): EnhancedMap<string, V> {
785
+ return new EnhancedMap(Object.entries(obj))
786
+ }
787
+
788
+ /**
789
+ * Pluck a property from each value in the map.
790
+ *
791
+ * @param {P} key The key to pluck
792
+ * @returns {EnhancedArray<V[P]>} A new collection of plucked values
793
+ */
794
+ pluck<P extends keyof V>(key: P): EnhancedArray<V[P]> {
795
+ return new EnhancedArray(
796
+ ...(Array.from(this.values())
797
+ .map((value) => (value && 'object' === typeof value ? value[key] : undefined))
798
+ .filter((val) => val !== undefined) as V[P][])
799
+ )
800
+ }
801
+
802
+ /**
803
+ * Group the map's entries by a given accessor.
804
+ *
805
+ * @param {Function} accessor The accessor to group by
806
+ * @returns {Record<string, EnhancedArray<[K, V]>>} A record of grouped entries
807
+ */
808
+ groupBy(accessor: (value: V, key: K) => string | number): Record<string, EnhancedArray<[K, V]>> {
809
+ const groups: Record<string, EnhancedArray<[K, V]>> = {}
810
+
811
+ for (const [key, value] of this) {
812
+ const groupKey = String(accessor(value, key))
813
+
814
+ if (!groups[groupKey]) {
815
+ groups[groupKey] = new EnhancedArray<[K, V]>()
816
+ }
817
+
818
+ groups[groupKey].push([key, value])
819
+ }
820
+
821
+ return groups
822
+ }
823
+
824
+ /**
825
+ * Convert the map to an array of entries.
826
+ *
827
+ * @returns {EnhancedArray<[K, V]>} A collection of entries
828
+ */
829
+ toArray(): EnhancedArray<[K, V]> {
830
+ return new EnhancedArray(...this.entries())
831
+ }
832
+
833
+ /**
834
+ * Convert the map to a plain object.
835
+ *
836
+ * @returns {Record<string, V>} A plain object representation of the map
837
+ */
838
+ toObject(): Record<string, V> {
839
+ const obj: Record<string, V> = {}
840
+ for (const [key, value] of this) {
841
+ obj[String(key)] = value
842
+ }
843
+ return obj
844
+ }
845
+ }
846
+
847
+ // Factory functions
848
+ /**
849
+ * Create a new EnhancedArray from an array of items.
850
+ *
851
+ * @param {T[]} items The items to collect
852
+ * @returns {EnhancedArray<T>} A new EnhancedArray instance
853
+ */
854
+ export const collect = <T>(items: T[]): EnhancedArray<T> => {
855
+ return new EnhancedArray(...items)
856
+ }
857
+
858
+ /**
859
+ * Create a new EnhancedMap from an iterable of entries.
860
+ *
861
+ * @param {Iterable<[K, V]>} [entries] Optional entries to initialize the map
862
+ * @returns {EnhancedMap<K, V>} A new EnhancedMap instance
863
+ */
864
+ export const collectMap = <K, V>(entries?: Iterable<[K, V]>): EnhancedMap<K, V> => {
865
+ return new EnhancedMap(entries)
866
+ }