Merge pull request #1101 from KristianTashkov/kris/implement_at_most

Implement numerical habits with AT_MOST target type
This commit is contained in:
2021-09-29 04:20:13 -05:00
committed by GitHub
29 changed files with 407 additions and 56 deletions

View File

@@ -31,5 +31,6 @@ data class CreateHabitCommand(
val habit = modelFactory.buildHabit()
habit.copyFrom(model)
habitList.add(habit)
habit.recompute()
}
}

View File

@@ -88,14 +88,16 @@ data class Habit(
isNumerical = isNumerical,
)
val to = DateUtils.getTodayWithOffset().plus(30)
val today = DateUtils.getTodayWithOffset()
val to = today.plus(30)
val entries = computedEntries.getKnown()
var from = entries.lastOrNull()?.timestamp ?: to
var from = entries.lastOrNull()?.timestamp ?: today
if (from.isNewerThan(to)) from = to
scores.recompute(
frequency = frequency,
isNumerical = isNumerical,
numericalHabitType = targetType,
targetValue = targetValue,
computedEntries = computedEntries,
from = from,

View File

@@ -68,19 +68,19 @@ class ScoreList {
fun recompute(
frequency: Frequency,
isNumerical: Boolean,
numericalHabitType: NumericalHabitType,
targetValue: Double,
computedEntries: EntryList,
from: Timestamp,
to: Timestamp,
) {
map.clear()
if (computedEntries.getKnown().isEmpty()) return
if (from.isNewerThan(to)) return
var rollingSum = 0.0
var numerator = frequency.numerator
var denominator = frequency.denominator
val freq = frequency.toDouble()
val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray()
val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST
// For non-daily boolean habits, we double the numerator and the denominator to smooth
// out irregular repetition schedules (for example, weekly habits performed on different
@@ -90,19 +90,29 @@ class ScoreList {
denominator *= 2
}
var previousValue = 0.0
var previousValue = if (isNumerical && isAtMost) 1.0 else 0.0
for (i in values.indices) {
val offset = values.size - i - 1
if (isNumerical) {
rollingSum += max(0, values[offset])
if (offset + denominator < values.size) {
rollingSum -= values[offset + denominator]
rollingSum -= max(0, values[offset + denominator])
}
val percentageCompleted = if (targetValue > 0) {
min(1.0, rollingSum / 1000 / targetValue)
val normalizedRollingSum = rollingSum / 1000
val percentageCompleted = if (!isAtMost) {
if (targetValue > 0)
min(1.0, normalizedRollingSum / targetValue)
else
1.0
} else {
1.0
if (targetValue > 0) {
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(0.0, 1.0)
} else {
if (normalizedRollingSum > 0) 0.0 else 1.0
}
}
previousValue = compute(freq, previousValue, percentageCompleted)
} else {
if (values[offset] == Entry.YES_MANUAL) {

View File

@@ -50,6 +50,19 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
return habit
}
fun createEmptyNumericalHabit(targetType: NumericalHabitType): Habit {
val habit = modelFactory.buildHabit()
habit.type = HabitType.NUMERICAL
habit.name = "Run"
habit.question = "How many miles did you run today?"
habit.unit = "miles"
habit.targetType = targetType
habit.targetValue = 2.0
habit.color = PaletteColor(1)
saveIfSQLite(habit)
return habit
}
fun createLongHabit(): Habit {
val habit = createEmptyHabit()
habit.frequency = Frequency(3, 7)

View File

@@ -29,6 +29,7 @@ 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.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
@@ -44,6 +45,7 @@ data class HistoryCardState(
val color: PaletteColor,
val firstWeekday: DayOfWeek,
val series: List<HistoryChart.Square>,
val defaultSquare: HistoryChart.Square,
val theme: Theme,
val today: LocalDate,
)
@@ -105,12 +107,19 @@ class HistoryCardPresenter(
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today)
val series = if (habit.isNumerical) {
entries.map {
Entry(it.timestamp, max(0, it.value))
}.map {
when (it.value) {
0 -> HistoryChart.Square.OFF
else -> HistoryChart.Square.ON
if (habit.targetType == NumericalHabitType.AT_LEAST) {
entries.map {
when (max(0, it.value)) {
0 -> HistoryChart.Square.OFF
else -> HistoryChart.Square.ON
}
}
} else {
entries.map {
when {
max(0.0, it.value / 1000.0) <= habit.targetValue -> HistoryChart.Square.ON
else -> HistoryChart.Square.OFF
}
}
}
} else {
@@ -123,6 +132,10 @@ class HistoryCardPresenter(
}
}
}
val defaultSquare = if (habit.isNumerical && habit.targetType == NumericalHabitType.AT_MOST)
HistoryChart.Square.ON
else
HistoryChart.Square.OFF
return HistoryCardState(
color = habit.color,
@@ -130,6 +143,7 @@ class HistoryCardPresenter(
today = today.toLocalDate(),
theme = theme,
series = series,
defaultSquare = defaultSquare
)
}
}

View File

@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.ui.views.Theme
@@ -31,8 +32,9 @@ data class SubtitleCardState(
val isNumerical: Boolean,
val question: String,
val reminder: Reminder?,
val targetValue: Double,
val unit: String,
val targetValue: Double = 0.0,
val targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
val unit: String = "",
val theme: Theme,
)
@@ -48,6 +50,7 @@ class SubtitleCardPresenter {
question = habit.question,
reminder = habit.reminder,
targetValue = habit.targetValue,
targetType = habit.targetType,
unit = habit.unit,
theme = theme,
)

View File

@@ -41,6 +41,7 @@ class HistoryChart(
var firstWeekday: DayOfWeek,
var paletteColor: PaletteColor,
var series: List<Square>,
var defaultSquare: Square,
var theme: Theme,
var today: LocalDate,
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { },
@@ -189,7 +190,7 @@ class HistoryChart(
offset: Int,
) {
val value = if (offset >= series.size) Square.OFF else series[offset]
val value = if (offset >= series.size) defaultSquare else series[offset]
val squareColor: Color
val color = theme.color(paletteColor.paletteIndex)
squareColor = when (value) {