diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index 00d7d3c63..e932e8873 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -47,6 +47,7 @@ import org.isoron.uhabits.core.commands.EditHabitCommand import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitType +import org.isoron.uhabits.core.models.NumericalEmptyDaysMode import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.NumericalHistoryType import org.isoron.uhabits.core.models.PaletteColor @@ -86,6 +87,7 @@ class EditHabitActivity : AppCompatActivity() { var reminderMin = -1 var reminderDays: WeekdayList = WeekdayList.EVERY_DAY var historyType = NumericalHistoryType.TOTAL + var emptyDaysMode = NumericalEmptyDaysMode.EXCLUDE_EMPTY var targetType = NumericalHabitType.AT_LEAST override fun onCreate(state: Bundle?) { @@ -109,6 +111,7 @@ class EditHabitActivity : AppCompatActivity() { freqNum = habit.frequency.numerator freqDen = habit.frequency.denominator historyType = habit.historyType + emptyDaysMode = habit.emptyDaysMode targetType = habit.targetType habit.reminder?.let { reminderHour = it.hour @@ -195,6 +198,24 @@ class EditHabitActivity : AppCompatActivity() { dialog.dismissCurrentAndShow() } + populateIncludeEmptyDays() + binding.includeEmptyDaysPicker.setOnClickListener { + val builder = AlertDialog.Builder(this) + val arrayAdapter = ArrayAdapter(this, android.R.layout.select_dialog_item) + arrayAdapter.add(getString(R.string.yes)) + arrayAdapter.add(getString(R.string.no)) + builder.setAdapter(arrayAdapter) { dialog, which -> + emptyDaysMode = when (which) { + 0 -> NumericalEmptyDaysMode.INCLUDE_EMPTY + else -> NumericalEmptyDaysMode.EXCLUDE_EMPTY + } + populateIncludeEmptyDays() + dialog.dismiss() + } + val dialog = builder.create() + dialog.dismissCurrentAndShow() + } + populateTargetType() binding.targetTypePicker.setOnClickListener { val builder = AlertDialog.Builder(this) @@ -308,6 +329,7 @@ class EditHabitActivity : AppCompatActivity() { habit.unit = binding.unitInput.text.trim().toString() } habit.type = habitType + habit.emptyDaysMode = emptyDaysMode val command = if (habitId >= 0) { EditHabitCommand( @@ -372,6 +394,13 @@ class EditHabitActivity : AppCompatActivity() { else -> getString(R.string.average) } } + + private fun populateIncludeEmptyDays() { + binding.includeEmptyDaysPicker.text = when (emptyDaysMode) { + NumericalEmptyDaysMode.EXCLUDE_EMPTY -> getString(R.string.no) + else -> getString(R.string.yes) + } + } private fun populateTargetType() { binding.targetTypePicker.text = when (targetType) { NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most) diff --git a/uhabits-android/src/main/res/layout/activity_edit_habit.xml b/uhabits-android/src/main/res/layout/activity_edit_habit.xml index de728ba6d..2904ee1fb 100644 --- a/uhabits-android/src/main/res/layout/activity_edit_habit.xml +++ b/uhabits-android/src/main/res/layout/activity_edit_habit.xml @@ -172,19 +172,58 @@ - - - - - - + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:baselineAligned="false"> + + + + + + + + + + + + + + + + + + + + + + + + + + Calendar Unit History Type + Include empty days Target Type At least At most diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index 5bad8f507..92289824f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -338,34 +338,34 @@ fun List.groupedSum( fun List.groupedAverage( truncateField: DateUtils.TruncateField, firstWeekday: Int = Calendar.SATURDAY, - isNumerical: Boolean + isNumerical: Boolean, + emptyDaysMode: NumericalEmptyDaysMode ): List = this .map { (timestamp, value) -> if (isNumerical) { if (value == SKIP) Entry(timestamp, 0) - else Entry(timestamp, max(0, value)) + else Entry(timestamp, max(0, value)) // UNKNOWN/negatives -> 0 } else { Entry(timestamp, if (value == YES_MANUAL) 1000 else 0) } } .groupBy { entry -> - when (truncateField) { - DateUtils.TruncateField.WEEK_NUMBER -> - entry.timestamp.truncate(truncateField, firstWeekday) - else -> - entry.timestamp.truncate(truncateField, firstWeekday) - } + entry.timestamp.truncate(truncateField, firstWeekday) } .entries.map { (timestamp, entries) -> - val nonEmpty = entries.filter { it.value > 0} - val avg = if (nonEmpty.isEmpty()) 0 - else nonEmpty.map { it.value }.average().roundToInt() + val values = when (emptyDaysMode) { + NumericalEmptyDaysMode.INCLUDE_EMPTY -> + entries.map { it.value } + NumericalEmptyDaysMode.EXCLUDE_EMPTY -> + entries.map { it.value }.filter { it > 0 } + } + val avg = if (values.isEmpty()) 0 else values.average().roundToInt() Entry(timestamp, avg) } - // 4) Sort buckets (latest first) .sortedByDescending { (timestamp, _) -> timestamp.unixTime } + /** * Counts the number of days with vaLue SKIP in the given period. */ diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 7e711ea96..d0fb6c550 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -32,6 +32,7 @@ data class Habit( var question: String = "", var reminder: Reminder? = null, var historyType: NumericalHistoryType = NumericalHistoryType.TOTAL, + var emptyDaysMode: NumericalEmptyDaysMode = NumericalEmptyDaysMode.EXCLUDE_EMPTY, var targetType: NumericalHabitType = NumericalHabitType.AT_LEAST, var targetValue: Double = 0.0, var type: HabitType = HabitType.YES_NO, @@ -119,6 +120,7 @@ data class Habit( this.question = other.question this.reminder = other.reminder this.historyType = other.historyType + this.emptyDaysMode = other.emptyDaysMode this.targetType = other.targetType this.targetValue = other.targetValue this.type = other.type @@ -140,6 +142,7 @@ data class Habit( if (question != other.question) return false if (reminder != other.reminder) return false if (historyType != other.historyType) return false + if (emptyDaysMode != other.emptyDaysMode) return false if (targetType != other.targetType) return false if (targetValue != other.targetValue) return false if (type != other.type) return false @@ -160,6 +163,7 @@ data class Habit( result = 31 * result + question.hashCode() result = 31 * result + (reminder?.hashCode() ?: 0) result = 31 * result + historyType.value + result = 31 * result + emptyDaysMode.value result = 31 * result + targetType.value result = 31 * result + targetValue.hashCode() result = 31 * result + type.value diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/NumericalEmptyDaysMode.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/NumericalEmptyDaysMode.kt new file mode 100644 index 000000000..95c001514 --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/NumericalEmptyDaysMode.kt @@ -0,0 +1,17 @@ +package org.isoron.uhabits.core.models + +import java.lang.IllegalStateException + +enum class NumericalEmptyDaysMode(val value: Int) { + EXCLUDE_EMPTY(0), INCLUDE_EMPTY(1); + + companion object { + fun fromInt(value: Int): NumericalEmptyDaysMode { + return when (value) { + EXCLUDE_EMPTY.value -> EXCLUDE_EMPTY + INCLUDE_EMPTY.value -> INCLUDE_EMPTY + else -> throw IllegalStateException() + } + } + } +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt index bab06a5f7..5baa4079b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt @@ -70,7 +70,8 @@ class BarCardPresenter( NumericalHistoryType.AVERAGE -> habit.computedEntries.getByInterval(oldest, today).groupedAverage( truncateField = ScoreCardPresenter.getTruncateField(bucketSize), firstWeekday = firstWeekday, - isNumerical = habit.isNumerical + isNumerical = habit.isNumerical, + emptyDaysMode = habit.emptyDaysMode ) } return BarCardState(