Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 

346 řádky
9.6 KiB

  1. import {
  2. Group,
  3. GROUP_DIGITS_INDEX,
  4. GROUP_PLACE_INDEX, GroupDigits,
  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 '../../en/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 currentMillia - Current millia- group.
  75. * @param longestMilliaCount - Number of millia- groups.
  76. * @returns string The millions prefix.
  77. */
  78. const makeMillionsPrefix = (
  79. millions: number,
  80. currentMillia: GroupPlace,
  81. longestMilliaCount: GroupPlace,
  82. ) => {
  83. if (currentMillia === BigInt(0) && longestMilliaCount === BigInt(0)) {
  84. return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix;
  85. }
  86. return MILLIONS_PREFIXES[millions] as MillionsPrefix;
  87. };
  88. /**
  89. * Builds a name for numbers in the decillions.
  90. * @param decillions - Decillions digit.
  91. * @param millions - Millions digit.
  92. * @param currentMillia - Current millia- group.
  93. * @param longestMilliaCount - Number of millia- groups.
  94. * @returns string The decillions prefix.
  95. */
  96. const makeDecillionsPrefix = (
  97. decillions: number,
  98. millions: number,
  99. currentMillia: GroupPlace,
  100. longestMilliaCount: GroupPlace,
  101. ) => {
  102. if (decillions === 0) {
  103. return makeMillionsPrefix(millions, currentMillia, longestMilliaCount);
  104. }
  105. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  106. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  107. return `${onesPrefix}${tensName}` as const;
  108. };
  109. /**
  110. * Builds a name for numbers in the centillions.
  111. * @param centillions - Centillions digit.
  112. * @param decillions - Decillions digit.
  113. * @param millions - Millions digit.
  114. * @param currentMillia - Current millia- group.
  115. * @param longestMilliaCount - Number of millia- groups.
  116. * @returns string The centillions prefix.
  117. */
  118. const makeCentillionsPrefix = (
  119. centillions: number,
  120. decillions: number,
  121. millions: number,
  122. currentMillia: GroupPlace,
  123. longestMilliaCount: GroupPlace,
  124. ) => {
  125. if (centillions === 0) {
  126. return makeDecillionsPrefix(decillions, millions, currentMillia, longestMilliaCount);
  127. }
  128. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  129. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  130. const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix;
  131. return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
  132. };
  133. /**
  134. * Repeats a string a given number of times.
  135. * @param s - String to repeat.
  136. * @param count - Number of times to repeat the string.
  137. * @returns string The repeated string.
  138. */
  139. const repeatString = (s: string, count: GroupPlace) => {
  140. let result = '';
  141. for (let i = BigInt(0); i < count; i += BigInt(1)) {
  142. result += s;
  143. }
  144. return result;
  145. };
  146. const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
  147. if (place === BigInt(0)) {
  148. return '' as const;
  149. }
  150. if (place === BigInt(1)) {
  151. return THOUSAND;
  152. }
  153. const isThousand = place % BigInt(2) === BigInt(1);
  154. const bigGroupPlace = place / BigInt(2);
  155. const groupGroups = bigGroupPlace
  156. .toString()
  157. .split('')
  158. .reduceRight<Group[]>(
  159. (acc, c, i, cc) => {
  160. const firstGroup = acc.at(0);
  161. const currentPlace = BigInt(Math.floor((cc.length - i - 1) / 3));
  162. const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
  163. if (typeof firstGroup === 'undefined') {
  164. newGroup[GROUP_DIGITS_INDEX] = c as GroupDigits;
  165. return [newGroup];
  166. }
  167. if (firstGroup[0].length > 2) {
  168. newGroup[GROUP_DIGITS_INDEX] = c as GroupDigits;
  169. return [newGroup, ...acc];
  170. }
  171. newGroup[GROUP_DIGITS_INDEX] = (c + firstGroup[0]) as GroupDigits;
  172. return [
  173. newGroup,
  174. ...acc.slice(1),
  175. ];
  176. },
  177. [],
  178. )
  179. .map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const)
  180. .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
  181. .map(([groupDigits, groupPlace], _index, millias) => {
  182. const [hundreds, tens, ones] = groupDigits.split('').map(Number);
  183. const largestMillia = millias[0][GROUP_PLACE_INDEX];
  184. const centillionsPrefix = makeCentillionsPrefix(
  185. hundreds,
  186. tens,
  187. ones,
  188. groupPlace,
  189. largestMillia,
  190. );
  191. if (groupPlace < 1) {
  192. return centillionsPrefix;
  193. }
  194. const milliaSuffix = (
  195. shortenMillia && groupPlace > 1
  196. ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}${SHORT_MILLIA_ILLION_DELIMITER}`
  197. : repeatString(MILLIA_PREFIX, groupPlace)
  198. );
  199. if (groupDigits === '001' && groupPlace === largestMillia) {
  200. return milliaSuffix;
  201. }
  202. return `${centillionsPrefix}${milliaSuffix}`;
  203. })
  204. .join('');
  205. if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
  206. return `${isThousand ? `${THOUSAND} ` : ''}${groupGroups}${ILLION_SUFFIX}` as const;
  207. }
  208. if (bigGroupPlace > 10) {
  209. // vigin - t - illion, cen - t - illion, etc.
  210. return `${isThousand ? `${THOUSAND} ` : ''}${groupGroups}${T_AFFIX}${ILLION_SUFFIX}` as const;
  211. }
  212. return `${isThousand ? `${THOUSAND} ` : ''}${groupGroups}${ILLION_SUFFIX}` as const;
  213. };
  214. export interface StringifyGroupsOptions {
  215. /**
  216. * Whether to add dashes between tens and ones (e.g. "sixty-nine").
  217. */
  218. addTensDashes?: boolean;
  219. /**
  220. * Use "millia^2-tillion" instead of "milliamilliatillion".
  221. */
  222. shortenMillia?: boolean;
  223. }
  224. /**
  225. * Creates a group string.
  226. * @param groups - The groups.
  227. * @param options - Options to use when creating the group.
  228. * @returns string[] The groups represented into strings.
  229. */
  230. export const stringifyGroups = (groups: Group[], options?: StringifyGroupsOptions): string[] => {
  231. const filteredGroups = groups.filter(([digits, place]) => (
  232. place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS
  233. ));
  234. return filteredGroups.map(
  235. ([group, place]) => {
  236. const makeHundredsArgs = group
  237. .padStart(3, '0')
  238. .split('')
  239. .map((s) => Number(s)) as [number, number, number];
  240. const groupDigitsName = makeHundredsName(
  241. ...makeHundredsArgs,
  242. options?.addTensDashes ?? true,
  243. );
  244. const groupName = getGroupName(place, options?.shortenMillia ?? false);
  245. if (groupName.length > 0) {
  246. return `${groupDigitsName} ${groupName}`;
  247. }
  248. return groupDigitsName;
  249. },
  250. );
  251. };
  252. /**
  253. * Group a number string into groups of three digits, starting from the decimal point.
  254. * @param value - The number string to group.
  255. * @returns Group[] The groups.
  256. */
  257. export const splitIntoGroups = (value: string): Group[] => {
  258. const [significand, exponentString] = numberToExponential(
  259. value,
  260. {
  261. decimalPoint: DECIMAL_POINT,
  262. groupingSymbol: GROUPING_SYMBOL,
  263. exponentDelimiter: EXPONENT_DELIMITER,
  264. },
  265. )
  266. .split(EXPONENT_DELIMITER);
  267. // FIXME use bigint for exponent and indexing???
  268. const exponent = Number(exponentString);
  269. const significantDigits = significand.replace(new RegExp(`\\${DECIMAL_POINT}`, 'g'), '');
  270. return significantDigits.split('').reduce<Group[]>(
  271. (acc, c, i) => {
  272. const currentPlace = BigInt(Math.floor((exponent - i) / 3));
  273. const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
  274. const currentPlaceInGroup = 2 - ((exponent - i) % 3);
  275. if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
  276. const lastGroupDigits = lastGroup[0].split('');
  277. lastGroupDigits[currentPlaceInGroup] = c;
  278. return [...acc.slice(0, -1) ?? [], [
  279. lastGroupDigits.join('') as GroupDigits,
  280. currentPlace,
  281. ]];
  282. }
  283. return [...acc, [c.padEnd(3, '0') as GroupDigits, currentPlace]];
  284. },
  285. [],
  286. );
  287. };
  288. export interface MergeTokensOptions {
  289. oneGroupPerLine?: boolean;
  290. }
  291. /**
  292. * Formats the final tokenized string.
  293. * @param tokens - The tokens to finalize.
  294. * @param options - The options to use.
  295. */
  296. export const mergeTokens = (tokens: string[], options?: MergeTokensOptions) => (
  297. tokens
  298. .map((t) => t.trim())
  299. .join(options?.oneGroupPerLine ? '\n' : GROUP_SEPARATOR)
  300. .trim()
  301. );
  302. /**
  303. * Makes a negative string.
  304. * @param s - The string to make negative.
  305. */
  306. export const makeNegative = (s: string) => {
  307. const negativePrefix = `${NEGATIVE} `;
  308. return s.startsWith(negativePrefix) ? s.slice(negativePrefix.length) : `${negativePrefix}${s}`;
  309. };