Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

299 行
8.0 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,
  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 milliaCount - Number of millia- groups.
  75. * @returns string The millions prefix.
  76. */
  77. const makeMillionsPrefix = (millions: number, milliaCount: GroupPlace) => {
  78. if (milliaCount > 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 milliaCount - Number of millia- groups.
  88. * @returns string The decillions prefix.
  89. */
  90. const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: GroupPlace) => {
  91. if (decillions === 0) {
  92. return makeMillionsPrefix(millions, milliaCount);
  93. }
  94. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  95. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  96. return `${onesPrefix}${tensName}` as const;
  97. };
  98. /**
  99. * Builds a name for numbers in the centillions.
  100. * @param centillions - Centillions digit.
  101. * @param decillions - Decillions digit.
  102. * @param millions - Millions digit.
  103. * @param milliaCount - Number of millia- groups.
  104. * @returns string The centillions prefix.
  105. */
  106. const makeCentillionsPrefix = (
  107. centillions: number,
  108. decillions: number,
  109. millions: number,
  110. milliaCount: GroupPlace,
  111. ) => {
  112. if (centillions === 0) {
  113. return makeDecillionsPrefix(decillions, millions, milliaCount);
  114. }
  115. const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
  116. const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
  117. const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix;
  118. return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
  119. };
  120. const repeatString = (s: string, count: GroupPlace) => {
  121. let result = '';
  122. for (let i = BigInt(0); i < count; i += BigInt(1)) {
  123. result += s;
  124. }
  125. return result;
  126. };
  127. const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
  128. if (place === BigInt(0)) {
  129. return '' as const;
  130. }
  131. if (place === BigInt(1)) {
  132. return THOUSAND;
  133. }
  134. const bigGroupPlace = place - BigInt(1);
  135. const groupGroups = bigGroupPlace
  136. .toString()
  137. .split('')
  138. .reduceRight<Group[]>(
  139. (acc, c, i, cc) => {
  140. const firstGroup = acc.at(0);
  141. const currentPlace = BigInt(Math.floor((cc.length - i - 1) / 3));
  142. const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
  143. if (typeof firstGroup === 'undefined') {
  144. newGroup[GROUP_DIGITS_INDEX] = c;
  145. return [newGroup];
  146. }
  147. if (firstGroup[0].length > 2) {
  148. newGroup[GROUP_DIGITS_INDEX] = c;
  149. return [newGroup, ...acc];
  150. }
  151. newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
  152. return [
  153. newGroup,
  154. ...acc.slice(1),
  155. ];
  156. },
  157. [],
  158. )
  159. .map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const)
  160. .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
  161. .map(([groupDigits, groupPlace]) => {
  162. const [hundreds, tens, ones] = groupDigits.split('').map(Number);
  163. if (groupPlace < 1) {
  164. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace);
  165. }
  166. const milliaSuffix = (
  167. shortenMillia && groupPlace > 1
  168. ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}`
  169. : repeatString(MILLIA_PREFIX, groupPlace)
  170. );
  171. if (groupDigits === '001') {
  172. return milliaSuffix;
  173. }
  174. return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix;
  175. })
  176. .join('');
  177. if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
  178. return `${groupGroups}${ILLION_SUFFIX}` as const;
  179. }
  180. if (bigGroupPlace > 10) {
  181. return `${groupGroups}t${ILLION_SUFFIX}` as const;
  182. }
  183. return `${groupGroups}${ILLION_SUFFIX}` as const;
  184. };
  185. export interface MakeGroupsOptions {
  186. addTensDashes?: boolean;
  187. shortenMillia?: boolean;
  188. }
  189. export const makeGroups = (groups: Group[], options?: MakeGroupsOptions): string[] => {
  190. const filteredGroups = groups.filter(([digits, place]) => (
  191. place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS
  192. ));
  193. return filteredGroups.map(
  194. ([group, place]) => {
  195. const makeHundredsArgs = group
  196. .padStart(3, '0')
  197. .split('')
  198. .map((s) => Number(s)) as [number, number, number];
  199. const groupDigitsName = makeHundredsName(
  200. ...makeHundredsArgs,
  201. options?.addTensDashes ?? true,
  202. );
  203. const groupName = getGroupName(place, options?.shortenMillia ?? false);
  204. if (groupName.length > 0) {
  205. return `${groupDigitsName} ${groupName}`;
  206. }
  207. return groupDigitsName;
  208. },
  209. );
  210. };
  211. /**
  212. * Group a number string into groups of three digits, starting from the decimal point.
  213. * @param value - The number string to group.
  214. */
  215. export const group = (value: string): Group[] => {
  216. const [significand, exponentString] = numberToExponential(
  217. value,
  218. {
  219. decimalPoint: DECIMAL_POINT,
  220. groupingSymbol: GROUPING_SYMBOL,
  221. exponentDelimiter: EXPONENT_DELIMITER,
  222. },
  223. )
  224. .split(EXPONENT_DELIMITER);
  225. const exponent = Number(exponentString);
  226. const significantDigits = significand.replace(new RegExp(`\\${DECIMAL_POINT}`, 'g'), '');
  227. return significantDigits.split('').reduce<Group[]>(
  228. (acc, c, i) => {
  229. const currentPlace = BigInt(Math.floor((exponent - i) / 3));
  230. const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
  231. const currentPlaceInGroup = 2 - ((exponent - i) % 3);
  232. if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
  233. const lastGroupDigits = lastGroup[0].split('');
  234. lastGroupDigits[currentPlaceInGroup] = c;
  235. return [...acc.slice(0, -1) ?? [], [
  236. lastGroupDigits.join(''),
  237. currentPlace,
  238. ]];
  239. }
  240. return [...acc, [c.padEnd(3, '0'), currentPlace]];
  241. },
  242. [],
  243. );
  244. };
  245. export interface FinalizeOptions {
  246. oneGroupPerLine?: boolean;
  247. }
  248. /**
  249. * Formats the final tokenized string.
  250. * @param tokens - The tokens to finalize.
  251. * @param options - The options to use.
  252. */
  253. export const finalize = (tokens: string[], options?: FinalizeOptions) => (
  254. tokens
  255. .map((t) => t.trim())
  256. .join(options?.oneGroupPerLine ? '\n' : GROUP_SEPARATOR)
  257. .trim()
  258. );
  259. /**
  260. * Makes a negative string.
  261. * @param s - The string to make negative.
  262. */
  263. export const makeNegative = (s: string) => (
  264. `${NEGATIVE} ${s}`
  265. );