2 Commits

Author SHA1 Message Date
vizitor 1e17c93c6a add rate calculator
Gitea Actions / Gitea-Actions (push) Successful in 18s
2025-11-03 23:43:36 +03:00
vizitor a19da9a161 gitea action added 2025-11-03 21:22:20 +03:00
4 changed files with 211 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
name: Gitea Actions
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Gitea-Actions:
runs-on: ubuntu-latest
steps:
- name: prepare
run: |
echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v5
- name: Set up Maven
uses: stCarolas/setup-maven@v5
with:
maven-version: '3.9.9'
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
@@ -0,0 +1,38 @@
package dev.darktale
import dev.darktale.model.Rate
import java.math.BigDecimal
import java.time.LocalDate
class AmountByRateCalculator {
private val leapYearDuration = BigDecimal.valueOf(366L)
private val nonLeapYearDuration = BigDecimal.valueOf(365L)
fun calculate(amount: BigDecimal, rate: Rate, dateFrom: LocalDate, dateTo: LocalDate): BigDecimal {
var result = amount
var curRate = rate.rateOnDate(dateFrom)
while (curRate != null && curRate.since < dateTo) {
val curDateFrom = max(curRate.since, dateFrom)
val curDateTo = min(dateTo, curRate.till)
result *= (BigDecimal.ONE + getRateForPeriod(curRate.rate!!, curDateFrom, curDateTo))
curRate = rate.rateOnDate(curDateTo.plusDays(1))?.let {
if (it === curRate) null else it
}
}
return result
}
private fun getRateForPeriod(rate: BigDecimal, dateFrom: LocalDate, dateTo: LocalDate): BigDecimal {
return if (dateFrom.year == dateTo.year) {
rate.setScale(20) *
(dateTo.dayOfYear - dateFrom.dayOfYear + 1).toBigDecimal() /
if (dateFrom.isLeapYear) leapYearDuration else nonLeapYearDuration
} else getRateForPeriod(rate, dateFrom, LocalDate.of(dateFrom.year, 12, 31)) +
getRateForPeriod(rate, LocalDate.of(dateTo.year, 1, 1), dateTo)
}
}
private fun max(localDate1: LocalDate, localDate2: LocalDate?) =
if (localDate2 == null || localDate1 >= localDate2) localDate1 else localDate2
private fun min(localDate1: LocalDate, localDate2: LocalDate?) =
if (localDate2 == null || localDate1 <= localDate2) localDate1 else localDate2
@@ -0,0 +1,44 @@
package dev.darktale.model
import java.math.BigDecimal
import java.time.LocalDate
import java.util.*
sealed class Rate {
open val rate: BigDecimal? = null
abstract val since: LocalDate
open val till: LocalDate? = null
abstract fun rateOnDate(date: LocalDate): Rate?
override fun toString() = "Rate($rate since $since ${till?.let { "till $it" }}"
}
class SimpleRate(
override val rate: BigDecimal,
override val since: LocalDate,
override val till: LocalDate? = null,
) : Rate() {
constructor(rate: Double, since: LocalDate, till: LocalDate?) :
this(BigDecimal.valueOf(rate), since, till)
override fun rateOnDate(date: LocalDate) =
if (date >= since && (till == null || date <= till)) this else null
}
class PeriodicRate(
private val rates: TreeMap<LocalDate, Rate>
) : Rate() {
constructor(vararg rates: Rate) : this(TreeMap(rates.associateBy { it.since }))
override val since: LocalDate
get() = rates.firstKey()
override fun rateOnDate(date: LocalDate) =
rates.floorEntry(date)?.value?.let {
if (it.till == null || it.till!! >= date) it else null
}
override fun toString() = rates.values.toString()
}
@@ -0,0 +1,98 @@
package dev.darktale
import dev.darktale.model.PeriodicRate
import dev.darktale.model.Rate
import dev.darktale.model.SimpleRate
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.ArgumentsProvider
import org.junit.jupiter.params.provider.ArgumentsSource
import java.math.BigDecimal
import java.time.LocalDate
import java.util.stream.Stream
import kotlin.math.abs
import kotlin.math.pow
import kotlin.test.DefaultAsserter.assertTrue
class AmountByRateCalculatorTest {
private val amountByRateCalculator = AmountByRateCalculator()
@ParameterizedTest
@ArgumentsSource(RateProvider::class)
fun testRateCalculator(
rate: Rate,
sourceAmount: BigDecimal,
dateFrom: LocalDate,
dateTo: LocalDate,
expectedAmount: BigDecimal
) {
val result = amountByRateCalculator.calculate(sourceAmount, rate, dateFrom, dateTo)
val maxDelta = 10.0.pow(-expectedAmount.scale().toDouble())
assertTrue(
"expected amount was $expectedAmount ± ${
BigDecimal.valueOf(maxDelta).toPlainString()
}, but actual is $result",
abs((expectedAmount - result).toDouble()) <= maxDelta,
)
}
}
class RateProvider : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext): Stream<out Arguments> {
val baseAmount = BigDecimal.valueOf(1000L)
val startOfYear = LocalDate.of(2025, 1, 1)
val endOfMonth = LocalDate.of(2025, 1, 31)
val endOfMonth2 = LocalDate.of(2025, 2, 28)
val endOfYear = LocalDate.of(2025, 12, 31)
val middleOfYear = startOfYear.plusDays((endOfYear.dayOfYear / 2).toLong())
val simpleRate = SimpleRate(0.1, startOfYear, endOfYear)
val periodicRate = PeriodicRate(
SimpleRate(0.1, startOfYear, endOfMonth),
SimpleRate(0.1, endOfMonth.plusDays(1), endOfMonth2)
)
val decemberOfNonLeapYear = LocalDate.of(2023, 12, 1)
val januaryOfLeapYear = LocalDate.of(2024, 1, 31)
val nonLeapAndLeapYearRate = SimpleRate(
0.1,
decemberOfNonLeapYear,
januaryOfLeapYear
)
return Stream.of(
Arguments.of(
simpleRate, baseAmount, startOfYear, endOfYear, BigDecimal.valueOf(1100L)
),
Arguments.of(
simpleRate,
baseAmount,
startOfYear,
middleOfYear,
BigDecimal.valueOf(1050.137)
),
Arguments.of(
periodicRate,
baseAmount,
startOfYear,
endOfMonth2,
BigDecimal.valueOf(1016.2295)
),
Arguments.of(
periodicRate,
baseAmount,
startOfYear,
middleOfYear,
BigDecimal.valueOf(1016.2295)
),
Arguments.of(
nonLeapAndLeapYearRate,
baseAmount,
nonLeapAndLeapYearRate.since,
nonLeapAndLeapYearRate.till,
BigDecimal.valueOf(1016.963096)
)
)
}
}