@tim-code/my-util 0.6.2 → 0.6.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
package/src/array.js CHANGED
@@ -176,6 +176,80 @@ export function descending(transform) {
176
176
  }
177
177
  }
178
178
 
179
+ function compareUndefinedNullEmpty(a, b) {
180
+ if (b === undefined || b === null || b === "") {
181
+ if (a === undefined || a === null || a === "") {
182
+ return 0
183
+ }
184
+ return -1
185
+ } else if (a === undefined || a === null || a === "") {
186
+ return 1
187
+ }
188
+ return undefined
189
+ }
190
+
191
+ function naturalCompare(a, b) {
192
+ if (a[0] === "-" && b[0] === "-") {
193
+ a = a.slice(1, a.length)
194
+ b = b.slice(1, b.length)
195
+ return b.localeCompare(a, undefined, { numeric: true })
196
+ }
197
+ return a.localeCompare(b, undefined, { numeric: true })
198
+ }
199
+
200
+ /**
201
+ * Returns an "ascending" comparator using natural string sort, to be used to sort an array of strings.
202
+ * Empty string or undefined or null values are always sorted to the end.
203
+ * @param {string|number|Function=} transform
204
+ * If a function, calls the provided function on an element to get the value to sort on.
205
+ * If a string or number, treats transform as a key and sorts on each element's value at key.
206
+ * @returns {Function}
207
+ */
208
+ export function naturalAsc(transform) {
209
+ if (typeof transform === "function") {
210
+ return (a, b) => {
211
+ a = transform(a)
212
+ b = transform(b)
213
+ return compareUndefinedNullEmpty(a, b) ?? naturalCompare(a, b)
214
+ }
215
+ }
216
+ if (typeof transform === "string" || typeof transform === "number") {
217
+ return (a, b) => {
218
+ const invalid = compareUndefinedNullEmpty(a[transform], b[transform])
219
+ return invalid ?? naturalCompare(a[transform], b[transform])
220
+ }
221
+ }
222
+ return (a, b) => {
223
+ return compareUndefinedNullEmpty(a, b) ?? naturalCompare(a, b)
224
+ }
225
+ }
226
+ /**
227
+ * Returns a "descending" comparator using natural string sort, to be used to sort an array of strings.
228
+ * Empty string or undefined or null values are always sorted to the end.
229
+ * @param {string|number|Function=} transform
230
+ * If a function, calls the provided function on an element to get the value to sort on.
231
+ * If a string or number, treats transform as a key and sorts on each element's value at key.
232
+ * @returns {Function}
233
+ */
234
+ export function naturalDesc(transform) {
235
+ if (typeof transform === "function") {
236
+ return (a, b) => {
237
+ b = transform(b)
238
+ a = transform(a)
239
+ return compareUndefinedNullEmpty(a, b) ?? naturalCompare(b, a)
240
+ }
241
+ }
242
+ if (typeof transform === "string" || typeof transform === "number") {
243
+ return (a, b) => {
244
+ const invalid = compareUndefinedNullEmpty(a[transform], b[transform])
245
+ return invalid ?? naturalCompare(b[transform], a[transform])
246
+ }
247
+ }
248
+ return (a, b) => {
249
+ return compareUndefinedNullEmpty(a, b) ?? naturalCompare(b, a)
250
+ }
251
+ }
252
+
179
253
  /**
180
254
  * Combines multiple ascending and descending comparators.
181
255
  * @param {...Function} comparators
package/src/array.test.js CHANGED
@@ -1,8 +1,16 @@
1
1
  import { describe, expect, it, jest } from "@jest/globals"
2
2
 
3
- const { chunk, unique, duplicates, ascending, descending, multilevel, sortN } = await import(
4
- "./array.js"
5
- )
3
+ const {
4
+ chunk,
5
+ unique,
6
+ duplicates,
7
+ ascending,
8
+ descending,
9
+ naturalAsc,
10
+ naturalDesc,
11
+ multilevel,
12
+ sortN,
13
+ } = await import("./array.js")
6
14
 
7
15
  describe("chunk", () => {
8
16
  it("splits array into chunks of specified size", () => {
@@ -484,6 +492,102 @@ describe("descending", () => {
484
492
  })
485
493
  })
486
494
 
495
+ describe("naturalAsc", () => {
496
+ it("sorts strings using natural order when transform is a function", () => {
497
+ const arr = ["a10", "a2", "a1"]
498
+ arr.sort(naturalAsc((s) => s))
499
+ expect(arr).toEqual(["a1", "a2", "a10"])
500
+ })
501
+
502
+ it("sorts by key using natural order and defers undefined/null to the end", () => {
503
+ const arr = [
504
+ { v: "file10" },
505
+ { v: undefined },
506
+ { v: "file2" },
507
+ { v: null },
508
+ { v: "file1" },
509
+ ]
510
+ arr.sort(naturalAsc("v"))
511
+ expect(arr.map((o) => o.v)).toEqual(["file1", "file2", "file10", undefined, null])
512
+ })
513
+
514
+ it("defers empty string to the end along with undefined/null (default path)", () => {
515
+ const arr = ["b", "", "a", undefined, null, "c"]
516
+ arr.sort(naturalAsc())
517
+ expect(arr).toEqual(["a", "b", "c", "", null, undefined])
518
+ })
519
+
520
+ it("defers empty string to the end along with undefined/null (key path)", () => {
521
+ const arr = [{ v: "" }, { v: "a2" }, { v: undefined }, { v: "a1" }, { v: null }]
522
+ arr.sort(naturalAsc("v"))
523
+ expect(arr.map((o) => o.v)).toEqual(["a1", "a2", "", undefined, null])
524
+ })
525
+
526
+ it("returns 0 for equal values (function and key paths)", () => {
527
+ expect(naturalAsc((x) => x)("a", "a")).toBe(0)
528
+ expect(naturalAsc("v")({ v: "x" }, { v: "x" })).toBe(0)
529
+ })
530
+
531
+ it("default comparator sorts naturally with undefined/null at end", () => {
532
+ const arr = [undefined, "b", null, "a", "c"]
533
+ arr.sort(naturalAsc())
534
+ expect(arr).toEqual(["a", "b", "c", null, undefined])
535
+ const numericLike = ["a10", "a2", "a1"]
536
+ numericLike.sort(naturalAsc())
537
+ expect(numericLike).toEqual(["a1", "a2", "a10"])
538
+ })
539
+
540
+ it("handles negative number-like strings by comparing magnitudes (both negative)", () => {
541
+ const arr = ["-2", "-10", "-1"]
542
+ arr.sort(naturalAsc())
543
+ expect(arr).toEqual(["-10", "-2", "-1"])
544
+ })
545
+ })
546
+
547
+ describe("naturalDesc", () => {
548
+ it("sorts strings using natural order descending (default path)", () => {
549
+ const arr = [undefined, "a", null, "c", "b"]
550
+ arr.sort(naturalDesc())
551
+ expect(arr).toEqual(["c", "b", "a", null, undefined])
552
+ })
553
+
554
+ it("sorts using transform function in natural descending order", () => {
555
+ const arr = ["a1", "a10", "a2"]
556
+ arr.sort(naturalDesc((s) => s))
557
+ expect(arr).toEqual(["a10", "a2", "a1"])
558
+ })
559
+
560
+ it("sorts by key using natural descending order and defers undefined/null to the end", () => {
561
+ const arr = [
562
+ { v: "file1" },
563
+ { v: "file10" },
564
+ { v: undefined },
565
+ { v: "file2" },
566
+ { v: null },
567
+ ]
568
+ arr.sort(naturalDesc("v"))
569
+ expect(arr.map((o) => o.v)).toEqual(["file10", "file2", "file1", undefined, null])
570
+ })
571
+
572
+ it("defers empty string to the end along with undefined/null (default path)", () => {
573
+ const arr = ["b", "", "a", undefined, null, "c"]
574
+ arr.sort(naturalDesc())
575
+ expect(arr.slice(-3)).toEqual(["", null, undefined])
576
+ expect(arr.slice(0, 3)).toEqual(["c", "b", "a"])
577
+ })
578
+
579
+ it("returns 0 for equal values (function and key paths)", () => {
580
+ expect(naturalDesc((x) => x)("a", "a")).toBe(0)
581
+ expect(naturalDesc("v")({ v: "x" }, { v: "x" })).toBe(0)
582
+ })
583
+
584
+ it("handles negative number-like strings by comparing magnitudes (both negative) in descending", () => {
585
+ const arr = ["-2", "-10", "-1"]
586
+ arr.sort(naturalDesc())
587
+ expect(arr).toEqual(["-1", "-2", "-10"])
588
+ })
589
+ })
590
+
487
591
  describe("multilevel", () => {
488
592
  it("returns 0 if all comparators return 0", () => {
489
593
  const cmp = multilevel(