From: Ardis Lu Date: Sun, 25 May 2025 04:45:53 +0000 (-0700) Subject: Refactor `expandScientificNotation` to throw errors instead of using sentinel `null... X-Git-Tag: v0.1.0~17 X-Git-Url: https://git.ardis.lu/?a=commitdiff_plain;h=ce24095c36e9fa1bd55148f690cc396b49f33968;p=csvio Refactor `expandScientificNotation` to throw errors instead of using sentinel `null` value --- diff --git a/src/normalization.js b/src/normalization.js index 3debefe..a241eb1 100644 --- a/src/normalization.js +++ b/src/normalization.js @@ -24,26 +24,31 @@ export function toCamelCase(str) { * * @param {string} str A string representing a number expressed using scientific notation (e.g., `"1E+18"`). * @param {boolean} truncate If `true`, decimals will be truncated in the expanded string. Defaults to `false`. - * @returns {string|null} A string representing the normal form of the number (e.g., `"1000000000000000000"`), or `null` - * if the input could not be parsed. + * @returns {string} A string representing the normal form of the number (e.g., `"1000000000000000000"`). + * @throws {TypeError} If `str` is not a `string`, a `TypeError` will be thrown. + * @throws {SyntaxError} If `str` is not valid scientific notation, a `SyntaxError` will be thrown. */ export function expandScientificNotation(str, truncate = false) { + if (typeof str !== 'string') { + throw new TypeError(`Expected a string, but received ${typeof str}.`); + } + const [mantissa, exponent] = str.split('E'); + if (mantissa === undefined || exponent === undefined) { + throw new SyntaxError(`Unable to parse string "${str}" as scientific notation.`); + } + const e = Number(exponent); let expanded = null; - const [mantissa, exponent] = str.split?.('E') ?? [undefined, undefined]; - if (mantissa !== undefined && exponent !== undefined) { - const e = Number(exponent); - if (mantissa.indexOf(DECIMAL_SEPARATOR) === -1) { - expanded = mantissa + '0'.repeat(e); + if (mantissa.indexOf(DECIMAL_SEPARATOR) === -1) { + expanded = mantissa + '0'.repeat(e); + } + else { + const [integer, decimal] = mantissa.split('.'); + expanded = integer + decimal.padEnd(e, '0'); + if (truncate) { + expanded = expanded.substring(0, e + 1); } - else { - const [integer, decimal] = mantissa.split('.'); - expanded = integer + decimal.padEnd(e, '0'); - if (truncate) { - expanded = expanded.substring(0, e + 1); - } - else if (mantissa.length - 2 > e) { // Must re-insert decimal - expanded = expanded.substring(0, e + 1) + '.' + expanded.substring(e + 1); - } + else if (mantissa.length - 2 > e) { // Must re-insert decimal + expanded = expanded.substring(0, e + 1) + '.' + expanded.substring(e + 1); } } return expanded; @@ -228,7 +233,6 @@ export class CSVNormalizer extends TransformStream { // Excel converted to scientific notation try { const expanded = expandScientificNotation(original, true); - if (expanded === null) { throw new Error(); } return BigInt(expanded).toString(); } catch { } diff --git a/test/normalization.test.js b/test/normalization.test.js index 8398651..92b8fe3 100644 --- a/test/normalization.test.js +++ b/test/normalization.test.js @@ -1,5 +1,5 @@ import { suite, test } from 'node:test'; -import { deepStrictEqual } from 'node:assert/strict'; +import { deepStrictEqual, throws } from 'node:assert/strict'; import { csvStreamEqualWritable, createCSVMockStream, assertConsole } from './utils.js'; import { CSVReader } from '../src/core.js'; @@ -32,14 +32,14 @@ suite('toCamelCase', { concurrency: true }, () => { suite('expandScientificNotation', { concurrency: true }, () => { const vectors = [ - { name: 'returns null for string integer', input: '123', output: null }, - { name: 'returns null for string BigInt', input: '123n', output: null }, - { name: 'returns null for string number', input: '123.456', output: null }, - { name: 'returns null for string non-numeric', input: 'abc', output: null }, - { name: 'returns null for number', input: 123, output: null }, - { name: 'returns null for BigInt', input: 123n, output: null }, - { name: 'returns null for array', input: [], output: null }, - { name: 'returns null for object', input: {}, output: null }, + { name: 'throws SyntaxError for string integer', input: '123', error: SyntaxError }, + { name: 'throws SyntaxError for string BigInt', input: '123n', error: SyntaxError }, + { name: 'throws SyntaxError for string number', input: '123.456', error: SyntaxError }, + { name: 'throws SyntaxError for string non-numeric', input: 'abc', error: SyntaxError }, + { name: 'throws TypeError for number', input: 123, error: TypeError }, + { name: 'throws TypeError for BigInt', input: 123n, error: TypeError }, + { name: 'throws TypeError for array', input: [], error: TypeError }, + { name: 'throws TypeError for object', input: {}, error: TypeError }, { name: 'returns expanded form for positive exponent', input: '1E18', output: '1000000000000000000' }, { name: 'returns expanded form for positive exponent (with plus)', input: '1E+18', output: '1000000000000000000' }, { name: 'returns expanded form for negative mantissa', input: '-1E18', output: '-1000000000000000000' }, @@ -51,9 +51,15 @@ suite('expandScientificNotation', { concurrency: true }, () => { { name: 'truncates when decimals greater than exponent by 1', input: '1.111E2', truncate: true, output: '111' }, { name: 'truncates when decimals greater than exponent by 2', input: '1.1111E2', truncate: true, output: '111' }, ]; - for (const { name, input, truncate = false, output } of vectors) { + for (const { name, input, truncate = false, output, error } of vectors) { test(name, { concurrency: true }, () => { - deepStrictEqual(expandScientificNotation(input, truncate), output); + if (error !== undefined) { + // @ts-expect-error + throws(() => expandScientificNotation(input, truncate), error); + } + else { + deepStrictEqual(expandScientificNotation(input, truncate), output); + } }); } });