EntryList: simplify groupBy

pull/707/head
Alinson S. Xavier 5 years ago
parent 51b9517897
commit 1cdbc53dc5

@ -28,6 +28,7 @@ import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter.Compan
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.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
@ -61,10 +62,17 @@ class BarCard(context: Context, attrs: AttributeSet) : LinearLayout(context, att
binding.numericalSpinner.onItemSelectedListener = null
binding.numericalSpinner.setSelection(data.numericalSpinnerPosition)
binding.numericalSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
binding.numericalSpinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onNumericalSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
@ -72,9 +80,15 @@ class BarCard(context: Context, attrs: AttributeSet) : LinearLayout(context, att
binding.boolSpinner.onItemSelectedListener = null
binding.boolSpinner.setSelection(data.boolSpinnerPosition)
binding.boolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onBoolSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
@ -99,18 +113,11 @@ class BarCardPresenter(
}
val today = DateUtils.getToday()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = if (bucketSize == 1) {
habit.computedEntries.getByInterval(oldest, today).map {
if (it.value < 0) Entry(it.timestamp, 0) else it
}
} else {
habit.computedEntries.groupBy(
original = habit.computedEntries.getByInterval(oldest, today),
field = getTruncateField(bucketSize),
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum(
truncateField = getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
)
}
return BarCardViewModel(
entries = entries,
bucketSize = bucketSize,

@ -28,7 +28,13 @@ import kotlinx.coroutines.invoke
import org.isoron.uhabits.R
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.utils.DateUtils
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.DAY
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.MONTH
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.QUARTER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.WEEK_NUMBER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.YEAR
import org.isoron.uhabits.databinding.ShowHabitTargetBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.ArrayList
@ -61,12 +67,34 @@ class TargetCardPresenter(
) {
suspend fun present(): TargetCardViewModel = Dispatchers.IO {
val today = DateUtils.getTodayWithOffset()
val entries = habit.computedEntries
val valueToday = entries.get(today).value / 1e3
val valueThisWeek = entries.getThisWeekValue(firstWeekday, habit.isNumerical) / 1e3
val valueThisMonth = entries.getThisMonthValue(habit.isNumerical) / 1e3
val valueThisQuarter = entries.getThisQuarterValue(habit.isNumerical) / 1e3
val valueThisYear = entries.getThisYearValue(habit.isNumerical) / 1e3
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today)
val valueToday = entries.groupedSum(
truncateField = DAY,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisWeek = entries.groupedSum(
truncateField = WEEK_NUMBER,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisMonth = entries.groupedSum(
truncateField = MONTH,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisQuarter = entries.groupedSum(
truncateField = QUARTER,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisYear = entries.groupedSum(
truncateField = YEAR,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
@ -80,11 +108,11 @@ class TargetCardPresenter(
val targetThisYear = targetToday * daysInYear
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek)
values.add(valueThisMonth)
values.add(valueThisQuarter)
values.add(valueThisYear)
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>()
if (habit.frequency.denominator <= 1) targets.add(targetToday)

@ -28,6 +28,7 @@ import java.util.ArrayList
import java.util.Calendar
import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
@ThreadSafe
@ -79,45 +80,6 @@ open class EntryList {
return entriesByTimestamp.values.sortedBy { it.timestamp }.reversed()
}
/**
* Truncates the timestamps of all known entries, then aggregates their values. This function
* is used to generate bar plots where each bar shows the number of repetitions in a given week,
* month or year.
*
* For boolean habits, the value of the aggregated entry equals to the number of YES_MANUAL
* entries. For numerical habits, the value is the total sum. The field [firstWeekday] is only
* relevant when grouping by week.
*/
@Synchronized
open fun groupBy(
original: List<Entry>,
field: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean,
): List<Entry> {
val truncated = original.map {
Entry(it.timestamp.truncate(field, firstWeekday), it.value)
}
val timestamps = mutableListOf<Timestamp>()
val values = mutableListOf<Int>()
for (i in truncated.indices) {
if (i == 0 || timestamps.last() != truncated[i].timestamp) {
timestamps.add(truncated[i].timestamp)
values.add(0)
}
if (isNumerical) {
if (truncated[i].value > 0) {
values[values.lastIndex] += truncated[i].value
}
} else {
if (truncated[i].value == YES_MANUAL) {
values[values.lastIndex] += 1000
}
}
}
return timestamps.indices.map { Entry(timestamps[it], values[it]) }
}
/**
* Replaces all entries in this list by entries computed automatically from another list.
*
@ -189,55 +151,6 @@ open class EntryList {
return map
}
@Deprecated("")
@Synchronized
open fun getThisWeekValue(firstWeekday: Int, isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisMonthValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisQuarterValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisYearValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
private fun getThisIntervalValue(
truncateField: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean,
): Int {
val groups: List<Entry> = groupBy(getKnown(), truncateField, firstWeekday, isNumerical)
return if (groups.isEmpty()) 0 else groups[0].value
}
data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) {
val length: Int
get() = begin.daysUntil(end) + 1
@ -344,3 +257,41 @@ open class EntryList {
}
}
}
/**
* Given a list of entries, truncates the timestamp of each entry (according to the field given),
* groups the entries according to this truncated timestamp, then creates a new entry (t,v) for
* each group, where t is the truncated timestamp and v is the sum of the values of all entries in
* the group.
*
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
* YES_MANUAL value is converted to 1000 and all other values are converted to 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<Entry>.groupedSum(
truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean,
): List<Entry> {
return this.map { (timestamp, value) ->
if (isNumerical) {
Entry(timestamp, max(0, value))
} else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
}
}.groupBy { entry ->
entry.timestamp.truncate(
truncateField,
firstWeekday,
)
}.entries.map { (timestamp, entries) ->
Entry(timestamp, entries.sumOf { it.value })
}.sortedBy { (timestamp, _) ->
- timestamp.unixTime
}
}

@ -21,9 +21,12 @@ package org.isoron.uhabits.core.models;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*;
import org.jetbrains.annotations.*;
import java.util.*;
import kotlin.*;
import static java.util.Calendar.*;
public final class Timestamp implements Comparable<Timestamp>

@ -26,7 +26,6 @@ import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
import org.isoron.uhabits.core.utils.DateUtils
class SQLiteEntryList(database: Database) : EntryList() {
val repository = Repository(EntryRecord::class.java, database)
@ -79,16 +78,6 @@ class SQLiteEntryList(database: Database) : EntryList() {
return super.getKnown()
}
override fun groupBy(
original: List<Entry>,
field: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean
): List<Entry> {
loadRecords()
return super.groupBy(original, field, firstWeekday, isNumerical)
}
override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) {
throw UnsupportedOperationException()
}

@ -142,10 +142,8 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), values[it]))
}
val byMonth = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
val byMonth = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = true,
)
assertThat(byMonth.size, equalTo(17))
@ -153,10 +151,8 @@ class EntryListTest {
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.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
val byQuarter = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = true,
)
assertThat(byQuarter.size, equalTo(6))
@ -164,10 +160,8 @@ class EntryListTest {
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.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
val byYear = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = true,
)
assertThat(byYear.size, equalTo(2))
@ -192,10 +186,8 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL))
}
val byMonth = entries.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
val byMonth = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = false,
)
assertThat(byMonth.size, equalTo(17))
@ -203,10 +195,8 @@ class EntryListTest {
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.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
val byQuarter = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = false,
)
assertThat(byQuarter.size, equalTo(6))
@ -214,10 +204,8 @@ class EntryListTest {
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.groupBy(
original = entries.getKnown(),
field = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
val byYear = entries.getKnown().groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = false,
)
assertThat(byYear.size, equalTo(2))

Loading…
Cancel
Save