Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 

283 rader
7.4 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,
  16. GROUPING_SYMBOL,
  17. HUNDRED,
  18. ILLION_SUFFIX,
  19. MILLIA_PREFIX,
  20. MILLIONS_PREFIXES,
  21. MILLIONS_SPECIAL_PREFIXES,
  22. MillionsPrefix,
  23. MillionsSpecialPrefix, NEGATIVE,
  24. ONES,
  25. OnesName,
  26. SHORT_MILLIA_DELIMITER,
  27. TEN_PLUS_ONES,
  28. TenPlusOnesName,
  29. TENS,
  30. TENS_ONES_SEPARATOR,
  31. TensName,
  32. THOUSAND,
  33. } from './common';
  34. /**
  35. * Builds a name for numbers in tens and ones.
  36. * @param tens - Tens digit.
  37. * @param ones - Ones digit.
  38. * @returns string The name for the number.
  39. */
  40. const makeTensName = (tens: number, ones: number) => {
  41. if (tens === 0) {
  42. return ONES[ones];
  43. }
  44. if (tens === 1) {
  45. return TEN_PLUS_ONES[ones] as unknown as TenPlusOnesName;
  46. }
  47. if (ones === 0) {
  48. return TENS[tens];
  49. }
  50. return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${TENS_ONES_SEPARATOR}${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
  51. };
  52. /**
  53. * Builds a name for numbers in hundreds, tens, and ones.
  54. * @param hundreds - Hundreds digit.
  55. * @param tens - Tens digit.
  56. * @param ones - Ones digit.
  57. * @returns string The name for the number.
  58. */
  59. const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
  60. if (hundreds === 0) {
  61. return makeTensName(tens, ones);
  62. }
  63. if (tens === 0 && ones === 0) {
  64. return `${ONES[hundreds]} ${HUNDRED}` as const;
  65. }
  66. return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones)}` as const;
  67. };
  68. /**
  69. * Builds a name for numbers in the millions.
  70. * @param millions - Millions digit.
  71. * @param milliaCount - Number of millia- groups.
  72. * @returns string The millions prefix.
  73. */
  74. const makeMillionsPrefix = (millions: number, milliaCount: GroupPlace) => {
  75. if (milliaCount > 0) {
  76. return MILLIONS_PREFIXES[millions] as MillionsPrefix;
  77. }
  78. return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix;
  79. };
  80. /**
  81. * Builds a name for numbers in the decillions.
  82. * @param decillions - Decillions digit.
  83. * @param millions - Millions digit.
  84. * @param milliaCount - Number of millia- groups.
  85. * @returns string The decillions prefix.
  86. */
  87. const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: GroupPlace) => {
  88. if (decillions === 0) {
  89. return makeMillionsPrefix(millions, milliaCount);
  90. }
  91. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  92. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  93. return `${onesPrefix}${tensName}` as const;
  94. };
  95. /**
  96. * Builds a name for numbers in the centillions.
  97. * @param centillions - Centillions digit.
  98. * @param decillions - Decillions digit.
  99. * @param millions - Millions digit.
  100. * @param milliaCount - Number of millia- groups.
  101. * @returns string The centillions prefix.
  102. */
  103. const makeCentillionsPrefix = (
  104. centillions: number,
  105. decillions: number,
  106. millions: number,
  107. milliaCount: GroupPlace,
  108. ) => {
  109. if (centillions === 0) {
  110. return makeDecillionsPrefix(decillions, millions, milliaCount);
  111. }
  112. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  113. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  114. const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix;
  115. return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
  116. };
  117. const repeatString = (s: string, count: GroupPlace) => {
  118. let result = '';
  119. for (let i = BigInt(0); i < count; i += BigInt(1)) {
  120. result += s;
  121. }
  122. return result;
  123. };
  124. const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
  125. if (place === BigInt(0)) {
  126. return '' as const;
  127. }
  128. if (place === BigInt(1)) {
  129. return THOUSAND;
  130. }
  131. const bigGroupPlace = place - BigInt(1);
  132. const groupGroups = bigGroupPlace
  133. .toString()
  134. .split('')
  135. .reduceRight<Group[]>(
  136. (acc, c, i, cc) => {
  137. const firstGroup = acc.at(0);
  138. const currentPlace = BigInt(Math.floor((cc.length - i - 1) / 3));
  139. const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
  140. if (typeof firstGroup === 'undefined') {
  141. newGroup[GROUP_DIGITS_INDEX] = c;
  142. return [newGroup];
  143. }
  144. if (firstGroup[0].length > 2) {
  145. newGroup[GROUP_DIGITS_INDEX] = c;
  146. return [newGroup, ...acc];
  147. }
  148. newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
  149. return [
  150. newGroup,
  151. ...acc.slice(1),
  152. ];
  153. },
  154. [],
  155. )
  156. .map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const)
  157. .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
  158. .map(([groupDigits, groupPlace]) => {
  159. const [hundreds, tens, ones] = groupDigits.split('').map(Number);
  160. if (groupPlace < 1) {
  161. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace);
  162. }
  163. const milliaSuffix = (
  164. shortenMillia && groupPlace > 1
  165. ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}`
  166. : repeatString(MILLIA_PREFIX, groupPlace)
  167. );
  168. if (groupDigits === '001') {
  169. return milliaSuffix;
  170. }
  171. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix;
  172. })
  173. .join('');
  174. if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
  175. return `${groupGroups}${ILLION_SUFFIX}` as const;
  176. }
  177. if (bigGroupPlace > 10) {
  178. return `${groupGroups}t${ILLION_SUFFIX}` as const;
  179. }
  180. return `${groupGroups}${ILLION_SUFFIX}` as const;
  181. };
  182. export const makeGroups = (groups: Group[], options?: Record<string, unknown>): string[] => {
  183. const filteredGroups = groups.filter(([digits, place]) => (
  184. place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS
  185. ));
  186. return filteredGroups.map(
  187. ([group, place]) => {
  188. const makeHundredsArgs = group
  189. .padStart(3, '0')
  190. .split('')
  191. .map((s) => Number(s)) as [number, number, number];
  192. const groupDigitsName = makeHundredsName(...makeHundredsArgs);
  193. const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false);
  194. if (groupName.length > 0) {
  195. return `${groupDigitsName} ${groupName}`;
  196. }
  197. return groupDigitsName;
  198. },
  199. );
  200. };
  201. /**
  202. * Group a number string into groups of three digits, starting from the decimal point.
  203. * @param value - The number string to group.
  204. */
  205. export const group = (value: string): Group[] => {
  206. const [significand, exponentString] = numberToExponential(
  207. value,
  208. {
  209. decimalPoint: DECIMAL_POINT,
  210. groupingSymbol: GROUPING_SYMBOL,
  211. exponentDelimiter: EXPONENT_DELIMITER,
  212. },
  213. )
  214. .split(EXPONENT_DELIMITER);
  215. const exponent = Number(exponentString);
  216. const significantDigits = significand.replace(DECIMAL_POINT, '');
  217. return significantDigits.split('').reduce<Group[]>(
  218. (acc, c, i) => {
  219. const currentPlace = BigInt(Math.floor((exponent - i) / 3));
  220. const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
  221. const currentPlaceInGroup = 2 - ((exponent - i) % 3);
  222. if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
  223. const lastGroupDigits = lastGroup[0].split('');
  224. lastGroupDigits[currentPlaceInGroup] = c;
  225. return [...acc.slice(0, -1) ?? [], [
  226. lastGroupDigits.join(''),
  227. currentPlace,
  228. ]];
  229. }
  230. return [...acc, [c.padEnd(3, '0'), currentPlace]];
  231. },
  232. [],
  233. );
  234. };
  235. /**
  236. * Formats the final tokenized string.
  237. * @param tokens - The tokens to finalize.
  238. */
  239. export const finalize = (tokens: string[]) => (
  240. tokens
  241. .map((t) => t.trim())
  242. .join(' ')
  243. .trim()
  244. );
  245. /**
  246. * Makes a negative string.
  247. * @param s - The string to make negative.
  248. */
  249. export const makeNegative = (s: string) => (
  250. `${NEGATIVE} ${s}`
  251. );