Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 

807 satır
20 KiB

  1. // noinspection SpellCheckingInspection
  2. import {
  3. Group,
  4. GROUP_DIGITS_INDEX,
  5. GROUP_PLACE_INDEX,
  6. InvalidTokenError,
  7. } from '../common';
  8. import { numberToExponential } from '../exponent';
  9. const DECIMAL_POINT = '.' as const;
  10. const GROUPING_SYMBOL = ',' as const;
  11. const NEGATIVE = 'negative' as const;
  12. const NEGATIVE_SYMBOL = '-' as const;
  13. // replace with hyphen with option
  14. const TENS_ONES_SEPARATOR = ' ' as const;
  15. const POSITIVE_SYMBOL = '+' as const;
  16. const SHORT_MILLIA_DELIMITER = '^' as const;
  17. const EXPONENT_DELIMITER = 'e' as const;
  18. const EMPTY_GROUP_DIGITS = '000' as const;
  19. const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, 0];
  20. /**
  21. * Ones number names.
  22. */
  23. const ONES = [
  24. 'zero',
  25. 'one',
  26. 'two',
  27. 'three',
  28. 'four',
  29. 'five',
  30. 'six',
  31. 'seven',
  32. 'eight',
  33. 'nine',
  34. ] as const;
  35. type OnesName = typeof ONES[number];
  36. /**
  37. * Ten plus ones number names.
  38. */
  39. const TEN_PLUS_ONES = [
  40. 'ten',
  41. 'eleven',
  42. 'twelve',
  43. 'thirteen',
  44. 'fourteen',
  45. 'fifteen',
  46. 'sixteen',
  47. 'seventeen',
  48. 'eighteen',
  49. 'nineteen',
  50. ] as const;
  51. type TenPlusOnesName = typeof TEN_PLUS_ONES[number];
  52. /**
  53. * Tens number names.
  54. */
  55. const TENS = [
  56. 'zero',
  57. TEN_PLUS_ONES[0],
  58. 'twenty',
  59. 'thirty',
  60. 'forty',
  61. 'fifty',
  62. 'sixty',
  63. 'seventy',
  64. 'eighty',
  65. 'ninety',
  66. ] as const;
  67. type TensName = typeof TENS[number];
  68. /**
  69. * Hundreds name.
  70. */
  71. const HUNDRED = 'hundred' as const;
  72. /**
  73. * Thousands name.
  74. */
  75. const THOUSAND = 'thousand' as const;
  76. // const ILLION_ORDINAL_SUFFIX = 'illionth' as const;
  77. // const THOUSAND_ORDINAL = 'thousandth' as const;
  78. /**
  79. * Special millions name.
  80. */
  81. const MILLIONS_SPECIAL_PREFIXES = [
  82. '',
  83. 'm',
  84. 'b',
  85. 'tr',
  86. 'quadr',
  87. 'quint',
  88. 'sext',
  89. 'sept',
  90. 'oct',
  91. 'non',
  92. ] as const;
  93. type MillionsSpecialPrefix = Exclude<typeof MILLIONS_SPECIAL_PREFIXES[number], ''>;
  94. /**
  95. * Millions name.
  96. */
  97. const MILLIONS_PREFIXES = [
  98. '',
  99. 'un',
  100. 'duo',
  101. 'tre',
  102. 'quattuor',
  103. 'quin',
  104. 'sex',
  105. 'septen',
  106. 'octo',
  107. 'novem',
  108. ] as const;
  109. type MillionsPrefix = Exclude<typeof MILLIONS_PREFIXES[number], ''>;
  110. /**
  111. * Decillions name.
  112. */
  113. const DECILLIONS_PREFIXES = [
  114. '',
  115. 'dec',
  116. 'vigin',
  117. 'trigin',
  118. 'quadragin',
  119. 'quinquagin',
  120. 'sexagin',
  121. 'septuagin',
  122. 'octogin',
  123. 'nonagin',
  124. ] as const;
  125. type DecillionsPrefix = Exclude<typeof DECILLIONS_PREFIXES[number], ''>;
  126. /**
  127. * Centillions name.
  128. */
  129. const CENTILLIONS_PREFIXES = [
  130. '',
  131. 'cen',
  132. 'duocen',
  133. 'trecen',
  134. 'quadringen',
  135. 'quingen',
  136. 'sescen',
  137. 'septingen',
  138. 'octingen',
  139. 'nongen',
  140. ] as const;
  141. type CentillionsPrefix = Exclude<typeof CENTILLIONS_PREFIXES[number], ''>;
  142. /**
  143. * Prefix for millia- number names.
  144. */
  145. const MILLIA_PREFIX = 'millia' as const;
  146. /**
  147. * Suffix for -illion number names.
  148. */
  149. const ILLION_SUFFIX = 'illion' as const;
  150. /**
  151. * Builds a name for numbers in tens and ones.
  152. * @param tens - Tens digit.
  153. * @param ones - Ones digit.
  154. * @returns string The name for the number.
  155. */
  156. const makeTensName = (tens: number, ones: number) => {
  157. if (tens === 0) {
  158. return ONES[ones];
  159. }
  160. if (tens === 1) {
  161. return TEN_PLUS_ONES[ones] as unknown as TenPlusOnesName;
  162. }
  163. if (ones === 0) {
  164. return TENS[tens];
  165. }
  166. return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${TENS_ONES_SEPARATOR}${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
  167. };
  168. /**
  169. * Builds a name for numbers in hundreds, tens, and ones.
  170. * @param hundreds - Hundreds digit.
  171. * @param tens - Tens digit.
  172. * @param ones - Ones digit.
  173. * @returns string The name for the number.
  174. */
  175. const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
  176. if (hundreds === 0) {
  177. return makeTensName(tens, ones);
  178. }
  179. if (tens === 0 && ones === 0) {
  180. return `${ONES[hundreds]} ${HUNDRED}` as const;
  181. }
  182. return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones)}` as const;
  183. };
  184. /**
  185. * Builds a name for numbers in the millions.
  186. * @param millions - Millions digit.
  187. * @param milliaCount - Number of millia- groups.
  188. * @returns string The millions prefix.
  189. */
  190. const makeMillionsPrefix = (millions: number, milliaCount: number) => {
  191. if (milliaCount > 0) {
  192. return MILLIONS_PREFIXES[millions] as MillionsPrefix;
  193. }
  194. return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix;
  195. };
  196. /**
  197. * Builds a name for numbers in the decillions.
  198. * @param decillions - Decillions digit.
  199. * @param millions - Millions digit.
  200. * @param milliaCount - Number of millia- groups.
  201. * @returns string The decillions prefix.
  202. */
  203. const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => {
  204. if (decillions === 0) {
  205. return makeMillionsPrefix(millions, milliaCount);
  206. }
  207. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  208. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  209. return `${onesPrefix}${tensName}` as const;
  210. };
  211. /**
  212. * Builds a name for numbers in the centillions.
  213. * @param centillions - Centillions digit.
  214. * @param decillions - Decillions digit.
  215. * @param millions - Millions digit.
  216. * @param milliaCount - Number of millia- groups.
  217. * @returns string The centillions prefix.
  218. */
  219. const makeCentillionsPrefix = (
  220. centillions: number,
  221. decillions: number,
  222. millions: number,
  223. milliaCount: number,
  224. ) => {
  225. if (centillions === 0) {
  226. return makeDecillionsPrefix(decillions, millions, milliaCount);
  227. }
  228. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  229. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  230. const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix;
  231. return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
  232. };
  233. const getGroupName = (place: number, shortenMillia: boolean) => {
  234. if (place === 0) {
  235. return '' as const;
  236. }
  237. if (place === 1) {
  238. return THOUSAND;
  239. }
  240. const bigGroupPlace = place - 1;
  241. const groupGroups = bigGroupPlace
  242. .toString()
  243. .split('')
  244. .reduceRight<Group[]>(
  245. (acc, c, i, cc) => {
  246. const firstGroup = acc.at(0);
  247. const currentPlace = Math.floor((cc.length - i - 1) / 3);
  248. const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
  249. if (typeof firstGroup === 'undefined') {
  250. newGroup[GROUP_DIGITS_INDEX] = c;
  251. return [newGroup];
  252. }
  253. if (firstGroup[0].length > 2) {
  254. newGroup[GROUP_DIGITS_INDEX] = c;
  255. return [newGroup, ...acc];
  256. }
  257. newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
  258. return [
  259. newGroup,
  260. ...acc.slice(1),
  261. ];
  262. },
  263. [],
  264. )
  265. .map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const)
  266. .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
  267. .map(([groupDigits, groupPlace]) => {
  268. const [hundreds, tens, ones] = groupDigits.split('').map(Number);
  269. if (groupPlace < 1) {
  270. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace);
  271. }
  272. const milliaSuffix = (
  273. shortenMillia && groupPlace > 1
  274. ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}`
  275. : MILLIA_PREFIX.repeat(groupPlace)
  276. );
  277. if (groupDigits === '001') {
  278. return milliaSuffix;
  279. }
  280. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix;
  281. })
  282. .join('');
  283. if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
  284. return `${groupGroups}${ILLION_SUFFIX}` as const;
  285. }
  286. if (bigGroupPlace > 10) {
  287. return `${groupGroups}t${ILLION_SUFFIX}` as const;
  288. }
  289. return `${groupGroups}${ILLION_SUFFIX}` as const;
  290. };
  291. export const makeGroups = (groups: Group[], options?: Record<string, unknown>): string[] => {
  292. const filteredGroups = groups.filter(([digits, place]) => (
  293. place === 0 || digits !== EMPTY_GROUP_DIGITS
  294. ));
  295. return filteredGroups.map(
  296. ([group, place]) => {
  297. const makeHundredsArgs = group
  298. .padStart(3, '0')
  299. .split('')
  300. .map((s) => Number(s)) as [number, number, number];
  301. const groupDigitsName = makeHundredsName(...makeHundredsArgs);
  302. const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false);
  303. if (groupName.length > 0) {
  304. return `${groupDigitsName} ${groupName}`;
  305. }
  306. return groupDigitsName;
  307. },
  308. );
  309. };
  310. /**
  311. * Group a number string into groups of three digits, starting from the decimal point.
  312. * @param value - The number string to group.
  313. */
  314. export const group = (value: string): Group[] => {
  315. const [significand, exponentString] = numberToExponential(
  316. value,
  317. {
  318. decimalPoint: DECIMAL_POINT,
  319. groupingSymbol: GROUPING_SYMBOL,
  320. exponentDelimiter: EXPONENT_DELIMITER,
  321. },
  322. )
  323. .split(EXPONENT_DELIMITER);
  324. const exponent = Number(exponentString);
  325. const significantDigits = significand.replace(DECIMAL_POINT, '');
  326. return significantDigits.split('').reduce<Group[]>(
  327. (acc, c, i) => {
  328. const currentPlace = Math.floor((exponent - i) / 3);
  329. const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
  330. const currentPlaceInGroup = 2 - ((exponent - i) % 3);
  331. if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
  332. const lastGroupDigits = lastGroup[0].split('');
  333. lastGroupDigits[currentPlaceInGroup] = c;
  334. return [...acc.slice(0, -1) ?? [], [
  335. lastGroupDigits.join(''),
  336. currentPlace,
  337. ]];
  338. }
  339. return [...acc, [c.padEnd(3, '0'), currentPlace]];
  340. },
  341. [],
  342. );
  343. };
  344. /**
  345. * Formats the final tokenized string.
  346. * @param tokens - The tokens to finalize.
  347. */
  348. export const finalize = (tokens: string[]) => (
  349. tokens
  350. .map((t) => t.trim())
  351. .join(' ')
  352. .trim()
  353. );
  354. /**
  355. * Makes a negative string.
  356. * @param s - The string to make negative.
  357. */
  358. export const makeNegative = (s: string) => (
  359. `${NEGATIVE} ${s}`
  360. );
  361. export const tokenize = (stringValue: string) => (
  362. stringValue
  363. .replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ')
  364. .split(' ')
  365. .filter((maybeToken) => maybeToken.length > 0)
  366. );
  367. const FINAL_TOKEN = '' as const;
  368. interface DoParseState {
  369. groupNameCurrent: string;
  370. millias: number[];
  371. milliaIndex: number;
  372. done: boolean;
  373. }
  374. const doParseGroupName = (result: DoParseState): DoParseState => {
  375. if (result.groupNameCurrent.length < 1) {
  376. return {
  377. ...result,
  378. done: true,
  379. };
  380. }
  381. if (result.groupNameCurrent === 't') {
  382. // If the current group name is "t", then we're done.
  383. // We use the -t- affix to attach the group prefix to the -illion suffix, except for decillion.
  384. return {
  385. ...result,
  386. done: true,
  387. };
  388. }
  389. const centillions = CENTILLIONS_PREFIXES.findIndex((p) => (
  390. p.length > 0 && result.groupNameCurrent.startsWith(p)
  391. ));
  392. if (centillions > -1) {
  393. return {
  394. milliaIndex: 0,
  395. millias: result.millias.map((m, i) => (
  396. i === 0
  397. ? m + (centillions * 100)
  398. : m
  399. )),
  400. groupNameCurrent: result.groupNameCurrent.slice(
  401. CENTILLIONS_PREFIXES[centillions].length,
  402. ),
  403. done: false,
  404. };
  405. }
  406. const decillions = DECILLIONS_PREFIXES.findIndex((p) => (
  407. p.length > 0 && result.groupNameCurrent.startsWith(p)
  408. ));
  409. if (decillions > -1) {
  410. return {
  411. milliaIndex: 0,
  412. millias: result.millias.map((m, i) => (
  413. i === 0
  414. ? m + (decillions * 10)
  415. : m
  416. )),
  417. groupNameCurrent: result.groupNameCurrent.slice(
  418. DECILLIONS_PREFIXES[decillions].length,
  419. ),
  420. done: false,
  421. };
  422. }
  423. const millions = MILLIONS_PREFIXES.findIndex((p) => (
  424. p.length > 0 && result.groupNameCurrent.startsWith(p)
  425. ));
  426. if (millions > -1) {
  427. return {
  428. milliaIndex: 0,
  429. millias: result.millias.map((m, i) => (
  430. i === 0
  431. ? m + millions
  432. : m
  433. )),
  434. groupNameCurrent: result.groupNameCurrent.slice(
  435. MILLIONS_PREFIXES[millions].length,
  436. ),
  437. done: false,
  438. };
  439. }
  440. if (result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`)) {
  441. // short millia
  442. const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/);
  443. if (!matchedMilliaArray) {
  444. throw new InvalidTokenError(result.groupNameCurrent);
  445. }
  446. const matchedMillia = matchedMilliaArray[0];
  447. const newMillia = Number(matchedMillia);
  448. const oldMillia = result.milliaIndex;
  449. const newMillias = [...result.millias];
  450. newMillias[newMillia] = newMillias[oldMillia] || 1;
  451. newMillias[oldMillia] = 0;
  452. return {
  453. milliaIndex: newMillia,
  454. millias: newMillias,
  455. groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
  456. done: false,
  457. };
  458. }
  459. if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) {
  460. const newMillia = result.milliaIndex + 1;
  461. const oldMillia = result.milliaIndex;
  462. const newMillias = [...result.millias];
  463. newMillias[newMillia] = newMillias[oldMillia] || 1;
  464. newMillias[oldMillia] = 0;
  465. return {
  466. milliaIndex: newMillia,
  467. millias: newMillias,
  468. groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
  469. done: false,
  470. };
  471. }
  472. throw new InvalidTokenError(result.groupNameCurrent);
  473. };
  474. const getGroupPlaceFromGroupName = (groupName: string) => {
  475. if (groupName === THOUSAND) {
  476. return 1;
  477. }
  478. const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
  479. const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);
  480. if (specialMillions > -1) {
  481. return specialMillions + 1;
  482. }
  483. let result: DoParseState = {
  484. groupNameCurrent: groupNameBase,
  485. millias: [0],
  486. milliaIndex: 0,
  487. done: false,
  488. };
  489. do {
  490. result = doParseGroupName(result);
  491. } while (!result.done);
  492. const bigGroupPlace = Number(
  493. result.millias
  494. .map((s) => s.toString().padStart(3, '0'))
  495. .reverse()
  496. .join(''),
  497. );
  498. return bigGroupPlace + 1;
  499. };
  500. enum ParseGroupsMode {
  501. INITIAL = 'unknown',
  502. ONES_MODE = 'ones',
  503. TENS_MODE = 'tens',
  504. TEN_PLUS_ONES_MODE = 'tenPlusOnes',
  505. HUNDRED_MODE = 'hundred',
  506. THOUSAND_MODE = 'thousand',
  507. DONE = 'done',
  508. }
  509. interface ParserState {
  510. lastToken?: string;
  511. groups: Group[];
  512. mode: ParseGroupsMode;
  513. }
  514. const parseThousand = (acc: ParserState, token: string): ParserState => {
  515. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  516. if (acc.mode === ParseGroupsMode.ONES_MODE) {
  517. const ones = ONES.findIndex((o) => o === acc.lastToken);
  518. if (ones > -1) {
  519. lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}`;
  520. }
  521. } else if (acc.mode === ParseGroupsMode.TENS_MODE) {
  522. const tens = TENS.findIndex((t) => t === acc.lastToken);
  523. if (tens > -1) {
  524. lastGroup[GROUP_DIGITS_INDEX] = (
  525. `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
  526. );
  527. }
  528. }
  529. // Put the digits in the right place.
  530. lastGroup[GROUP_PLACE_INDEX] = getGroupPlaceFromGroupName(token);
  531. return {
  532. ...acc,
  533. groups: [...acc.groups.slice(0, -1), lastGroup],
  534. lastToken: token,
  535. mode: ParseGroupsMode.THOUSAND_MODE,
  536. };
  537. };
  538. const parseHundred = (acc: ParserState): ParserState => {
  539. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  540. const hundreds = ONES.findIndex((o) => o === acc.lastToken);
  541. lastGroup[GROUP_DIGITS_INDEX] = `${hundreds}${lastGroup[GROUP_DIGITS_INDEX].slice(1)}`;
  542. return {
  543. ...acc,
  544. groups: [...acc.groups.slice(0, -1), lastGroup],
  545. mode: ParseGroupsMode.HUNDRED_MODE,
  546. };
  547. };
  548. const parseFinal = (acc: ParserState): ParserState => {
  549. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  550. if (acc.mode === ParseGroupsMode.ONES_MODE) {
  551. const ones = ONES.findIndex((o) => o === acc.lastToken);
  552. if (ones > -1) {
  553. lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}`;
  554. }
  555. // We assume last token without parsed place will always be the smallest
  556. lastGroup[GROUP_PLACE_INDEX] = 0;
  557. return {
  558. ...acc,
  559. groups: [...acc.groups.slice(0, -1), lastGroup],
  560. mode: ParseGroupsMode.DONE,
  561. };
  562. }
  563. if (acc.mode === ParseGroupsMode.TENS_MODE) {
  564. const tens = TENS.findIndex((o) => o === acc.lastToken);
  565. if (tens > -1) {
  566. lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`;
  567. }
  568. lastGroup[GROUP_PLACE_INDEX] = 0;
  569. return {
  570. ...acc,
  571. groups: [...acc.groups.slice(0, -1), lastGroup],
  572. mode: ParseGroupsMode.DONE,
  573. };
  574. }
  575. return acc;
  576. };
  577. const parseOnes = (acc: ParserState, token: string): ParserState => {
  578. if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
  579. // Create next empty place
  580. return {
  581. ...acc,
  582. lastToken: token,
  583. mode: ParseGroupsMode.ONES_MODE,
  584. groups: [...acc.groups, [...EMPTY_PLACE]],
  585. };
  586. }
  587. return {
  588. ...acc,
  589. lastToken: token,
  590. mode: ParseGroupsMode.ONES_MODE,
  591. };
  592. };
  593. const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => {
  594. const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
  595. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  596. if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
  597. return {
  598. ...acc,
  599. lastToken: token,
  600. mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
  601. groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[GROUP_PLACE_INDEX] - 1]],
  602. };
  603. }
  604. lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}`;
  605. return {
  606. ...acc,
  607. lastToken: token,
  608. mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
  609. groups: [...acc.groups.slice(0, -1), lastGroup],
  610. };
  611. };
  612. const parseTens = (acc: ParserState, token: string): ParserState => {
  613. const tens = TENS.findIndex((t) => t === token);
  614. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  615. if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
  616. return {
  617. ...acc,
  618. lastToken: token,
  619. mode: ParseGroupsMode.TENS_MODE,
  620. groups: [...acc.groups, [`0${tens}0`, lastGroup[GROUP_PLACE_INDEX] - 1]],
  621. };
  622. }
  623. lastGroup[GROUP_DIGITS_INDEX] = (
  624. `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
  625. );
  626. return {
  627. ...acc,
  628. lastToken: token,
  629. mode: ParseGroupsMode.TENS_MODE,
  630. groups: [...acc.groups.slice(0, -1), lastGroup],
  631. };
  632. };
  633. export const parseGroups = (tokens: string[]) => {
  634. // We add a final token which is an empty string to parse whatever the last non-empty token is.
  635. const tokensToParse = [...tokens, FINAL_TOKEN];
  636. const { groups } = tokensToParse.reduce<ParserState>(
  637. (acc, token) => {
  638. if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
  639. return parseThousand(acc, token);
  640. }
  641. if (token === HUNDRED && acc.mode === ParseGroupsMode.ONES_MODE) {
  642. return parseHundred(acc);
  643. }
  644. if (token === FINAL_TOKEN) {
  645. return parseFinal(acc);
  646. }
  647. if (ONES.includes(token as OnesName)) {
  648. return parseOnes(acc, token);
  649. }
  650. if (TEN_PLUS_ONES.includes(token as TenPlusOnesName)) {
  651. return parseTenPlusOnes(acc, token);
  652. }
  653. if (TENS.includes(token as TensName)) {
  654. return parseTens(acc, token);
  655. }
  656. return {
  657. ...acc,
  658. lastToken: token,
  659. };
  660. },
  661. {
  662. lastToken: undefined,
  663. groups: [],
  664. mode: ParseGroupsMode.INITIAL,
  665. },
  666. );
  667. return groups;
  668. };
  669. export const combineGroups = (groups: Group[]) => {
  670. const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
  671. const maxPlace = Math.max(...places);
  672. const minPlace = Math.min(...places);
  673. const firstGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === maxPlace) ?? [...EMPTY_PLACE];
  674. const firstGroupPlace = firstGroup[GROUP_PLACE_INDEX];
  675. const groupsSorted = [];
  676. for (let i = maxPlace; i >= minPlace; i -= 1) {
  677. const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [...EMPTY_PLACE];
  678. groupsSorted.push(thisGroup);
  679. }
  680. const digits = groupsSorted.reduce(
  681. (previousDigits, thisGroup) => {
  682. const [groupDigits] = thisGroup;
  683. return `${previousDigits}${groupDigits}`;
  684. },
  685. '',
  686. ).replace(/^0+/, '') || '0';
  687. const firstGroupDigits = firstGroup[0];
  688. const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, '');
  689. const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length;
  690. const exponentValue = BigInt((firstGroupPlace * 3) + (2 - exponentExtra));
  691. const isExponentNegative = exponentValue < 0;
  692. const exponentValueAbs = isExponentNegative ? -exponentValue : exponentValue;
  693. const exponentSign = isExponentNegative ? NEGATIVE_SYMBOL : POSITIVE_SYMBOL;
  694. const exponent = `${exponentSign}${exponentValueAbs}`;
  695. const significandInteger = digits.slice(0, 1);
  696. const significandFraction = digits.slice(1).replace(/0+$/, '');
  697. if (significandFraction.length > 0) {
  698. return `${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
  699. }
  700. return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
  701. };