Implement numerical habits with AT_MOST target type

pull/1101/head
KristianTashkov 4 years ago
parent fc1478645b
commit 804edfa64e

@ -117,6 +117,10 @@ class EditHabitActivity : AppCompatActivity() {
binding.notesInput.setText(habit.description) binding.notesInput.setText(habit.description)
binding.unitInput.setText(habit.unit) binding.unitInput.setText(habit.unit)
binding.targetInput.setText(habit.targetValue.toString()) binding.targetInput.setText(habit.targetValue.toString())
if (habit.targetType == NumericalHabitType.AT_MOST) {
binding.targetTypeAtMost.isChecked = true
binding.targetTypeAtLeast.isChecked = false
}
} else { } else {
habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value)) habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value))
} }
@ -138,6 +142,7 @@ class EditHabitActivity : AppCompatActivity() {
HabitType.YES_NO -> { HabitType.YES_NO -> {
binding.unitOuterBox.visibility = View.GONE binding.unitOuterBox.visibility = View.GONE
binding.targetOuterBox.visibility = View.GONE binding.targetOuterBox.visibility = View.GONE
binding.targetTypeOuterBox.visibility = View.GONE
} }
HabitType.NUMERICAL -> { HabitType.NUMERICAL -> {
binding.nameInput.hint = getString(R.string.measurable_short_example) binding.nameInput.hint = getString(R.string.measurable_short_example)
@ -262,7 +267,10 @@ class EditHabitActivity : AppCompatActivity() {
habit.frequency = Frequency(freqNum, freqDen) habit.frequency = Frequency(freqNum, freqDen)
if (habitType == HabitType.NUMERICAL) { if (habitType == HabitType.NUMERICAL) {
habit.targetValue = targetInput.text.toString().toDouble() 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.unit = unitInput.text.trim().toString()
} }
habit.type = habitType habit.type = habitType

@ -36,6 +36,7 @@ import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.BundleSavedState import org.isoron.uhabits.activities.common.views.BundleSavedState
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject import javax.inject.Inject
@ -97,7 +98,13 @@ class HabitCardListView(
cardView.dataOffset = dataOffset cardView.dataOffset = dataOffset
cardView.score = score cardView.score = score
cardView.unit = habit.unit 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)) val detector = GestureDetector(context, CardViewGestureDetector(holder))
cardView.setOnTouchListener { _, ev -> cardView.setOnTouchListener { _, ev ->

@ -109,10 +109,16 @@ class HabitCardView(
numberPanel.values = values.map { it / 1000.0 }.toDoubleArray() numberPanel.values = values.map { it / 1000.0 }.toDoubleArray()
} }
var threshold: Double var lowerThreshold: Double
get() = numberPanel.threshold get() = numberPanel.lowerThreshold
set(value) { set(value) {
numberPanel.threshold = value numberPanel.lowerThreshold = value
}
var higherThreshold: Double
get() = numberPanel.higherThreshold
set(value) {
numberPanel.higherThreshold = value
} }
var checkmarkPanel: CheckmarkPanelView var checkmarkPanel: CheckmarkPanelView
@ -236,7 +242,9 @@ class HabitCardView(
numberPanel.apply { numberPanel.apply {
color = c color = c
units = h.unit units = h.unit
threshold = h.targetValue targetType = h.targetType
lowerThreshold = 0.0
higherThreshold = h.targetValue
visibility = when (h.isNumerical) { visibility = when (h.isNumerical) {
true -> View.VISIBLE true -> View.VISIBLE
false -> View.GONE false -> View.GONE

@ -29,13 +29,14 @@ import android.view.View
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres
import java.text.DecimalFormat import java.text.DecimalFormat
import javax.inject.Inject import javax.inject.Inject
@ -82,7 +83,19 @@ class NumberButtonView(
invalidate() 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) { set(value) {
field = value field = value
invalidate() invalidate()
@ -127,7 +140,6 @@ class NumberButtonView(
private val em: Float private val em: Float
private val rect: RectF = RectF() private val rect: RectF = RectF()
private val sr = StyledResources(context)
private val lowContrast: Int private val lowContrast: Int
private val mediumContrast: Int private val mediumContrast: Int
@ -148,15 +160,23 @@ class NumberButtonView(
init { init {
em = pNumber.measureText("m") em = pNumber.measureText("m")
lowContrast = sr.getColor(R.attr.contrast40) lowContrast = sres.getColor(R.attr.contrast40)
mediumContrast = sr.getColor(R.attr.contrast60) mediumContrast = sres.getColor(R.attr.contrast60)
} }
fun draw(canvas: Canvas) { fun draw(canvas: Canvas) {
val activeColor = when { var activeColor = if (targetType == NumericalHabitType.AT_LEAST) {
value <= 0.0 -> lowContrast when {
value < threshold -> mediumContrast value <= lowerThreshold -> lowContrast
else -> color value < higherThreshold -> mediumContrast
else -> color
}
} else {
when {
value >= higherThreshold || value < 0 -> lowContrast
value > lowerThreshold -> mediumContrast
else -> color
}
} }
val label: String val label: String
@ -175,7 +195,7 @@ class NumberButtonView(
textSize = dim(R.dimen.smallerTextSize) textSize = dim(R.dimen.smallerTextSize)
} }
else -> { else -> {
label = "0" label = if (targetType == NumericalHabitType.AT_LEAST) "0" else "inf"
typeface = BOLD_TYPEFACE typeface = BOLD_TYPEFACE
textSize = dim(R.dimen.smallTextSize) textSize = dim(R.dimen.smallTextSize)
} }

@ -20,6 +20,7 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
@ -47,7 +48,19 @@ class NumberPanelView(
setupButtons() 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) { set(value) {
field = value field = value
setupButtons() setupButtons()
@ -84,7 +97,9 @@ class NumberPanelView(
else -> 0.0 else -> 0.0
} }
button.color = color button.color = color
button.threshold = threshold button.targetType = targetType
button.lowerThreshold = lowerThreshold
button.higherThreshold = higherThreshold
button.units = units button.units = units
button.onEdit = { onEdit(timestamp) } button.onEdit = { onEdit(timestamp) }
} }

@ -167,6 +167,29 @@
android:hint="@string/measurable_units_example"/> android:hint="@string/measurable_units_example"/>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
<FrameLayout
android:id="@+id/targetTypeOuterBox"
style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/target_type" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton android:id="@+id/targetTypeAtLeast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/target_type_at_least"
android:checked="true"/>
<RadioButton android:id="@+id/targetTypeAtMost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/target_type_at_most"/>
</RadioGroup>
</LinearLayout>
</FrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/targetOuterBox" android:id="@+id/targetOuterBox"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -184,6 +184,9 @@
<string name="change_value">Change value</string> <string name="change_value">Change value</string>
<string name="calendar">Calendar</string> <string name="calendar">Calendar</string>
<string name="unit">Unit</string> <string name="unit">Unit</string>
<string name="target_type">Target Type</string>
<string name="target_type_at_least">At Least</string>
<string name="target_type_at_most">At Most</string>
<string name="example_question_boolean">e.g. Did you exercise today?</string> <string name="example_question_boolean">e.g. Did you exercise today?</string>
<string name="question">Question</string> <string name="question">Question</string>
<string name="target">Target</string> <string name="target">Target</string>

@ -59,9 +59,10 @@ data class Habit(
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val value = computedEntries.get(today).value val value = computedEntries.get(today).value
return if (isNumerical) { return if (isNumerical) {
val targetValuePerDay = (targetValue / frequency.denominator)
when (targetType) { when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValuePerDay
NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValuePerDay
} }
} else { } else {
value != Entry.NO && value != Entry.UNKNOWN value != Entry.NO && value != Entry.UNKNOWN
@ -72,9 +73,10 @@ data class Habit(
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val value = computedEntries.get(today).value val value = computedEntries.get(today).value
return if (isNumerical) { return if (isNumerical) {
val targetValuePerDay = (targetValue / frequency.denominator)
when (targetType) { when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValue NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValuePerDay
NumericalHabitType.AT_MOST -> value / 1000.0 > targetValue NumericalHabitType.AT_MOST -> value / 1000.0 > targetValuePerDay
} }
} else { } else {
value == Entry.NO value == Entry.NO
@ -96,6 +98,7 @@ data class Habit(
scores.recompute( scores.recompute(
frequency = frequency, frequency = frequency,
isNumerical = isNumerical, isNumerical = isNumerical,
numericalHabitType = targetType,
targetValue = targetValue, targetValue = targetValue,
computedEntries = computedEntries, computedEntries = computedEntries,
from = from, from = from,

@ -68,6 +68,7 @@ class ScoreList {
fun recompute( fun recompute(
frequency: Frequency, frequency: Frequency,
isNumerical: Boolean, isNumerical: Boolean,
numericalHabitType: NumericalHabitType,
targetValue: Double, targetValue: Double,
computedEntries: EntryList, computedEntries: EntryList,
from: Timestamp, from: Timestamp,
@ -91,18 +92,37 @@ class ScoreList {
} }
var previousValue = 0.0 var previousValue = 0.0
val numericalUnknownDayValue = (targetValue * 2 * 1000) / denominator
for (i in values.indices) { for (i in values.indices) {
val offset = values.size - i - 1 val offset = values.size - i - 1
if (isNumerical) { 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) { 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) var percentageCompleted = 0.0
} else { val normalizedRollingSum = rollingSum / 1000
1.0 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) previousValue = compute(freq, previousValue, percentageCompleted)
} else { } else {
if (values[offset] == Entry.YES_MANUAL) { if (values[offset] == Entry.YES_MANUAL) {

@ -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.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList 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.PaletteColor
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
@ -105,12 +106,21 @@ class HistoryCardPresenter(
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today) val entries = habit.computedEntries.getByInterval(oldest, today)
val series = if (habit.isNumerical) { val series = if (habit.isNumerical) {
entries.map { if (habit.targetType == NumericalHabitType.AT_LEAST) {
Entry(it.timestamp, max(0, it.value)) entries.map {
}.map { when (max(0, it.value)) {
when (it.value) { 0 -> HistoryChart.Square.OFF
0 -> HistoryChart.Square.OFF else -> HistoryChart.Square.ON
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 { } else {

@ -128,6 +128,10 @@ class ScoreListTest : BaseUnitTest() {
habit.targetValue = 0.0 habit.targetValue = 0.0
habit.recompute() habit.recompute()
assertTrue(habit.scores[today].value.isFinite()) assertTrue(habit.scores[today].value.isFinite())
habit.targetType = NumericalHabitType.AT_MOST
habit.recompute()
assertTrue(habit.scores[today].value.isFinite())
} }
@Test @Test

Loading…
Cancel
Save