Skip to content

Commit

Permalink
Implement numerical habits with AT_MOST target type
Browse files Browse the repository at this point in the history
  • Loading branch information
KristianTashkov committed Sep 11, 2021
1 parent fc14786 commit cbf6fad
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) }
}
Expand Down
23 changes: 23 additions & 0 deletions uhabits-android/src/main/res/layout/activity_edit_habit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,29 @@
android:hint="@string/measurable_units_example"/>
</LinearLayout>
</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
android:id="@+id/targetOuterBox"
android:layout_width="match_parent"
Expand Down
3 changes: 3 additions & 0 deletions uhabits-android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@
<string name="change_value">Change value</string>
<string name="calendar">Calendar</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="question">Question</string>
<string name="target">Target</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -96,6 +98,7 @@ data class Habit(
scores.recompute(
frequency = frequency,
isNumerical = isNumerical,
numericalHabitType = targetType,
targetValue = targetValue,
computedEntries = computedEntries,
from = from,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class ScoreList {
fun recompute(
frequency: Frequency,
isNumerical: Boolean,
numericalHabitType: NumericalHabitType,
targetValue: Double,
computedEntries: EntryList,
from: Timestamp,
Expand All @@ -91,18 +92,38 @@ 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit cbf6fad

Please sign in to comment.