@synnaxlabs/x 0.46.0 → 0.46.1

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.
@@ -37,4 +37,14 @@ import { bounds } from '../spatial/bounds';
37
37
  * ```
38
38
  */
39
39
  export declare const roundBySpan: (value: number, b: bounds.Bounds<number>) => number;
40
+ /**
41
+ * Intelligently rounds a number using span-based or significant figure logic.
42
+ * Designed for UI display where floating-point noise needs to be cleaned up.
43
+ *
44
+ * @param value - The number to be rounded.
45
+ * @param b - Optional bounds. Uses span-based rounding when span is significant,
46
+ * otherwise uses significant figures.
47
+ * @returns The rounded number.
48
+ */
49
+ export declare const smartRound: (value: number, b?: bounds.Bounds<number>) => number;
40
50
  //# sourceMappingURL=round.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"round.d.ts","sourceRoot":"","sources":["../../../src/math/round.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAQ1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAG,MAarE,CAAC"}
1
+ {"version":3,"file":"round.d.ts","sourceRoot":"","sources":["../../../src/math/round.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAQ1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAG,MAarE,CAAC;AAKF;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAG,MAkCrE,CAAC"}
@@ -45,6 +45,7 @@ export type Crude<DetailsSchema = z.ZodNever, V extends Variant = Variant> = Opt
45
45
  });
46
46
  export declare const exceptionDetailsSchema: z.ZodObject<{
47
47
  stack: z.ZodString;
48
+ error: z.ZodCustom<Error, Error>;
48
49
  }, z.core.$strip>;
49
50
  export declare const fromException: (exc: unknown, message?: string) => Status<typeof exceptionDetailsSchema, "error">;
50
51
  export declare const create: <DetailsSchema = z.ZodNever, V extends Variant = Variant>(spec: Crude<DetailsSchema, V>) => Status<DetailsSchema, V>;
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/status/status.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/status/status.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synnaxlabs/x",
3
- "version": "0.46.0",
3
+ "version": "0.46.1",
4
4
  "type": "module",
5
5
  "description": "Common Utilities for Synnax Labs",
6
6
  "repository": "https://github.com/synnaxlabs/synnax/tree/main/x/ts",
@@ -27,9 +27,9 @@
27
27
  "typescript": "^5.9.2",
28
28
  "vite": "^7.1.5",
29
29
  "vitest": "^3.2.4",
30
+ "eslint-config-synnaxlabs": "^0.43.0",
30
31
  "@synnaxlabs/tsconfig": "^0.43.0",
31
- "@synnaxlabs/vite-plugin": "^0.43.0",
32
- "eslint-config-synnaxlabs": "^0.43.0"
32
+ "@synnaxlabs/vite-plugin": "^0.43.0"
33
33
  },
34
34
  "main": "./dist/index.cjs",
35
35
  "module": "./dist/index.js",
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-loss-of-precision */
1
2
  // Copyright 2025 Synnax Labs, Inc.
2
3
  //
3
4
  // Use of this software is governed by the Business Source License included in the file
@@ -79,3 +80,170 @@ describe("roundBySpan", () => {
79
80
  });
80
81
  });
81
82
  });
83
+
84
+ interface SmartRoundTestCase {
85
+ value: number;
86
+ bounds?: bounds.Bounds<number>;
87
+ expected: number;
88
+ }
89
+
90
+ describe("smartRound", () => {
91
+ describe("span-based rounding (significant span)", () => {
92
+ const cases: SmartRoundTestCase[] = [
93
+ { value: 1234.5678, bounds: { lower: 0, upper: 2000 }, expected: 1234.57 },
94
+ { value: -5432.987654, bounds: { lower: -6000, upper: 0 }, expected: -5432.99 },
95
+ { value: 1.23456, bounds: { lower: 0, upper: 2 }, expected: 1.235 },
96
+ { value: -15.6789, bounds: { lower: -20, upper: -10 }, expected: -15.679 },
97
+ { value: 0.123456, bounds: { lower: 0, upper: 0.2 }, expected: 0.123 },
98
+ { value: 0.0001234567, bounds: { lower: 0, upper: 0.001 }, expected: 0.00012 },
99
+ {
100
+ value: 0.00000123456789,
101
+ bounds: { lower: 0, upper: 0.00001 },
102
+ expected: 0.0000012,
103
+ },
104
+ { value: 999.9999, bounds: { lower: 0, upper: 1500 }, expected: 1000.0 },
105
+ { value: 0.99999, bounds: { lower: 0, upper: 1.5 }, expected: 1.0 },
106
+ ];
107
+ cases.forEach(({ value, bounds, expected }) => {
108
+ it(`${value} with span ${(bounds!.upper - bounds!.lower).toFixed(1)}`, () => {
109
+ expect(math.smartRound(value, bounds)).toBe(expected);
110
+ });
111
+ });
112
+ });
113
+
114
+ describe("zero or negligible span", () => {
115
+ const cases: SmartRoundTestCase[] = [
116
+ {
117
+ value: 71.60000610351562,
118
+ bounds: { lower: 71.6, upper: 71.6 },
119
+ expected: 71.6,
120
+ },
121
+ {
122
+ value: 1234567.89123456,
123
+ bounds: { lower: 1234567, upper: 1234567 },
124
+ expected: 1234567.89,
125
+ },
126
+ {
127
+ value: -9876543.21987654,
128
+ bounds: { lower: -9876543, upper: -9876543 },
129
+ expected: -9876543.22,
130
+ },
131
+ { value: 123.456789123, bounds: { lower: 123, upper: 123 }, expected: 123.457 },
132
+ {
133
+ value: -456.789123456,
134
+ bounds: { lower: -456, upper: -456 },
135
+ expected: -456.789,
136
+ },
137
+ {
138
+ value: 0.0001234567,
139
+ bounds: { lower: 0.00012, upper: 0.00012 },
140
+ expected: 0.00012346,
141
+ },
142
+ {
143
+ value: 0.00000987654321,
144
+ bounds: { lower: 0.0000099, upper: 0.0000099 },
145
+ expected: 0.0000098765,
146
+ },
147
+ { value: 1.00000123456, bounds: { lower: 1, upper: 1 }, expected: 1.0 },
148
+ {
149
+ value: 0.999999123456,
150
+ bounds: { lower: 0.999999, upper: 0.999999 },
151
+ expected: 1.0,
152
+ },
153
+ {
154
+ value: 1000.123456789,
155
+ bounds: { lower: 1000, upper: 1000 },
156
+ expected: 1000.12,
157
+ },
158
+ {
159
+ value: 1000000.5,
160
+ bounds: { lower: 1000000.499999, upper: 1000000.500001 },
161
+ expected: 1000000.5,
162
+ },
163
+ ];
164
+ cases.forEach(({ value, bounds, expected }) => {
165
+ it(`${value}`, () => expect(math.smartRound(value, bounds)).toBe(expected));
166
+ });
167
+ });
168
+
169
+ describe("no bounds", () => {
170
+ const cases: SmartRoundTestCase[] = [
171
+ { value: 123456.789123, expected: 123456.79 },
172
+ { value: 71.60000610351562, expected: 71.6 },
173
+ { value: 0.00012345678, expected: 0.00012346 },
174
+ { value: 9876543210.123456, expected: 9876543210.12 },
175
+ { value: 0.00000000123456789, expected: 0.0000000012346 },
176
+ { value: -987654.321987, expected: -987654.32 },
177
+ { value: -42.123456789, expected: -42.123 },
178
+ { value: -0.000456789123, expected: -0.00045679 },
179
+ { value: 999.9999, expected: 1000.0 },
180
+ { value: 0.9999999, expected: 1.0 },
181
+ ];
182
+ cases.forEach(({ value, expected }) => {
183
+ it(`${value}`, () => expect(math.smartRound(value)).toBe(expected));
184
+ });
185
+ });
186
+
187
+ describe("edge cases", () => {
188
+ it("NaN", () => expect(Number.isNaN(math.smartRound(NaN))).toBe(true));
189
+ it("NaN with bounds", () =>
190
+ expect(Number.isNaN(math.smartRound(NaN, { lower: 0, upper: 10 }))).toBe(true));
191
+ it("Infinity", () => expect(math.smartRound(Infinity)).toBe(Infinity));
192
+ it("-Infinity", () => expect(math.smartRound(-Infinity)).toBe(-Infinity));
193
+ it("Infinity with bounds", () =>
194
+ expect(math.smartRound(Infinity, { lower: 0, upper: 100 })).toBe(Infinity));
195
+ it("zero", () => expect(math.smartRound(0)).toBe(0));
196
+ it("zero with bounds", () =>
197
+ expect(math.smartRound(0, { lower: -10, upper: 10 })).toBe(0));
198
+ it("very small value", () => expect(math.smartRound(1e-15)).toBeCloseTo(1e-15, 17));
199
+ it("very large value", () => expect(math.smartRound(1e15)).toBe(1e15));
200
+ it("negative zero", () => expect(math.smartRound(-0)).toBe(0));
201
+ });
202
+ describe("boundary conditions", () => {
203
+ it("zero span", () =>
204
+ expect(math.smartRound(123.456789, { lower: 100, upper: 100 })).toBe(123.457));
205
+ it("span above threshold", () =>
206
+ expect(math.smartRound(1000, { lower: 999, upper: 1000 })).toBe(1000.0));
207
+ it("span below threshold", () =>
208
+ expect(math.smartRound(1000000, { lower: 1000000, upper: 1000000 + 1e-12 })).toBe(
209
+ 1000000.0,
210
+ ));
211
+ it("1000 boundary", () => {
212
+ expect(math.smartRound(999.999)).toBe(999.999);
213
+ expect(math.smartRound(1000.001)).toBe(1000.0);
214
+ });
215
+ it("1 boundary", () => {
216
+ expect(math.smartRound(0.999999)).toBe(1.0);
217
+ expect(math.smartRound(1.000001)).toBe(1.0);
218
+ });
219
+ });
220
+ describe("precision preservation", () => {
221
+ it("no artificial precision", () =>
222
+ expect(math.smartRound(1.5, { lower: 1, upper: 2 })).toBe(1.5));
223
+ it("0.1 + 0.2", () => expect(math.smartRound(0.1 + 0.2)).toBe(0.3));
224
+ it("0.1 * 3", () => expect(math.smartRound(0.1 * 3)).toBe(0.3));
225
+ it("integer preservation", () =>
226
+ expect(math.smartRound(42, { lower: 0, upper: 100 })).toBe(42.0));
227
+ it("rounds to integer", () =>
228
+ expect(math.smartRound(42.0001, { lower: 42, upper: 42 })).toBe(42.0));
229
+ });
230
+ describe("stress tests", () => {
231
+ it("random values", () => {
232
+ for (let i = 0; i < 100; i++) {
233
+ const value = Math.random() * 1000000 - 500000;
234
+ expect(Number.isFinite(math.smartRound(value))).toBe(true);
235
+ }
236
+ });
237
+ it("random with bounds", () => {
238
+ for (let i = 0; i < 100; i++) {
239
+ const lower = Math.random() * 1000 - 500;
240
+ const upper = lower + Math.random() * 1000;
241
+ const value = lower + (upper - lower) * Math.random();
242
+ const result = math.smartRound(value, { lower, upper });
243
+ expect(Number.isFinite(result)).toBe(true);
244
+ expect(result).toBeGreaterThanOrEqual(lower - 1);
245
+ expect(result).toBeLessThanOrEqual(upper + 1);
246
+ }
247
+ });
248
+ });
249
+ });
package/src/math/round.ts CHANGED
@@ -66,3 +66,51 @@ export const roundBySpan = (value: number, b: bounds.Bounds<number>): number =>
66
66
  const multiplier = 10 ** decimalPlaces;
67
67
  return Math.round(value * multiplier) / multiplier;
68
68
  };
69
+
70
+ const SIGNIFICANT_FIGURES = 5;
71
+ const MIN_SPAN_THRESHOLD = 1e-10;
72
+
73
+ /**
74
+ * Intelligently rounds a number using span-based or significant figure logic.
75
+ * Designed for UI display where floating-point noise needs to be cleaned up.
76
+ *
77
+ * @param value - The number to be rounded.
78
+ * @param b - Optional bounds. Uses span-based rounding when span is significant,
79
+ * otherwise uses significant figures.
80
+ * @returns The rounded number.
81
+ */
82
+ export const smartRound = (value: number, b?: bounds.Bounds<number>): number => {
83
+ if (Number.isNaN(value) || !Number.isFinite(value)) return value;
84
+ const absValue = Math.abs(value);
85
+ if (absValue === 0) return 0;
86
+ let useSpanBased = false;
87
+ let span = 0;
88
+ if (b != null) {
89
+ span = bounds.span(b);
90
+ const spanRatio = span / absValue;
91
+ useSpanBased = span > 0 && spanRatio > MIN_SPAN_THRESHOLD;
92
+ }
93
+ if (useSpanBased) {
94
+ let decimalPlaces: number;
95
+ if (span >= 1000) decimalPlaces = LARGE_SPAN_DECIMAL_PLACES;
96
+ else if (span >= 1) decimalPlaces = MEDIUM_SPAN_DECIMAL_PLACES;
97
+ else {
98
+ const decimalPlacesInSpan = Math.ceil(-Math.log10(span));
99
+ decimalPlaces = decimalPlacesInSpan + EXTRA_DECIMAL_PLACES;
100
+ }
101
+ const multiplier = 10 ** decimalPlaces;
102
+ return Math.round(value * multiplier) / multiplier;
103
+ }
104
+ if (absValue >= 1000) {
105
+ const multiplier = 10 ** LARGE_SPAN_DECIMAL_PLACES;
106
+ return Math.round(value * multiplier) / multiplier;
107
+ }
108
+ if (absValue >= 1) {
109
+ const multiplier = 10 ** MEDIUM_SPAN_DECIMAL_PLACES;
110
+ return Math.round(value * multiplier) / multiplier;
111
+ }
112
+ const magnitude = Math.floor(Math.log10(absValue));
113
+ const decimalPlaces = SIGNIFICANT_FIGURES - magnitude - 1;
114
+ const multiplier = 10 ** decimalPlaces;
115
+ return Math.round(value * multiplier) / multiplier;
116
+ };
@@ -74,4 +74,59 @@ describe("status", () => {
74
74
  expect(status.removeVariants("success", [])).toBe("success");
75
75
  });
76
76
  });
77
+
78
+ describe("fromException", () => {
79
+ it("should create an error status from an Error instance", () => {
80
+ const error = new Error("Something went wrong");
81
+ const s = status.fromException(error);
82
+
83
+ expect(s.variant).toBe("error");
84
+ expect(s.message).toBe("Something went wrong");
85
+ expect(s.description).toBeUndefined();
86
+ expect(s.details.error).toBe(error);
87
+ expect(s.details.stack).toBe(error.stack ?? "");
88
+ });
89
+
90
+ it("should use custom message and move error message to description", () => {
91
+ const error = new Error("Original error");
92
+ const s = status.fromException(error, "Custom message");
93
+
94
+ expect(s.variant).toBe("error");
95
+ expect(s.message).toBe("Custom message");
96
+ expect(s.description).toBe("Original error");
97
+ expect(s.details.error).toBe(error);
98
+ expect(s.details.stack).toBe(error.stack ?? "");
99
+ });
100
+
101
+ it("should handle errors without stack trace", () => {
102
+ const error = new Error("No stack");
103
+ error.stack = undefined;
104
+ const s = status.fromException(error);
105
+
106
+ expect(s.details.stack).toBe("");
107
+ expect(s.details.error).toBe(error);
108
+ });
109
+
110
+ it("should throw when exception is not an Error instance", () => {
111
+ const notAnError = "just a string";
112
+ expect(() => status.fromException(notAnError)).toThrow("just a string");
113
+ });
114
+
115
+ it("should include valid key and timestamp", () => {
116
+ const error = new Error("Test error");
117
+ const s = status.fromException(error);
118
+
119
+ expect(s.key).toHaveLength(id.LENGTH);
120
+ expect(s.time).toBeInstanceOf(TimeStamp);
121
+ expect(s.time.beforeEq(TimeStamp.now())).toBe(true);
122
+ });
123
+
124
+ it("should conform to exceptionDetailsSchema", () => {
125
+ const error = new Error("Test error");
126
+ const s = status.fromException(error);
127
+
128
+ const result = status.exceptionDetailsSchema.safeParse(s.details);
129
+ expect(result.success).toBe(true);
130
+ });
131
+ });
77
132
  });
@@ -83,6 +83,7 @@ export type Crude<DetailsSchema = z.ZodNever, V extends Variant = Variant> = Opt
83
83
 
84
84
  export const exceptionDetailsSchema = z.object({
85
85
  stack: z.string(),
86
+ error: z.instanceof(Error),
86
87
  });
87
88
 
88
89
  export const fromException = (
@@ -94,9 +95,7 @@ export const fromException = (
94
95
  variant: "error",
95
96
  message: message ?? exc.message,
96
97
  description: message != null ? exc.message : undefined,
97
- details: {
98
- stack: exc.stack ?? "",
99
- },
98
+ details: { stack: exc.stack ?? "", error: exc },
100
99
  });
101
100
  };
102
101