From 6f148df84dfe5d0d0d1c6a0614f86e374fc8d1aa Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Wed, 22 Jan 2025 01:10:10 +0100 Subject: feat(server): Add first analysis --- .../kotlin/moe/nea/ledger/analysis/Analysis.kt | 9 ++++ .../moe/nea/ledger/analysis/AnalysisFilter.kt | 25 ++++++++++ .../moe/nea/ledger/analysis/AnalysisResult.kt | 8 ++++ .../nea/ledger/analysis/CoinsSpentOnAuctions.kt | 55 ++++++++++++++++++++++ .../moe/nea/ledger/analysis/Visualization.kt | 30 ++++++++++++ 5 files changed, 127 insertions(+) create mode 100644 server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt create mode 100644 server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt create mode 100644 server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt create mode 100644 server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt create mode 100644 server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt (limited to 'server/analysis/src/main') diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt new file mode 100644 index 0000000..abcf8ed --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt @@ -0,0 +1,9 @@ +package moe.nea.ledger.analysis + +import java.sql.Connection + +interface Analysis { + val id: String + val name: String + fun perform(database: Connection, filter: AnalysisFilter): AnalysisResult +} \ No newline at end of file diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt new file mode 100644 index 0000000..b9178cc --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt @@ -0,0 +1,25 @@ +package moe.nea.ledger.analysis + +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.Query +import moe.nea.ledger.database.sql.Clause +import moe.nea.ledger.utils.ULIDWrapper +import java.time.Instant +import java.time.ZoneId +import java.util.UUID + +interface AnalysisFilter { + fun applyTo(query: Query) { + query.where(Clause { column(DBLogEntry.transactionId) ge string(ULIDWrapper.lowerBound(startWindow).wrapped) }) + .where(Clause { column(DBLogEntry.transactionId) le string(ULIDWrapper.upperBound(endWindow).wrapped) }) + // TODO: apply profiles filter + } + + fun timeZone(): ZoneId { + return ZoneId.systemDefault() + } + + val startWindow: Instant + val endWindow: Instant + val profiles: List +} diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt new file mode 100644 index 0000000..4ad47f7 --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt @@ -0,0 +1,8 @@ +package moe.nea.ledger.analysis + +import kotlinx.serialization.Serializable + +@Serializable +data class AnalysisResult( + val visualizations: List +) \ No newline at end of file diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt new file mode 100644 index 0000000..5e2e633 --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt @@ -0,0 +1,55 @@ +package moe.nea.ledger.analysis + +import com.google.auto.service.AutoService +import moe.nea.ledger.ItemChange +import moe.nea.ledger.ItemId +import moe.nea.ledger.TransactionType +import moe.nea.ledger.database.DBItemEntry +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.sql.Clause +import java.sql.Connection +import java.time.LocalDate + +@AutoService(Analysis::class) +class CoinsSpentOnAuctions : Analysis { + override val name: String + get() = "Shopping Costs" + override val id: String + get() = "coins-spent-on-auctions" + + override fun perform(database: Connection, filter: AnalysisFilter): AnalysisResult { + val query = DBLogEntry.from(database) + .join(DBItemEntry, Clause { column(DBItemEntry.transactionId) eq column(DBLogEntry.transactionId) }) + .where(Clause { + (column(DBItemEntry.itemId) eq string(ItemId.COINS.string)) + }) + .where(Clause { + (column(DBItemEntry.mode) eq string(ItemChange.ChangeDirection.LOST.name)) + }) + .where(Clause { + (column(DBLogEntry.type) eq string(TransactionType.AUCTION_BOUGHT.name)) + }) + .select(DBItemEntry.size, DBLogEntry.transactionId) + filter.applyTo(query) + val spentThatDay = mutableMapOf() + for (resultRow in query) { + val timestamp = resultRow[DBLogEntry.transactionId].getTimestamp() + val damage = resultRow[DBItemEntry.size] + val localZone = filter.timeZone() + val localDate = timestamp.atZone(localZone).toLocalDate() + spentThatDay.merge(localDate, damage) { a, b -> a + b } + } + return AnalysisResult( + listOf( + Visualization( + "Coins spent on auctions", + xLabel = "Time", + yLabel = "Coins Spent that day", + dataPoints = spentThatDay.entries.map { (k, v) -> + DataPoint(k.atTime(12, 0).atZone(filter.timeZone()).toInstant(), v) + } + ) + ) + ) + } +} \ No newline at end of file diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt new file mode 100644 index 0000000..d0c0d56 --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt @@ -0,0 +1,30 @@ +package moe.nea.ledger.analysis + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.Instant + +@Serializable +data class Visualization( + val label: String, + val xLabel: String, + val yLabel: String, + val dataPoints: List +) + +@Serializable +data class DataPoint( + val time: @Serializable(InstantSerializer::class) Instant, + val value: Double, +) + +object InstantSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.time.Instant", PrimitiveKind.LONG) + override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.toEpochMilli()) + override fun deserialize(decoder: Decoder): Instant = Instant.ofEpochMilli(decoder.decodeLong()) +} \ No newline at end of file -- cgit