From f185f4b257db22c1612caf528a18494dfd908a89 Mon Sep 17 00:00:00 2001 From: Luca Nicola Debiasi <63785793+lucanicoladebiasi@users.noreply.github.com> Date: Mon, 28 Oct 2024 07:50:09 +0000 Subject: [PATCH] 1439 bug FixedPointNumber class power function (#1443) * fix: 1439 pow fixed * fix: 1439 pow fixed * fix: 1439 pow fixed * fix: 1439 pow fixed --- docs/diagrams/architecture/vcdm.md | 3 +- packages/core/src/vcdm/FixedPointNumber.ts | 84 +++--- .../tests/vcdm/FixedPointNumber.unit.test.ts | 258 ++++++++++++------ 3 files changed, 219 insertions(+), 126 deletions(-) diff --git a/docs/diagrams/architecture/vcdm.md b/docs/diagrams/architecture/vcdm.md index 7be37c715..21904fd83 100644 --- a/docs/diagrams/architecture/vcdm.md +++ b/docs/diagrams/architecture/vcdm.md @@ -69,6 +69,7 @@ classDiagram +bigint scaledValue +FixedPointNumber NaN$ +FixedPointNumber NEGATIVE_INFINITY$ + +FixedPointNumber ONE$ +FixedPointNumber POSITIVE_INFINITY$ +FixedPointNumber ZERO$ +FixedPointNumber abs() @@ -96,7 +97,7 @@ classDiagram +FixedPointNumber minus(FixedPointNumber that) +FixedPointNumber modulo(FixedPointNumber that) +FixedPointNumber negated() - +FixedPointNumber of(bigint|number|string exp)$ + +FixedPointNumber of(bigint|number|string|FixedPointNumber exp)$ +FixedPointNumber plus(FixedPointNumber that) +FixedPointNumber pow(FixedPointNumber that) +FixedPointNumber sqrt() diff --git a/packages/core/src/vcdm/FixedPointNumber.ts b/packages/core/src/vcdm/FixedPointNumber.ts index 24f06d31b..0b6173a76 100644 --- a/packages/core/src/vcdm/FixedPointNumber.ts +++ b/packages/core/src/vcdm/FixedPointNumber.ts @@ -44,6 +44,11 @@ class FixedPointNumber implements VeChainDataModel { Number.NEGATIVE_INFINITY ); + /** + * Represents the one constant. + */ + public static readonly ONE = FixedPointNumber.of(1n); + /** * The positive Infinite value. * @@ -306,6 +311,7 @@ class FixedPointNumber implements VeChainDataModel { let fd = this.fractionalDigits; let sv = this.scaledValue; if (dp > fd) { + // Scale up. sv *= FixedPointNumber.BASE ** (dp - fd); fd = dp; } else { @@ -773,10 +779,17 @@ class FixedPointNumber implements VeChainDataModel { * @throws {InvalidDataType} If `exp` is not a numeric expression. */ public static of( - exp: bigint | number | string, + exp: bigint | number | string | FixedPointNumber, decimalPlaces: bigint = this.DEFAULT_FRACTIONAL_DECIMALS ): FixedPointNumber { try { + if (exp instanceof FixedPointNumber) { + return new FixedPointNumber( + exp.fractionalDigits, + exp.scaledValue, + exp.edgeFlag + ); + } if (Number.isNaN(exp)) return new FixedPointNumber(decimalPlaces, 0n, Number.NaN); if (exp === Number.NEGATIVE_INFINITY) @@ -843,6 +856,10 @@ class FixedPointNumber implements VeChainDataModel { /** * Returns a FixedPointNumber whose value is the value of this FixedPointNumber raised to the power of `that` FixedPointNumber. * + * This method implements the + * [Exponentiation by Squaring](https://en.wikipedia.org/wiki/Exponentiation_by_squaring) + * algorithm. + * * Limit cases * * NaN ^ e = NaN * * b ^ NaN = NaN @@ -853,67 +870,40 @@ class FixedPointNumber implements VeChainDataModel { * * ±Infinite ^ +e = +Infinite * * @param {FixedPointNumber} that - The exponent as a fixed-point number. - * It can be negative, it can be not an integer value - * ([bignumber.js pow](https://mikemcl.github.io/bignumber.js/#pow) - * doesn't support not integer exponents). + * truncated to its integer component because **Exponentiation by Squaring** is not valid for rational exponents. * @return {FixedPointNumber} - The result of raising this fixed-point number to the power of the given exponent. * - * @remarks The precision is the greater of the precision of the two operands. - * @remarks In fixed-precision math, the comparisons between powers of operands having different fractional - * precision can lead to differences. - * * @see [bignumber.js exponentiatedBy](https://mikemcl.github.io/bignumber.js/#pow) */ public pow(that: FixedPointNumber): FixedPointNumber { + // Limit cases if (this.isNaN() || that.isNaN()) return FixedPointNumber.NaN; if (this.isInfinite()) return that.isZero() - ? FixedPointNumber.of(1) + ? FixedPointNumber.ONE : that.isNegative() ? FixedPointNumber.ZERO : FixedPointNumber.POSITIVE_INFINITY; if (that.isNegativeInfinite()) return FixedPointNumber.ZERO; - if (that.isPositiveInfinite()) + if (that.isPositiveInfinite()) { return FixedPointNumber.POSITIVE_INFINITY; - const fd = this.maxFractionalDigits(that, this.fractionalDigits); // Max common fractional decimals. - return new FixedPointNumber( - fd, - FixedPointNumber.pow( - fd, - this.dp(fd).scaledValue, - that.dp(fd).scaledValue - ) - ).dp(this.fractionalDigits); // Minimize fractional decimals without precision loss. - } - - /** - * Computes the power of a given base raised to a specified exponent. - * - * @param {bigint} fd - The scale factor for decimal precision. - * @param {bigint} base - The base number to be raised to the power. - * @param {bigint} exponent - The exponent to which the base should be raised. - * @return {bigint} The result of base raised to the power of exponent, scaled by the scale factor. - */ - private static pow(fd: bigint, base: bigint, exponent: bigint): bigint { - const sf = FixedPointNumber.BASE ** fd; // Scale factor. - if (exponent < 0n) { - return FixedPointNumber.pow( - fd, - FixedPointNumber.div(fd, sf, base), - -exponent - ); // Recursive. } - if (exponent === 0n) { - return 1n * sf; - } - if (exponent === sf) { - return base; + if (that.isZero()) return FixedPointNumber.ONE; + // Exponentiation by squaring works for natural exponent value. + let exponent = that.abs().bi; + let base = FixedPointNumber.of(this); + let result = FixedPointNumber.ONE; + while (exponent > 0n) { + // If the exponent is odd, multiply the result by the current base. + if (exponent % 2n === 1n) { + result = result.times(base); + } + // Square the base and halve the exponent. + base = base.times(base); + exponent = exponent / 2n; } - return FixedPointNumber.pow( - fd, - this.mul(base, base, fd), - exponent - sf - ); // Recursive. + // If exponent is negative, convert the problem to positive exponent. + return that.isNegative() ? FixedPointNumber.ONE.div(result) : result; } /** diff --git a/packages/core/tests/vcdm/FixedPointNumber.unit.test.ts b/packages/core/tests/vcdm/FixedPointNumber.unit.test.ts index 00f042fb7..028338e5b 100644 --- a/packages/core/tests/vcdm/FixedPointNumber.unit.test.ts +++ b/packages/core/tests/vcdm/FixedPointNumber.unit.test.ts @@ -335,65 +335,71 @@ describe('FixedPointNumber class tests', () => { describe('Construction tests', () => { test('of NaN', () => { const n = NaN; - const fpn = FixedPointNumber.of(n); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(n.toString()); + const actual = FixedPointNumber.of(n); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(n.toString()); }); test('of -Infinity', () => { const n = -Infinity; - const fpn = FixedPointNumber.of(n); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(n.toString()); + const actual = FixedPointNumber.of(n); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(n.toString()); }); test('of +Infinity', () => { const n = Infinity; - const fpn = FixedPointNumber.of(n); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(n.toString()); + const actual = FixedPointNumber.of(n); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(n.toString()); }); - test('of bigint', () => { - const bi = Infinity; - const fpn = FixedPointNumber.of(bi); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(bi.toString()); + test('of -bigint', () => { + const bi = -12345678901234567890n; + const actual = FixedPointNumber.of(bi); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(bi.toString()); }); - test('of -n', () => { - const n = -123.0067; - const fpn = FixedPointNumber.of(n); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(n.toString()); + test('of +bigint', () => { + const bi = 12345678901234567890n; + const actual = FixedPointNumber.of(bi); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(bi.toString()); + }); + + test('of FixedPointNumber', () => { + const expected = FixedPointNumber.of(-123.45); + const actual = FixedPointNumber.of(expected); + expect(actual.isEqual(expected)).toBe(true); }); test('of +n', () => { const n = 123.0067; - const fpn = FixedPointNumber.of(n); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(n.toString()); + const actual = FixedPointNumber.of(n); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(n.toString()); }); test('of -n', () => { const n = -123.0067; - const fpn = FixedPointNumber.of(n); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(n.toString()); + const actual = FixedPointNumber.of(n); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(n.toString()); }); test('of negative string', () => { - const n = -123.0067; - const fpn = FixedPointNumber.of(n.toString()); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.toString()).toBe(n.toString()); + const n = '-123.0067'; + const actual = FixedPointNumber.of(n.toString()); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.toString()).toBe(n.toString()); }); test('of positive string', () => { const exp = '+123.45'; - const fpn = FixedPointNumber.of(exp); - expect(fpn).toBeInstanceOf(FixedPointNumber); - expect(fpn.n).toBe(Number(exp)); + const actual = FixedPointNumber.of(exp); + expect(actual).toBeInstanceOf(FixedPointNumber); + expect(actual.n).toBe(Number(exp)); }); test('of an illegal expression throws exception', () => { @@ -2243,48 +2249,68 @@ describe('FixedPointNumber class tests', () => { }); describe('pow method tests', () => { - test('NaN ^ ±e', () => { + test('NaN ^ -e', () => { + const b = NaN; + const e = -123.45; + const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); + const expected = b ** e; + expect(actual.n).toBe(expected); + }); + + test('NaN ^ +e', () => { const b = NaN; const e = 123.45; const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); const expected = b ** e; expect(actual.n).toBe(expected); - expect(FixedPointNumber.of(-b).pow(FixedPointNumber.of(e))).toEqual( - actual - ); }); - test('±b ^ NaN', () => { + test('-b ^ NaN', () => { + const b = -123.45; + const e = NaN; + const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); + const expected = b ** e; + expect(actual.n).toBe(expected); + }); + + test('+b ^ NaN', () => { const b = 123.45; const e = NaN; const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); const expected = b ** e; expect(actual.n).toBe(expected); - expect(FixedPointNumber.of(-b).pow(FixedPointNumber.of(e))).toEqual( - actual - ); }); - test('±b ^ -Infinity', () => { + test('-b ^ -Infinity', () => { + const b = -123.45; + const e = -Infinity; + const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); + const expected = b ** e; + expect(actual.n).toBe(expected); + }); + + test('+b ^ -Infinity', () => { const b = 123.45; const e = -Infinity; const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); const expected = b ** e; expect(actual.n).toBe(expected); - expect(FixedPointNumber.of(-b).pow(FixedPointNumber.of(e))).toEqual( - actual - ); }); - test('±b ^ +Infinity', () => { + test('-b ^ +Infinity', () => { + const b = -123.45; + const e = Infinity; + const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); + const expected = b ** e; + expect(actual.n).toBe(expected); + }); + + test('+b ^ +Infinity', () => { const b = 123.45; const e = Infinity; const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); const expected = b ** e; expect(actual.n).toBe(expected); - expect(FixedPointNumber.of(-b).pow(FixedPointNumber.of(e))).toEqual( - actual - ); }); test('-Infinity ^ 0', () => { @@ -2367,47 +2393,88 @@ describe('FixedPointNumber class tests', () => { expect(actual.n).toBe(expected); }); - test('b ^ -e - scale test', () => { + test('b ^ -e', () => { const b = 3; const e = -2; - const actualUp = FixedPointNumber.of(b, 25n).pow( - FixedPointNumber.of(e, 15n) - ); - const actualDn = FixedPointNumber.of(b, 15n).pow( - FixedPointNumber.of(e, 25n) - ); const expected = BigNumber(b).pow(BigNumber(e)); - const fd = 16; // Fractional digits before divergence. - expect(actualUp.n.toFixed(fd)).toBe( - expected.toNumber().toFixed(fd) - ); - expect(actualUp.eq(actualDn)).toBe(true); + const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); + console.log(actual.toString()); + console.log(expected.toString()); }); - test('±b ^ +e - scale test', () => { - const b = 0.7; - const e = -2; + test('-b ^ +e', () => { + const b = -2; + const e = 7; + const expected = BigNumber(b).pow(BigNumber(e)); const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); + expect(actual.toString()).toBe(expected.toString()); + }); + + test('+b ^ +e', () => { + const b = 0.7; + const e = 8; const expected = BigNumber(b).pow(BigNumber(e)); - const fd = 14; // Fractional digits before divergence. - expect(actual.n.toFixed(fd)).toBe(expected.toNumber().toFixed(fd)); - expect(FixedPointNumber.of(-b).pow(FixedPointNumber.of(e))).toEqual( - actual - ); + const actual = FixedPointNumber.of(b).pow(FixedPointNumber.of(e)); + expect(actual.toString()).toBe(expected.toString()); }); - test('±b ^ 0 = 1', () => { + test('-b ^ 0 = 1', () => { + const b = -123.45; + const e = 0; + const expected = FixedPointNumber.ONE; + const actual = FixedPointNumber.of(-b).pow(FixedPointNumber.of(e)); + expect(actual.isEqual(expected)).toBe(true); + }); + + test('+b ^ 0 = 1', () => { const b = 123.45; const e = 0; - const expected = FixedPointNumber.of(1); - const actualFromNegative = FixedPointNumber.of(-b).pow( - FixedPointNumber.of(e) - ); - const actualFromPositive = FixedPointNumber.of(b).pow( - FixedPointNumber.of(e) - ); - expect(actualFromNegative.isEqual(expected)).toBe(true); - expect(actualFromPositive.isEqual(expected)).toBe(true); + const expected = FixedPointNumber.ONE; + const actual = FixedPointNumber.of(-b).pow(FixedPointNumber.of(e)); + expect(actual.isEqual(expected)).toBe(true); + }); + + // https://en.wikipedia.org/wiki/Compound_interest + test('compound interest - once per year', () => { + const P = 10000; // 10,000 $ + const R = 0.15; // 15% interest rate + const N = 1; // interest accrued times per year + const T = 1; // 1 year of investment time + const jsA = interestWithNumberType(P, R, N, T); + // console.log( + // `JS number => ${P} at ${R} accrued ${N} per year for ${T} years = ${jsA} ` + // ); + const bnA = interestWithBigNumberType(P, R, N, T); + // console.log( + // `BigNumber => ${P} at ${R} accrued ${N} per year for ${T} years = ${bnA} ` + // ); + const fpA = interestWithFixedPointNumberType(P, R, N, T); + // console.log( + // `SDK FixedPointNumber => ${P} at ${R} accrued ${N} per year for ${T} years = ${fpA} ` + // ); + expect(fpA.toString()).toBe(jsA.toString()); + expect(fpA.toString()).toBe(bnA.toString()); + }); + + test('compound interest - once per day', () => { + const P = 10000; // 10,000 $ + const R = 0.15; // 15% interest rate + const N = 365; // interest accrued times per day + const T = 1; // 1 year of investment time + const jsA = interestWithNumberType(P, R, N, T); + // console.log( + // `JS number => ${P} at ${R} accrued ${N} per year for ${T} years = ${jsA} ` + // ); + const bnA = interestWithBigNumberType(P, R, N, T); + // console.log( + // `BigNumber => ${P} at ${R} accrued ${N} per year for ${T} years = ${bnA} ` + // ); + const fpA = interestWithFixedPointNumberType(P, R, N, T); + // console.log( + // `SDK FixedPointNumber => ${P} at ${R} accrued ${N} per year for ${T} years = ${fpA} ` + // ); + expect(fpA.toString()).not.toBe(jsA.toString()); + expect(fpA.toString()).not.toBe(bnA.toString()); }); }); @@ -2595,3 +2662,38 @@ describe('FixedPointNumber class tests', () => { }); }); }); + +function interestWithBigNumberType( + P: number, + r: number, + n: number, + t: number +): BigNumber { + const _P = BigNumber(P); + const _r = BigNumber(r); + const _n = BigNumber(n); + const _t = BigNumber(t); + return BigNumber(1).plus(_r.div(n)).pow(_t.times(_n)).times(_P); +} + +function interestWithFixedPointNumberType( + P: number, + r: number, + n: number, + t: number +): FixedPointNumber { + const _P = FixedPointNumber.of(P); + const _r = FixedPointNumber.of(r); + const _n = FixedPointNumber.of(n); + const _t = FixedPointNumber.of(t); + return FixedPointNumber.ONE.plus(_r.div(_n)).pow(_t.times(_n)).times(_P); +} + +function interestWithNumberType( + P: number, + r: number, + n: number, + t: number +): number { + return (1 + r / n) ** (t * n) * P; +}