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.
 
 

554 satır
13 KiB

  1. // noinspection SpellCheckingInspection
  2. import { Group } from '../common';
  3. import { numberToExponential } from '../exponent';
  4. const DECIMAL_POINT = '.';
  5. const GROUPING_SYMBOL = ',';
  6. const NEGATIVE = 'negative';
  7. const ONES = [
  8. 'zero',
  9. 'one',
  10. 'two',
  11. 'three',
  12. 'four',
  13. 'five',
  14. 'six',
  15. 'seven',
  16. 'eight',
  17. 'nine',
  18. ] as const;
  19. type OnesName = typeof ONES[number];
  20. const TEN_PLUS_ONES = [
  21. 'ten',
  22. 'eleven',
  23. 'twelve',
  24. 'thirteen',
  25. 'fourteen',
  26. 'fifteen',
  27. 'sixteen',
  28. 'seventeen',
  29. 'eighteen',
  30. 'nineteen',
  31. ] as const;
  32. type TenPlusOnesName = typeof TEN_PLUS_ONES[number];
  33. const TENS = [
  34. 'zero',
  35. TEN_PLUS_ONES[0],
  36. 'twenty',
  37. 'thirty',
  38. 'forty',
  39. 'fifty',
  40. 'sixty',
  41. 'seventy',
  42. 'eighty',
  43. 'ninety',
  44. ] as const;
  45. type TensName = typeof TENS[number];
  46. const HUNDRED = 'hundred' as const;
  47. const THOUSAND = 'thousand' as const;
  48. // const ILLION_ORDINAL_SUFFIX = 'illionth' as const;
  49. // const THOUSAND_ORDINAL = 'thousandth' as const;
  50. const MILLIONS_SPECIAL_PREFIXES = [
  51. '',
  52. 'm',
  53. 'b',
  54. 'tr',
  55. 'quadr',
  56. 'quint',
  57. 'sext',
  58. 'sept',
  59. 'oct',
  60. 'non',
  61. ] as const;
  62. type MillionsSpecialPrefix = typeof MILLIONS_SPECIAL_PREFIXES[number];
  63. const MILLIONS_PREFIXES = [
  64. '',
  65. 'un',
  66. 'duo',
  67. 'tre',
  68. 'quattuor',
  69. 'quin',
  70. 'sex',
  71. 'septen',
  72. 'octo',
  73. 'novem',
  74. ] as const;
  75. type MillionsPrefix = typeof MILLIONS_PREFIXES[number];
  76. const DECILLIONS_PREFIXES = [
  77. '',
  78. 'dec',
  79. 'vigin',
  80. 'trigin',
  81. 'quadragin',
  82. 'quinquagin',
  83. 'sexagin',
  84. 'septuagin',
  85. 'octogin',
  86. 'nonagin',
  87. ] as const;
  88. type DecillionsPrefix = typeof DECILLIONS_PREFIXES[number];
  89. const CENTILLIONS_PREFIXES = [
  90. '',
  91. 'cen',
  92. 'duocen',
  93. 'trecen',
  94. 'quadringen',
  95. 'quingen',
  96. 'sescen',
  97. 'septingen',
  98. 'octingen',
  99. 'nongen',
  100. ] as const;
  101. type CentillionsPrefix = typeof CENTILLIONS_PREFIXES[number];
  102. const MILLIA_PREFIX = 'millia' as const;
  103. const ILLION_SUFFIX = 'illion' as const;
  104. const makeTensName = (tens: number, ones: number) => {
  105. if (tens === 0) {
  106. return ONES[ones];
  107. }
  108. if (tens === 1) {
  109. return TEN_PLUS_ONES[ones] as TenPlusOnesName;
  110. }
  111. if (ones === 0) {
  112. return TENS[tens];
  113. }
  114. return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>} ${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
  115. };
  116. const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
  117. if (hundreds === 0) {
  118. return makeTensName(tens, ones);
  119. }
  120. if (tens === 0 && ones === 0) {
  121. return `${ONES[hundreds]} ${HUNDRED}` as const;
  122. }
  123. return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones)}` as const;
  124. };
  125. const makeMillionsPrefix = (millions: number, milliaCount: number) => {
  126. if (milliaCount > 0) {
  127. return MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
  128. }
  129. return MILLIONS_SPECIAL_PREFIXES[millions] as Exclude<MillionsSpecialPrefix, ''>;
  130. };
  131. const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => {
  132. if (decillions === 0) {
  133. return makeMillionsPrefix(millions, milliaCount);
  134. }
  135. const onesPrefix = MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
  136. const tensName = DECILLIONS_PREFIXES[decillions] as Exclude<DecillionsPrefix, ''>;
  137. return `${onesPrefix}${tensName}` as const;
  138. };
  139. const makeCentillionsPrefix = (centillions: number, decillions: number, millions: number, milliaCount: number) => {
  140. if (centillions === 0) {
  141. return makeDecillionsPrefix(decillions, millions, milliaCount);
  142. }
  143. const onesPrefix = MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
  144. const tensName = DECILLIONS_PREFIXES[decillions] as Exclude<DecillionsPrefix, ''>;
  145. const hundredsName = CENTILLIONS_PREFIXES[centillions] as Exclude<CentillionsPrefix, ''>;
  146. return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
  147. };
  148. const getGroupName = (place: number, shortenMillia: boolean) => {
  149. if (place === 0) {
  150. return '' as const;
  151. }
  152. if (place === 1) {
  153. return THOUSAND;
  154. }
  155. const bigGroupPlace = place - 1;
  156. const groupGroups = bigGroupPlace
  157. .toString()
  158. .split('')
  159. .reduceRight<Group[]>(
  160. (acc, c, i, cc) => {
  161. const firstGroup = acc.at(0);
  162. const currentPlace = Math.floor((cc.length - i - 1) / 3);
  163. if (typeof firstGroup === 'undefined') {
  164. return [[c, currentPlace]];
  165. }
  166. if (firstGroup[0].length > 2) {
  167. return [[c, currentPlace], ...acc];
  168. }
  169. return [
  170. [c + firstGroup[0], currentPlace],
  171. ...acc.slice(1)
  172. ];
  173. },
  174. []
  175. )
  176. .map(([group, groupPlace]) => [group.padStart(3, '0'), groupPlace] as const)
  177. .filter(([group]) => group !== '000')
  178. .map(([group, groupPlace]) => {
  179. const [hundreds, tens, ones] = group.split('').map(Number);
  180. if (groupPlace < 1) {
  181. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace);
  182. }
  183. const milliaSuffix = (
  184. shortenMillia && groupPlace > 1
  185. ? `${MILLIA_PREFIX}^${groupPlace}`
  186. : MILLIA_PREFIX.repeat(groupPlace)
  187. );
  188. if (group === '001') {
  189. return milliaSuffix;
  190. }
  191. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix;
  192. })
  193. .join('');
  194. if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
  195. return `${groupGroups}${ILLION_SUFFIX}` as const;
  196. }
  197. if (bigGroupPlace > 10) {
  198. return `${groupGroups}t${ILLION_SUFFIX}` as const;
  199. }
  200. return `${groupGroups}${ILLION_SUFFIX}` as const;
  201. };
  202. export const makeGroup = (group: string, place: number, options?: Record<string, unknown>): string => {
  203. const makeHundredsArgs = group
  204. .padStart(3, '0')
  205. .split('')
  206. .map((s) => Number(s)) as [number, number, number];
  207. const groupDigitsName = makeHundredsName(...makeHundredsArgs);
  208. const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false);
  209. if (groupName.length > 0) {
  210. return `${groupDigitsName} ${groupName}` as const;
  211. }
  212. return groupDigitsName;
  213. };
  214. /**
  215. * Group a number string into groups of three digits, starting from the decimal point.
  216. * @param value - The number string to group.
  217. */
  218. export const group = (value: string): Group[] => {
  219. const exponentDelimiter = 'e';
  220. const [significand, exponentString] = numberToExponential(
  221. value,
  222. {
  223. decimalPoint: DECIMAL_POINT,
  224. groupingSymbol: GROUPING_SYMBOL,
  225. exponentDelimiter,
  226. })
  227. .split(exponentDelimiter);
  228. const exponent = Number(exponentString);
  229. const significantDigits = significand.replace(DECIMAL_POINT, '');
  230. return significantDigits.split('').reduce<Group[]>(
  231. (acc, c, i) => {
  232. const currentPlace = Math.floor((exponent - i) / 3);
  233. const lastGroup = acc.at(-1) ?? ['000', currentPlace];
  234. const currentPlaceInGroup = 2 - ((exponent - i) % 3);
  235. if (lastGroup[1] === currentPlace) {
  236. const lastGroupDigits = lastGroup[0].split('');
  237. lastGroupDigits[currentPlaceInGroup] = c;
  238. return [...acc.slice(0, -1) ?? [], [
  239. lastGroupDigits.join(''),
  240. currentPlace
  241. ]];
  242. }
  243. return [...acc, [c.padEnd(3, '0'), currentPlace]];
  244. },
  245. [],
  246. );
  247. };
  248. /**
  249. * Formats the final tokenized string.
  250. * @param tokens - The tokens to finalize.
  251. */
  252. export const finalize = (tokens: string[]) => (
  253. tokens
  254. .map((t) => t.trim())
  255. .join(' ')
  256. .trim()
  257. );
  258. /**
  259. * Makes a negative string.
  260. * @param s - The string to make negative.
  261. */
  262. export const makeNegative = (s: string) => (
  263. `${NEGATIVE} ${s}`
  264. );
  265. export const tokenize = (s: string) => (
  266. s.split(' ').filter((s) => s.length > 0)
  267. );
  268. const FINAL_TOKEN = '';
  269. const getGroupFromGroupName = (groupName: string) => {
  270. if (groupName === THOUSAND) {
  271. return 1;
  272. }
  273. const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
  274. const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => {
  275. return groupNameBase === p;
  276. });
  277. if (specialMillions > -1) {
  278. return 1 + specialMillions;
  279. }
  280. let groupNameCurrent = groupNameBase;
  281. let millias = [0];
  282. let milliaIndex = 0;
  283. while (groupNameCurrent.length > 0) {
  284. if (groupNameCurrent === 't') {
  285. break;
  286. }
  287. const centillions = CENTILLIONS_PREFIXES.findIndex((p) => {
  288. return p.length > 0 && groupNameCurrent.startsWith(p);
  289. });
  290. if (centillions > -1) {
  291. milliaIndex = 0;
  292. millias[milliaIndex] += (centillions * 100);
  293. groupNameCurrent = groupNameCurrent.slice(CENTILLIONS_PREFIXES[centillions].length);
  294. continue;
  295. }
  296. const decillions = DECILLIONS_PREFIXES.findIndex((p) => {
  297. return p.length > 0 && groupNameCurrent.startsWith(p);
  298. });
  299. if (decillions > -1) {
  300. milliaIndex = 0;
  301. millias[milliaIndex] += decillions * 10;
  302. groupNameCurrent = groupNameCurrent.slice(DECILLIONS_PREFIXES[decillions].length);
  303. continue;
  304. }
  305. const millions = MILLIONS_PREFIXES.findIndex((p) => {
  306. return p.length > 0 && groupNameCurrent.startsWith(p);
  307. });
  308. if (millions > -1) {
  309. milliaIndex = 0;
  310. millias[milliaIndex] += millions;
  311. groupNameCurrent = groupNameCurrent.slice(MILLIONS_PREFIXES[millions].length);
  312. continue;
  313. }
  314. if (groupNameCurrent.startsWith(`${MILLIA_PREFIX}^`)) {
  315. // short millia
  316. groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
  317. const matchedMilliaArray = groupNameCurrent.match(/^\d+/);
  318. if (!matchedMilliaArray) {
  319. throw new Error(`Invalid groupName: ${groupName}`);
  320. }
  321. const matchedMillia = matchedMilliaArray[0];
  322. millias[Number(matchedMillia)] = millias[milliaIndex] || 1;
  323. millias[milliaIndex] = 0;
  324. groupNameCurrent = groupNameCurrent.slice(matchedMillia.length);
  325. }
  326. if (groupNameCurrent.startsWith(MILLIA_PREFIX)) {
  327. millias[milliaIndex + 1] = millias[milliaIndex] || 1;
  328. millias[milliaIndex] = 0;
  329. milliaIndex += 1;
  330. groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
  331. continue;
  332. }
  333. break;
  334. }
  335. const bigGroupPlace = Number(
  336. millias
  337. .map((s) => s.toString().padStart(3, '0'))
  338. .reverse()
  339. .join('')
  340. );
  341. return 1 + bigGroupPlace;
  342. };
  343. enum ParseGroupsMode {
  344. INITIAL = 'unknown',
  345. ONES = 'ones',
  346. TENS = 'tens',
  347. TEN_PLUS_ONES = 'tenPlusOnes',
  348. HUNDRED = 'hundred',
  349. THOUSAND = 'thousand',
  350. DONE = 'done',
  351. }
  352. interface ParserState {
  353. lastToken?: string;
  354. groups: Group[];
  355. mode: ParseGroupsMode;
  356. }
  357. export const parseGroups = (tokens: string[]) => {
  358. const { groups } = [...tokens, FINAL_TOKEN].reduce<ParserState>(
  359. (acc, token) => {
  360. const lastGroup = acc.groups.at(-1) ?? ['000', 0];
  361. if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
  362. if (acc.mode === ParseGroupsMode.ONES) {
  363. const ones = ONES.findIndex((o) => o === acc.lastToken);
  364. lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
  365. }
  366. lastGroup[1] = getGroupFromGroupName(token);
  367. return {
  368. ...acc,
  369. groups: [...acc.groups.slice(0, -1), lastGroup],
  370. lastToken: token,
  371. mode: ParseGroupsMode.THOUSAND,
  372. }
  373. }
  374. if (token === HUNDRED) {
  375. if (acc.mode === ParseGroupsMode.ONES) {
  376. const hundreds = ONES.findIndex((o) => o === acc.lastToken);
  377. lastGroup[0] = `${hundreds}${lastGroup[0].slice(1)}`;
  378. return {
  379. ...acc,
  380. groups: [...acc.groups.slice(0, -1), lastGroup],
  381. mode: ParseGroupsMode.HUNDRED,
  382. };
  383. }
  384. }
  385. if (token === FINAL_TOKEN) {
  386. if (acc.mode === ParseGroupsMode.ONES) {
  387. const ones = ONES.findIndex((o) => o === acc.lastToken);
  388. lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
  389. lastGroup[1] = 0;
  390. return {
  391. ...acc,
  392. groups: [...acc.groups.slice(0, -1), lastGroup],
  393. mode: ParseGroupsMode.DONE,
  394. };
  395. }
  396. }
  397. if (ONES.includes(token as OnesName)) {
  398. if (acc.mode === ParseGroupsMode.THOUSAND) {
  399. return {
  400. ...acc,
  401. lastToken: token,
  402. mode: ParseGroupsMode.ONES,
  403. groups: [...acc.groups, ['000', 0]],
  404. }
  405. }
  406. return {
  407. ...acc,
  408. lastToken: token,
  409. mode: ParseGroupsMode.ONES,
  410. }
  411. }
  412. const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
  413. if (tenPlusOnes > -1) {
  414. lastGroup[0] = `${lastGroup[0].slice(0, 1)}1${tenPlusOnes}`;
  415. return {
  416. ...acc,
  417. lastToken: token,
  418. mode: ParseGroupsMode.TEN_PLUS_ONES,
  419. groups: [...acc.groups.slice(0, -1), lastGroup]
  420. };
  421. }
  422. const tens = TENS.findIndex((t) => t === token);
  423. if (tens > -1) {
  424. lastGroup[0] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[0].slice(2)}`;
  425. return {
  426. ...acc,
  427. lastToken: token,
  428. mode: ParseGroupsMode.TENS,
  429. groups: [...acc.groups.slice(0, -1), lastGroup],
  430. };
  431. }
  432. return {
  433. ...acc,
  434. lastToken: token,
  435. };
  436. },
  437. {
  438. lastToken: undefined,
  439. groups: [],
  440. mode: ParseGroupsMode.INITIAL,
  441. },
  442. );
  443. return groups;
  444. };
  445. export const combineGroups = (groups: Group[]) => {
  446. const groupsSorted = groups.sort((a, b) => b[1] - a[1]); // sort by place
  447. const firstGroup = groupsSorted[0];
  448. const firstGroupPlace = firstGroup[1];
  449. const digits = groupsSorted.reduce(
  450. (s, group) => {
  451. const [groupDigits] = group;
  452. return `${s}${groupDigits}`;
  453. },
  454. ''
  455. ).replace(/^0+/, '') || '0';
  456. const firstGroupDigits = firstGroup[0];
  457. const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, '');
  458. const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length;
  459. const exponentValue = BigInt((firstGroupPlace * 3) + (2 - exponentExtra));
  460. const exponent = exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`;
  461. const significandInteger = digits.slice(0, 1);
  462. const significandFraction = digits.slice(1);
  463. if (significandFraction.length > 0) {
  464. return `${significandInteger}${DECIMAL_POINT}${significandFraction}e${exponent}`;
  465. }
  466. return `${significandInteger}e${exponent}`;
  467. };