aboutsummaryrefslogtreecommitdiff
path: root/server/analysis/src/main/kotlin/moe
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-01-22 01:10:10 +0100
committerLinnea Gräf <nea@nea.moe>2025-01-22 01:10:10 +0100
commit6f148df84dfe5d0d0d1c6a0614f86e374fc8d1aa (patch)
tree1a49a6aeb9e7f901ede729f1fed9d1d230dadc87 /server/analysis/src/main/kotlin/moe
parent550441921eed03b88ec94bea10deb1c45ef6e17b (diff)
downloadLocalTransactionLedger-6f148df84dfe5d0d0d1c6a0614f86e374fc8d1aa.tar.gz
LocalTransactionLedger-6f148df84dfe5d0d0d1c6a0614f86e374fc8d1aa.tar.bz2
LocalTransactionLedger-6f148df84dfe5d0d0d1c6a0614f86e374fc8d1aa.zip
feat(server): Add first analysis
Diffstat (limited to 'server/analysis/src/main/kotlin/moe')
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt9
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt25
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt8
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt55
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt30
5 files changed, 127 insertions, 0 deletions
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<UUID>
+}
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<Visualization>
+) \ 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<LocalDate, Double>()
+ 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<DataPoint>
+)
+
+@Serializable
+data class DataPoint(
+ val time: @Serializable(InstantSerializer::class) Instant,
+ val value: Double,
+)
+
+object InstantSerializer : KSerializer<Instant> {
+ 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