Browse Source

Fix stringify bugs

Address errors with gapped groups.
master
TheoryOfNekomata 9 months ago
parent
commit
0071c9d493
7 changed files with 85 additions and 65 deletions
  1. +2
    -1
      packages/core/.eslintrc
  2. +1
    -1
      packages/core/src/common.ts
  3. +5
    -9
      packages/core/src/converter.ts
  4. +11
    -5
      packages/core/src/exponent.ts
  5. +34
    -22
      packages/core/src/systems/en-US.ts
  6. +31
    -26
      packages/core/test/systems/en-US.test.ts
  7. +1
    -1
      packages/core/test/systems/en-US/chongo.test.ts

+ 2
- 1
packages/core/.eslintrc View File

@@ -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"


+ 1
- 1
packages/core/src/common.ts View File

@@ -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, unknown>) => string;
makeGroups: (groups: Group[], options?: Record<string, unknown>) => string[];
/**
* Groups a string.
* @param value - The string to group.


+ 5
- 9
packages/core/src/converter.ts View File

@@ -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;


+ 11
- 5
packages/core/src/exponent.ts View File

@@ -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,


+ 34
- 22
packages/core/src/systems/en-US.ts View File

@@ -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, unknown>,
): string => {
const makeHundredsArgs = group
.padStart(3, '0')
.split('')
.map((s) => Number(s)) as [number, number, number];
export const makeGroups = (groups: Group[], options?: Record<string, unknown>): 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;


+ 31
- 26
packages/core/test/systems/en-US.test.ts View File

@@ -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);
});
});

+ 1
- 1
packages/core/test/systems/en-US/chongo.test.ts View File

@@ -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


Loading…
Cancel
Save