Add option to include or exclude empty days in average calculation

pull/2215/head
Janssen 3 weeks ago
parent f6eb741b9d
commit 8d74e2fd03

@ -47,6 +47,7 @@ import org.isoron.uhabits.core.commands.EditHabitCommand
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.HabitType 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.NumericalHabitType
import org.isoron.uhabits.core.models.NumericalHistoryType import org.isoron.uhabits.core.models.NumericalHistoryType
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
@ -86,6 +87,7 @@ class EditHabitActivity : AppCompatActivity() {
var reminderMin = -1 var reminderMin = -1
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
var historyType = NumericalHistoryType.TOTAL var historyType = NumericalHistoryType.TOTAL
var emptyDaysMode = NumericalEmptyDaysMode.EXCLUDE_EMPTY
var targetType = NumericalHabitType.AT_LEAST var targetType = NumericalHabitType.AT_LEAST
override fun onCreate(state: Bundle?) { override fun onCreate(state: Bundle?) {
@ -109,6 +111,7 @@ class EditHabitActivity : AppCompatActivity() {
freqNum = habit.frequency.numerator freqNum = habit.frequency.numerator
freqDen = habit.frequency.denominator freqDen = habit.frequency.denominator
historyType = habit.historyType historyType = habit.historyType
emptyDaysMode = habit.emptyDaysMode
targetType = habit.targetType targetType = habit.targetType
habit.reminder?.let { habit.reminder?.let {
reminderHour = it.hour reminderHour = it.hour
@ -195,6 +198,24 @@ class EditHabitActivity : AppCompatActivity() {
dialog.dismissCurrentAndShow() dialog.dismissCurrentAndShow()
} }
populateIncludeEmptyDays()
binding.includeEmptyDaysPicker.setOnClickListener {
val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(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() populateTargetType()
binding.targetTypePicker.setOnClickListener { binding.targetTypePicker.setOnClickListener {
val builder = AlertDialog.Builder(this) val builder = AlertDialog.Builder(this)
@ -308,6 +329,7 @@ class EditHabitActivity : AppCompatActivity() {
habit.unit = binding.unitInput.text.trim().toString() habit.unit = binding.unitInput.text.trim().toString()
} }
habit.type = habitType habit.type = habitType
habit.emptyDaysMode = emptyDaysMode
val command = if (habitId >= 0) { val command = if (habitId >= 0) {
EditHabitCommand( EditHabitCommand(
@ -372,6 +394,13 @@ class EditHabitActivity : AppCompatActivity() {
else -> getString(R.string.average) 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() { private fun populateTargetType() {
binding.targetTypePicker.text = when (targetType) { binding.targetTypePicker.text = when (targetType) {
NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most) NumericalHabitType.AT_MOST -> getString(R.string.target_type_at_most)

@ -172,9 +172,16 @@
</FrameLayout> </FrameLayout>
<!-- History Type --> <!-- History Type -->
<FrameLayout <LinearLayout
android:id="@+id/historyTypeOuterBox" android:id="@+id/historyTypeOuterBox"
style="@style/FormOuterBox"> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/FormInnerBox"> <LinearLayout style="@style/FormInnerBox">
<TextView <TextView
style="@style/FormLabel" style="@style/FormLabel"
@ -182,9 +189,41 @@
<TextView <TextView
style="@style/FormDropdown" style="@style/FormDropdown"
android:id="@+id/historyTypePicker" android:id="@+id/historyTypePicker"
android:text="@string/total" /> android:text="@string/total"
android:textColor="?attr/contrast100"
/>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_weight="1">
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/include_empty_days" />
<TextView
style="@style/FormDropdown"
android:id="@+id/includeEmptyDaysPicker"
android:text="@string/no"
android:textColor="?attr/contrast100"
/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<!-- <FrameLayout-->
<!-- android:id="@+id/historyTypeOuterBox"-->
<!-- style="@style/FormOuterBox">-->
<!-- <LinearLayout style="@style/FormInnerBox">-->
<!-- <TextView-->
<!-- style="@style/FormLabel"-->
<!-- android:text="@string/history_type" />-->
<!-- <TextView-->
<!-- style="@style/FormDropdown"-->
<!-- android:id="@+id/historyTypePicker"-->
<!-- android:text="@string/total" />-->
<!-- </LinearLayout>-->
<!-- </FrameLayout>-->
<LinearLayout <LinearLayout
android:id="@+id/targetOuterBox" android:id="@+id/targetOuterBox"

@ -185,6 +185,7 @@
<string name="calendar">Calendar</string> <string name="calendar">Calendar</string>
<string name="unit">Unit</string> <string name="unit">Unit</string>
<string name="history_type">History Type</string> <string name="history_type">History Type</string>
<string name="include_empty_days">Include empty days</string>
<string name="target_type">Target Type</string> <string name="target_type">Target Type</string>
<string name="target_type_at_least">At least</string> <string name="target_type_at_least">At least</string>
<string name="target_type_at_most">At most</string> <string name="target_type_at_most">At most</string>

@ -338,34 +338,34 @@ fun List<Entry>.groupedSum(
fun List<Entry>.groupedAverage( fun List<Entry>.groupedAverage(
truncateField: DateUtils.TruncateField, truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY, firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean isNumerical: Boolean,
emptyDaysMode: NumericalEmptyDaysMode
): List<Entry> = ): List<Entry> =
this this
.map { (timestamp, value) -> .map { (timestamp, value) ->
if (isNumerical) { if (isNumerical) {
if (value == SKIP) Entry(timestamp, 0) if (value == SKIP) Entry(timestamp, 0)
else Entry(timestamp, max(0, value)) else Entry(timestamp, max(0, value)) // UNKNOWN/negatives -> 0
} else { } else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0) Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
} }
} }
.groupBy { entry -> .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) -> .entries.map { (timestamp, entries) ->
val nonEmpty = entries.filter { it.value > 0} val values = when (emptyDaysMode) {
val avg = if (nonEmpty.isEmpty()) 0 NumericalEmptyDaysMode.INCLUDE_EMPTY ->
else nonEmpty.map { it.value }.average().roundToInt() 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) Entry(timestamp, avg)
} }
// 4) Sort buckets (latest first)
.sortedByDescending { (timestamp, _) -> timestamp.unixTime } .sortedByDescending { (timestamp, _) -> timestamp.unixTime }
/** /**
* Counts the number of days with vaLue SKIP in the given period. * Counts the number of days with vaLue SKIP in the given period.
*/ */

@ -32,6 +32,7 @@ data class Habit(
var question: String = "", var question: String = "",
var reminder: Reminder? = null, var reminder: Reminder? = null,
var historyType: NumericalHistoryType = NumericalHistoryType.TOTAL, var historyType: NumericalHistoryType = NumericalHistoryType.TOTAL,
var emptyDaysMode: NumericalEmptyDaysMode = NumericalEmptyDaysMode.EXCLUDE_EMPTY,
var targetType: NumericalHabitType = NumericalHabitType.AT_LEAST, var targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
var targetValue: Double = 0.0, var targetValue: Double = 0.0,
var type: HabitType = HabitType.YES_NO, var type: HabitType = HabitType.YES_NO,
@ -119,6 +120,7 @@ data class Habit(
this.question = other.question this.question = other.question
this.reminder = other.reminder this.reminder = other.reminder
this.historyType = other.historyType this.historyType = other.historyType
this.emptyDaysMode = other.emptyDaysMode
this.targetType = other.targetType this.targetType = other.targetType
this.targetValue = other.targetValue this.targetValue = other.targetValue
this.type = other.type this.type = other.type
@ -140,6 +142,7 @@ data class Habit(
if (question != other.question) return false if (question != other.question) return false
if (reminder != other.reminder) return false if (reminder != other.reminder) return false
if (historyType != other.historyType) return false if (historyType != other.historyType) return false
if (emptyDaysMode != other.emptyDaysMode) return false
if (targetType != other.targetType) return false if (targetType != other.targetType) return false
if (targetValue != other.targetValue) return false if (targetValue != other.targetValue) return false
if (type != other.type) return false if (type != other.type) return false
@ -160,6 +163,7 @@ data class Habit(
result = 31 * result + question.hashCode() result = 31 * result + question.hashCode()
result = 31 * result + (reminder?.hashCode() ?: 0) result = 31 * result + (reminder?.hashCode() ?: 0)
result = 31 * result + historyType.value result = 31 * result + historyType.value
result = 31 * result + emptyDaysMode.value
result = 31 * result + targetType.value result = 31 * result + targetType.value
result = 31 * result + targetValue.hashCode() result = 31 * result + targetValue.hashCode()
result = 31 * result + type.value result = 31 * result + type.value

@ -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()
}
}
}
}

@ -70,7 +70,8 @@ class BarCardPresenter(
NumericalHistoryType.AVERAGE -> habit.computedEntries.getByInterval(oldest, today).groupedAverage( NumericalHistoryType.AVERAGE -> habit.computedEntries.getByInterval(oldest, today).groupedAverage(
truncateField = ScoreCardPresenter.getTruncateField(bucketSize), truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
firstWeekday = firstWeekday, firstWeekday = firstWeekday,
isNumerical = habit.isNumerical isNumerical = habit.isNumerical,
emptyDaysMode = habit.emptyDaysMode
) )
} }
return BarCardState( return BarCardState(

Loading…
Cancel
Save