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.
 
 

501 satır
13 KiB

  1. import {
  2. bigIntMax, bigIntMin,
  3. Group,
  4. GROUP_DIGITS_INDEX,
  5. GROUP_PLACE_INDEX, GroupDigits,
  6. InvalidTokenError,
  7. } from '../../../common';
  8. import {
  9. CENTILLIONS_PREFIXES,
  10. DECILLIONS_PREFIXES,
  11. DECIMAL_POINT,
  12. EMPTY_GROUP_DIGITS,
  13. EMPTY_PLACE,
  14. EXPONENT_DELIMITER,
  15. HUNDRED,
  16. ILLION_SUFFIX,
  17. MILLIA_PREFIX,
  18. MILLIONS_PREFIXES,
  19. MILLIONS_SPECIAL_PREFIXES, NEGATIVE,
  20. NEGATIVE_SYMBOL,
  21. ONES,
  22. OnesName,
  23. POSITIVE_SYMBOL,
  24. SHORT_MILLIA_DELIMITER,
  25. SHORT_MILLIA_ILLION_DELIMITER,
  26. T_AFFIX,
  27. TEN_PLUS_ONES,
  28. TenPlusOnesName,
  29. TENS,
  30. TENS_ONES_SEPARATOR,
  31. TensName,
  32. THOUSAND,
  33. } from '../../en/common';
  34. const FINAL_TOKEN = '' as const;
  35. /**
  36. * Tokenizes a string.
  37. * @param value - The string to tokenize.
  38. * @see {NumberNameSystem.mergeTokens}
  39. * @returns string[] The tokens.
  40. */
  41. export const tokenize = (value: string) => (
  42. value
  43. .toLowerCase()
  44. .trim()
  45. .replace(/\n+/gs, ' ')
  46. .replace(/\s+/g, ' ')
  47. .replace(
  48. new RegExp(`${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(.+?)${SHORT_MILLIA_ILLION_DELIMITER}`, 'g'),
  49. (_substring, milliaCount: string) => `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${milliaCount}`,
  50. )
  51. .replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ')
  52. .split(' ')
  53. .filter((maybeToken) => maybeToken.length > 0)
  54. );
  55. interface DoParseState {
  56. groupNameCurrent: string;
  57. millias: number[];
  58. milliaIndex: number;
  59. done: boolean;
  60. }
  61. /**
  62. * Deconstructs a group name token (e.g. "million", "duodecillion", etc.) to its affixes and
  63. * parses them.
  64. * @param result - The current state of the parser.
  65. * @param originalGroupName - The original group name token.
  66. * @returns DoParseState The next state of the parser.
  67. */
  68. const doParseGroupName = (result: DoParseState, originalGroupName: string): DoParseState => {
  69. if (
  70. result.groupNameCurrent.length < 1
  71. // If the current group name is "t", then we're done.
  72. // We use the -t- affix to attach the group prefix to the -illion suffix, except for decillion.
  73. || result.groupNameCurrent === T_AFFIX
  74. ) {
  75. return {
  76. ...result,
  77. // Fill the gaps of millias with zeros.
  78. millias: new Array(result.millias.length)
  79. .fill(0)
  80. .map((z, i) => (
  81. result.millias[i] ?? z
  82. )),
  83. done: true,
  84. };
  85. }
  86. const centillions = CENTILLIONS_PREFIXES.findIndex((p) => (
  87. p.length > 0 && result.groupNameCurrent.startsWith(p)
  88. ));
  89. if (centillions > -1) {
  90. return {
  91. milliaIndex: 0,
  92. millias: result.millias.map((m, i) => (
  93. i === 0
  94. ? m + (centillions * 100)
  95. : m
  96. )),
  97. groupNameCurrent: result.groupNameCurrent.slice(
  98. CENTILLIONS_PREFIXES[centillions].length,
  99. ),
  100. done: false,
  101. };
  102. }
  103. const decillions = DECILLIONS_PREFIXES.findIndex((p) => (
  104. p.length > 0 && result.groupNameCurrent.startsWith(p)
  105. ));
  106. if (decillions > -1) {
  107. return {
  108. milliaIndex: 0,
  109. millias: result.millias.map((m, i) => (
  110. i === 0
  111. ? m + (decillions * 10)
  112. : m
  113. )),
  114. groupNameCurrent: result.groupNameCurrent.slice(
  115. DECILLIONS_PREFIXES[decillions].length,
  116. ),
  117. done: false,
  118. };
  119. }
  120. const millions = MILLIONS_PREFIXES.findIndex((p) => (
  121. p.length > 0 && result.groupNameCurrent.startsWith(p)
  122. ));
  123. if (millions > -1) {
  124. return {
  125. milliaIndex: 0,
  126. millias: result.millias.map((m, i) => (
  127. i === 0
  128. ? m + millions
  129. : m
  130. )),
  131. groupNameCurrent: result.groupNameCurrent.slice(
  132. MILLIONS_PREFIXES[millions].length,
  133. ),
  134. done: false,
  135. };
  136. }
  137. if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) {
  138. let newMillia: number;
  139. let prefix: string;
  140. const isShortMillia = result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`);
  141. if (isShortMillia) {
  142. const matchedMilliaArray = result.groupNameCurrent
  143. .match(new RegExp(`^${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(\\d+)`));
  144. if (!matchedMilliaArray) {
  145. throw new InvalidTokenError(originalGroupName);
  146. }
  147. const [wholeMilliaPrefix, matchedMillia] = matchedMilliaArray;
  148. newMillia = Number(matchedMillia);
  149. if (newMillia < 1) {
  150. throw new InvalidTokenError(originalGroupName);
  151. }
  152. prefix = wholeMilliaPrefix;
  153. } else {
  154. newMillia = result.milliaIndex + 1;
  155. prefix = MILLIA_PREFIX;
  156. }
  157. const oldMillia = result.milliaIndex;
  158. const newMillias = [...result.millias];
  159. newMillias[newMillia] = newMillias[oldMillia] || 1;
  160. newMillias[oldMillia] = 0;
  161. return {
  162. milliaIndex: newMillia,
  163. millias: newMillias,
  164. groupNameCurrent: result.groupNameCurrent.slice(prefix.length),
  165. done: false,
  166. };
  167. }
  168. throw new InvalidTokenError(originalGroupName);
  169. };
  170. /**
  171. * Gets the place of a group name (e.g. "million", "duodecillion", etc.).
  172. * @param groupName - The group name.
  173. * @returns bigint The place of the group name.
  174. */
  175. const getGroupPlaceFromGroupName = (groupName: string) => {
  176. if (groupName === THOUSAND) {
  177. return BigInt(1);
  178. }
  179. const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
  180. const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);
  181. if (specialMillions > -1) {
  182. return BigInt(specialMillions + 1);
  183. }
  184. let result: DoParseState = {
  185. groupNameCurrent: groupNameBase,
  186. millias: [0],
  187. milliaIndex: 0,
  188. done: false,
  189. };
  190. do {
  191. result = doParseGroupName(result, groupName);
  192. } while (!result.done);
  193. const bigGroupPlace = BigInt(
  194. result.millias
  195. .map((s) => s.toString().padStart(3, '0'))
  196. .reverse()
  197. .join(''),
  198. );
  199. return bigGroupPlace + BigInt(1);
  200. };
  201. /**
  202. * Mode of the group parser.
  203. */
  204. enum ParseGroupsMode {
  205. /**
  206. * Initial mode.
  207. */
  208. INITIAL = 'initial',
  209. /**
  210. * Has parsed a ones name.
  211. */
  212. ONES_MODE = 'ones',
  213. /**
  214. * Has parsed a tens name.
  215. */
  216. TENS_MODE = 'tens',
  217. /**
  218. * Has parsed a ten-plus-ones name.
  219. */
  220. TEN_PLUS_ONES_MODE = 'tenPlusOnes',
  221. /**
  222. * Has parsed a "hundred" token.
  223. */
  224. HUNDRED_MODE = 'hundred',
  225. /**
  226. * Has parsed a "thousand" or any "-illion" token.
  227. */
  228. THOUSAND_MODE = 'thousand',
  229. /**
  230. * Done parsing.
  231. */
  232. DONE = 'done',
  233. }
  234. /**
  235. * State of the group parser.
  236. */
  237. interface ParserState {
  238. lastToken?: string;
  239. groups: Group[];
  240. mode: ParseGroupsMode;
  241. negative: boolean;
  242. }
  243. const parseThousand = (acc: ParserState, token: string): ParserState => {
  244. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  245. if (acc.mode === ParseGroupsMode.ONES_MODE) {
  246. const ones = ONES.findIndex((o) => o === acc.lastToken);
  247. if (ones > -1) {
  248. lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
  249. }
  250. } else if (acc.mode === ParseGroupsMode.TENS_MODE) {
  251. const tens = TENS.findIndex((t) => t === acc.lastToken);
  252. if (tens > -1) {
  253. lastGroup[GROUP_DIGITS_INDEX] = (
  254. `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
  255. ) as GroupDigits;
  256. }
  257. }
  258. // Put the digits in the right place.
  259. lastGroup[GROUP_PLACE_INDEX] = getGroupPlaceFromGroupName(token);
  260. return {
  261. ...acc,
  262. groups: [...acc.groups.slice(0, -1), lastGroup],
  263. lastToken: token,
  264. mode: ParseGroupsMode.THOUSAND_MODE,
  265. };
  266. };
  267. const parseHundred = (acc: ParserState): ParserState => {
  268. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  269. const hundreds = ONES.findIndex((o) => o === acc.lastToken);
  270. lastGroup[GROUP_DIGITS_INDEX] = `${hundreds}${lastGroup[GROUP_DIGITS_INDEX].slice(1)}` as GroupDigits;
  271. return {
  272. ...acc,
  273. groups: [...acc.groups.slice(0, -1), lastGroup],
  274. mode: ParseGroupsMode.HUNDRED_MODE,
  275. };
  276. };
  277. const parseFinal = (acc: ParserState): ParserState => {
  278. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  279. if (acc.mode === ParseGroupsMode.ONES_MODE) {
  280. const ones = ONES.findIndex((o) => o === acc.lastToken);
  281. if (ones > -1) {
  282. lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
  283. }
  284. // We assume last token without parsed place will always be the smallest
  285. lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
  286. return {
  287. ...acc,
  288. groups: [...acc.groups.slice(0, -1), lastGroup],
  289. mode: ParseGroupsMode.DONE,
  290. };
  291. }
  292. if (acc.mode === ParseGroupsMode.TENS_MODE) {
  293. const tens = TENS.findIndex((o) => o === acc.lastToken);
  294. if (tens > -1) {
  295. lastGroup[GROUP_DIGITS_INDEX] = (
  296. `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
  297. ) as GroupDigits;
  298. }
  299. lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
  300. return {
  301. ...acc,
  302. groups: [...acc.groups.slice(0, -1), lastGroup],
  303. mode: ParseGroupsMode.DONE,
  304. };
  305. }
  306. return acc;
  307. };
  308. const parseOnes = (acc: ParserState, token: string): ParserState => {
  309. if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
  310. // Create next empty place
  311. return {
  312. ...acc,
  313. lastToken: token,
  314. mode: ParseGroupsMode.ONES_MODE,
  315. groups: [...acc.groups, [...EMPTY_PLACE]],
  316. };
  317. }
  318. return {
  319. ...acc,
  320. lastToken: token,
  321. mode: ParseGroupsMode.ONES_MODE,
  322. };
  323. };
  324. const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => {
  325. const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
  326. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  327. if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
  328. return {
  329. ...acc,
  330. lastToken: token,
  331. mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
  332. groups: [...acc.groups, [`01${tenPlusOnes}` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
  333. };
  334. }
  335. lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}` as GroupDigits;
  336. return {
  337. ...acc,
  338. lastToken: token,
  339. mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
  340. groups: [...acc.groups.slice(0, -1), lastGroup],
  341. };
  342. };
  343. const parseTens = (acc: ParserState, token: string): ParserState => {
  344. const tens = TENS.findIndex((t) => t === token);
  345. const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
  346. if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
  347. return {
  348. ...acc,
  349. lastToken: token,
  350. mode: ParseGroupsMode.TENS_MODE,
  351. groups: [...acc.groups, [`0${tens}0` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
  352. };
  353. }
  354. lastGroup[GROUP_DIGITS_INDEX] = (
  355. `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
  356. ) as GroupDigits;
  357. return {
  358. ...acc,
  359. lastToken: token,
  360. mode: ParseGroupsMode.TENS_MODE,
  361. groups: [...acc.groups.slice(0, -1), lastGroup],
  362. };
  363. };
  364. /**
  365. * Parses groups from a string.
  366. * @param tokens - The string to parse groups from.
  367. * @see {NumberNameSystem.stringifyGroups}
  368. * @returns Group[] The parsed groups.
  369. */
  370. export const parseGroups = (tokens: string[]) => {
  371. // We add a final token which is an empty string to parse whatever the last non-empty token is.
  372. const tokensToParse = [...tokens, FINAL_TOKEN];
  373. const { groups, negative } = tokensToParse.reduce<ParserState>(
  374. (acc, token) => {
  375. if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
  376. return parseThousand(acc, token);
  377. }
  378. if (token === HUNDRED && acc.mode === ParseGroupsMode.ONES_MODE) {
  379. return parseHundred(acc);
  380. }
  381. if (token === FINAL_TOKEN) {
  382. return parseFinal(acc);
  383. }
  384. if (ONES.includes(token as OnesName)) {
  385. return parseOnes(acc, token);
  386. }
  387. if (TEN_PLUS_ONES.includes(token as TenPlusOnesName)) {
  388. return parseTenPlusOnes(acc, token);
  389. }
  390. if (TENS.includes(token as TensName)) {
  391. return parseTens(acc, token);
  392. }
  393. if (token === NEGATIVE) {
  394. return {
  395. ...acc,
  396. negative: !acc.negative,
  397. };
  398. }
  399. throw new InvalidTokenError(token);
  400. },
  401. {
  402. lastToken: undefined,
  403. groups: [],
  404. mode: ParseGroupsMode.INITIAL,
  405. negative: false,
  406. },
  407. );
  408. return { groups, negative };
  409. };
  410. /**
  411. * Combines groups into a string.
  412. * @param groups - The groups to combine.
  413. * @param negative - Whether the number is negative.
  414. * @see {NumberNameSystem.splitIntoGroups}
  415. * @returns string The combined groups in exponential form.
  416. */
  417. export const combineGroups = (groups: Group[], negative: boolean) => {
  418. if (groups.length < 1) {
  419. return '';
  420. }
  421. const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
  422. const maxPlace = bigIntMax(...places) as bigint;
  423. const minPlace = bigIntMin(...places) as bigint;
  424. const firstGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === maxPlace) as Group;
  425. const firstGroupPlace = firstGroup[GROUP_PLACE_INDEX];
  426. const groupsSorted = [];
  427. for (let i = maxPlace; i >= minPlace; i = BigInt(i) - BigInt(1)) {
  428. const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [EMPTY_GROUP_DIGITS, i];
  429. groupsSorted.push(thisGroup);
  430. }
  431. const digits = groupsSorted.reduce(
  432. (previousDigits, thisGroup) => {
  433. const [groupDigits] = thisGroup;
  434. return `${previousDigits}${groupDigits}`;
  435. },
  436. '',
  437. ).replace(/^0+/, '') || '0';
  438. const firstGroupDigits = firstGroup[0];
  439. const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, '');
  440. const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length;
  441. const exponentValue = BigInt(
  442. (BigInt(firstGroupPlace) * BigInt(3)) + (BigInt(2) - BigInt(exponentExtra)),
  443. );
  444. const isExponentNegative = exponentValue < 0;
  445. const exponentValueAbs = isExponentNegative ? -exponentValue : exponentValue;
  446. const exponentSign = isExponentNegative ? NEGATIVE_SYMBOL : POSITIVE_SYMBOL;
  447. const exponent = `${exponentSign}${exponentValueAbs}`;
  448. const significandInteger = digits.slice(0, 1);
  449. const significandFraction = digits.slice(1).replace(/0+$/, '');
  450. if (significandFraction.length > 0) {
  451. return `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
  452. }
  453. return `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
  454. };