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<FixedPointNumber> {
         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<FixedPointNumber> {
             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<FixedPointNumber> {
      * @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<FixedPointNumber> {
     /**
      * 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<FixedPointNumber> {
      * * ±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;
+}