Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

330 lignes
8.8 KiB

  1. import {
  2. Group,
  3. GROUP_DIGITS_INDEX,
  4. GROUP_PLACE_INDEX,
  5. GroupPlace,
  6. } from '../../../common';
  7. import { numberToExponential } from '../../../exponent';
  8. import {
  9. CENTILLIONS_PREFIXES,
  10. CentillionsPrefix,
  11. DECILLIONS_PREFIXES,
  12. DecillionsPrefix,
  13. DECIMAL_POINT,
  14. EMPTY_GROUP_DIGITS,
  15. EXPONENT_DELIMITER, GROUP_SEPARATOR,
  16. GROUPING_SYMBOL,
  17. HUNDRED,
  18. ILLION_SUFFIX,
  19. MILLIA_PREFIX,
  20. MILLIONS_PREFIXES,
  21. MILLIONS_SPECIAL_PREFIXES,
  22. MillionsPrefix,
  23. MillionsSpecialPrefix,
  24. NEGATIVE,
  25. ONES,
  26. OnesName,
  27. SHORT_MILLIA_DELIMITER, SHORT_MILLIA_ILLION_DELIMITER, T_AFFIX,
  28. TEN_PLUS_ONES,
  29. TenPlusOnesName,
  30. TENS,
  31. TENS_ONES_SEPARATOR,
  32. TensName,
  33. THOUSAND,
  34. } from '../common';
  35. /**
  36. * Builds a name for numbers in tens and ones.
  37. * @param tens - Tens digit.
  38. * @param ones - Ones digit.
  39. * @param addTensDashes - Whether to add dashes between the tens and ones.
  40. * @returns string The name for the number.
  41. */
  42. const makeTensName = (tens: number, ones: number, addTensDashes: boolean) => {
  43. if (tens === 0) {
  44. return ONES[ones];
  45. }
  46. if (tens === 1) {
  47. return TEN_PLUS_ONES[ones] as unknown as TenPlusOnesName;
  48. }
  49. if (ones === 0) {
  50. return TENS[tens];
  51. }
  52. return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${addTensDashes ? TENS_ONES_SEPARATOR : ' '}${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
  53. };
  54. /**
  55. * Builds a name for numbers in hundreds, tens, and ones.
  56. * @param hundreds - Hundreds digit.
  57. * @param tens - Tens digit.
  58. * @param ones - Ones digit.
  59. * @param addTensDashes - Whether to add dashes between the tens and ones.
  60. * @returns string The name for the number.
  61. */
  62. const makeHundredsName = (hundreds: number, tens: number, ones: number, addTensDashes: boolean) => {
  63. if (hundreds === 0) {
  64. return makeTensName(tens, ones, addTensDashes);
  65. }
  66. if (tens === 0 && ones === 0) {
  67. return `${ONES[hundreds]} ${HUNDRED}` as const;
  68. }
  69. return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones, addTensDashes)}` as const;
  70. };
  71. /**
  72. * Builds a name for numbers in the millions.
  73. * @param millions - Millions digit.
  74. * @param longestMilliaCount - Number of millia- groups.
  75. * @returns string The millions prefix.
  76. */
  77. const makeMillionsPrefix = (millions: number, longestMilliaCount: GroupPlace) => {
  78. if (longestMilliaCount > 0) {
  79. return MILLIONS_PREFIXES[millions] as MillionsPrefix;
  80. }
  81. return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix;
  82. };
  83. /**
  84. * Builds a name for numbers in the decillions.
  85. * @param decillions - Decillions digit.
  86. * @param millions - Millions digit.
  87. * @param longestMilliaCount - Number of millia- groups.
  88. * @returns string The decillions prefix.
  89. */
  90. const makeDecillionsPrefix = (
  91. decillions: number,
  92. millions: number,
  93. longestMilliaCount: GroupPlace,
  94. ) => {
  95. if (decillions === 0) {
  96. return makeMillionsPrefix(millions, longestMilliaCount);
  97. }
  98. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  99. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  100. return `${onesPrefix}${tensName}` as const;
  101. };
  102. /**
  103. * Builds a name for numbers in the centillions.
  104. * @param centillions - Centillions digit.
  105. * @param decillions - Decillions digit.
  106. * @param millions - Millions digit.
  107. * @param longestMilliaCount - Number of millia- groups.
  108. * @returns string The centillions prefix.
  109. */
  110. const makeCentillionsPrefix = (
  111. centillions: number,
  112. decillions: number,
  113. millions: number,
  114. longestMilliaCount: GroupPlace,
  115. ) => {
  116. if (centillions === 0) {
  117. return makeDecillionsPrefix(decillions, millions, longestMilliaCount);
  118. }
  119. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  120. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  121. const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix;
  122. return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
  123. };
  124. /**
  125. * Repeats a string a given number of times.
  126. * @param s - String to repeat.
  127. * @param count - Number of times to repeat the string.
  128. * @returns string The repeated string.
  129. */
  130. const repeatString = (s: string, count: GroupPlace) => {
  131. let result = '';
  132. for (let i = BigInt(0); i < count; i += BigInt(1)) {
  133. result += s;
  134. }
  135. return result;
  136. };
  137. const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
  138. if (place === BigInt(0)) {
  139. return '' as const;
  140. }
  141. if (place === BigInt(1)) {
  142. return THOUSAND;
  143. }
  144. const bigGroupPlace = place - BigInt(1);
  145. const groupGroups = bigGroupPlace
  146. .toString()
  147. .split('')
  148. .reduceRight<Group[]>(
  149. (acc, c, i, cc) => {
  150. const firstGroup = acc.at(0);
  151. const currentPlace = BigInt(Math.floor((cc.length - i - 1) / 3));
  152. const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
  153. if (typeof firstGroup === 'undefined') {
  154. newGroup[GROUP_DIGITS_INDEX] = c;
  155. return [newGroup];
  156. }
  157. if (firstGroup[0].length > 2) {
  158. newGroup[GROUP_DIGITS_INDEX] = c;
  159. return [newGroup, ...acc];
  160. }
  161. newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
  162. return [
  163. newGroup,
  164. ...acc.slice(1),
  165. ];
  166. },
  167. [],
  168. )
  169. .map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const)
  170. .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
  171. .map(([groupDigits, groupPlace], _index, millias) => {
  172. const [hundreds, tens, ones] = groupDigits.split('').map(Number);
  173. const centillionsPrefix = makeCentillionsPrefix(
  174. hundreds,
  175. tens,
  176. ones,
  177. BigInt(millias.length - 1)
  178. );
  179. if (groupPlace < 1) {
  180. return centillionsPrefix;
  181. }
  182. const milliaSuffix = (
  183. shortenMillia && groupPlace > 1
  184. ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}${SHORT_MILLIA_ILLION_DELIMITER}`
  185. : repeatString(MILLIA_PREFIX, groupPlace)
  186. );
  187. if (groupDigits === '001') {
  188. return milliaSuffix;
  189. }
  190. return `${centillionsPrefix}${milliaSuffix}`;
  191. })
  192. .join('');
  193. if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
  194. return `${groupGroups}${ILLION_SUFFIX}` as const;
  195. }
  196. if (bigGroupPlace > 10) {
  197. // vigin - t - illion, cen - t - illion, etc.
  198. return `${groupGroups}${T_AFFIX}${ILLION_SUFFIX}` as const;
  199. }
  200. return `${groupGroups}${ILLION_SUFFIX}` as const;
  201. };
  202. export interface StringifyGroupsOptions {
  203. /**
  204. * Whether to add dashes between tens and ones (e.g. "sixty-nine").
  205. */
  206. addTensDashes?: boolean;
  207. /**
  208. * Use "millia^2-tillion" instead of "milliamilliatillion".
  209. */
  210. shortenMillia?: boolean;
  211. }
  212. /**
  213. * Creates a group string.
  214. * @param groups - The groups.
  215. * @param options - Options to use when creating the group.
  216. * @returns string[] The groups represented into strings.
  217. */
  218. export const stringifyGroups = (groups: Group[], options?: StringifyGroupsOptions): string[] => {
  219. const filteredGroups = groups.filter(([digits, place]) => (
  220. place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS
  221. ));
  222. return filteredGroups.map(
  223. ([group, place]) => {
  224. const makeHundredsArgs = group
  225. .padStart(3, '0')
  226. .split('')
  227. .map((s) => Number(s)) as [number, number, number];
  228. const groupDigitsName = makeHundredsName(
  229. ...makeHundredsArgs,
  230. options?.addTensDashes ?? true,
  231. );
  232. const groupName = getGroupName(place, options?.shortenMillia ?? false);
  233. if (groupName.length > 0) {
  234. return `${groupDigitsName} ${groupName}`;
  235. }
  236. return groupDigitsName;
  237. },
  238. );
  239. };
  240. /**
  241. * Group a number string into groups of three digits, starting from the decimal point.
  242. * @param value - The number string to group.
  243. * @returns Group[] The groups.
  244. */
  245. export const splitIntoGroups = (value: string): Group[] => {
  246. const [significand, exponentString] = numberToExponential(
  247. value,
  248. {
  249. decimalPoint: DECIMAL_POINT,
  250. groupingSymbol: GROUPING_SYMBOL,
  251. exponentDelimiter: EXPONENT_DELIMITER,
  252. },
  253. )
  254. .split(EXPONENT_DELIMITER);
  255. const exponent = Number(exponentString);
  256. const significantDigits = significand.replace(new RegExp(`\\${DECIMAL_POINT}`, 'g'), '');
  257. return significantDigits.split('').reduce<Group[]>(
  258. (acc, c, i) => {
  259. const currentPlace = BigInt(Math.floor((exponent - i) / 3));
  260. const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
  261. const currentPlaceInGroup = 2 - ((exponent - i) % 3);
  262. if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
  263. const lastGroupDigits = lastGroup[0].split('');
  264. lastGroupDigits[currentPlaceInGroup] = c;
  265. return [...acc.slice(0, -1) ?? [], [
  266. lastGroupDigits.join(''),
  267. currentPlace,
  268. ]];
  269. }
  270. return [...acc, [c.padEnd(3, '0'), currentPlace]];
  271. },
  272. [],
  273. );
  274. };
  275. export interface MergeTokensOptions {
  276. oneGroupPerLine?: boolean;
  277. }
  278. /**
  279. * Formats the final tokenized string.
  280. * @param tokens - The tokens to finalize.
  281. * @param options - The options to use.
  282. */
  283. export const mergeTokens = (tokens: string[], options?: MergeTokensOptions) => (
  284. tokens
  285. .map((t) => t.trim())
  286. .join(options?.oneGroupPerLine ? '\n' : GROUP_SEPARATOR)
  287. .trim()
  288. );
  289. /**
  290. * Makes a negative string.
  291. * @param s - The string to make negative.
  292. */
  293. export const makeNegative = (s: string) => (
  294. `${NEGATIVE} ${s}`
  295. );