implement average aggregation

pull/2121/head
DasCapschen 6 months ago
parent 107c898f51
commit efd82cc001

@ -44,6 +44,7 @@ import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.EditHabitCommand import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.models.AggregationType
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
@ -85,6 +86,7 @@ class EditHabitActivity : AppCompatActivity() {
var reminderMin = -1 var reminderMin = -1
var reminderDays: WeekdayList = WeekdayList.EVERY_DAY var reminderDays: WeekdayList = WeekdayList.EVERY_DAY
var targetType = NumericalHabitType.AT_LEAST var targetType = NumericalHabitType.AT_LEAST
var aggregationType = AggregationType.SUM
override fun onCreate(state: Bundle?) { override fun onCreate(state: Bundle?) {
super.onCreate(state) super.onCreate(state)
@ -107,6 +109,7 @@ class EditHabitActivity : AppCompatActivity() {
freqNum = habit.frequency.numerator freqNum = habit.frequency.numerator
freqDen = habit.frequency.denominator freqDen = habit.frequency.denominator
targetType = habit.targetType targetType = habit.targetType
aggregationType = habit.aggregationType
habit.reminder?.let { habit.reminder?.let {
reminderHour = it.hour reminderHour = it.hour
reminderMin = it.minute reminderMin = it.minute
@ -191,6 +194,24 @@ class EditHabitActivity : AppCompatActivity() {
dialog.dismissCurrentAndShow() dialog.dismissCurrentAndShow()
} }
populateAggregationType()
binding.aggregationTypePicker.setOnClickListener {
val builder = AlertDialog.Builder(this)
val arrayAdapter = ArrayAdapter<String>(this, android.R.layout.select_dialog_item)
arrayAdapter.add(getString(R.string.aggregation_type_sum))
arrayAdapter.add(getString(R.string.aggregation_type_average))
builder.setAdapter(arrayAdapter) { dialog, which ->
aggregationType = when (which) {
0 -> AggregationType.SUM
else -> AggregationType.AVERAGE
}
populateAggregationType()
dialog.dismiss()
}
val dialog = builder.create()
dialog.dismissCurrentAndShow()
}
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)
@ -282,6 +303,7 @@ class EditHabitActivity : AppCompatActivity() {
if (habitType == HabitType.NUMERICAL) { if (habitType == HabitType.NUMERICAL) {
habit.targetValue = binding.targetInput.text.toString().toDouble() habit.targetValue = binding.targetInput.text.toString().toDouble()
habit.targetType = targetType habit.targetType = targetType
habit.aggregationType = aggregationType
habit.unit = binding.unitInput.text.trim().toString() habit.unit = binding.unitInput.text.trim().toString()
} }
habit.type = habitType habit.type = habitType
@ -350,6 +372,13 @@ class EditHabitActivity : AppCompatActivity() {
} }
} }
private fun populateAggregationType() {
binding.aggregationTypePicker.text = when(aggregationType) {
AggregationType.SUM -> getString(R.string.aggregation_type_sum)
AggregationType.AVERAGE -> getString(R.string.aggregation_type_average)
}
}
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)

@ -211,21 +211,52 @@
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
<FrameLayout <LinearLayout
android:id="@+id/targetTypeOuterBox" android:id="@+id/targetTypeOuterBox"
style="@style/FormOuterBox"> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<LinearLayout style="@style/FormInnerBox"> <LinearLayout style="@style/FormInnerBox">
<TextView <TextView
style="@style/FormLabel" style="@style/FormLabel"
android:text="@string/target_type" /> android:text="@string/target_type" />
<TextView <TextView
style="@style/FormDropdown"
android:id="@+id/targetTypePicker" android:id="@+id/targetTypePicker"
android:textColor="?attr/contrast100" style="@style/FormDropdown"
/> android:textColor="?attr/contrast100" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
<FrameLayout
style="@style/FormOuterBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout style="@style/FormInnerBox">
<TextView
style="@style/FormLabel"
android:text="@string/aggregation_type" />
<TextView
android:id="@+id/aggregationTypePicker"
style="@style/FormDropdown"
android:textColor="?attr/contrast100" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<!-- Reminder --> <!-- Reminder -->
<FrameLayout style="@style/FormOuterBox"> <FrameLayout style="@style/FormOuterBox">

@ -218,4 +218,7 @@
<string name="activity_not_found">Für diese Aktion wurde keine App gefunden.</string> <string name="activity_not_found">Für diese Aktion wurde keine App gefunden.</string>
<string name="pref_midnight_delay_title">Verlängere den Tag um ein paar Stunden nach Mitternacht</string> <string name="pref_midnight_delay_title">Verlängere den Tag um ein paar Stunden nach Mitternacht</string>
<string name="pref_midnight_delay_description">Bis 3:00 Uhr warten, bevor ein neuer Tag angezeigt wird. Nützlich, wenn du normalerweise nach Mitternacht schlafen gehst. Benötigt einen Neustart der App.</string> <string name="pref_midnight_delay_description">Bis 3:00 Uhr warten, bevor ein neuer Tag angezeigt wird. Nützlich, wenn du normalerweise nach Mitternacht schlafen gehst. Benötigt einen Neustart der App.</string>
<string name="aggregation_type">Aggregation</string>
<string name="aggregation_type_sum">Summe</string>
<string name="aggregation_type_average">Mittelwert</string>
</resources> </resources>

@ -233,4 +233,7 @@
<string name="activity_not_found">No app was found to support this action</string> <string name="activity_not_found">No app was found to support this action</string>
<string name="pref_midnight_delay_title">Extend day a few hours past midnight</string> <string name="pref_midnight_delay_title">Extend day a few hours past midnight</string>
<string name="pref_midnight_delay_description">Wait until 3:00 AM to show a new day. Useful if you typically go to sleep after midnight. Requires app restart.</string> <string name="pref_midnight_delay_description">Wait until 3:00 AM to show a new day. Useful if you typically go to sleep after midnight. Requires app restart.</string>
<string name="aggregation_type">Aggregation</string>
<string name="aggregation_type_sum">Sum</string>
<string name="aggregation_type_average">Average</string>
</resources> </resources>

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

@ -0,0 +1,15 @@
package org.isoron.uhabits.core.models
enum class AggregationType(val value: Int) {
SUM(0), AVERAGE(1);
companion object {
fun fromInt(value: Int): AggregationType {
return when (value) {
SUM.value -> SUM
AVERAGE.value -> AVERAGE
else -> throw IllegalStateException()
}
}
}
}

@ -30,6 +30,7 @@ import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
@ThreadSafe @ThreadSafe
open class EntryList { open class EntryList {
@ -285,20 +286,28 @@ open class EntryList {
* *
* SKIP values are converted to zero (if they weren't, each SKIP day would count as 0.003). * SKIP values are converted to zero (if they weren't, each SKIP day would count as 0.003).
* *
* If average aggregation is used, we do not convert any values. Instead we filter out special
* values like SKIP and UNKNOWN, because they should not contribute to the average calculated,
* but if the user explicitly enters a 0, it SHOULD count towards the average.
* Because we filter out entries, we must also be careful not to divide by zero.
*
* 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.
* *
* The argument [firstWeekday] is only relevant when truncating by week. * The argument [firstWeekday] is only relevant when truncating by week.
*/ */
fun List<Entry>.groupedSum( fun List<Entry>.groupedAggregate(
truncateField: DateUtils.TruncateField, truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY, firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean isNumerical: Boolean,
aggregationType: AggregationType
): List<Entry> { ): List<Entry> {
return this.map { (timestamp, value) -> return this.map { (timestamp, value) ->
if (isNumerical) { if (isNumerical) {
if (value == SKIP) { if (aggregationType == AggregationType.AVERAGE) {
Entry(timestamp, value)
} else if (value == SKIP) {
Entry(timestamp, 0) Entry(timestamp, 0)
} else { } else {
Entry(timestamp, max(0, value)) Entry(timestamp, max(0, value))
@ -312,7 +321,17 @@ fun List<Entry>.groupedSum(
firstWeekday firstWeekday
) )
}.entries.map { (timestamp, entries) -> }.entries.map { (timestamp, entries) ->
if (isNumerical && aggregationType == AggregationType.AVERAGE) {
val filteredEntries = entries.filter { it.value == 0 || it.value >= 1000 }
if (filteredEntries.size == 0) {
Entry(timestamp, 0)
} else {
val value = filteredEntries.sumOf { it.value }.toFloat() / filteredEntries.size
Entry(timestamp, value.roundToInt() )
}
} else {
Entry(timestamp, entries.sumOf { it.value }) Entry(timestamp, entries.sumOf { it.value })
}
}.sortedBy { (timestamp, _) -> }.sortedBy { (timestamp, _) ->
-timestamp.unixTime -timestamp.unixTime
} }

@ -22,6 +22,7 @@ import org.isoron.uhabits.core.utils.DateUtils
import java.util.UUID import java.util.UUID
data class Habit( data class Habit(
var aggregationType: AggregationType = AggregationType.SUM,
var color: PaletteColor = PaletteColor(8), var color: PaletteColor = PaletteColor(8),
var description: String = "", var description: String = "",
var frequency: Frequency = Frequency.DAILY, var frequency: Frequency = Frequency.DAILY,
@ -108,6 +109,7 @@ data class Habit(
} }
fun copyFrom(other: Habit) { fun copyFrom(other: Habit) {
this.aggregationType = other.aggregationType
this.color = other.color this.color = other.color
this.description = other.description this.description = other.description
this.frequency = other.frequency this.frequency = other.frequency
@ -128,6 +130,7 @@ data class Habit(
if (this === other) return true if (this === other) return true
if (other !is Habit) return false if (other !is Habit) return false
if (aggregationType != other.aggregationType) return false
if (color != other.color) return false if (color != other.color) return false
if (description != other.description) return false if (description != other.description) return false
if (frequency != other.frequency) return false if (frequency != other.frequency) return false
@ -148,6 +151,7 @@ data class Habit(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = color.hashCode() var result = color.hashCode()
result = 31 * result + aggregationType.value
result = 31 * result + description.hashCode() result = 31 * result + description.hashCode()
result = 31 * result + frequency.hashCode() result = 31 * result + frequency.hashCode()
result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + (id?.hashCode() ?: 0)

@ -20,6 +20,7 @@ package org.isoron.uhabits.core.models.sqlite.records
import org.isoron.uhabits.core.database.Column import org.isoron.uhabits.core.database.Column
import org.isoron.uhabits.core.database.Table import org.isoron.uhabits.core.database.Table
import org.isoron.uhabits.core.models.AggregationType
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
@ -49,6 +50,9 @@ class HabitRecord {
@field:Column(name = "freq_den") @field:Column(name = "freq_den")
var freqDen: Int? = null var freqDen: Int? = null
@field:Column(name = "aggregation_type")
var aggregationType: Int? = null
@field:Column @field:Column
var color: Int? = null var color: Int? = null
@ -93,6 +97,7 @@ class HabitRecord {
name = model.name name = model.name
description = model.description description = model.description
highlight = 0 highlight = 0
aggregationType = model.aggregationType.value
color = model.color.paletteIndex color = model.color.paletteIndex
archived = if (model.isArchived) 1 else 0 archived = if (model.isArchived) 1 else 0
type = model.type.value type = model.type.value
@ -122,6 +127,7 @@ class HabitRecord {
habit.description = description!! habit.description = description!!
habit.question = question!! habit.question = question!!
habit.frequency = Frequency(freqNum!!, freqDen!!) habit.frequency = Frequency(freqNum!!, freqDen!!)
habit.aggregationType = AggregationType.fromInt(aggregationType!!)
habit.color = PaletteColor(color!!) habit.color = PaletteColor(color!!)
habit.isArchived = archived != 0 habit.isArchived = archived != 0
habit.type = HabitType.fromInt(type!!) habit.type = HabitType.fromInt(type!!)

@ -56,7 +56,7 @@ class ListHabitsSelectionMenuBehavior @Inject constructor(
} }
fun onChangeColor() { fun onChangeColor() {
val (color) = adapter.getSelected()[0] val (_, color) = adapter.getSelected()[0]
screen.showColorPicker(color) { selectedColor: PaletteColor -> screen.showColorPicker(color) { selectedColor: PaletteColor ->
commandRunner.run(ChangeHabitColorCommand(habitList, adapter.getSelected(), selectedColor)) commandRunner.run(ChangeHabitColorCommand(habitList, adapter.getSelected(), selectedColor))
adapter.clearSelection() adapter.clearSelection()

@ -22,7 +22,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry
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.groupedSum import org.isoron.uhabits.core.models.groupedAggregate
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
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
@ -59,10 +59,11 @@ class BarCardPresenter(
} }
val today = DateUtils.getTodayWithOffset() val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum( val entries = habit.computedEntries.getByInterval(oldest, today).groupedAggregate(
truncateField = ScoreCardPresenter.getTruncateField(bucketSize), truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
firstWeekday = firstWeekday, firstWeekday = firstWeekday,
isNumerical = habit.isNumerical isNumerical = habit.isNumerical,
aggregationType = habit.aggregationType,
) )
return BarCardState( return BarCardState(
theme = theme, theme = theme,

@ -19,10 +19,12 @@
package org.isoron.uhabits.core.ui.screens.habits.show.views package org.isoron.uhabits.core.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.AggregationType
import org.isoron.uhabits.core.models.Entry
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.countSkippedDays
import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.models.groupedAggregate
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
import java.util.ArrayList import java.util.ArrayList
@ -48,19 +50,72 @@ class TargetCardPresenter {
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 valueToday = entries.groupedSum( val valueToday = entries.groupedAggregate(
truncateField = DateUtils.TruncateField.DAY, truncateField = DateUtils.TruncateField.DAY,
isNumerical = habit.isNumerical isNumerical = habit.isNumerical,
aggregationType = habit.aggregationType
).firstOrNull()?.value ?: 0 ).firstOrNull()?.value ?: 0
val skippedDayToday = entries.countSkippedDays( val valueThisWeek = entries.groupedAggregate(
truncateField = DateUtils.TruncateField.DAY
).firstOrNull()?.value ?: 0
val valueThisWeek = entries.groupedSum(
truncateField = DateUtils.TruncateField.WEEK_NUMBER, truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday, firstWeekday = firstWeekday,
isNumerical = habit.isNumerical isNumerical = habit.isNumerical,
aggregationType = habit.aggregationType
).firstOrNull()?.value ?: 0
val valueThisMonth = entries.groupedAggregate(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = habit.isNumerical,
aggregationType = habit.aggregationType
).firstOrNull()?.value ?: 0
val valueThisQuarter = entries.groupedAggregate(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = habit.isNumerical,
aggregationType = habit.aggregationType
).firstOrNull()?.value ?: 0
val valueThisYear = entries.groupedAggregate(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = habit.isNumerical,
aggregationType = habit.aggregationType
).firstOrNull()?.value ?: 0
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek / 1e3)
values.add(valueThisMonth / 1e3)
values.add(valueThisQuarter / 1e3)
values.add(valueThisYear / 1e3)
val targets = when(habit.aggregationType) {
AggregationType.SUM -> getTargetsSum(habit, firstWeekday, entries)
AggregationType.AVERAGE -> getTargetsAvg(habit)
}
val intervals = ArrayList<Int>()
if (habit.frequency.denominator <= 1) intervals.add(1)
if (habit.frequency.denominator <= 7) intervals.add(7)
intervals.add(30)
intervals.add(91)
intervals.add(365)
return TargetCardState(
color = habit.color,
values = values,
targets = targets,
intervals = intervals,
theme = theme
)
}
private fun getTargetsSum(
habit: Habit,
firstWeekday: Int,
entries: List<Entry>
): ArrayList<Double> {
val skippedDayToday = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.DAY
).firstOrNull()?.value ?: 0 ).firstOrNull()?.value ?: 0
val skippedDaysThisWeek = entries.countSkippedDays( val skippedDaysThisWeek = entries.countSkippedDays(
@ -68,29 +123,14 @@ class TargetCardPresenter {
firstWeekday = firstWeekday firstWeekday = firstWeekday
).firstOrNull()?.value ?: 0 ).firstOrNull()?.value ?: 0
val valueThisMonth = entries.groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDaysThisMonth = entries.countSkippedDays( val skippedDaysThisMonth = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.MONTH truncateField = DateUtils.TruncateField.MONTH
).firstOrNull()?.value ?: 0 ).firstOrNull()?.value ?: 0
val valueThisQuarter = entries.groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDaysThisQuarter = entries.countSkippedDays( val skippedDaysThisQuarter = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.QUARTER truncateField = DateUtils.TruncateField.QUARTER
).firstOrNull()?.value ?: 0 ).firstOrNull()?.value ?: 0
val valueThisYear = entries.groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val skippedDaysThisYear = entries.countSkippedDays( val skippedDaysThisYear = entries.countSkippedDays(
truncateField = DateUtils.TruncateField.YEAR truncateField = DateUtils.TruncateField.YEAR
).firstOrNull()?.value ?: 0 ).firstOrNull()?.value ?: 0
@ -136,13 +176,6 @@ class TargetCardPresenter {
targetThisQuarter = max(0.0, targetThisQuarter - dailyTarget * skippedDaysThisQuarter) targetThisQuarter = max(0.0, targetThisQuarter - dailyTarget * skippedDaysThisQuarter)
targetThisYear = max(0.0, targetThisYear - dailyTarget * skippedDaysThisYear) targetThisYear = max(0.0, targetThisYear - dailyTarget * skippedDaysThisYear)
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek / 1e3)
values.add(valueThisMonth / 1e3)
values.add(valueThisQuarter / 1e3)
values.add(valueThisYear / 1e3)
val targets = ArrayList<Double>() val targets = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targets.add(targetToday) if (habit.frequency.denominator <= 1) targets.add(targetToday)
if (habit.frequency.denominator <= 7) targets.add(targetThisWeek) if (habit.frequency.denominator <= 7) targets.add(targetThisWeek)
@ -150,20 +183,17 @@ class TargetCardPresenter {
targets.add(targetThisQuarter) targets.add(targetThisQuarter)
targets.add(targetThisYear) targets.add(targetThisYear)
val intervals = ArrayList<Int>() return targets
if (habit.frequency.denominator <= 1) intervals.add(1) }
if (habit.frequency.denominator <= 7) intervals.add(7)
intervals.add(30)
intervals.add(91)
intervals.add(365)
return TargetCardState( private fun getTargetsAvg(habit: Habit): ArrayList<Double> {
color = habit.color, val targets = ArrayList<Double>()
values = values, if (habit.frequency.denominator <= 1) targets.add(habit.targetValue)
targets = targets, if (habit.frequency.denominator <= 7) targets.add(habit.targetValue)
intervals = intervals, targets.add(habit.targetValue)
theme = theme targets.add(habit.targetValue)
) targets.add(habit.targetValue)
return targets
} }
} }
} }

@ -0,0 +1 @@
alter table Habits add column aggregation_type integer not null default 0;

@ -142,33 +142,93 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), values[it])) entries.add(Entry(reference.minus(offsets[it]), values[it]))
} }
val byMonth = entries.getKnown().groupedSum( val byMonth = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.MONTH, truncateField = DateUtils.TruncateField.MONTH,
isNumerical = true isNumerical = true,
aggregationType = AggregationType.SUM
) )
assertThat(byMonth.size, equalTo(17)) assertThat(byMonth.size, equalTo(17))
assertThat(byMonth[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 230))) assertThat(byMonth[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 230)))
assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 1988))) assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 1988)))
assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 1271))) assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 1271)))
val byQuarter = entries.getKnown().groupedSum( val byQuarter = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.QUARTER, truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = true isNumerical = true,
aggregationType = AggregationType.SUM
) )
assertThat(byQuarter.size, equalTo(6)) assertThat(byQuarter.size, equalTo(6))
assertThat(byQuarter[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 3263))) assertThat(byQuarter[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 3263)))
assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 3838))) assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 3838)))
assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 4975))) assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 4975)))
val byYear = entries.getKnown().groupedSum( val byYear = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.YEAR, truncateField = DateUtils.TruncateField.YEAR,
isNumerical = true isNumerical = true,
aggregationType = AggregationType.SUM
) )
assertThat(byYear.size, equalTo(2)) assertThat(byYear.size, equalTo(2))
assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 8227))) assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 8227)))
assertThat(byYear[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 16172))) assertThat(byYear[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 16172)))
} }
@Test
fun testGroupByNumericalAverage() {
val offsets = intArrayOf(
0, 5, 9, 15, 17, 21, 23, 27, 28, 35, 41, 45, 47, 53, 56, 62, 70, 73, 78,
83, 86, 94, 101, 106, 113, 114, 120, 126, 130, 133, 141, 143, 148, 151, 157, 164,
166, 171, 173, 176, 179, 183, 191, 259, 264, 268, 270, 275, 282, 284, 289, 295,
302, 306, 310, 315, 323, 325, 328, 335, 343, 349, 351, 353, 357, 359, 360, 367,
372, 376, 380, 385, 393, 400, 404, 412, 415, 418, 422, 425, 433, 437, 444, 449,
455, 460, 462, 465, 470, 471, 479, 481, 485, 489, 494, 495, 500, 501, 503, 507
)
val values = intArrayOf(
230, 306, 148, 281, 134, 285, 104, 158, 325, 236, 303, 210, 118, 124,
301, 201, 156, 376, 347, 367, 396, 134, 160, 381, 155, 354, 231, 134, 164, 354,
236, 398, 199, 221, 208, 397, 253, 276, 214, 341, 299, 221, 353, 250, 341, 168,
374, 205, 182, 217, 297, 321, 104, 237, 294, 110, 136, 229, 102, 271, 250, 294,
158, 319, 379, 126, 282, 155, 288, 159, 215, 247, 207, 226, 244, 158, 371, 219,
272, 228, 350, 153, 356, 279, 394, 202, 213, 214, 112, 248, 139, 245, 165, 256,
370, 187, 208, 231, 341, 312
)
val reference = Timestamp.from(2014, Calendar.JUNE, 1)
val entries = EntryList()
offsets.indices.forEach {
entries.add(Entry(reference.minus(offsets[it]), values[it]))
}
val byMonthAvg = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = true,
aggregationType = AggregationType.AVERAGE
)
assertThat(byMonthAvg.size, equalTo(17))
assertThat(byMonthAvg[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 230)))
assertThat(byMonthAvg[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 284)))
assertThat(byMonthAvg[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 212)))
val byQuarterAvg = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = true,
aggregationType = AggregationType.AVERAGE
)
assertThat(byQuarterAvg.size, equalTo(6))
assertThat(byQuarterAvg[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 218)))
assertThat(byQuarterAvg[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 226)))
assertThat(byQuarterAvg[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 249)))
val byYearAvg = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = true,
aggregationType = AggregationType.AVERAGE
)
assertThat(byYearAvg.size, equalTo(2))
assertThat(byYearAvg[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 242)))
assertThat(byYearAvg[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 245)))
}
@Test @Test
fun testGroupByBoolean() { fun testGroupByBoolean() {
val offsets = intArrayOf( val offsets = intArrayOf(
@ -186,27 +246,30 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL)) entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL))
} }
val byMonth = entries.getKnown().groupedSum( val byMonth = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.MONTH, truncateField = DateUtils.TruncateField.MONTH,
isNumerical = false isNumerical = false,
aggregationType = AggregationType.SUM
) )
assertThat(byMonth.size, equalTo(17)) assertThat(byMonth.size, equalTo(17))
assertThat(byMonth[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 1_000))) assertThat(byMonth[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 1_000)))
assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 7_000))) assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 7_000)))
assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 6_000))) assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 6_000)))
val byQuarter = entries.getKnown().groupedSum( val byQuarter = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.QUARTER, truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = false isNumerical = false,
aggregationType = AggregationType.SUM
) )
assertThat(byQuarter.size, equalTo(6)) assertThat(byQuarter.size, equalTo(6))
assertThat(byQuarter[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 15_000))) assertThat(byQuarter[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 15_000)))
assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 17_000))) assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 17_000)))
assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 20_000))) assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 20_000)))
val byYear = entries.getKnown().groupedSum( val byYear = entries.getKnown().groupedAggregate(
truncateField = DateUtils.TruncateField.YEAR, truncateField = DateUtils.TruncateField.YEAR,
isNumerical = false isNumerical = false,
aggregationType = AggregationType.SUM
) )
assertThat(byYear.size, equalTo(2)) assertThat(byYear.size, equalTo(2))
assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 34_000))) assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 34_000)))

Loading…
Cancel
Save