tri.timeseries.TimeSeriesAggregate.kt Maven / Gradle / Ivy
The newest version!
/*-
* #%L
* coda-data
* --
* Copyright (C) 2020 - 2023 Elisha Peterson
* --
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package tri.timeseries
import tri.util.DateRange
import tri.util.minus
import tri.util.plus
import java.time.LocalDate
/**
* Provides various ways to aggregate date/value data into a [TimeSeries].
*/
enum class TimeSeriesAggregate(private val aggregator: (List>, LocalDate?) -> Map) {
/** Gets first entry for each date. */
FIRST({ pairs, _ ->
pairs.filter { it.second != null }.associate { it as Pair }.toSortedMap()
}),
/** Sums entries. */
SUM({ pairs, finalDate ->
if (pairs.isEmpty()) mapOf() else {
val dateValues = pairs.groupBy { it.first }.mapValues { it.value.map { it.second } }.toSortedMap()
associateDates(dateValues.keys, finalDate) {
val values = dateValues[it] ?: listOf()
val integers = values.all { it is Int }
it to if (integers) values.sumBy { it?.toInt() ?: 0 } else values.sumByDouble { it?.toDouble() ?: 0.0 }
}
}
}),
/** Takes a 7-day average, skipping any missing values. If there is more than one entry per date, sums across those dates. */
AVERAGE_7_DAY({ pairs, finalDate ->
if (pairs.isEmpty()) mapOf() else {
val dateValues = pairs.groupBy { it.first }.mapValues { it.value.map { it.second } }.toSortedMap()
associateDates(dateValues.keys, finalDate) {
it to dateValues.subMap(it - 6, it + 1).values
.mapNotNull { it.sumOrNull() }
.average()
}.filter { it.value.isFinite() }
}
}),
/** Fills latest value, ensuring gaps between missing entries are all filled. */
FILL_WITH_LATEST_VALUE({ pairs, finalDate ->
if (pairs.isEmpty()) mapOf() else {
val sortedDates = pairs.associate { it.first to it.second }.toSortedMap()
associateDates(sortedDates.keys, finalDate) {
it to (sortedDates[it] ?: sortedDates.headMap(it).values.last()!!)
}
}
}),
/** Fills latest value, but does not allow filling forward more than 7 days. */
FILL_WITH_LATEST_VALUE_UP_TO_7({ pairs, finalDate ->
if (pairs.isEmpty()) mapOf() else {
val sortedDates = pairs.associate { it.first to it.second?.toDouble() }.toSortedMap()
val first = sortedDates.keys.first()
val last = finalDate ?: sortedDates.keys.last()
val dates = DateRange(first..last).toList()
val values = dates.map { sortedDates[it] }
var lastValueIndex = 0
var lastValue = 0.0
values.mapIndexed { i, value ->
dates[i] to when {
value != null -> {
lastValueIndex = i
lastValue = value
value
}
i - lastValueIndex <= 7 -> lastValue
else -> 0.0
}
}.toMap().toSortedMap()
}
});
operator fun invoke(entries: List>, date: LocalDate?) = aggregator(entries, date)
companion object {
private fun associateDates(sortedKeys: Set, finalDate: LocalDate?, op: (LocalDate) -> Pair) =
DateRange(sortedKeys.first()..(finalDate ?: sortedKeys.last())).associate(op).toSortedMap()
private fun List.sumOrNull(): Double? = mapNotNull { it?.toDouble() }.filter { it.isFinite() }.let {
if (it.isEmpty()) null else it.sum()
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy