Add support for average aggregation of entries

pull/2215/head
Janssen 3 weeks ago
parent fbdbcf4b97
commit f6eb741b9d

@ -30,6 +30,7 @@ import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
@ThreadSafe @ThreadSafe
open class EntryList { open class EntryList {
@ -318,6 +319,53 @@ fun List<Entry>.groupedSum(
} }
} }
/**
* Given a list of entries, truncates the timestamp of each entry (according to the field given),
* groups the entries according to this truncated timestamp, then creates a new entry (t,v) for
* each group, where t is the truncated timestamp and v is the average of the values of all
* non-empty entries in the group.
*
* For numerical habits, non-positive entry values are ignored when computing the average. For
* boolean habits, each YES_MANUAL value is converted to 1000 and all other values are converted
* to zero before averaging.
*
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest
* entry coming last. If the original list has gaps in it (for example, weeks or months without
* any entries), then the list produced by this method will also have gaps.
*
* The argument [firstWeekday] is only relevant when truncating by week.
*/
fun List<Entry>.groupedAverage(
truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean
): List<Entry> =
this
.map { (timestamp, value) ->
if (isNumerical) {
if (value == SKIP) Entry(timestamp, 0)
else Entry(timestamp, max(0, value))
} else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
}
}
.groupBy { entry ->
when (truncateField) {
DateUtils.TruncateField.WEEK_NUMBER ->
entry.timestamp.truncate(truncateField, firstWeekday)
else ->
entry.timestamp.truncate(truncateField, firstWeekday)
}
}
.entries.map { (timestamp, entries) ->
val nonEmpty = entries.filter { it.value > 0}
val avg = if (nonEmpty.isEmpty()) 0
else nonEmpty.map { it.value }.average().roundToInt()
Entry(timestamp, avg)
}
// 4) Sort buckets (latest first)
.sortedByDescending { (timestamp, _) -> timestamp.unixTime }
/** /**
* Counts the number of days with vaLue SKIP in the given period. * Counts the number of days with vaLue SKIP in the given period.
*/ */

@ -23,6 +23,8 @@ import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.models.groupedAverage
import org.isoron.uhabits.core.models.NumericalHistoryType
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.views.Theme import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
@ -59,11 +61,18 @@ class BarCardPresenter(
} }
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum( val entries = when (habit.historyType) {
truncateField = ScoreCardPresenter.getTruncateField(bucketSize), NumericalHistoryType.TOTAL -> habit.computedEntries.getByInterval(oldest, today).groupedSum(
firstWeekday = firstWeekday, truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
isNumerical = habit.isNumerical firstWeekday = firstWeekday,
) isNumerical = habit.isNumerical
)
NumericalHistoryType.AVERAGE -> habit.computedEntries.getByInterval(oldest, today).groupedAverage(
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical
)
}
return BarCardState( return BarCardState(
theme = theme, theme = theme,
entries = entries, entries = entries,

Loading…
Cancel
Save