mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Issue 1316: Skip measurable habit (#1319)
Co-authored-by: Jakub Kalinowski <kalj@netcompany.com>
This commit is contained in:
@@ -22,6 +22,7 @@ package org.isoron.uhabits.activities.common.dialogs
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.content.DialogInterface.BUTTON_NEGATIVE
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -33,7 +34,11 @@ import android.widget.EditText
|
|||||||
import android.widget.NumberPicker
|
import android.widget.NumberPicker
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import org.isoron.uhabits.HabitsApplication
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Entry
|
||||||
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
|
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||||
import org.isoron.uhabits.inject.ActivityContext
|
import org.isoron.uhabits.inject.ActivityContext
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils
|
import org.isoron.uhabits.utils.InterfaceUtils
|
||||||
@@ -52,6 +57,7 @@ class NumberPickerFactory
|
|||||||
unit: String,
|
unit: String,
|
||||||
notes: String,
|
notes: String,
|
||||||
dateString: String,
|
dateString: String,
|
||||||
|
frequency: Frequency,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
): AlertDialog {
|
): AlertDialog {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
@@ -92,7 +98,7 @@ class NumberPickerFactory
|
|||||||
picker2.value = intValue % 100
|
picker2.value = intValue % 100
|
||||||
|
|
||||||
etNotes.setText(notes)
|
etNotes.setText(notes)
|
||||||
val dialog = AlertDialog.Builder(context)
|
val dialogBuilder = AlertDialog.Builder(context)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setTitle(dateString)
|
.setTitle(dateString)
|
||||||
.setPositiveButton(R.string.save) { _, _ ->
|
.setPositiveButton(R.string.save) { _, _ ->
|
||||||
@@ -108,9 +114,24 @@ class NumberPickerFactory
|
|||||||
.setOnDismissListener {
|
.setOnDismissListener {
|
||||||
callback.onNumberPickerDismissed()
|
callback.onNumberPickerDismissed()
|
||||||
}
|
}
|
||||||
.create()
|
|
||||||
|
if (frequency == DAILY) {
|
||||||
|
dialogBuilder.setNegativeButton(R.string.skip_day) { _, _ ->
|
||||||
|
picker.clearFocus()
|
||||||
|
val v = Entry.SKIP.toDouble() / 1000
|
||||||
|
val note = etNotes.text.toString()
|
||||||
|
callback.onNumberPicked(v, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = dialogBuilder.create()
|
||||||
|
|
||||||
dialog.setOnShowListener {
|
dialog.setOnShowListener {
|
||||||
|
val preferences =
|
||||||
|
(context.applicationContext as HabitsApplication).component.preferences
|
||||||
|
if (!preferences.isSkipEnabled) {
|
||||||
|
dialog.getButton(BUTTON_NEGATIVE).visibility = View.GONE
|
||||||
|
}
|
||||||
showSoftInput(dialog, pickerInputText)
|
showSoftInput(dialog, pickerInputText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,8 @@ class FrequencyChart : ScrollableChart {
|
|||||||
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
|
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
|
||||||
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
|
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
|
||||||
val i = localeWeekdayList[j] % 7
|
val i = localeWeekdayList[j] % 7
|
||||||
if (values != null) drawMarker(canvas, rect, values[i])
|
if (values != null)
|
||||||
|
drawMarker(canvas, rect, values[i])
|
||||||
rect.offset(0f, rowHeight)
|
rect.offset(0f, rowHeight)
|
||||||
}
|
}
|
||||||
drawFooter(canvas, rect, date)
|
drawFooter(canvas, rect, date)
|
||||||
@@ -222,11 +223,14 @@ class FrequencyChart : ScrollableChart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
|
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
|
||||||
|
// value can be negative when the entry is skipped
|
||||||
|
val valueCopy = value?.let { max(0, it) }
|
||||||
|
|
||||||
val padding = rect!!.height() * 0.2f
|
val padding = rect!!.height() * 0.2f
|
||||||
// maximal allowed mark radius
|
// maximal allowed mark radius
|
||||||
val maxRadius = (rect.height() - 2 * padding) / 2.0f
|
val maxRadius = (rect.height() - 2 * padding) / 2.0f
|
||||||
// the real mark radius is scaled down by a factor depending on the maximal frequency
|
// the real mark radius is scaled down by a factor depending on the maximal frequency
|
||||||
val scale = 1.0f / maxFreq * value!!
|
val scale = 1.0f / maxFreq * valueCopy!!
|
||||||
val radius = maxRadius * scale
|
val radius = maxRadius * scale
|
||||||
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
|
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
|
||||||
pGraph!!.color = colors[colorIndex]
|
pGraph!!.color = colors[colorIndex]
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import org.isoron.uhabits.core.commands.CreateHabitCommand
|
|||||||
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
|
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
|
||||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||||
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
|
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
|
||||||
|
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.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||||
@@ -230,9 +231,10 @@ class ListHabitsScreen
|
|||||||
unit: String,
|
unit: String,
|
||||||
notes: String,
|
notes: String,
|
||||||
dateString: String,
|
dateString: String,
|
||||||
|
frequency: Frequency,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
numberPickerFactory.create(value, unit, notes, dateString, callback).show()
|
numberPickerFactory.create(value, unit, notes, dateString, frequency, callback).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showCheckmarkDialog(
|
override fun showCheckmarkDialog(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ 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.Entry
|
||||||
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
|
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
|
||||||
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
|
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
|
||||||
import org.isoron.uhabits.core.preferences.Preferences
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
@@ -143,6 +144,12 @@ class NumberButtonView(
|
|||||||
private val lowContrast: Int
|
private val lowContrast: Int
|
||||||
private val mediumContrast: Int
|
private val mediumContrast: Int
|
||||||
|
|
||||||
|
private val paint = TextPaint().apply {
|
||||||
|
typeface = getFontAwesome()
|
||||||
|
isAntiAlias = true
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
private val pUnit: TextPaint = TextPaint().apply {
|
private val pUnit: TextPaint = TextPaint().apply {
|
||||||
textSize = getDimension(context, R.dimen.smallerTextSize)
|
textSize = getDimension(context, R.dimen.smallerTextSize)
|
||||||
typeface = NORMAL_TYPEFACE
|
typeface = NORMAL_TYPEFACE
|
||||||
@@ -176,6 +183,11 @@ class NumberButtonView(
|
|||||||
val textSize: Float
|
val textSize: Float
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
value == Entry.SKIP.toDouble() / 1000 -> {
|
||||||
|
label = resources.getString(R.string.fa_skipped)
|
||||||
|
textSize = dim(R.dimen.smallTextSize)
|
||||||
|
typeface = getFontAwesome()
|
||||||
|
}
|
||||||
value >= 0 -> {
|
value >= 0 -> {
|
||||||
label = value.toShortString()
|
label = value.toShortString()
|
||||||
typeface = BOLD_TYPEFACE
|
typeface = BOLD_TYPEFACE
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
|||||||
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
|
||||||
import org.isoron.uhabits.core.commands.Command
|
import org.isoron.uhabits.core.commands.Command
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
|
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.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.preferences.Preferences
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
@@ -169,9 +170,10 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
unit: String,
|
unit: String,
|
||||||
notes: String,
|
notes: String,
|
||||||
dateString: String,
|
dateString: String,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
frequency: Frequency,
|
||||||
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, callback).show()
|
NumberPickerFactory(this@ShowHabitActivity).create(value, unit, notes, dateString, frequency, callback).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showCheckmarkDialog(
|
override fun showCheckmarkDialog(
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
|
|||||||
data.habit.unit,
|
data.habit.unit,
|
||||||
entry.notes,
|
entry.notes,
|
||||||
today.toDialogDateString(),
|
today.toDialogDateString(),
|
||||||
|
data.habit.frequency,
|
||||||
this
|
this
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,7 @@
|
|||||||
<string name="increment">Increment</string>
|
<string name="increment">Increment</string>
|
||||||
<string name="decrement">Decrement</string>
|
<string name="decrement">Decrement</string>
|
||||||
<string name="pref_skip_title">Enable skip days</string>
|
<string name="pref_skip_title">Enable skip days</string>
|
||||||
|
<string name="skip_day">Skip</string>
|
||||||
<string name="pref_skip_description">Toggle twice to add a skip instead of a checkmark. Skips keep your score unchanged and don\'t break your streak.</string>
|
<string name="pref_skip_description">Toggle twice to add a skip instead of a checkmark. Skips keep your score unchanged and don\'t break your streak.</string>
|
||||||
<string name="pref_unknown_title">Show question marks for missing data</string>
|
<string name="pref_unknown_title">Show question marks for missing data</string>
|
||||||
<string name="pref_unknown_description">Differentiate days without data from actual lapses. To enter a lapse, toggle twice.</string>
|
<string name="pref_unknown_description">Differentiate days without data from actual lapses. To enter a lapse, toggle twice.</string>
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ open class EntryList {
|
|||||||
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
|
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
|
||||||
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
|
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
|
||||||
*
|
*
|
||||||
|
* SKIP values are converted to zero (if they weren't, each SKIP day would count as 0.003).
|
||||||
|
*
|
||||||
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
|
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
|
||||||
* coming last. If the original list has gaps in it (for example, weeks or months without any
|
* coming last. If the original list has gaps in it (for example, weeks or months without any
|
||||||
* entries), then the list produced by this method will also have gaps.
|
* entries), then the list produced by this method will also have gaps.
|
||||||
@@ -289,7 +291,10 @@ fun List<Entry>.groupedSum(
|
|||||||
): List<Entry> {
|
): List<Entry> {
|
||||||
return this.map { (timestamp, value) ->
|
return this.map { (timestamp, value) ->
|
||||||
if (isNumerical) {
|
if (isNumerical) {
|
||||||
Entry(timestamp, max(0, value))
|
if (value == SKIP)
|
||||||
|
Entry(timestamp, 0)
|
||||||
|
else
|
||||||
|
Entry(timestamp, max(0, value))
|
||||||
} else {
|
} else {
|
||||||
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
|
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
|
||||||
}
|
}
|
||||||
@@ -301,6 +306,31 @@ fun List<Entry>.groupedSum(
|
|||||||
}.entries.map { (timestamp, entries) ->
|
}.entries.map { (timestamp, entries) ->
|
||||||
Entry(timestamp, entries.sumOf { it.value })
|
Entry(timestamp, entries.sumOf { it.value })
|
||||||
}.sortedBy { (timestamp, _) ->
|
}.sortedBy { (timestamp, _) ->
|
||||||
- timestamp.unixTime
|
-timestamp.unixTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of days with vaLue SKIP in the given period.
|
||||||
|
*/
|
||||||
|
fun List<Entry>.countSkippedDays(
|
||||||
|
truncateField: DateUtils.TruncateField,
|
||||||
|
firstWeekday: Int = Calendar.SATURDAY
|
||||||
|
): List<Entry> {
|
||||||
|
return this.map { (timestamp, value) ->
|
||||||
|
if (value == SKIP) {
|
||||||
|
Entry(timestamp, 1)
|
||||||
|
} else {
|
||||||
|
Entry(timestamp, 0)
|
||||||
|
}
|
||||||
|
}.groupBy { entry ->
|
||||||
|
entry.timestamp.truncate(
|
||||||
|
truncateField,
|
||||||
|
firstWeekday,
|
||||||
|
)
|
||||||
|
}.entries.map { (timestamp, entries) ->
|
||||||
|
Entry(timestamp, entries.sumOf { it.value })
|
||||||
|
}.sortedBy { (timestamp, _) ->
|
||||||
|
-timestamp.unixTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,20 +100,25 @@ class ScoreList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val normalizedRollingSum = rollingSum / 1000
|
val normalizedRollingSum = rollingSum / 1000
|
||||||
val percentageCompleted = if (!isAtMost) {
|
if (values[offset] != Entry.SKIP) {
|
||||||
if (targetValue > 0)
|
val percentageCompleted = if (!isAtMost) {
|
||||||
min(1.0, normalizedRollingSum / targetValue)
|
if (targetValue > 0)
|
||||||
else
|
min(1.0, normalizedRollingSum / targetValue)
|
||||||
1.0
|
else
|
||||||
} else {
|
1.0
|
||||||
if (targetValue > 0) {
|
|
||||||
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(0.0, 1.0)
|
|
||||||
} else {
|
} else {
|
||||||
if (normalizedRollingSum > 0) 0.0 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) {
|
||||||
rollingSum += 1.0
|
rollingSum += 1.0
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list
|
|||||||
import org.isoron.platform.time.LocalDate
|
import org.isoron.platform.time.LocalDate
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
||||||
|
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.HabitList
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
import org.isoron.uhabits.core.models.HabitType
|
import org.isoron.uhabits.core.models.HabitType
|
||||||
@@ -58,6 +59,7 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
habit.unit,
|
habit.unit,
|
||||||
entry.notes,
|
entry.notes,
|
||||||
timestamp.toDialogDateString(),
|
timestamp.toDialogDateString(),
|
||||||
|
habit.frequency
|
||||||
) { newValue: Double, newNotes: String, ->
|
) { newValue: Double, newNotes: String, ->
|
||||||
val value = (newValue * 1000).roundToInt()
|
val value = (newValue * 1000).roundToInt()
|
||||||
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
|
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
|
||||||
@@ -167,6 +169,7 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
unit: String,
|
unit: String,
|
||||||
notes: String,
|
notes: String,
|
||||||
dateString: String,
|
dateString: String,
|
||||||
|
frequency: Frequency,
|
||||||
callback: NumberPickerCallback
|
callback: NumberPickerCallback
|
||||||
)
|
)
|
||||||
fun showCheckmarkDialog(
|
fun showCheckmarkDialog(
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.isoron.uhabits.core.models.Entry
|
|||||||
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
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.Frequency
|
||||||
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.AT_LEAST
|
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
|
||||||
@@ -123,6 +124,7 @@ class HistoryCardPresenter(
|
|||||||
habit.unit,
|
habit.unit,
|
||||||
entry.notes,
|
entry.notes,
|
||||||
timestamp.toDialogDateString(),
|
timestamp.toDialogDateString(),
|
||||||
|
frequency = habit.frequency
|
||||||
) { newValue: Double, newNotes: String ->
|
) { newValue: Double, newNotes: String ->
|
||||||
val thousands = (newValue * 1000).roundToInt()
|
val thousands = (newValue * 1000).roundToInt()
|
||||||
commandRunner.run(
|
commandRunner.run(
|
||||||
@@ -154,6 +156,7 @@ class HistoryCardPresenter(
|
|||||||
entries.map {
|
entries.map {
|
||||||
when {
|
when {
|
||||||
it.value == Entry.UNKNOWN -> OFF
|
it.value == Entry.UNKNOWN -> OFF
|
||||||
|
it.value == SKIP -> HATCHED
|
||||||
(habit.targetType == AT_MOST) && (it.value / 1000.0 <= habit.targetValue) -> ON
|
(habit.targetType == AT_MOST) && (it.value / 1000.0 <= habit.targetValue) -> ON
|
||||||
(habit.targetType == AT_LEAST) && (it.value / 1000.0 >= habit.targetValue) -> ON
|
(habit.targetType == AT_LEAST) && (it.value / 1000.0 >= habit.targetValue) -> ON
|
||||||
else -> GREY
|
else -> GREY
|
||||||
@@ -196,8 +199,10 @@ class HistoryCardPresenter(
|
|||||||
unit: String,
|
unit: String,
|
||||||
notes: String,
|
notes: String,
|
||||||
dateString: String,
|
dateString: String,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
frequency: Frequency,
|
||||||
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
)
|
)
|
||||||
|
|
||||||
fun showCheckmarkDialog(
|
fun showCheckmarkDialog(
|
||||||
selectedValue: Int,
|
selectedValue: Int,
|
||||||
notes: String,
|
notes: String,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
|
|||||||
|
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.core.models.countSkippedDays
|
||||||
import org.isoron.uhabits.core.models.groupedSum
|
import org.isoron.uhabits.core.models.groupedSum
|
||||||
import org.isoron.uhabits.core.ui.views.Theme
|
import org.isoron.uhabits.core.ui.views.Theme
|
||||||
import org.isoron.uhabits.core.utils.DateUtils
|
import org.isoron.uhabits.core.utils.DateUtils
|
||||||
@@ -51,37 +52,59 @@ class TargetCardPresenter {
|
|||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
|
val skippedDayToday = entries.countSkippedDays(
|
||||||
|
truncateField = DateUtils.TruncateField.DAY
|
||||||
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisWeek = entries.groupedSum(
|
val valueThisWeek = entries.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
||||||
firstWeekday = firstWeekday,
|
firstWeekday = firstWeekday,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
|
val skippedDaysThisWeek = entries.countSkippedDays(
|
||||||
|
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
||||||
|
firstWeekday = firstWeekday
|
||||||
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisMonth = entries.groupedSum(
|
val valueThisMonth = entries.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.MONTH,
|
truncateField = DateUtils.TruncateField.MONTH,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
|
val skippedDaysThisMonth = entries.countSkippedDays(
|
||||||
|
truncateField = DateUtils.TruncateField.MONTH,
|
||||||
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisQuarter = entries.groupedSum(
|
val valueThisQuarter = entries.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.QUARTER,
|
truncateField = DateUtils.TruncateField.QUARTER,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
|
val skippedDaysThisQuarter = entries.countSkippedDays(
|
||||||
|
truncateField = DateUtils.TruncateField.QUARTER
|
||||||
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisYear = entries.groupedSum(
|
val valueThisYear = entries.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.YEAR,
|
truncateField = DateUtils.TruncateField.YEAR,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
|
val skippedDaysThisYear = entries.countSkippedDays(
|
||||||
|
truncateField = DateUtils.TruncateField.YEAR
|
||||||
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
|
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
|
||||||
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
|
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
|
||||||
val daysInQuarter = 91
|
val daysInQuarter = 91
|
||||||
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
|
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
|
||||||
|
|
||||||
val targetToday = habit.targetValue / habit.frequency.denominator
|
val dailyTarget = habit.targetValue / habit.frequency.denominator
|
||||||
val targetThisWeek = targetToday * 7
|
val targetToday = dailyTarget * (1 - skippedDayToday)
|
||||||
val targetThisMonth = targetToday * daysInMonth
|
val targetThisWeek = dailyTarget * (7 - skippedDaysThisWeek)
|
||||||
val targetThisQuarter = targetToday * daysInQuarter
|
val targetThisMonth = dailyTarget * (daysInMonth - skippedDaysThisMonth)
|
||||||
val targetThisYear = targetToday * daysInYear
|
val targetThisQuarter = dailyTarget * (daysInQuarter - skippedDaysThisQuarter)
|
||||||
|
val targetThisYear = dailyTarget * (daysInYear - skippedDaysThisYear)
|
||||||
|
|
||||||
val values = ArrayList<Double>()
|
val values = ArrayList<Double>()
|
||||||
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
|
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.hamcrest.MatcherAssert.assertThat
|
|||||||
import org.hamcrest.number.IsCloseTo
|
import org.hamcrest.number.IsCloseTo
|
||||||
import org.hamcrest.number.OrderingComparison
|
import org.hamcrest.number.OrderingComparison
|
||||||
import org.isoron.uhabits.core.BaseUnitTest
|
import org.isoron.uhabits.core.BaseUnitTest
|
||||||
|
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -381,6 +382,66 @@ class NumericalAtLeastScoreListTest : NumericalScoreListTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NumericalAtLeastScoreListWithSkipTest : NumericalScoreListTest() {
|
||||||
|
@Before
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getValue() {
|
||||||
|
addEntries(0, 10, 2000)
|
||||||
|
addEntries(10, 11, SKIP)
|
||||||
|
addEntries(11, 15, 2000)
|
||||||
|
addEntries(15, 16, SKIP)
|
||||||
|
addEntries(16, 20, 2000)
|
||||||
|
val expectedValues = doubleArrayOf(
|
||||||
|
0.617008,
|
||||||
|
0.596033,
|
||||||
|
0.573910,
|
||||||
|
0.550574,
|
||||||
|
0.525961,
|
||||||
|
0.500000,
|
||||||
|
0.472617,
|
||||||
|
0.443734,
|
||||||
|
0.413270,
|
||||||
|
0.381137,
|
||||||
|
0.347244, // skipped day should have the same score as the previous day
|
||||||
|
0.347244,
|
||||||
|
0.311495,
|
||||||
|
0.273788,
|
||||||
|
0.234017,
|
||||||
|
0.192067, // skipped day should have the same score as the previous day
|
||||||
|
0.192067,
|
||||||
|
0.147820,
|
||||||
|
0.101149,
|
||||||
|
0.051922,
|
||||||
|
0.000000,
|
||||||
|
0.000000,
|
||||||
|
0.000000
|
||||||
|
)
|
||||||
|
checkScoreValues(expectedValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun skipsShouldNotAffectScore() {
|
||||||
|
addEntries(0, 500, 1000)
|
||||||
|
val initialScore = habit.scores[today].value
|
||||||
|
|
||||||
|
addEntries(500, 1000, SKIP)
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(initialScore, E))
|
||||||
|
|
||||||
|
addEntries(0, 300, 1000)
|
||||||
|
addEntries(300, 500, SKIP)
|
||||||
|
addEntries(500, 700, 1000)
|
||||||
|
|
||||||
|
// skipped days should be treated as if they never existed
|
||||||
|
assertThat(habit.scores[today].value, IsCloseTo.closeTo(initialScore, E))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NumericalAtMostScoreListTest : NumericalScoreListTest() {
|
class NumericalAtMostScoreListTest : NumericalScoreListTest() {
|
||||||
@Before
|
@Before
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.hamcrest.MatcherAssert.assertThat
|
|||||||
import org.hamcrest.core.IsEqual.equalTo
|
import org.hamcrest.core.IsEqual.equalTo
|
||||||
import org.isoron.uhabits.core.BaseUnitTest
|
import org.isoron.uhabits.core.BaseUnitTest
|
||||||
import org.isoron.uhabits.core.models.Entry
|
import org.isoron.uhabits.core.models.Entry
|
||||||
|
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.preferences.Preferences
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
@@ -79,7 +80,14 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testOnEdit() {
|
fun testOnEdit() {
|
||||||
behavior.onEdit(habit2, getToday())
|
behavior.onEdit(habit2, getToday())
|
||||||
verify(screen).showNumberPicker(eq(0.1), eq("miles"), eq(""), eq("Jan 25, 2015"), picker.capture())
|
verify(screen).showNumberPicker(
|
||||||
|
eq(0.1),
|
||||||
|
eq("miles"),
|
||||||
|
eq(""),
|
||||||
|
eq("Jan 25, 2015"),
|
||||||
|
eq(Frequency.DAILY),
|
||||||
|
picker.capture()
|
||||||
|
)
|
||||||
picker.lastValue.onNumberPicked(100.0, "")
|
picker.lastValue.onNumberPicked(100.0, "")
|
||||||
val today = getTodayWithOffset()
|
val today = getTodayWithOffset()
|
||||||
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
|
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
|
||||||
|
|||||||
Reference in New Issue
Block a user