From 94c78ebb726b319d6915d4dae6ef7db420c113ed Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 24 Dec 2020 09:43:03 -0600 Subject: [PATCH] Write cleaner version of EntryList --- .../org/isoron/uhabits/core/models/Entries.kt | 229 +++++++++++++ .../isoron/uhabits/core/models/EntryList.java | 2 +- .../uhabits/core/models/RepetitionList.java | 2 +- .../org/isoron/uhabits/core/models/Score.java | 2 +- .../isoron/uhabits/core/models/Streak.java | 2 +- .../isoron/uhabits/core/models/Timestamp.java | 20 +- .../core/models/memory/MemoryScoreList.java | 2 +- .../isoron/uhabits/core/models/EntriesTest.kt | 302 ++++++++++++++++++ .../uhabits/core/models/TimestampTest.java | 7 +- 9 files changed, 552 insertions(+), 16 deletions(-) create mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Entries.kt create mode 100644 android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/EntriesTest.kt 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 new file mode 100644 index 000000000..203cbaaf1 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Entries.kt @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2016-2020 Á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 org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN +import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO +import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL +import org.isoron.uhabits.core.utils.* +import kotlin.collections.set +import kotlin.math.* + +class Entries { + + private val entriesByTimestamp: HashMap = HashMap() + + /** + * Returns the entry corresponding to the given timestamp. If no entry with such timestamp + * has been previously added, returns Entry(timestamp, UNKNOWN). + */ + fun get(timestamp: Timestamp): Entry { + return entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) + } + + /** + * Returns one entry for each day in the given interval. The first element corresponds to the + * newest entry, and the last element corresponds to the oldest. The interval endpoints are + * included. + */ + fun getByInterval(from: Timestamp, to: Timestamp): List { + val result = mutableListOf() + var current = to + while (current >= from) { + result.add(get(current)) + current = current.minus(1) + } + return result + } + + /** + * Adds the given entry to the list. If another entry with the same timestamp already exists, + * replaces it. + */ + fun add(entry: Entry) { + entriesByTimestamp[entry.timestamp] = entry + } + + /** + * Returns all entries whose values are known, sorted by timestamp. The first element + * corresponds to the newest entry, and the last element corresponds to the oldest. + */ + fun getKnown(): List { + 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. + */ + fun groupBy( + field: DateUtils.TruncateField, + firstWeekday: Int, + isNumerical: Boolean, + ): List { + val original = getKnown() + val truncated = original.map { + Entry(it.timestamp.truncate(field, firstWeekday), it.value) + } + val timestamps = mutableListOf() + val values = mutableListOf() + for (i in truncated.indices) { + if (i == 0 || timestamps.last() != truncated[i].timestamp) { + timestamps.add(truncated[i].timestamp) + values.add(0) + } + if (isNumerical) { + values[values.lastIndex] += truncated[i].value + } else { + if (values[values.lastIndex] == YES_MANUAL) { + values[values.lastIndex] += 1 + } + } + } + return timestamps.indices.map { Entry(timestamps[it], values[it]) } + } + + /** + * Replaces all entries in this list by entries computed automatically from another list. + * + * For boolean habits, this function creates additional entries (with value YES_AUTO) according + * to the frequency of the habit. For numerical habits, this function simply copies all entries. + */ + fun computeFrom( + other: Entries, + frequency: Frequency, + isNumerical: Boolean, + ) { + clear() + val original = other.getKnown() + if (isNumerical) { + 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) } + } + } + + data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) { + val length: Int + get() = begin.daysUntil(end) + 1; + } + + /** + * Removes all known entries. + */ + fun clear() { + entriesByTimestamp.clear() + } + + /** + * Converts a list of intervals into a list of entries. Entries that fall outside of any + * interval receive value UNKNOWN. Entries that fall within an interval but do not appear + * in [original] receive value YES_AUTO. Entries provided in [original] are just copied over. + * + * The intervals should be sorted by timestamp. The first element in the list should + * correspond to the newest interval. + */ + companion object { + fun buildEntriesFromInterval( + original: List, + intervals: List, + ): List { + val toTimestamp = intervals.first().end + val fromTimstamp = intervals.last().begin + val result = arrayListOf() + + // Create unknown entries + var current = toTimestamp + while (current >= fromTimstamp) { + result.add(Entry(current, UNKNOWN)) + current = current.minus(1) + } + + // Create YES_AUTO entries + intervals.forEach { interval -> + current = interval.end + while (current >= interval.begin) { + val offset = current.daysUntil(toTimestamp) + result[offset] = Entry(current, YES_AUTO) + current = current.minus(1) + } + } + + // Copy original entries + original.forEach { entry -> + val offset = entry.timestamp.daysUntil(toTimestamp) + result[offset] = entry + } + + return result + } + + /** + * 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. + * + * The intervals should be sorted by timestamp. The first element in the list should + * correspond to the newest interval. + */ + fun snapIntervalsTogether(intervals: ArrayList) { + for (i in 1 until intervals.size) { + val curr = intervals[i] + val next = intervals[i - 1] + val gapNextToCurrent = next.begin.daysUntil(curr.end) + val gapCenterToEnd = curr.center.daysUntil(curr.end) + if (gapNextToCurrent >= 0) { + val shift = min(gapCenterToEnd, gapNextToCurrent + 1) + intervals[i] = Interval(curr.begin.minus(shift), + curr.center, + curr.end.minus(shift)) + } + } + } + + fun buildIntervals( + freq: Frequency, + entries: List, + ): ArrayList { + val filtered = entries.filter { it.value == YES_MANUAL } + val num = freq.numerator + val den = freq.denominator + val intervals = arrayListOf() + for (i in num - 1 until filtered.size) { + val (begin, _) = filtered[i] + val (center, _) = filtered[i - num + 1] + if (begin.daysUntil(center) < den) { + val end = begin.plus(den - 1) + intervals.add(Interval(begin, center, end)) + } + } + return intervals + } + } +} \ No newline at end of file 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 index 5a7f8d346..464d48341 100644 --- 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 @@ -158,7 +158,7 @@ public class EntryList { list.addAll(entries); Collections.sort(list, - (c1, c2) -> c2.getTimestamp().compare(c1.getTimestamp())); + (c1, c2) -> c2.getTimestamp().compareTo(c1.getTimestamp())); } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java index 7ff5ffbf2..994f4c848 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/RepetitionList.java @@ -74,7 +74,7 @@ public class RepetitionList } Collections.sort(filtered, - (r1, r2) -> r1.getTimestamp().compare(r2.getTimestamp())); + (r1, r2) -> r1.getTimestamp().compareTo(r2.getTimestamp())); return filtered; } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.java index d01791422..533fffb0f 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.java @@ -73,7 +73,7 @@ public final class Score public int compareNewer(Score other) { - return getTimestamp().compare(other.getTimestamp()); + return getTimestamp().compareTo(other.getTimestamp()); } public Timestamp getTimestamp() diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Streak.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Streak.java index 4a66bed7a..b30bbea59 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Streak.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Streak.java @@ -45,7 +45,7 @@ public final class Streak public int compareNewer(Streak other) { - return end.compare(other.end); + return end.compareTo(other.end); } public Timestamp getEnd() diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java index deca3de9a..992894b8b 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Timestamp.java @@ -20,15 +20,13 @@ package org.isoron.uhabits.core.models; import org.apache.commons.lang3.builder.*; -import org.isoron.uhabits.core.utils.DateFormats; -import org.isoron.uhabits.core.utils.DateUtils; +import org.isoron.uhabits.core.utils.*; import java.util.*; import static java.util.Calendar.*; -import static org.isoron.uhabits.core.utils.StringUtils.*; -public final class Timestamp +public final class Timestamp implements Comparable { public static final long DAY_LENGTH = 86400000; @@ -53,6 +51,13 @@ public final class Timestamp this(cal.getTimeInMillis()); } + public static Timestamp from(int year, int javaMonth, int day) + { + GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); + cal.set(year, javaMonth, day, 0, 0, 0); + return new Timestamp(cal.getTimeInMillis()); + } + public long getUnixTime() { return unixTime; @@ -62,7 +67,8 @@ public final class Timestamp * Returns -1 if this timestamp is older than the given timestamp, 1 if this * timestamp is newer, or zero if they are equal. */ - public int compare(Timestamp other) + @Override + public int compareTo(Timestamp other) { return Long.signum(this.unixTime - other.unixTime); } @@ -117,12 +123,12 @@ public final class Timestamp public boolean isNewerThan(Timestamp other) { - return compare(other) > 0; + return compareTo(other) > 0; } public boolean isOlderThan(Timestamp other) { - return compare(other) < 0; + return compareTo(other) < 0; } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryScoreList.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryScoreList.java index aa897d50d..23d378e4a 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryScoreList.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/memory/MemoryScoreList.java @@ -40,7 +40,7 @@ public class MemoryScoreList extends ScoreList { list.addAll(scores); Collections.sort(list, - (s1, s2) -> s2.getTimestamp().compare(s1.getTimestamp())); + (s1, s2) -> s2.getTimestamp().compareTo(s1.getTimestamp())); getObservable().notifyListeners(); } 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 new file mode 100644 index 000000000..f98474824 --- /dev/null +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/EntriesTest.kt @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2016-2020 Á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 org.hamcrest.MatcherAssert.* +import org.hamcrest.core.IsEqual.* +import org.isoron.uhabits.core.models.Entry.Companion.NO +import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN +import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO +import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL +import org.isoron.uhabits.core.utils.* +import org.junit.* +import java.util.* +import kotlin.test.* + +class EntriesTest { + @Test + fun testEmptyList() { + val entries = Entries() + val today = DateUtils.getToday() + + assertEquals(Entry(today.minus(0), UNKNOWN), entries.get(today.minus(0))) + assertEquals(Entry(today.minus(2), UNKNOWN), entries.get(today.minus(2))) + assertEquals(Entry(today.minus(5), UNKNOWN), entries.get(today.minus(5))) + + entries.add(Entry(today.minus(0), 10)) + entries.add(Entry(today.minus(0), 15)) // replace previous one + entries.add(Entry(today.minus(5), 20)) + entries.add(Entry(today.minus(8), 30)) + assertEquals(Entry(today.minus(0), 15), entries.get(today.minus(0))) + assertEquals(Entry(today.minus(5), 20), entries.get(today.minus(5))) + assertEquals(Entry(today.minus(8), 30), entries.get(today.minus(8))) + + val known = entries.getKnown() + assertEquals(3, known.size) + assertEquals(Entry(today.minus(0), 15), known[0]) + assertEquals(Entry(today.minus(5), 20), known[1]) + assertEquals(Entry(today.minus(8), 30), known[2]) + + val actual = entries.getByInterval(today.minus(5), today) + assertEquals(6, actual.size) + assertEquals(Entry(today.minus(0), 15), actual[0]) + assertEquals(Entry(today.minus(1), UNKNOWN), actual[1]) + assertEquals(Entry(today.minus(2), UNKNOWN), actual[2]) + assertEquals(Entry(today.minus(3), UNKNOWN), actual[3]) + assertEquals(Entry(today.minus(4), UNKNOWN), actual[4]) + assertEquals(Entry(today.minus(5), 20), actual[5]) + } + + @Test + fun testComputeBoolean() { + val today = DateUtils.getToday() + + val original = Entries() + original.add(Entry(today.minus(4), YES_MANUAL)) + original.add(Entry(today.minus(9), YES_MANUAL)) + original.add(Entry(today.minus(10), YES_MANUAL)) + + val computed = Entries() + computed.computeFrom(original, Frequency(1, 3), isNumerical = false) + + val expected = listOf( + Entry(today.minus(2), YES_AUTO), + Entry(today.minus(3), YES_AUTO), + Entry(today.minus(4), YES_MANUAL), + Entry(today.minus(7), YES_AUTO), + Entry(today.minus(8), YES_AUTO), + Entry(today.minus(9), YES_MANUAL), + Entry(today.minus(10), YES_MANUAL), + Entry(today.minus(11), YES_AUTO), + Entry(today.minus(12), YES_AUTO), + ) + assertEquals(expected, computed.getKnown()) + + // Second call should replace all previously added entries + computed.computeFrom(Entries(), Frequency(1, 3), isNumerical = false) + assertEquals(listOf(), computed.getKnown()) + + } + + @Test + fun testComputeNumerical() { + val today = DateUtils.getToday() + + val original = Entries() + original.add(Entry(today.minus(4), 100)) + original.add(Entry(today.minus(9), 200)) + original.add(Entry(today.minus(10), 300)) + + val computed = Entries() + computed.computeFrom(original, Frequency.DAILY, isNumerical = true) + + val expected = listOf( + Entry(today.minus(4), 100), + Entry(today.minus(9), 200), + Entry(today.minus(10), 300), + ) + assertEquals(expected, computed.getKnown()) + } + + @Test + fun testGroupBy() { + 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 = Entries() + offsets.indices.forEach { + entries.add(Entry(reference.minus(offsets[it]), values[it])) + } + + val byMonth = entries.groupBy( + field = DateUtils.TruncateField.MONTH, + firstWeekday = Calendar.SATURDAY, + isNumerical = true, + ) + 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.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), 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.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), 8227))) + assertThat(byYear[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 16172))) + } + + @Test + fun testAddFromInterval() { + val entries = listOf( + Entry(day(1), YES_MANUAL), + Entry(day(2), NO), + Entry(day(5), YES_MANUAL), + Entry(day(10), YES_MANUAL), + ) + val intervals = listOf( + Entries.Interval(day(2), day(2), day(1)), + Entries.Interval(day(6), day(5), day(4)), + Entries.Interval(day(10), day(8), day(8)), + ) + val expected = listOf( + Entry(day(1), YES_MANUAL), + Entry(day(2), NO), + Entry(day(3), UNKNOWN), + Entry(day(4), YES_AUTO), + Entry(day(5), YES_MANUAL), + Entry(day(6), YES_AUTO), + Entry(day(7), UNKNOWN), + Entry(day(8), YES_AUTO), + Entry(day(9), YES_AUTO), + Entry(day(10), YES_MANUAL), + ) + val actual = Entries.buildEntriesFromInterval(entries, intervals) + assertThat(actual, equalTo(expected)) + } + + @Test + fun testSnapIntervalsTogether1() { + val original = arrayListOf( + Entries.Interval(day(8), day(8), day(2)), + Entries.Interval(day(12), day(12), day(6)), + Entries.Interval(day(20), day(20), day(14)), + Entries.Interval(day(27), day(27), day(21)), + ) + val expected = arrayListOf( + Entries.Interval(day(8), day(8), day(2)), + Entries.Interval(day(15), day(12), day(9)), + Entries.Interval(day(22), day(20), day(16)), + Entries.Interval(day(29), day(27), day(23)), + ) + Entries.snapIntervalsTogether(original) + assertThat(original, equalTo(expected)) + } + + @Test + fun testSnapIntervalsTogether2() { + val original = arrayListOf( + Entries.Interval(day(6), day(4), day(0)), + Entries.Interval(day(11), day(8), day(5)), + ) + val expected = arrayListOf( + Entries.Interval(day(6), day(4), day(0)), + Entries.Interval(day(13), day(8), day(7)), + ) + Entries.snapIntervalsTogether(original) + assertThat(original, equalTo(expected)) + } + + @Test + fun testBuildIntervals1() { + val entries = listOf( + Entry(day(8), YES_MANUAL), + Entry(day(18), YES_MANUAL), + Entry(day(23), YES_MANUAL), + ) + val expected = listOf( + Entries.Interval(day(8), day(8), day(2)), + Entries.Interval(day(18), day(18), day(12)), + Entries.Interval(day(23), day(23), day(17)), + ) + val actual = Entries.buildIntervals(Frequency.WEEKLY, entries) + assertThat(actual, equalTo(expected)) + } + + @Test + fun testBuildIntervals2() { + val entries = listOf( + Entry(day(8), YES_MANUAL), + Entry(day(18), YES_MANUAL), + Entry(day(23), YES_MANUAL), + ) + val expected = listOf( + Entries.Interval(day(8), day(8), day(8)), + Entries.Interval(day(18), day(18), day(18)), + Entries.Interval(day(23), day(23), day(23)), + ) + val actual = Entries.buildIntervals(Frequency.DAILY, entries) + assertThat(actual, equalTo(expected)) + } + + @Test + fun testBuildIntervals3() { + val entries = listOf( + Entry(day(8), YES_MANUAL), + Entry(day(15), YES_MANUAL), + Entry(day(18), YES_MANUAL), + Entry(day(22), YES_MANUAL), + Entry(day(23), YES_MANUAL), + ) + val expected = listOf( + Entries.Interval(day(18), day(15), day(12)), + Entries.Interval(day(22), day(18), day(16)), + Entries.Interval(day(23), day(22), day(17)), + ) + val actual = Entries.buildIntervals(Frequency.TWO_TIMES_PER_WEEK, entries) + assertThat(actual, equalTo(expected)) + } + + + @Test + fun testBuildIntervals4() { + val entries = listOf( + Entry(day(10), YES_MANUAL), + Entry(day(20), Entry.SKIP), + Entry(day(30), YES_MANUAL), + ) + val expected = listOf( + Entries.Interval(day(10), day(10), day(8)), + Entries.Interval(day(30), day(30), day(28)), + ) + val actual = Entries.buildIntervals(Frequency(1, 3), entries) + assertThat(actual, equalTo(expected)) + } + + fun day(offset: Int) = DateUtils.getToday().minus(offset) + +} diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/TimestampTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/TimestampTest.java index 25b5cd897..2e9c0709a 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/TimestampTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/TimestampTest.java @@ -22,7 +22,6 @@ package org.isoron.uhabits.core.models; import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.utils.*; import org.junit.*; -import org.mockito.internal.verification.*; import static junit.framework.TestCase.assertFalse; import static org.hamcrest.MatcherAssert.*; @@ -38,9 +37,9 @@ public class TimestampTest extends BaseUnitTest Timestamp t2 = t1.minus(1); Timestamp t3 = t1.plus(3); - assertThat(t1.compare(t2), greaterThan(0)); - assertThat(t1.compare(t1), equalTo(0)); - assertThat(t1.compare(t3), lessThan(0)); + assertThat(t1.compareTo(t2), greaterThan(0)); + assertThat(t1.compareTo(t1), equalTo(0)); + assertThat(t1.compareTo(t3), lessThan(0)); assertTrue(t1.isNewerThan(t2)); assertFalse(t1.isNewerThan(t1));