@forthtilliath/ts-kit 0.10.0 → 0.11.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 (87) hide show
  1. package/README.md +72 -22
  2. package/dist/array/flattenDeep.d.ts +7 -5
  3. package/dist/array/flattenDeep.d.ts.map +1 -1
  4. package/dist/array/flattenDeep.js +8 -5
  5. package/dist/async/abortReason.d.ts +9 -0
  6. package/dist/async/abortReason.d.ts.map +1 -0
  7. package/dist/async/abortReason.js +11 -0
  8. package/dist/async/memoize.d.ts +2 -18
  9. package/dist/async/memoize.d.ts.map +1 -1
  10. package/dist/async/memoize.js +4 -25
  11. package/dist/async/retry.d.ts +15 -3
  12. package/dist/async/retry.d.ts.map +1 -1
  13. package/dist/async/retry.js +15 -11
  14. package/dist/async/sleep.d.ts +2 -1
  15. package/dist/async/sleep.d.ts.map +1 -1
  16. package/dist/async/sleep.js +23 -2
  17. package/dist/async/withTimeout.d.ts +11 -4
  18. package/dist/async/withTimeout.d.ts.map +1 -1
  19. package/dist/async/withTimeout.js +31 -9
  20. package/dist/csv/escapeCsvField.d.ts +21 -0
  21. package/dist/csv/escapeCsvField.d.ts.map +1 -0
  22. package/dist/csv/escapeCsvField.js +28 -0
  23. package/dist/csv/formatCsvNumber.d.ts +14 -0
  24. package/dist/csv/formatCsvNumber.d.ts.map +1 -0
  25. package/dist/csv/formatCsvNumber.js +15 -0
  26. package/dist/csv/index.d.ts +2 -0
  27. package/dist/csv/index.d.ts.map +1 -1
  28. package/dist/csv/index.js +2 -0
  29. package/dist/date/dateInTimeZone.d.ts +22 -0
  30. package/dist/date/dateInTimeZone.d.ts.map +1 -0
  31. package/dist/date/dateInTimeZone.js +35 -0
  32. package/dist/date/daysUntil.d.ts +15 -0
  33. package/dist/date/daysUntil.d.ts.map +1 -0
  34. package/dist/date/daysUntil.js +22 -0
  35. package/dist/date/index.d.ts +3 -0
  36. package/dist/date/index.d.ts.map +1 -1
  37. package/dist/date/index.js +3 -0
  38. package/dist/date/todayInTimeZone.d.ts +14 -0
  39. package/dist/date/todayInTimeZone.d.ts.map +1 -0
  40. package/dist/date/todayInTimeZone.js +15 -0
  41. package/dist/files/downloadText.d.ts.map +1 -1
  42. package/dist/files/downloadText.js +9 -1
  43. package/dist/function/index.d.ts +2 -0
  44. package/dist/function/index.d.ts.map +1 -0
  45. package/dist/function/index.js +1 -0
  46. package/dist/function/memoize.d.ts +34 -0
  47. package/dist/function/memoize.d.ts.map +1 -0
  48. package/dist/function/memoize.js +50 -0
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -1
  52. package/dist/maths/average.d.ts +2 -7
  53. package/dist/maths/average.d.ts.map +1 -1
  54. package/dist/maths/average.js +4 -10
  55. package/dist/maths/sum.d.ts +2 -7
  56. package/dist/maths/sum.d.ts.map +1 -1
  57. package/dist/maths/sum.js +4 -9
  58. package/dist/number/average.d.ts +13 -0
  59. package/dist/number/average.d.ts.map +1 -0
  60. package/dist/number/average.js +15 -0
  61. package/dist/number/formatBytes.d.ts.map +1 -1
  62. package/dist/number/formatBytes.js +6 -2
  63. package/dist/number/formatCsvNumber.d.ts +2 -13
  64. package/dist/number/formatCsvNumber.d.ts.map +1 -1
  65. package/dist/number/formatCsvNumber.js +4 -15
  66. package/dist/number/index.d.ts +2 -0
  67. package/dist/number/index.d.ts.map +1 -1
  68. package/dist/number/index.js +2 -0
  69. package/dist/number/sum.d.ts +10 -0
  70. package/dist/number/sum.d.ts.map +1 -0
  71. package/dist/number/sum.js +11 -0
  72. package/dist/object/deepEqual.d.ts +6 -0
  73. package/dist/object/deepEqual.d.ts.map +1 -1
  74. package/dist/object/deepEqual.js +45 -11
  75. package/dist/string/escapeCsvField.d.ts +2 -13
  76. package/dist/string/escapeCsvField.d.ts.map +1 -1
  77. package/dist/string/escapeCsvField.js +4 -17
  78. package/dist/string/escapeHtml.d.ts +6 -4
  79. package/dist/string/escapeHtml.d.ts.map +1 -1
  80. package/dist/string/escapeHtml.js +8 -5
  81. package/dist/string/pluralize.d.ts +30 -7
  82. package/dist/string/pluralize.d.ts.map +1 -1
  83. package/dist/string/pluralize.js +48 -15
  84. package/dist/version/compareVersions.d.ts +8 -1
  85. package/dist/version/compareVersions.d.ts.map +1 -1
  86. package/dist/version/compareVersions.js +63 -9
  87. package/package.json +7 -6
@@ -1,31 +1,65 @@
1
1
  /**
2
2
  * Recursively compares two values for structural equality.
3
3
  *
4
+ * Both values must share the same prototype (a plain object never equals an
5
+ * array or a class instance). `Date`s are compared by timestamp, `RegExp`s by
6
+ * source and flags, `Map`s by keys (identity) then values (deeply), and
7
+ * `Set`s by membership (identity, like `Set.prototype.has`).
8
+ *
4
9
  * @param a - The first value.
5
10
  * @param b - The second value.
6
11
  * @returns `true` if `a` and `b` are deeply equal.
7
12
  * @example
8
13
  * deepEqual({ a: [1, 2] }, { a: [1, 2] }); // => true
9
14
  * deepEqual({ a: 1 }, { a: 2 }); // => false
15
+ * deepEqual(new Date(0), new Date(0)); // => true
10
16
  */
11
17
  export function deepEqual(a, b) {
12
18
  if (Object.is(a, b))
13
19
  return true;
14
- if (typeof a !== typeof b || a === null || b === null)
20
+ if (typeof a !== "object" ||
21
+ typeof b !== "object" ||
22
+ a === null ||
23
+ b === null ||
24
+ Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
15
25
  return false;
16
- if (Array.isArray(a) || Array.isArray(b)) {
17
- if (!Array.isArray(a) || !Array.isArray(b))
26
+ }
27
+ if (a instanceof Date) {
28
+ return Object.is(a.getTime(), b.getTime());
29
+ }
30
+ if (a instanceof RegExp) {
31
+ const other = b;
32
+ return a.source === other.source && a.flags === other.flags;
33
+ }
34
+ if (a instanceof Map) {
35
+ const other = b;
36
+ if (a.size !== other.size)
18
37
  return false;
19
- if (a.length !== b.length)
38
+ for (const [key, value] of a) {
39
+ if (!other.has(key) || !deepEqual(value, other.get(key)))
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ if (a instanceof Set) {
45
+ const other = b;
46
+ if (a.size !== other.size)
20
47
  return false;
21
- return a.every((item, index) => deepEqual(item, b[index]));
48
+ for (const value of a) {
49
+ if (!other.has(value))
50
+ return false;
51
+ }
52
+ return true;
22
53
  }
23
- if (typeof a === "object" && typeof b === "object") {
24
- const aKeys = Object.keys(a);
25
- const bKeys = Object.keys(b);
26
- if (aKeys.length !== bKeys.length)
54
+ if (Array.isArray(a)) {
55
+ const other = b;
56
+ if (a.length !== other.length)
27
57
  return false;
28
- return aKeys.every((key) => deepEqual(a[key], b[key]));
58
+ return a.every((item, index) => deepEqual(item, other[index]));
29
59
  }
30
- return false;
60
+ const aKeys = Object.keys(a);
61
+ if (aKeys.length !== Object.keys(b).length)
62
+ return false;
63
+ return aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key) &&
64
+ deepEqual(a[key], b[key]));
31
65
  }
@@ -1,14 +1,3 @@
1
- /**
2
- * Escapes a CSV field per RFC 4180: wraps it in double quotes and doubles any
3
- * internal double quote if the value contains a double quote, a semicolon
4
- * (the delimiter), or a newline — returned as-is otherwise.
5
- *
6
- * @param value - The raw field value.
7
- * @returns The value, quoted and escaped only if needed.
8
- * @example
9
- * escapeCsvField("Apple"); // => "Apple"
10
- * escapeCsvField("a;b"); // => '"a;b"'
11
- * escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
12
- */
13
- export declare function escapeCsvField(value: string): string;
1
+ /** @deprecated Import from `@forthtilliath/ts-kit/csv/escapeCsvField` instead. */
2
+ export { escapeCsvField } from "../csv/escapeCsvField.js";
14
3
  //# sourceMappingURL=escapeCsvField.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/string/escapeCsvField.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD"}
1
+ {"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/string/escapeCsvField.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC"}
@@ -1,17 +1,4 @@
1
- /**
2
- * Escapes a CSV field per RFC 4180: wraps it in double quotes and doubles any
3
- * internal double quote if the value contains a double quote, a semicolon
4
- * (the delimiter), or a newline — returned as-is otherwise.
5
- *
6
- * @param value - The raw field value.
7
- * @returns The value, quoted and escaped only if needed.
8
- * @example
9
- * escapeCsvField("Apple"); // => "Apple"
10
- * escapeCsvField("a;b"); // => '"a;b"'
11
- * escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
12
- */
13
- export function escapeCsvField(value) {
14
- if (!/[";\n]/.test(value))
15
- return value;
16
- return `"${value.replace(/"/g, '""')}"`;
17
- }
1
+ // CSV helpers now live together in `csv/`. Re-exported so the
2
+ // `@forthtilliath/ts-kit/string/escapeCsvField` deep import keeps working.
3
+ /** @deprecated Import from `@forthtilliath/ts-kit/csv/escapeCsvField` instead. */
4
+ export { escapeCsvField } from "../csv/escapeCsvField.js";
@@ -1,12 +1,14 @@
1
1
  /**
2
- * Escapes the HTML entities that matter when inserting arbitrary text into
3
- * an HTML template (`&`, `<`, `>`, `"`), without pulling in a templating
4
- * dependency.
2
+ * Escapes the HTML special characters (`&`, `<`, `>`, `"`, `'`) when
3
+ * inserting arbitrary text into an HTML template, without pulling in a
4
+ * templating dependency. Safe in text content and in attribute values,
5
+ * whether they're quoted with `"` or `'`.
5
6
  *
6
7
  * @param text - The text to escape.
7
- * @returns `text` with `&`, `<`, `>` and `"` replaced by their HTML entities.
8
+ * @returns `text` with `&`, `<`, `>`, `"` and `'` replaced by HTML entities.
8
9
  * @example
9
10
  * escapeHtml("Tom & Jerry <script>"); // => "Tom &amp; Jerry &lt;script&gt;"
11
+ * escapeHtml("it's"); // => "it&#39;s"
10
12
  */
11
13
  export declare function escapeHtml(text: string): string;
12
14
  //# sourceMappingURL=escapeHtml.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"escapeHtml.d.ts","sourceRoot":"","sources":["../../src/string/escapeHtml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C"}
1
+ {"version":3,"file":"escapeHtml.d.ts","sourceRoot":"","sources":["../../src/string/escapeHtml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C"}
@@ -1,17 +1,20 @@
1
1
  /**
2
- * Escapes the HTML entities that matter when inserting arbitrary text into
3
- * an HTML template (`&`, `<`, `>`, `"`), without pulling in a templating
4
- * dependency.
2
+ * Escapes the HTML special characters (`&`, `<`, `>`, `"`, `'`) when
3
+ * inserting arbitrary text into an HTML template, without pulling in a
4
+ * templating dependency. Safe in text content and in attribute values,
5
+ * whether they're quoted with `"` or `'`.
5
6
  *
6
7
  * @param text - The text to escape.
7
- * @returns `text` with `&`, `<`, `>` and `"` replaced by their HTML entities.
8
+ * @returns `text` with `&`, `<`, `>`, `"` and `'` replaced by HTML entities.
8
9
  * @example
9
10
  * escapeHtml("Tom & Jerry <script>"); // => "Tom &amp; Jerry &lt;script&gt;"
11
+ * escapeHtml("it's"); // => "it&#39;s"
10
12
  */
11
13
  export function escapeHtml(text) {
12
14
  return text
13
15
  .replace(/&/g, "&amp;")
14
16
  .replace(/</g, "&lt;")
15
17
  .replace(/>/g, "&gt;")
16
- .replace(/"/g, "&quot;");
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&#39;");
17
20
  }
@@ -1,17 +1,40 @@
1
+ export interface PluralizeOptions {
2
+ /** An explicit plural form (overrides the default suffix rules). */
3
+ plural?: string;
4
+ /**
5
+ * The locale deciding which counts take the singular (defaults to `"en"`):
6
+ * only ±1 in English, but also 0 and 1.5 in French.
7
+ */
8
+ locale?: string;
9
+ }
1
10
  /**
2
- * Pluralizes a word based on a count, with a naive English default (`s`
3
- * suffix, `y` → `ies`, `es` after `s`/`x`/`z`/`sh`/`ch`) or an explicit
4
- * plural form for irregular words.
11
+ * Pluralizes a word based on a count. Which counts take the singular follows
12
+ * the locale's plural rules (`Intl.PluralRules`). Without an explicit plural
13
+ * form, the default suffix is naive: in English `s`, `y` → `ies`, `es` after
14
+ * `s`/`x`/`z`/`sh`/`ch`; in French `s`, nothing after `s`/`x`/`z`; `s` in
15
+ * any other language.
5
16
  *
6
17
  * @param count - The count driving singular vs. plural.
7
18
  * @param singular - The singular form.
8
- * @param plural - An explicit plural form (overrides the naive default).
9
- * @returns `singular` when `count` is 1 or -1, otherwise the plural form.
19
+ * @param options - An explicit plural form, or `{ plural?, locale? }`.
20
+ * @returns The singular or plural form.
10
21
  * @example
11
22
  * pluralize(1, "item"); // => "item"
12
- * pluralize(3, "item"); // => "items"
13
23
  * pluralize(3, "city"); // => "cities"
14
24
  * pluralize(2, "child", "children"); // => "children"
25
+ * pluralize(0, "chant", { locale: "fr" }); // => "chant"
26
+ * pluralize(2, "journal", { plural: "journaux", locale: "fr" }); // => "journaux"
15
27
  */
16
- export declare function pluralize(count: number, singular: string, plural?: string): string;
28
+ export declare function pluralize(count: number, singular: string, options?: string | PluralizeOptions): string;
29
+ /**
30
+ * Returns `pluralize` bound to a locale, to declare once per app.
31
+ *
32
+ * @param locale - The locale of every call.
33
+ * @returns `(count, singular, plural?) => string`.
34
+ * @example
35
+ * const plural = createPluralize("fr");
36
+ * plural(0, "chant"); // => "chant"
37
+ * plural(2, "est", "sont"); // => "sont"
38
+ */
39
+ export declare function createPluralize(locale: string): (count: number, singular: string, plural?: string) => string;
17
40
  //# sourceMappingURL=pluralize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pluralize.d.ts","sourceRoot":"","sources":["../../src/string/pluralize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAMR"}
1
+ {"version":3,"file":"pluralize.d.ts","sourceRoot":"","sources":["../../src/string/pluralize.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAyBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,GAClC,MAAM,CAKR;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,IACpC,OAAO,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,MAAM,KAAG,MAAM,CAElE"}
@@ -1,26 +1,59 @@
1
+ const pluralRules = new Map();
2
+ function takesSingular(count, locale) {
3
+ let rules = pluralRules.get(locale);
4
+ if (!rules) {
5
+ rules = new Intl.PluralRules(locale);
6
+ pluralRules.set(locale, rules);
7
+ }
8
+ return rules.select(count) === "one";
9
+ }
10
+ function defaultPlural(singular, locale) {
11
+ const language = locale.split("-")[0]?.toLowerCase();
12
+ if (language === "fr") {
13
+ return /[sxz]$/i.test(singular) ? singular : `${singular}s`;
14
+ }
15
+ if (language === "en") {
16
+ if (/[sxz]$|[cs]h$/i.test(singular))
17
+ return `${singular}es`;
18
+ if (/[^aeiou]y$/i.test(singular))
19
+ return `${singular.slice(0, -1)}ies`;
20
+ }
21
+ return `${singular}s`;
22
+ }
1
23
  /**
2
- * Pluralizes a word based on a count, with a naive English default (`s`
3
- * suffix, `y` → `ies`, `es` after `s`/`x`/`z`/`sh`/`ch`) or an explicit
4
- * plural form for irregular words.
24
+ * Pluralizes a word based on a count. Which counts take the singular follows
25
+ * the locale's plural rules (`Intl.PluralRules`). Without an explicit plural
26
+ * form, the default suffix is naive: in English `s`, `y` → `ies`, `es` after
27
+ * `s`/`x`/`z`/`sh`/`ch`; in French `s`, nothing after `s`/`x`/`z`; `s` in
28
+ * any other language.
5
29
  *
6
30
  * @param count - The count driving singular vs. plural.
7
31
  * @param singular - The singular form.
8
- * @param plural - An explicit plural form (overrides the naive default).
9
- * @returns `singular` when `count` is 1 or -1, otherwise the plural form.
32
+ * @param options - An explicit plural form, or `{ plural?, locale? }`.
33
+ * @returns The singular or plural form.
10
34
  * @example
11
35
  * pluralize(1, "item"); // => "item"
12
- * pluralize(3, "item"); // => "items"
13
36
  * pluralize(3, "city"); // => "cities"
14
37
  * pluralize(2, "child", "children"); // => "children"
38
+ * pluralize(0, "chant", { locale: "fr" }); // => "chant"
39
+ * pluralize(2, "journal", { plural: "journaux", locale: "fr" }); // => "journaux"
15
40
  */
16
- export function pluralize(count, singular, plural) {
17
- if (Math.abs(count) === 1)
41
+ export function pluralize(count, singular, options) {
42
+ const { plural, locale = "en" } = typeof options === "string" ? { plural: options } : (options ?? {});
43
+ if (takesSingular(count, locale))
18
44
  return singular;
19
- if (plural !== undefined)
20
- return plural;
21
- if (/[sxz]$|[cs]h$/i.test(singular))
22
- return `${singular}es`;
23
- if (/[^aeiou]y$/i.test(singular))
24
- return `${singular.slice(0, -1)}ies`;
25
- return `${singular}s`;
45
+ return plural ?? defaultPlural(singular, locale);
46
+ }
47
+ /**
48
+ * Returns `pluralize` bound to a locale, to declare once per app.
49
+ *
50
+ * @param locale - The locale of every call.
51
+ * @returns `(count, singular, plural?) => string`.
52
+ * @example
53
+ * const plural = createPluralize("fr");
54
+ * plural(0, "chant"); // => "chant"
55
+ * plural(2, "est", "sont"); // => "sont"
56
+ */
57
+ export function createPluralize(locale) {
58
+ return (count, singular, plural) => pluralize(count, singular, { plural, locale });
26
59
  }
@@ -1,7 +1,14 @@
1
1
  /**
2
- * Compares two "x.y.z" version strings, segment by segment.
2
+ * Compares two version strings following semver precedence: "x.y.z"
3
+ * segments first (missing ones count as `0`), then pre-release identifiers
4
+ * (`1.0.0-beta.2` < `1.0.0-beta.10` < `1.0.0`). A leading `v` and build
5
+ * metadata (`+build.1`) are ignored.
3
6
  *
4
7
  * @returns -1 if `a` < `b`, 0 if they're equal, 1 if `a` > `b`.
8
+ * @example
9
+ * compareVersions("1.10.0", "1.2.0"); // => 1
10
+ * compareVersions("1.0.0-beta", "1.0.0"); // => -1
11
+ * compareVersions("v2.0.0", "2.0.0"); // => 0
5
12
  */
6
13
  export declare function compareVersions(a: string, b: string): number;
7
14
  //# sourceMappingURL=compareVersions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compareVersions.d.ts","sourceRoot":"","sources":["../../src/version/compareVersions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU5D"}
1
+ {"version":3,"file":"compareVersions.d.ts","sourceRoot":"","sources":["../../src/version/compareVersions.ts"],"names":[],"mappings":"AA6CA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAe5D"}
@@ -1,17 +1,71 @@
1
+ function parseVersion(version) {
2
+ // Drop a leading "v" (git tags) and build metadata ("+build.1"), which
3
+ // semver ignores for precedence.
4
+ const [withoutBuild = ""] = version.trim().replace(/^v/i, "").split("+");
5
+ const dashIndex = withoutBuild.indexOf("-");
6
+ const core = dashIndex === -1 ? withoutBuild : withoutBuild.slice(0, dashIndex);
7
+ const prerelease = dashIndex === -1 ? "" : withoutBuild.slice(dashIndex + 1);
8
+ return {
9
+ core: core.split(".").map((part) => Number(part) || 0),
10
+ prerelease: prerelease === "" ? [] : prerelease.split("."),
11
+ };
12
+ }
13
+ function compareNumbers(a, b) {
14
+ if (a === b)
15
+ return 0;
16
+ return a > b ? 1 : -1;
17
+ }
18
+ // Semver rules: numeric identifiers compare numerically and rank below
19
+ // alphanumeric ones, which compare in ASCII order; with equal leading
20
+ // identifiers, the longer list ranks higher.
21
+ function comparePrerelease(a, b) {
22
+ const length = Math.max(a.length, b.length);
23
+ for (let i = 0; i < length; i++) {
24
+ const idA = a[i];
25
+ const idB = b[i];
26
+ if (idA === undefined)
27
+ return -1;
28
+ if (idB === undefined)
29
+ return 1;
30
+ if (idA === idB)
31
+ continue;
32
+ const numA = /^\d+$/.test(idA) ? Number(idA) : null;
33
+ const numB = /^\d+$/.test(idB) ? Number(idB) : null;
34
+ if (numA !== null && numB !== null)
35
+ return compareNumbers(numA, numB);
36
+ if (numA !== null)
37
+ return -1;
38
+ if (numB !== null)
39
+ return 1;
40
+ return idA > idB ? 1 : -1;
41
+ }
42
+ return 0;
43
+ }
1
44
  /**
2
- * Compares two "x.y.z" version strings, segment by segment.
45
+ * Compares two version strings following semver precedence: "x.y.z"
46
+ * segments first (missing ones count as `0`), then pre-release identifiers
47
+ * (`1.0.0-beta.2` < `1.0.0-beta.10` < `1.0.0`). A leading `v` and build
48
+ * metadata (`+build.1`) are ignored.
3
49
  *
4
50
  * @returns -1 if `a` < `b`, 0 if they're equal, 1 if `a` > `b`.
51
+ * @example
52
+ * compareVersions("1.10.0", "1.2.0"); // => 1
53
+ * compareVersions("1.0.0-beta", "1.0.0"); // => -1
54
+ * compareVersions("v2.0.0", "2.0.0"); // => 0
5
55
  */
6
56
  export function compareVersions(a, b) {
7
- const partsA = a.split(".").map(Number);
8
- const partsB = b.split(".").map(Number);
9
- const length = Math.max(partsA.length, partsB.length);
57
+ const parsedA = parseVersion(a);
58
+ const parsedB = parseVersion(b);
59
+ const length = Math.max(parsedA.core.length, parsedB.core.length);
10
60
  for (let i = 0; i < length; i++) {
11
- const numA = partsA[i] ?? 0;
12
- const numB = partsB[i] ?? 0;
13
- if (numA !== numB)
14
- return numA > numB ? 1 : -1;
61
+ const result = compareNumbers(parsedA.core[i] ?? 0, parsedB.core[i] ?? 0);
62
+ if (result !== 0)
63
+ return result;
15
64
  }
16
- return 0;
65
+ // A version without pre-release identifiers ranks above one that has some.
66
+ const isPrereleaseA = parsedA.prerelease.length > 0;
67
+ const isPrereleaseB = parsedB.prerelease.length > 0;
68
+ if (isPrereleaseA !== isPrereleaseB)
69
+ return isPrereleaseA ? -1 : 1;
70
+ return comparePrerelease(parsedA.prerelease, parsedB.prerelease);
17
71
  }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@forthtilliath/ts-kit",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
+ "sideEffects": false,
5
6
  "license": "MIT",
6
7
  "publishConfig": {
7
8
  "access": "public"
@@ -21,12 +22,12 @@
21
22
  "./*": "./dist/*.js"
22
23
  },
23
24
  "devDependencies": {
24
- "@vitest/coverage-v8": "4.1.10",
25
+ "@vitest/coverage-v8": "5.0.2",
25
26
  "eslint": "10.7.0",
26
- "jsdom": "^29.1.1",
27
+ "jsdom": "^30.1.1",
27
28
  "typescript": "^5.9.3",
28
- "vitest": "^4.1.10",
29
- "@forthtilliath/eslint-config": "0.7.0",
29
+ "vitest": "^5.0.2",
30
+ "@forthtilliath/eslint-config": "0.8.0",
30
31
  "@forthtilliath/typescript-config": "0.1.1"
31
32
  },
32
33
  "scripts": {
@@ -34,7 +35,7 @@
34
35
  "build": "tsc -p tsconfig.build.json",
35
36
  "check-types": "tsc --noEmit",
36
37
  "lint": "eslint . --max-warnings 0",
37
- "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
38
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
38
39
  "test": "vitest run",
39
40
  "test:watch": "vitest",
40
41
  "coverage": "vitest run --coverage"