Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 

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