number.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.97
  5. */
  6. import { numberKeys } from "./key";
  7. import { numberStringFormatter } from "./locale";
  8. // adopted from https://stackoverflow.com/a/66939244
  9. export class BigDecimal {
  10. constructor(input) {
  11. if (input instanceof BigDecimal) {
  12. return input;
  13. }
  14. const [integers, decimals] = String(input).split(".").concat("");
  15. this.value =
  16. BigInt(integers + decimals.padEnd(BigDecimal.DECIMALS, "0").slice(0, BigDecimal.DECIMALS)) +
  17. BigInt(BigDecimal.ROUNDED && decimals[BigDecimal.DECIMALS] >= "5");
  18. this.isNegative = input.charAt(0) === "-";
  19. }
  20. static _divRound(dividend, divisor) {
  21. return BigDecimal.fromBigInt(dividend / divisor + (BigDecimal.ROUNDED ? ((dividend * BigInt(2)) / divisor) % BigInt(2) : BigInt(0)));
  22. }
  23. static fromBigInt(bigint) {
  24. return Object.assign(Object.create(BigDecimal.prototype), { value: bigint });
  25. }
  26. toString() {
  27. const s = this.value
  28. .toString()
  29. .replace(new RegExp("-", "g"), "")
  30. .padStart(BigDecimal.DECIMALS + 1, "0");
  31. const i = s.slice(0, -BigDecimal.DECIMALS);
  32. const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
  33. const value = i.concat(d.length ? "." + d : "");
  34. return `${this.isNegative ? "-" : ""}${value}`;
  35. }
  36. formatToParts(formatter) {
  37. const s = this.value
  38. .toString()
  39. .replace(new RegExp("-", "g"), "")
  40. .padStart(BigDecimal.DECIMALS + 1, "0");
  41. const i = s.slice(0, -BigDecimal.DECIMALS);
  42. const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
  43. const parts = formatter.formatToParts(BigInt(i));
  44. this.isNegative && parts.unshift({ type: "minusSign", value: numberStringFormatter.minusSign });
  45. if (d.length) {
  46. parts.push({ type: "decimal", value: numberStringFormatter.decimal });
  47. d.split("").forEach((char) => parts.push({ type: "fraction", value: char }));
  48. }
  49. return parts;
  50. }
  51. format(formatter) {
  52. const s = this.value
  53. .toString()
  54. .replace(new RegExp("-", "g"), "")
  55. .padStart(BigDecimal.DECIMALS + 1, "0");
  56. const i = s.slice(0, -BigDecimal.DECIMALS);
  57. const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
  58. const iFormatted = `${this.isNegative ? numberStringFormatter.minusSign : ""}${formatter.format(BigInt(i))}`;
  59. const dFormatted = d.length ? `${numberStringFormatter.decimal}${formatter.format(BigInt(d))}` : "";
  60. return `${iFormatted}${dFormatted}`;
  61. }
  62. add(num) {
  63. return BigDecimal.fromBigInt(this.value + new BigDecimal(num).value);
  64. }
  65. subtract(num) {
  66. return BigDecimal.fromBigInt(this.value - new BigDecimal(num).value);
  67. }
  68. multiply(num) {
  69. return BigDecimal._divRound(this.value * new BigDecimal(num).value, BigDecimal.SHIFT);
  70. }
  71. divide(num) {
  72. return BigDecimal._divRound(this.value * BigDecimal.SHIFT, new BigDecimal(num).value);
  73. }
  74. }
  75. // Configuration: constants
  76. BigDecimal.DECIMALS = 100; // number of decimals on all instances
  77. BigDecimal.ROUNDED = true; // numbers are truncated (false) or rounded (true)
  78. BigDecimal.SHIFT = BigInt("1" + "0".repeat(BigDecimal.DECIMALS)); // derived constant
  79. export function isValidNumber(numberString) {
  80. return !(!numberString || isNaN(Number(numberString)));
  81. }
  82. export function parseNumberString(numberString) {
  83. if (!numberString || !stringContainsNumbers(numberString)) {
  84. return "";
  85. }
  86. return sanitizeExponentialNumberString(numberString, (nonExpoNumString) => {
  87. let containsDecimal = false;
  88. const result = nonExpoNumString
  89. .split("")
  90. .filter((value, i) => {
  91. if (value.match(/\./g) && !containsDecimal) {
  92. containsDecimal = true;
  93. return true;
  94. }
  95. if (value.match(/\-/g) && i === 0) {
  96. return true;
  97. }
  98. return numberKeys.includes(value);
  99. })
  100. .reduce((string, part) => string + part);
  101. return isValidNumber(result) ? new BigDecimal(result).toString() : "";
  102. });
  103. }
  104. // regex for number sanitization
  105. const allLeadingZerosOptionallyNegative = /^([-0])0+(?=\d)/;
  106. const decimalOnlyAtEndOfString = /(?!^\.)\.$/;
  107. const allHyphensExceptTheStart = /(?!^-)-/g;
  108. const isNegativeDecimalOnlyZeros = /^-\b0\b\.?0*$/;
  109. export const sanitizeNumberString = (numberString) => sanitizeExponentialNumberString(numberString, (nonExpoNumString) => {
  110. const sanitizedValue = nonExpoNumString
  111. .replace(allHyphensExceptTheStart, "")
  112. .replace(decimalOnlyAtEndOfString, "")
  113. .replace(allLeadingZerosOptionallyNegative, "$1");
  114. return isValidNumber(sanitizedValue)
  115. ? isNegativeDecimalOnlyZeros.test(sanitizedValue)
  116. ? sanitizedValue
  117. : new BigDecimal(sanitizedValue).toString()
  118. : nonExpoNumString;
  119. });
  120. export function sanitizeExponentialNumberString(numberString, func) {
  121. if (!numberString) {
  122. return numberString;
  123. }
  124. const firstE = numberString.toLowerCase().indexOf("e") + 1;
  125. if (!firstE) {
  126. return func(numberString);
  127. }
  128. return numberString
  129. .replace(/[eE]*$/g, "")
  130. .substring(0, firstE)
  131. .concat(numberString.slice(firstE).replace(/[eE]/g, ""))
  132. .split(/[eE]/)
  133. .map((section, i) => (i === 1 ? func(section.replace(/\./g, "")) : func(section)))
  134. .join("e")
  135. .replace(/^e/, "1e");
  136. }
  137. function stringContainsNumbers(string) {
  138. return numberKeys.some((number) => string.includes(number));
  139. }