Add option to set default value for habits on unknown days

pull/1106/head
KristianTashkov 4 years ago
parent 2ab6c396d0
commit c7917dc185

@ -62,6 +62,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
firstWeekday = preferences.firstWeekday,
paletteColor = habit.color,
series = emptyList(),
defaultSquare = HistoryChart.Square.OFF,
theme = themeSwitcher.currentTheme,
today = DateUtils.getTodayWithOffset().toLocalDate(),
onDateClickedListener = onDateClickedListener ?: OnDateClickedListener { },
@ -101,6 +102,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
theme = LightTheme()
)
chart?.series = model.series
chart?.defaultSquare = model.defaultSquare
dataView.postInvalidate()
}

@ -35,11 +35,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.android.datetimepicker.time.RadialPickerLayout
import com.android.datetimepicker.time.TimePickerDialog
import kotlinx.android.synthetic.main.activity_edit_habit.nameInput
import kotlinx.android.synthetic.main.activity_edit_habit.notesInput
import kotlinx.android.synthetic.main.activity_edit_habit.questionInput
import kotlinx.android.synthetic.main.activity_edit_habit.targetInput
import kotlinx.android.synthetic.main.activity_edit_habit.unitInput
import kotlinx.android.synthetic.main.activity_edit_habit.*
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
@ -50,6 +46,7 @@ import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
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.HabitType
@ -116,6 +113,10 @@ class EditHabitActivity : AppCompatActivity() {
binding.questionInput.setText(habit.question)
binding.notesInput.setText(habit.description)
binding.unitInput.setText(habit.unit)
binding.defaultValueInput.setText((habit.defaultValue / 1000.0).toString())
binding.defaultNo.isChecked = habit.defaultValue == Entry.NO
binding.defaultYes.isChecked = habit.defaultValue == Entry.YES_MANUAL
binding.defaultSkip.isChecked = habit.defaultValue == Entry.SKIP
binding.targetInput.setText(habit.targetValue.toString())
} else {
habitType = HabitType.fromInt(intent.getIntExtra("habitType", HabitType.YES_NO.value))
@ -138,11 +139,15 @@ class EditHabitActivity : AppCompatActivity() {
HabitType.YES_NO -> {
binding.unitOuterBox.visibility = View.GONE
binding.targetOuterBox.visibility = View.GONE
binding.numericalFrequencyOuterBox.visibility = View.GONE
if (!component.preferences.isSkipEnabled)
binding.defaultSkip.visibility = View.GONE
}
HabitType.NUMERICAL -> {
binding.nameInput.hint = getString(R.string.measurable_short_example)
binding.questionInput.hint = getString(R.string.measurable_question_example)
binding.frequencyOuterBox.visibility = View.GONE
binding.yesNoDefaultOuterBox.visibility = View.GONE
}
}
@ -264,6 +269,15 @@ class EditHabitActivity : AppCompatActivity() {
habit.targetValue = targetInput.text.toString().toDouble()
habit.targetType = NumericalHabitType.AT_LEAST
habit.unit = unitInput.text.trim().toString()
habit.defaultValue = kotlin.math.floor(
defaultValueInput.text.toString().toDouble() * 1000.0
).toInt()
} else {
habit.defaultValue = when {
defaultYes.isChecked -> Entry.YES_MANUAL
defaultSkip.isChecked -> Entry.SKIP
else -> Entry.NO
}
}
habit.type = habitType

@ -65,6 +65,12 @@ class CheckmarkButtonView(
invalidate()
}
var defaultValue: Int = 0
set(value) {
field = value
invalidate()
}
var value: Int = 0
set(value) {
field = value
@ -128,7 +134,8 @@ class CheckmarkButtonView(
}
fun draw(canvas: Canvas) {
paint.color = when (value) {
val realValue = if (value != UNKNOWN) value else defaultValue
paint.color = when (realValue) {
YES_MANUAL, YES_AUTO, SKIP -> color
NO -> {
if (preferences.areQuestionMarksEnabled) mediumContrastColor
@ -136,15 +143,14 @@ class CheckmarkButtonView(
}
else -> lowContrastColor
}
val id = when (value) {
var id = when (realValue) {
SKIP -> R.string.fa_skipped
NO -> R.string.fa_times
UNKNOWN -> {
if (preferences.areQuestionMarksEnabled) R.string.fa_question
else R.string.fa_times
}
else -> R.string.fa_check
}
if (value == UNKNOWN && preferences.areQuestionMarksEnabled)
id = R.string.fa_question
if (value == YES_AUTO) {
paint.strokeWidth = 5f
paint.style = Paint.Style.STROKE

@ -48,6 +48,12 @@ class CheckmarkPanelView(
setupButtons()
}
var defaultValue = 0
set(value) {
field = value
setupButtons()
}
var color = 0
set(value) {
field = value
@ -72,6 +78,7 @@ class CheckmarkPanelView(
index + dataOffset < values.size -> values[index + dataOffset]
else -> UNKNOWN
}
button.defaultValue = defaultValue
button.color = color
button.onToggle = { value -> onToggle(timestamp, value) }
}

@ -228,6 +228,7 @@ class HabitCardView(
}
checkmarkPanel.apply {
color = c
defaultValue = h.defaultValue
visibility = when (h.isNumerical) {
true -> View.GONE
false -> View.VISIBLE
@ -235,6 +236,7 @@ class HabitCardView(
}
numberPanel.apply {
color = c
defaultValue = h.defaultValue / 1000.0
units = h.unit
threshold = h.targetValue
visibility = when (h.isNumerical) {

@ -76,6 +76,12 @@ class NumberButtonView(
invalidate()
}
var defaultValue = 0.0
set(value) {
field = value
invalidate()
}
var value = 0.0
set(value) {
field = value
@ -153,9 +159,10 @@ class NumberButtonView(
}
fun draw(canvas: Canvas) {
val realValue = if (value >= 0) value else defaultValue
val activeColor = when {
value <= 0.0 -> lowContrast
value < threshold -> mediumContrast
realValue == 0.0 -> lowContrast
realValue < threshold -> mediumContrast
else -> color
}
@ -175,7 +182,7 @@ class NumberButtonView(
textSize = dim(R.dimen.smallerTextSize)
}
else -> {
label = "0"
label = defaultValue.toShortString()
typeface = BOLD_TYPEFACE
textSize = dim(R.dimen.smallTextSize)
}

@ -47,6 +47,12 @@ class NumberPanelView(
setupButtons()
}
var defaultValue = 0.0
set(value) {
field = value
setupButtons()
}
var threshold = 0.0
set(value) {
field = value
@ -83,6 +89,7 @@ class NumberPanelView(
index + dataOffset < values.size -> values[index + dataOffset]
else -> 0.0
}
button.defaultValue = defaultValue
button.color = color
button.threshold = threshold
button.units = units

@ -43,6 +43,7 @@ class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(cont
theme = state.theme,
dateFormatter = JavaLocalDateFormatter(Locale.getDefault()),
series = state.series,
defaultSquare = state.defaultSquare,
firstWeekday = state.firstWeekday,
)
binding.chart.postInvalidate()

@ -56,7 +56,9 @@ class HistoryWidget(
theme = WidgetTheme(),
)
(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()),
firstWeekday = prefs.firstWeekday,
series = listOf(),
defaultSquare = HistoryChart.Square.OFF
)
}
).apply {

@ -151,6 +151,35 @@
</LinearLayout>
</FrameLayout>
<!-- DefaultValue -->
<FrameLayout
android:id="@+id/yesNoDefaultOuterBox"
style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/defaultValue" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton android:id="@+id/defaultNo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/no"/>
<RadioButton android:id="@+id/defaultYes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/yes"/>
<RadioButton android:id="@+id/defaultSkip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/skip"/>
</RadioGroup>
</LinearLayout>
</FrameLayout>
<!-- Target value, unit and frequency -->
<FrameLayout
android:id="@+id/unitOuterBox"
@ -180,13 +209,13 @@
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/target" />
android:text="@string/startAt" />
<EditText
style="@style/FormInput"
android:id="@+id/targetInput"
android:id="@+id/defaultValueInput"
android:maxLines="1"
android:inputType="numberDecimal"
android:hint="@string/example_target"/>
android:text="0"/>
</LinearLayout>
</FrameLayout>
<FrameLayout
@ -196,24 +225,40 @@
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/frequency" />
<TextView
style="@style/FormDropdown"
android:id="@+id/numericalFrequencyPicker"
android:text="@string/every_week"
android:textColor="?attr/contrast100"
/>
android:text="@string/target" />
<EditText
style="@style/FormInput"
android:id="@+id/targetInput"
android:maxLines="1"
android:inputType="numberDecimal"
android:hint="@string/example_target"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<FrameLayout
android:id='@+id/numericalFrequencyOuterBox'
style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/frequency" />
<TextView
style="@style/FormDropdown"
android:id="@+id/numericalFrequencyPicker"
android:text="@string/every_week"
android:textColor="?attr/contrast100"
/>
</LinearLayout>
</FrameLayout>
<!-- Reminder -->
<FrameLayout style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/reminder" />
<TextView
style="@style/FormDropdown"
android:id="@+id/reminderTimePicker"

@ -118,6 +118,7 @@
<string name="developers">Developers</string>
<string name="version_n">Version %s</string>
<string name="frequency">Frequency</string>
<string name="defaultValue">Default value</string>
<string name="checkmark">Checkmark</string>
<string name="checkmark_stack_widget" formatted="false">Checkmark Stack Widget</string>
<string name="frequency_stack_widget" formatted="false">Frequency Stack Widget</string>
@ -186,9 +187,11 @@
<string name="unit">Unit</string>
<string name="example_question_boolean">e.g. Did you exercise today?</string>
<string name="question">Question</string>
<string name="startAt">Starts at</string>
<string name="target">Target</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="skip">Skip</string>
<string name="customize_notification_summary">Change sound, vibration, light and other notification settings</string>
<string name="customize_notification">Customize notifications</string>
<string name="pref_view_privacy">View privacy policy</string>

@ -20,4 +20,4 @@ package org.isoron.uhabits.core
const val DATABASE_FILENAME = "uhabits.db"
const val DATABASE_VERSION = 24
const val DATABASE_VERSION = 25

@ -35,6 +35,7 @@ data class Habit(
var targetValue: Double = 0.0,
var type: HabitType = HabitType.YES_NO,
var unit: String = "",
var defaultValue: Int = 0,
var uuid: String? = null,
val computedEntries: EntryList,
val originalEntries: EntryList,
@ -69,7 +70,7 @@ data class Habit(
}
fun isFailedToday(): Boolean {
val today = DateUtils.getTodayWithOffset()
var today = DateUtils.getTodayWithOffset()
val value = computedEntries.get(today).value
return if (isNumerical) {
when (targetType) {
@ -88,15 +89,17 @@ data class Habit(
isNumerical = isNumerical,
)
val to = DateUtils.getTodayWithOffset().plus(30)
val today = DateUtils.getTodayWithOffset()
val to = today.plus(30)
val entries = computedEntries.getKnown()
var from = entries.lastOrNull()?.timestamp ?: to
var from = entries.lastOrNull()?.timestamp ?: today
if (from.isNewerThan(to)) from = to
scores.recompute(
frequency = frequency,
isNumerical = isNumerical,
targetValue = targetValue,
defaultValue = defaultValue,
computedEntries = computedEntries,
from = from,
to = to,
@ -123,6 +126,7 @@ data class Habit(
this.targetValue = other.targetValue
this.type = other.type
this.unit = other.unit
this.defaultValue = other.defaultValue
this.uuid = other.uuid
}
@ -143,6 +147,7 @@ data class Habit(
if (targetValue != other.targetValue) return false
if (type != other.type) return false
if (unit != other.unit) return false
if (defaultValue != other.defaultValue) return false
if (uuid != other.uuid) return false
return true
@ -162,6 +167,7 @@ data class Habit(
result = 31 * result + targetValue.hashCode()
result = 31 * result + type.value
result = 31 * result + unit.hashCode()
result = 31 * result + defaultValue.hashCode()
result = 31 * result + (uuid?.hashCode() ?: 0)
return result
}

@ -22,7 +22,6 @@ import org.isoron.uhabits.core.models.Score.Companion.compute
import java.util.ArrayList
import java.util.HashMap
import javax.annotation.concurrent.ThreadSafe
import kotlin.math.max
import kotlin.math.min
@ThreadSafe
@ -69,18 +68,18 @@ class ScoreList {
frequency: Frequency,
isNumerical: Boolean,
targetValue: Double,
defaultValue: Int,
computedEntries: EntryList,
from: Timestamp,
to: Timestamp,
) {
map.clear()
if (computedEntries.getKnown().isEmpty()) return
if (from.isNewerThan(to)) return
var rollingSum = 0.0
var numerator = frequency.numerator
var denominator = frequency.denominator
val freq = frequency.toDouble()
val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray()
val values = computedEntries.getByInterval(from, to).map {
if (it.value >= 0) it.value else defaultValue
}.toIntArray()
// 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
@ -90,19 +89,32 @@ class ScoreList {
denominator *= 2
}
var rollingSum = 0.0
var previousValue = 0.0
val numericalPercentageComplete = { valueAccumulated: Double ->
if (targetValue > 0) {
min(1.0, valueAccumulated / 1000.0 / targetValue)
} else {
1.0
}
}
if (isNumerical) {
rollingSum = defaultValue.toDouble() * denominator
previousValue = numericalPercentageComplete(rollingSum)
rollingSum -= defaultValue
} else if (defaultValue == Entry.YES_MANUAL) {
previousValue = 1.0
rollingSum = denominator.toDouble() - 1
}
for (i in values.indices) {
val offset = values.size - i - 1
if (isNumerical) {
rollingSum += max(0, values[offset])
rollingSum += values[offset]
if (offset + denominator < values.size) {
rollingSum -= values[offset + denominator]
}
val percentageCompleted = if (targetValue > 0) {
min(1.0, rollingSum / 1000 / targetValue)
} else {
1.0
}
val percentageCompleted = numericalPercentageComplete(rollingSum)
previousValue = compute(freq, previousValue, percentageCompleted)
} else {
if (values[offset] == Entry.YES_MANUAL) {

@ -82,6 +82,9 @@ class HabitRecord {
@field:Column
var unit: String? = null
@field:Column
var defaultValue: Int? = null
@field:Column
var id: Long? = null
@ -99,6 +102,7 @@ class HabitRecord {
targetType = model.targetType.value
targetValue = model.targetValue
unit = model.unit
defaultValue = model.defaultValue
position = model.position
question = model.question
uuid = model.uuid
@ -128,6 +132,7 @@ class HabitRecord {
habit.targetType = NumericalHabitType.fromInt(targetType!!)
habit.targetValue = targetValue!!
habit.unit = unit!!
habit.defaultValue = defaultValue!!
habit.position = position!!
habit.uuid = uuid
if (reminderHour != null && reminderMin != null) {

@ -20,6 +20,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.Timestamp
@ -48,9 +49,10 @@ open class ListHabitsBehavior @Inject constructor(
fun onEdit(habit: Habit, timestamp: Timestamp?) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp!!).value.toDouble()
var oldValue = entries.get(timestamp!!).value
oldValue = if (oldValue != Entry.UNKNOWN) oldValue else habit.defaultValue
screen.showNumberPicker(
oldValue / 1000,
oldValue.toDouble() / 1000,
habit.unit
) { newValue: Double ->
val value = (newValue * 1000).roundToInt()

@ -25,6 +25,7 @@ import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.models.Habit
@ -37,13 +38,13 @@ import org.isoron.uhabits.core.ui.views.HistoryChart
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils
import kotlin.math.max
import kotlin.math.roundToInt
data class HistoryCardState(
val color: PaletteColor,
val firstWeekday: DayOfWeek,
val series: List<HistoryChart.Square>,
val defaultSquare: HistoryChart.Square,
val theme: Theme,
val today: LocalDate,
)
@ -61,7 +62,8 @@ class HistoryCardPresenter(
screen.showFeedback()
if (habit.isNumerical) {
val entries = habit.computedEntries
val oldValue = entries.get(timestamp).value
var oldValue = entries.get(timestamp).value
oldValue = if (oldValue != UNKNOWN) oldValue else habit.defaultValue
screen.showNumberPicker(oldValue / 1000.0, habit.unit) { newValue: Double ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run(
@ -74,7 +76,8 @@ class HistoryCardPresenter(
)
}
} else {
val currentValue = habit.computedEntries.get(timestamp).value
var currentValue = habit.computedEntries.get(timestamp).value
currentValue = if (currentValue != UNKNOWN) currentValue else habit.defaultValue
val nextValue = Entry.nextToggleValue(
value = currentValue,
isSkipEnabled = preferences.isSkipEnabled,
@ -103,26 +106,33 @@ class HistoryCardPresenter(
): HistoryCardState {
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today)
val series = if (habit.isNumerical) {
entries.map {
Entry(it.timestamp, max(0, it.value))
}.map {
when (it.value) {
0 -> HistoryChart.Square.OFF
else -> HistoryChart.Square.ON
}
val entryValues = habit.computedEntries.getByInterval(oldest, today).map {
if (it.value != UNKNOWN) it.value else habit.defaultValue
}
val numericalToSquares = { value: Int ->
when (value) {
0 -> HistoryChart.Square.OFF
else -> HistoryChart.Square.ON
}
} else {
entries.map {
when (it.value) {
YES_MANUAL -> HistoryChart.Square.ON
YES_AUTO -> HistoryChart.Square.DIMMED
SKIP -> HistoryChart.Square.HATCHED
else -> HistoryChart.Square.OFF
}
}
val yesNoToSquares = { value: Int ->
when (value) {
YES_MANUAL -> HistoryChart.Square.ON
YES_AUTO -> HistoryChart.Square.DIMMED
SKIP -> HistoryChart.Square.HATCHED
else -> HistoryChart.Square.OFF
}
}
val series = if (habit.isNumerical) {
entryValues.map(numericalToSquares)
} else {
entryValues.map(yesNoToSquares)
}
val defaultSquare = if (habit.isNumerical) {
numericalToSquares(habit.defaultValue)
} else {
yesNoToSquares(habit.defaultValue)
}
return HistoryCardState(
color = habit.color,
@ -130,6 +140,7 @@ class HistoryCardPresenter(
today = today.toLocalDate(),
theme = theme,
series = series,
defaultSquare = defaultSquare
)
}
}

@ -41,6 +41,7 @@ class HistoryChart(
var firstWeekday: DayOfWeek,
var paletteColor: PaletteColor,
var series: List<Square>,
var defaultSquare: Square,
var theme: Theme,
var today: LocalDate,
var onDateClickedListener: OnDateClickedListener = OnDateClickedListener { },
@ -189,7 +190,7 @@ class HistoryChart(
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 color = theme.color(paletteColor.paletteIndex)
squareColor = when (value) {

@ -0,0 +1 @@
alter table Habits add column defaultValue integer not null default 0;
Loading…
Cancel
Save