|
|
@@ -1,6 +1,11 @@ |
|
|
|
// noinspection SpellCheckingInspection |
|
|
|
|
|
|
|
import { Group, InvalidTokenError } from '../common'; |
|
|
|
import { |
|
|
|
Group, |
|
|
|
GROUP_DIGITS_INDEX, |
|
|
|
GROUP_PLACE_INDEX, |
|
|
|
InvalidTokenError, |
|
|
|
} from '../common'; |
|
|
|
import { numberToExponential } from '../exponent'; |
|
|
|
|
|
|
|
const DECIMAL_POINT = '.' as const; |
|
|
@@ -287,16 +292,20 @@ const getGroupName = (place: number, shortenMillia: boolean) => { |
|
|
|
(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') { |
|
|
|
return [[c, currentPlace]]; |
|
|
|
newGroup[GROUP_DIGITS_INDEX] = c; |
|
|
|
return [newGroup]; |
|
|
|
} |
|
|
|
|
|
|
|
if (firstGroup[0].length > 2) { |
|
|
|
return [[c, currentPlace], ...acc]; |
|
|
|
newGroup[GROUP_DIGITS_INDEX] = c; |
|
|
|
return [newGroup, ...acc]; |
|
|
|
} |
|
|
|
|
|
|
|
newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0]; |
|
|
|
return [ |
|
|
|
[c + firstGroup[0], currentPlace], |
|
|
|
newGroup, |
|
|
|
...acc.slice(1), |
|
|
|
]; |
|
|
|
}, |
|
|
@@ -378,7 +387,7 @@ export const group = (value: string): Group[] => { |
|
|
|
const currentPlace = Math.floor((exponent - i) / 3); |
|
|
|
const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace]; |
|
|
|
const currentPlaceInGroup = 2 - ((exponent - i) % 3); |
|
|
|
if (lastGroup[1] === currentPlace) { |
|
|
|
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) { |
|
|
|
const lastGroupDigits = lastGroup[0].split(''); |
|
|
|
lastGroupDigits[currentPlaceInGroup] = c; |
|
|
|
return [...acc.slice(0, -1) ?? [], [ |
|
|
@@ -584,99 +593,166 @@ interface ParserState { |
|
|
|
mode: ParseGroupsMode; |
|
|
|
} |
|
|
|
|
|
|
|
export const parseGroups = (tokens: string[]) => { |
|
|
|
const { groups } = [...tokens, FINAL_TOKEN].reduce<ParserState>( |
|
|
|
(acc, token) => { |
|
|
|
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE]; |
|
|
|
const parseThousand = (acc: ParserState, token: string): ParserState => { |
|
|
|
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE]; |
|
|
|
|
|
|
|
if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) { |
|
|
|
if (acc.mode === ParseGroupsMode.ONES_MODE) { |
|
|
|
const ones = ONES.findIndex((o) => o === acc.lastToken); |
|
|
|
if (ones > -1) { |
|
|
|
lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`; |
|
|
|
} |
|
|
|
} |
|
|
|
if (acc.mode === ParseGroupsMode.ONES_MODE) { |
|
|
|
const ones = ONES.findIndex((o) => o === acc.lastToken); |
|
|
|
if (ones > -1) { |
|
|
|
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}`; |
|
|
|
} |
|
|
|
} else if (acc.mode === ParseGroupsMode.TENS_MODE) { |
|
|
|
const tens = TENS.findIndex((t) => t === acc.lastToken); |
|
|
|
if (tens > -1) { |
|
|
|
lastGroup[GROUP_DIGITS_INDEX] = ( |
|
|
|
`${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}` |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Put the digits in the right place. |
|
|
|
lastGroup[GROUP_PLACE_INDEX] = getGroupPlaceFromGroupName(token); |
|
|
|
|
|
|
|
return { |
|
|
|
...acc, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.THOUSAND_MODE, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
const parseHundred = (acc: ParserState): ParserState => { |
|
|
|
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE]; |
|
|
|
const hundreds = ONES.findIndex((o) => o === acc.lastToken); |
|
|
|
lastGroup[GROUP_DIGITS_INDEX] = `${hundreds}${lastGroup[GROUP_DIGITS_INDEX].slice(1)}`; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
mode: ParseGroupsMode.HUNDRED_MODE, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
const parseFinal = (acc: ParserState): ParserState => { |
|
|
|
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE]; |
|
|
|
|
|
|
|
if (acc.mode === ParseGroupsMode.ONES_MODE) { |
|
|
|
const ones = ONES.findIndex((o) => o === acc.lastToken); |
|
|
|
if (ones > -1) { |
|
|
|
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; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
mode: ParseGroupsMode.DONE, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
if (acc.mode === ParseGroupsMode.TENS_MODE) { |
|
|
|
const tens = TENS.findIndex((o) => o === acc.lastToken); |
|
|
|
if (tens > -1) { |
|
|
|
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`; |
|
|
|
} |
|
|
|
lastGroup[GROUP_PLACE_INDEX] = 0; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
mode: ParseGroupsMode.DONE, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
return acc; |
|
|
|
}; |
|
|
|
|
|
|
|
lastGroup[1] = getGroupPlaceFromGroupName(token); |
|
|
|
const parseOnes = (acc: ParserState, token: string): ParserState => { |
|
|
|
if (acc.mode === ParseGroupsMode.THOUSAND_MODE) { |
|
|
|
// Create next empty place |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.ONES_MODE, |
|
|
|
groups: [...acc.groups, [...EMPTY_PLACE]], |
|
|
|
}; |
|
|
|
} |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.ONES_MODE, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => { |
|
|
|
const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token); |
|
|
|
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE]; |
|
|
|
if (acc.mode === ParseGroupsMode.THOUSAND_MODE) { |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.TEN_PLUS_ONES_MODE, |
|
|
|
groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[GROUP_PLACE_INDEX] - 1]], |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}`; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.TEN_PLUS_ONES_MODE, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
const parseTens = (acc: ParserState, token: string): ParserState => { |
|
|
|
const tens = TENS.findIndex((t) => t === token); |
|
|
|
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE]; |
|
|
|
if (acc.mode === ParseGroupsMode.THOUSAND_MODE) { |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.TENS_MODE, |
|
|
|
groups: [...acc.groups, [`0${tens}0`, lastGroup[GROUP_PLACE_INDEX] - 1]], |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
...acc, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.THOUSAND_MODE, |
|
|
|
}; |
|
|
|
lastGroup[GROUP_DIGITS_INDEX] = ( |
|
|
|
`${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}` |
|
|
|
); |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.TENS_MODE, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
export const parseGroups = (tokens: string[]) => { |
|
|
|
// We add a final token which is an empty string to parse whatever the last non-empty token is. |
|
|
|
const tokensToParse = [...tokens, FINAL_TOKEN]; |
|
|
|
const { groups } = tokensToParse.reduce<ParserState>( |
|
|
|
(acc, token) => { |
|
|
|
if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) { |
|
|
|
return parseThousand(acc, token); |
|
|
|
} |
|
|
|
|
|
|
|
if (token === HUNDRED) { |
|
|
|
if (acc.mode === ParseGroupsMode.ONES_MODE) { |
|
|
|
const hundreds = ONES.findIndex((o) => o === acc.lastToken); |
|
|
|
lastGroup[0] = `${hundreds}${lastGroup[0].slice(1)}`; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
mode: ParseGroupsMode.HUNDRED_MODE, |
|
|
|
}; |
|
|
|
} |
|
|
|
if (token === HUNDRED && acc.mode === ParseGroupsMode.ONES_MODE) { |
|
|
|
return parseHundred(acc); |
|
|
|
} |
|
|
|
|
|
|
|
if (token === FINAL_TOKEN) { |
|
|
|
if (acc.mode === ParseGroupsMode.ONES_MODE) { |
|
|
|
const ones = ONES.findIndex((o) => o === acc.lastToken); |
|
|
|
lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`; |
|
|
|
lastGroup[1] = 0; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
mode: ParseGroupsMode.DONE, |
|
|
|
}; |
|
|
|
} |
|
|
|
return parseFinal(acc); |
|
|
|
} |
|
|
|
|
|
|
|
if (ONES.includes(token as OnesName)) { |
|
|
|
if (acc.mode === ParseGroupsMode.THOUSAND_MODE) { |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.ONES_MODE, |
|
|
|
groups: [...acc.groups, [...EMPTY_PLACE]], |
|
|
|
}; |
|
|
|
} |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.ONES_MODE, |
|
|
|
}; |
|
|
|
return parseOnes(acc, token); |
|
|
|
} |
|
|
|
|
|
|
|
const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token); |
|
|
|
if (tenPlusOnes > -1) { |
|
|
|
if (acc.mode === ParseGroupsMode.THOUSAND_MODE) { |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.ONES_MODE, |
|
|
|
groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[1] - 1]], |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
lastGroup[0] = `${lastGroup[0].slice(0, 1)}1${tenPlusOnes}`; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.TEN_PLUS_ONES_MODE, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
}; |
|
|
|
if (TEN_PLUS_ONES.includes(token as TenPlusOnesName)) { |
|
|
|
return parseTenPlusOnes(acc, token); |
|
|
|
} |
|
|
|
|
|
|
|
const tens = TENS.findIndex((t) => t === token); |
|
|
|
if (tens > -1) { |
|
|
|
lastGroup[0] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[0].slice(2)}`; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
lastToken: token, |
|
|
|
mode: ParseGroupsMode.TENS_MODE, |
|
|
|
groups: [...acc.groups.slice(0, -1), lastGroup], |
|
|
|
}; |
|
|
|
if (TENS.includes(token as TensName)) { |
|
|
|
return parseTens(acc, token); |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
@@ -695,14 +771,14 @@ export const parseGroups = (tokens: string[]) => { |
|
|
|
}; |
|
|
|
|
|
|
|
export const combineGroups = (groups: Group[]) => { |
|
|
|
const places = groups.map((g) => g[1]); |
|
|
|
const places = groups.map((g) => g[GROUP_PLACE_INDEX]); |
|
|
|
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 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) { |
|
|
|
const thisGroup = groups.find((g) => g[1] === i) ?? [...EMPTY_PLACE]; |
|
|
|
const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [...EMPTY_PLACE]; |
|
|
|
groupsSorted.push(thisGroup); |
|
|
|
} |
|
|
|
|
|
|
|