Use bigint for place values to be more adaptable with longer inputs.master
@@ -6,7 +6,7 @@ type GroupDigits = string; | |||||
/** | /** | ||||
* Group place. | * Group place. | ||||
*/ | */ | ||||
type GroupPlace = number; | |||||
export type GroupPlace = bigint; | |||||
/** | /** | ||||
* Group of digits and its place. | * Group of digits and its place. | ||||
@@ -81,7 +81,7 @@ export class InvalidValueTypeError extends TypeError { | |||||
* @param value - The value to force a sign on. | * @param value - The value to force a sign on. | ||||
* @returns string The value with a sign. | * @returns string The value with a sign. | ||||
*/ | */ | ||||
const forceNumberSign = (value: number | bigint) => { | |||||
const forceNumberSign = (value: bigint) => { | |||||
const isExponentNegative = value < 0; | const isExponentNegative = value < 0; | ||||
const exponentValueAbs = isExponentNegative ? -value : value; | const exponentValueAbs = isExponentNegative ? -value : value; | ||||
const exponentSign = isExponentNegative ? DEFAULT_NEGATIVE_SYMBOL : DEFAULT_POSITIVE_SYMBOL; | const exponentSign = isExponentNegative ? DEFAULT_NEGATIVE_SYMBOL : DEFAULT_POSITIVE_SYMBOL; | ||||
@@ -19,7 +19,7 @@ export const EXPONENT_DELIMITER = 'e' as const; | |||||
export const EMPTY_GROUP_DIGITS = '000' as const; | export const EMPTY_GROUP_DIGITS = '000' as const; | ||||
export const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, 0]; | |||||
export const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, BigInt(0)]; | |||||
/** | /** | ||||
* Ones number names. | * Ones number names. | ||||
@@ -155,14 +155,14 @@ const doParseGroupName = (result: DoParseState): DoParseState => { | |||||
const getGroupPlaceFromGroupName = (groupName: string) => { | const getGroupPlaceFromGroupName = (groupName: string) => { | ||||
if (groupName === THOUSAND) { | if (groupName === THOUSAND) { | ||||
return 1; | |||||
return BigInt(1); | |||||
} | } | ||||
const groupNameBase = groupName.replace(ILLION_SUFFIX, ''); | const groupNameBase = groupName.replace(ILLION_SUFFIX, ''); | ||||
const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p); | const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p); | ||||
if (specialMillions > -1) { | if (specialMillions > -1) { | ||||
return specialMillions + 1; | |||||
return BigInt(specialMillions + 1); | |||||
} | } | ||||
let result: DoParseState = { | let result: DoParseState = { | ||||
@@ -176,14 +176,14 @@ const getGroupPlaceFromGroupName = (groupName: string) => { | |||||
result = doParseGroupName(result); | result = doParseGroupName(result); | ||||
} while (!result.done); | } while (!result.done); | ||||
const bigGroupPlace = Number( | |||||
const bigGroupPlace = BigInt( | |||||
result.millias | result.millias | ||||
.map((s) => s.toString().padStart(3, '0')) | .map((s) => s.toString().padStart(3, '0')) | ||||
.reverse() | .reverse() | ||||
.join(''), | .join(''), | ||||
); | ); | ||||
return bigGroupPlace + 1; | |||||
return bigGroupPlace + BigInt(1); | |||||
}; | }; | ||||
enum ParseGroupsMode { | enum ParseGroupsMode { | ||||
@@ -250,7 +250,7 @@ const parseFinal = (acc: ParserState): ParserState => { | |||||
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}`; | lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}`; | ||||
} | } | ||||
// We assume last token without parsed place will always be the smallest | // We assume last token without parsed place will always be the smallest | ||||
lastGroup[GROUP_PLACE_INDEX] = 0; | |||||
lastGroup[GROUP_PLACE_INDEX] = BigInt(0); | |||||
return { | return { | ||||
...acc, | ...acc, | ||||
groups: [...acc.groups.slice(0, -1), lastGroup], | groups: [...acc.groups.slice(0, -1), lastGroup], | ||||
@@ -263,7 +263,7 @@ const parseFinal = (acc: ParserState): ParserState => { | |||||
if (tens > -1) { | if (tens > -1) { | ||||
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`; | lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`; | ||||
} | } | ||||
lastGroup[GROUP_PLACE_INDEX] = 0; | |||||
lastGroup[GROUP_PLACE_INDEX] = BigInt(0); | |||||
return { | return { | ||||
...acc, | ...acc, | ||||
groups: [...acc.groups.slice(0, -1), lastGroup], | groups: [...acc.groups.slice(0, -1), lastGroup], | ||||
@@ -299,7 +299,7 @@ const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => { | |||||
...acc, | ...acc, | ||||
lastToken: token, | lastToken: token, | ||||
mode: ParseGroupsMode.TEN_PLUS_ONES_MODE, | mode: ParseGroupsMode.TEN_PLUS_ONES_MODE, | ||||
groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[GROUP_PLACE_INDEX] - 1]], | |||||
groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]], | |||||
}; | }; | ||||
} | } | ||||
@@ -320,7 +320,7 @@ const parseTens = (acc: ParserState, token: string): ParserState => { | |||||
...acc, | ...acc, | ||||
lastToken: token, | lastToken: token, | ||||
mode: ParseGroupsMode.TENS_MODE, | mode: ParseGroupsMode.TENS_MODE, | ||||
groups: [...acc.groups, [`0${tens}0`, lastGroup[GROUP_PLACE_INDEX] - 1]], | |||||
groups: [...acc.groups, [`0${tens}0`, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]], | |||||
}; | }; | ||||
} | } | ||||
@@ -379,14 +379,37 @@ export const parseGroups = (tokens: string[]) => { | |||||
return groups; | return groups; | ||||
}; | }; | ||||
const bigIntMax = (...b: bigint[]) => b.reduce( | |||||
(previousMax, current) => { | |||||
if (typeof previousMax === 'undefined') { | |||||
return current; | |||||
} | |||||
return previousMax > current ? previousMax : current; | |||||
}, | |||||
undefined as bigint | undefined, | |||||
); | |||||
const bigIntMin = (...b: bigint[]) => b.reduce( | |||||
(previousMin, current) => { | |||||
if (typeof previousMin === 'undefined') { | |||||
return current; | |||||
} | |||||
return previousMin < current ? previousMin : current; | |||||
}, | |||||
undefined as bigint | undefined, | |||||
); | |||||
export const combineGroups = (groups: Group[]) => { | export const combineGroups = (groups: Group[]) => { | ||||
const places = groups.map((g) => g[GROUP_PLACE_INDEX]); | const places = groups.map((g) => g[GROUP_PLACE_INDEX]); | ||||
const maxPlace = Math.max(...places); | |||||
const minPlace = Math.min(...places); | |||||
if (places.length < 1) { | |||||
return ''; | |||||
} | |||||
const maxPlace = bigIntMax(...places) as bigint; | |||||
const minPlace = bigIntMin(...places) as bigint; | |||||
const firstGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === maxPlace) ?? [...EMPTY_PLACE]; | const firstGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === maxPlace) ?? [...EMPTY_PLACE]; | ||||
const firstGroupPlace = firstGroup[GROUP_PLACE_INDEX]; | const firstGroupPlace = firstGroup[GROUP_PLACE_INDEX]; | ||||
const groupsSorted = []; | const groupsSorted = []; | ||||
for (let i = maxPlace; i >= minPlace; i -= 1) { | |||||
for (let i = maxPlace; i >= minPlace; i = BigInt(i) - BigInt(1)) { | |||||
const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [EMPTY_GROUP_DIGITS, i]; | const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [EMPTY_GROUP_DIGITS, i]; | ||||
groupsSorted.push(thisGroup); | groupsSorted.push(thisGroup); | ||||
} | } | ||||
@@ -401,7 +424,9 @@ export const combineGroups = (groups: Group[]) => { | |||||
const firstGroupDigits = firstGroup[0]; | const firstGroupDigits = firstGroup[0]; | ||||
const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, ''); | const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, ''); | ||||
const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length; | const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length; | ||||
const exponentValue = BigInt((firstGroupPlace * 3) + (2 - exponentExtra)); | |||||
const exponentValue = BigInt( | |||||
(BigInt(firstGroupPlace) * BigInt(3)) + (BigInt(2) - BigInt(exponentExtra)), | |||||
); | |||||
const isExponentNegative = exponentValue < 0; | const isExponentNegative = exponentValue < 0; | ||||
const exponentValueAbs = isExponentNegative ? -exponentValue : exponentValue; | const exponentValueAbs = isExponentNegative ? -exponentValue : exponentValue; | ||||
const exponentSign = isExponentNegative ? NEGATIVE_SYMBOL : POSITIVE_SYMBOL; | const exponentSign = isExponentNegative ? NEGATIVE_SYMBOL : POSITIVE_SYMBOL; | ||||
@@ -1,4 +1,9 @@ | |||||
import { Group, GROUP_DIGITS_INDEX, GROUP_PLACE_INDEX } from '../../common'; | |||||
import { | |||||
Group, | |||||
GROUP_DIGITS_INDEX, | |||||
GROUP_PLACE_INDEX, | |||||
GroupPlace, | |||||
} from '../../common'; | |||||
import { numberToExponential } from '../../exponent'; | import { numberToExponential } from '../../exponent'; | ||||
import { | import { | ||||
CENTILLIONS_PREFIXES, | CENTILLIONS_PREFIXES, | ||||
@@ -74,7 +79,7 @@ const makeHundredsName = (hundreds: number, tens: number, ones: number) => { | |||||
* @param milliaCount - Number of millia- groups. | * @param milliaCount - Number of millia- groups. | ||||
* @returns string The millions prefix. | * @returns string The millions prefix. | ||||
*/ | */ | ||||
const makeMillionsPrefix = (millions: number, milliaCount: number) => { | |||||
const makeMillionsPrefix = (millions: number, milliaCount: GroupPlace) => { | |||||
if (milliaCount > 0) { | if (milliaCount > 0) { | ||||
return MILLIONS_PREFIXES[millions] as MillionsPrefix; | return MILLIONS_PREFIXES[millions] as MillionsPrefix; | ||||
} | } | ||||
@@ -89,7 +94,7 @@ const makeMillionsPrefix = (millions: number, milliaCount: number) => { | |||||
* @param milliaCount - Number of millia- groups. | * @param milliaCount - Number of millia- groups. | ||||
* @returns string The decillions prefix. | * @returns string The decillions prefix. | ||||
*/ | */ | ||||
const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => { | |||||
const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: GroupPlace) => { | |||||
if (decillions === 0) { | if (decillions === 0) { | ||||
return makeMillionsPrefix(millions, milliaCount); | return makeMillionsPrefix(millions, milliaCount); | ||||
} | } | ||||
@@ -111,7 +116,7 @@ const makeCentillionsPrefix = ( | |||||
centillions: number, | centillions: number, | ||||
decillions: number, | decillions: number, | ||||
millions: number, | millions: number, | ||||
milliaCount: number, | |||||
milliaCount: GroupPlace, | |||||
) => { | ) => { | ||||
if (centillions === 0) { | if (centillions === 0) { | ||||
return makeDecillionsPrefix(decillions, millions, milliaCount); | return makeDecillionsPrefix(decillions, millions, milliaCount); | ||||
@@ -123,23 +128,31 @@ const makeCentillionsPrefix = ( | |||||
return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const; | return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const; | ||||
}; | }; | ||||
const getGroupName = (place: number, shortenMillia: boolean) => { | |||||
if (place === 0) { | |||||
const repeatString = (s: string, count: GroupPlace) => { | |||||
let result = ''; | |||||
for (let i = BigInt(0); i < count; i += BigInt(1)) { | |||||
result += s; | |||||
} | |||||
return result; | |||||
}; | |||||
const getGroupName = (place: GroupPlace, shortenMillia: boolean) => { | |||||
if (place === BigInt(0)) { | |||||
return '' as const; | return '' as const; | ||||
} | } | ||||
if (place === 1) { | |||||
if (place === BigInt(1)) { | |||||
return THOUSAND; | return THOUSAND; | ||||
} | } | ||||
const bigGroupPlace = place - 1; | |||||
const bigGroupPlace = place - BigInt(1); | |||||
const groupGroups = bigGroupPlace | const groupGroups = bigGroupPlace | ||||
.toString() | .toString() | ||||
.split('') | .split('') | ||||
.reduceRight<Group[]>( | .reduceRight<Group[]>( | ||||
(acc, c, i, cc) => { | (acc, c, i, cc) => { | ||||
const firstGroup = acc.at(0); | const firstGroup = acc.at(0); | ||||
const currentPlace = Math.floor((cc.length - i - 1) / 3); | |||||
const currentPlace = BigInt(Math.floor((cc.length - i - 1) / 3)); | |||||
const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group; | const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group; | ||||
if (typeof firstGroup === 'undefined') { | if (typeof firstGroup === 'undefined') { | ||||
newGroup[GROUP_DIGITS_INDEX] = c; | newGroup[GROUP_DIGITS_INDEX] = c; | ||||
@@ -170,7 +183,7 @@ const getGroupName = (place: number, shortenMillia: boolean) => { | |||||
const milliaSuffix = ( | const milliaSuffix = ( | ||||
shortenMillia && groupPlace > 1 | shortenMillia && groupPlace > 1 | ||||
? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}` | ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}` | ||||
: MILLIA_PREFIX.repeat(groupPlace) | |||||
: repeatString(MILLIA_PREFIX, groupPlace) | |||||
); | ); | ||||
if (groupDigits === '001') { | if (groupDigits === '001') { | ||||
@@ -194,7 +207,7 @@ const getGroupName = (place: number, shortenMillia: boolean) => { | |||||
export const makeGroups = (groups: Group[], options?: Record<string, unknown>): string[] => { | export const makeGroups = (groups: Group[], options?: Record<string, unknown>): string[] => { | ||||
const filteredGroups = groups.filter(([digits, place]) => ( | const filteredGroups = groups.filter(([digits, place]) => ( | ||||
place === 0 || digits !== EMPTY_GROUP_DIGITS | |||||
place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS | |||||
)); | )); | ||||
return filteredGroups.map( | return filteredGroups.map( | ||||
@@ -232,7 +245,7 @@ export const group = (value: string): Group[] => { | |||||
const significantDigits = significand.replace(DECIMAL_POINT, ''); | const significantDigits = significand.replace(DECIMAL_POINT, ''); | ||||
return significantDigits.split('').reduce<Group[]>( | return significantDigits.split('').reduce<Group[]>( | ||||
(acc, c, i) => { | (acc, c, i) => { | ||||
const currentPlace = Math.floor((exponent - i) / 3); | |||||
const currentPlace = BigInt(Math.floor((exponent - i) / 3)); | |||||
const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace]; | const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace]; | ||||
const currentPlaceInGroup = 2 - ((exponent - i) % 3); | const currentPlaceInGroup = 2 - ((exponent - i) % 3); | ||||
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) { | if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) { | ||||