@synnaxlabs/x 0.46.2 → 0.48.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 (78) hide show
  1. package/.turbo/turbo-build.log +18 -18
  2. package/dist/{bounds-DeUXrllt.js → bounds-4BWKPqaP.js} +1 -4
  3. package/dist/bounds.js +1 -1
  4. package/dist/{index-C452Pas0.js → compare-Bnx9CdjS.js} +37 -47
  5. package/dist/compare-GPoFaKRW.cjs +1 -0
  6. package/dist/compare.cjs +1 -1
  7. package/dist/compare.js +34 -2
  8. package/dist/eslint.config.d.ts +3 -0
  9. package/dist/eslint.config.d.ts.map +1 -0
  10. package/dist/{index-D4NCYiQB.js → index-Bv029kh3.js} +2 -2
  11. package/dist/{index-udOjA9d-.cjs → index-CqisIWWC.cjs} +1 -1
  12. package/dist/index.cjs +3 -3
  13. package/dist/index.js +489 -465
  14. package/dist/{scale-BBWhTUqJ.js → scale-DJCMZbfU.js} +1 -1
  15. package/dist/scale.js +1 -1
  16. package/dist/series-B7l2au4y.cjs +6 -0
  17. package/dist/{series-Clbw-fZI.js → series-TpAaBlEg.js} +172 -145
  18. package/dist/spatial.js +2 -2
  19. package/dist/src/array/nullable.d.ts.map +1 -1
  20. package/dist/src/compare/binary.d.ts +25 -0
  21. package/dist/src/compare/binary.d.ts.map +1 -0
  22. package/dist/src/compare/binary.spec.d.ts +2 -0
  23. package/dist/src/compare/binary.spec.d.ts.map +1 -0
  24. package/dist/src/compare/external.d.ts +3 -0
  25. package/dist/src/compare/external.d.ts.map +1 -0
  26. package/dist/src/compare/index.d.ts +1 -1
  27. package/dist/src/compare/index.d.ts.map +1 -1
  28. package/dist/src/csv/csv.d.ts +11 -0
  29. package/dist/src/csv/csv.d.ts.map +1 -0
  30. package/dist/src/csv/csv.spec.d.ts +2 -0
  31. package/dist/src/csv/csv.spec.d.ts.map +1 -0
  32. package/dist/src/csv/index.d.ts +2 -0
  33. package/dist/src/csv/index.d.ts.map +1 -0
  34. package/dist/src/index.d.ts +1 -0
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/migrate/migrate.d.ts +1 -1
  37. package/dist/src/migrate/migrate.d.ts.map +1 -1
  38. package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
  39. package/dist/src/spatial/box/box.d.ts +5 -5
  40. package/dist/src/spatial/box/box.d.ts.map +1 -1
  41. package/dist/src/spatial/scale/scale.d.ts +6 -6
  42. package/dist/src/spatial/scale/scale.d.ts.map +1 -1
  43. package/dist/src/telem/telem.d.ts +14 -0
  44. package/dist/src/telem/telem.d.ts.map +1 -1
  45. package/dist/telem.cjs +1 -1
  46. package/dist/telem.js +1 -1
  47. package/dist/unique.cjs +1 -1
  48. package/dist/unique.js +1 -1
  49. package/package.json +9 -9
  50. package/src/array/nullable.ts +9 -0
  51. package/src/compare/binary.spec.ts +308 -0
  52. package/src/compare/binary.ts +50 -0
  53. package/src/compare/external.ts +11 -0
  54. package/src/compare/index.ts +1 -1
  55. package/src/csv/csv.spec.ts +28 -0
  56. package/src/csv/csv.ts +26 -0
  57. package/src/csv/index.ts +10 -0
  58. package/src/index.ts +1 -0
  59. package/src/jsonrpc/jsonrpc.spec.ts +9 -0
  60. package/src/math/round.spec.ts +2 -1
  61. package/src/migrate/migrate.spec.ts +238 -0
  62. package/src/migrate/migrate.ts +62 -4
  63. package/src/spatial/bounds/bounds.spec.ts +1 -1
  64. package/src/spatial/bounds/bounds.ts +9 -12
  65. package/src/spatial/box/box.spec.ts +3 -3
  66. package/src/spatial/box/box.ts +5 -5
  67. package/src/spatial/dimensions/dimensions.spec.ts +1 -1
  68. package/src/spatial/direction/direction.spec.ts +1 -1
  69. package/src/spatial/location/location.spec.ts +1 -1
  70. package/src/spatial/scale/scale.spec.ts +1 -1
  71. package/src/spatial/scale/scale.ts +7 -7
  72. package/src/telem/telem.spec.ts +87 -0
  73. package/src/telem/telem.ts +48 -0
  74. package/tsconfig.json +1 -1
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/dist/index-xaxa1hoa.cjs +0 -1
  77. package/dist/series-B2zqvP8A.cjs +0 -6
  78. /package/{eslint.config.js → eslint.config.ts} +0 -0
@@ -56,6 +56,59 @@ const migrations: migrate.Migrations = {
56
56
  "1.0.0": migrateV2,
57
57
  };
58
58
 
59
+ describe("semVerZ", () => {
60
+ describe("valid versions", () => {
61
+ it("should accept standard semver format", () => {
62
+ expect(() => migrate.semVerZ.parse("1.0.0")).not.toThrow();
63
+ expect(() => migrate.semVerZ.parse("0.0.0")).not.toThrow();
64
+ expect(() => migrate.semVerZ.parse("99.99.99")).not.toThrow();
65
+ });
66
+
67
+ it("should accept pre-release versions with single identifier", () => {
68
+ expect(() => migrate.semVerZ.parse("1.0.0-alpha")).not.toThrow();
69
+ expect(() => migrate.semVerZ.parse("1.0.0-beta")).not.toThrow();
70
+ expect(() => migrate.semVerZ.parse("1.0.0-rc")).not.toThrow();
71
+ expect(() => migrate.semVerZ.parse("0.48.0-rc")).not.toThrow();
72
+ });
73
+
74
+ it("should accept pre-release versions with numeric identifiers", () => {
75
+ expect(() => migrate.semVerZ.parse("1.0.0-1")).not.toThrow();
76
+ expect(() => migrate.semVerZ.parse("1.0.0-0")).not.toThrow();
77
+ expect(() => migrate.semVerZ.parse("1.0.0-99")).not.toThrow();
78
+ });
79
+
80
+ it("should accept pre-release versions with multiple identifiers", () => {
81
+ expect(() => migrate.semVerZ.parse("1.0.0-alpha.1")).not.toThrow();
82
+ expect(() => migrate.semVerZ.parse("1.0.0-rc.1")).not.toThrow();
83
+ expect(() => migrate.semVerZ.parse("1.0.0-beta.2.3")).not.toThrow();
84
+ expect(() => migrate.semVerZ.parse("1.0.0-0.3.7")).not.toThrow();
85
+ });
86
+
87
+ it("should accept pre-release versions with hyphens", () => {
88
+ expect(() => migrate.semVerZ.parse("1.0.0-x-beta")).not.toThrow();
89
+ expect(() => migrate.semVerZ.parse("1.0.0-alpha-beta")).not.toThrow();
90
+ });
91
+ });
92
+
93
+ describe("invalid versions", () => {
94
+ it("should reject versions without patch", () => {
95
+ expect(() => migrate.semVerZ.parse("1.0")).toThrow();
96
+ });
97
+
98
+ it("should reject versions without minor", () => {
99
+ expect(() => migrate.semVerZ.parse("1")).toThrow();
100
+ });
101
+
102
+ it("should reject versions with build metadata (not supported)", () => {
103
+ expect(() => migrate.semVerZ.parse("1.0.0+build")).toThrow();
104
+ });
105
+
106
+ it("should reject versions with empty pre-release", () => {
107
+ expect(() => migrate.semVerZ.parse("1.0.0-")).toThrow();
108
+ });
109
+ });
110
+ });
111
+
59
112
  describe("compareSemVer", () => {
60
113
  it("should return true when the major version is higher", () => {
61
114
  expect(migrate.compareSemVer("1.0.0", "0.0.0")).toBeGreaterThan(0);
@@ -139,6 +192,98 @@ describe("compareSemVer", () => {
139
192
  ).toBeLessThan(0);
140
193
  });
141
194
  });
195
+
196
+ describe("pre-release versions", () => {
197
+ it("should consider release version newer than pre-release", () => {
198
+ expect(migrate.compareSemVer("1.0.0", "1.0.0-rc")).toBeGreaterThan(0);
199
+ expect(migrate.compareSemVer("1.0.0", "1.0.0-alpha")).toBeGreaterThan(0);
200
+ expect(migrate.compareSemVer("1.0.0", "1.0.0-beta")).toBeGreaterThan(0);
201
+ expect(migrate.compareSemVer("0.48.0", "0.48.0-rc")).toBeGreaterThan(0);
202
+ expect(migrate.semVerNewer("1.0.0", "1.0.0-rc")).toBe(true);
203
+ });
204
+
205
+ it("should consider pre-release version older than release", () => {
206
+ expect(migrate.compareSemVer("1.0.0-rc", "1.0.0")).toBeLessThan(0);
207
+ expect(migrate.compareSemVer("1.0.0-alpha", "1.0.0")).toBeLessThan(0);
208
+ expect(migrate.compareSemVer("0.48.0-rc", "0.48.0")).toBeLessThan(0);
209
+ expect(migrate.semVerOlder("1.0.0-rc", "1.0.0")).toBe(true);
210
+ });
211
+
212
+ it("should compare pre-release versions lexicographically", () => {
213
+ expect(migrate.compareSemVer("1.0.0-alpha", "1.0.0-beta")).toBeLessThan(0);
214
+ expect(migrate.compareSemVer("1.0.0-beta", "1.0.0-rc")).toBeLessThan(0);
215
+ expect(migrate.compareSemVer("1.0.0-rc", "1.0.0-alpha")).toBeGreaterThan(0);
216
+ expect(migrate.semVerNewer("1.0.0-rc", "1.0.0-alpha")).toBe(true);
217
+ expect(migrate.semVerOlder("1.0.0-alpha", "1.0.0-beta")).toBe(true);
218
+ });
219
+
220
+ it("should compare numeric pre-release identifiers numerically", () => {
221
+ expect(migrate.compareSemVer("1.0.0-1", "1.0.0-2")).toBeLessThan(0);
222
+ expect(migrate.compareSemVer("1.0.0-2", "1.0.0-10")).toBeLessThan(0);
223
+ expect(migrate.compareSemVer("1.0.0-10", "1.0.0-2")).toBeGreaterThan(0);
224
+ });
225
+
226
+ it("should compare pre-release versions with multiple identifiers", () => {
227
+ expect(migrate.compareSemVer("1.0.0-rc.1", "1.0.0-rc.2")).toBeLessThan(0);
228
+ expect(migrate.compareSemVer("1.0.0-rc.2", "1.0.0-rc.10")).toBeLessThan(0);
229
+ expect(migrate.compareSemVer("1.0.0-alpha.1", "1.0.0-alpha.2")).toBeLessThan(0);
230
+ expect(
231
+ migrate.compareSemVer("1.0.0-alpha.beta", "1.0.0-alpha.gamma"),
232
+ ).toBeLessThan(0);
233
+ });
234
+
235
+ it("should consider numeric identifiers lower than alphanumeric", () => {
236
+ expect(migrate.compareSemVer("1.0.0-1", "1.0.0-alpha")).toBeLessThan(0);
237
+ expect(migrate.compareSemVer("1.0.0-alpha", "1.0.0-1")).toBeGreaterThan(0);
238
+ expect(migrate.compareSemVer("1.0.0-rc.1", "1.0.0-rc.beta")).toBeLessThan(0);
239
+ });
240
+
241
+ it("should consider longer pre-release identifiers higher precedence", () => {
242
+ expect(migrate.compareSemVer("1.0.0-rc", "1.0.0-rc.1")).toBeLessThan(0);
243
+ expect(migrate.compareSemVer("1.0.0-rc.1", "1.0.0-rc")).toBeGreaterThan(0);
244
+ expect(migrate.compareSemVer("1.0.0-alpha.1", "1.0.0-alpha.1.2")).toBeLessThan(0);
245
+ });
246
+
247
+ it("should consider equal pre-release versions equal", () => {
248
+ expect(migrate.compareSemVer("1.0.0-rc", "1.0.0-rc")).toBe(0);
249
+ expect(migrate.compareSemVer("1.0.0-alpha.1", "1.0.0-alpha.1")).toBe(0);
250
+ expect(migrate.versionsEqual("1.0.0-rc", "1.0.0-rc")).toBe(true);
251
+ });
252
+
253
+ it("should handle complex pre-release comparison chains", () => {
254
+ const versions = [
255
+ "1.0.0-alpha",
256
+ "1.0.0-alpha.1",
257
+ "1.0.0-alpha.beta",
258
+ "1.0.0-beta",
259
+ "1.0.0-beta.2",
260
+ "1.0.0-beta.11",
261
+ "1.0.0-rc.1",
262
+ "1.0.0",
263
+ ];
264
+
265
+ for (let i = 0; i < versions.length - 1; i++) {
266
+ expect(migrate.compareSemVer(versions[i], versions[i + 1])).toBeLessThan(0);
267
+ expect(migrate.compareSemVer(versions[i + 1], versions[i])).toBeGreaterThan(0);
268
+ }
269
+ });
270
+
271
+ it("should respect checkMajor/checkMinor/checkPatch with pre-release", () => {
272
+ expect(
273
+ migrate.compareSemVer("2.0.0-rc", "1.0.0", {
274
+ checkMinor: false,
275
+ checkPatch: false,
276
+ }),
277
+ ).toBeGreaterThan(0);
278
+
279
+ expect(
280
+ migrate.compareSemVer("1.2.0-rc", "1.1.0", {
281
+ checkMajor: false,
282
+ checkPatch: false,
283
+ }),
284
+ ).toBeGreaterThan(0);
285
+ });
286
+ });
142
287
  });
143
288
 
144
289
  describe("migrator", () => {
@@ -172,4 +317,97 @@ describe("migrator", () => {
172
317
  })(entity);
173
318
  expect(migrated).toEqual(entity);
174
319
  });
320
+
321
+ describe("with pre-release versions", () => {
322
+ const entityV1RC = z.object({
323
+ version: z.literal("1.0.0-rc"),
324
+ title: z.string(),
325
+ });
326
+
327
+ type EntityV1RC = z.infer<typeof entityV1RC>;
328
+
329
+ const migrateV1RC = migrate.createMigration<EntityV1RC, EntityV2>({
330
+ name: "entity",
331
+ inputSchema: entityV1RC,
332
+ outputSchema: entityV2,
333
+ migrate: (entity) => ({ ...entity, version: "2.0.0", description: "" }),
334
+ });
335
+
336
+ const migrationsWithRC: migrate.Migrations = {
337
+ "0.0.0": migrateV1,
338
+ "1.0.0-rc": migrateV1RC,
339
+ "1.0.0": migrateV2,
340
+ };
341
+
342
+ it("should migrate from pre-release version to stable", () => {
343
+ const entity: EntityV1RC = { version: "1.0.0-rc", title: "foo" };
344
+ const DEFAULT: EntityV2 = { version: "2.0.0", title: "", description: "" };
345
+ const migrated = migrate.migrator({
346
+ name: "entity",
347
+ migrations: migrationsWithRC,
348
+ def: DEFAULT,
349
+ })(entity);
350
+ expect(migrated).toEqual({ version: "2.0.0", title: "foo", description: "" });
351
+ });
352
+
353
+ it("should handle version sorting with pre-release correctly", () => {
354
+ const versions = ["0.0.0", "1.0.0-rc", "1.0.0"];
355
+ const sorted = versions.sort(migrate.compareSemVer);
356
+ expect(sorted).toEqual(["0.0.0", "1.0.0-rc", "1.0.0"]);
357
+ });
358
+
359
+ it("should not migrate if current version is newer than pre-release target", () => {
360
+ const entity: EntityV1 = { version: "1.0.0", title: "foo" };
361
+ const DEFAULT: EntityV1RC = { version: "1.0.0-rc", title: "" };
362
+ const migrated = migrate.migrator({
363
+ name: "entity",
364
+ migrations: { "0.0.0": migrateV1 },
365
+ def: DEFAULT,
366
+ })(entity);
367
+ expect(migrated).toEqual({ version: "1.0.0", title: "foo" });
368
+ });
369
+
370
+ it("should migrate through multiple pre-release versions", () => {
371
+ interface EntityV1Alpha {
372
+ version: "1.0.0-alpha";
373
+ title: string;
374
+ }
375
+
376
+ interface EntityV1Beta extends Omit<EntityV1Alpha, "version"> {
377
+ version: "1.0.0-beta";
378
+ newField: string;
379
+ }
380
+
381
+ const migrateAlphaToBeta = migrate.createMigration<EntityV1Alpha, EntityV1Beta>({
382
+ name: "entity",
383
+ migrate: (entity) => ({
384
+ ...entity,
385
+ version: "1.0.0-beta",
386
+ newField: "added",
387
+ }),
388
+ });
389
+
390
+ const migrateBetaToRC = migrate.createMigration<EntityV1Beta, EntityV1RC>({
391
+ name: "entity",
392
+ migrate: (entity) => {
393
+ const { newField, ...rest } = entity;
394
+ return { ...rest, version: "1.0.0-rc" };
395
+ },
396
+ });
397
+
398
+ const preReleaseMigrations: migrate.Migrations = {
399
+ "1.0.0-alpha": migrateAlphaToBeta,
400
+ "1.0.0-beta": migrateBetaToRC,
401
+ };
402
+
403
+ const entity: EntityV1Alpha = { version: "1.0.0-alpha", title: "test" };
404
+ const DEFAULT: EntityV1RC = { version: "1.0.0-rc", title: "" };
405
+ const migrated = migrate.migrator({
406
+ name: "entity",
407
+ migrations: preReleaseMigrations,
408
+ def: DEFAULT,
409
+ })(entity);
410
+ expect(migrated).toEqual({ version: "1.0.0-rc", title: "test" });
411
+ });
412
+ });
175
413
  });
@@ -12,7 +12,9 @@ import { z } from "zod";
12
12
  import { compare } from "@/compare";
13
13
  import { type Optional } from "@/optional";
14
14
 
15
- export const semVerZ = z.string().regex(/^\d+\.\d+\.\d+$/);
15
+ export const semVerZ = z
16
+ .string()
17
+ .regex(/^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/);
16
18
 
17
19
  export type SemVer = z.infer<typeof semVerZ>;
18
20
 
@@ -34,6 +36,48 @@ export interface CompareSemVerOptions {
34
36
  checkPatch?: boolean;
35
37
  }
36
38
 
39
+ /**
40
+ * Compares two pre-release identifiers according to semver spec.
41
+ * @param a - First pre-release identifier (without leading hyphen)
42
+ * @param b - Second pre-release identifier (without leading hyphen)
43
+ * @returns compare.LESS_THAN if a < b, compare.GREATER_THAN if a > b, compare.EQUAL if equal
44
+ */
45
+ const comparePreRelease = (a: string, b: string): number => {
46
+ const aParts = a.split(".");
47
+ const bParts = b.split(".");
48
+ const maxLength = Math.max(aParts.length, bParts.length);
49
+
50
+ for (let i = 0; i < maxLength; i++) {
51
+ const aPart = aParts[i];
52
+ const bPart = bParts[i];
53
+
54
+ // A larger set of pre-release fields has higher precedence
55
+ if (aPart === undefined) return compare.LESS_THAN;
56
+ if (bPart === undefined) return compare.GREATER_THAN;
57
+
58
+ const aIsNumeric = /^\d+$/.test(aPart);
59
+ const bIsNumeric = /^\d+$/.test(bPart);
60
+
61
+ // Numeric identifiers always have lower precedence than non-numeric
62
+ if (aIsNumeric && !bIsNumeric) return compare.LESS_THAN;
63
+ if (!aIsNumeric && bIsNumeric) return compare.GREATER_THAN;
64
+
65
+ if (aIsNumeric && bIsNumeric) {
66
+ // Compare numerically
67
+ const aNum = parseInt(aPart);
68
+ const bNum = parseInt(bPart);
69
+ if (aNum < bNum) return compare.LESS_THAN;
70
+ if (aNum > bNum) return compare.GREATER_THAN;
71
+ } else {
72
+ // Compare lexically (ASCII sort order)
73
+ if (aPart < bPart) return compare.LESS_THAN;
74
+ if (aPart > bPart) return compare.GREATER_THAN;
75
+ }
76
+ }
77
+
78
+ return compare.EQUAL;
79
+ };
80
+
37
81
  /**
38
82
  * Compares the two semantic versions.
39
83
  *
@@ -55,8 +99,14 @@ export const compareSemVer = ((
55
99
  opts.checkPatch ??= true;
56
100
  const semA = semVerZ.parse(a);
57
101
  const semB = semVerZ.parse(b);
58
- const [aMajor, aMinor, aPatch] = semA.split(".").map(Number);
59
- const [bMajor, bMinor, bPatch] = semB.split(".").map(Number);
102
+
103
+ // Split version and pre-release parts
104
+ const [aCore, aPreRelease] = semA.split("-");
105
+ const [bCore, bPreRelease] = semB.split("-");
106
+
107
+ const [aMajor, aMinor, aPatch] = aCore.split(".").map(Number);
108
+ const [bMajor, bMinor, bPatch] = bCore.split(".").map(Number);
109
+
60
110
  if (opts.checkMajor) {
61
111
  if (aMajor < bMajor) return compare.LESS_THAN;
62
112
  if (aMajor > bMajor) return compare.GREATER_THAN;
@@ -69,7 +119,15 @@ export const compareSemVer = ((
69
119
  if (aPatch < bPatch) return compare.LESS_THAN;
70
120
  if (aPatch > bPatch) return compare.GREATER_THAN;
71
121
  }
72
- return compare.EQUAL;
122
+
123
+ // When major.minor.patch are equal, compare pre-release versions
124
+ // Version without pre-release > version with pre-release
125
+ if (aPreRelease === undefined && bPreRelease === undefined) return compare.EQUAL;
126
+ if (aPreRelease === undefined) return compare.GREATER_THAN;
127
+ if (bPreRelease === undefined) return compare.LESS_THAN;
128
+
129
+ // Both have pre-release, compare them
130
+ return comparePreRelease(aPreRelease, bPreRelease);
73
131
  }) satisfies compare.Comparator<SemVer>;
74
132
 
75
133
  /**
@@ -10,7 +10,7 @@
10
10
  import { describe, expect, it, test } from "vitest";
11
11
 
12
12
  import { type numeric } from "@/numeric";
13
- import * as bounds from "@/spatial/bounds/bounds";
13
+ import { bounds } from "@/spatial/bounds";
14
14
  import { testutil } from "@/testutil";
15
15
 
16
16
  describe("Bounds", () => {
@@ -7,7 +7,7 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { math } from "@/math";
10
+ import { abs, add, equal as mathEqual, min as mathMin, sub } from "@/math/math";
11
11
  import { type numeric } from "@/numeric";
12
12
  import { type Bounds, bounds, type CrudeBounds } from "@/spatial/base";
13
13
 
@@ -479,7 +479,7 @@ export const buildInsertionPlan = <T extends numeric.Value>(
479
479
  }
480
480
  let deleteInBetween = upper.index - lower.index;
481
481
  let insertInto = lower.index;
482
- let removeBefore = math.sub(Number(span(_bounds[lower.index])), lower.position);
482
+ let removeBefore = sub(Number(span(_bounds[lower.index])), lower.position);
483
483
  // If we're overlapping with the previous bound, we need to slice out one less
484
484
  // and insert one further up.
485
485
  if (lower.position !== 0) {
@@ -596,7 +596,7 @@ export const traverse = <T extends numeric.Value = number>(
596
596
  let remainingDist = dist;
597
597
  let currentPosition = start as number | bigint;
598
598
 
599
- while (math.equal(remainingDist, 0) === false) {
599
+ while (mathEqual(remainingDist, 0) === false) {
600
600
  // Find the bound we're currently in or adjacent to
601
601
  const index = _bounds.findIndex((b) => {
602
602
  if (dir > 0) return currentPosition >= b.lower && currentPosition < b.upper;
@@ -606,19 +606,16 @@ export const traverse = <T extends numeric.Value = number>(
606
606
  if (index !== -1) {
607
607
  const b = _bounds[index];
608
608
  let distanceInBound: T;
609
- if (dir > 0) distanceInBound = math.sub(b.upper, currentPosition);
610
- else distanceInBound = math.sub(currentPosition, b.lower) as T;
609
+ if (dir > 0) distanceInBound = sub(b.upper, currentPosition);
610
+ else distanceInBound = sub(currentPosition, b.lower) as T;
611
611
 
612
612
  if (distanceInBound > (0 as T)) {
613
- const moveDist = math.min(math.abs(remainingDist), distanceInBound);
614
- currentPosition = math.add(
615
- currentPosition,
616
- dir > 0 ? moveDist : -moveDist,
617
- ) as T;
618
- remainingDist = math.sub<T>(remainingDist, dir > 0 ? moveDist : -moveDist);
613
+ const moveDist = mathMin(abs(remainingDist), distanceInBound);
614
+ currentPosition = add(currentPosition, dir > 0 ? moveDist : -moveDist) as T;
615
+ remainingDist = sub<T>(remainingDist, dir > 0 ? moveDist : -moveDist);
619
616
 
620
617
  // If we've exhausted the distance, return the current position
621
- if (math.equal(remainingDist, 0)) return currentPosition as T;
618
+ if (mathEqual(remainingDist, 0)) return currentPosition as T;
622
619
  continue;
623
620
  }
624
621
  }
@@ -9,9 +9,9 @@
9
9
 
10
10
  import { describe, expect, it, test } from "vitest";
11
11
 
12
- import * as box from "@/spatial/box/box";
13
- import * as location from "@/spatial/location/location";
14
- import type * as xy from "@/spatial/xy/xy";
12
+ import { box } from "@/spatial/box";
13
+ import { location } from "@/spatial/location";
14
+ import { type xy } from "@/spatial/xy";
15
15
 
16
16
  describe("Box", () => {
17
17
  describe("construction", () => {
@@ -9,11 +9,11 @@
9
9
 
10
10
  import { z } from "zod";
11
11
 
12
- import type * as bounds from "@/spatial/bounds/bounds";
13
- import type * as dimensions from "@/spatial/dimensions/dimensions";
14
- import * as direction from "@/spatial/direction/direction";
15
- import * as location from "@/spatial/location/location";
16
- import * as xy from "@/spatial/xy/xy";
12
+ import { type bounds } from "@/spatial/bounds";
13
+ import { type dimensions } from "@/spatial/dimensions";
14
+ import { direction } from "@/spatial/direction";
15
+ import { location } from "@/spatial/location";
16
+ import { xy } from "@/spatial/xy";
17
17
 
18
18
  const cssPos = z.union([z.number(), z.string()]);
19
19
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { describe, expect, test } from "vitest";
11
11
 
12
- import * as dimensions from "@/spatial/dimensions/dimensions";
12
+ import { dimensions } from "@/spatial/dimensions";
13
13
 
14
14
  describe("Dimensions", () => {
15
15
  describe("construction", () => {
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { describe, expect, it, test } from "vitest";
11
11
 
12
- import * as direction from "@/spatial/direction/direction";
12
+ import { direction } from "@/spatial/direction";
13
13
 
14
14
  describe("Direction", () => {
15
15
  describe("construction", () => {
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { describe, expect, test } from "vitest";
11
11
 
12
- import * as location from "@/spatial/location/location";
12
+ import { location } from "@/spatial/location";
13
13
 
14
14
  describe("Location", () => {
15
15
  describe("construction", () => {
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { describe, expect, it, test } from "vitest";
11
11
 
12
- import * as box from "@/spatial/box/box";
12
+ import { box } from "@/spatial/box";
13
13
  import { Scale, XY } from "@/spatial/scale/scale";
14
14
 
15
15
  type ScaleSpec = [name: string, scale: Scale<number>, i: number, o: number];
@@ -11,12 +11,12 @@ import { z } from "zod";
11
11
 
12
12
  import { clamp } from "@/clamp/clamp";
13
13
  import { type numeric } from "@/numeric";
14
- import * as bounds from "@/spatial/bounds/bounds";
14
+ import { bounds } from "@/spatial/bounds";
15
+ import { box } from "@/spatial/box";
15
16
  import { type Box, isBox } from "@/spatial/box/box";
16
- import * as box from "@/spatial/box/box";
17
- import type * as dims from "@/spatial/dimensions/dimensions";
18
- import * as location from "@/spatial/location/location";
19
- import * as xy from "@/spatial/xy/xy";
17
+ import { type dimensions } from "@/spatial/dimensions";
18
+ import { location } from "@/spatial/location";
19
+ import { xy } from "@/spatial/xy";
20
20
 
21
21
  export const crudeXYTransform = z.object({ offset: xy.crudeZ, scale: xy.crudeZ });
22
22
  export type XYTransformT = z.infer<typeof crudeXYTransform>;
@@ -449,7 +449,7 @@ export class XY {
449
449
  return new XY().magnify(xy);
450
450
  }
451
451
 
452
- static scale(box: dims.Dimensions | Box): XY {
452
+ static scale(box: dimensions.Dimensions | Box): XY {
453
453
  return new XY().scale(box);
454
454
  }
455
455
 
@@ -484,7 +484,7 @@ export class XY {
484
484
  return next;
485
485
  }
486
486
 
487
- scale(b: Box | dims.Dimensions): XY {
487
+ scale(b: Box | dimensions.Dimensions): XY {
488
488
  const next = this.copy();
489
489
  if (isBox(b)) {
490
490
  const prevRoot = this.currRoot;
@@ -98,6 +98,55 @@ describe("TimeStamp", () => {
98
98
  ).toBe(true);
99
99
  });
100
100
 
101
+ describe("datetime-local format parsing", () => {
102
+ test("should parse as UTC by default", () => {
103
+ const ts = new TimeStamp("2025-11-03T17:44:45.500");
104
+ const utcTS = new TimeStamp("2025-11-03T17:44:45.500Z");
105
+
106
+ expect(ts.valueOf()).toEqual(utcTS.valueOf());
107
+ });
108
+
109
+ test("should handle 1-digit milliseconds with default UTC", () => {
110
+ const ts = new TimeStamp("2025-11-03T17:44:45.5");
111
+ expect(ts.millisecond).toBe(500);
112
+ });
113
+
114
+ test("should handle 1-digit milliseconds with local", () => {
115
+ const ts = new TimeStamp("2025-11-03T17:44:45.5", "local");
116
+ expect(ts.millisecond).toBe(500);
117
+ });
118
+
119
+ test("should handle 2-digit milliseconds", () => {
120
+ const ts = new TimeStamp("2025-11-03T17:44:45.50");
121
+ expect(ts.millisecond).toBe(500);
122
+ });
123
+
124
+ test("should handle 3-digit milliseconds", () => {
125
+ const ts = new TimeStamp("2025-11-03T17:44:45.809");
126
+ expect(ts.millisecond).toBe(809);
127
+ });
128
+
129
+ test("should handle 810 milliseconds", () => {
130
+ const ts = new TimeStamp("2025-11-03T17:44:45.810");
131
+ expect(ts.millisecond).toBeGreaterThanOrEqual(809);
132
+ expect(ts.millisecond).toBeLessThanOrEqual(810);
133
+ });
134
+
135
+ test("should handle datetime without milliseconds", () => {
136
+ const ts = new TimeStamp("2025-11-03T17:44:45");
137
+ expect(ts.millisecond).toBe(0);
138
+ });
139
+
140
+ test("should round-trip when using local tzInfo", () => {
141
+ const input = "2025-11-03T17:44:45.809";
142
+ const ts1 = new TimeStamp(input, "local");
143
+ const output = ts1.toString("ISO", "local").slice(0, -1);
144
+ const ts2 = new TimeStamp(output, "local");
145
+
146
+ expect(ts1.valueOf()).toEqual(ts2.valueOf());
147
+ });
148
+ });
149
+
101
150
  test("construct from date", () => {
102
151
  const ts = new TimeStamp([2021, 1, 1], "UTC");
103
152
  expect(ts.date().getUTCFullYear()).toEqual(2021);
@@ -734,6 +783,44 @@ describe("TimeStamp", () => {
734
783
  });
735
784
  });
736
785
  });
786
+
787
+ describe("formatBySpan", () => {
788
+ test("should return 'shortDate' for spans >= 30 days", () => {
789
+ const ts = new TimeStamp([2022, 12, 15], "UTC");
790
+ const span = TimeSpan.days(30);
791
+ expect(ts.formatBySpan(span)).toBe("shortDate");
792
+ });
793
+
794
+ test("should return 'dateTime' for spans >= 1 day", () => {
795
+ const ts = new TimeStamp([2022, 12, 15], "UTC");
796
+ const span = TimeSpan.days(1);
797
+ expect(ts.formatBySpan(span)).toBe("dateTime");
798
+ });
799
+
800
+ test("should return 'time' for spans >= 1 hour", () => {
801
+ const ts = new TimeStamp([2022, 12, 15], "UTC");
802
+ const span = TimeSpan.hours(1);
803
+ expect(ts.formatBySpan(span)).toBe("time");
804
+ });
805
+
806
+ test("should return 'preciseTime' for spans >= 1 second", () => {
807
+ const ts = new TimeStamp([2022, 12, 15], "UTC");
808
+ const span = TimeSpan.seconds(1);
809
+ expect(ts.formatBySpan(span)).toBe("preciseTime");
810
+ });
811
+
812
+ test("should return 'ISOTime' for spans < 1 second", () => {
813
+ const ts = new TimeStamp([2022, 12, 15], "UTC");
814
+ const span = TimeSpan.milliseconds(500);
815
+ expect(ts.formatBySpan(span)).toBe("ISOTime");
816
+ });
817
+
818
+ test("should work with very small spans", () => {
819
+ const ts = new TimeStamp([2022, 12, 15], "UTC");
820
+ const span = TimeSpan.microseconds(100);
821
+ expect(ts.formatBySpan(span)).toBe("ISOTime");
822
+ });
823
+ });
737
824
  });
738
825
 
739
826
  describe("TimeSpan", () => {