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 5479ab78c..a3d11c278 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 @@ -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.CreateHabitCommand 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.Habit import org.isoron.uhabits.core.models.HabitType @@ -85,6 +86,7 @@ class EditHabitActivity : AppCompatActivity() { var reminderMin = -1 var reminderDays: WeekdayList = WeekdayList.EVERY_DAY var targetType = NumericalHabitType.AT_LEAST + var aggregationType = AggregationType.SUM override fun onCreate(state: Bundle?) { super.onCreate(state) @@ -107,6 +109,7 @@ class EditHabitActivity : AppCompatActivity() { freqNum = habit.frequency.numerator freqDen = habit.frequency.denominator targetType = habit.targetType + aggregationType = habit.aggregationType habit.reminder?.let { reminderHour = it.hour reminderMin = it.minute @@ -191,6 +194,24 @@ class EditHabitActivity : AppCompatActivity() { dialog.dismissCurrentAndShow() } + populateAggregationType() + binding.aggregationTypePicker.setOnClickListener { + val builder = AlertDialog.Builder(this) + val arrayAdapter = ArrayAdapter(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 { val builder = AlertDialog.Builder(this) val arrayAdapter = ArrayAdapter(this, android.R.layout.select_dialog_item) @@ -282,6 +303,7 @@ class EditHabitActivity : AppCompatActivity() { if (habitType == HabitType.NUMERICAL) { habit.targetValue = binding.targetInput.text.toString().toDouble() habit.targetType = targetType + habit.aggregationType = aggregationType habit.unit = binding.unitInput.text.trim().toString() } 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() { androidColor = themeSwitcher.currentTheme.color(color).toInt() binding.colorButton.backgroundTintList = ColorStateList.valueOf(androidColor) 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 32866f654..f4f9fab2d 100644 --- a/uhabits-android/src/main/res/layout/activity_edit_habit.xml +++ b/uhabits-android/src/main/res/layout/activity_edit_habit.xml @@ -211,20 +211,51 @@ - - - - - - + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/uhabits-android/src/main/res/values-de-rDE/strings.xml b/uhabits-android/src/main/res/values-de-rDE/strings.xml index ca119039f..fd854897e 100644 --- a/uhabits-android/src/main/res/values-de-rDE/strings.xml +++ b/uhabits-android/src/main/res/values-de-rDE/strings.xml @@ -218,4 +218,7 @@ Für diese Aktion wurde keine App gefunden. Verlängere den Tag um ein paar Stunden nach Mitternacht 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. + Aggregation + Summe + Mittelwert diff --git a/uhabits-android/src/main/res/values/strings.xml b/uhabits-android/src/main/res/values/strings.xml index 9fec1cbca..7e1af2ba6 100644 --- a/uhabits-android/src/main/res/values/strings.xml +++ b/uhabits-android/src/main/res/values/strings.xml @@ -233,4 +233,7 @@ No app was found to support this action Extend day a few hours past midnight Wait until 3:00 AM to show a new day. Useful if you typically go to sleep after midnight. Requires app restart. + Aggregation + Sum + Average diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt index be6c9634b..328b8cd40 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/Constants.kt @@ -20,4 +20,4 @@ package org.isoron.uhabits.core const val DATABASE_FILENAME = "uhabits.db" -const val DATABASE_VERSION = 25 +const val DATABASE_VERSION = 26 diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/AggregationType.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/AggregationType.kt new file mode 100644 index 000000000..0ac04e6de --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/AggregationType.kt @@ -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() + } + } + } +} 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 5c5499f49..4ed0ace6a 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 @@ -30,6 +30,7 @@ import javax.annotation.concurrent.ThreadSafe import kotlin.collections.set import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt @ThreadSafe 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). * + * 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 * 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. * * The argument [firstWeekday] is only relevant when truncating by week. */ -fun List.groupedSum( +fun List.groupedAggregate( truncateField: DateUtils.TruncateField, firstWeekday: Int = Calendar.SATURDAY, - isNumerical: Boolean + isNumerical: Boolean, + aggregationType: AggregationType ): List { return this.map { (timestamp, value) -> if (isNumerical) { - if (value == SKIP) { + if (aggregationType == AggregationType.AVERAGE) { + Entry(timestamp, value) + } else if (value == SKIP) { Entry(timestamp, 0) } else { Entry(timestamp, max(0, value)) @@ -312,7 +321,17 @@ fun List.groupedSum( firstWeekday ) }.entries.map { (timestamp, entries) -> - Entry(timestamp, entries.sumOf { it.value }) + 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 }) + } }.sortedBy { (timestamp, _) -> -timestamp.unixTime } 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 4b31d2a2e..a19e2d4b3 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 @@ -22,6 +22,7 @@ import org.isoron.uhabits.core.utils.DateUtils import java.util.UUID data class Habit( + var aggregationType: AggregationType = AggregationType.SUM, var color: PaletteColor = PaletteColor(8), var description: String = "", var frequency: Frequency = Frequency.DAILY, @@ -108,6 +109,7 @@ data class Habit( } fun copyFrom(other: Habit) { + this.aggregationType = other.aggregationType this.color = other.color this.description = other.description this.frequency = other.frequency @@ -128,6 +130,7 @@ data class Habit( if (this === other) return true if (other !is Habit) return false + if (aggregationType != other.aggregationType) return false if (color != other.color) return false if (description != other.description) return false if (frequency != other.frequency) return false @@ -148,6 +151,7 @@ data class Habit( override fun hashCode(): Int { var result = color.hashCode() + result = 31 * result + aggregationType.value result = 31 * result + description.hashCode() result = 31 * result + frequency.hashCode() result = 31 * result + (id?.hashCode() ?: 0) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index dc0386799..33caa8add 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -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.Table +import org.isoron.uhabits.core.models.AggregationType import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitType @@ -49,6 +50,9 @@ class HabitRecord { @field:Column(name = "freq_den") var freqDen: Int? = null + @field:Column(name = "aggregation_type") + var aggregationType: Int? = null + @field:Column var color: Int? = null @@ -93,6 +97,7 @@ class HabitRecord { name = model.name description = model.description highlight = 0 + aggregationType = model.aggregationType.value color = model.color.paletteIndex archived = if (model.isArchived) 1 else 0 type = model.type.value @@ -122,6 +127,7 @@ class HabitRecord { habit.description = description!! habit.question = question!! habit.frequency = Frequency(freqNum!!, freqDen!!) + habit.aggregationType = AggregationType.fromInt(aggregationType!!) habit.color = PaletteColor(color!!) habit.isArchived = archived != 0 habit.type = HabitType.fromInt(type!!) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt index 00d90a1a7..b4e73d216 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsSelectionMenuBehavior.kt @@ -56,7 +56,7 @@ class ListHabitsSelectionMenuBehavior @Inject constructor( } fun onChangeColor() { - val (color) = adapter.getSelected()[0] + val (_, color) = adapter.getSelected()[0] screen.showColorPicker(color) { selectedColor: PaletteColor -> commandRunner.run(ChangeHabitColorCommand(habitList, adapter.getSelected(), selectedColor)) adapter.clearSelection() 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 303b33cee..8db6f6c5e 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 @@ -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.Habit 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.ui.views.Theme import org.isoron.uhabits.core.utils.DateUtils @@ -59,10 +59,11 @@ class BarCardPresenter( } val today = DateUtils.getTodayWithOffset() 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), firstWeekday = firstWeekday, - isNumerical = habit.isNumerical + isNumerical = habit.isNumerical, + aggregationType = habit.aggregationType, ) return BarCardState( theme = theme, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt index 49c95011d..96cb320ab 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt @@ -19,10 +19,12 @@ 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.PaletteColor 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.utils.DateUtils import java.util.ArrayList @@ -48,19 +50,72 @@ class TargetCardPresenter { val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today val entries = habit.computedEntries.getByInterval(oldest, today) - val valueToday = entries.groupedSum( + val valueToday = entries.groupedAggregate( truncateField = DateUtils.TruncateField.DAY, - isNumerical = habit.isNumerical + isNumerical = habit.isNumerical, + aggregationType = habit.aggregationType ).firstOrNull()?.value ?: 0 - val skippedDayToday = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.DAY - ).firstOrNull()?.value ?: 0 - - val valueThisWeek = entries.groupedSum( + val valueThisWeek = entries.groupedAggregate( truncateField = DateUtils.TruncateField.WEEK_NUMBER, 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() + 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() + 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 + ): ArrayList { + val skippedDayToday = entries.countSkippedDays( + truncateField = DateUtils.TruncateField.DAY ).firstOrNull()?.value ?: 0 val skippedDaysThisWeek = entries.countSkippedDays( @@ -68,29 +123,14 @@ class TargetCardPresenter { firstWeekday = firstWeekday ).firstOrNull()?.value ?: 0 - val valueThisMonth = entries.groupedSum( - truncateField = DateUtils.TruncateField.MONTH, - isNumerical = habit.isNumerical - ).firstOrNull()?.value ?: 0 - val skippedDaysThisMonth = entries.countSkippedDays( truncateField = DateUtils.TruncateField.MONTH ).firstOrNull()?.value ?: 0 - val valueThisQuarter = entries.groupedSum( - truncateField = DateUtils.TruncateField.QUARTER, - isNumerical = habit.isNumerical - ).firstOrNull()?.value ?: 0 - val skippedDaysThisQuarter = entries.countSkippedDays( truncateField = DateUtils.TruncateField.QUARTER ).firstOrNull()?.value ?: 0 - val valueThisYear = entries.groupedSum( - truncateField = DateUtils.TruncateField.YEAR, - isNumerical = habit.isNumerical - ).firstOrNull()?.value ?: 0 - val skippedDaysThisYear = entries.countSkippedDays( truncateField = DateUtils.TruncateField.YEAR ).firstOrNull()?.value ?: 0 @@ -136,13 +176,6 @@ class TargetCardPresenter { targetThisQuarter = max(0.0, targetThisQuarter - dailyTarget * skippedDaysThisQuarter) targetThisYear = max(0.0, targetThisYear - dailyTarget * skippedDaysThisYear) - val values = ArrayList() - 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() if (habit.frequency.denominator <= 1) targets.add(targetToday) if (habit.frequency.denominator <= 7) targets.add(targetThisWeek) @@ -150,20 +183,17 @@ class TargetCardPresenter { targets.add(targetThisQuarter) targets.add(targetThisYear) - val intervals = ArrayList() - 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 targets + } - return TargetCardState( - color = habit.color, - values = values, - targets = targets, - intervals = intervals, - theme = theme - ) + private fun getTargetsAvg(habit: Habit): ArrayList { + val targets = ArrayList() + if (habit.frequency.denominator <= 1) targets.add(habit.targetValue) + if (habit.frequency.denominator <= 7) targets.add(habit.targetValue) + targets.add(habit.targetValue) + targets.add(habit.targetValue) + targets.add(habit.targetValue) + return targets } } } diff --git a/uhabits-core/src/jvmMain/resources/migrations/26.sql b/uhabits-core/src/jvmMain/resources/migrations/26.sql new file mode 100644 index 000000000..6a27b40e9 --- /dev/null +++ b/uhabits-core/src/jvmMain/resources/migrations/26.sql @@ -0,0 +1 @@ +alter table Habits add column aggregation_type integer not null default 0; diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt index 7b163f945..416412c2c 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt @@ -142,33 +142,93 @@ class EntryListTest { entries.add(Entry(reference.minus(offsets[it]), values[it])) } - val byMonth = entries.getKnown().groupedSum( + val byMonth = entries.getKnown().groupedAggregate( truncateField = DateUtils.TruncateField.MONTH, - isNumerical = true + isNumerical = true, + aggregationType = AggregationType.SUM ) assertThat(byMonth.size, equalTo(17)) 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[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 1271))) - val byQuarter = entries.getKnown().groupedSum( + val byQuarter = entries.getKnown().groupedAggregate( truncateField = DateUtils.TruncateField.QUARTER, - isNumerical = true + isNumerical = true, + aggregationType = AggregationType.SUM ) assertThat(byQuarter.size, equalTo(6)) 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[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 4975))) - val byYear = entries.getKnown().groupedSum( + val byYear = entries.getKnown().groupedAggregate( truncateField = DateUtils.TruncateField.YEAR, - isNumerical = true + isNumerical = true, + aggregationType = AggregationType.SUM ) assertThat(byYear.size, equalTo(2)) assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 8227))) 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 fun testGroupByBoolean() { val offsets = intArrayOf( @@ -186,27 +246,30 @@ class EntryListTest { entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL)) } - val byMonth = entries.getKnown().groupedSum( + val byMonth = entries.getKnown().groupedAggregate( truncateField = DateUtils.TruncateField.MONTH, - isNumerical = false + isNumerical = false, + aggregationType = AggregationType.SUM ) assertThat(byMonth.size, equalTo(17)) 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[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, - isNumerical = false + isNumerical = false, + aggregationType = AggregationType.SUM ) assertThat(byQuarter.size, equalTo(6)) 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[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, - isNumerical = false + isNumerical = false, + aggregationType = AggregationType.SUM ) assertThat(byYear.size, equalTo(2)) assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 34_000)))