From 78f31a65d4517b02e130e3231af8a7649078cbd9 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sat, 26 Dec 2020 11:54:05 -0600 Subject: [PATCH] Remove EntryList --- .../org/isoron/uhabits/HabitFixtures.java | 4 + .../activities/common/views/BarChartTest.java | 2 +- .../habits/list/views/HabitCardViewTest.kt | 4 +- .../uhabits/performance/PerformanceTest.java | 11 - .../uhabits/widgets/CheckmarkWidgetTest.java | 11 +- .../views/CheckmarkWidgetViewTest.java | 6 +- .../org/isoron/uhabits/HabitsApplication.kt | 3 + .../activities/habits/show/views/BarCard.kt | 8 +- .../habits/show/views/HistoryCard.kt | 2 +- .../habits/show/views/TargetCard.kt | 11 +- .../isoron/uhabits/widgets/CheckmarkWidget.kt | 7 +- .../isoron/uhabits/widgets/HistoryWidget.kt | 2 +- .../NumericalCheckmarkWidgetActivity.kt | 5 +- .../uhabits/core/io/HabitsCSVExporter.java | 11 +- .../org/isoron/uhabits/core/models/Entries.kt | 81 ++- .../isoron/uhabits/core/models/EntryList.java | 555 ------------------ .../org/isoron/uhabits/core/models/Habit.kt | 22 +- .../isoron/uhabits/core/models/HabitList.java | 1 - .../uhabits/core/models/ModelFactory.kt | 7 +- .../core/models/memory/MemoryHabitList.java | 6 +- .../core/models/memory/MemoryModelFactory.kt | 1 - .../core/models/sqlite/SQLModelFactory.kt | 1 - .../uhabits/core/test/HabitFixtures.java | 4 + .../uhabits/core/ui/NotificationTray.java | 4 +- .../habits/list/ListHabitsBehavior.java | 4 +- .../screens/habits/show/ShowHabitBehavior.kt | 7 +- .../isoron/uhabits/core/models/EntriesTest.kt | 48 +- .../isoron/uhabits/core/models/HabitTest.java | 1 + .../uhabits/core/models/ScoreListTest.java | 6 + .../uhabits/core/models/StreakListTest.java | 1 + .../habits/list/HabitCardListCacheTest.java | 3 +- .../habits/list/ListHabitsBehaviorTest.java | 3 +- .../core/ui/widgets/WidgetBehaviorTest.java | 2 + 33 files changed, 220 insertions(+), 624 deletions(-) delete mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/EntryList.java diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java index 63647faa5..b6eb6df7e 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java @@ -77,6 +77,7 @@ public class HabitFixtures for (int mark : marks) habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL)); + habit.recompute(); return habit; } @@ -111,6 +112,7 @@ public class HabitFixtures for (int mark : marks) habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL)); + habit.recompute(); return habit; } @@ -132,6 +134,7 @@ public class HabitFixtures timestamp = timestamp.minus(1); } + habit.recompute(); return habit; } @@ -150,6 +153,7 @@ public class HabitFixtures timestamp = timestamp.minus(1); } + habit.recompute(); return habit; } diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/BarChartTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/BarChartTest.java index bf0bb4a84..eaafc01cf 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/BarChartTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/common/views/BarChartTest.java @@ -46,7 +46,7 @@ public class BarChartTest extends BaseViewTest Habit habit = fixtures.createLongNumericalHabit(); view = new BarChart(targetContext); Timestamp today = DateUtils.getToday(); - EntryList entries = habit.getComputedEntries(); + Entries entries = habit.getComputedEntries(); view.setEntries(entries.getByInterval(today.minus(20), today)); view.setColor(PaletteUtilsKt.toThemedAndroidColor(habit.getColor(), targetContext)); view.setTarget(200.0); diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt index 8ee718ee1..fd9d1058b 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt @@ -43,7 +43,7 @@ class HabitCardViewTest : BaseViewTest() { habit2 = fixtures.createLongNumericalHabit() view = component.getHabitCardViewFactory().create().apply { habit = habit1 - values = habit1.computedEntries.allValues + values = habit1.computedEntries.getAllValues() score = habit1.scores.todayValue isSelected = false buttonCount = 5 @@ -70,7 +70,7 @@ class HabitCardViewTest : BaseViewTest() { fun testRender_numerical() { view.apply { habit = habit2 - values = habit2.computedEntries.allValues + values = habit2.computedEntries.getAllValues() } assertRenders(view, "$PATH/render_numerical.png") } diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java index 92ca230e6..20ba64f2b 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.java @@ -45,17 +45,6 @@ public class PerformanceTest extends BaseAndroidTest habit = fixtures.createLongHabit(); } - @Ignore - @Test(timeout = 5000) - public void testRepeatedGetTodayValue() - { - for (int i = 0; i < 100000; i++) - { - habit.getScores().getTodayValue(); - habit.getComputedEntries().getTodayValue(); - } - } - @Ignore @Test(timeout = 5000) public void benchmarkCreateHabitCommand() diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java index 7f887aec3..6c7a29c1d 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java @@ -26,6 +26,7 @@ import androidx.test.filters.*; import org.isoron.uhabits.*; import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.utils.*; import org.junit.*; import org.junit.runner.*; @@ -41,10 +42,12 @@ public class CheckmarkWidgetTest extends BaseViewTest private Habit habit; - private EntryList entries; + private Entries entries; private FrameLayout view; + private Timestamp today = DateUtils.getTodayWithOffset(); + @Override public void setUp() { @@ -58,7 +61,7 @@ public class CheckmarkWidgetTest extends BaseViewTest CheckmarkWidget widget = new CheckmarkWidget(targetContext, 0, habit); view = convertToView(widget, 150, 200); - assertThat(entries.getTodayValue(), equalTo(YES_MANUAL)); + assertThat(entries.get(today).getValue(), equalTo(YES_MANUAL)); } @Test @@ -71,11 +74,11 @@ public class CheckmarkWidgetTest extends BaseViewTest // possible to capture intents sent to BroadcastReceivers. button.performClick(); sleep(1000); - assertThat(entries.getTodayValue(), equalTo(SKIP)); + assertThat(entries.get(today).getValue(), equalTo(SKIP)); button.performClick(); sleep(1000); - assertThat(entries.getTodayValue(), equalTo(NO)); + assertThat(entries.get(today).getValue(), equalTo(NO)); } @Test diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java index 698dba3e8..b23010bb2 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java @@ -24,6 +24,7 @@ import androidx.test.filters.*; import org.isoron.uhabits.*; import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.utils.*; import org.junit.*; import org.junit.runner.*; @@ -50,9 +51,10 @@ public class CheckmarkWidgetViewTest extends BaseViewTest double score = habit.getScores().getTodayValue(); float percentage = (float) score; + Timestamp today = DateUtils.getTodayWithOffset(); view.setActiveColor(PaletteUtils.getAndroidTestColor(0)); - view.setEntryState(habit.getComputedEntries().getTodayValue()); - view.setEntryValue(habit.getComputedEntries().getTodayValue()); + view.setEntryState(habit.getComputedEntries().get(today).getValue()); + view.setEntryValue(habit.getComputedEntries().get(today).getValue()); view.setPercentage(percentage); view.setName(habit.getName()); view.refresh(); diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt index 36783ed24..f21e3d9e0 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt @@ -66,6 +66,9 @@ class HabitsApplication : Application() { DateUtils.setStartDayOffset(3, 0) + val habitList = component.habitList + for (h in habitList) h.recompute() + widgetUpdater = component.widgetUpdater widgetUpdater.startListening() widgetUpdater.scheduleStartDayWidgetUpdate() diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.kt index a1d819ab3..361b0294f 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCard.kt @@ -96,9 +96,13 @@ class BarCardPresenter( boolBucketSizes[boolSpinnerPosition] } val entries = if (bucketSize == 1) { - habit.computedEntries.all + habit.computedEntries.getKnown() } else { - habit.computedEntries.groupBy(getTruncateField(bucketSize), firstWeekday) + habit.computedEntries.groupBy( + field = getTruncateField(bucketSize), + firstWeekday = firstWeekday, + isNumerical = habit.isNumerical, + ).map { Entry(it.timestamp, it.value * 1000) } } return BarCardViewModel( entries = entries, diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.kt index a95b210f6..0efa57b1f 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.kt @@ -66,7 +66,7 @@ class HistoryCardPresenter( val isSkipEnabled: Boolean, ) { fun present() = HistoryCardViewModel( - entries = habit.computedEntries.allValues, + entries = habit.computedEntries.getAllValues(), color = habit.color, firstWeekday = firstWeekday, isNumerical = habit.isNumerical, diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.kt index 363fd6aca..e69483b82 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/TargetCard.kt @@ -57,12 +57,13 @@ class TargetCardPresenter( val resources: Resources, ) { suspend fun present(): TargetCardViewModel = Dispatchers.IO { + val today = DateUtils.getTodayWithOffset() val entries = habit.computedEntries - val valueToday = entries.todayValue / 1e3 - val valueThisWeek = entries.getThisWeekValue(firstWeekday) / 1e3 - val valueThisMonth = entries.thisMonthValue / 1e3 - val valueThisQuarter = entries.thisQuarterValue / 1e3 - val valueThisYear = entries.thisYearValue / 1e3 + 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 cal = DateUtils.getStartOfTodayCalendarWithOffset() val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH) diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt index 88662a013..9203e442c 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt @@ -23,6 +23,7 @@ import android.app.* import android.content.* import android.view.* import org.isoron.uhabits.core.models.* +import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.utils.* import org.isoron.uhabits.widgets.views.* @@ -42,16 +43,16 @@ open class CheckmarkWidget( override fun refreshData(v: View) { (v as CheckmarkWidgetView).apply { + val today = DateUtils.getTodayWithOffset() setBackgroundAlpha(preferedBackgroundAlpha) - setActiveColor(habit.color.toThemedAndroidColor(context)) setName(habit.name) - setEntryValue(habit.computedEntries.todayValue) + setEntryValue(habit.computedEntries.get(today).value) if (habit.isNumerical) { setNumerical(true) setEntryState(getNumericalEntryState()) } else { - setEntryState(habit.computedEntries.todayValue) + setEntryState(habit.computedEntries.get(today).value) } setPercentage(habit.scores.todayValue.toFloat()) refresh() diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt index f910da355..fdf76d848 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt @@ -46,7 +46,7 @@ class HistoryWidget( setFirstWeekday(firstWeekday) setSkipEnabled(prefs.isSkipEnabled) setColor(habit.color.toThemedAndroidColor(context)) - setEntries(habit.computedEntries.allValues) + setEntries(habit.computedEntries.getAllValues()) setNumerical(habit.isNumerical) setTarget(habit.targetValue / habit.frequency.denominator) } diff --git a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/NumericalCheckmarkWidgetActivity.kt b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/NumericalCheckmarkWidgetActivity.kt index 4117252ac..fdb67790b 100644 --- a/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/NumericalCheckmarkWidgetActivity.kt +++ b/android/uhabits-android/src/main/java/org/isoron/uhabits/widgets/activities/NumericalCheckmarkWidgetActivity.kt @@ -29,6 +29,7 @@ import org.isoron.uhabits.activities.* import org.isoron.uhabits.activities.common.dialogs.* import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.ui.widgets.* +import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.intents.* import org.isoron.uhabits.utils.* import org.isoron.uhabits.widgets.* @@ -71,7 +72,9 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi val app = this.applicationContext as HabitsApplication AndroidThemeSwitcher(this, app.component.preferences).apply() val numberPickerFactory = NumberPickerFactory(context) - numberPickerFactory.create(data.habit.computedEntries.today!!.value.toDouble() / 1000, + val today = DateUtils.getTodayWithOffset() + val entry = data.habit.computedEntries.get(today) + numberPickerFactory.create(entry.value / 1000.0, data.habit.unit, this).show() } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java index 316359896..8b4c4972c 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java @@ -142,13 +142,20 @@ public class HabitsCSVExporter out.close(); } - private void writeEntries(String habitDirName, EntryList entries) + private void writeEntries(String habitDirName, Entries entries) throws IOException { String filename = habitDirName + "Checkmarks.csv"; FileWriter out = new FileWriter(exportDirName + filename); generateFilenames.add(filename); - entries.writeCSV(out); + + SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); + for (Entry e : entries.getKnown()) + { + String date = dateFormat.format(e.getTimestamp().toJavaDate()); + out.write(String.format(Locale.US, "%s,%d\n", date, e.getValue())); + } + out.close(); } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Entries.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Entries.kt index a7bb2d6df..9cd15ce25 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Entries.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Entries.kt @@ -99,7 +99,7 @@ open class Entries { if (isNumerical) { values[values.lastIndex] += truncated[i].value } else { - if (values[values.lastIndex] == YES_MANUAL) { + if (truncated[i].value == YES_MANUAL) { values[values.lastIndex] += 1 } } @@ -124,7 +124,6 @@ open class Entries { original.forEach { add(it) } } else { val intervals = buildIntervals(frequency, original) - if (intervals.isEmpty()) return snapIntervalsTogether(intervals) val computed = buildEntriesFromInterval(original, intervals) computed.filter { it.value != UNKNOWN }.forEach { add(it) } @@ -181,6 +180,7 @@ open class Entries { * corresponds to one day older than the previous entry. The boundaries of the time interval * are included. */ + @Deprecated("") fun getValues(from: Timestamp, to: Timestamp): IntArray { if (from.isNewerThan(to)) throw IllegalArgumentException() val nDays = from.daysUntil(to) + 1 @@ -194,6 +194,60 @@ open class Entries { return result } + @Deprecated("") + fun getAllValues(): IntArray { + val entries = getKnown() + if (entries.isEmpty()) return IntArray(0) + val (fromTimestamp, _) = entries.last() + val toTimestamp = DateUtils.getTodayWithOffset() + return getValues(fromTimestamp, toTimestamp) + } + + @Deprecated("") + open fun getThisWeekValue(firstWeekday: Int, isNumerical: Boolean): Int { + return getThisIntervalValue( + truncateField = DateUtils.TruncateField.WEEK_NUMBER, + firstWeekday = firstWeekday, + isNumerical = isNumerical + ) + } + + @Deprecated("") + open fun getThisMonthValue(isNumerical: Boolean): Int { + return getThisIntervalValue( + truncateField = DateUtils.TruncateField.MONTH, + firstWeekday = Calendar.SATURDAY, + isNumerical = isNumerical + ) + } + + @Deprecated("") + open fun getThisQuarterValue(isNumerical: Boolean): Int { + return getThisIntervalValue( + truncateField = DateUtils.TruncateField.QUARTER, + firstWeekday = Calendar.SATURDAY, + isNumerical = isNumerical + ) + } + + @Deprecated("") + 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 = groupBy(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; @@ -212,13 +266,24 @@ open class Entries { original: List, intervals: List, ): List { - val toTimestamp = intervals.first().end - val fromTimstamp = intervals.last().begin val result = arrayListOf() + if (original.isEmpty()) return result + + var from = original[0].timestamp + var to = original[0].timestamp + + for (e in original) { + if (e.timestamp < from) from = e.timestamp + if (e.timestamp > to) to = e.timestamp + } + for (interval in intervals) { + if (interval.begin < from) from = interval.begin + if (interval.end > to) to = interval.end + } // Create unknown entries - var current = toTimestamp - while (current >= fromTimstamp) { + var current = to + while (current >= from) { result.add(Entry(current, UNKNOWN)) current = current.minus(1) } @@ -227,7 +292,7 @@ open class Entries { intervals.forEach { interval -> current = interval.end while (current >= interval.begin) { - val offset = current.daysUntil(toTimestamp) + val offset = current.daysUntil(to) result[offset] = Entry(current, YES_AUTO) current = current.minus(1) } @@ -235,7 +300,7 @@ open class Entries { // Copy original entries original.forEach { entry -> - val offset = entry.timestamp.daysUntil(toTimestamp) + val offset = entry.timestamp.daysUntil(to) result[offset] = entry } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/EntryList.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/EntryList.java deleted file mode 100644 index 38088f8a8..000000000 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/EntryList.java +++ /dev/null @@ -1,555 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.core.models; - -import androidx.annotation.*; - -import org.apache.commons.lang3.builder.*; -import org.isoron.uhabits.core.utils.*; - -import java.io.*; -import java.text.*; -import java.util.*; - -import javax.annotation.concurrent.*; - -import static org.isoron.uhabits.core.models.Entry.*; -import static org.isoron.uhabits.core.models.Habit.*; -import static org.isoron.uhabits.core.utils.StringUtils.*; - -/** - * The collection of {@link Entry}s belonging to a habit. - */ -@ThreadSafe -public class EntryList -{ - protected Habit habit = null; - - protected ArrayList list; - - public EntryList() - { - this.list = new ArrayList<>(); - } - - public void setHabit(Habit habit) - { - this.habit = habit; - } - - @NonNull - static List buildEntriesFromInterval(Entry[] original, - ArrayList intervals) - { - if (original.length == 0) throw new IllegalArgumentException(); - - Timestamp today = DateUtils.getTodayWithOffset(); - Timestamp begin = original[0].getTimestamp(); - if (intervals.size() > 0) begin = Timestamp.oldest(begin, intervals.get(0).begin); - - int nDays = begin.daysUntil(today) + 1; - List entries = new ArrayList<>(nDays); - for (int i = 0; i < nDays; i++) - entries.add(new Entry(today.minus(i), UNKNOWN)); - - for (Interval interval : intervals) - { - for (int i = 0; i < interval.length(); i++) - { - Timestamp date = interval.begin.plus(i); - int offset = date.daysUntil(today); - if (offset < 0) continue; - entries.set(offset, new Entry(date, YES_AUTO)); - } - } - - for (Entry e : original) - { - Timestamp date = e.getTimestamp(); - int offset = date.daysUntil(today); - int value = e.getValue(); - int prevValue = entries.get(offset).getValue(); - if (prevValue < value) - entries.set(offset, new Entry(date, value)); - } - - return entries; - } - - /** - * For non-daily habits, some manual entries generate many - * automatic entries. For example, for weekly habits, each repetition generates - * seven checkmarks. For twice-a-week habits, two repetitions that are close - * enough together also generate seven checkmarks. - *

- * This group of generated entries is represented by an interval. This function - * computes the list of intervals for a given list of original entries. It tries - * to build the intervals as far away in the future as possible. - */ - @NonNull - static ArrayList buildIntervals(@NonNull Frequency freq, - @NonNull Entry[] entries) - { - ArrayList filteredEntries = new ArrayList<>(); - for (Entry e : entries) - if (e.getValue() == YES_MANUAL) - filteredEntries.add(e); - - int num = freq.getNumerator(); - int den = freq.getDenominator(); - - ArrayList intervals = new ArrayList<>(); - for (int i = 0; i < filteredEntries.size() - num + 1; i++) - { - Entry first = filteredEntries.get(i); - Entry last = filteredEntries.get(i + num - 1); - - long distance = first.getTimestamp().daysUntil(last.getTimestamp()); - if (distance >= den) continue; - - Timestamp begin = first.getTimestamp(); - Timestamp center = last.getTimestamp(); - Timestamp end = begin.plus(den - 1); - intervals.add(new Interval(begin, center, end)); - } - - return intervals; - } - - /** - * Starting from the second newest interval, this function tries to slide the - * intervals backwards into the past, so that gaps are eliminated and - * streaks are maximized. - */ - static void snapIntervalsTogether(@NonNull ArrayList intervals) - { - int n = intervals.size(); - for (int i = n - 2; i >= 0; i--) - { - Interval curr = intervals.get(i); - Interval next = intervals.get(i + 1); - - int gapNextToCurrent = next.begin.daysUntil(curr.end); - int gapCenterToEnd = curr.center.daysUntil(curr.end); - - if (gapNextToCurrent >= 0) - { - int shift = Math.min(gapCenterToEnd, gapNextToCurrent + 1); - intervals.set(i, new Interval(curr.begin.minus(shift), - curr.center, - curr.end.minus(shift))); - } - } - } - - public void add(List entries) - { - list.addAll(entries); - Collections.sort(list, - (c1, c2) -> c2.getTimestamp().compareTo(c1.getTimestamp())); - - } - - /** - * Returns the values for all the checkmarks, since the oldest repetition of - * the habit until today. - *

- * If there are no repetitions at all, returns an empty array. The values - * are returned in an array containing one integer value for each day since - * the first repetition of the habit until today. The first entry - * corresponds to today, the second entry corresponds to yesterday, and so - * on. - * - * @return values for the checkmarks in the interval - */ - @NonNull - public synchronized final int[] getAllValues() - { - List entries = habit.getOriginalEntries().getKnown(); - if(entries.isEmpty()) return new int[0]; - Entry oldestOriginal = entries.get(entries.size() - 1); - - Timestamp fromTimestamp = oldestOriginal.getTimestamp(); - Timestamp toTimestamp = DateUtils.getTodayWithOffset(); - - return getValues(fromTimestamp, toTimestamp); - } - - /** - * Returns the list of checkmarks that fall within the given interval. - *

- * There is exactly one checkmark per day in the interval. The endpoints of - * the interval are included. The list is ordered by timestamp (decreasing). - * That is, the first checkmark corresponds to the newest timestamp, and the - * last checkmark corresponds to the oldest timestamp. - * - * @param from timestamp of the beginning of the interval. - * @param to timestamp of the end of the interval. - * @return the list of checkmarks within the interval. - */ - @NonNull - public List getByInterval(Timestamp from, - Timestamp to) - { - compute(); - - Timestamp newestComputed = new Timestamp(0); - Timestamp oldestComputed = new Timestamp(0).plus(1000000); - - Entry newest = getNewestComputed(); - Entry oldest = getOldestComputed(); - if (newest != null) newestComputed = newest.getTimestamp(); - if (oldest != null) oldestComputed = oldest.getTimestamp(); - - List filtered = new ArrayList<>( - Math.max(0, oldestComputed.daysUntil(newestComputed) + 1)); - - for (int i = 0; i <= from.daysUntil(to); i++) - { - Timestamp t = to.minus(i); - if (t.isNewerThan(newestComputed) || t.isOlderThan(oldestComputed)) - filtered.add(new Entry(t, Entry.UNKNOWN)); - else - filtered.add(list.get(t.daysUntil(newestComputed))); - } - - return filtered; - - } - - /** - * Returns the checkmark for today. - * - * @return checkmark for today - */ - @Nullable - public synchronized final Entry getToday() - { - compute(); - Timestamp today = DateUtils.getTodayWithOffset(); - return getByInterval(today, today).get(0); - } - - /** - * Returns the value of today's checkmark. - * - * @return value of today's checkmark - */ - public synchronized int getTodayValue() - { - Entry today = getToday(); - if (today != null) return today.getValue(); - else return UNKNOWN; - } - - public synchronized int getThisWeekValue(int firstWeekday) - { - return getThisIntervalValue(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday); - } - - public synchronized int getThisMonthValue() - { - return getThisIntervalValue(DateUtils.TruncateField.MONTH, Calendar.SATURDAY); - } - - - public synchronized int getThisQuarterValue() - { - return getThisIntervalValue(DateUtils.TruncateField.QUARTER, Calendar.SATURDAY); - } - - - public synchronized int getThisYearValue() - { - return getThisIntervalValue(DateUtils.TruncateField.YEAR, Calendar.SATURDAY); - } - - private int getThisIntervalValue(DateUtils.TruncateField truncateField, int firstWeekday) - { - List groups = habit.getComputedEntries().groupBy(truncateField, firstWeekday, 1); - if (groups.isEmpty()) return 0; - return groups.get(0).getValue(); - } - - /** - * Returns the values of the checkmarks that fall inside a certain interval - * of time. - *

- * The values are returned in an array containing one integer value for each - * day of the interval. The first entry corresponds to the most recent day - * in the interval. Each subsequent entry corresponds to one day older than - * the previous entry. The boundaries of the time interval are included. - * - * @param from timestamp for the oldest checkmark - * @param to timestamp for the newest checkmark - * @return values for the checkmarks inside the given interval - */ - public final int[] getValues(Timestamp from, Timestamp to) - { - if (from.isNewerThan(to)) return new int[0]; - - List entries = getByInterval(from, to); - int values[] = new int[entries.size()]; - - int i = 0; - for (Entry c : entries) - values[i++] = c.getValue(); - - return values; - } - - /** - * Marks as invalid every checkmark that has timestamp either equal or newer - * than a given timestamp. These checkmarks will be recomputed at the next - * time they are queried. - * - */ - public void recompute() - { - list.clear(); - } - - /** - * Writes the entire list of checkmarks to the given writer, in CSV format. - * - * @param out the writer where the CSV will be output - * @throws IOException in case write operations fail - */ - public final void writeCSV(Writer out) throws IOException - { - int values[]; - - synchronized (this) - { - compute(); - values = getAllValues(); - } - - Timestamp timestamp = DateUtils.getToday(); - SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); - - for (int value : values) - { - String date = dateFormat.format(timestamp.toJavaDate()); - out.write(String.format("%s,%d\n", date, value)); - timestamp = timestamp.minus(1); - } - } - - /** - * Computes and stores one checkmark for each day, from the first habit - * repetition to today. If this list is already computed, does nothing. - */ - protected final synchronized void compute() - { - final Timestamp today = DateUtils.getTodayWithOffset(); - - Entry newest = getNewestComputed(); - if (newest != null && newest.getTimestamp().equals(today)) return; - recompute(); - - List entries = habit.getOriginalEntries().getKnown(); - if(entries.isEmpty()) return; - final Timestamp from = entries.get(entries.size() - 1).getTimestamp(); - - if (from.isNewerThan(today)) return; - - Entry[] reps = entries.stream().filter(e -> - !e.getTimestamp().isOlderThan(from) && !e.getTimestamp().isNewerThan(today) - ).toArray(Entry[]::new); - List repsAsList = Arrays.asList(reps); - Collections.reverse(repsAsList); - reps = repsAsList.toArray(reps); - - if (habit.isNumerical()) computeNumerical(reps); - else computeYesNo(reps); - } - - @Nullable - protected Entry getNewestComputed() - { - if (list.isEmpty()) return null; - return list.get(0); - } - - @Nullable - protected Entry getOldestComputed() - { - if (list.isEmpty()) return null; - return list.get(list.size() - 1); - } - - private void computeNumerical(Entry[] original) - { - if (original.length == 0) return; - - Timestamp today = DateUtils.getTodayWithOffset(); - Timestamp begin = original[0].getTimestamp(); - - int nDays = begin.daysUntil(today) + 1; - List computed = new ArrayList<>(nDays); - for (int i = 0; i < nDays; i++) - computed.add(new Entry(today.minus(i), 0)); - - for (Entry e : original) - { - int offset = e.getTimestamp().daysUntil(today); - computed.set(offset, new Entry(e.getTimestamp(), e.getValue())); - } - - add(computed); - } - - private void computeYesNo(Entry[] original) - { - ArrayList intervals; - intervals = buildIntervals(habit.getFrequency(), original); - snapIntervalsTogether(intervals); - add(buildEntriesFromInterval(original, intervals)); - } - - public List getAll() - { - List entries = habit.getOriginalEntries().getKnown(); - if(entries.isEmpty()) return new ArrayList<>(); - Entry oldest = entries.get(entries.size() - 1); - return getByInterval(oldest.getTimestamp(), DateUtils.getTodayWithOffset()); - } - - public boolean isCompletedToday() - { - int todayCheckmark = getTodayValue(); - if (habit.isNumerical()) - { - if (habit.getTargetType() == AT_LEAST) - return todayCheckmark / 1000.0 >= habit.getTargetValue(); - else - return todayCheckmark / 1000.0 <= habit.getTargetValue(); - } - else - { - return (todayCheckmark != NO && todayCheckmark != UNKNOWN); - } - } - - static final class Interval - { - final Timestamp begin; - - final Timestamp center; - - final Timestamp end; - - Interval(Timestamp begin, Timestamp center, Timestamp end) - { - if(begin.isNewerThan(center)) throw new IllegalArgumentException(); - if(center.isNewerThan(end)) throw new IllegalArgumentException(); - - this.begin = begin; - this.center = center; - this.end = end; - } - - public int length() - { - return begin.daysUntil(end) + 1; - } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - - if (o == null || getClass() != o.getClass()) return false; - - Interval interval = (Interval) o; - - return new EqualsBuilder() - .append(begin, interval.begin) - .append(center, interval.center) - .append(end, interval.end) - .isEquals(); - } - - @Override - public int hashCode() - { - return new HashCodeBuilder(17, 37) - .append(begin) - .append(center) - .append(end) - .toHashCode(); - } - - @Override - public String toString() - { - return new ToStringBuilder(this, defaultToStringStyle()) - .append("begin", begin) - .append("center", center) - .append("end", end) - .toString(); - } - } - - @NonNull - public List groupBy(DateUtils.TruncateField field, int firstWeekday) - { - return groupBy(field, firstWeekday, 0); - } - - - @NonNull - public List groupBy(DateUtils.TruncateField field, - int firstWeekday, - int maxGroups) - { - List checks = getAll(); - - int count = 0; - Timestamp[] truncatedTimestamps = new Timestamp[checks.size()]; - int[] values = new int[checks.size()]; - - for (Entry rep : checks) - { - Timestamp tt = rep.getTimestamp().truncate(field, firstWeekday); - if (count == 0 || !truncatedTimestamps[count - 1].equals(tt)) - { - if (maxGroups > 0 && count >= maxGroups) break; - truncatedTimestamps[count++] = tt; - } - - if (habit.isNumerical()) - values[count - 1] += rep.getValue(); - else if (rep.getValue() == Entry.YES_MANUAL) - values[count - 1] += 1000; - - } - - ArrayList groupedEntries = new ArrayList<>(); - for (int i = 0; i < count; i++) - { - Entry rep = new Entry(truncatedTimestamps[i], values[i]); - groupedEntries.add(rep); - } - - return groupedEntries; - } -} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt index 9abdc29e1..2e2f84c2e 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt @@ -18,6 +18,8 @@ */ package org.isoron.uhabits.core.models +import org.isoron.uhabits.core.utils.* + data class Habit( var color: PaletteColor = PaletteColor(8), var description: String = "", @@ -33,8 +35,7 @@ data class Habit( var type: Int = YES_NO_HABIT, var unit: String = "", var uuid: String? = null, - val computedEntries: EntryList, - val newComputedEntries: Entries, + val computedEntries: Entries, val originalEntries: Entries, val scores: ScoreList, val streaks: StreakList, @@ -49,13 +50,24 @@ data class Habit( fun hasReminder(): Boolean = reminder != null - fun isCompletedToday(): Boolean = computedEntries.isCompletedToday + fun isCompletedToday(): Boolean { + val today = DateUtils.getTodayWithOffset() + val value = computedEntries.get(today).value + return if (isNumerical) { + if (targetType == AT_LEAST) { + value / 1000.0 >= targetValue + } else { + value / 1000.0 <= targetValue + } + } else { + value != Entry.NO && value != Entry.UNKNOWN + } + } fun recompute() { scores.recompute() - computedEntries.recompute() streaks.recompute() - newComputedEntries.recomputeFrom( + computedEntries.recomputeFrom( originalEntries = originalEntries, frequency = frequency, isNumerical = isNumerical, diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java index 008d454af..5c8e94871 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/HabitList.java @@ -178,7 +178,6 @@ public abstract class HabitList implements Iterable { for (Habit h : this) { - h.getComputedEntries().recompute(); h.getStreaks().recompute(); h.getScores().recompute(); } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ModelFactory.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ModelFactory.kt index aff942d46..8b6c8fc4c 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ModelFactory.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ModelFactory.kt @@ -28,25 +28,20 @@ import org.isoron.uhabits.core.models.sqlite.records.* interface ModelFactory { fun buildHabit(): Habit { - val computedEntries = buildEntryList() val scores = buildScoreList() val streaks = buildStreakList() val habit = Habit( - computedEntries = computedEntries, scores = scores, streaks = streaks, originalEntries = buildOriginalEntries(), - newComputedEntries = buildNewComputedEntries(), + computedEntries = buildNewComputedEntries(), ) - computedEntries.setHabit(habit) scores.setHabit(habit) streaks.setHabit(habit) - habit.recompute() return habit } fun buildNewComputedEntries(): Entries fun buildOriginalEntries(): Entries - fun buildEntryList(): EntryList fun buildHabitList(): HabitList fun buildScoreList(): ScoreList fun buildStreakList(): StreakList diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java index 65ec9f448..2524c22a8 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java @@ -22,6 +22,7 @@ package org.isoron.uhabits.core.models.memory; import androidx.annotation.*; import org.isoron.uhabits.core.models.*; +import org.isoron.uhabits.core.utils.*; import java.util.*; @@ -186,8 +187,9 @@ public class MemoryHabitList extends HabitList return h1.isNumerical() ? -1 : 1; } - Integer v1 = Objects.requireNonNull(h1.getComputedEntries().getToday()).getValue(); - Integer v2 = Objects.requireNonNull(h2.getComputedEntries().getToday()).getValue(); + Timestamp today = DateUtils.getTodayWithOffset(); + Integer v1 = h1.getComputedEntries().get(today).getValue(); + Integer v2 = h2.getComputedEntries().get(today).getValue(); return v2.compareTo(v1); }; diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryModelFactory.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryModelFactory.kt index bc6ca55f2..6e135abcd 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryModelFactory.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryModelFactory.kt @@ -23,7 +23,6 @@ import org.isoron.uhabits.core.models.* class MemoryModelFactory : ModelFactory { override fun buildNewComputedEntries() = Entries() override fun buildOriginalEntries() = Entries() - override fun buildEntryList() = EntryList() override fun buildHabitList() = MemoryHabitList() override fun buildScoreList() = MemoryScoreList() override fun buildStreakList() = MemoryStreakList() diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt index 7fc452083..bf7f5f064 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLModelFactory.kt @@ -35,7 +35,6 @@ class SQLModelFactory ) : ModelFactory { override fun buildOriginalEntries() = SQLiteEntries(database) override fun buildNewComputedEntries() = Entries() - override fun buildEntryList() = EntryList() override fun buildHabitList() = SQLiteHabitList(this) override fun buildScoreList() = MemoryScoreList() override fun buildStreakList() = MemoryStreakList() diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java index bfb230bae..50995b7eb 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/test/HabitFixtures.java @@ -67,6 +67,7 @@ public class HabitFixtures for (int mark : marks) habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL)); + habit.recompute(); return habit; } @@ -92,6 +93,7 @@ public class HabitFixtures habit.getOriginalEntries().add(new Entry(timestamp, values[i])); } + habit.recompute(); return habit; } @@ -128,6 +130,7 @@ public class HabitFixtures habit.getOriginalEntries().add(new Entry(timestamp, values[i])); } + habit.recompute(); return habit; } @@ -148,6 +151,7 @@ public class HabitFixtures timestamp = timestamp.minus(1); } + habit.recompute(); return habit; } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/NotificationTray.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/NotificationTray.java index 3ab1ac437..4bea74ffd 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/NotificationTray.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/NotificationTray.java @@ -26,6 +26,7 @@ import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.tasks.*; +import org.isoron.uhabits.core.utils.*; import java.util.*; @@ -178,7 +179,8 @@ public class NotificationTray @Override public void doInBackground() { - todayValue = habit.getComputedEntries().getTodayValue(); + Timestamp today = DateUtils.getTodayWithOffset(); + todayValue = habit.getComputedEntries().get(today).getValue(); } @Override diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java index 3c5565ea9..ade9f15c5 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.java @@ -82,8 +82,8 @@ public class ListHabitsBehavior public void onEdit(@NonNull Habit habit, Timestamp timestamp) { - EntryList entries = habit.getComputedEntries(); - double oldValue = entries.getValues(timestamp, timestamp)[0]; + Entries entries = habit.getComputedEntries(); + double oldValue = entries.get(timestamp).getValue(); screen.showNumberPicker(oldValue / 1000, habit.getUnit(), newValue -> { diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitBehavior.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitBehavior.kt index f8fbdc674..35d1ef6e9 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitBehavior.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitBehavior.kt @@ -23,6 +23,7 @@ import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.ui.callbacks.* import org.isoron.uhabits.core.ui.screens.habits.list.* +import kotlin.math.* class ShowHabitBehavior( private val habitList: HabitList, @@ -57,9 +58,9 @@ class ShowHabitBehavior( override fun onToggleEntry(timestamp: Timestamp, value: Int) { if (habit.isNumerical) { val entries = habit.computedEntries - val oldValue = entries.getValues(timestamp, timestamp)[0].toDouble() - screen.showNumberPicker(oldValue / 1000, habit.unit) { newValue: Double -> - val thousands = Math.round(newValue * 1000).toInt() + val oldValue = entries.get(timestamp).value + screen.showNumberPicker(oldValue / 1000.0, habit.unit) { newValue: Double -> + val thousands = (newValue * 1000).roundToInt() commandRunner.execute( CreateRepetitionCommand( habitList, diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/EntriesTest.kt b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/EntriesTest.kt index aa17a8f89..f763b0902 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/EntriesTest.kt +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/EntriesTest.kt @@ -137,7 +137,7 @@ class EntriesTest { } @Test - fun testGroupBy() { + fun testGroupByNumerical() { 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, @@ -191,6 +191,52 @@ class EntriesTest { assertThat(byYear[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 16172))) } + @Test + fun testGroupByBoolean() { + 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 reference = Timestamp.from(2014, Calendar.JUNE, 1) + val entries = Entries() + offsets.indices.forEach { + entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL)) + } + + val byMonth = entries.groupBy( + field = DateUtils.TruncateField.MONTH, + firstWeekday = Calendar.SATURDAY, + isNumerical = false, + ) + assertThat(byMonth.size, equalTo(17)) + assertThat(byMonth[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 1))) + assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 7))) + assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 6))) + + val byQuarter = entries.groupBy( + field = DateUtils.TruncateField.QUARTER, + firstWeekday = Calendar.SATURDAY, + isNumerical = true, + ) + assertThat(byQuarter.size, equalTo(6)) + assertThat(byQuarter[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 30))) + assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 34))) + assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 40))) + + val byYear = entries.groupBy( + field = DateUtils.TruncateField.YEAR, + firstWeekday = Calendar.SATURDAY, + isNumerical = true, + ) + assertThat(byYear.size, equalTo(2)) + assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 68))) + assertThat(byYear[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 132))) + } + @Test fun testAddFromInterval() { val entries = listOf( diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitTest.java index dae05ea76..dca8795a5 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/HabitTest.java @@ -74,6 +74,7 @@ public class HabitTest extends BaseUnitTest Habit h = modelFactory.buildHabit(); assertFalse(h.isCompletedToday()); h.getOriginalEntries().add(new Entry(getToday(), Entry.YES_MANUAL)); + h.recompute(); assertTrue(h.isCompletedToday()); } diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreListTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreListTest.java index 6fe259b7c..518e9beea 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreListTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/ScoreListTest.java @@ -128,6 +128,7 @@ public class ScoreListTest extends BaseUnitTest addSkip(5); addSkip(10); addSkip(11); + habit.recompute(); double expectedValues[] = { 0.596033, @@ -163,6 +164,7 @@ public class ScoreListTest extends BaseUnitTest { check(5); addSkip(4); + habit.recompute(); double[] expectedValues = { 0.041949, @@ -261,17 +263,20 @@ public class ScoreListTest extends BaseUnitTest habit = fixtures.createEmptyHabit(); habit.setFrequency(Frequency.DAILY); for (int i = 0; i < 90; i++) check(i); + habit.recompute(); assertThat(habit.getScores().getTodayValue(), greaterThan(0.99)); // Weekly habits should achieve at least 99% in 9 months habit = fixtures.createEmptyHabit(); habit.setFrequency(Frequency.WEEKLY); for (int i = 0; i < 39; i++) check(7 * i); + habit.recompute(); assertThat(habit.getScores().getTodayValue(), greaterThan(0.99)); // Monthly habits should achieve at least 99% in 18 months habit.setFrequency(new Frequency(1, 30)); for (int i = 0; i < 18; i++) check(30 * i); + habit.recompute(); assertThat(habit.getScores().getTodayValue(), greaterThan(0.99)); } @@ -349,6 +354,7 @@ public class ScoreListTest extends BaseUnitTest for (int i = 0; i < values.size(); i++) if (values.get(i) == YES_MANUAL) entries.add(new Entry(today.minus(i), YES_MANUAL)); + habit.recompute(); } private void addSkip(final int day) diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java index 5ffb8afd2..b33b024e5 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/StreakListTest.java @@ -48,6 +48,7 @@ public class StreakListTest extends BaseUnitTest super.setUp(); habit = fixtures.createLongHabit(); habit.setFrequency(Frequency.DAILY); + habit.recompute(); streaks = habit.getStreaks(); streaks.rebuild(); diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.java index a236b5c3a..a51e237c5 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.java @@ -104,8 +104,7 @@ public class HabitCardListCacheTest extends BaseUnitTest Timestamp today = DateUtils.getToday(); int[] actualCheckmarks = cache.getCheckmarks(h.getId()); - int[] expectedCheckmarks = - h.getComputedEntries().getValues(today.minus(9), today); + int[] expectedCheckmarks = h.getComputedEntries().getValues(today.minus(9), today); assertThat(actualCheckmarks, equalTo(expectedCheckmarks)); } diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.java index f4f70a68b..a3307d953 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.java @@ -86,7 +86,8 @@ public class ListHabitsBehaviorTest extends BaseUnitTest behavior.onEdit(habit2, DateUtils.getToday()); verify(screen).showNumberPicker(eq(0.1), eq("miles"), picker.capture()); picker.getValue().onNumberPicked(100); - assertThat(habit2.getComputedEntries().getTodayValue(), equalTo(100000)); + Timestamp today = DateUtils.getTodayWithOffset(); + assertThat(habit2.getComputedEntries().get(today).getValue(), equalTo(100000)); } @Test diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.java index 9f0e8bb1a..6aec0f051 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.java @@ -107,6 +107,7 @@ public class WidgetBehaviorTest extends BaseUnitTest { habit = fixtures.createNumericalHabit(); habit.getOriginalEntries().add(new Entry(timestamp, 500)); + habit.recompute(); behavior.onIncrement(habit, timestamp, 100); verify(commandRunner).execute( @@ -121,6 +122,7 @@ public class WidgetBehaviorTest extends BaseUnitTest { habit = fixtures.createNumericalHabit(); habit.getOriginalEntries().add(new Entry(timestamp, 500)); + habit.recompute(); behavior.onDecrement(habit, timestamp, 100); verify(commandRunner).execute(