Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 

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