fixednumber.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. "use strict";
  2. import { arrayify, hexZeroPad, isBytes } from "@ethersproject/bytes";
  3. import { Logger } from "@ethersproject/logger";
  4. import { version } from "./_version";
  5. const logger = new Logger(version);
  6. import { BigNumber, isBigNumberish } from "./bignumber";
  7. const _constructorGuard = {};
  8. const Zero = BigNumber.from(0);
  9. const NegativeOne = BigNumber.from(-1);
  10. function throwFault(message, fault, operation, value) {
  11. const params = { fault: fault, operation: operation };
  12. if (value !== undefined) {
  13. params.value = value;
  14. }
  15. return logger.throwError(message, Logger.errors.NUMERIC_FAULT, params);
  16. }
  17. // Constant to pull zeros from for multipliers
  18. let zeros = "0";
  19. while (zeros.length < 256) {
  20. zeros += zeros;
  21. }
  22. // Returns a string "1" followed by decimal "0"s
  23. function getMultiplier(decimals) {
  24. if (typeof (decimals) !== "number") {
  25. try {
  26. decimals = BigNumber.from(decimals).toNumber();
  27. }
  28. catch (e) { }
  29. }
  30. if (typeof (decimals) === "number" && decimals >= 0 && decimals <= 256 && !(decimals % 1)) {
  31. return ("1" + zeros.substring(0, decimals));
  32. }
  33. return logger.throwArgumentError("invalid decimal size", "decimals", decimals);
  34. }
  35. export function formatFixed(value, decimals) {
  36. if (decimals == null) {
  37. decimals = 0;
  38. }
  39. const multiplier = getMultiplier(decimals);
  40. // Make sure wei is a big number (convert as necessary)
  41. value = BigNumber.from(value);
  42. const negative = value.lt(Zero);
  43. if (negative) {
  44. value = value.mul(NegativeOne);
  45. }
  46. let fraction = value.mod(multiplier).toString();
  47. while (fraction.length < multiplier.length - 1) {
  48. fraction = "0" + fraction;
  49. }
  50. // Strip training 0
  51. fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
  52. const whole = value.div(multiplier).toString();
  53. if (multiplier.length === 1) {
  54. value = whole;
  55. }
  56. else {
  57. value = whole + "." + fraction;
  58. }
  59. if (negative) {
  60. value = "-" + value;
  61. }
  62. return value;
  63. }
  64. export function parseFixed(value, decimals) {
  65. if (decimals == null) {
  66. decimals = 0;
  67. }
  68. const multiplier = getMultiplier(decimals);
  69. if (typeof (value) !== "string" || !value.match(/^-?[0-9.]+$/)) {
  70. logger.throwArgumentError("invalid decimal value", "value", value);
  71. }
  72. // Is it negative?
  73. const negative = (value.substring(0, 1) === "-");
  74. if (negative) {
  75. value = value.substring(1);
  76. }
  77. if (value === ".") {
  78. logger.throwArgumentError("missing value", "value", value);
  79. }
  80. // Split it into a whole and fractional part
  81. const comps = value.split(".");
  82. if (comps.length > 2) {
  83. logger.throwArgumentError("too many decimal points", "value", value);
  84. }
  85. let whole = comps[0], fraction = comps[1];
  86. if (!whole) {
  87. whole = "0";
  88. }
  89. if (!fraction) {
  90. fraction = "0";
  91. }
  92. // Trim trailing zeros
  93. while (fraction[fraction.length - 1] === "0") {
  94. fraction = fraction.substring(0, fraction.length - 1);
  95. }
  96. // Check the fraction doesn't exceed our decimals size
  97. if (fraction.length > multiplier.length - 1) {
  98. throwFault("fractional component exceeds decimals", "underflow", "parseFixed");
  99. }
  100. // If decimals is 0, we have an empty string for fraction
  101. if (fraction === "") {
  102. fraction = "0";
  103. }
  104. // Fully pad the string with zeros to get to wei
  105. while (fraction.length < multiplier.length - 1) {
  106. fraction += "0";
  107. }
  108. const wholeValue = BigNumber.from(whole);
  109. const fractionValue = BigNumber.from(fraction);
  110. let wei = (wholeValue.mul(multiplier)).add(fractionValue);
  111. if (negative) {
  112. wei = wei.mul(NegativeOne);
  113. }
  114. return wei;
  115. }
  116. export class FixedFormat {
  117. constructor(constructorGuard, signed, width, decimals) {
  118. if (constructorGuard !== _constructorGuard) {
  119. logger.throwError("cannot use FixedFormat constructor; use FixedFormat.from", Logger.errors.UNSUPPORTED_OPERATION, {
  120. operation: "new FixedFormat"
  121. });
  122. }
  123. this.signed = signed;
  124. this.width = width;
  125. this.decimals = decimals;
  126. this.name = (signed ? "" : "u") + "fixed" + String(width) + "x" + String(decimals);
  127. this._multiplier = getMultiplier(decimals);
  128. Object.freeze(this);
  129. }
  130. static from(value) {
  131. if (value instanceof FixedFormat) {
  132. return value;
  133. }
  134. if (typeof (value) === "number") {
  135. value = `fixed128x${value}`;
  136. }
  137. let signed = true;
  138. let width = 128;
  139. let decimals = 18;
  140. if (typeof (value) === "string") {
  141. if (value === "fixed") {
  142. // defaults...
  143. }
  144. else if (value === "ufixed") {
  145. signed = false;
  146. }
  147. else {
  148. const match = value.match(/^(u?)fixed([0-9]+)x([0-9]+)$/);
  149. if (!match) {
  150. logger.throwArgumentError("invalid fixed format", "format", value);
  151. }
  152. signed = (match[1] !== "u");
  153. width = parseInt(match[2]);
  154. decimals = parseInt(match[3]);
  155. }
  156. }
  157. else if (value) {
  158. const check = (key, type, defaultValue) => {
  159. if (value[key] == null) {
  160. return defaultValue;
  161. }
  162. if (typeof (value[key]) !== type) {
  163. logger.throwArgumentError("invalid fixed format (" + key + " not " + type + ")", "format." + key, value[key]);
  164. }
  165. return value[key];
  166. };
  167. signed = check("signed", "boolean", signed);
  168. width = check("width", "number", width);
  169. decimals = check("decimals", "number", decimals);
  170. }
  171. if (width % 8) {
  172. logger.throwArgumentError("invalid fixed format width (not byte aligned)", "format.width", width);
  173. }
  174. if (decimals > 80) {
  175. logger.throwArgumentError("invalid fixed format (decimals too large)", "format.decimals", decimals);
  176. }
  177. return new FixedFormat(_constructorGuard, signed, width, decimals);
  178. }
  179. }
  180. export class FixedNumber {
  181. constructor(constructorGuard, hex, value, format) {
  182. logger.checkNew(new.target, FixedNumber);
  183. if (constructorGuard !== _constructorGuard) {
  184. logger.throwError("cannot use FixedNumber constructor; use FixedNumber.from", Logger.errors.UNSUPPORTED_OPERATION, {
  185. operation: "new FixedFormat"
  186. });
  187. }
  188. this.format = format;
  189. this._hex = hex;
  190. this._value = value;
  191. this._isFixedNumber = true;
  192. Object.freeze(this);
  193. }
  194. _checkFormat(other) {
  195. if (this.format.name !== other.format.name) {
  196. logger.throwArgumentError("incompatible format; use fixedNumber.toFormat", "other", other);
  197. }
  198. }
  199. addUnsafe(other) {
  200. this._checkFormat(other);
  201. const a = parseFixed(this._value, this.format.decimals);
  202. const b = parseFixed(other._value, other.format.decimals);
  203. return FixedNumber.fromValue(a.add(b), this.format.decimals, this.format);
  204. }
  205. subUnsafe(other) {
  206. this._checkFormat(other);
  207. const a = parseFixed(this._value, this.format.decimals);
  208. const b = parseFixed(other._value, other.format.decimals);
  209. return FixedNumber.fromValue(a.sub(b), this.format.decimals, this.format);
  210. }
  211. mulUnsafe(other) {
  212. this._checkFormat(other);
  213. const a = parseFixed(this._value, this.format.decimals);
  214. const b = parseFixed(other._value, other.format.decimals);
  215. return FixedNumber.fromValue(a.mul(b).div(this.format._multiplier), this.format.decimals, this.format);
  216. }
  217. divUnsafe(other) {
  218. this._checkFormat(other);
  219. const a = parseFixed(this._value, this.format.decimals);
  220. const b = parseFixed(other._value, other.format.decimals);
  221. return FixedNumber.fromValue(a.mul(this.format._multiplier).div(b), this.format.decimals, this.format);
  222. }
  223. floor() {
  224. const comps = this.toString().split(".");
  225. if (comps.length === 1) {
  226. comps.push("0");
  227. }
  228. let result = FixedNumber.from(comps[0], this.format);
  229. const hasFraction = !comps[1].match(/^(0*)$/);
  230. if (this.isNegative() && hasFraction) {
  231. result = result.subUnsafe(ONE.toFormat(result.format));
  232. }
  233. return result;
  234. }
  235. ceiling() {
  236. const comps = this.toString().split(".");
  237. if (comps.length === 1) {
  238. comps.push("0");
  239. }
  240. let result = FixedNumber.from(comps[0], this.format);
  241. const hasFraction = !comps[1].match(/^(0*)$/);
  242. if (!this.isNegative() && hasFraction) {
  243. result = result.addUnsafe(ONE.toFormat(result.format));
  244. }
  245. return result;
  246. }
  247. // @TODO: Support other rounding algorithms
  248. round(decimals) {
  249. if (decimals == null) {
  250. decimals = 0;
  251. }
  252. // If we are already in range, we're done
  253. const comps = this.toString().split(".");
  254. if (comps.length === 1) {
  255. comps.push("0");
  256. }
  257. if (decimals < 0 || decimals > 80 || (decimals % 1)) {
  258. logger.throwArgumentError("invalid decimal count", "decimals", decimals);
  259. }
  260. if (comps[1].length <= decimals) {
  261. return this;
  262. }
  263. const factor = FixedNumber.from("1" + zeros.substring(0, decimals), this.format);
  264. const bump = BUMP.toFormat(this.format);
  265. return this.mulUnsafe(factor).addUnsafe(bump).floor().divUnsafe(factor);
  266. }
  267. isZero() {
  268. return (this._value === "0.0" || this._value === "0");
  269. }
  270. isNegative() {
  271. return (this._value[0] === "-");
  272. }
  273. toString() { return this._value; }
  274. toHexString(width) {
  275. if (width == null) {
  276. return this._hex;
  277. }
  278. if (width % 8) {
  279. logger.throwArgumentError("invalid byte width", "width", width);
  280. }
  281. const hex = BigNumber.from(this._hex).fromTwos(this.format.width).toTwos(width).toHexString();
  282. return hexZeroPad(hex, width / 8);
  283. }
  284. toUnsafeFloat() { return parseFloat(this.toString()); }
  285. toFormat(format) {
  286. return FixedNumber.fromString(this._value, format);
  287. }
  288. static fromValue(value, decimals, format) {
  289. // If decimals looks more like a format, and there is no format, shift the parameters
  290. if (format == null && decimals != null && !isBigNumberish(decimals)) {
  291. format = decimals;
  292. decimals = null;
  293. }
  294. if (decimals == null) {
  295. decimals = 0;
  296. }
  297. if (format == null) {
  298. format = "fixed";
  299. }
  300. return FixedNumber.fromString(formatFixed(value, decimals), FixedFormat.from(format));
  301. }
  302. static fromString(value, format) {
  303. if (format == null) {
  304. format = "fixed";
  305. }
  306. const fixedFormat = FixedFormat.from(format);
  307. const numeric = parseFixed(value, fixedFormat.decimals);
  308. if (!fixedFormat.signed && numeric.lt(Zero)) {
  309. throwFault("unsigned value cannot be negative", "overflow", "value", value);
  310. }
  311. let hex = null;
  312. if (fixedFormat.signed) {
  313. hex = numeric.toTwos(fixedFormat.width).toHexString();
  314. }
  315. else {
  316. hex = numeric.toHexString();
  317. hex = hexZeroPad(hex, fixedFormat.width / 8);
  318. }
  319. const decimal = formatFixed(numeric, fixedFormat.decimals);
  320. return new FixedNumber(_constructorGuard, hex, decimal, fixedFormat);
  321. }
  322. static fromBytes(value, format) {
  323. if (format == null) {
  324. format = "fixed";
  325. }
  326. const fixedFormat = FixedFormat.from(format);
  327. if (arrayify(value).length > fixedFormat.width / 8) {
  328. throw new Error("overflow");
  329. }
  330. let numeric = BigNumber.from(value);
  331. if (fixedFormat.signed) {
  332. numeric = numeric.fromTwos(fixedFormat.width);
  333. }
  334. const hex = numeric.toTwos((fixedFormat.signed ? 0 : 1) + fixedFormat.width).toHexString();
  335. const decimal = formatFixed(numeric, fixedFormat.decimals);
  336. return new FixedNumber(_constructorGuard, hex, decimal, fixedFormat);
  337. }
  338. static from(value, format) {
  339. if (typeof (value) === "string") {
  340. return FixedNumber.fromString(value, format);
  341. }
  342. if (isBytes(value)) {
  343. return FixedNumber.fromBytes(value, format);
  344. }
  345. try {
  346. return FixedNumber.fromValue(value, 0, format);
  347. }
  348. catch (error) {
  349. // Allow NUMERIC_FAULT to bubble up
  350. if (error.code !== Logger.errors.INVALID_ARGUMENT) {
  351. throw error;
  352. }
  353. }
  354. return logger.throwArgumentError("invalid FixedNumber value", "value", value);
  355. }
  356. static isFixedNumber(value) {
  357. return !!(value && value._isFixedNumber);
  358. }
  359. }
  360. const ONE = FixedNumber.from(1);
  361. const BUMP = FixedNumber.from("0.5");
  362. //# sourceMappingURL=fixednumber.js.map