diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2140a37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/src/main/kotlin/dev/darktale/AccrualCalculator.kt b/src/main/kotlin/dev/darktale/AccrualCalculator.kt new file mode 100644 index 0000000..8b2ecb6 --- /dev/null +++ b/src/main/kotlin/dev/darktale/AccrualCalculator.kt @@ -0,0 +1,60 @@ +package dev.darktale + +import dev.darktale.model.Asset +import dev.darktale.model.CompoundInterestDeposit +import dev.darktale.model.Deposit +import java.math.BigDecimal +import java.time.LocalDate + +interface AccrualCalculator { + fun calculateAccrual( + asset: Asset, + dateFrom: LocalDate? = null, + dateTo: LocalDate + ): BigDecimal +} + +class DepositAccrualCalculator : AccrualCalculator { + override fun calculateAccrual( + asset: Asset, + dateFrom: LocalDate?, + dateTo: LocalDate, + ): BigDecimal { + val deposit = asset as Deposit + val startDate = asset.dateFrom + var curDateFrom = startDate + var curDateTo: LocalDate? = deposit.nextAccrualDate(curDateFrom) + var calculatedAccrual = BigDecimal.ZERO + while (curDateTo != null && curDateTo <= dateTo) { + if (curDateFrom.year == curDateTo.year) + calculatedAccrual += accrualBetween( + curDateFrom, curDateTo, + asset.quantity + calculatedAccrual, + deposit.percent + ) + else + calculatedAccrual += (accrualBetween( + curDateFrom, LocalDate.of(curDateFrom.year, 12, 31), + asset.quantity + calculatedAccrual, + deposit.percent + ) + accrualBetween( + LocalDate.of(curDateTo.year, 1, 1), curDateTo, + asset.quantity + calculatedAccrual, + deposit.percent + )) + curDateFrom = curDateTo + curDateTo = deposit.nextAccrualDate(curDateFrom) + } + return calculatedAccrual - if (dateFrom != null && dateFrom > asset.dateFrom) + calculateAccrual(asset, asset.dateFrom, startDate) else BigDecimal.ZERO + } + + private fun accrualBetween( + dateFrom: LocalDate, + dateTo: LocalDate, + baseAmount: BigDecimal, + percent: BigDecimal, + ) = baseAmount * percent * + BigDecimal.valueOf((dateTo.dayOfYear - dateFrom.dayOfYear + 1).toLong()) / + BigDecimal.valueOf(dateFrom.lengthOfYear().toLong()) +} \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/dev/darktale/Main.kt similarity index 66% rename from src/main/kotlin/Main.kt rename to src/main/kotlin/dev/darktale/Main.kt index 732adf1..003a564 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/dev/darktale/Main.kt @@ -1,5 +1,4 @@ -package org.example - +package dev.darktale fun main() { println("Hello World!") } \ No newline at end of file diff --git a/src/main/kotlin/dev/darktale/model/Asset.kt b/src/main/kotlin/dev/darktale/model/Asset.kt new file mode 100644 index 0000000..610a747 --- /dev/null +++ b/src/main/kotlin/dev/darktale/model/Asset.kt @@ -0,0 +1,58 @@ +package dev.darktale.model + +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalTime + +interface Asset { + val assetType: AssetType + var quantity: BigDecimal + var storagePlace: StoragePlace + val dateFrom: LocalDate + var dateTo: LocalDate? +} + +enum class AssetType { + TRADING_INSTRUMENT, + DEPOSIT; +} + +interface Deposit : Asset { + override val assetType: AssetType + get() = AssetType.DEPOSIT + var percent: BigDecimal + + fun nextAccrualDate(onDate: LocalDate): LocalDate? +} + +class CompoundInterestDeposit( + override var quantity: BigDecimal, + override var storagePlace: StoragePlace, + override var dateFrom: LocalDate, + override var dateTo: LocalDate?, + override var percent: BigDecimal, + var accrualDay: Int, +) : Deposit { + override fun nextAccrualDate(onDate: LocalDate): LocalDate? { + val currentMonthAccrualDate = LocalDate.of( + onDate.year, + onDate.month, + accrualDay.coerceAtMost(onDate.lengthOfMonth()) + ) + val calculatedDate = if (currentMonthAccrualDate > onDate) currentMonthAccrualDate + else currentMonthAccrualDate.plusMonths(1) + return calculatedDate.takeIf { it < dateTo } + } +} + +class SimpleInterestDeposit( + override var quantity: BigDecimal, + override var storagePlace: StoragePlace, + override var dateFrom: LocalDate, + override var dateTo: LocalDate?, + override var percent: BigDecimal, +) : Deposit { + override fun nextAccrualDate(onDate: LocalDate) = dateTo.takeIf { it != null && it > onDate } + + override fun toString() = "$quantity since $dateFrom till $dateTo with ${percent*BigDecimal.valueOf(100L)}%" +} \ No newline at end of file diff --git a/src/main/kotlin/model/StoragePlace.kt b/src/main/kotlin/dev/darktale/model/StoragePlace.kt similarity index 64% rename from src/main/kotlin/model/StoragePlace.kt rename to src/main/kotlin/dev/darktale/model/StoragePlace.kt index 335a251..8541ae2 100644 --- a/src/main/kotlin/model/StoragePlace.kt +++ b/src/main/kotlin/dev/darktale/model/StoragePlace.kt @@ -1,4 +1,4 @@ -package org.example.model +package dev.darktale.model interface StoragePlace { var name: String diff --git a/src/main/kotlin/model/Asset.kt b/src/main/kotlin/model/Asset.kt deleted file mode 100644 index 9af346a..0000000 --- a/src/main/kotlin/model/Asset.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.example.model - -import java.math.BigDecimal - -interface Asset { - var quantity: BigDecimal - var assetType: AssetType - var storagePlace: StoragePlace -} - -enum class AssetType { - TRADING_INSTRUMENT, - DEPOSIT; -} \ No newline at end of file diff --git a/src/test/java/dev/darktale/DepositAccrualCalculatorTest.kt b/src/test/java/dev/darktale/DepositAccrualCalculatorTest.kt new file mode 100644 index 0000000..87dfb34 --- /dev/null +++ b/src/test/java/dev/darktale/DepositAccrualCalculatorTest.kt @@ -0,0 +1,53 @@ +package dev.darktale + +import dev.darktale.model.Deposit +import dev.darktale.model.SimpleInterestDeposit +import dev.darktale.model.StoragePlace +import org.junit.jupiter.api.Assertions.assertEquals +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.time.LocalTime +import java.util.stream.Stream + +class DepositAccrualCalculatorTest { + @ParameterizedTest + @ArgumentsSource(DepositProvider::class) + fun testAccrual(deposit: Deposit, dateFrom: LocalDate, dateTo:LocalDate, expected: BigDecimal) { + val accrued = DepositAccrualCalculator().calculateAccrual(deposit, dateFrom, dateTo).stripTrailingZeros() + + assertEquals( + 0, expected.compareTo(accrued), + "expected $expected, actual $accrued" + ) + } +} + +class DepositProvider : ArgumentsProvider { + private val testStoragePlace = object : StoragePlace { + override var name: String = "Qwe" + } + + override fun provideArguments(context: ExtensionContext): Stream { + val yearStart = LocalDate.of(2024, 1, 1) + val yearEnd = LocalDate.of(2024, 12, 31) + + val deposit = SimpleInterestDeposit( + BigDecimal.valueOf(1000L), + testStoragePlace, + yearStart, + yearEnd, + BigDecimal.valueOf(0.1), + ) + + return Stream.of( + Arguments.of(deposit, yearStart, yearEnd, BigDecimal.valueOf(100L)), + Arguments.of(deposit, yearStart, yearEnd.minusDays(1), BigDecimal.ZERO), + Arguments.of(deposit, yearEnd.minusDays(1), yearEnd, BigDecimal.valueOf(100L)), + ) + } +}