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