From 804edfa64e15b8d2aa3ba19de9b8c260d8063801 Mon Sep 17 00:00:00 2001 From: KristianTashkov Date: Sat, 11 Sep 2021 23:23:52 +0300 Subject: [PATCH] Implement numerical habits with AT_MOST target type --- .../habits/edit/EditHabitActivity.kt | 10 ++++- .../habits/list/views/HabitCardListView.kt | 9 ++++- .../habits/list/views/HabitCardView.kt | 16 ++++++-- .../habits/list/views/NumberButtonView.kt | 40 ++++++++++++++----- .../habits/list/views/NumberPanelView.kt | 19 ++++++++- .../main/res/layout/activity_edit_habit.xml | 23 +++++++++++ .../src/main/res/values/strings.xml | 3 ++ .../org/isoron/uhabits/core/models/Habit.kt | 11 +++-- .../isoron/uhabits/core/models/ScoreList.kt | 32 ++++++++++++--- .../screens/habits/show/views/HistoryCard.kt | 22 +++++++--- .../uhabits/core/models/ScoreListTest.kt | 4 ++ 11 files changed, 155 insertions(+), 34 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index 1a02d8845..8bd84b98c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -117,6 +117,10 @@ class EditHabitActivity : AppCompatActivity() { binding.notesInput.setText(habit.description) binding.unitInput.setText(habit.unit) binding.targetInput.setText(habit.targetValue.toString()) + if (habit.targetType == NumericalHabitType.AT_MOST) { + binding.targetTypeAtMost.isChecked = true + binding.targetTypeAtLeast.isChecked = false + } } else { habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value)) } @@ -138,6 +142,7 @@ class EditHabitActivity : AppCompatActivity() { HabitType.YES_NO -> { binding.unitOuterBox.visibility = View.GONE binding.targetOuterBox.visibility = View.GONE + binding.targetTypeOuterBox.visibility = View.GONE } HabitType.NUMERICAL -> { binding.nameInput.hint = getString(R.string.measurable_short_example) @@ -262,7 +267,10 @@ class EditHabitActivity : AppCompatActivity() { habit.frequency = Frequency(freqNum, freqDen) if (habitType == HabitType.NUMERICAL) { habit.targetValue = targetInput.text.toString().toDouble() - habit.targetType = NumericalHabitType.AT_LEAST + if (binding.targetTypeAtLeast.isChecked) + habit.targetType = NumericalHabitType.AT_LEAST + else + habit.targetType = NumericalHabitType.AT_MOST habit.unit = unitInput.text.trim().toString() } habit.type = habitType diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt index 58ee2b36a..c1935be75 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.kt @@ -36,6 +36,7 @@ import dagger.Lazy import org.isoron.uhabits.R import org.isoron.uhabits.activities.common.views.BundleSavedState import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.inject.ActivityContext import javax.inject.Inject @@ -97,7 +98,13 @@ class HabitCardListView( cardView.dataOffset = dataOffset cardView.score = score cardView.unit = habit.unit - cardView.threshold = habit.targetValue / habit.frequency.denominator + if (habit.targetType == NumericalHabitType.AT_LEAST) { + cardView.higherThreshold = habit.targetValue / habit.frequency.denominator + cardView.lowerThreshold = 0.0 + } else { + cardView.higherThreshold = (habit.targetValue * 2) / habit.frequency.denominator + cardView.lowerThreshold = habit.targetValue / habit.frequency.denominator + } val detector = GestureDetector(context, CardViewGestureDetector(holder)) cardView.setOnTouchListener { _, ev -> diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index c59b61ec1..4fca5befa 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -109,10 +109,16 @@ class HabitCardView( numberPanel.values = values.map { it / 1000.0 }.toDoubleArray() } - var threshold: Double - get() = numberPanel.threshold + var lowerThreshold: Double + get() = numberPanel.lowerThreshold set(value) { - numberPanel.threshold = value + numberPanel.lowerThreshold = value + } + + var higherThreshold: Double + get() = numberPanel.higherThreshold + set(value) { + numberPanel.higherThreshold = value } var checkmarkPanel: CheckmarkPanelView @@ -236,7 +242,9 @@ class HabitCardView( numberPanel.apply { color = c units = h.unit - threshold = h.targetValue + targetType = h.targetType + lowerThreshold = 0.0 + higherThreshold = h.targetValue visibility = when (h.isNumerical) { true -> View.VISIBLE false -> View.GONE diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt index 3e48ed1a6..1d474a22f 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt @@ -29,13 +29,14 @@ import android.view.View import android.view.View.OnClickListener import android.view.View.OnLongClickListener import org.isoron.uhabits.R +import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.utils.InterfaceUtils.getDimension -import org.isoron.uhabits.utils.StyledResources import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.showMessage +import org.isoron.uhabits.utils.sres import java.text.DecimalFormat import javax.inject.Inject @@ -82,7 +83,19 @@ class NumberButtonView( invalidate() } - var threshold = 0.0 + var lowerThreshold = 0.0 + set(value) { + field = value + invalidate() + } + + var higherThreshold = 0.0 + set(value) { + field = value + invalidate() + } + + var targetType = NumericalHabitType.AT_LEAST set(value) { field = value invalidate() @@ -127,7 +140,6 @@ class NumberButtonView( private val em: Float private val rect: RectF = RectF() - private val sr = StyledResources(context) private val lowContrast: Int private val mediumContrast: Int @@ -148,15 +160,23 @@ class NumberButtonView( init { em = pNumber.measureText("m") - lowContrast = sr.getColor(R.attr.contrast40) - mediumContrast = sr.getColor(R.attr.contrast60) + lowContrast = sres.getColor(R.attr.contrast40) + mediumContrast = sres.getColor(R.attr.contrast60) } fun draw(canvas: Canvas) { - val activeColor = when { - value <= 0.0 -> lowContrast - value < threshold -> mediumContrast - else -> color + var activeColor = if (targetType == NumericalHabitType.AT_LEAST) { + when { + value <= lowerThreshold -> lowContrast + value < higherThreshold -> mediumContrast + else -> color + } + } else { + when { + value >= higherThreshold || value < 0 -> lowContrast + value > lowerThreshold -> mediumContrast + else -> color + } } val label: String @@ -175,7 +195,7 @@ class NumberButtonView( textSize = dim(R.dimen.smallerTextSize) } else -> { - label = "0" + label = if (targetType == NumericalHabitType.AT_LEAST) "0" else "inf" typeface = BOLD_TYPEFACE textSize = dim(R.dimen.smallTextSize) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt index 491656a77..94980dfac 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt @@ -20,6 +20,7 @@ package org.isoron.uhabits.activities.habits.list.views import android.content.Context +import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.utils.DateUtils @@ -47,7 +48,19 @@ class NumberPanelView( setupButtons() } - var threshold = 0.0 + var targetType = NumericalHabitType.AT_LEAST + set(value) { + field = value + setupButtons() + } + + var lowerThreshold = 0.0 + set(value) { + field = value + setupButtons() + } + + var higherThreshold = 0.0 set(value) { field = value setupButtons() @@ -84,7 +97,9 @@ class NumberPanelView( else -> 0.0 } button.color = color - button.threshold = threshold + button.targetType = targetType + button.lowerThreshold = lowerThreshold + button.higherThreshold = higherThreshold button.units = units button.onEdit = { onEdit(timestamp) } } diff --git a/uhabits-android/src/main/res/layout/activity_edit_habit.xml b/uhabits-android/src/main/res/layout/activity_edit_habit.xml index b3a33c373..e30490bfb 100644 --- a/uhabits-android/src/main/res/layout/activity_edit_habit.xml +++ b/uhabits-android/src/main/res/layout/activity_edit_habit.xml @@ -167,6 +167,29 @@ android:hint="@string/measurable_units_example"/> + + + + + + + + + Change value Calendar Unit + Target Type + At Least + At Most e.g. Did you exercise today? Question Target diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 2d354e7b8..a2dd13846 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -59,9 +59,10 @@ data class Habit( val today = DateUtils.getTodayWithOffset() val value = computedEntries.get(today).value return if (isNumerical) { + val targetValuePerDay = (targetValue / frequency.denominator) when (targetType) { - NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue - NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue + NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValuePerDay + NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValuePerDay } } else { value != Entry.NO && value != Entry.UNKNOWN @@ -72,9 +73,10 @@ data class Habit( val today = DateUtils.getTodayWithOffset() val value = computedEntries.get(today).value return if (isNumerical) { + val targetValuePerDay = (targetValue / frequency.denominator) when (targetType) { - NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValue - NumericalHabitType.AT_MOST -> value / 1000.0 > targetValue + NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValuePerDay + NumericalHabitType.AT_MOST -> value / 1000.0 > targetValuePerDay } } else { value == Entry.NO @@ -96,6 +98,7 @@ data class Habit( scores.recompute( frequency = frequency, isNumerical = isNumerical, + numericalHabitType = targetType, targetValue = targetValue, computedEntries = computedEntries, from = from, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt index b5dff17de..037721d82 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt @@ -68,6 +68,7 @@ class ScoreList { fun recompute( frequency: Frequency, isNumerical: Boolean, + numericalHabitType: NumericalHabitType, targetValue: Double, computedEntries: EntryList, from: Timestamp, @@ -91,18 +92,37 @@ class ScoreList { } var previousValue = 0.0 + val numericalUnknownDayValue = (targetValue * 2 * 1000) / denominator for (i in values.indices) { val offset = values.size - i - 1 if (isNumerical) { - rollingSum += max(0, values[offset]) + if (values[offset] >= 0) + rollingSum += values[offset] + else if (numericalHabitType == NumericalHabitType.AT_MOST) + rollingSum += numericalUnknownDayValue if (offset + denominator < values.size) { - rollingSum -= values[offset + denominator] + if (values[offset + denominator] >= 0) { + rollingSum -= values[offset + denominator] + } else if (numericalHabitType == NumericalHabitType.AT_MOST) { + rollingSum -= numericalUnknownDayValue + } } - val percentageCompleted = if (targetValue > 0) { - min(1.0, rollingSum / 1000 / targetValue) - } else { - 1.0 + + var percentageCompleted = 0.0 + val normalizedRollingSum = rollingSum / 1000 + if (numericalHabitType == NumericalHabitType.AT_LEAST) { + percentageCompleted = if (targetValue > 0) + min(1.0, normalizedRollingSum / targetValue) + else + 1.0 + } else if (numericalHabitType == NumericalHabitType.AT_MOST) { + percentageCompleted = if (targetValue > 0 && normalizedRollingSum > targetValue) + max( + 0.0, 1 - ((normalizedRollingSum - targetValue) / targetValue) + ) + else if (normalizedRollingSum <= targetValue) 1.0 else 0.0 } + previousValue = compute(freq, previousValue, percentageCompleted) } else { if (values[offset] == Entry.YES_MANUAL) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt index bd201e0c5..f1c05861c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt @@ -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 @@ -105,12 +106,21 @@ 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 { + if (it.value < 0) habit.targetValue * 2.0 * 1000.0 else it.value / 1000.0 + }.map { + when { + it <= habit.targetValue -> HistoryChart.Square.ON + else -> HistoryChart.Square.OFF + } } } } else { diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt index f78af6d21..ae2a8eedb 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt @@ -128,6 +128,10 @@ class ScoreListTest : BaseUnitTest() { habit.targetValue = 0.0 habit.recompute() assertTrue(habit.scores[today].value.isFinite()) + + habit.targetType = NumericalHabitType.AT_MOST + habit.recompute() + assertTrue(habit.scores[today].value.isFinite()) } @Test