aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/firmament/features/mining/Histogram.kt
blob: e897aaad9ce89d5af946821b5ba77096176ec344 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*
 * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

package moe.nea.firmament.features.mining

import java.util.*
import kotlin.time.Duration
import moe.nea.firmament.util.TimeMark

class Histogram<T>(
    val maxSize: Int,
    val maxDuration: Duration,
) {

    data class OrderedTimestamp(val timestamp: TimeMark, val order: Int) : Comparable<OrderedTimestamp> {
        override fun compareTo(other: OrderedTimestamp): Int {
            val o = timestamp.compareTo(other.timestamp)
            if (o != 0) return o
            return order.compareTo(other.order)
        }
    }

    val size: Int get() = dataPoints.size
    private val dataPoints: NavigableMap<OrderedTimestamp, T> = TreeMap()

    private var order = Int.MIN_VALUE

    fun record(entry: T, timestamp: TimeMark = TimeMark.now()) {
        dataPoints[OrderedTimestamp(timestamp, order++)] = entry
        trim()
    }

    fun oldestUpdate(): TimeMark {
        trim()
        return if (dataPoints.isEmpty()) TimeMark.now() else dataPoints.firstKey().timestamp
    }

    fun latestUpdate(): TimeMark {
        trim()
        return if (dataPoints.isEmpty()) TimeMark.farPast() else dataPoints.lastKey().timestamp
    }

    fun averagePer(valueExtractor: (T) -> Double, perDuration: Duration): Double? {
        return aggregate(
            seed = 0.0,
            operator = { accumulator, entry, _ -> accumulator + valueExtractor(entry) },
            finish = { sum, beginning, end ->
                val timespan = end - beginning
                if (timespan > perDuration)
                    sum / (timespan / perDuration)
                else null
            })
    }

    fun <V, R> aggregate(
        seed: V,
        operator: (V, T, TimeMark) -> V,
        finish: (V, TimeMark, TimeMark) -> R
    ): R? {
        trim()
        var accumulator = seed
        var min: TimeMark? = null
        var max: TimeMark? = null
        dataPoints.forEach { (key, value) ->
            max = key.timestamp
            if (min == null)
                min = key.timestamp
            accumulator = operator(accumulator, value, key.timestamp)
        }
        if (min == null)
            return null
        return finish(accumulator, min!!, max!!)
    }

    private fun trim() {
        while (maxSize < dataPoints.size) {
            dataPoints.pollFirstEntry()
        }
        dataPoints.headMap(OrderedTimestamp(TimeMark.ago(maxDuration), Int.MAX_VALUE)).clear()
    }


}