Browse Source

Refactor code

Make code more cleaner when parsing.
master
TheoryOfNekomata 1 year ago
parent
commit
fd0ea31fe4
7 changed files with 489 additions and 135 deletions
  1. +7
    -0
      README.md
  2. +4
    -4
      packages/core/package.json
  3. +73
    -0
      packages/core/src/common.ts
  4. +69
    -11
      packages/core/src/converter.ts
  5. +93
    -23
      packages/core/src/exponent.ts
  6. +237
    -97
      packages/core/src/systems/en-US.ts
  7. +6
    -0
      packages/core/test/systems/en-US.test.ts

+ 7
- 0
README.md View File

@@ -0,0 +1,7 @@
# numerica

Get the name of a number, even if it's stupidly big.

## References

* [How high can you count?](http://www.isthe.com/chongo/tech/math/number/howhigh.html)

+ 4
- 4
packages/core/package.json View File

@@ -11,10 +11,10 @@
"license": "UNLICENSED",
"keywords": [
"number",
"language",
"conversion",
"name",
"words"
"language",
"conversion",
"name",
"words"
],
"devDependencies": {
"@types/node": "^18.14.1",


+ 73
- 0
packages/core/src/common.ts View File

@@ -1,15 +1,88 @@
/**
* Group digits.
*/
type GroupDigits = string;

/**
* Group place.
*/
type GroupPlace = number;

/**
* Group of digits and its place.
*
* The place refers to the order which the digits are grouped, e.g. for a number like
*
* 1,234,567
*
* The groups would be:
*
* ['001', 2]
* ['234', 1]
* ['567', 0]
*
* Note that groups do not necessarily have the same length of digits, such in the case of
* South Asian numbering system:
*
* 1,00,00,000
*
* The groups would be:
*
* ['01', 3]
* ['00', 2]
* ['00', 1]
* ['000', 0]
*/
export type Group = [GroupDigits, GroupPlace];

/**
* System for stringifying and parsing numbers.
*/
export interface StringifySystem {
/**
* Creates a negative string.
* @param s - The string to make negative.
*/
makeNegative: (s: string) => string;
/**
* Creates a group string.
* @param group - The group digits.
* @param place - The group place.
* @param options - Options to use when creating the group.
*/
makeGroup: (group: string, place?: GroupPlace, options?: Record<string, unknown>) => string;
/**
* Groups a string.
* @param value - The string to group.
*/
group: (value: string) => Group[];
/**
* Finalizes a string.
* @param tokens - The tokens to finalize.
*/
finalize: (tokens: string[]) => string;
/**
* Tokenizes a string.
* @param value - The string to tokenize.
*/
tokenize: (value: string) => string[];
/**
* Parses groups from a string.
* @param value - The string to parse groups from.
*/
parseGroups: (value: string[]) => Group[];
/**
* Combines groups into a string.
* @param value - The groups to combine.
*/
combineGroups: (value: Group[]) => string;
}

/**
* Error thrown when an invalid token is encountered.
*/
export class InvalidTokenError extends Error {
constructor(token: string) {
super(`Invalid token: ${token}`);
}
}

+ 69
- 11
packages/core/src/converter.ts View File

@@ -1,30 +1,72 @@
import { enUS } from './systems';
import { StringifySystem } from './common';

type StringifyValue = string | number | bigint;
/**
* Negative symbol.
*/
const NEGATIVE_SYMBOL = '-' as const;

/**
* Allowed value type for {@link stringify}.
*/
type AllowedValue = string | number | bigint;

/**
* Array of allowed types for {@link parse}.
*/
const ALLOWED_PARSE_RESULT_TYPES = [
'string',
'number',
'bigint',
] as const;

/**
* Allowed type for {@link parse}.
*/
type ParseResult = typeof ALLOWED_PARSE_RESULT_TYPES[number];

/**
* Options to use when converting a value to a string.
*/
export interface StringifyOptions {
/**
* The system to use when converting a value to a string.
*
* Defaults to en-US (American short count).
*/
system?: StringifySystem;
/**
* Options to use when making a group. This is used to override the default options for a group.
*/
makeGroupOptions?: Record<string, unknown>;
}

/**
* 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.
*/
export const stringify = (
valueRaw: StringifyValue,
value: AllowedValue,
options = {} as StringifyOptions,
): string => {
if (!(['bigint', 'number', 'string'].includes(typeof (valueRaw as unknown)))) {
throw new TypeError('value must be a string, number, or bigint');
if (!(
(ALLOWED_PARSE_RESULT_TYPES as unknown as string[])
.includes(typeof (value as unknown))
)) {
throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof value}`);
}

const value = valueRaw.toString().replace(/\s/g, '');
const valueStr = value.toString().replace(/\s/g, '');
const { system = enUS, makeGroupOptions } = options;

if (value.startsWith('-')) {
return system.makeNegative(stringify(value.slice(1), options));
if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
}

const groups = system
.group(value)
.group(valueStr)
.map(([group, place]) => (
system.makeGroup(group, place, makeGroupOptions)
));
@@ -32,13 +74,28 @@ export const stringify = (
return system.finalize(groups);
};

type ParseType = 'string' | 'number' | 'bigint';

/**
* Options to use when parsing a name of a number to its numeric equivalent.
*/
export interface ParseOptions {
/**
* The system to use when parsing a name of a number to its numeric equivalent.
*
* Defaults to en-US (American short count).
*/
system?: StringifySystem;
type?: ParseType;
/**
* The type to parse the value as.
*/
type?: ParseResult;
}

/**
* 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.
*/
export const parse = (value: string, options = {} as ParseOptions) => {
const { system = enUS, type = 'string' } = options;

@@ -48,6 +105,7 @@ export const parse = (value: string, options = {} as ParseOptions) => {

switch (type) {
case 'number':
// Precision might be lost here. Use bigint when not using fractional parts.
return Number(stringValue);
case 'bigint':
return BigInt(stringValue);


+ 93
- 23
packages/core/src/exponent.ts View File

@@ -16,24 +16,92 @@ export interface NumberToExponentialOptions {
*/
groupingSymbol?: string;
/**
* Exponent character to use. Defaults to "e".
* The exponent character to use. Defaults to "e".
*/
exponentDelimiter?: string;
}

/**
* Default decimal point character.
*/
const DEFAULT_DECIMAL_POINT = '.' as const;

/**
* Default grouping symbol.
*/
const DEFAULT_GROUPING_SYMBOL = ',' as const;

/**
* Default exponent character.
*/
const DEFAULT_EXPONENT_DELIMITER = 'e' as const;

/**
* Default positive symbol.
*/
const DEFAULT_POSITIVE_SYMBOL = '+' as const;

/**
* Default negative symbol.
*/
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.
*/
const forceDecimalPoint = (value: string, decimalPoint: string) => (
value.includes(decimalPoint)
? value
: `${value}${decimalPoint}0`
);

/**
* Error thrown when a value is not in exponential notation.
*/
export class InvalidFormatError extends TypeError {
constructor(value: string) {
super(`Value must be in exponential notation. Received: ${value}`);
}
}

/**
* Error thrown when a value is not a string, number, or bigint.
*/
export class InvalidValueTypeError extends TypeError {
constructor(value: unknown) {
super(`Value must be a string, number, or bigint. Received: ${typeof value}`);
}
}

/**
* Forces a number to have a sign.
* @param value - The value to force a sign on.
* @returns The value with a sign.
*/
const forceNumberSign = (value: number | bigint) => {
const isExponentNegative = value < 0;
const exponentValueAbs = isExponentNegative ? -value : value;
const exponentSign = isExponentNegative ? DEFAULT_NEGATIVE_SYMBOL : DEFAULT_POSITIVE_SYMBOL;
return `${exponentSign}${exponentValueAbs}`;
};

/**
* 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.
*/
export const extractExponentialComponents = (
value: string,
options = {} as NumberToExponentialOptions,
) => {
const {
decimalPoint = '.',
groupingSymbol = ',',
exponentDelimiter = 'e',
decimalPoint = DEFAULT_DECIMAL_POINT,
groupingSymbol = DEFAULT_GROUPING_SYMBOL,
exponentDelimiter = DEFAULT_EXPONENT_DELIMITER,
} = options;
const valueWithoutGroupingSymbols = value.replace(new RegExp(`${groupingSymbol}`, 'g'), '');
const exponentDelimiterIndex = valueWithoutGroupingSymbols.indexOf(exponentDelimiter);
@@ -41,21 +109,20 @@ export const extractExponentialComponents = (
if (exponentDelimiterIndex < 0) {
// We force the value to have decimal point so that we can extract the integer and fractional
// components.
const stringValueWithDecimal = valueWithoutGroupingSymbols.includes(decimalPoint)
? valueWithoutGroupingSymbols
: `${valueWithoutGroupingSymbols}${decimalPoint}0`;
const stringValueWithDecimal = forceDecimalPoint(valueWithoutGroupingSymbols, decimalPoint);
const [integerRaw, fractionalRaw] = stringValueWithDecimal.split(decimalPoint);
const integer = integerRaw.replace(/^0+/g, '');
const fractional = fractionalRaw.replace(/0+$/g, '');
const exponentValue = BigInt(integer.length - 1);
return {
integer,
exponent: exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`,
fractional: fractionalRaw.replace(/0+$/g, ''),
exponent: forceNumberSign(exponentValue),
fractional,
};
}

if (exponentDelimiterIndex !== valueWithoutGroupingSymbols.lastIndexOf(exponentDelimiter)) {
throw new TypeError('Value must not contain more than one exponent character');
throw new InvalidFormatError(value);
}

const [base, exponentRaw] = valueWithoutGroupingSymbols.split(exponentDelimiter);
@@ -67,7 +134,7 @@ export const extractExponentialComponents = (
const exponentValue = BigInt(exponentRaw) + BigInt(extraIntegerDigits.length);
return {
integer,
exponent: exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`,
exponent: forceNumberSign(exponentValue),
fractional,
};
};
@@ -76,32 +143,35 @@ 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.
*/
export const numberToExponential = (
value: ValidValue,
options = {} as NumberToExponentialOptions,
): string => {
const stringValueRaw = value as unknown;
const valueRaw = value as unknown;

if (typeof stringValueRaw === 'bigint' || typeof stringValueRaw === 'number') {
return numberToExponential(stringValueRaw.toString(), options);
if (typeof valueRaw === 'bigint' || typeof valueRaw === 'number') {
return numberToExponential(valueRaw.toString(), options);
}

if (typeof stringValueRaw !== 'string') {
throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof stringValueRaw}`);
if (typeof valueRaw !== 'string') {
throw new InvalidValueTypeError(valueRaw);
}

if (stringValueRaw.startsWith('-')) {
return `-${numberToExponential(stringValueRaw.slice(1), options)}}`;
if (valueRaw.startsWith(DEFAULT_NEGATIVE_SYMBOL)) {
return `${DEFAULT_NEGATIVE_SYMBOL}${numberToExponential(valueRaw.slice(DEFAULT_NEGATIVE_SYMBOL.length), options)}}`;
}

const {
decimalPoint = '.',
groupingSymbol = ',',
exponentDelimiter = 'e',
decimalPoint = DEFAULT_DECIMAL_POINT,
groupingSymbol = DEFAULT_GROUPING_SYMBOL,
exponentDelimiter = DEFAULT_EXPONENT_DELIMITER,
} = options;

const stringValue = stringValueRaw
// Remove invalid characters.
// We also remove the grouping symbols in case they come from human input.
const stringValue = valueRaw
.replace(new RegExp(`${groupingSymbol}`, 'g'), '')
.toLowerCase()
.replace(/\s/g, '');
@@ -115,7 +185,7 @@ export const numberToExponential = (
const significantDigits = `${integer}${fractional}`;
if (significantDigits.length === 0) {
// We copy the behavior from `Number.prototype.toExponential` here.
return `0${exponentDelimiter}+0`;
return `0${exponentDelimiter}${DEFAULT_POSITIVE_SYMBOL}0`;
}

const significandInteger = significantDigits[0];


+ 237
- 97
packages/core/src/systems/en-US.ts View File

@@ -1,14 +1,29 @@
// noinspection SpellCheckingInspection

import { Group } from '../common';
import { Group, InvalidTokenError } from '../common';
import { numberToExponential } from '../exponent';

const DECIMAL_POINT = '.';
const DECIMAL_POINT = '.' as const;

const GROUPING_SYMBOL = ',';
const GROUPING_SYMBOL = ',' as const;

const NEGATIVE = 'negative';
const NEGATIVE = 'negative' as const;

const NEGATIVE_SYMBOL = '-' as const;

const POSITIVE_SYMBOL = '+' as const;

const SHORT_MILLIA_DELIMITER = '^' as const;

const EXPONENT_DELIMITER = 'e' as const;

const EMPTY_GROUP_DIGITS = '000' as const;

const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, 0];

/**
* Ones number names.
*/
const ONES = [
'zero',
'one',
@@ -24,6 +39,9 @@ const ONES = [

type OnesName = typeof ONES[number];

/**
* Ten plus ones number names.
*/
const TEN_PLUS_ONES = [
'ten',
'eleven',
@@ -39,6 +57,9 @@ const TEN_PLUS_ONES = [

type TenPlusOnesName = typeof TEN_PLUS_ONES[number];

/**
* Tens number names.
*/
const TENS = [
'zero',
TEN_PLUS_ONES[0],
@@ -54,14 +75,23 @@ const TENS = [

type TensName = typeof TENS[number];

/**
* Hundreds name.
*/
const HUNDRED = 'hundred' as const;

/**
* Thousands name.
*/
const THOUSAND = 'thousand' as const;

// const ILLION_ORDINAL_SUFFIX = 'illionth' as const;

// const THOUSAND_ORDINAL = 'thousandth' as const;

/**
* Special millions name.
*/
const MILLIONS_SPECIAL_PREFIXES = [
'',
'm',
@@ -75,8 +105,11 @@ const MILLIONS_SPECIAL_PREFIXES = [
'non',
] as const;

type MillionsSpecialPrefix = typeof MILLIONS_SPECIAL_PREFIXES[number];
type MillionsSpecialPrefix = Exclude<typeof MILLIONS_SPECIAL_PREFIXES[number], ''>;

/**
* Millions name.
*/
const MILLIONS_PREFIXES = [
'',
'un',
@@ -90,8 +123,11 @@ const MILLIONS_PREFIXES = [
'novem',
] as const;

type MillionsPrefix = typeof MILLIONS_PREFIXES[number];
type MillionsPrefix = Exclude<typeof MILLIONS_PREFIXES[number], ''>;

/**
* Decillions name.
*/
const DECILLIONS_PREFIXES = [
'',
'dec',
@@ -105,8 +141,11 @@ const DECILLIONS_PREFIXES = [
'nonagin',
] as const;

type DecillionsPrefix = typeof DECILLIONS_PREFIXES[number];
type DecillionsPrefix = Exclude<typeof DECILLIONS_PREFIXES[number], ''>;

/**
* Centillions name.
*/
const CENTILLIONS_PREFIXES = [
'',
'cen',
@@ -120,12 +159,24 @@ const CENTILLIONS_PREFIXES = [
'nongen',
] as const;

type CentillionsPrefix = typeof CENTILLIONS_PREFIXES[number];
type CentillionsPrefix = Exclude<typeof CENTILLIONS_PREFIXES[number], ''>;

/**
* Prefix for millia- number names.
*/
const MILLIA_PREFIX = 'millia' as const;

/**
* Suffix for -illion number names.
*/
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.
*/
const makeTensName = (tens: number, ones: number) => {
if (tens === 0) {
return ONES[ones];
@@ -142,6 +193,13 @@ const makeTensName = (tens: number, ones: number) => {
return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>} ${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
};

/**
* Builds a name for numbers in hundreds, tens, and ones.
* @param hundreds - Hundreds digit.
* @param tens - Tens digit.
* @param ones - Ones digit.
* @returns The name for the number.
*/
const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
if (hundreds === 0) {
return makeTensName(tens, ones);
@@ -154,24 +212,45 @@ const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones)}` as const;
};

/**
* Builds a name for numbers in the millions.
* @param millions - Millions digit.
* @param milliaCount - Number of millia- groups.
* @returns The millions prefix.
*/
const makeMillionsPrefix = (millions: number, milliaCount: number) => {
if (milliaCount > 0) {
return MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
return MILLIONS_PREFIXES[millions] as MillionsPrefix;
}

return MILLIONS_SPECIAL_PREFIXES[millions] as Exclude<MillionsSpecialPrefix, ''>;
return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix;
};

/**
* Builds a name for numbers in the decillions.
* @param decillions - Decillions digit.
* @param millions - Millions digit.
* @param milliaCount - Number of millia- groups.
* @returns The decillions prefix.
*/
const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => {
if (decillions === 0) {
return makeMillionsPrefix(millions, milliaCount);
}

const onesPrefix = MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
const tensName = DECILLIONS_PREFIXES[decillions] as Exclude<DecillionsPrefix, ''>;
const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
return `${onesPrefix}${tensName}` as const;
};

/**
* Builds a name for numbers in the centillions.
* @param centillions - Centillions digit.
* @param decillions - Decillions digit.
* @param millions - Millions digit.
* @param milliaCount - Number of millia- groups.
* @returns The centillions prefix.
*/
const makeCentillionsPrefix = (
centillions: number,
decillions: number,
@@ -182,9 +261,9 @@ const makeCentillionsPrefix = (
return makeDecillionsPrefix(decillions, millions, milliaCount);
}

const onesPrefix = MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
const tensName = DECILLIONS_PREFIXES[decillions] as Exclude<DecillionsPrefix, ''>;
const hundredsName = CENTILLIONS_PREFIXES[centillions] as Exclude<CentillionsPrefix, ''>;
const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix;
return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
};

@@ -220,21 +299,21 @@ const getGroupName = (place: number, shortenMillia: boolean) => {
},
[],
)
.map(([group, groupPlace]) => [group.padStart(3, '0'), groupPlace] as const)
.filter(([group]) => group !== '000')
.map(([group, groupPlace]) => {
const [hundreds, tens, ones] = group.split('').map(Number);
.map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const)
.filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
.map(([groupDigits, groupPlace]) => {
const [hundreds, tens, ones] = groupDigits.split('').map(Number);
if (groupPlace < 1) {
return makeCentillionsPrefix(hundreds, tens, ones, groupPlace);
}

const milliaSuffix = (
shortenMillia && groupPlace > 1
? `${MILLIA_PREFIX}^${groupPlace}`
? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}`
: MILLIA_PREFIX.repeat(groupPlace)
);

if (group === '001') {
if (groupDigits === '001') {
return milliaSuffix;
}

@@ -276,22 +355,21 @@ export const makeGroup = (
* @param value - The number string to group.
*/
export const group = (value: string): Group[] => {
const exponentDelimiter = 'e';
const [significand, exponentString] = numberToExponential(
value,
{
decimalPoint: DECIMAL_POINT,
groupingSymbol: GROUPING_SYMBOL,
exponentDelimiter,
exponentDelimiter: EXPONENT_DELIMITER,
},
)
.split(exponentDelimiter);
.split(EXPONENT_DELIMITER);
const exponent = Number(exponentString);
const significantDigits = significand.replace(DECIMAL_POINT, '');
return significantDigits.split('').reduce<Group[]>(
(acc, c, i) => {
const currentPlace = Math.floor((exponent - i) / 3);
const lastGroup = acc.at(-1) ?? ['000', currentPlace];
const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
const currentPlaceInGroup = 2 - ((exponent - i) % 3);
if (lastGroup[1] === currentPlace) {
const lastGroupDigits = lastGroup[0].split('');
@@ -330,95 +408,154 @@ export const tokenize = (stringValue: string) => (
stringValue.split(' ').filter((maybeToken) => maybeToken.length > 0)
);

const FINAL_TOKEN = '';
const FINAL_TOKEN = '' as const;

const getGroupFromGroupName = (groupName: string) => {
if (groupName === THOUSAND) {
return 1;
}

const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);
interface DoParseState {
groupNameCurrent: string;
millias: number[];
milliaIndex: number;
done: boolean;
}

if (specialMillions > -1) {
return 1 + specialMillions;
const doParseGroupName = (result: DoParseState): DoParseState => {
if (result.groupNameCurrent.length < 1) {
return {
...result,
done: true,
};
}

let groupNameCurrent = groupNameBase;
if (result.groupNameCurrent === 't') {
// If the current group name is "t", then we're done.
// We use the -t- affix to attach the group prefix to the -illion suffix, except for decillion.
return {
...result,
done: true,
};
}

const millias = [0];
let milliaIndex = 0;
const centillions = CENTILLIONS_PREFIXES.findIndex((p) => (
p.length > 0 && result.groupNameCurrent.startsWith(p)
));
if (centillions > -1) {
return {
milliaIndex: 0,
millias: result.millias.map((m, i) => (
i === 0
? m + (centillions * 100)
: m
)),
groupNameCurrent: result.groupNameCurrent.slice(
CENTILLIONS_PREFIXES[centillions].length,
),
done: false,
};
}

while (groupNameCurrent.length > 0) {
if (groupNameCurrent === 't') {
break;
}
const decillions = DECILLIONS_PREFIXES.findIndex((p) => (
p.length > 0 && result.groupNameCurrent.startsWith(p)
));
if (decillions > -1) {
return {
milliaIndex: 0,
millias: result.millias.map((m, i) => (
i === 0
? m + (decillions * 10)
: m
)),
groupNameCurrent: result.groupNameCurrent.slice(
DECILLIONS_PREFIXES[decillions].length,
),
done: false,
};
}

const centillions = CENTILLIONS_PREFIXES.findIndex((p) => (
p.length > 0 && groupNameCurrent.startsWith(p)
));
const millions = MILLIONS_PREFIXES.findIndex((p) => (
p.length > 0 && result.groupNameCurrent.startsWith(p)
));
if (millions > -1) {
return {
milliaIndex: 0,
millias: result.millias.map((m, i) => (
i === 0
? m + millions
: m
)),
groupNameCurrent: result.groupNameCurrent.slice(
MILLIONS_PREFIXES[millions].length,
),
done: false,
};
}

if (centillions > -1) {
milliaIndex = 0;
millias[milliaIndex] += (centillions * 100);
groupNameCurrent = groupNameCurrent.slice(CENTILLIONS_PREFIXES[centillions].length);
continue;
if (result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`)) {
// short millia
const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/);
if (!matchedMilliaArray) {
throw new InvalidTokenError(result.groupNameCurrent);
}
const matchedMillia = matchedMilliaArray[0];
const newMillia = Number(matchedMillia);
const oldMillia = result.milliaIndex;
const newMillias = [...result.millias];
newMillias[newMillia] = newMillias[oldMillia] || 1;
newMillias[oldMillia] = 0;
return {
milliaIndex: newMillia,
millias: newMillias,
groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
done: false,
};
}

const decillions = DECILLIONS_PREFIXES.findIndex((p) => (
p.length > 0 && groupNameCurrent.startsWith(p)
));
if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) {
const newMillia = result.milliaIndex + 1;
const oldMillia = result.milliaIndex;
const newMillias = [...result.millias];
newMillias[newMillia] = newMillias[oldMillia] || 1;
newMillias[oldMillia] = 0;
return {
milliaIndex: newMillia,
millias: newMillias,
groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
done: false,
};
}

if (decillions > -1) {
milliaIndex = 0;
millias[milliaIndex] += decillions * 10;
groupNameCurrent = groupNameCurrent.slice(DECILLIONS_PREFIXES[decillions].length);
continue;
}
throw new InvalidTokenError(result.groupNameCurrent);
};

const millions = MILLIONS_PREFIXES.findIndex((p) => (
p.length > 0 && groupNameCurrent.startsWith(p)
));
const getGroupPlaceFromGroupName = (groupName: string) => {
if (groupName === THOUSAND) {
return 1;
}

if (millions > -1) {
milliaIndex = 0;
millias[milliaIndex] += millions;
groupNameCurrent = groupNameCurrent.slice(MILLIONS_PREFIXES[millions].length);
continue;
}
const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);

if (groupNameCurrent.startsWith(`${MILLIA_PREFIX}^`)) {
// short millia
groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
const matchedMilliaArray = groupNameCurrent.match(/^\d+/);
if (!matchedMilliaArray) {
throw new Error(`Invalid groupName: ${groupName}`);
}
const matchedMillia = matchedMilliaArray[0];
millias[Number(matchedMillia)] = millias[milliaIndex] || 1;
millias[milliaIndex] = 0;
groupNameCurrent = groupNameCurrent.slice(matchedMillia.length);
}
if (specialMillions > -1) {
return specialMillions + 1;
}

if (groupNameCurrent.startsWith(MILLIA_PREFIX)) {
millias[milliaIndex + 1] = millias[milliaIndex] || 1;
millias[milliaIndex] = 0;
milliaIndex += 1;
groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
continue;
}
let result: DoParseState = {
groupNameCurrent: groupNameBase,
millias: [0],
milliaIndex: 0,
done: false,
};

break;
}
do {
result = doParseGroupName(result);
} while (!result.done);

const bigGroupPlace = Number(
millias
result.millias
.map((s) => s.toString().padStart(3, '0'))
.reverse()
.join(''),
);

return 1 + bigGroupPlace;
return bigGroupPlace + 1;
};

enum ParseGroupsMode {
@@ -440,7 +577,7 @@ interface ParserState {
export const parseGroups = (tokens: string[]) => {
const { groups } = [...tokens, FINAL_TOKEN].reduce<ParserState>(
(acc, token) => {
const lastGroup = acc.groups.at(-1) ?? ['000', 0];
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];

if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
if (acc.mode === ParseGroupsMode.ONES_MODE) {
@@ -448,7 +585,7 @@ export const parseGroups = (tokens: string[]) => {
lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
}

lastGroup[1] = getGroupFromGroupName(token);
lastGroup[1] = getGroupPlaceFromGroupName(token);

return {
...acc,
@@ -489,7 +626,7 @@ export const parseGroups = (tokens: string[]) => {
...acc,
lastToken: token,
mode: ParseGroupsMode.ONES_MODE,
groups: [...acc.groups, ['000', 0]],
groups: [...acc.groups, [...EMPTY_PLACE]],
};
}
return {
@@ -551,11 +688,14 @@ export const combineGroups = (groups: Group[]) => {
const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, '');
const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length;
const exponentValue = BigInt((firstGroupPlace * 3) + (2 - exponentExtra));
const exponent = exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`;
const isExponentNegative = exponentValue < 0;
const exponentValueAbs = isExponentNegative ? -exponentValue : exponentValue;
const exponentSign = isExponentNegative ? NEGATIVE_SYMBOL : POSITIVE_SYMBOL;
const exponent = `${exponentSign}${exponentValueAbs}`;
const significandInteger = digits.slice(0, 1);
const significandFraction = digits.slice(1);
if (significandFraction.length > 0) {
return `${significandInteger}${DECIMAL_POINT}${significandFraction}e${exponent}`;
return `${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
}
return `${significandInteger}e${exponent}`;
return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
};

+ 6
- 0
packages/core/test/systems/en-US.test.ts View File

@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import {parse, stringify, systems} from '../../src';
import { numberToExponential } from '../../src/exponent';

const options = { system: systems.enUS };

@@ -242,4 +243,9 @@ describe('numerica', () => {
expect(stringify(value, options)).toBe(expected);
expect(parse(expected, options)).toBe(value);
});

it('converts values', () => {
const exp = numberToExponential('123456789012345678901234567890');
expect(parse(stringify('123456789012345678901234567890'))).toBe(exp);
});
});

Loading…
Cancel
Save