Skip to content

Commit fa775c5

Browse files
committed
fix(tables): read a lone separator against the column's currency
Review of the previous commit found the marker signal alone was too coarse. The evidence: a currency with decimal places ALWAYS formats with both separators — `1.234,56 €`, `$1,234.56`, `1.234,000 KWD`. Only a zero-decimal currency emits a lone one (`1.235 ¥`). So a marker does not imply grouping; the currency's own decimal places decide, and a three-decimal currency's trailing three digits are decimals however the value arrived. That matters most for CSV import, which passed no currency at all: a KWD column importing `0,500` read as five hundred rather than a half. The column's code is now forwarded, since nothing in a CSV carries a marker to fall back on. A bare typed `1.234` still reads as decimals.
1 parent dabf2d1 commit fa775c5

4 files changed

Lines changed: 40 additions & 7 deletions

File tree

apps/sim/lib/table/__tests__/currency.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,19 @@ describe('parseCurrencyInput', () => {
172172
expect(parseCurrencyInput('1,500')).toBe(1500)
173173
})
174174

175+
it('reads a lone separator against the currency, not just the marker', () => {
176+
// A currency with decimals always formats with BOTH separators, so a lone
177+
// one never comes from a formatter for those — only zero-decimal
178+
// currencies produce `1.235 ¥`. And a three-decimal currency's trailing
179+
// three digits are decimals however the value arrived, which matters for
180+
// CSV import, where nothing carries a marker.
181+
expect(parseCurrencyInput('1.235 ¥', 'JPY')).toBe(1235)
182+
expect(parseCurrencyInput('0,500', 'KWD')).toBe(0.5)
183+
expect(parseCurrencyInput('12,000', 'TND')).toBe(12)
184+
expect(parseCurrencyInput('1,500', 'USD')).toBe(1500)
185+
expect(parseCurrencyInput('1.234')).toBe(1.234)
186+
})
187+
175188
it('refuses a scale suffix rather than shrinking the value', () => {
176189
// `1.2 M` read as 1.2 would rewrite a column of millions a millionfold too
177190
// small — the same invented-value failure as an identifier, inverted.

apps/sim/lib/table/currency.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,15 @@ function loneSeparatorIsGrouping(
314314
currencyCode: string | undefined
315315
): boolean {
316316
if (!new RegExp(`\\${separator}\\d{3}$`).test(digitsAndSeps)) return false
317-
if (separator === ',' && !hadMarker) return true
318-
if (!hadMarker) return false
319-
return currencyFractionDigits(currencyCode) !== 3
317+
// A currency with three decimal places (KWD, TND) legitimately ends in three
318+
// digits after its separator, so for those the reading is always decimal —
319+
// regardless of how the value arrived, since a CSV import carries no marker.
320+
if (currencyFractionDigits(currencyCode) === 3) return false
321+
// A comma tail of exactly three digits is grouping by convention (`1,500`).
322+
if (separator === ',') return true
323+
// A dot is the decimal point in the notation most users type, so it only
324+
// reads as grouping when a marker shows a formatter produced the string.
325+
return hadMarker
320326
}
321327

322328
/** A currency's conventional decimal places, defaulting to 2 when unknown. */

apps/sim/lib/table/import.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ describe('import', () => {
145145
expect(coerceValue('ask sales', 'currency')).toBeNull()
146146
})
147147

148+
it('reads an imported amount against the column currency', () => {
149+
// Nothing in a CSV carries a currency marker, so the column's own code is
150+
// the only signal for a three-decimal currency: `0,500` KWD is a half,
151+
// not five hundred.
152+
expect(coerceValue('0,500', 'currency', { currencyCode: 'KWD' })).toBe(0.5)
153+
expect(coerceValue('12,000', 'currency', { currencyCode: 'TND' })).toBe(12)
154+
expect(coerceValue('1,500', 'currency', { currencyCode: 'USD' })).toBe(1500)
155+
})
156+
148157
it('coerces booleans strictly', () => {
149158
expect(coerceValue('true', 'boolean')).toBe(true)
150159
expect(coerceValue('FALSE', 'boolean')).toBe(false)

apps/sim/lib/table/import.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ export function inferSchemaFromCsv(
385385
export function coerceValue(
386386
value: unknown,
387387
colType: CsvColumnType,
388-
options?: NormalizeDateCellOptions
388+
options?: NormalizeDateCellOptions & { currencyCode?: string }
389389
): string | number | boolean | null | Record<string, unknown> | unknown[] {
390390
if (value === null || value === undefined || value === '') return null
391391
switch (colType) {
@@ -394,9 +394,11 @@ export function coerceValue(
394394
return Number.isNaN(n) ? null : n
395395
}
396396
// Importing into an existing currency column: the file carries the
397-
// formatted amount (`$1,234.56`) but the cell stores a bare number.
397+
// formatted amount (`$1,234.56`) but the cell stores a bare number. The
398+
// column's currency is forwarded because it decides how a lone separator
399+
// reads — a three-decimal currency's `0,500` is a half, not five hundred.
398400
case 'currency':
399-
return parseCurrencyInput(value)
401+
return parseCurrencyInput(value, options?.currencyCode)
400402
case 'boolean': {
401403
const s = String(value).toLowerCase()
402404
if (s === 'true') return true
@@ -590,7 +592,10 @@ export function coerceRowsForTable(
590592
const col = colByName.get(colName)
591593
if (!col) continue
592594
const colType = (col.type as CsvColumnType) ?? 'string'
593-
coerced[getColumnId(col)] = coerceValue(value, colType, options) as RowData[string]
595+
coerced[getColumnId(col)] = coerceValue(value, colType, {
596+
...options,
597+
...(col.currencyCode !== undefined ? { currencyCode: col.currencyCode } : {}),
598+
}) as RowData[string]
594599
}
595600
return coerced
596601
})

0 commit comments

Comments
 (0)