From 0071c9d493d1a3444de73ca0978f3f526d24ec0c Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Tue, 15 Aug 2023 01:51:09 +0800 Subject: [PATCH] Fix stringify bugs Address errors with gapped groups. --- packages/core/.eslintrc | 3 +- packages/core/src/common.ts | 2 +- packages/core/src/converter.ts | 14 ++--- packages/core/src/exponent.ts | 16 ++++-- packages/core/src/systems/en-US.ts | 56 +++++++++++------- packages/core/test/systems/en-US.test.ts | 57 ++++++++++--------- .../core/test/systems/en-US/chongo.test.ts | 2 +- 7 files changed, 85 insertions(+), 65 deletions(-) diff --git a/packages/core/.eslintrc b/packages/core/.eslintrc index 216dd0a..ee1ddec 100644 --- a/packages/core/.eslintrc +++ b/packages/core/.eslintrc @@ -3,7 +3,8 @@ "rules": { "indent": ["error", "tab"], "no-tabs": "off", - "no-continue": "off" + "no-continue": "off", + "max-classes-per-file": "off" }, "extends": [ "lxsmnsyc/typescript" diff --git a/packages/core/src/common.ts b/packages/core/src/common.ts index f73458c..99cd092 100644 --- a/packages/core/src/common.ts +++ b/packages/core/src/common.ts @@ -50,7 +50,7 @@ export interface StringifySystem { * @param place - The group place. * @param options - Options to use when creating the group. */ - makeGroup: (group: string, place?: GroupPlace, options?: Record) => string; + makeGroups: (groups: Group[], options?: Record) => string[]; /** * Groups a string. * @param value - The string to group. diff --git a/packages/core/src/converter.ts b/packages/core/src/converter.ts index 4522203..b095c86 100644 --- a/packages/core/src/converter.ts +++ b/packages/core/src/converter.ts @@ -45,7 +45,7 @@ export interface StringifyOptions { * Converts a numeric value to its name. * @param value - The value to convert. * @param options - Options to use when converting a value to its name. - * @returns The name of the value. + * @returns string The name of the value. */ export const stringify = ( value: AllowedValue, @@ -65,13 +65,9 @@ export const stringify = ( return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options)); } - const groups = system - .group(valueStr) - .map(([group, place]) => ( - system.makeGroup(group, place, makeGroupOptions) - )); - - return system.finalize(groups); + const groups = system.group(valueStr); + const groupNames = system.makeGroups(groups, makeGroupOptions); + return system.finalize(groupNames); }; /** @@ -94,7 +90,7 @@ export interface ParseOptions { * Parses a name of a number to its numeric equivalent. * @param value - The value to parse. * @param options - Options to use when parsing a name of a number to its numeric equivalent. - * @returns The numeric equivalent of the value. + * @returns AllowedValue The numeric equivalent of the value. */ export const parse = (value: string, options = {} as ParseOptions) => { const { system = enUS, type = 'string' } = options; diff --git a/packages/core/src/exponent.ts b/packages/core/src/exponent.ts index 54398e9..3d235e9 100644 --- a/packages/core/src/exponent.ts +++ b/packages/core/src/exponent.ts @@ -50,7 +50,7 @@ const DEFAULT_NEGATIVE_SYMBOL = '-' as const; * Forces a value to have a decimal point. * @param value - The value to force a decimal point on. * @param decimalPoint - The decimal point character to use. - * @returns The value with a decimal point. + * @returns string The value with a decimal point. */ const forceDecimalPoint = (value: string, decimalPoint: string) => ( value.includes(decimalPoint) @@ -79,7 +79,7 @@ export class InvalidValueTypeError extends TypeError { /** * Forces a number to have a sign. * @param value - The value to force a sign on. - * @returns The value with a sign. + * @returns string The value with a sign. */ const forceNumberSign = (value: number | bigint) => { const isExponentNegative = value < 0; @@ -88,16 +88,22 @@ const forceNumberSign = (value: number | bigint) => { return `${exponentSign}${exponentValueAbs}`; }; +interface ExponentialComponents { + integer: string; + fractional: string; + exponent: string; +} + /** * Extracts the integer, fractional, and exponent components of a string in exponential notation. * @param value - The string value to extract components from. * @param options - Options to use when extracting components. - * @returns The extracted components. + * @returns ExponentialComponents The extracted components. */ export const extractExponentialComponents = ( value: string, options = {} as NumberToExponentialOptions, -) => { +): ExponentialComponents => { const { decimalPoint = DEFAULT_DECIMAL_POINT, groupingSymbol = DEFAULT_GROUPING_SYMBOL, @@ -143,7 +149,7 @@ export const extractExponentialComponents = ( * Converts a numeric value to a string in exponential notation. Supports numbers of all types. * @param value - The value to convert. * @param options - Options to use when extracting components. - * @returns The value in exponential notation. + * @returns string The value in exponential notation. */ export const numberToExponential = ( value: ValidValue, diff --git a/packages/core/src/systems/en-US.ts b/packages/core/src/systems/en-US.ts index 92a3988..8873514 100644 --- a/packages/core/src/systems/en-US.ts +++ b/packages/core/src/systems/en-US.ts @@ -175,7 +175,7 @@ const ILLION_SUFFIX = 'illion' as const; * Builds a name for numbers in tens and ones. * @param tens - Tens digit. * @param ones - Ones digit. - * @returns The name for the number. + * @returns string The name for the number. */ const makeTensName = (tens: number, ones: number) => { if (tens === 0) { @@ -198,7 +198,7 @@ const makeTensName = (tens: number, ones: number) => { * @param hundreds - Hundreds digit. * @param tens - Tens digit. * @param ones - Ones digit. - * @returns The name for the number. + * @returns string The name for the number. */ const makeHundredsName = (hundreds: number, tens: number, ones: number) => { if (hundreds === 0) { @@ -216,7 +216,7 @@ const makeHundredsName = (hundreds: number, tens: number, ones: number) => { * Builds a name for numbers in the millions. * @param millions - Millions digit. * @param milliaCount - Number of millia- groups. - * @returns The millions prefix. + * @returns string The millions prefix. */ const makeMillionsPrefix = (millions: number, milliaCount: number) => { if (milliaCount > 0) { @@ -231,7 +231,7 @@ const makeMillionsPrefix = (millions: number, milliaCount: number) => { * @param decillions - Decillions digit. * @param millions - Millions digit. * @param milliaCount - Number of millia- groups. - * @returns The decillions prefix. + * @returns string The decillions prefix. */ const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => { if (decillions === 0) { @@ -249,7 +249,7 @@ const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: * @param decillions - Decillions digit. * @param millions - Millions digit. * @param milliaCount - Number of millia- groups. - * @returns The centillions prefix. + * @returns string The centillions prefix. */ const makeCentillionsPrefix = ( centillions: number, @@ -332,22 +332,26 @@ const getGroupName = (place: number, shortenMillia: boolean) => { return `${groupGroups}${ILLION_SUFFIX}` as const; }; -export const makeGroup = ( - group: string, - place: number, - options?: Record, -): string => { - const makeHundredsArgs = group - .padStart(3, '0') - .split('') - .map((s) => Number(s)) as [number, number, number]; +export const makeGroups = (groups: Group[], options?: Record): string[] => { + const filteredGroups = groups.filter(([digits, place]) => ( + place === 0 || digits !== EMPTY_GROUP_DIGITS + )); - const groupDigitsName = makeHundredsName(...makeHundredsArgs); - const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false); - if (groupName.length > 0) { - return `${groupDigitsName} ${groupName}` as const; - } - return groupDigitsName; + return filteredGroups.map( + ([group, place]) => { + const makeHundredsArgs = group + .padStart(3, '0') + .split('') + .map((s) => Number(s)) as [number, number, number]; + + const groupDigitsName = makeHundredsName(...makeHundredsArgs); + const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false); + if (groupName.length > 0) { + return `${groupDigitsName} ${groupName}`; + } + return groupDigitsName; + }, + ); }; /** @@ -685,9 +689,17 @@ export const parseGroups = (tokens: string[]) => { }; export const combineGroups = (groups: Group[]) => { - const groupsSorted = groups.sort((a, b) => b[1] - a[1]); // sort by place - const firstGroup = groupsSorted[0]; + const places = groups.map((g) => g[1]); + const maxPlace = Math.max(...places); + const minPlace = Math.min(...places); + const firstGroup = groups.find((g) => g[1] === maxPlace) ?? [...EMPTY_PLACE]; const firstGroupPlace = firstGroup[1]; + const groupsSorted = []; + for (let i = maxPlace; i >= minPlace; i -= 1) { + const thisGroup = groups.find((g) => g[1] === i) ?? [...EMPTY_PLACE]; + groupsSorted.push(thisGroup); + } + const digits = groupsSorted.reduce( (previousDigits, thisGroup) => { const [groupDigits] = thisGroup; diff --git a/packages/core/test/systems/en-US.test.ts b/packages/core/test/systems/en-US.test.ts index e5b8873..7aa8313 100644 --- a/packages/core/test/systems/en-US.test.ts +++ b/packages/core/test/systems/en-US.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import {parse, stringify, systems} from '../../src'; +import { parse, stringify, systems } from '../../src'; import { numberToExponential } from '../../src/exponent'; const options = { system: systems.enUS }; @@ -19,8 +19,8 @@ describe('numerica', () => { ${7} | ${'seven'} ${8} | ${'eight'} ${9} | ${'nine'} - `('converts $ones to $expected', ({ ones, expected }) => { - expect(stringify(ones, options)).toBe(expected) + `('converts $ones to $expected', ({ ones, expected }: { ones: number, expected: string }) => { + expect(stringify(ones, options)).toBe(expected); expect(parse(expected, { ...options, type: 'number' })).toBe(ones); }); }); @@ -38,8 +38,8 @@ describe('numerica', () => { ${17} | ${'seventeen'} ${18} | ${'eighteen'} ${19} | ${'nineteen'} - `('converts $tenPlusOnes to $expected', ({ tenPlusOnes, expected }) => { - expect(stringify(tenPlusOnes, options)).toBe(expected) + `('converts $tenPlusOnes to $expected', ({ tenPlusOnes, expected }: { tenPlusOnes: number, expected: string }) => { + expect(stringify(tenPlusOnes, options)).toBe(expected); expect(parse(expected, { ...options, type: 'number' })).toBe(tenPlusOnes); }); }); @@ -56,10 +56,10 @@ describe('numerica', () => { ${90} | ${99} | ${'ninety'} `('$tensStart-$tensEnd', ({ tensStart, tensBase, - }) => { + }: { tensStart: number, tensBase: string }) => { it.each` value | expected - ${tensStart + 0} | ${tensBase} + ${tensStart} | ${tensBase} ${tensStart + 1} | ${`${tensBase} one`} ${tensStart + 2} | ${`${tensBase} two`} ${tensStart + 3} | ${`${tensBase} three`} @@ -69,7 +69,7 @@ describe('numerica', () => { ${tensStart + 7} | ${`${tensBase} seven`} ${tensStart + 8} | ${`${tensBase} eight`} ${tensStart + 9} | ${`${tensBase} nine`} - `('converts $value to $expected', ({ value, expected }) => { + `('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => { expect(stringify(value, options)).toBe(expected); expect(parse(expected, { ...options, type: 'number' })).toBe(value); }); @@ -88,11 +88,11 @@ describe('numerica', () => { ${900} | ${999} | ${'nine hundred'} `('$hundredsStart-$hundredsEnd', ({ hundredsStart, hundredsBase, - }) => { + }: { hundredsStart: number, hundredsBase: string }) => { describe(`${hundredsStart}-${hundredsStart + 9}`, () => { it.each` value | expected - ${hundredsStart + 0} | ${hundredsBase} + ${hundredsStart} | ${hundredsBase} ${hundredsStart + 1} | ${`${hundredsBase} one`} ${hundredsStart + 2} | ${`${hundredsBase} two`} ${hundredsStart + 3} | ${`${hundredsBase} three`} @@ -102,9 +102,9 @@ describe('numerica', () => { ${hundredsStart + 7} | ${`${hundredsBase} seven`} ${hundredsStart + 8} | ${`${hundredsBase} eight`} ${hundredsStart + 9} | ${`${hundredsBase} nine`} - `('converts $value to $expected', ({ value, expected }) => { - expect(stringify(value, options)).toBe(expected) - expect(parse(expected, { ...options, type: 'number' })).toBe(value) + `('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => { + expect(stringify(value, options)).toBe(expected); + expect(parse(expected, { ...options, type: 'number' })).toBe(value); }); }); @@ -121,9 +121,9 @@ describe('numerica', () => { ${hundredsStart + 17} | ${`${hundredsBase} seventeen`} ${hundredsStart + 18} | ${`${hundredsBase} eighteen`} ${hundredsStart + 19} | ${`${hundredsBase} nineteen`} - `('converts $value to $expected', ({ value, expected }) => { - expect(stringify(value, options)).toBe(expected) - expect(parse(expected, { ...options, type: 'number' })).toBe(value) + `('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => { + expect(stringify(value, options)).toBe(expected); + expect(parse(expected, { ...options, type: 'number' })).toBe(value); }); }); @@ -139,10 +139,10 @@ describe('numerica', () => { ${90} | ${99} | ${'ninety'} `('$start-$end', ({ start, base, - }) => { + }: { start: number, base: string }) => { it.each` value | expected - ${hundredsStart + start + 0} | ${`${hundredsBase} ${base}`} + ${hundredsStart + start} | ${`${hundredsBase} ${base}`} ${hundredsStart + start + 1} | ${`${hundredsBase} ${base} one`} ${hundredsStart + start + 2} | ${`${hundredsBase} ${base} two`} ${hundredsStart + start + 3} | ${`${hundredsBase} ${base} three`} @@ -152,7 +152,7 @@ describe('numerica', () => { ${hundredsStart + start + 7} | ${`${hundredsBase} ${base} seven`} ${hundredsStart + start + 8} | ${`${hundredsBase} ${base} eight`} ${hundredsStart + start + 9} | ${`${hundredsBase} ${base} nine`} - `('converts $value to $expected', ({ value, expected }) => { + `('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => { expect(stringify(value, options)).toBe(expected); expect(parse(expected, { ...options, type: 'number' })).toBe(value); }); @@ -191,7 +191,7 @@ describe('numerica', () => { ${1e+24} | ${'one septillion'} ${1e+27} | ${'one octillion'} ${1e+30} | ${'one nonillion'} - `('converts $value to $expected', ({ value, expected }) => { + `('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => { expect(stringify(value, options)).toBe(expected); expect(parse(expected, { ...options, type: 'number' })).toBe(value); }); @@ -208,7 +208,7 @@ describe('numerica', () => { ${'1e+54'} | ${'one septendecillion'} ${'1e+57'} | ${'one octodecillion'} ${'1e+60'} | ${'one novemdecillion'} - `('converts $value to $expected', ({ value, expected }) => { + `('converts $value to $expected', ({ value, expected }: { value: string, expected: string }) => { expect(stringify(value, options)).toBe(expected); expect(parse(expected, options)).toBe(value); }); @@ -225,7 +225,7 @@ describe('numerica', () => { ${'1e+84'} | ${'one septenvigintillion'} ${'1e+87'} | ${'one octovigintillion'} ${'1e+90'} | ${'one novemvigintillion'} - `('converts $value to $expected', ({ value, expected }) => { + `('converts $value to $expected', ({ value, expected }: { value: string, expected: string }) => { expect(stringify(value, options)).toBe(expected); expect(parse(expected, options)).toBe(value); }); @@ -239,13 +239,13 @@ describe('numerica', () => { ${'1e+213'} | ${'one septuagintillion'} ${'1e+243'} | ${'one octogintillion'} ${'1e+273'} | ${'one nonagintillion'} - `('converts $value to $expected', ({ value, expected }) => { + `('converts $value to $expected', ({ value, expected }: { value: string, expected: string }) => { expect(stringify(value, options)).toBe(expected); expect(parse(expected, options)).toBe(value); }); - it.only('converts values', () => { - const exp = numberToExponential('123,456,789,012,345,678,901,234,567,890'); + it('converts values', () => { + const exp1 = numberToExponential('123,456,789,012,345,678,901,234,567,890'); // // one hundred twenty three octillion // four hundred fifty six septillion @@ -257,6 +257,11 @@ describe('numerica', () => { // two hundred thirty four million // five hundred sixty seven thousand // eight hundred ninety - expect(parse(stringify('123456789012345678901234567890'))).toBe(exp); + expect(parse(stringify('123456789012345678901234567890'))).toBe(exp1); + + const value2 = '1000005000000'; + const exp2 = numberToExponential(value2); + expect(stringify(value2)).toBe('one trillion five million'); + expect(parse(stringify(value2))).toBe(exp2); }); }); diff --git a/packages/core/test/systems/en-US/chongo.test.ts b/packages/core/test/systems/en-US/chongo.test.ts index 9da2bd7..093ef8c 100644 --- a/packages/core/test/systems/en-US/chongo.test.ts +++ b/packages/core/test/systems/en-US/chongo.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { stringify, parse } from '../../../src'; import { numberToExponential } from '../../../src/exponent'; -describe.skip('Landon\'s original test cases', () => { +describe('Landon\'s original test cases', () => { describe('Basic conversions', () => { it.each` value | americanName