formatNumeral.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import { strings } from './constants';
  2. // rule types: 'text' | 'numbers' | 'bytes-decimal' | 'bytes-binary' | 'percentages' | 'exponential'
  3. // TODO: Refining the 'currency' type
  4. type Rule = typeof strings.RULE[number];
  5. type Truncate = typeof strings.TRUNCATE[number];
  6. type Parser = (value: string) => string;
  7. type RuleMethods = {
  8. [key in Rule]?: (value: number) => string;
  9. };
  10. type TruncateMethods = {
  11. [key in Truncate]: (value: number) => number;
  12. };
  13. export default class FormatNumeral {
  14. private readonly content: string;
  15. private readonly rule: Rule;
  16. private readonly precision: number;
  17. private readonly truncate: Truncate;
  18. private readonly parser: Parser | undefined;
  19. private readonly isDiyParser: boolean;
  20. // A collection of methods for formatting numbers; Methods key: Rule (strings.RULE); Not included: 'text' & 'numbers'
  21. private readonly ruleMethods: RuleMethods = {
  22. 'bytes-decimal': (value: number) => {
  23. const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  24. let i = 0;
  25. while (value >= 1000) {
  26. value /= 1000;
  27. i++;
  28. }
  29. return `${this.truncatePrecision(value)} ${units[i]}`;
  30. },
  31. 'bytes-binary': (value: number) => {
  32. const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
  33. let i = 0;
  34. while (value >= 1024) {
  35. value /= 1024;
  36. i++;
  37. }
  38. return `${this.truncatePrecision(value)} ${units[i]}`;
  39. },
  40. percentages: (value: number) => {
  41. // The rules here have been modified in version v2.30.0
  42. return `${this.truncatePrecision(value * 100)}%`;
  43. },
  44. exponential: (value: number) => {
  45. const vExponential = value.toExponential(this.precision + 2);
  46. const vArr = vExponential.split('e');
  47. return `${this.truncatePrecision(Number(vArr[0]))}e${vArr[1]}`;
  48. },
  49. };
  50. // A collection of methods for truncating numbers; Methods key: Truncate (strings.Truncate);
  51. private readonly truncateMethods: TruncateMethods = {
  52. ceil: Math.ceil,
  53. floor: Math.floor,
  54. round: Math.round,
  55. };
  56. constructor(content: string, rule: Rule, precision: number, truncate: Truncate, parser: Parser | undefined) {
  57. this.isDiyParser = typeof parser !== 'undefined';
  58. this.content = content;
  59. this.rule = rule;
  60. this.precision = precision;
  61. this.truncate = truncate;
  62. this.parser = parser;
  63. }
  64. // Formatting numbers within a string.
  65. public format(): string {
  66. // Executed when a custom method exists
  67. if (this.isDiyParser) {
  68. return this.parser(this.content);
  69. }
  70. // When the `rule` is `text`, only the `truncatePrecision` method is executed for numeric processing.
  71. if (this.rule === 'text') {
  72. return extractNumbers(this.content)
  73. .map(item => (checkIsNumeral(item) ? this.truncatePrecision(item) : item))
  74. .join('');
  75. }
  76. // Separate extraction of numbers when `rule` is `numbers`.
  77. if (this.rule === 'numbers') {
  78. return extractNumbers(this.content)
  79. .filter(item => checkIsNumeral(item))
  80. .map(item => this.truncatePrecision(item))
  81. .join(',');
  82. }
  83. // Run formatting methods that exist.
  84. return extractNumbers(this.content)
  85. .map(item => (checkIsNumeral(item) ? this.ruleMethods[this.rule](Number(item)) : item))
  86. .join('');
  87. }
  88. private truncatePrecision(content: string | number): string {
  89. // Truncation and selection of rounding methods for processing. function from: truncateMethods
  90. const cTruncated =
  91. this.truncateMethods[this.truncate](Number(content) * Math.pow(10, this.precision)) /
  92. Math.pow(10, this.precision);
  93. const cArr = cTruncated.toString().split('.');
  94. // is an integer then the end number is normalised
  95. if (cArr.length === 1) {
  96. return cTruncated.toFixed(this.precision);
  97. }
  98. const cTLength = cArr[1].length;
  99. // Fill in any missing `0` at the end.
  100. if (cTLength < this.precision) {
  101. return `${cArr[0]}.${cArr[1]}${'0'.repeat(this.precision - cTLength)}`;
  102. }
  103. return cTruncated.toString();
  104. }
  105. }
  106. // Separate numbers from strings, the `-` symbol is a numeric prefix not allowed on its own.
  107. function extractNumbers(content: string): Array<string> {
  108. const reg = /(-?[0-9]*\.?[0-9]+([eE]-?[0-9]+)?)|([^-\d\.]+)/g;
  109. return content.match(reg) || [];
  110. }
  111. function checkIsNumeral(str: string): boolean {
  112. return !(isNaN(Number(str)) || str.replace(/\s+/g, '') === '');
  113. }