FINERACT-1981: EMI Calculator performance optimization

This commit is contained in:
Janos Meszaros
2024-10-17 17:33:37 +02:00
committed by Adam Saghy
parent c8e6adba1e
commit f94e293d1e
3 changed files with 196 additions and 10 deletions
@@ -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) {
@@ -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);
}
}