mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-15 21:48:50 -06:00
Implement all relevant card views for habit groups
This commit is contained in:
@@ -24,12 +24,12 @@ import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import java.util.ArrayList
|
||||
import java.util.Calendar
|
||||
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 {
|
||||
@@ -151,6 +151,22 @@ open class EntryList {
|
||||
return map
|
||||
}
|
||||
|
||||
@Synchronized fun normalizeEntries(
|
||||
isNumerical: Boolean,
|
||||
frequency: Frequency,
|
||||
targetValue: Double
|
||||
): EntryList {
|
||||
val entries = getKnown()
|
||||
val normalized = EntryList()
|
||||
val dailyTarget = frequency.toDouble() * (if (isNumerical) targetValue else 0.001)
|
||||
for (entry in entries) {
|
||||
if (!isNumerical && entry.value != YES_MANUAL) continue
|
||||
val newValue = (entry.value.toDouble() / dailyTarget).roundToInt()
|
||||
normalized.add(Entry(entry.timestamp, newValue))
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) {
|
||||
val length: Int
|
||||
get() = begin.daysUntil(end) + 1
|
||||
@@ -318,6 +334,16 @@ fun List<Entry>.groupedSum(
|
||||
}
|
||||
}
|
||||
|
||||
fun List<Entry>.groupedSum(): List<Entry> {
|
||||
return this
|
||||
.groupBy { entry -> entry.timestamp }
|
||||
.entries.map { (timestamp, entries) ->
|
||||
Entry(timestamp, entries.sumOf { it.value })
|
||||
}.sortedBy { (timestamp, _) ->
|
||||
-timestamp.unixTime
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of days with vaLue SKIP in the given period.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,10 @@ import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.models.HabitGroup
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardPresenter
|
||||
@@ -14,16 +18,24 @@ import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCartPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardState
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
|
||||
data class ShowHabitGroupState(
|
||||
val title: String = "",
|
||||
val isEmpty: Boolean = false,
|
||||
val isNumerical: Boolean = false,
|
||||
val isBoolean: Boolean = false,
|
||||
val color: PaletteColor = PaletteColor(1),
|
||||
val subtitle: SubtitleCardState,
|
||||
val overview: OverviewCardState,
|
||||
val notes: NotesCardState,
|
||||
val target: TargetCardState,
|
||||
val streaks: StreakCardState,
|
||||
val scores: ScoreCardState,
|
||||
val frequency: FrequencyCardState,
|
||||
val bar: BarCardState,
|
||||
val theme: Theme
|
||||
)
|
||||
|
||||
@@ -33,6 +45,12 @@ class ShowHabitGroupPresenter(
|
||||
val screen: Screen,
|
||||
val commandRunner: CommandRunner
|
||||
) {
|
||||
|
||||
val barCardPresenter = BarCardPresenter(
|
||||
preferences = preferences,
|
||||
screen = screen
|
||||
)
|
||||
|
||||
val scoreCardPresenter = ScoreCardPresenter(
|
||||
preferences = preferences,
|
||||
screen = screen
|
||||
@@ -46,6 +64,9 @@ class ShowHabitGroupPresenter(
|
||||
): ShowHabitGroupState {
|
||||
return ShowHabitGroupState(
|
||||
title = habitGroup.name,
|
||||
isEmpty = habitGroup.habitList.isEmpty,
|
||||
isNumerical = habitGroup.habitList.all { it.isNumerical },
|
||||
isBoolean = habitGroup.habitList.all { !it.isNumerical },
|
||||
color = habitGroup.color,
|
||||
theme = theme,
|
||||
subtitle = SubtitleCardPresenter.buildState(
|
||||
@@ -59,6 +80,11 @@ class ShowHabitGroupPresenter(
|
||||
notes = NotesCardPresenter.buildState(
|
||||
habitGroup = habitGroup
|
||||
),
|
||||
target = TargetCardPresenter.buildState(
|
||||
habitGroup = habitGroup,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
theme = theme
|
||||
),
|
||||
streaks = StreakCartPresenter.buildState(
|
||||
habitGroup = habitGroup,
|
||||
theme = theme
|
||||
@@ -68,11 +94,24 @@ class ShowHabitGroupPresenter(
|
||||
habitGroup = habitGroup,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
theme = theme
|
||||
),
|
||||
frequency = FrequencyCardPresenter.buildState(
|
||||
habitGroup = habitGroup,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
theme = theme
|
||||
),
|
||||
bar = BarCardPresenter.buildState(
|
||||
habitGroup = habitGroup,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
|
||||
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
|
||||
theme = theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface Screen :
|
||||
BarCardPresenter.Screen,
|
||||
ScoreCardPresenter.Screen
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
|
||||
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitGroup
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.groupedSum
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
@@ -74,6 +75,53 @@ class BarCardPresenter(
|
||||
boolSpinnerPosition = boolSpinnerPosition
|
||||
)
|
||||
}
|
||||
|
||||
fun buildState(
|
||||
habitGroup: HabitGroup,
|
||||
firstWeekday: Int,
|
||||
numericalSpinnerPosition: Int,
|
||||
boolSpinnerPosition: Int,
|
||||
theme: Theme
|
||||
): BarCardState {
|
||||
val isNumerical = habitGroup.habitList.all { it.isNumerical }
|
||||
val isBoolean = habitGroup.habitList.all { !it.isNumerical }
|
||||
if ((!isNumerical && !isBoolean) || habitGroup.habitList.isEmpty) {
|
||||
return BarCardState(
|
||||
theme = theme,
|
||||
entries = listOf(Entry(DateUtils.getTodayWithOffset(), 0)),
|
||||
bucketSize = 1,
|
||||
color = habitGroup.color,
|
||||
isNumerical = isNumerical,
|
||||
numericalSpinnerPosition = numericalSpinnerPosition,
|
||||
boolSpinnerPosition = boolSpinnerPosition
|
||||
)
|
||||
}
|
||||
val bucketSize = if (isNumerical) {
|
||||
numericalBucketSizes[numericalSpinnerPosition]
|
||||
} else {
|
||||
boolBucketSizes[boolSpinnerPosition]
|
||||
}
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val allEntries = habitGroup.habitList.map { habit ->
|
||||
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
||||
habit.computedEntries.getByInterval(oldest, today).groupedSum(
|
||||
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
|
||||
firstWeekday = firstWeekday,
|
||||
isNumerical = habit.isNumerical
|
||||
)
|
||||
}.flatten()
|
||||
|
||||
val summedEntries = allEntries.groupedSum()
|
||||
return BarCardState(
|
||||
theme = theme,
|
||||
entries = summedEntries,
|
||||
bucketSize = bucketSize,
|
||||
color = habitGroup.color,
|
||||
isNumerical = isNumerical,
|
||||
numericalSpinnerPosition = numericalSpinnerPosition,
|
||||
boolSpinnerPosition = boolSpinnerPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNumericalSpinnerPosition(position: Int) {
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
package org.isoron.uhabits.core.ui.screens.habits.show.views
|
||||
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitGroup
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import java.util.HashMap
|
||||
|
||||
data class FrequencyCardState(
|
||||
val color: PaletteColor,
|
||||
@@ -48,5 +48,42 @@ class FrequencyCardPresenter {
|
||||
firstWeekday = firstWeekday,
|
||||
theme = theme
|
||||
)
|
||||
|
||||
fun buildState(
|
||||
habitGroup: HabitGroup,
|
||||
firstWeekday: Int,
|
||||
theme: Theme
|
||||
): FrequencyCardState {
|
||||
val normalizedEntries = habitGroup.habitList.map {
|
||||
it.computedEntries.normalizeEntries(it.isNumerical, it.frequency, it.targetValue)
|
||||
}
|
||||
val frequencies = normalizedEntries.map {
|
||||
it.computeWeekdayFrequency(isNumerical = true)
|
||||
}.reduce { acc, hashMap ->
|
||||
mergeMaps(acc, hashMap) { value1, value2 -> addArray(value1, value2) }
|
||||
}
|
||||
|
||||
return FrequencyCardState(
|
||||
color = habitGroup.color,
|
||||
isNumerical = true,
|
||||
frequency = frequencies,
|
||||
firstWeekday = firstWeekday,
|
||||
theme = theme
|
||||
)
|
||||
}
|
||||
|
||||
private fun <K, V> mergeMaps(map1: HashMap<K, V>, map2: HashMap<K, V>, mergeFunction: (V, V) -> V): HashMap<K, V> {
|
||||
val result = map1 // Step 1
|
||||
for ((key, value) in map2) { // Step 2
|
||||
result[key] = result[key]?.let { existingValue ->
|
||||
mergeFunction(existingValue, value) // Step 3 (merge logic)
|
||||
} ?: value
|
||||
}
|
||||
return result // Step 4
|
||||
}
|
||||
|
||||
private fun addArray(array1: Array<Int>, array2: Array<Int>): Array<Int> {
|
||||
return array1.zip(array2) { a, b -> a + b }.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package org.isoron.uhabits.core.ui.screens.habits.show.views
|
||||
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitGroup
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.models.countSkippedDays
|
||||
@@ -176,5 +177,66 @@ class TargetCardPresenter {
|
||||
if (thisWeek.daysUntil(newest) < 7) newest = thisWeek.plus(7)
|
||||
return Pair(yearBegin, newest)
|
||||
}
|
||||
|
||||
fun buildState(
|
||||
habitGroup: HabitGroup,
|
||||
firstWeekday: Int,
|
||||
theme: Theme
|
||||
): TargetCardState {
|
||||
val maxDen = habitGroup.habitList.maxOfOrNull { habit -> habit.frequency.denominator }
|
||||
val isNumerical = habitGroup.habitList.all { it.isNumerical }
|
||||
if (maxDen == null || !isNumerical) {
|
||||
return TargetCardState(
|
||||
color = habitGroup.color,
|
||||
values = arrayListOf(0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
targets = arrayListOf(0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
intervals = arrayListOf(1, 7, 30, 91, 365),
|
||||
theme = theme
|
||||
)
|
||||
}
|
||||
|
||||
val states = habitGroup.habitList.map { Companion.buildState(it, firstWeekday, theme) }
|
||||
|
||||
val values = states
|
||||
.map {
|
||||
val startIdx = it.intervals.indexOf(maxDen)
|
||||
val endIdx = it.intervals.size
|
||||
it.values.subList(startIdx, endIdx)
|
||||
}
|
||||
.reduce { acc, list ->
|
||||
acc.zip(list) { a, b -> a + b }
|
||||
}
|
||||
|
||||
val targets = states
|
||||
.map {
|
||||
val startIdx = it.intervals.indexOf(maxDen)
|
||||
val endIdx = it.intervals.size
|
||||
it.targets.subList(startIdx, endIdx)
|
||||
}
|
||||
.reduce { acc, list ->
|
||||
acc.zip(list) { a, b -> a + b }
|
||||
}
|
||||
|
||||
val intervals = arrayListOf(1, 7, 30, 91, 365).filter { it >= maxDen }
|
||||
|
||||
return TargetCardState(
|
||||
color = habitGroup.color,
|
||||
values = values,
|
||||
targets = targets,
|
||||
intervals = intervals,
|
||||
theme = theme
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getYearRange(firstWeekday: Int): Pair<Timestamp, Timestamp> {
|
||||
val today = DateUtils.getTodayWithOffset()
|
||||
val yearBegin = today.truncate(DateUtils.TruncateField.YEAR, firstWeekday)
|
||||
val cali = yearBegin.toCalendar()
|
||||
cali.add(Calendar.YEAR, 1)
|
||||
var newest = Timestamp(cali)
|
||||
val thisWeek = today.truncate(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday)
|
||||
if (thisWeek.daysUntil(newest) < 7) newest = thisWeek.plus(7)
|
||||
return Pair(yearBegin, newest)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user