@tim-code/my-util 0.6.1 → 0.6.3

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.1",
3
+ "version": "0.6.3",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
package/src/array.js CHANGED
@@ -176,6 +176,59 @@ export function descending(transform) {
176
176
  }
177
177
  }
178
178
 
179
+ /**
180
+ * Returns an "ascending" comparator using natural string sort, to be used to sort an array of strings.
181
+ * Undefined or null values are always sorted to the end.
182
+ * @param {string|number|Function=} transform
183
+ * If a function, calls the provided function on an element to get the value to sort on.
184
+ * If a string or number, treats transform as a key and sorts on each element's value at key.
185
+ * @returns {Function}
186
+ */
187
+ export function naturalAsc(transform) {
188
+ if (typeof transform === "function") {
189
+ return (a, b) => {
190
+ a = transform(a)
191
+ b = transform(b)
192
+ return compareUndefinedNull(a, b) ?? a.localeCompare(b, undefined, { numeric: true })
193
+ }
194
+ }
195
+ if (typeof transform === "string" || typeof transform === "number") {
196
+ return (a, b) => {
197
+ const invalid = compareUndefinedNull(a[transform], b[transform])
198
+ return invalid ?? a[transform].localeCompare(b[transform], undefined, { numeric: true })
199
+ }
200
+ }
201
+ return (a, b) => {
202
+ return compareUndefinedNull(a, b) ?? a.localeCompare(b, undefined, { numeric: true })
203
+ }
204
+ }
205
+ /**
206
+ * Returns a "descending" comparator using natural string sort, to be used to sort an array of strings.
207
+ * Undefined or null values are always sorted to the end.
208
+ * @param {string|number|Function=} transform
209
+ * If a function, calls the provided function on an element to get the value to sort on.
210
+ * If a string or number, treats transform as a key and sorts on each element's value at key.
211
+ * @returns {Function}
212
+ */
213
+ export function naturalDesc(transform) {
214
+ if (typeof transform === "function") {
215
+ return (a, b) => {
216
+ b = transform(b)
217
+ a = transform(a)
218
+ return compareUndefinedNull(a, b) ?? b.localeCompare(a, undefined, { numeric: true })
219
+ }
220
+ }
221
+ if (typeof transform === "string" || typeof transform === "number") {
222
+ return (a, b) => {
223
+ const invalid = compareUndefinedNull(a[transform], b[transform])
224
+ return invalid ?? b[transform].localeCompare(a[transform], undefined, { numeric: true })
225
+ }
226
+ }
227
+ return (a, b) => {
228
+ return compareUndefinedNull(a, b) ?? b.localeCompare(a, undefined, { numeric: true })
229
+ }
230
+ }
231
+
179
232
  /**
180
233
  * Combines multiple ascending and descending comparators.
181
234
  * @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,71 @@ 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("returns 0 for equal values (function and key paths)", () => {
515
+ expect(naturalAsc((x) => x)("a", "a")).toBe(0)
516
+ expect(naturalAsc("v")({ v: "x" }, { v: "x" })).toBe(0)
517
+ })
518
+
519
+ it("default comparator sorts with undefined/null at end but uses non-natural, descending semantics", () => {
520
+ const arr = [undefined, "b", null, "a", "c"]
521
+ arr.sort(naturalAsc())
522
+ expect(arr).toEqual(["a", "b", "c", null, undefined])
523
+ const numericLike = ["a10", "a2", "a1"]
524
+ numericLike.sort(naturalAsc())
525
+ expect(numericLike).toEqual(["a1", "a2", "a10"])
526
+ })
527
+ })
528
+
529
+ describe("naturalDesc", () => {
530
+ it("sorts strings using natural order descending (default path)", () => {
531
+ const arr = [undefined, "a", null, "c", "b"]
532
+ arr.sort(naturalDesc())
533
+ expect(arr).toEqual(["c", "b", "a", null, undefined])
534
+ })
535
+
536
+ it("sorts using transform function in natural descending order", () => {
537
+ const arr = ["a1", "a10", "a2"]
538
+ arr.sort(naturalDesc((s) => s))
539
+ expect(arr).toEqual(["a10", "a2", "a1"])
540
+ })
541
+
542
+ it("sorts by key using natural descending order and defers undefined/null to the end", () => {
543
+ const arr = [
544
+ { v: "file1" },
545
+ { v: "file10" },
546
+ { v: undefined },
547
+ { v: "file2" },
548
+ { v: null },
549
+ ]
550
+ arr.sort(naturalDesc("v"))
551
+ expect(arr.map((o) => o.v)).toEqual(["file10", "file2", "file1", undefined, null])
552
+ })
553
+
554
+ it("returns 0 for equal values (function and key paths)", () => {
555
+ expect(naturalDesc((x) => x)("a", "a")).toBe(0)
556
+ expect(naturalDesc("v")({ v: "x" }, { v: "x" })).toBe(0)
557
+ })
558
+ })
559
+
487
560
  describe("multilevel", () => {
488
561
  it("returns 0 if all comparators return 0", () => {
489
562
  const cmp = multilevel(
package/src/object.js CHANGED
@@ -182,3 +182,20 @@ export function deepEqual(a, b) {
182
182
  }
183
183
  return true
184
184
  }
185
+
186
+ /**
187
+ * Checks if the argument is a class.
188
+ * Example: `isClass(class {})`
189
+ * Returns: true
190
+ * In general, this will only work for third-party or user-defined classes, not built-ins.
191
+ * @param {any} thing
192
+ * @returns {boolean}
193
+ */
194
+ export function isClass(thing) {
195
+ if (typeof thing !== "function") {
196
+ return false
197
+ }
198
+ const stringified = Function.prototype.toString.call(thing)
199
+ const result = /^class\s/u.test(stringified)
200
+ return result
201
+ }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-empty-function */
1
2
  import { jest } from "@jest/globals"
2
3
  import {
3
4
  deepCopy,
@@ -5,6 +6,7 @@ import {
5
6
  deepMerge,
6
7
  deepMergeCopy,
7
8
  deleteUndefinedValues,
9
+ isClass,
8
10
  isObject,
9
11
  like,
10
12
  mapValues,
@@ -38,7 +40,7 @@ describe("isObject", () => {
38
40
 
39
41
  it("returns false for functions", () => {
40
42
  expect(isObject(() => {})).toBe(false)
41
- // eslint-disable-next-line func-names, prefer-arrow-callback, no-empty-function
43
+ // eslint-disable-next-line func-names, prefer-arrow-callback
42
44
  expect(isObject(function () {})).toBe(false)
43
45
  })
44
46
  })
@@ -592,3 +594,33 @@ describe("deepEqual", () => {
592
594
  expect(deepEqual({}, { a: undefined })).toBe(false)
593
595
  })
594
596
  })
597
+
598
+ // --- isClass ---
599
+ describe("isClass", () => {
600
+ it("returns true for class declarations and expressions", () => {
601
+ class A {}
602
+ const B = class {}
603
+ expect(isClass(A)).toBe(true)
604
+ expect(isClass(B)).toBe(true)
605
+ })
606
+
607
+ it("returns false for non-class functions", () => {
608
+ function f() {}
609
+ const g = () => {}
610
+ async function af() {}
611
+ function* gf() {}
612
+ expect(isClass(f)).toBe(false)
613
+ expect(isClass(g)).toBe(false)
614
+ expect(isClass(af)).toBe(false)
615
+ expect(isClass(gf)).toBe(false)
616
+ })
617
+
618
+ it("returns false for built-in constructors and non-functions", () => {
619
+ // Built-ins typically stringify as 'function X() { [native code] }'
620
+ expect(isClass(Date)).toBe(false)
621
+ expect(isClass(Map)).toBe(false)
622
+ expect(isClass(123)).toBe(false)
623
+ expect(isClass({})).toBe(false)
624
+ expect(isClass(null)).toBe(false)
625
+ })
626
+ })