Browse Source

Split system functions

Put functions with different purposes into their own files.
master
TheoryOfNekomata 1 year ago
parent
commit
83f0f28012
4 changed files with 469 additions and 417 deletions
  1. +172
    -0
      packages/core/src/systems/en-US/common.ts
  2. +2
    -0
      packages/core/src/systems/en-US/index.ts
  3. +26
    -417
      packages/core/src/systems/en-US/parse.ts
  4. +269
    -0
      packages/core/src/systems/en-US/stringify.ts

+ 172
- 0
packages/core/src/systems/en-US/common.ts View File

@@ -0,0 +1,172 @@
import { Group } from '../../common';

export const DECIMAL_POINT = '.' as const;

export const GROUPING_SYMBOL = ',' as const;

export const NEGATIVE = 'negative' as const;

export const NEGATIVE_SYMBOL = '-' as const;

// replace with hyphen with option
export const TENS_ONES_SEPARATOR = ' ' as const;

export const POSITIVE_SYMBOL = '+' as const;

export const SHORT_MILLIA_DELIMITER = '^' as const;

export const EXPONENT_DELIMITER = 'e' as const;

export const EMPTY_GROUP_DIGITS = '000' as const;

export const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, 0];

/**
* Ones number names.
*/
export const ONES = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
] as const;

export type OnesName = typeof ONES[number];

/**
* Ten plus ones number names.
*/
export const TEN_PLUS_ONES = [
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen',
] as const;

export type TenPlusOnesName = typeof TEN_PLUS_ONES[number];

/**
* Tens number names.
*/
export const TENS = [
'zero',
TEN_PLUS_ONES[0],
'twenty',
'thirty',
'forty',
'fifty',
'sixty',
'seventy',
'eighty',
'ninety',
] as const;

export type TensName = typeof TENS[number];

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

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

// export const ILLION_ORDINAL_SUFFIX = 'illionth' as const;

// export const THOUSAND_ORDINAL = 'thousandth' as const;

/**
* Special millions name.
*/
export const MILLIONS_SPECIAL_PREFIXES = [
'',
'm',
'b',
'tr',
'quadr',
'quint',
'sext',
'sept',
'oct',
'non',
] as const;

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

/**
* Millions name.
*/
export const MILLIONS_PREFIXES = [
'',
'un',
'duo',
'tre',
'quattuor',
'quin',
'sex',
'septen',
'octo',
'novem',
] as const;

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

/**
* Decillions name.
*/
export const DECILLIONS_PREFIXES = [
'',
'dec',
'vigin',
'trigin',
'quadragin',
'quinquagin',
'sexagin',
'septuagin',
'octogin',
'nonagin',
] as const;

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

/**
* Centillions name.
*/
export const CENTILLIONS_PREFIXES = [
'',
'cen',
'duocen',
'trecen',
'quadringen',
'quingen',
'sescen',
'septingen',
'octingen',
'nongen',
] as const;

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

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

/**
* Suffix for -illion number names.
*/
export const ILLION_SUFFIX = 'illion' as const;

+ 2
- 0
packages/core/src/systems/en-US/index.ts View File

@@ -0,0 +1,2 @@
export * from './parse';
export * from './stringify';

packages/core/src/systems/en-US.ts → packages/core/src/systems/en-US/parse.ts View File

@@ -1,424 +1,35 @@
// noinspection SpellCheckingInspection

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

const DECIMAL_POINT = '.' as const;

const GROUPING_SYMBOL = ',' as const;

const NEGATIVE = 'negative' as const;

const NEGATIVE_SYMBOL = '-' as const;

// replace with hyphen with option
const TENS_ONES_SEPARATOR = ' ' 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',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
] as const;

type OnesName = typeof ONES[number];

/**
* Ten plus ones number names.
*/
const TEN_PLUS_ONES = [
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen',
] as const;

type TenPlusOnesName = typeof TEN_PLUS_ONES[number];

/**
* Tens number names.
*/
const TENS = [
'zero',
TEN_PLUS_ONES[0],
'twenty',
'thirty',
'forty',
'fifty',
'sixty',
'seventy',
'eighty',
'ninety',
] as const;

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',
'b',
'tr',
'quadr',
'quint',
'sext',
'sept',
'oct',
'non',
] as const;

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

/**
* Millions name.
*/
const MILLIONS_PREFIXES = [
'',
'un',
'duo',
'tre',
'quattuor',
'quin',
'sex',
'septen',
'octo',
'novem',
] as const;

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

/**
* Decillions name.
*/
const DECILLIONS_PREFIXES = [
'',
'dec',
'vigin',
'trigin',
'quadragin',
'quinquagin',
'sexagin',
'septuagin',
'octogin',
'nonagin',
] as const;

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

/**
* Centillions name.
*/
const CENTILLIONS_PREFIXES = [
'',
'cen',
'duocen',
'trecen',
'quadringen',
'quingen',
'sescen',
'septingen',
'octingen',
'nongen',
] as const;

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 string The name for the number.
*/
const makeTensName = (tens: number, ones: number) => {
if (tens === 0) {
return ONES[ones];
}

if (tens === 1) {
return TEN_PLUS_ONES[ones] as unknown as TenPlusOnesName;
}

if (ones === 0) {
return TENS[tens];
}

return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${TENS_ONES_SEPARATOR}${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 string The name for the number.
*/
const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
if (hundreds === 0) {
return makeTensName(tens, ones);
}

if (tens === 0 && ones === 0) {
return `${ONES[hundreds]} ${HUNDRED}` as const;
}

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 string The millions prefix.
*/
const makeMillionsPrefix = (millions: number, milliaCount: number) => {
if (milliaCount > 0) {
return MILLIONS_PREFIXES[millions] as MillionsPrefix;
}

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 string The decillions prefix.
*/
const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => {
if (decillions === 0) {
return makeMillionsPrefix(millions, milliaCount);
}

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 string The centillions prefix.
*/
const makeCentillionsPrefix = (
centillions: number,
decillions: number,
millions: number,
milliaCount: number,
) => {
if (centillions === 0) {
return makeDecillionsPrefix(decillions, millions, milliaCount);
}

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

const getGroupName = (place: number, shortenMillia: boolean) => {
if (place === 0) {
return '' as const;
}

if (place === 1) {
return THOUSAND;
}

const bigGroupPlace = place - 1;
const groupGroups = bigGroupPlace
.toString()
.split('')
.reduceRight<Group[]>(
(acc, c, i, cc) => {
const firstGroup = acc.at(0);
const currentPlace = Math.floor((cc.length - i - 1) / 3);
const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
if (typeof firstGroup === 'undefined') {
newGroup[GROUP_DIGITS_INDEX] = c;
return [newGroup];
}

if (firstGroup[0].length > 2) {
newGroup[GROUP_DIGITS_INDEX] = c;
return [newGroup, ...acc];
}

newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
return [
newGroup,
...acc.slice(1),
];
},
[],
)
.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}${SHORT_MILLIA_DELIMITER}${groupPlace}`
: MILLIA_PREFIX.repeat(groupPlace)
);

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

return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix;
})
.join('');

if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
return `${groupGroups}${ILLION_SUFFIX}` as const;
}

if (bigGroupPlace > 10) {
return `${groupGroups}t${ILLION_SUFFIX}` as const;
}

return `${groupGroups}${ILLION_SUFFIX}` as const;
};

export const makeGroups = (groups: Group[], options?: Record<string, unknown>): string[] => {
const filteredGroups = groups.filter(([digits, place]) => (
place === 0 || digits !== EMPTY_GROUP_DIGITS
));

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

/**
* Group a number string into groups of three digits, starting from the decimal point.
* @param value - The number string to group.
*/
export const group = (value: string): Group[] => {
const [significand, exponentString] = numberToExponential(
value,
{
decimalPoint: DECIMAL_POINT,
groupingSymbol: GROUPING_SYMBOL,
exponentDelimiter: EXPONENT_DELIMITER,
},
)
.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) ?? [EMPTY_GROUP_DIGITS, currentPlace];
const currentPlaceInGroup = 2 - ((exponent - i) % 3);
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
const lastGroupDigits = lastGroup[0].split('');
lastGroupDigits[currentPlaceInGroup] = c;
return [...acc.slice(0, -1) ?? [], [
lastGroupDigits.join(''),
currentPlace,
]];
}
return [...acc, [c.padEnd(3, '0'), currentPlace]];
},
[],
);
};

/**
* Formats the final tokenized string.
* @param tokens - The tokens to finalize.
*/
export const finalize = (tokens: string[]) => (
tokens
.map((t) => t.trim())
.join(' ')
.trim()
);
} from '../../common';
import {
CENTILLIONS_PREFIXES,
DECILLIONS_PREFIXES,
DECIMAL_POINT,
EMPTY_GROUP_DIGITS,
EMPTY_PLACE,
EXPONENT_DELIMITER,
HUNDRED,
ILLION_SUFFIX,
MILLIA_PREFIX,
MILLIONS_PREFIXES,
MILLIONS_SPECIAL_PREFIXES,
NEGATIVE_SYMBOL,
ONES,
OnesName,
POSITIVE_SYMBOL,
SHORT_MILLIA_DELIMITER,
TEN_PLUS_ONES,
TenPlusOnesName,
TENS,
TENS_ONES_SEPARATOR,
TensName,
THOUSAND,
} from './common';

/**
* Makes a negative string.
* @param s - The string to make negative.
*/
export const makeNegative = (s: string) => (
`${NEGATIVE} ${s}`
);
const FINAL_TOKEN = '' as const;

export const tokenize = (stringValue: string) => (
stringValue
@@ -427,8 +38,6 @@ export const tokenize = (stringValue: string) => (
.filter((maybeToken) => maybeToken.length > 0)
);

const FINAL_TOKEN = '' as const;

interface DoParseState {
groupNameCurrent: string;
millias: number[];

+ 269
- 0
packages/core/src/systems/en-US/stringify.ts View File

@@ -0,0 +1,269 @@
import { Group, GROUP_DIGITS_INDEX, GROUP_PLACE_INDEX } from '../../common';
import { numberToExponential } from '../../exponent';
import {
CENTILLIONS_PREFIXES,
CentillionsPrefix,
DECILLIONS_PREFIXES,
DecillionsPrefix,
DECIMAL_POINT,
EMPTY_GROUP_DIGITS,
EXPONENT_DELIMITER,
GROUPING_SYMBOL,
HUNDRED,
ILLION_SUFFIX,
MILLIA_PREFIX,
MILLIONS_PREFIXES,
MILLIONS_SPECIAL_PREFIXES,
MillionsPrefix,
MillionsSpecialPrefix, NEGATIVE,
ONES,
OnesName,
SHORT_MILLIA_DELIMITER,
TEN_PLUS_ONES,
TenPlusOnesName,
TENS,
TENS_ONES_SEPARATOR,
TensName,
THOUSAND,
} from './common';

/**
* Builds a name for numbers in tens and ones.
* @param tens - Tens digit.
* @param ones - Ones digit.
* @returns string The name for the number.
*/
const makeTensName = (tens: number, ones: number) => {
if (tens === 0) {
return ONES[ones];
}

if (tens === 1) {
return TEN_PLUS_ONES[ones] as unknown as TenPlusOnesName;
}

if (ones === 0) {
return TENS[tens];
}

return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${TENS_ONES_SEPARATOR}${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 string The name for the number.
*/
const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
if (hundreds === 0) {
return makeTensName(tens, ones);
}

if (tens === 0 && ones === 0) {
return `${ONES[hundreds]} ${HUNDRED}` as const;
}

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 string The millions prefix.
*/
const makeMillionsPrefix = (millions: number, milliaCount: number) => {
if (milliaCount > 0) {
return MILLIONS_PREFIXES[millions] as MillionsPrefix;
}

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 string The decillions prefix.
*/
const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => {
if (decillions === 0) {
return makeMillionsPrefix(millions, milliaCount);
}

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 string The centillions prefix.
*/
const makeCentillionsPrefix = (
centillions: number,
decillions: number,
millions: number,
milliaCount: number,
) => {
if (centillions === 0) {
return makeDecillionsPrefix(decillions, millions, milliaCount);
}

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

const getGroupName = (place: number, shortenMillia: boolean) => {
if (place === 0) {
return '' as const;
}

if (place === 1) {
return THOUSAND;
}

const bigGroupPlace = place - 1;
const groupGroups = bigGroupPlace
.toString()
.split('')
.reduceRight<Group[]>(
(acc, c, i, cc) => {
const firstGroup = acc.at(0);
const currentPlace = Math.floor((cc.length - i - 1) / 3);
const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
if (typeof firstGroup === 'undefined') {
newGroup[GROUP_DIGITS_INDEX] = c;
return [newGroup];
}

if (firstGroup[0].length > 2) {
newGroup[GROUP_DIGITS_INDEX] = c;
return [newGroup, ...acc];
}

newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
return [
newGroup,
...acc.slice(1),
];
},
[],
)
.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}${SHORT_MILLIA_DELIMITER}${groupPlace}`
: MILLIA_PREFIX.repeat(groupPlace)
);

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

return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix;
})
.join('');

if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
return `${groupGroups}${ILLION_SUFFIX}` as const;
}

if (bigGroupPlace > 10) {
return `${groupGroups}t${ILLION_SUFFIX}` as const;
}

return `${groupGroups}${ILLION_SUFFIX}` as const;
};

export const makeGroups = (groups: Group[], options?: Record<string, unknown>): string[] => {
const filteredGroups = groups.filter(([digits, place]) => (
place === 0 || digits !== EMPTY_GROUP_DIGITS
));

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

/**
* Group a number string into groups of three digits, starting from the decimal point.
* @param value - The number string to group.
*/
export const group = (value: string): Group[] => {
const [significand, exponentString] = numberToExponential(
value,
{
decimalPoint: DECIMAL_POINT,
groupingSymbol: GROUPING_SYMBOL,
exponentDelimiter: EXPONENT_DELIMITER,
},
)
.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) ?? [EMPTY_GROUP_DIGITS, currentPlace];
const currentPlaceInGroup = 2 - ((exponent - i) % 3);
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
const lastGroupDigits = lastGroup[0].split('');
lastGroupDigits[currentPlaceInGroup] = c;
return [...acc.slice(0, -1) ?? [], [
lastGroupDigits.join(''),
currentPlace,
]];
}
return [...acc, [c.padEnd(3, '0'), currentPlace]];
},
[],
);
};

/**
* Formats the final tokenized string.
* @param tokens - The tokens to finalize.
*/
export const finalize = (tokens: string[]) => (
tokens
.map((t) => t.trim())
.join(' ')
.trim()
);

/**
* Makes a negative string.
* @param s - The string to make negative.
*/
export const makeNegative = (s: string) => (
`${NEGATIVE} ${s}`
);

Loading…
Cancel
Save