@synnaxlabs/x 0.46.0 → 0.46.2

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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"}
@@ -0,0 +1,2 @@
1
+ export declare const deduplicateFileName: (name: string, existingNames: Set<string>) => string;
2
+ //# sourceMappingURL=deduplicateFileName.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deduplicateFileName.d.ts","sourceRoot":"","sources":["../../../src/strings/deduplicateFileName.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,EACZ,eAAe,GAAG,CAAC,MAAM,CAAC,KACzB,MAiBF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=deduplicateFileName.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deduplicateFileName.spec.d.ts","sourceRoot":"","sources":["../../../src/strings/deduplicateFileName.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ export * from './deduplicateFileName';
2
+ export * from './strings';
3
+ //# sourceMappingURL=external.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external.d.ts","sourceRoot":"","sources":["../../../src/strings/external.ts"],"names":[],"mappings":"AASA,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mBAAmB,CAAC"}
@@ -1,2 +1,2 @@
1
- export * as strings from './strings';
1
+ export * as strings from './external';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/strings/index.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,OAAO,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/strings/index.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,OAAO,MAAM,oBAAoB,CAAC"}
@@ -45,5 +45,4 @@ export declare const createShortIdentifiers: (name: string) => string[];
45
45
  * ```
46
46
  */
47
47
  export declare const trimPrefix: (str: string, prefix: string) => string;
48
- export declare const pluralName: (name: string) => string;
49
48
  //# sourceMappingURL=strings.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../../src/strings/strings.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,MAAM,GAAG,MAAM,EAAE,EAC1B,aAAY,MAAW,KACtB,MAOF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,MAAM,KAAG,MAAM,EAuC3D,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,EAAE,QAAQ,MAAM,KAAG,MAGxD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,MAYzC,CAAC"}
1
+ {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../../src/strings/strings.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAS,MAAM,GAAG,MAAM,EAAE,EAC1B,aAAY,MAAW,KACtB,MAOF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,MAAM,KAAG,MAAM,EAuC3D,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,EAAE,QAAQ,MAAM,KAAG,MAGxD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synnaxlabs/x",
3
- "version": "0.46.0",
3
+ "version": "0.46.2",
4
4
  "type": "module",
5
5
  "description": "Common Utilities for Synnax Labs",
6
6
  "repository": "https://github.com/synnaxlabs/synnax/tree/main/x/ts",
@@ -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
 
@@ -0,0 +1,84 @@
1
+ // Copyright 2025 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ import { deduplicateFileName } from "@/strings/deduplicateFileName";
13
+
14
+ describe("deduplicateFileName", () => {
15
+ it("should return the original name when it does not exist", () =>
16
+ expect(deduplicateFileName("Report", new Set(["Summary"]))).toBe("Report"));
17
+
18
+ it("should append (1) when a duplicate exists", () =>
19
+ expect(deduplicateFileName("Report", new Set(["Report"]))).toBe("Report (1)"));
20
+
21
+ it("should increment to the next available number", () =>
22
+ expect(
23
+ deduplicateFileName("Report", new Set(["Report", "Report (1)", "Report (2)"])),
24
+ ).toBe("Report (3)"));
25
+
26
+ it("should fill gaps in numbering", () =>
27
+ expect(
28
+ deduplicateFileName("Report", new Set(["Report", "Report (1)", "Report (3)"])),
29
+ ).toBe("Report (2)"));
30
+
31
+ it("should handle names already suffixed with a number", () =>
32
+ expect(deduplicateFileName("Report (2)", new Set(["Report (2)"]))).toBe(
33
+ "Report (3)",
34
+ ));
35
+
36
+ it("should not be confused by numbers elsewhere in the name", () =>
37
+ expect(
38
+ deduplicateFileName("Report 2024", new Set(["Report 2024", "Report 2024 (1)"])),
39
+ ).toBe("Report 2024 (2)"));
40
+
41
+ it("should handle names with non-numeric parentheses at the end", () =>
42
+ expect(deduplicateFileName("Report (draft)", new Set(["Report (draft)"]))).toBe(
43
+ "Report (draft) (1)",
44
+ ));
45
+
46
+ it("should escalate correctly when many duplicates exist", () => {
47
+ const existing = new Set<string>(["Report"]);
48
+ for (let i = 1; i <= 100; i++) existing.add(`Report (${i})`);
49
+ expect(deduplicateFileName("Report", existing)).toBe("Report (101)");
50
+ });
51
+
52
+ it("should return empty when empty name does not exist", () =>
53
+ expect(deduplicateFileName("", new Set(["Report"]))).toBe(""));
54
+
55
+ it("should append (1) to empty when empty name exists", () =>
56
+ expect(deduplicateFileName("", new Set([""]))).toBe(" (1)"));
57
+
58
+ // Edge cases
59
+ it("should keep unique names with trailing spaces unchanged", () =>
60
+ expect(deduplicateFileName("Report ", new Set(["Report"]))).toBe("Report "));
61
+
62
+ it("should normalize double spaces before numeric suffix when incrementing", () =>
63
+ expect(deduplicateFileName("Report (1)", new Set(["Report (1)"]))).toBe(
64
+ "Report (2)",
65
+ ));
66
+
67
+ it("should preserve multiple internal spaces when appending suffix", () =>
68
+ expect(deduplicateFileName("Annual Report", new Set(["Annual Report"]))).toBe(
69
+ "Annual Report (1)",
70
+ ));
71
+
72
+ it("should handle unicode names when appending suffix", () =>
73
+ expect(deduplicateFileName("café", new Set(["café"]))).toBe("café (1)"));
74
+
75
+ it("should not treat fullwidth parentheses/digits as numeric suffix", () =>
76
+ expect(deduplicateFileName("Report(1)", new Set(["Report(1)"]))).toBe(
77
+ "Report(1) (1)",
78
+ ));
79
+
80
+ it("should handle emoji in names when appending suffix", () =>
81
+ expect(deduplicateFileName("Report 📄", new Set(["Report 📄"]))).toBe(
82
+ "Report 📄 (1)",
83
+ ));
84
+ });
@@ -0,0 +1,32 @@
1
+ // Copyright 2025 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ export const deduplicateFileName = (
11
+ name: string,
12
+ existingNames: Set<string>,
13
+ ): string => {
14
+ if (!existingNames.has(name)) return name;
15
+ let baseName = name;
16
+ let i = 1;
17
+ let currentName = name;
18
+ while (existingNames.has(currentName)) {
19
+ const match = currentName.match(filenameEndingRegex);
20
+ if (match) {
21
+ baseName = currentName.slice(0, match.index).trim();
22
+ i = parseInt(match[1]) + 1;
23
+ } else {
24
+ baseName = currentName;
25
+ i = 1;
26
+ }
27
+ currentName = `${baseName} (${i})`;
28
+ }
29
+ return currentName;
30
+ };
31
+
32
+ const filenameEndingRegex = /\((\d+)\)$/;
@@ -0,0 +1,11 @@
1
+ // Copyright 2025 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ export * from "@/strings/deduplicateFileName";
11
+ export * from "@/strings/strings";
@@ -7,4 +7,4 @@
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
- export * as strings from "@/strings/strings";
10
+ export * as strings from "@/strings/external";
@@ -103,45 +103,3 @@ describe("trimPrefix", () => {
103
103
  it("should handle numbers in prefix", () =>
104
104
  expect(strings.trimPrefix("123abc", "123")).toBe("abc"));
105
105
  });
106
-
107
- describe("pluralName", () => {
108
- it("should handle empty string", () => expect(strings.pluralName("")).toBe(""));
109
-
110
- it("should add 's' to regular words", () => {
111
- expect(strings.pluralName("cat")).toBe("cats");
112
- expect(strings.pluralName("dog")).toBe("dogs");
113
- expect(strings.pluralName("regularType")).toBe("regularTypes");
114
- });
115
-
116
- it("should convert 'y' endings to 'ies'", () => {
117
- expect(strings.pluralName("company")).toBe("companies");
118
- expect(strings.pluralName("yEndingType")).toBe("yEndingTypes");
119
- expect(strings.pluralName("baby")).toBe("babies");
120
- });
121
-
122
- it("should add 'es' to words ending in 's'", () => {
123
- expect(strings.pluralName("class")).toBe("classes");
124
- expect(strings.pluralName("bus")).toBe("buses");
125
- });
126
-
127
- it("should add 'es' to words ending in 'x'", () => {
128
- expect(strings.pluralName("box")).toBe("boxes");
129
- expect(strings.pluralName("fox")).toBe("foxes");
130
- });
131
-
132
- it("should add 'es' to words ending in 'ch'", () => {
133
- expect(strings.pluralName("catch")).toBe("catches");
134
- expect(strings.pluralName("church")).toBe("churches");
135
- });
136
-
137
- it("should add 'es' to words ending in 'sh'", () => {
138
- expect(strings.pluralName("bush")).toBe("bushes");
139
- expect(strings.pluralName("brush")).toBe("brushes");
140
- });
141
-
142
- it("should work with built-in type names", () => {
143
- expect(strings.pluralName("string")).toBe("strings");
144
- expect(strings.pluralName("number")).toBe("numbers");
145
- expect(strings.pluralName("object")).toBe("objects");
146
- });
147
- });
@@ -108,17 +108,3 @@ export const trimPrefix = (str: string, prefix: string): string => {
108
108
  if (str.startsWith(prefix)) return str.slice(prefix.length);
109
109
  return str;
110
110
  };
111
-
112
- export const pluralName = (name: string): string => {
113
- if (name.length === 0) return name;
114
- if (name[name.length - 1] === "y") return `${name.slice(0, -1)}ies`;
115
- if (
116
- name[name.length - 1] === "s" ||
117
- name[name.length - 1] === "x" ||
118
- name[name.length - 1] === "z" ||
119
- name.endsWith("ch") ||
120
- name.endsWith("sh")
121
- )
122
- return `${name}es`;
123
- return `${name}s`;
124
- };