mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Implement numerical habits with AT_MOST target type
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user