Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

562 行
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 unknown 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 = (
  140. centillions: number,
  141. decillions: number,
  142. millions: number,
  143. milliaCount: number,
  144. ) => {
  145. if (centillions === 0) {
  146. return makeDecillionsPrefix(decillions, millions, milliaCount);
  147. }
  148. const onesPrefix = MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
  149. const tensName = DECILLIONS_PREFIXES[decillions] as Exclude<DecillionsPrefix, ''>;
  150. const hundredsName = CENTILLIONS_PREFIXES[centillions] as Exclude<CentillionsPrefix, ''>;
  151. return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
  152. };
  153. const getGroupName = (place: number, shortenMillia: boolean) => {
  154. if (place === 0) {
  155. return '' as const;
  156. }
  157. if (place === 1) {
  158. return THOUSAND;
  159. }
  160. const bigGroupPlace = place - 1;
  161. const groupGroups = bigGroupPlace
  162. .toString()
  163. .split('')
  164. .reduceRight<Group[]>(
  165. (acc, c, i, cc) => {
  166. const firstGroup = acc.at(0);
  167. const currentPlace = Math.floor((cc.length - i - 1) / 3);
  168. if (typeof firstGroup === 'undefined') {
  169. return [[c, currentPlace]];
  170. }
  171. if (firstGroup[0].length > 2) {
  172. return [[c, currentPlace], ...acc];
  173. }
  174. return [
  175. [c + firstGroup[0], currentPlace],
  176. ...acc.slice(1),
  177. ];
  178. },
  179. [],
  180. )
  181. .map(([group, groupPlace]) => [group.padStart(3, '0'), groupPlace] as const)
  182. .filter(([group]) => group !== '000')
  183. .map(([group, groupPlace]) => {
  184. const [hundreds, tens, ones] = group.split('').map(Number);
  185. if (groupPlace < 1) {
  186. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace);
  187. }
  188. const milliaSuffix = (
  189. shortenMillia && groupPlace > 1
  190. ? `${MILLIA_PREFIX}^${groupPlace}`
  191. : MILLIA_PREFIX.repeat(groupPlace)
  192. );
  193. if (group === '001') {
  194. return milliaSuffix;
  195. }
  196. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix;
  197. })
  198. .join('');
  199. if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
  200. return `${groupGroups}${ILLION_SUFFIX}` as const;
  201. }
  202. if (bigGroupPlace > 10) {
  203. return `${groupGroups}t${ILLION_SUFFIX}` as const;
  204. }
  205. return `${groupGroups}${ILLION_SUFFIX}` as const;
  206. };
  207. export const makeGroup = (
  208. group: string,
  209. place: number,
  210. options?: Record<string, unknown>,
  211. ): string => {
  212. const makeHundredsArgs = group
  213. .padStart(3, '0')
  214. .split('')
  215. .map((s) => Number(s)) as [number, number, number];
  216. const groupDigitsName = makeHundredsName(...makeHundredsArgs);
  217. const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false);
  218. if (groupName.length > 0) {
  219. return `${groupDigitsName} ${groupName}` as const;
  220. }
  221. return groupDigitsName;
  222. };
  223. /**
  224. * Group a number string into groups of three digits, starting from the decimal point.
  225. * @param value - The number string to group.
  226. */
  227. export const group = (value: string): Group[] => {
  228. const exponentDelimiter = 'e';
  229. const [significand, exponentString] = numberToExponential(
  230. value,
  231. {
  232. decimalPoint: DECIMAL_POINT,
  233. groupingSymbol: GROUPING_SYMBOL,
  234. exponentDelimiter,
  235. },
  236. )
  237. .split(exponentDelimiter);
  238. const exponent = Number(exponentString);
  239. const significantDigits = significand.replace(DECIMAL_POINT, '');
  240. return significantDigits.split('').reduce<Group[]>(
  241. (acc, c, i) => {
  242. const currentPlace = Math.floor((exponent - i) / 3);
  243. const lastGroup = acc.at(-1) ?? ['000', currentPlace];
  244. const currentPlaceInGroup = 2 - ((exponent - i) % 3);
  245. if (lastGroup[1] === currentPlace) {
  246. const lastGroupDigits = lastGroup[0].split('');
  247. lastGroupDigits[currentPlaceInGroup] = c;
  248. return [...acc.slice(0, -1) ?? [], [
  249. lastGroupDigits.join(''),
  250. currentPlace,
  251. ]];
  252. }
  253. return [...acc, [c.padEnd(3, '0'), currentPlace]];
  254. },
  255. [],
  256. );
  257. };
  258. /**
  259. * Formats the final tokenized string.
  260. * @param tokens - The tokens to finalize.
  261. */
  262. export const finalize = (tokens: string[]) => (
  263. tokens
  264. .map((t) => t.trim())
  265. .join(' ')
  266. .trim()
  267. );
  268. /**
  269. * Makes a negative string.
  270. * @param s - The string to make negative.
  271. */
  272. export const makeNegative = (s: string) => (
  273. `${NEGATIVE} ${s}`
  274. );
  275. export const tokenize = (stringValue: string) => (
  276. stringValue.split(' ').filter((maybeToken) => maybeToken.length > 0)
  277. );
  278. const FINAL_TOKEN = '';
  279. const getGroupFromGroupName = (groupName: string) => {
  280. if (groupName === THOUSAND) {
  281. return 1;
  282. }
  283. const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
  284. const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);
  285. if (specialMillions > -1) {
  286. return 1 + specialMillions;
  287. }
  288. let groupNameCurrent = groupNameBase;
  289. const millias = [0];
  290. let milliaIndex = 0;
  291. while (groupNameCurrent.length > 0) {
  292. if (groupNameCurrent === 't') {
  293. break;
  294. }
  295. const centillions = CENTILLIONS_PREFIXES.findIndex((p) => (
  296. p.length > 0 && groupNameCurrent.startsWith(p)
  297. ));
  298. if (centillions > -1) {
  299. milliaIndex = 0;
  300. millias[milliaIndex] += (centillions * 100);
  301. groupNameCurrent = groupNameCurrent.slice(CENTILLIONS_PREFIXES[centillions].length);
  302. continue;
  303. }
  304. const decillions = DECILLIONS_PREFIXES.findIndex((p) => (
  305. p.length > 0 && groupNameCurrent.startsWith(p)
  306. ));
  307. if (decillions > -1) {
  308. milliaIndex = 0;
  309. millias[milliaIndex] += decillions * 10;
  310. groupNameCurrent = groupNameCurrent.slice(DECILLIONS_PREFIXES[decillions].length);
  311. continue;
  312. }
  313. const millions = MILLIONS_PREFIXES.findIndex((p) => (
  314. p.length > 0 && groupNameCurrent.startsWith(p)
  315. ));
  316. if (millions > -1) {
  317. milliaIndex = 0;
  318. millias[milliaIndex] += millions;
  319. groupNameCurrent = groupNameCurrent.slice(MILLIONS_PREFIXES[millions].length);
  320. continue;
  321. }
  322. if (groupNameCurrent.startsWith(`${MILLIA_PREFIX}^`)) {
  323. // short millia
  324. groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
  325. const matchedMilliaArray = groupNameCurrent.match(/^\d+/);
  326. if (!matchedMilliaArray) {
  327. throw new Error(`Invalid groupName: ${groupName}`);
  328. }
  329. const matchedMillia = matchedMilliaArray[0];
  330. millias[Number(matchedMillia)] = millias[milliaIndex] || 1;
  331. millias[milliaIndex] = 0;
  332. groupNameCurrent = groupNameCurrent.slice(matchedMillia.length);
  333. }
  334. if (groupNameCurrent.startsWith(MILLIA_PREFIX)) {
  335. millias[milliaIndex + 1] = millias[milliaIndex] || 1;
  336. millias[milliaIndex] = 0;
  337. milliaIndex += 1;
  338. groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
  339. continue;
  340. }
  341. break;
  342. }
  343. const bigGroupPlace = Number(
  344. millias
  345. .map((s) => s.toString().padStart(3, '0'))
  346. .reverse()
  347. .join(''),
  348. );
  349. return 1 + bigGroupPlace;
  350. };
  351. enum ParseGroupsMode {
  352. INITIAL = 'unknown',
  353. ONES_MODE = 'ones',
  354. TENS_MODE = 'tens',
  355. TEN_PLUS_ONES_MODE = 'tenPlusOnes',
  356. HUNDRED_MODE = 'hundred',
  357. THOUSAND_MODE = 'thousand',
  358. DONE = 'done',
  359. }
  360. interface ParserState {
  361. lastToken?: string;
  362. groups: Group[];
  363. mode: ParseGroupsMode;
  364. }
  365. export const parseGroups = (tokens: string[]) => {
  366. const { groups } = [...tokens, FINAL_TOKEN].reduce<ParserState>(
  367. (acc, token) => {
  368. const lastGroup = acc.groups.at(-1) ?? ['000', 0];
  369. if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
  370. if (acc.mode === ParseGroupsMode.ONES_MODE) {
  371. const ones = ONES.findIndex((o) => o === acc.lastToken);
  372. lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
  373. }
  374. lastGroup[1] = getGroupFromGroupName(token);
  375. return {
  376. ...acc,
  377. groups: [...acc.groups.slice(0, -1), lastGroup],
  378. lastToken: token,
  379. mode: ParseGroupsMode.THOUSAND_MODE,
  380. };
  381. }
  382. if (token === HUNDRED) {
  383. if (acc.mode === ParseGroupsMode.ONES_MODE) {
  384. const hundreds = ONES.findIndex((o) => o === acc.lastToken);
  385. lastGroup[0] = `${hundreds}${lastGroup[0].slice(1)}`;
  386. return {
  387. ...acc,
  388. groups: [...acc.groups.slice(0, -1), lastGroup],
  389. mode: ParseGroupsMode.HUNDRED_MODE,
  390. };
  391. }
  392. }
  393. if (token === FINAL_TOKEN) {
  394. if (acc.mode === ParseGroupsMode.ONES_MODE) {
  395. const ones = ONES.findIndex((o) => o === acc.lastToken);
  396. lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
  397. lastGroup[1] = 0;
  398. return {
  399. ...acc,
  400. groups: [...acc.groups.slice(0, -1), lastGroup],
  401. mode: ParseGroupsMode.DONE,
  402. };
  403. }
  404. }
  405. if (ONES.includes(token as OnesName)) {
  406. if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
  407. return {
  408. ...acc,
  409. lastToken: token,
  410. mode: ParseGroupsMode.ONES_MODE,
  411. groups: [...acc.groups, ['000', 0]],
  412. };
  413. }
  414. return {
  415. ...acc,
  416. lastToken: token,
  417. mode: ParseGroupsMode.ONES_MODE,
  418. };
  419. }
  420. const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
  421. if (tenPlusOnes > -1) {
  422. lastGroup[0] = `${lastGroup[0].slice(0, 1)}1${tenPlusOnes}`;
  423. return {
  424. ...acc,
  425. lastToken: token,
  426. mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
  427. groups: [...acc.groups.slice(0, -1), lastGroup],
  428. };
  429. }
  430. const tens = TENS.findIndex((t) => t === token);
  431. if (tens > -1) {
  432. lastGroup[0] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[0].slice(2)}`;
  433. return {
  434. ...acc,
  435. lastToken: token,
  436. mode: ParseGroupsMode.TENS_MODE,
  437. groups: [...acc.groups.slice(0, -1), lastGroup],
  438. };
  439. }
  440. return {
  441. ...acc,
  442. lastToken: token,
  443. };
  444. },
  445. {
  446. lastToken: undefined,
  447. groups: [],
  448. mode: ParseGroupsMode.INITIAL,
  449. },
  450. );
  451. return groups;
  452. };
  453. export const combineGroups = (groups: Group[]) => {
  454. const groupsSorted = groups.sort((a, b) => b[1] - a[1]); // sort by place
  455. const firstGroup = groupsSorted[0];
  456. const firstGroupPlace = firstGroup[1];
  457. const digits = groupsSorted.reduce(
  458. (previousDigits, thisGroup) => {
  459. const [groupDigits] = thisGroup;
  460. return `${previousDigits}${groupDigits}`;
  461. },
  462. '',
  463. ).replace(/^0+/, '') || '0';
  464. const firstGroupDigits = firstGroup[0];
  465. const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, '');
  466. const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length;
  467. const exponentValue = BigInt((firstGroupPlace * 3) + (2 - exponentExtra));
  468. const exponent = exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`;
  469. const significandInteger = digits.slice(0, 1);
  470. const significandFraction = digits.slice(1);
  471. if (significandFraction.length > 0) {
  472. return `${significandInteger}${DECIMAL_POINT}${significandFraction}e${exponent}`;
  473. }
  474. return `${significandInteger}e${exponent}`;
  475. };