Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

441 lines
12 KiB

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