@fogpipe/forma-core 0.10.2 → 0.10.4

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.
@@ -0,0 +1,241 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ formatValue,
4
+ isValidFormat,
5
+ parseDecimalFormat,
6
+ SUPPORTED_FORMATS,
7
+ DECIMAL_FORMAT_PATTERN,
8
+ } from "../format/index.js";
9
+
10
+ describe("format module", () => {
11
+ describe("constants", () => {
12
+ it("SUPPORTED_FORMATS includes expected formats", () => {
13
+ expect(SUPPORTED_FORMATS).toContain("currency");
14
+ expect(SUPPORTED_FORMATS).toContain("percent");
15
+ expect(SUPPORTED_FORMATS).toContain("date");
16
+ expect(SUPPORTED_FORMATS).toContain("datetime");
17
+ });
18
+
19
+ it("DECIMAL_FORMAT_PATTERN matches valid decimal formats", () => {
20
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal(0)")).toBe(true);
21
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal(1)")).toBe(true);
22
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal(2)")).toBe(true);
23
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal(10)")).toBe(true);
24
+ });
25
+
26
+ it("DECIMAL_FORMAT_PATTERN rejects invalid formats", () => {
27
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal")).toBe(false);
28
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal()")).toBe(false);
29
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal(-1)")).toBe(false);
30
+ expect(DECIMAL_FORMAT_PATTERN.test("decimal(a)")).toBe(false);
31
+ expect(DECIMAL_FORMAT_PATTERN.test("DECIMAL(2)")).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe("isValidFormat", () => {
36
+ it("returns true for supported formats", () => {
37
+ expect(isValidFormat("currency")).toBe(true);
38
+ expect(isValidFormat("percent")).toBe(true);
39
+ expect(isValidFormat("date")).toBe(true);
40
+ expect(isValidFormat("datetime")).toBe(true);
41
+ });
42
+
43
+ it("returns true for valid decimal formats", () => {
44
+ expect(isValidFormat("decimal(0)")).toBe(true);
45
+ expect(isValidFormat("decimal(2)")).toBe(true);
46
+ expect(isValidFormat("decimal(5)")).toBe(true);
47
+ });
48
+
49
+ it("returns false for invalid formats", () => {
50
+ expect(isValidFormat("invalid")).toBe(false);
51
+ expect(isValidFormat("decimal")).toBe(false);
52
+ expect(isValidFormat("decimal()")).toBe(false);
53
+ expect(isValidFormat("CURRENCY")).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe("parseDecimalFormat", () => {
58
+ it("extracts decimal places from valid format", () => {
59
+ expect(parseDecimalFormat("decimal(0)")).toBe(0);
60
+ expect(parseDecimalFormat("decimal(1)")).toBe(1);
61
+ expect(parseDecimalFormat("decimal(2)")).toBe(2);
62
+ expect(parseDecimalFormat("decimal(10)")).toBe(10);
63
+ });
64
+
65
+ it("returns null for non-decimal formats", () => {
66
+ expect(parseDecimalFormat("currency")).toBeNull();
67
+ expect(parseDecimalFormat("percent")).toBeNull();
68
+ expect(parseDecimalFormat("invalid")).toBeNull();
69
+ });
70
+ });
71
+
72
+ describe("formatValue", () => {
73
+ describe("no format specified", () => {
74
+ it("converts values to strings", () => {
75
+ expect(formatValue(123)).toBe("123");
76
+ expect(formatValue("hello")).toBe("hello");
77
+ expect(formatValue(true)).toBe("true");
78
+ expect(formatValue(false)).toBe("false");
79
+ });
80
+
81
+ it("handles null and undefined", () => {
82
+ expect(formatValue(null)).toBe("null");
83
+ expect(formatValue(undefined)).toBe("undefined");
84
+ });
85
+ });
86
+
87
+ describe("decimal format", () => {
88
+ it("formats numbers with specified decimal places", () => {
89
+ expect(formatValue(123.456, "decimal(0)")).toBe("123");
90
+ expect(formatValue(123.456, "decimal(1)")).toBe("123.5");
91
+ expect(formatValue(123.456, "decimal(2)")).toBe("123.46");
92
+ expect(formatValue(123.456, "decimal(5)")).toBe("123.45600");
93
+ });
94
+
95
+ it("pads with zeros when needed", () => {
96
+ expect(formatValue(123, "decimal(2)")).toBe("123.00");
97
+ });
98
+
99
+ it("handles negative numbers", () => {
100
+ expect(formatValue(-123.456, "decimal(2)")).toBe("-123.46");
101
+ });
102
+
103
+ it("falls back to string for non-numbers", () => {
104
+ expect(formatValue("not a number", "decimal(2)")).toBe("not a number");
105
+ });
106
+ });
107
+
108
+ describe("currency format", () => {
109
+ it("formats numbers as USD currency by default", () => {
110
+ const result = formatValue(1234.56, "currency");
111
+ expect(result).toBe("$1,234.56");
112
+ });
113
+
114
+ it("formats zero", () => {
115
+ expect(formatValue(0, "currency")).toBe("$0.00");
116
+ });
117
+
118
+ it("formats negative numbers", () => {
119
+ expect(formatValue(-100, "currency")).toBe("-$100.00");
120
+ });
121
+
122
+ it("respects currency option", () => {
123
+ const result = formatValue(1234.56, "currency", { currency: "EUR" });
124
+ // EUR formatting varies by locale
125
+ expect(result).toContain("1,234.56");
126
+ });
127
+
128
+ it("falls back to string for non-numbers", () => {
129
+ expect(formatValue("not a number", "currency")).toBe("not a number");
130
+ });
131
+ });
132
+
133
+ describe("percent format", () => {
134
+ it("formats numbers as percentages", () => {
135
+ expect(formatValue(0.5, "percent")).toBe("50%");
136
+ expect(formatValue(1, "percent")).toBe("100%");
137
+ expect(formatValue(0.156, "percent")).toBe("15.6%");
138
+ });
139
+
140
+ it("handles edge cases", () => {
141
+ expect(formatValue(0, "percent")).toBe("0%");
142
+ expect(formatValue(-0.5, "percent")).toBe("-50%");
143
+ });
144
+
145
+ it("falls back to string for non-numbers", () => {
146
+ expect(formatValue("not a number", "percent")).toBe("not a number");
147
+ });
148
+ });
149
+
150
+ describe("date format", () => {
151
+ it("formats Date objects", () => {
152
+ const date = new Date("2024-03-15T10:30:00Z");
153
+ const result = formatValue(date, "date");
154
+ // Contains month, day, year in some format
155
+ expect(result).toMatch(/3\/15\/2024|15\/3\/2024/);
156
+ });
157
+
158
+ it("formats date strings", () => {
159
+ const result = formatValue("2024-03-15", "date");
160
+ expect(result).toMatch(/3\/15\/2024|15\/3\/2024|3\/14\/2024|14\/3\/2024/);
161
+ });
162
+
163
+ it("falls back to string for invalid dates", () => {
164
+ expect(formatValue("not a date", "date")).toBe("not a date");
165
+ });
166
+ });
167
+
168
+ describe("datetime format", () => {
169
+ it("formats Date objects with time", () => {
170
+ const date = new Date("2024-03-15T10:30:00Z");
171
+ const result = formatValue(date, "datetime");
172
+ // Contains both date and time components
173
+ expect(result).toMatch(/\d{1,2}\/\d{1,2}\/\d{2,4}/);
174
+ expect(result).toMatch(/\d{1,2}:\d{2}/);
175
+ });
176
+
177
+ it("falls back to string for invalid dates", () => {
178
+ expect(formatValue("not a date", "datetime")).toBe("not a date");
179
+ });
180
+ });
181
+
182
+ describe("unknown format", () => {
183
+ it("falls back to string conversion", () => {
184
+ expect(formatValue(123, "unknown")).toBe("123");
185
+ expect(formatValue("hello", "invalid")).toBe("hello");
186
+ });
187
+ });
188
+
189
+ describe("nullDisplay option", () => {
190
+ it("uses nullDisplay for null values", () => {
191
+ expect(formatValue(null, undefined, { nullDisplay: "—" })).toBe("—");
192
+ expect(formatValue(null, "decimal(2)", { nullDisplay: "N/A" })).toBe("N/A");
193
+ });
194
+
195
+ it("uses nullDisplay for undefined values", () => {
196
+ expect(formatValue(undefined, undefined, { nullDisplay: "—" })).toBe("—");
197
+ expect(formatValue(undefined, "currency", { nullDisplay: "-" })).toBe("-");
198
+ });
199
+
200
+ it("does not affect non-null values", () => {
201
+ expect(formatValue(123, "decimal(2)", { nullDisplay: "—" })).toBe("123.00");
202
+ expect(formatValue(0, "decimal(2)", { nullDisplay: "—" })).toBe("0.00");
203
+ });
204
+ });
205
+
206
+ describe("locale option", () => {
207
+ it("respects locale for currency formatting", () => {
208
+ const result = formatValue(1234.56, "currency", { locale: "de-DE", currency: "EUR" });
209
+ // German locale uses comma for decimals
210
+ expect(result).toContain("1.234,56");
211
+ });
212
+
213
+ it("respects locale for percent formatting", () => {
214
+ const result = formatValue(0.5, "percent", { locale: "de-DE" });
215
+ // German locale uses comma for decimals
216
+ expect(result).toContain("50");
217
+ expect(result).toContain("%");
218
+ });
219
+ });
220
+
221
+ describe("edge cases", () => {
222
+ it("handles NaN", () => {
223
+ expect(formatValue(NaN, "decimal(2)")).toBe("NaN");
224
+ });
225
+
226
+ it("handles Infinity", () => {
227
+ expect(formatValue(Infinity, "decimal(2)")).toBe("Infinity");
228
+ expect(formatValue(-Infinity, "decimal(2)")).toBe("-Infinity");
229
+ });
230
+
231
+ it("handles very large numbers", () => {
232
+ const result = formatValue(1e15, "currency");
233
+ expect(result).toContain("$");
234
+ });
235
+
236
+ it("handles very small numbers", () => {
237
+ expect(formatValue(0.0001, "decimal(4)")).toBe("0.0001");
238
+ });
239
+ });
240
+ });
241
+ });
@@ -275,4 +275,349 @@ describe("validate", () => {
275
275
  expect(result.errors[0].message).toBe("Name must be at least 2 characters");
276
276
  });
277
277
  });
278
+
279
+ // ============================================================================
280
+ // Array validation
281
+ // ============================================================================
282
+
283
+ describe("array validation", () => {
284
+ describe("minItems/maxItems from schema", () => {
285
+ it("should validate minItems from schema when fieldDef does not specify it", () => {
286
+ const spec: Forma = {
287
+ version: "1.0",
288
+ meta: { id: "test", title: "Test" },
289
+ schema: {
290
+ type: "object",
291
+ properties: {
292
+ items: {
293
+ type: "array",
294
+ items: { type: "string" },
295
+ minItems: 2,
296
+ },
297
+ },
298
+ },
299
+ fields: {
300
+ items: { label: "Items", type: "array" },
301
+ },
302
+ fieldOrder: ["items"],
303
+ };
304
+
305
+ // Valid - has minimum 2 items
306
+ expect(validate({ items: ["a", "b"] }, spec).valid).toBe(true);
307
+ expect(validate({ items: ["a", "b", "c"] }, spec).valid).toBe(true);
308
+
309
+ // Invalid - less than minItems
310
+ const result = validate({ items: ["a"] }, spec);
311
+ expect(result.valid).toBe(false);
312
+ expect(result.errors[0].field).toBe("items");
313
+ expect(result.errors[0].message).toBe("Items must have at least 2 items");
314
+ });
315
+
316
+ it("should validate maxItems from schema when fieldDef does not specify it", () => {
317
+ const spec: Forma = {
318
+ version: "1.0",
319
+ meta: { id: "test", title: "Test" },
320
+ schema: {
321
+ type: "object",
322
+ properties: {
323
+ tags: {
324
+ type: "array",
325
+ items: { type: "string" },
326
+ maxItems: 3,
327
+ },
328
+ },
329
+ },
330
+ fields: {
331
+ tags: { label: "Tags", type: "array" },
332
+ },
333
+ fieldOrder: ["tags"],
334
+ };
335
+
336
+ // Valid - has maximum 3 items
337
+ expect(validate({ tags: ["a", "b", "c"] }, spec).valid).toBe(true);
338
+ expect(validate({ tags: ["a"] }, spec).valid).toBe(true);
339
+
340
+ // Invalid - more than maxItems
341
+ const result = validate({ tags: ["a", "b", "c", "d"] }, spec);
342
+ expect(result.valid).toBe(false);
343
+ expect(result.errors[0].field).toBe("tags");
344
+ expect(result.errors[0].message).toBe("Tags must have no more than 3 items");
345
+ });
346
+
347
+ it("should allow fieldDef.minItems to override schema.minItems", () => {
348
+ const spec: Forma = {
349
+ version: "1.0",
350
+ meta: { id: "test", title: "Test" },
351
+ schema: {
352
+ type: "object",
353
+ properties: {
354
+ items: {
355
+ type: "array",
356
+ items: { type: "string" },
357
+ minItems: 5, // schema says 5
358
+ },
359
+ },
360
+ },
361
+ fields: {
362
+ items: {
363
+ label: "Items",
364
+ type: "array",
365
+ minItems: 2, // fieldDef overrides to 2
366
+ },
367
+ },
368
+ fieldOrder: ["items"],
369
+ };
370
+
371
+ // fieldDef.minItems (2) should take precedence over schema.minItems (5)
372
+ expect(validate({ items: ["a", "b"] }, spec).valid).toBe(true);
373
+ expect(validate({ items: ["a"] }, spec).valid).toBe(false);
374
+ });
375
+ });
376
+
377
+ describe("array item validation from schema", () => {
378
+ it("should validate array item type constraints from schema", () => {
379
+ const spec: Forma = {
380
+ version: "1.0",
381
+ meta: { id: "test", title: "Test" },
382
+ schema: {
383
+ type: "object",
384
+ properties: {
385
+ scores: {
386
+ type: "array",
387
+ items: {
388
+ type: "object",
389
+ properties: {
390
+ name: { type: "string" },
391
+ score: { type: "number", minimum: 0, maximum: 100 },
392
+ },
393
+ required: ["name", "score"],
394
+ },
395
+ },
396
+ },
397
+ },
398
+ fields: {
399
+ scores: { label: "Scores", type: "array" },
400
+ },
401
+ fieldOrder: ["scores"],
402
+ };
403
+
404
+ // Valid data
405
+ const validResult = validate(
406
+ {
407
+ scores: [
408
+ { name: "Alice", score: 95 },
409
+ { name: "Bob", score: 87 },
410
+ ],
411
+ },
412
+ spec
413
+ );
414
+ expect(validResult.valid).toBe(true);
415
+
416
+ // Invalid - score above maximum
417
+ const maxResult = validate(
418
+ {
419
+ scores: [
420
+ { name: "Alice", score: 105 },
421
+ { name: "Bob", score: 87 },
422
+ ],
423
+ },
424
+ spec
425
+ );
426
+ expect(maxResult.valid).toBe(false);
427
+ expect(maxResult.errors[0].field).toBe("scores[0].score");
428
+ expect(maxResult.errors[0].message).toContain("no more than 100");
429
+
430
+ // Invalid - score below minimum
431
+ const minResult = validate(
432
+ {
433
+ scores: [
434
+ { name: "Alice", score: -5 },
435
+ { name: "Bob", score: 87 },
436
+ ],
437
+ },
438
+ spec
439
+ );
440
+ expect(minResult.valid).toBe(false);
441
+ expect(minResult.errors[0].field).toBe("scores[0].score");
442
+ expect(minResult.errors[0].message).toContain("at least 0");
443
+ });
444
+
445
+ it("should validate required fields in array items from schema", () => {
446
+ const spec: Forma = {
447
+ version: "1.0",
448
+ meta: { id: "test", title: "Test" },
449
+ schema: {
450
+ type: "object",
451
+ properties: {
452
+ people: {
453
+ type: "array",
454
+ items: {
455
+ type: "object",
456
+ properties: {
457
+ name: { type: "string" },
458
+ age: { type: "integer" },
459
+ },
460
+ required: ["name"],
461
+ },
462
+ },
463
+ },
464
+ },
465
+ fields: {
466
+ people: { label: "People", type: "array" },
467
+ },
468
+ fieldOrder: ["people"],
469
+ };
470
+
471
+ // Valid - name is present (age is optional)
472
+ expect(
473
+ validate({ people: [{ name: "Alice" }, { name: "Bob", age: 30 }] }, spec).valid
474
+ ).toBe(true);
475
+
476
+ // Invalid - missing required name
477
+ const result = validate({ people: [{ age: 25 }] }, spec);
478
+ expect(result.valid).toBe(false);
479
+ expect(result.errors[0].field).toBe("people[0].name");
480
+ expect(result.errors[0].message).toContain("required");
481
+ });
482
+
483
+ it("should validate nested string constraints in array items", () => {
484
+ const spec: Forma = {
485
+ version: "1.0",
486
+ meta: { id: "test", title: "Test" },
487
+ schema: {
488
+ type: "object",
489
+ properties: {
490
+ emails: {
491
+ type: "array",
492
+ items: {
493
+ type: "object",
494
+ properties: {
495
+ address: { type: "string", format: "email" },
496
+ label: { type: "string", minLength: 1, maxLength: 20 },
497
+ },
498
+ required: ["address"],
499
+ },
500
+ },
501
+ },
502
+ },
503
+ fields: {
504
+ emails: { label: "Emails", type: "array" },
505
+ },
506
+ fieldOrder: ["emails"],
507
+ };
508
+
509
+ // Valid
510
+ expect(
511
+ validate({ emails: [{ address: "test@example.com", label: "Work" }] }, spec).valid
512
+ ).toBe(true);
513
+
514
+ // Invalid email format
515
+ const emailResult = validate({ emails: [{ address: "not-an-email" }] }, spec);
516
+ expect(emailResult.valid).toBe(false);
517
+ expect(emailResult.errors[0].field).toBe("emails[0].address");
518
+ expect(emailResult.errors[0].message).toContain("valid email");
519
+
520
+ // Invalid label (too long)
521
+ const labelResult = validate(
522
+ { emails: [{ address: "test@example.com", label: "This label is way too long for the constraint" }] },
523
+ spec
524
+ );
525
+ expect(labelResult.valid).toBe(false);
526
+ expect(labelResult.errors[0].field).toBe("emails[0].label");
527
+ expect(labelResult.errors[0].message).toContain("no more than 20");
528
+ });
529
+
530
+ it("should validate multipleOf in array item fields", () => {
531
+ const spec: Forma = {
532
+ version: "1.0",
533
+ meta: { id: "test", title: "Test" },
534
+ schema: {
535
+ type: "object",
536
+ properties: {
537
+ prices: {
538
+ type: "array",
539
+ items: {
540
+ type: "object",
541
+ properties: {
542
+ amount: { type: "number", multipleOf: 0.01 },
543
+ },
544
+ },
545
+ },
546
+ },
547
+ },
548
+ fields: {
549
+ prices: { label: "Prices", type: "array" },
550
+ },
551
+ fieldOrder: ["prices"],
552
+ };
553
+
554
+ // Valid - correct precision
555
+ expect(validate({ prices: [{ amount: 10.99 }, { amount: 5.0 }] }, spec).valid).toBe(true);
556
+
557
+ // Invalid - wrong precision
558
+ const result = validate({ prices: [{ amount: 10.999 }] }, spec);
559
+ expect(result.valid).toBe(false);
560
+ expect(result.errors[0].field).toBe("prices[0].amount");
561
+ expect(result.errors[0].message).toContain("multiple of 0.01");
562
+ });
563
+ });
564
+
565
+ describe("combined fieldDef and schema validation", () => {
566
+ it("should use itemFields for custom validations while using schema for type constraints", () => {
567
+ const spec: Forma = {
568
+ version: "1.0",
569
+ meta: { id: "test", title: "Test" },
570
+ schema: {
571
+ type: "object",
572
+ properties: {
573
+ orders: {
574
+ type: "array",
575
+ items: {
576
+ type: "object",
577
+ properties: {
578
+ quantity: { type: "integer", minimum: 1 },
579
+ price: { type: "number", minimum: 0 },
580
+ },
581
+ },
582
+ minItems: 1,
583
+ },
584
+ },
585
+ },
586
+ fields: {
587
+ orders: {
588
+ label: "Orders",
589
+ type: "array",
590
+ itemFields: {
591
+ quantity: {
592
+ label: "Quantity",
593
+ validations: [
594
+ { rule: "value <= 100", message: "Cannot order more than 100 items" },
595
+ ],
596
+ },
597
+ },
598
+ },
599
+ },
600
+ fieldOrder: ["orders"],
601
+ };
602
+
603
+ // Valid
604
+ expect(validate({ orders: [{ quantity: 5, price: 10.0 }] }, spec).valid).toBe(true);
605
+
606
+ // Invalid - schema minimum violated
607
+ const minResult = validate({ orders: [{ quantity: 0, price: 10.0 }] }, spec);
608
+ expect(minResult.valid).toBe(false);
609
+ expect(minResult.errors[0].message).toContain("at least 1");
610
+
611
+ // Invalid - custom FEEL validation violated
612
+ const customResult = validate({ orders: [{ quantity: 150, price: 10.0 }] }, spec);
613
+ expect(customResult.valid).toBe(false);
614
+ expect(customResult.errors.some((e) => e.message.includes("more than 100"))).toBe(true);
615
+
616
+ // Invalid - empty array (minItems from schema)
617
+ const emptyResult = validate({ orders: [] }, spec);
618
+ expect(emptyResult.valid).toBe(false);
619
+ expect(emptyResult.errors[0].message).toContain("at least 1 items");
620
+ });
621
+ });
622
+ });
278
623
  });
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { evaluate } from "../feel/index.js";
9
+ import { formatValue, type FormatOptions } from "../format/index.js";
9
10
  import type {
10
11
  Forma,
11
12
  ComputedField,
@@ -275,12 +276,14 @@ function findComputedDependencies(
275
276
  * @param fieldName - Name of the computed field
276
277
  * @param data - Current form data
277
278
  * @param spec - Form specification
278
- * @returns Formatted string or null if not displayable
279
+ * @param options - Formatting options (locale, currency, nullDisplay)
280
+ * @returns Formatted string or null if field not found or value is null/undefined
279
281
  */
280
282
  export function getFormattedValue(
281
283
  fieldName: string,
282
284
  data: Record<string, unknown>,
283
- spec: Forma
285
+ spec: Forma,
286
+ options?: FormatOptions
284
287
  ): string | null {
285
288
  if (!spec.computed?.[fieldName]) {
286
289
  return null;
@@ -291,54 +294,14 @@ export function getFormattedValue(
291
294
  const value = computed[fieldName];
292
295
 
293
296
  if (value === null || value === undefined) {
297
+ // If nullDisplay is specified, use formatValue to get it
298
+ if (options?.nullDisplay !== undefined) {
299
+ return formatValue(value, fieldDef.format, options);
300
+ }
294
301
  return null;
295
302
  }
296
303
 
297
- return formatValue(value, fieldDef.format);
298
- }
299
-
300
- /**
301
- * Format a value according to a format specification
302
- *
303
- * Supported formats:
304
- * - decimal(n) - Number with n decimal places
305
- * - currency - Number formatted as currency
306
- * - percent - Number formatted as percentage
307
- * - (none) - Default string conversion
308
- */
309
- function formatValue(value: unknown, format?: string): string {
310
- if (!format) {
311
- return String(value);
312
- }
313
-
314
- // Handle decimal(n) format
315
- const decimalMatch = format.match(/^decimal\((\d+)\)$/);
316
- if (decimalMatch) {
317
- const decimals = parseInt(decimalMatch[1], 10);
318
- return typeof value === "number" ? value.toFixed(decimals) : String(value);
319
- }
320
-
321
- // Handle currency format
322
- if (format === "currency") {
323
- return typeof value === "number"
324
- ? new Intl.NumberFormat("en-US", {
325
- style: "currency",
326
- currency: "USD",
327
- }).format(value)
328
- : String(value);
329
- }
330
-
331
- // Handle percent format
332
- if (format === "percent") {
333
- return typeof value === "number"
334
- ? new Intl.NumberFormat("en-US", {
335
- style: "percent",
336
- minimumFractionDigits: 1,
337
- }).format(value)
338
- : String(value);
339
- }
340
-
341
- return String(value);
304
+ return formatValue(value, fieldDef.format, options);
342
305
  }
343
306
 
344
307
  // ============================================================================
@@ -12,6 +12,17 @@ export {
12
12
  getFormattedValue,
13
13
  } from "./calculate.js";
14
14
 
15
+ // Format
16
+ export {
17
+ formatValue,
18
+ isValidFormat,
19
+ parseDecimalFormat,
20
+ SUPPORTED_FORMATS,
21
+ DECIMAL_FORMAT_PATTERN,
22
+ } from "../format/index.js";
23
+
24
+ export type { FormatOptions, SupportedFormat } from "../format/index.js";
25
+
15
26
  // Visibility
16
27
  export {
17
28
  getVisibility,