From: Ardis Lu Date: Tue, 27 May 2025 05:10:07 +0000 (-0700) Subject: Refactor `DECIMAL_SEPARATOR` to a function X-Git-Tag: v0.1.0~10 X-Git-Url: https://git.ardis.lu/?a=commitdiff_plain;h=d352efcf0e1a8a6e9ada5f9195bc9dc05b783af8;p=csvio Refactor `DECIMAL_SEPARATOR` to a function This change enables easier testing and also allows future improvements for dynamically changing the locale during runtime. --- diff --git a/src/normalization.js b/src/normalization.js index f142843..04e24af 100644 --- a/src/normalization.js +++ b/src/normalization.js @@ -1,7 +1,14 @@ import { TransformStream } from 'node:stream/web'; -// User locale's decimal separator -const DECIMAL_SEPARATOR = Intl.NumberFormat().formatToParts(.1).find(p => p.type === 'decimal').value; +/** + * Get the decimal separator character to parse numbers formatted in different locales. + * @param {string|Intl.Locale} [locale] A `string` Unicode locale identifier, or an `Intl.Locale` object specifying + * the locale that is applicable. If no value is provided, the current execution environment's locale is used. + * @returns {string} The locale's decimal separator as a `string` (e.g., "." for the "en-US" locale). + */ +export function getDecimalSeparator(locale) { + return Intl.NumberFormat(locale).formatToParts(.1).find(p => p.type === 'decimal').value; +} /** * Convert a string to camelCase. @@ -40,7 +47,7 @@ export function expandScientificNotation(str, truncate = false) { } const e = Number(exponent); let expanded = null; - if (mantissa.indexOf(DECIMAL_SEPARATOR) === -1) { + if (mantissa.indexOf(getDecimalSeparator()) === -1) { expanded = mantissa + '0'.repeat(e); } else { @@ -201,7 +208,7 @@ export class CSVNormalizer extends TransformStream { // Excel added extra characters from setting the format to "Accounting" or other custom formats // Delete everything that's not a number or the decimal separator - const replaced = original.replaceAll(new RegExp(`[^0-9${DECIMAL_SEPARATOR}]`, 'g'), ''); + const replaced = original.replaceAll(new RegExp(`[^0-9${getDecimalSeparator()}]`, 'g'), ''); const num2 = Number(replaced); if (!Number.isNaN(num2) && replaced !== '') { return num2; @@ -240,9 +247,10 @@ export class CSVNormalizer extends TransformStream { // Excel added extra characters from setting the format to "Accounting" or other custom formats // Delete everything that's not a number or the decimal separator try { - const str = original.replaceAll(new RegExp(`[^0-9${DECIMAL_SEPARATOR}]`, 'g'), ''); + const decimalSeparator = getDecimalSeparator(); + const str = original.replaceAll(new RegExp(`[^0-9${decimalSeparator}]`, 'g'), ''); if (str === '') { throw new Error(); } - return BigInt(str.split(DECIMAL_SEPARATOR)[0]).toString(); + return BigInt(str.split(decimalSeparator)[0]).toString(); } catch { } // Otherwise, pass through as string diff --git a/test/normalization.test.js b/test/normalization.test.js index 4ca8e49..e41a876 100644 --- a/test/normalization.test.js +++ b/test/normalization.test.js @@ -1,9 +1,33 @@ import { suite, test } from 'node:test'; -import { deepStrictEqual, throws } from 'node:assert/strict'; +import { ok, deepStrictEqual, throws } from 'node:assert/strict'; import { csvStreamEqualWritable, createCSVMockStream, assertConsole } from './utils.js'; import { CSVReader } from '../src/core.js'; -import { toCamelCase, expandScientificNotation, CSVNormalizer, CSVDenormalizer } from '../src/normalization.js'; +import { getDecimalSeparator, toCamelCase, expandScientificNotation, CSVNormalizer, CSVDenormalizer } from '../src/normalization.js'; + +suite('getDecimalSeparator', { concurrency: true }, () => { + test('returns a value if no locale is specified', { concurrency: true }, () => { + const s = getDecimalSeparator(); + ok(typeof s === 'string'); + ok(s.length === 1); + }); + test('returns "." for locale identifier "en-US"', { concurrency: true }, () => { + const s = getDecimalSeparator('en-US'); + deepStrictEqual(s, '.'); + }); + test('returns "," for locale identifier "de-DE"', { concurrency: true }, () => { + const s = getDecimalSeparator('de-DE'); + deepStrictEqual(s, ','); + }); + test('returns "." for `Intl.Locale` object set to "en-US"', { concurrency: true }, () => { + const s = getDecimalSeparator(new Intl.Locale('en-US')); + deepStrictEqual(s, '.'); + }); + test('returns "," for `Intl.Locale` object set to "de-DE"', { concurrency: true }, () => { + const s = getDecimalSeparator(new Intl.Locale('de-DE')); + deepStrictEqual(s, ','); + }); +}); suite('toCamelCase', { concurrency: true }, () => { const vectors = [