This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new f94e293d1 FINERACT-1981: EMI Calculator performance optimization
f94e293d1 is described below

commit f94e293d1e160e7624999e972c7810727c379119
Author: Janos Meszaros <[email protected]>
AuthorDate: Thu Oct 17 15:56:26 2024 +0200

    FINERACT-1981: EMI Calculator performance optimization
---
 .../loanschedule/data/RepaymentPeriod.java         |  50 ++++++++--
 .../org/apache/fineract/portfolio/util/Memo.java   | 103 +++++++++++++++++++++
 .../calc/ProgressiveEMICalculatorTest.java         |  53 +++++++++++
 3 files changed, 196 insertions(+), 10 deletions(-)

diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java
index 008f8c39c..132bfb67f 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java
@@ -29,6 +29,7 @@ import lombok.Setter;
 import lombok.ToString;
 import org.apache.fineract.infrastructure.core.service.MathUtil;
 import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.portfolio.util.Memo;
 
 @ToString(exclude = { "previous" })
 @EqualsAndHashCode(exclude = { "previous" })
@@ -49,6 +50,11 @@ public class RepaymentPeriod {
     @Getter
     private Money paidInterest;
 
+    private Memo<BigDecimal> rateFactorPlus1Calculation;
+    private Memo<Money> calculatedDueInterestCalculation;
+    private Memo<Money> dueInterestCalculation;
+    private Memo<Money> outstandingBalanceCalculation;
+
     public RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, 
LocalDate dueDate, Money emi) {
         this.previous = previous;
         this.fromDate = fromDate;
@@ -80,10 +86,25 @@ public class RepaymentPeriod {
     }
 
     public BigDecimal getRateFactorPlus1() {
+        if (rateFactorPlus1Calculation == null) {
+            rateFactorPlus1Calculation = 
Memo.of(this::calculateRateFactorPlus1, () -> this.interestPeriods);
+        }
+        return rateFactorPlus1Calculation.get();
+    }
+
+    private BigDecimal calculateRateFactorPlus1() {
         return 
interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE,
 BigDecimal::add);
     }
 
     public Money getCalculatedDueInterest() {
+        if (calculatedDueInterestCalculation == null) {
+            calculatedDueInterestCalculation = 
Memo.of(this::calculateCalculatedDueInterest,
+                    () -> new Object[] { this.previous, this.interestPeriods 
});
+        }
+        return calculatedDueInterestCalculation.get();
+    }
+
+    private Money calculateCalculatedDueInterest() {
         Money calculatedDueInterest = 
getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(getZero(),
                 Money::plus);
         if (getPrevious().isPresent()) {
@@ -106,9 +127,13 @@ public class RepaymentPeriod {
     }
 
     public Money getDueInterest() {
-        // Due interest might be the maximum paid if there is pay-off or early 
repayment
-        return 
MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? 
getPaidInterest() : getCalculatedDueInterest(),
-                getPaidInterest(), false);
+        if (dueInterestCalculation == null) {
+            // Due interest might be the maximum paid if there is pay-off or 
early repayment
+            dueInterestCalculation = Memo.of(() -> MathUtil.max(
+                    
getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? 
getPaidInterest() : getCalculatedDueInterest(),
+                    getPaidInterest(), false), () -> new Object[] { 
paidPrincipal, paidInterest, interestPeriods });
+        }
+        return dueInterestCalculation.get();
     }
 
     public Money getDuePrincipal() {
@@ -121,13 +146,18 @@ public class RepaymentPeriod {
     }
 
     public Money getOutstandingLoanBalance() {
-        InterestPeriod lastInstallmentPeriod = 
getInterestPeriods().get(getInterestPeriods().size() - 1);
-        Money calculatedOutStandingLoanBalance = 
lastInstallmentPeriod.getOutstandingLoanBalance() //
-                .plus(lastInstallmentPeriod.getBalanceCorrectionAmount()) //
-                .plus(lastInstallmentPeriod.getDisbursementAmount()) //
-                .minus(getDuePrincipal())//
-                .plus(getPaidPrincipal());//
-        return MathUtil.negativeToZero(calculatedOutStandingLoanBalance);
+        if (outstandingBalanceCalculation == null) {
+            outstandingBalanceCalculation = Memo.of(() -> {
+                InterestPeriod lastInstallmentPeriod = 
getInterestPeriods().get(getInterestPeriods().size() - 1);
+                Money calculatedOutStandingLoanBalance = 
lastInstallmentPeriod.getOutstandingLoanBalance() //
+                        
.plus(lastInstallmentPeriod.getBalanceCorrectionAmount()) //
+                        .plus(lastInstallmentPeriod.getDisbursementAmount()) //
+                        .minus(getDuePrincipal())//
+                        .plus(getPaidPrincipal());//
+                return 
MathUtil.negativeToZero(calculatedOutStandingLoanBalance);
+            }, () -> interestPeriods);
+        }
+        return outstandingBalanceCalculation.get();
     }
 
     public void addPaidPrincipalAmount(Money paidPrincipal) {
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/util/Memo.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/util/Memo.java
new file mode 100644
index 000000000..dd3f71a12
--- /dev/null
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/util/Memo.java
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.util;
+
+import java.util.function.Supplier;
+
+/**
+ * Memo (Object value cache) for calculations
+ *
+ */
+public final class Memo<T> {
+
+    private final Object lock = new Object();
+    private final Supplier<? extends T> supplier;
+    private final Supplier<Object> dependenciesGetter;
+    private final boolean useReferenceCheck;
+
+    private volatile T value;
+    private volatile int[] dependencyHashCodes = new int[0];
+
+    private Memo(Supplier<? extends T> supplier, Supplier<Object> 
dependenciesGetter, boolean useReferenceCheck) {
+        this.supplier = supplier;
+        this.dependenciesGetter = dependenciesGetter;
+        this.useReferenceCheck = useReferenceCheck;
+    }
+
+    public T get() {
+        Object actualDependencies = dependenciesGetter != null ? 
dependenciesGetter.get() : null;
+        if (actualDependencies == null && value != null) {
+            return value;
+        }
+        synchronized (lock) {
+            if (checkDependencyChangedAndUpdate(actualDependencies)) {
+                value = supplier.get();
+            }
+        }
+        return value;
+    }
+
+    private boolean checkDependencyChangedAndUpdate(Object actualDependencies) 
{
+        if (actualDependencies == null) {
+            return true;
+        }
+        if (actualDependencies instanceof Object[] actualDependencyList) {
+            boolean isSame = dependencyHashCodes.length == 
actualDependencyList.length;
+            int[] actualDependencyHashCodes = new 
int[actualDependencyList.length];
+            for (int i = 0; i < actualDependencyList.length; i++) {
+                actualDependencyHashCodes[i] = 
getHashCode(actualDependencyList[i]);
+                if (isSame) {
+                    isSame = dependencyHashCodes[i] == 
actualDependencyHashCodes[i];
+                }
+            }
+            if (!isSame) {
+                dependencyHashCodes = actualDependencyHashCodes;
+            }
+            return !isSame;
+        } else {
+            final int[] actualDependencyHashCodes = { 
getHashCode(actualDependencies) };
+            final boolean isSame = dependencyHashCodes.length == 
actualDependencyHashCodes.length
+                    && dependencyHashCodes[0] == actualDependencyHashCodes[0];
+            if (!isSame) {
+                dependencyHashCodes = actualDependencyHashCodes;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private int getHashCode(Object dependency) {
+        if (dependency == null) {
+            return 0;
+        }
+        return useReferenceCheck ? System.identityHashCode(dependency) : 
dependency.hashCode();
+    }
+
+    public static <T> Memo<T> of(Supplier<? extends T> supplier) {
+        return new Memo<>(supplier, null, false);
+    }
+
+    public static <T> Memo<T> of(Supplier<? extends T> supplier, 
Supplier<Object> dependenciesFunction) {
+        return new Memo<>(supplier, dependenciesFunction, false);
+    }
+
+    public static <T> Memo<T> of(Supplier<? extends T> supplier, 
Supplier<Object> dependenciesFunction, boolean useReferenceCheck) {
+        return new Memo<>(supplier, dependenciesFunction, useReferenceCheck);
+    }
+}
diff --git 
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
 
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
index 6aebb4ad5..6d3de1cd4 100644
--- 
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
+++ 
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
@@ -46,6 +46,7 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
@@ -180,6 +181,58 @@ class ProgressiveEMICalculatorTest {
         Assertions.assertEquals(121, 
interestScheduleModel.getLoanTermInDays());
     }
 
+    @Test
+    @Timeout(1) // seconds
+    public void testEMICalculation_performance() {
+
+        final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods 
= new ArrayList<>();
+
+        expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), 
LocalDate.of(2024, 2, 1)));
+        expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), 
LocalDate.of(2024, 3, 1)));
+        expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), 
LocalDate.of(2024, 4, 1)));
+        expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), 
LocalDate.of(2024, 5, 1)));
+        expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), 
LocalDate.of(2024, 6, 1)));
+        expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), 
LocalDate.of(2024, 7, 1)));
+        expectedRepaymentPeriods.add(repayment(7, LocalDate.of(2024, 7, 1), 
LocalDate.of(2024, 8, 1)));
+        expectedRepaymentPeriods.add(repayment(8, LocalDate.of(2024, 8, 1), 
LocalDate.of(2024, 9, 1)));
+        expectedRepaymentPeriods.add(repayment(9, LocalDate.of(2024, 9, 1), 
LocalDate.of(2024, 10, 1)));
+        expectedRepaymentPeriods.add(repayment(10, LocalDate.of(2024, 10, 1), 
LocalDate.of(2024, 11, 1)));
+        expectedRepaymentPeriods.add(repayment(11, LocalDate.of(2024, 11, 1), 
LocalDate.of(2024, 12, 1)));
+        expectedRepaymentPeriods.add(repayment(12, LocalDate.of(2024, 12, 1), 
LocalDate.of(2025, 1, 1)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(7.0);
+        final Integer installmentAmountInMultiplesOf = null;
+
+        
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+        
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
+
+        final ProgressiveLoanInterestScheduleModel interestSchedule = 
emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
+                loanProductRelatedDetail, installmentAmountInMultiplesOf);
+
+        final Money disbursedAmount = toMoney(100.0);
+        emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 
1), disbursedAmount);
+
+        Assertions.assertEquals(interestSchedule.getLoanTermInDays(), 366);
+        Assertions.assertEquals(interestSchedule.repaymentPeriods().size(), 
12);
+
+        List<RepaymentPeriod> repaymentPeriods = 
interestSchedule.repaymentPeriods();
+        for (int i = 0; i < repaymentPeriods.size(); i++) {
+            final RepaymentPeriod repaymentPeriod = repaymentPeriods.get(i);
+            Assertions.assertTrue(0 < 
toDouble(repaymentPeriod.getDuePrincipal().getAmount()));
+            Assertions.assertTrue(0 < 
toDouble(repaymentPeriod.getDueInterest().getAmount()));
+            if (i == repaymentPeriods.size() - 1) {
+                Assertions.assertEquals(0.0, 
toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
+            } else {
+                Assertions.assertEquals(8.65, 
toDouble(repaymentPeriod.getEmi().getAmount()));
+                Assertions.assertTrue(0 < 
toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
+            }
+        }
+    }
+
     @Test
     public void 
testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month()
 {
 

Reply via email to