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