Merge pull request #1101 from KristianTashkov/kris/implement_at_most
Implement numerical habits with AT_MOST target type
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
|
|||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
import org.isoron.uhabits.utils.PaletteUtils
|
import org.isoron.uhabits.utils.PaletteUtils
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -42,6 +43,7 @@ class NumberButtonViewTest : BaseViewTest() {
|
|||||||
super.setUp()
|
super.setUp()
|
||||||
view = component.getNumberButtonViewFactory().create().apply {
|
view = component.getNumberButtonViewFactory().create().apply {
|
||||||
units = "steps"
|
units = "steps"
|
||||||
|
targetType = NumericalHabitType.AT_LEAST
|
||||||
threshold = 100.0
|
threshold = 100.0
|
||||||
color = PaletteUtils.getAndroidTestColor(8)
|
color = PaletteUtils.getAndroidTestColor(8)
|
||||||
onEdit = { edited = true }
|
onEdit = { edited = true }
|
||||||
@@ -74,10 +76,10 @@ class NumberButtonViewTest : BaseViewTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRender_emptyUnits() {
|
fun testRender_atMostAboveThreshold() {
|
||||||
view.value = 500.0
|
view.value = 500.0
|
||||||
view.units = ""
|
view.targetType = NumericalHabitType.AT_MOST
|
||||||
assertRenders(view, "$PATH/render_unitless.png")
|
assertRenders(view, "$PATH/render_at_most_above.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -86,12 +88,33 @@ class NumberButtonViewTest : BaseViewTest() {
|
|||||||
assertRenders(view, "$PATH/render_below.png")
|
assertRenders(view, "$PATH/render_below.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRender_atMostBetweenThresholds() {
|
||||||
|
view.value = 110.0
|
||||||
|
view.targetType = NumericalHabitType.AT_MOST
|
||||||
|
assertRenders(view, "$PATH/render_at_most_between.png")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRender_zero() {
|
fun testRender_zero() {
|
||||||
view.value = 0.0
|
view.value = 0.0
|
||||||
assertRenders(view, "$PATH/render_zero.png")
|
assertRenders(view, "$PATH/render_zero.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRender_atMostBelowThreshold() {
|
||||||
|
view.value = 0.0
|
||||||
|
view.targetType = NumericalHabitType.AT_MOST
|
||||||
|
assertRenders(view, "$PATH/render_at_most_below.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRender_emptyUnits() {
|
||||||
|
view.value = 500.0
|
||||||
|
view.units = ""
|
||||||
|
assertRenders(view, "$PATH/render_unitless.png")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testClick_shortToggleDisabled() {
|
fun testClick_shortToggleDisabled() {
|
||||||
prefs.isShortToggleEnabled = false
|
prefs.isShortToggleEnabled = false
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.test.filters.MediumTest
|
|||||||
import org.hamcrest.CoreMatchers.equalTo
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
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.utils.PaletteUtils
|
import org.isoron.uhabits.utils.PaletteUtils
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@@ -55,6 +56,7 @@ class NumberPanelViewTest : BaseViewTest() {
|
|||||||
buttonCount = 4
|
buttonCount = 4
|
||||||
color = PaletteUtils.getAndroidTestColor(7)
|
color = PaletteUtils.getAndroidTestColor(7)
|
||||||
units = "steps"
|
units = "steps"
|
||||||
|
targetType = NumericalHabitType.AT_LEAST
|
||||||
threshold = 5000.0
|
threshold = 5000.0
|
||||||
}
|
}
|
||||||
view.onAttachedToWindow()
|
view.onAttachedToWindow()
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ class SubtitleCardViewTest : BaseViewTest() {
|
|||||||
isNumerical = false,
|
isNumerical = false,
|
||||||
question = "Did you meditate this morning?",
|
question = "Did you meditate this morning?",
|
||||||
reminder = Reminder(8, 30, EVERY_DAY),
|
reminder = Reminder(8, 30, EVERY_DAY),
|
||||||
unit = "",
|
|
||||||
targetValue = 0.0,
|
|
||||||
theme = LightTheme(),
|
theme = LightTheme(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
|||||||
firstWeekday = preferences.firstWeekday,
|
firstWeekday = preferences.firstWeekday,
|
||||||
paletteColor = habit.color,
|
paletteColor = habit.color,
|
||||||
series = emptyList(),
|
series = emptyList(),
|
||||||
|
defaultSquare = HistoryChart.Square.OFF,
|
||||||
theme = themeSwitcher.currentTheme,
|
theme = themeSwitcher.currentTheme,
|
||||||
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
today = DateUtils.getTodayWithOffset().toLocalDate(),
|
||||||
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
|
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
|
||||||
@@ -101,6 +102,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
|
|||||||
theme = LightTheme()
|
theme = LightTheme()
|
||||||
)
|
)
|
||||||
chart?.series = model.series
|
chart?.series = model.series
|
||||||
|
chart?.defaultSquare = model.defaultSquare
|
||||||
dataView.postInvalidate()
|
dataView.postInvalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
var reminderHour = -1
|
var reminderHour = -1
|
||||||
var reminderMin = -1
|
var reminderMin = -1
|
||||||
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
|
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
|
||||||
|
var targetType = NumericalHabitType.AT_LEAST
|
||||||
|
|
||||||
override fun onCreate(state: Bundle?) {
|
override fun onCreate(state: Bundle?) {
|
||||||
super.onCreate(state)
|
super.onCreate(state)
|
||||||
@@ -107,6 +108,7 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
color = habit.color
|
color = habit.color
|
||||||
freqNum = habit.frequency.numerator
|
freqNum = habit.frequency.numerator
|
||||||
freqDen = habit.frequency.denominator
|
freqDen = habit.frequency.denominator
|
||||||
|
targetType = habit.targetType
|
||||||
habit.reminder?.let {
|
habit.reminder?.let {
|
||||||
reminderHour = it.hour
|
reminderHour = it.hour
|
||||||
reminderMin = it.minute
|
reminderMin = it.minute
|
||||||
@@ -138,6 +140,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)
|
||||||
@@ -172,6 +175,23 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
dialog.show(supportFragmentManager, "frequencyPicker")
|
dialog.show(supportFragmentManager, "frequencyPicker")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
populateTargetType()
|
||||||
|
binding.targetTypePicker.setOnClickListener {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
|
||||||
|
arrayAdapter.add(getString(R.string.target_type_at_least))
|
||||||
|
arrayAdapter.add(getString(R.string.target_type_at_most))
|
||||||
|
builder.setAdapter(arrayAdapter) { dialog, which ->
|
||||||
|
targetType = when (which) {
|
||||||
|
0 -> NumericalHabitType.AT_LEAST
|
||||||
|
else -> NumericalHabitType.AT_MOST
|
||||||
|
}
|
||||||
|
populateTargetType()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
|
||||||
binding.numericalFrequencyPicker.setOnClickListener {
|
binding.numericalFrequencyPicker.setOnClickListener {
|
||||||
val builder = AlertDialog.Builder(this)
|
val builder = AlertDialog.Builder(this)
|
||||||
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
|
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
|
||||||
@@ -262,7 +282,7 @@ 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
|
habit.targetType = targetType
|
||||||
habit.unit = unitInput.text.trim().toString()
|
habit.unit = unitInput.text.trim().toString()
|
||||||
}
|
}
|
||||||
habit.type = habitType
|
habit.type = habitType
|
||||||
@@ -324,6 +344,13 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun populateTargetType() {
|
||||||
|
binding.targetTypePicker.text = when (targetType) {
|
||||||
|
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)
|
||||||
|
else -> getString(R.string.target_type_at_least)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateColors() {
|
private fun updateColors() {
|
||||||
androidColor = themeSwitcher.currentTheme.color(color).toInt()
|
androidColor = themeSwitcher.currentTheme.color(color).toInt()
|
||||||
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor)
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ class HabitCardView(
|
|||||||
numberPanel.apply {
|
numberPanel.apply {
|
||||||
color = c
|
color = c
|
||||||
units = h.unit
|
units = h.unit
|
||||||
|
targetType = h.targetType
|
||||||
threshold = h.targetValue
|
threshold = h.targetValue
|
||||||
visibility = when (h.isNumerical) {
|
visibility = when (h.isNumerical) {
|
||||||
true -> View.VISIBLE
|
true -> View.VISIBLE
|
||||||
|
|||||||
@@ -29,13 +29,15 @@ 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.lang.Double.max
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -88,6 +90,12 @@ class NumberButtonView(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var targetType = NumericalHabitType.AT_LEAST
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
var units = ""
|
var units = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -127,7 +135,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 +155,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 < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
|
||||||
else -> color
|
max(0.0, value) >= threshold -> color
|
||||||
|
else -> mediumContrast
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when {
|
||||||
|
value < 0.0 && preferences.areQuestionMarksEnabled -> lowContrast
|
||||||
|
value <= threshold -> color
|
||||||
|
else -> mediumContrast
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val label: String
|
val label: String
|
||||||
|
|||||||
@@ -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,6 +48,12 @@ class NumberPanelView(
|
|||||||
setupButtons()
|
setupButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var targetType = NumericalHabitType.AT_LEAST
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
setupButtons()
|
||||||
|
}
|
||||||
|
|
||||||
var threshold = 0.0
|
var threshold = 0.0
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -84,6 +91,7 @@ class NumberPanelView(
|
|||||||
else -> 0.0
|
else -> 0.0
|
||||||
}
|
}
|
||||||
button.color = color
|
button.color = color
|
||||||
|
button.targetType = targetType
|
||||||
button.threshold = threshold
|
button.threshold = threshold
|
||||||
button.units = units
|
button.units = units
|
||||||
button.onEdit = { onEdit(timestamp) }
|
button.onEdit = { onEdit(timestamp) }
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
|
|||||||
theme = state.theme,
|
theme = state.theme,
|
||||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||||
series = state.series,
|
series = state.series,
|
||||||
|
defaultSquare = state.defaultSquare,
|
||||||
firstWeekday = state.firstWeekday,
|
firstWeekday = state.firstWeekday,
|
||||||
)
|
)
|
||||||
binding.chart.postInvalidate()
|
binding.chart.postInvalidate()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import org.isoron.platform.gui.toInt
|
|||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.habits.edit.formatFrequency
|
import org.isoron.uhabits.activities.habits.edit.formatFrequency
|
||||||
import org.isoron.uhabits.activities.habits.list.views.toShortString
|
import org.isoron.uhabits.activities.habits.list.views.toShortString
|
||||||
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
||||||
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
|
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils
|
import org.isoron.uhabits.utils.InterfaceUtils
|
||||||
@@ -65,7 +66,12 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
|
|||||||
binding.questionLabel.visibility = View.VISIBLE
|
binding.questionLabel.visibility = View.VISIBLE
|
||||||
binding.targetIcon.visibility = View.VISIBLE
|
binding.targetIcon.visibility = View.VISIBLE
|
||||||
binding.targetText.visibility = View.VISIBLE
|
binding.targetText.visibility = View.VISIBLE
|
||||||
if (!state.isNumerical) {
|
if (state.isNumerical) {
|
||||||
|
binding.targetIcon.text = when (state.targetType) {
|
||||||
|
NumericalHabitType.AT_LEAST -> resources.getString(R.string.fa_arrow_circle_up)
|
||||||
|
else -> resources.getString(R.string.fa_arrow_circle_down)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
binding.targetIcon.visibility = View.GONE
|
binding.targetIcon.visibility = View.GONE
|
||||||
binding.targetText.visibility = View.GONE
|
binding.targetText.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ class HistoryWidget(
|
|||||||
theme = WidgetTheme(),
|
theme = WidgetTheme(),
|
||||||
)
|
)
|
||||||
(widgetView.dataView as AndroidDataView).apply {
|
(widgetView.dataView as AndroidDataView).apply {
|
||||||
(this.view as HistoryChart).series = model.series
|
val historyChart = (this.view as HistoryChart)
|
||||||
|
historyChart.series = model.series
|
||||||
|
historyChart.defaultSquare = model.defaultSquare
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +73,7 @@ class HistoryWidget(
|
|||||||
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
|
||||||
firstWeekday = prefs.firstWeekday,
|
firstWeekday = prefs.firstWeekday,
|
||||||
series = listOf(),
|
series = listOf(),
|
||||||
|
defaultSquare = HistoryChart.Square.OFF,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
).apply {
|
).apply {
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
android:hint="@string/measurable_units_example"/>
|
android:hint="@string/measurable_units_example"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/targetOuterBox"
|
android:id="@+id/targetOuterBox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -207,6 +208,22 @@
|
|||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/targetTypeOuterBox"
|
||||||
|
style="@style/FormOuterBox">
|
||||||
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
<TextView
|
||||||
|
style="@style/FormLabel"
|
||||||
|
android:text="@string/target_type" />
|
||||||
|
<TextView
|
||||||
|
style="@style/FormDropdown"
|
||||||
|
android:id="@+id/targetTypePicker"
|
||||||
|
android:textColor="?attr/contrast100"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
<!-- Reminder -->
|
<!-- Reminder -->
|
||||||
<FrameLayout style="@style/FormOuterBox">
|
<FrameLayout style="@style/FormOuterBox">
|
||||||
<LinearLayout style="@style/FormInnerBox">
|
<LinearLayout style="@style/FormInnerBox">
|
||||||
|
|||||||
@@ -47,7 +47,6 @@
|
|||||||
android:id="@+id/targetIcon"
|
android:id="@+id/targetIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/fa_arrow_circle_up"
|
|
||||||
android:textColor="?attr/contrast60"
|
android:textColor="?attr/contrast60"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string translatable="false" name="fa_star_half_o"></string>
|
<string translatable="false" name="fa_star_half_o"></string>
|
||||||
<string translatable="false" name="fa_arrow_circle_up"></string>
|
<string translatable="false" name="fa_arrow_circle_up"></string>
|
||||||
|
<string translatable="false" name="fa_arrow_circle_down"></string>
|
||||||
<string translatable="false" name="fa_check"></string>
|
<string translatable="false" name="fa_check"></string>
|
||||||
<string translatable="false" name="fa_times"></string>
|
<string translatable="false" name="fa_times"></string>
|
||||||
<string translatable="false" name="fa_skipped"></string>
|
<string translatable="false" name="fa_skipped"></string>
|
||||||
@@ -181,7 +182,6 @@
|
|||||||
<!--<string translatable="false" name="fa_hand_o_down"></string>-->
|
<!--<string translatable="false" name="fa_hand_o_down"></string>-->
|
||||||
<!--<string translatable="false" name="fa_arrow_circle_left"></string>-->
|
<!--<string translatable="false" name="fa_arrow_circle_left"></string>-->
|
||||||
<!--<string translatable="false" name="fa_arrow_circle_right"></string>-->
|
<!--<string translatable="false" name="fa_arrow_circle_right"></string>-->
|
||||||
<!--<string translatable="false" name="fa_arrow_circle_down"></string>-->
|
|
||||||
<!--<string translatable="false" name="fa_globe"></string>-->
|
<!--<string translatable="false" name="fa_globe"></string>-->
|
||||||
<!--<string translatable="false" name="fa_wrench"></string>-->
|
<!--<string translatable="false" name="fa_wrench"></string>-->
|
||||||
<!--<string translatable="false" name="fa_tasks"></string>-->
|
<!--<string translatable="false" name="fa_tasks"></string>-->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -31,5 +31,6 @@ data class CreateHabitCommand(
|
|||||||
val habit = modelFactory.buildHabit()
|
val habit = modelFactory.buildHabit()
|
||||||
habit.copyFrom(model)
|
habit.copyFrom(model)
|
||||||
habitList.add(habit)
|
habitList.add(habit)
|
||||||
|
habit.recompute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,14 +88,16 @@ data class Habit(
|
|||||||
isNumerical = isNumerical,
|
isNumerical = isNumerical,
|
||||||
)
|
)
|
||||||
|
|
||||||
val to = DateUtils.getTodayWithOffset().plus(30)
|
val today = DateUtils.getTodayWithOffset()
|
||||||
|
val to = today.plus(30)
|
||||||
val entries = computedEntries.getKnown()
|
val entries = computedEntries.getKnown()
|
||||||
var from = entries.lastOrNull()?.timestamp ?: to
|
var from = entries.lastOrNull()?.timestamp ?: today
|
||||||
if (from.isNewerThan(to)) from = to
|
if (from.isNewerThan(to)) from = to
|
||||||
|
|
||||||
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,19 +68,19 @@ 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,
|
||||||
to: Timestamp,
|
to: Timestamp,
|
||||||
) {
|
) {
|
||||||
map.clear()
|
map.clear()
|
||||||
if (computedEntries.getKnown().isEmpty()) return
|
|
||||||
if (from.isNewerThan(to)) return
|
|
||||||
var rollingSum = 0.0
|
var rollingSum = 0.0
|
||||||
var numerator = frequency.numerator
|
var numerator = frequency.numerator
|
||||||
var denominator = frequency.denominator
|
var denominator = frequency.denominator
|
||||||
val freq = frequency.toDouble()
|
val freq = frequency.toDouble()
|
||||||
val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray()
|
val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray()
|
||||||
|
val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST
|
||||||
|
|
||||||
// For non-daily boolean habits, we double the numerator and the denominator to smooth
|
// For non-daily boolean habits, we double the numerator and the denominator to smooth
|
||||||
// out irregular repetition schedules (for example, weekly habits performed on different
|
// out irregular repetition schedules (for example, weekly habits performed on different
|
||||||
@@ -90,19 +90,29 @@ class ScoreList {
|
|||||||
denominator *= 2
|
denominator *= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
var previousValue = 0.0
|
var previousValue = if (isNumerical && isAtMost) 1.0 else 0.0
|
||||||
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])
|
rollingSum += max(0, values[offset])
|
||||||
if (offset + denominator < values.size) {
|
if (offset + denominator < values.size) {
|
||||||
rollingSum -= values[offset + denominator]
|
rollingSum -= max(0, values[offset + denominator])
|
||||||
}
|
}
|
||||||
val percentageCompleted = if (targetValue > 0) {
|
|
||||||
min(1.0, rollingSum / 1000 / targetValue)
|
val normalizedRollingSum = rollingSum / 1000
|
||||||
|
val percentageCompleted = if (!isAtMost) {
|
||||||
|
if (targetValue > 0)
|
||||||
|
min(1.0, normalizedRollingSum / targetValue)
|
||||||
|
else
|
||||||
|
1.0
|
||||||
} else {
|
} else {
|
||||||
1.0
|
if (targetValue > 0) {
|
||||||
|
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(0.0, 1.0)
|
||||||
|
} else {
|
||||||
|
if (normalizedRollingSum > 0) 0.0 else 1.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) {
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
|
|||||||
return habit
|
return habit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createEmptyNumericalHabit(targetType: NumericalHabitType): Habit {
|
||||||
|
val habit = modelFactory.buildHabit()
|
||||||
|
habit.type = HabitType.NUMERICAL
|
||||||
|
habit.name = "Run"
|
||||||
|
habit.question = "How many miles did you run today?"
|
||||||
|
habit.unit = "miles"
|
||||||
|
habit.targetType = targetType
|
||||||
|
habit.targetValue = 2.0
|
||||||
|
habit.color = PaletteColor(1)
|
||||||
|
saveIfSQLite(habit)
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
|
||||||
fun createLongHabit(): Habit {
|
fun createLongHabit(): Habit {
|
||||||
val habit = createEmptyHabit()
|
val habit = createEmptyHabit()
|
||||||
habit.frequency = Frequency(3, 7)
|
habit.frequency = Frequency(3, 7)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -44,6 +45,7 @@ data class HistoryCardState(
|
|||||||
val color: PaletteColor,
|
val color: PaletteColor,
|
||||||
val firstWeekday: DayOfWeek,
|
val firstWeekday: DayOfWeek,
|
||||||
val series: List<HistoryChart.Square>,
|
val series: List<HistoryChart.Square>,
|
||||||
|
val defaultSquare: HistoryChart.Square,
|
||||||
val theme: Theme,
|
val theme: Theme,
|
||||||
val today: LocalDate,
|
val today: LocalDate,
|
||||||
)
|
)
|
||||||
@@ -105,12 +107,19 @@ 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 {
|
||||||
|
when {
|
||||||
|
max(0.0, it.value / 1000.0) <= habit.targetValue -> HistoryChart.Square.ON
|
||||||
|
else -> HistoryChart.Square.OFF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -123,6 +132,10 @@ class HistoryCardPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val defaultSquare = if (habit.isNumerical && habit.targetType == NumericalHabitType.AT_MOST)
|
||||||
|
HistoryChart.Square.ON
|
||||||
|
else
|
||||||
|
HistoryChart.Square.OFF
|
||||||
|
|
||||||
return HistoryCardState(
|
return HistoryCardState(
|
||||||
color = habit.color,
|
color = habit.color,
|
||||||
@@ -130,6 +143,7 @@ class HistoryCardPresenter(
|
|||||||
today = today.toLocalDate(),
|
today = today.toLocalDate(),
|
||||||
theme = theme,
|
theme = theme,
|
||||||
series = series,
|
series = series,
|
||||||
|
defaultSquare = defaultSquare
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
|
|||||||
|
|
||||||
import org.isoron.uhabits.core.models.Frequency
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
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.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.models.Reminder
|
import org.isoron.uhabits.core.models.Reminder
|
||||||
import org.isoron.uhabits.core.ui.views.Theme
|
import org.isoron.uhabits.core.ui.views.Theme
|
||||||
@@ -31,8 +32,9 @@ data class SubtitleCardState(
|
|||||||
val isNumerical: Boolean,
|
val isNumerical: Boolean,
|
||||||
val question: String,
|
val question: String,
|
||||||
val reminder: Reminder?,
|
val reminder: Reminder?,
|
||||||
val targetValue: Double,
|
val targetValue: Double = 0.0,
|
||||||
val unit: String,
|
val targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
|
||||||
|
val unit: String = "",
|
||||||
val theme: Theme,
|
val theme: Theme,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ class SubtitleCardPresenter {
|
|||||||
question = habit.question,
|
question = habit.question,
|
||||||
reminder = habit.reminder,
|
reminder = habit.reminder,
|
||||||
targetValue = habit.targetValue,
|
targetValue = habit.targetValue,
|
||||||
|
targetType = habit.targetType,
|
||||||
unit = habit.unit,
|
unit = habit.unit,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class HistoryChart(
|
|||||||
var firstWeekday: DayOfWeek,
|
var firstWeekday: DayOfWeek,
|
||||||
var paletteColor: PaletteColor,
|
var paletteColor: PaletteColor,
|
||||||
var series: List<Square>,
|
var series: List<Square>,
|
||||||
|
var defaultSquare: Square,
|
||||||
var theme: Theme,
|
var theme: Theme,
|
||||||
var today: LocalDate,
|
var today: LocalDate,
|
||||||
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { },
|
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { },
|
||||||
@@ -189,7 +190,7 @@ class HistoryChart(
|
|||||||
offset: Int,
|
offset: Int,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val value = if (offset >= series.size) Square.OFF else series[offset]
|
val value = if (offset >= series.size) defaultSquare else series[offset]
|
||||||
val squareColor: Color
|
val squareColor: Color
|
||||||
val color = theme.color(paletteColor.paletteIndex)
|
val color = theme.color(paletteColor.paletteIndex)
|
||||||
squareColor = when (value) {
|
squareColor = when (value) {
|
||||||
|
|||||||
@@ -28,14 +28,36 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
|
|
||||||
class ScoreListTest : BaseUnitTest() {
|
open class BaseScoreListTest : BaseUnitTest() {
|
||||||
private lateinit var habit: Habit
|
protected lateinit var habit: Habit
|
||||||
private lateinit var today: Timestamp
|
protected lateinit var today: Timestamp
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
today = getToday()
|
today = getToday()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun checkScoreValues(expectedValues: DoubleArray) {
|
||||||
|
var current = today
|
||||||
|
val scores = habit.scores
|
||||||
|
for (expectedValue in expectedValues) {
|
||||||
|
assertThat(scores[current].value, IsCloseTo.closeTo(expectedValue, E))
|
||||||
|
current = current.minus(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val E = 1e-6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class YesNoScoreListTest : BaseScoreListTest() {
|
||||||
|
@Before
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
habit = fixtures.createEmptyHabit()
|
habit = fixtures.createEmptyHabit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,14 +144,6 @@ class ScoreListTest : BaseUnitTest() {
|
|||||||
checkScoreValues(expectedValues)
|
checkScoreValues(expectedValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_withZeroTarget() {
|
|
||||||
habit = fixtures.createNumericalHabit()
|
|
||||||
habit.targetValue = 0.0
|
|
||||||
habit.recompute()
|
|
||||||
assertTrue(habit.scores[today].value.isFinite())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_imperfectNonDaily() {
|
fun test_imperfectNonDaily() {
|
||||||
// If the habit should be performed 3 times per week and the user misses 1 repetition
|
// If the habit should be performed 3 times per week and the user misses 1 repetition
|
||||||
@@ -255,17 +269,204 @@ class ScoreListTest : BaseUnitTest() {
|
|||||||
val entries = habit.originalEntries
|
val entries = habit.originalEntries
|
||||||
entries.add(Entry(today.minus(day), Entry.SKIP))
|
entries.add(Entry(today.minus(day), Entry.SKIP))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkScoreValues(expectedValues: DoubleArray) {
|
open class NumericalScoreListTest : BaseScoreListTest() {
|
||||||
var current = today
|
protected fun addEntry(day: Int, value: Int) {
|
||||||
val scores = habit.scores
|
val entries = habit.originalEntries
|
||||||
for (expectedValue in expectedValues) {
|
entries.add(Entry(today.minus(day), value))
|
||||||
assertThat(scores[current].value, IsCloseTo.closeTo(expectedValue, E))
|
|
||||||
current = current.minus(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
protected fun addEntries(from: Int, to: Int, value: Int) {
|
||||||
private const val E = 1e-6
|
val entries = habit.originalEntries
|
||||||
|
for (i in from until to) entries.add(Entry(today.minus(i), value))
|
||||||
|
habit.recompute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumericalAtLeastScoreListTest : NumericalScoreListTest() {
|
||||||
|
@Before
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_withZeroTarget() {
|
||||||
|
habit = fixtures.createNumericalHabit()
|
||||||
|
habit.targetValue = 0.0
|
||||||
|
habit.recompute()
|
||||||
|
assertTrue(habit.scores[today].value.isFinite())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getValue() {
|
||||||
|
addEntries(0, 20, 2000)
|
||||||
|
val expectedValues = doubleArrayOf(
|
||||||
|
0.655747,
|
||||||
|
0.636894,
|
||||||
|
0.617008,
|
||||||
|
0.596033,
|
||||||
|
0.573910,
|
||||||
|
0.550574,
|
||||||
|
0.525961,
|
||||||
|
0.500000,
|
||||||
|
0.472617,
|
||||||
|
0.443734,
|
||||||
|
0.413270,
|
||||||
|
0.381137,
|
||||||
|
0.347244,
|
||||||
|
0.311495,
|
||||||
|
0.273788,
|
||||||
|
0.234017,
|
||||||
|
0.192067,
|
||||||
|
0.147820,
|
||||||
|
0.101149,
|
||||||
|
0.051922,
|
||||||
|
0.000000,
|
||||||
|
0.000000,
|
||||||
|
0.000000
|
||||||
|
)
|
||||||
|
checkScoreValues(expectedValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_recompute() {
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.0, E))
|
||||||
|
addEntries(0, 2, 2000)
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.101149, E))
|
||||||
|
habit.frequency = Frequency(1, 2)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.072631, E))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldAchieveHighScoreInReasonableTime() {
|
||||||
|
// Daily habits should achieve at least 99% in 3 months
|
||||||
|
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
|
||||||
|
habit.frequency = Frequency.DAILY
|
||||||
|
for (i in 0..89) addEntry(i, 2000)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, OrderingComparison.greaterThan(0.99))
|
||||||
|
|
||||||
|
// Weekly habits should achieve at least 99% in 9 months
|
||||||
|
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
|
||||||
|
habit.frequency = Frequency.WEEKLY
|
||||||
|
for (i in 0..38) addEntry(7 * i, 2000)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, OrderingComparison.greaterThan(0.99))
|
||||||
|
|
||||||
|
// Monthly habits should achieve at least 99% in 18 months
|
||||||
|
habit.frequency = Frequency(1, 30)
|
||||||
|
for (i in 0..17) addEntry(30 * i, 2000)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, OrderingComparison.greaterThan(0.99))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldAchieveComparableScoreToProgress() {
|
||||||
|
addEntries(0, 500, 1000)
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.5, E))
|
||||||
|
|
||||||
|
addEntries(0, 500, 500)
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.25, E))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun overeachievingIsntRelevant() {
|
||||||
|
addEntry(0, 10000000)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.051922, E))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumericalAtMostScoreListTest : NumericalScoreListTest() {
|
||||||
|
@Before
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_MOST)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_withZeroTarget() {
|
||||||
|
habit = fixtures.createNumericalHabit()
|
||||||
|
habit.targetType = NumericalHabitType.AT_MOST
|
||||||
|
habit.targetValue = 0.0
|
||||||
|
habit.recompute()
|
||||||
|
assertTrue(habit.scores[today].value.isFinite())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getValue() {
|
||||||
|
addEntry(20, 1000)
|
||||||
|
addEntries(0, 20, 5000)
|
||||||
|
val expectedValues = doubleArrayOf(
|
||||||
|
0.344253,
|
||||||
|
0.363106,
|
||||||
|
0.382992,
|
||||||
|
0.403967,
|
||||||
|
0.426090,
|
||||||
|
0.449426,
|
||||||
|
0.474039,
|
||||||
|
0.500000,
|
||||||
|
0.527383,
|
||||||
|
0.556266,
|
||||||
|
0.586730,
|
||||||
|
0.618863,
|
||||||
|
0.652756,
|
||||||
|
0.688505,
|
||||||
|
0.726212,
|
||||||
|
0.765983,
|
||||||
|
0.807933,
|
||||||
|
0.852180,
|
||||||
|
0.898851,
|
||||||
|
0.948078,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
checkScoreValues(expectedValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_recompute() {
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(1.0, E))
|
||||||
|
addEntries(0, 2, 5000)
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.898850, E))
|
||||||
|
habit.frequency = Frequency(1, 2)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.927369, E))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldAchieveComparableScoreToProgress() {
|
||||||
|
addEntries(0, 500, 3000)
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.5, E))
|
||||||
|
|
||||||
|
addEntries(0, 500, 3500)
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.25, E))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun undereachievingIsntRelevant() {
|
||||||
|
addEntry(1, 10000000)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.950773, E))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun overeachievingIsntRelevant() {
|
||||||
|
addEntry(0, 5000)
|
||||||
|
|
||||||
|
addEntry(1, 0)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.948077, E))
|
||||||
|
|
||||||
|
addEntry(1, 1000)
|
||||||
|
habit.recompute()
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(0.948077, E))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class HistoryChartTest {
|
|||||||
dateFormatter = JavaLocalDateFormatter(Locale.US),
|
dateFormatter = JavaLocalDateFormatter(Locale.US),
|
||||||
firstWeekday = SUNDAY,
|
firstWeekday = SUNDAY,
|
||||||
onDateClickedListener = dateClickedListener,
|
onDateClickedListener = dateClickedListener,
|
||||||
|
defaultSquare = OFF,
|
||||||
series = listOf(
|
series = listOf(
|
||||||
2, // today
|
2, // today
|
||||||
2, 1, 2, 1, 2, 1, 2,
|
2, 1, 2, 1, 2, 1, 2,
|
||||||
|
|||||||