diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java index a178ceeca..2e6186b5d 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.java @@ -97,15 +97,15 @@ public class LoopDBImporter extends AbstractImporter helper.migrateTo(DATABASE_VERSION); Repository habitsRepository; - Repository repsRepository; + Repository entryRepository; habitsRepository = new Repository<>(HabitRecord.class, db); - repsRepository = new Repository<>(RepetitionRecord.class, db); + entryRepository = new Repository<>(EntryRecord.class, db); List records = habitsRepository.findAll("order by position"); for (HabitRecord habitRecord : records) { - List reps = - repsRepository.findAll("where habit = ?", + List reps = + entryRepository.findAll("where habit = ?", habitRecord.id.toString()); Habit habit = habitList.getByUUID(habitRecord.uuid); @@ -127,7 +127,7 @@ public class LoopDBImporter extends AbstractImporter // Reload saved version of the habit habit = habitList.getByUUID(habitRecord.uuid); - for (RepetitionRecord r : reps) + for (EntryRecord r : reps) { Timestamp t = new Timestamp(r.timestamp); Entry entry = habit.getOriginalEntries().getByTimestamp(t); 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 203cbaaf1..e7140e59f 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 @@ -26,7 +26,7 @@ import org.isoron.uhabits.core.utils.* import kotlin.collections.set import kotlin.math.* -class Entries { +open class Entries { private val entriesByTimestamp: HashMap = HashMap() @@ -34,7 +34,7 @@ class Entries { * 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 { + open fun get(timestamp: Timestamp): Entry { return entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) } @@ -43,7 +43,7 @@ class Entries { * newest entry, and the last element corresponds to the oldest. The interval endpoints are * included. */ - fun getByInterval(from: Timestamp, to: Timestamp): List { + open fun getByInterval(from: Timestamp, to: Timestamp): List { val result = mutableListOf() var current = to while (current >= from) { @@ -57,7 +57,7 @@ class Entries { * Adds the given entry to the list. If another entry with the same timestamp already exists, * replaces it. */ - fun add(entry: Entry) { + open fun add(entry: Entry) { entriesByTimestamp[entry.timestamp] = entry } @@ -65,7 +65,7 @@ class Entries { * 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 { + open fun getKnown(): List { return entriesByTimestamp.values.sortedBy { it.timestamp }.reversed() } @@ -78,7 +78,7 @@ class Entries { * entries. For numerical habits, the value is the total sum. The field [firstWeekday] is only * relevant when grouping by week. */ - fun groupBy( + open fun groupBy( field: DateUtils.TruncateField, firstWeekday: Int, isNumerical: Boolean, @@ -111,7 +111,7 @@ class Entries { * 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( + open fun computeFrom( other: Entries, frequency: Frequency, isNumerical: Boolean, @@ -129,18 +129,18 @@ class Entries { } } - 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() { + open fun clear() { entriesByTimestamp.clear() } + data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) { + val length: Int + get() = begin.daysUntil(end) + 1; + } + /** * 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 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 3f7579e49..5a0e725e6 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 @@ -51,5 +51,5 @@ interface ModelFactory { fun buildScoreList(): ScoreList fun buildStreakList(): StreakList fun buildHabitListRepository(): Repository - fun buildRepetitionListRepository(): Repository + fun buildRepetitionListRepository(): Repository } \ No newline at end of file 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 af32668e3..2eae29d70 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 @@ -44,5 +44,5 @@ class SQLModelFactory Repository(HabitRecord::class.java, database) override fun buildRepetitionListRepository() = - Repository(RepetitionRecord::class.java, database) + Repository(EntryRecord::class.java, database) } \ No newline at end of file diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntries.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntries.kt new file mode 100644 index 000000000..d96e16a73 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntries.kt @@ -0,0 +1,86 @@ +/* + * 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.sqlite + +import org.isoron.uhabits.core.database.* +import org.isoron.uhabits.core.models.* +import org.isoron.uhabits.core.models.sqlite.records.* +import org.isoron.uhabits.core.utils.* + +class SQLiteEntries(database: Database) : Entries() { + val repository = Repository(EntryRecord::class.java, database) + var habitId: Long? = null + var isLoaded = false + + private fun loadRecords() { + if (isLoaded) return + val habitId = habitId ?: throw IllegalStateException("habitId must be set") + val records = repository.findAll("where habit = ? order by timestamp", + habitId.toString()) + for (rec in records) super.add(rec.toEntry()) + isLoaded = true + } + + override fun get(timestamp: Timestamp): Entry { + loadRecords() + return super.get(timestamp) + } + + override fun getByInterval(from: Timestamp, to: Timestamp): List { + loadRecords() + return super.getByInterval(from, to) + } + + override fun add(entry: Entry) { + loadRecords() + val habitId = habitId ?: throw IllegalStateException("habitId must be set") + + // Remove existing rows + repository.execSQL("delete from repetitions where habit = ? and timestamp = ?", + habitId.toString(), + entry.timestamp.unixTime.toString()) + + // Add new row + val record = EntryRecord().apply { copyFrom(entry) } + record.habitId = habitId + repository.save(record) + + // Add to memory list + super.add(entry) + } + + override fun getKnown(): List { + loadRecords() + return super.getKnown() + } + + override fun groupBy(field: DateUtils.TruncateField, firstWeekday: Int, isNumerical: Boolean): List { + loadRecords() + return super.groupBy(field, firstWeekday, isNumerical) + } + + override fun computeFrom(other: Entries, frequency: Frequency, isNumerical: Boolean) { + throw UnsupportedOperationException() + } + + override fun clear() { + throw UnsupportedOperationException() + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionList.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionList.java index 783cb14ae..075829404 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionList.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionList.java @@ -36,7 +36,7 @@ import java.util.*; */ public class SQLiteRepetitionList extends RepetitionList { - private final Repository repository; + private final Repository repository; private boolean loaded = false; @@ -51,12 +51,12 @@ public class SQLiteRepetitionList extends RepetitionList loaded = true; check(habit.getId()); - List records = + List records = repository.findAll("where habit = ? order by timestamp", habit.getId().toString()); - for (RepetitionRecord rec : records) - super.add(rec.toCheckmark()); + for (EntryRecord rec : records) + super.add(rec.toEntry()); } @Override @@ -65,8 +65,8 @@ public class SQLiteRepetitionList extends RepetitionList loadRecords(); super.add(entry); check(habit.getId()); - RepetitionRecord record = new RepetitionRecord(); - record.habit_id = habit.getId(); + EntryRecord record = new EntryRecord(); + record.habitId = habit.getId(); record.copyFrom(entry); repository.save(record); } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/records/RepetitionRecord.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.java similarity index 94% rename from android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/records/RepetitionRecord.java rename to android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.java index 95494c8bd..56b21ff4c 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/records/RepetitionRecord.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.java @@ -28,12 +28,12 @@ import org.isoron.uhabits.core.models.*; * The SQLite database record corresponding to a {@link Entry}. */ @Table(name = "Repetitions") -public class RepetitionRecord +public class EntryRecord { public HabitRecord habit; @Column(name = "habit") - public Long habit_id; + public Long habitId; @Column public Long timestamp; @@ -50,7 +50,7 @@ public class RepetitionRecord value = entry.getValue(); } - public Entry toCheckmark() + public Entry toEntry() { return new Entry(new Timestamp(timestamp), value); } diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java index 46188e1a2..bf34936aa 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/BaseUnitTest.java @@ -120,7 +120,7 @@ public class BaseUnitTest } - protected Database buildMemoryDatabase() + public static Database buildMemoryDatabase() { try { diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntriesTest.kt b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntriesTest.kt new file mode 100644 index 000000000..ea4a80a72 --- /dev/null +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntriesTest.kt @@ -0,0 +1,105 @@ +/* + * 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.sqlite + +import junit.framework.Assert.* +import org.isoron.uhabits.core.BaseUnitTest.* +import org.isoron.uhabits.core.database.* +import org.isoron.uhabits.core.models.* +import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN +import org.isoron.uhabits.core.models.sqlite.records.* +import org.isoron.uhabits.core.utils.* +import org.junit.* + +class SQLiteEntriesTest { + + private val database = buildMemoryDatabase() + private val repository = Repository(EntryRecord::class.java, database) + private val entries = SQLiteEntries(database) + private val today = DateUtils.getToday() + + @Before + fun setUp() { + // Create a habit and add it to the database to satisfy foreign key requirements + val factory = SQLModelFactory(database) + val habitList = factory.buildHabitList() + val habit = factory.buildHabit() + habitList.add(habit) + entries.habitId = habit.id + } + + @Test + fun testLoad() { + val today = DateUtils.getToday() + repository.save(EntryRecord().apply { + habitId = entries.habitId + timestamp = today.unixTime + value = 500 + }) + repository.save(EntryRecord().apply { + habitId = entries.habitId + timestamp = today.minus(5).unixTime + value = 300 + }) + assertEquals( + Entry(timestamp = today, value = 500), + entries.get(today), + ) + assertEquals( + Entry(timestamp = today.minus(1), value = UNKNOWN), + entries.get(today.minus(1)), + ) + assertEquals( + Entry(timestamp = today.minus(5), value = 300), + entries.get(today.minus(5)), + ) + } + + @Test + fun testAdd() { + assertNull(getByTimestamp(1, today)) + + val original = Entry(today, 150) + entries.add(original) + + val retrieved = getByTimestamp(1, today) + assertNotNull(retrieved) + assertEquals(original, retrieved!!.toEntry()) + + val replacement = Entry(today, 90) + entries.add(replacement) + + val retrieved2 = getByTimestamp(1, today) + assertNotNull(retrieved2) + assertEquals(replacement, retrieved2!!.toEntry()) + } + + private fun getByTimestamp( + habitId: Int, + timestamp: Timestamp, + ): EntryRecord? { + return repository.findFirst( + "where habit = ? and timestamp = ?", + habitId.toString(), + timestamp.unixTime.toString(), + ) + } + +} \ No newline at end of file diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionListTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionListTest.java index 1df33778f..c9d9385f4 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionListTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/SQLiteRepetitionListTest.java @@ -46,7 +46,7 @@ public class SQLiteRepetitionListTest extends BaseUnitTest private long day; - private Repository repository; + private Repository repository; @Override public void setUp() throws Exception @@ -57,7 +57,7 @@ public class SQLiteRepetitionListTest extends BaseUnitTest modelFactory = new SQLModelFactory(db); habitList = modelFactory.buildHabitList(); fixtures = new HabitFixtures(modelFactory, habitList); - repository = new Repository<>(RepetitionRecord.class, db); + repository = new Repository<>(EntryRecord.class, db); habit = fixtures.createLongHabit(); originalCheckmarks = habit.getOriginalEntries(); @@ -67,7 +67,7 @@ public class SQLiteRepetitionListTest extends BaseUnitTest @Test public void testAdd() { - RepetitionRecord record = getByTimestamp(today.plus(1)); + EntryRecord record = getByTimestamp(today.plus(1)); assertNull(record); Entry rep = new Entry(today.plus(1), YES_MANUAL); @@ -120,10 +120,10 @@ public class SQLiteRepetitionListTest extends BaseUnitTest @Test public void testRemove() { - RepetitionRecord record = getByTimestamp(today); + EntryRecord record = getByTimestamp(today); assertNotNull(record); - Entry rep = record.toCheckmark(); + Entry rep = record.toEntry(); originalCheckmarks.remove(rep); record = getByTimestamp(today); @@ -131,7 +131,7 @@ public class SQLiteRepetitionListTest extends BaseUnitTest } @Nullable - private RepetitionRecord getByTimestamp(Timestamp timestamp) + private EntryRecord getByTimestamp(Timestamp timestamp) { String query = "where habit = ? and timestamp = ?"; String params[] = { diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/records/RepetitionRecordTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecordTest.java similarity index 87% rename from android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/records/RepetitionRecordTest.java rename to android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecordTest.java index b8338f0f4..2687ce6c7 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/records/RepetitionRecordTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecordTest.java @@ -28,14 +28,14 @@ import org.junit.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; -public class RepetitionRecordTest extends BaseUnitTest +public class EntryRecordTest extends BaseUnitTest { @Test public void testRecord() throws Exception { Entry check = new Entry(Timestamp.ZERO.plus(100), 50); - RepetitionRecord record = new RepetitionRecord(); + EntryRecord record = new EntryRecord(); record.copyFrom(check); - assertThat(check, equalTo(record.toCheckmark())); + assertThat(check, equalTo(record.toEntry())); } }