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()
{