diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index 53b92541c..5bad8f507 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -30,6 +30,7 @@ import javax.annotation.concurrent.ThreadSafe import kotlin.collections.set import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt @ThreadSafe open class EntryList { @@ -318,6 +319,53 @@ fun List.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.groupedAverage( + truncateField: DateUtils.TruncateField, + firstWeekday: Int = Calendar.SATURDAY, + isNumerical: Boolean +): List = + 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. */ diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt index 54ec93499..bab06a5f7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt @@ -23,6 +23,8 @@ import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.PaletteColor 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.ui.views.Theme import org.isoron.uhabits.core.utils.DateUtils @@ -59,11 +61,18 @@ class BarCardPresenter( } val today = DateUtils.getTodayWithOffset() val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today - val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum( - truncateField = ScoreCardPresenter.getTruncateField(bucketSize), - firstWeekday = firstWeekday, - isNumerical = habit.isNumerical - ) + val entries = when (habit.historyType) { + NumericalHistoryType.TOTAL -> habit.computedEntries.getByInterval(oldest, today).groupedSum( + truncateField = ScoreCardPresenter.getTruncateField(bucketSize), + firstWeekday = firstWeekday, + isNumerical = habit.isNumerical + ) + NumericalHistoryType.AVERAGE -> habit.computedEntries.getByInterval(oldest, today).groupedAverage( + truncateField = ScoreCardPresenter.getTruncateField(bucketSize), + firstWeekday = firstWeekday, + isNumerical = habit.isNumerical + ) + } return BarCardState( theme = theme, entries = entries,