From 2b23b36e36703842cf589f36d75cbfb6cecb06d0 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 10 Jun 2016 18:54:47 -0400 Subject: [PATCH] Move remaining model tests to JVM; simplify SQLite implementation --- app/build.gradle | 11 + .../uhabits/unit/models/ScoreListTest.java | 163 ---------- .../org/isoron/uhabits/BaseComponent.java | 4 +- .../org/isoron/uhabits/models/Checkmark.java | 11 +- .../org/isoron/uhabits/models/HabitList.java | 9 +- .../isoron/uhabits/models/RepetitionList.java | 6 +- .../java/org/isoron/uhabits/models/Score.java | 9 +- .../org/isoron/uhabits/models/ScoreList.java | 125 ++++---- .../org/isoron/uhabits/models/StreakList.java | 9 +- .../models/memory/MemoryModelFactory.java | 2 +- .../models/memory/MemoryScoreList.java | 96 ++++++ .../models/sqlite/SQLiteCheckmarkList.java | 1 + .../models/sqlite/SQLiteHabitList.java | 4 +- .../models/sqlite/SQLiteScoreList.java | 62 ++-- .../models/sqlite/SQLiteStreakList.java | 12 +- .../ui/habits/edit/BaseDialogFragment.java | 11 +- .../habits/edit/EditHabitDialogFragment.java | 9 - .../ui/habits/show/views/HabitScoreView.java | 60 +++- .../org/isoron/uhabits/utils/DateUtils.java | 288 ++++++++++-------- .../isoron/uhabits/models/ScoreListTest.java | 195 ++++++++++++ .../org/isoron/uhabits}/models/ScoreTest.java | 46 ++- .../isoron/uhabits/utils/DateUtilsTest.java | 146 +++++++++ 22 files changed, 817 insertions(+), 462 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java create mode 100644 app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java rename app/src/{androidTest/java/org/isoron/uhabits/unit => test/java/org/isoron/uhabits}/models/ScoreTest.java (71%) create mode 100644 app/src/test/java/org/isoron/uhabits/utils/DateUtilsTest.java diff --git a/app/build.gradle b/app/build.gradle index 318bc5d6c..93b68ca48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,6 +35,17 @@ android { targetCompatibility 1.8 sourceCompatibility 1.8 } + + testOptions { + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } + } dependencies { diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java index fdda41504..d64cd3aea 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java @@ -23,174 +23,11 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.utils.DatabaseUtils; -import org.isoron.uhabits.utils.DateUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; import org.junit.runner.RunWith; -import java.io.IOException; -import java.io.StringWriter; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - @RunWith(AndroidJUnit4.class) @SmallTest public class ScoreListTest extends BaseAndroidTest { - private Habit habit; - - @Before - public void setUp() - { - super.setUp(); - - habitFixtures.purgeHabits(habitList); - habit = habitFixtures.createEmptyHabit(); - } - - @After - public void tearDown() - { - DateUtils.setFixedLocalTime(null); - } - - @Test - public void test_getAllValues_withGroups() - { - toggleRepetitions(0, 20); - - int expectedValues[] = {11434978, 7894999, 3212362}; - - int actualValues[] = habit.getScores().getAllValues(7); - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_withoutGroups() - { - toggleRepetitions(0, 20); - - int expectedValues[] = { - 12629351, - 12266245, - 11883254, - 11479288, - 11053198, - 10603773, - 10129735, - 9629735, - 9102352, - 8546087, - 7959357, - 7340494, - 6687738, - 5999234, - 5273023, - 4507040, - 3699107, - 2846927, - 1948077, - 1000000 - }; - - int actualValues[] = habit.getScores().getAllValues(1); - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getTodayValue() - { - toggleRepetitions(0, 20); - assertThat(habit.getScores().getTodayValue(), equalTo(12629351)); - } - - @Test - public void test_getValue() - { - toggleRepetitions(0, 20); - - int expectedValues[] = { - 12629351, - 12266245, - 11883254, - 11479288, - 11053198, - 10603773, - 10129735, - 9629735, - 9102352, - 8546087, - 7959357, - 7340494, - 6687738, - 5999234, - 5273023, - 4507040, - 3699107, - 2846927, - 1948077, - 1000000 - }; - - long current = DateUtils.getStartOfToday(); - for (int expectedValue : expectedValues) - { - assertThat(habit.getScores().getValue(current), - equalTo(expectedValue)); - current -= DateUtils.millisecondsInOneDay; - } - } - - @Test - public void test_invalidateNewerThan() - { - assertThat(habit.getScores().getTodayValue(), equalTo(0)); - - toggleRepetitions(0, 2); - assertThat(habit.getScores().getTodayValue(), equalTo(1948077)); - - habit.setFreqNum(1); - habit.setFreqDen(2); - habit.getScores().invalidateNewerThan(0); - - assertThat(habit.getScores().getTodayValue(), equalTo(1974654)); - } - - @Test - public void test_writeCSV() throws IOException - { - habitFixtures.purgeHabits(habitList); - Habit habit = habitFixtures.createShortHabit(); - - String expectedCSV = "2015-01-16,0.0519\n" + - "2015-01-17,0.1021\n" + - "2015-01-18,0.0986\n" + - "2015-01-19,0.0952\n" + - "2015-01-20,0.1439\n" + - "2015-01-21,0.1909\n" + - "2015-01-22,0.2364\n" + - "2015-01-23,0.2283\n" + - "2015-01-24,0.2205\n" + - "2015-01-25,0.2649\n"; - - StringWriter writer = new StringWriter(); - habit.getScores().writeCSV(writer); - - assertThat(writer.toString(), equalTo(expectedCSV)); - } - private void toggleRepetitions(final int from, final int to) - { - DatabaseUtils.executeAsTransaction(() -> { - long today = DateUtils.getStartOfToday(); - for (int i = from; i < to; i++) - habit - .getRepetitions() - .toggleTimestamp(today - i * DateUtils.millisecondsInOneDay); - }); - } } diff --git a/app/src/main/java/org/isoron/uhabits/BaseComponent.java b/app/src/main/java/org/isoron/uhabits/BaseComponent.java index 2c6c550ea..069caae05 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseComponent.java +++ b/app/src/main/java/org/isoron/uhabits/BaseComponent.java @@ -57,8 +57,6 @@ public interface BaseComponent void inject(ToggleRepetitionTask toggleRepetitionTask); - void inject(BaseDialogFragment baseDialogFragment); - void inject(HabitCardListCache habitCardListCache); void inject(HabitBroadcastReceiver habitBroadcastReceiver); @@ -100,4 +98,6 @@ public interface BaseComponent void inject(AbstractImporter abstractImporter); void inject(HabitsCSVExporter habitsCSVExporter); + + void inject(BaseDialogFragment baseDialogFragment); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java index 775b1b032..f7c712dfb 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -49,11 +49,11 @@ public class Checkmark */ public static final int UNCHECKED = 0; - final Habit habit; + private final Habit habit; - final long timestamp; + private final long timestamp; - final int value; + private final int value; public Checkmark(Habit habit, long timestamp, int value) { @@ -62,6 +62,11 @@ public class Checkmark this.value = value; } + public Habit getHabit() + { + return habit; + } + public long getTimestamp() { return timestamp; diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitList.java b/app/src/main/java/org/isoron/uhabits/models/HabitList.java index 8c8fd8226..946aab86b 100644 --- a/app/src/main/java/org/isoron/uhabits/models/HabitList.java +++ b/app/src/main/java/org/isoron/uhabits/models/HabitList.java @@ -55,7 +55,7 @@ public abstract class HabitList * * @param habit the habit to be inserted */ - public abstract void add(Habit habit); + public abstract void add(@NonNull Habit habit); /** * Returns the total number of unarchived habits. @@ -87,6 +87,7 @@ public abstract class HabitList * @param id the id of the habit * @return the habit, or null if none exist */ + @Nullable public abstract Habit getById(long id); /** @@ -136,7 +137,7 @@ public abstract class HabitList * @param h the habit * @return the index of the habit, or -1 if not in the list */ - public abstract int indexOf(Habit h); + public abstract int indexOf(@NonNull Habit h); /** * Removes the given habit from the list. @@ -173,7 +174,7 @@ public abstract class HabitList * * @param habit the habit that has been modified. */ - public void update(Habit habit) + public void update(@NonNull Habit habit) { update(Collections.singletonList(habit)); } @@ -187,7 +188,7 @@ public abstract class HabitList * @param out the writer that will receive the result * @throws IOException if write operations fail */ - public void writeCSV(Writer out) throws IOException + public void writeCSV(@NonNull Writer out) throws IOException { String header[] = { "Position", diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index 051825805..aa0511c0a 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -189,9 +189,9 @@ public abstract class RepetitionList add(rep); } -// habit.getScores().invalidateNewerThan(timestamp); -// habit.getCheckmarks().invalidateNewerThan(timestamp); -// habit.getStreaks().invalidateNewerThan(timestamp); + habit.getScores().invalidateNewerThan(timestamp); + habit.getCheckmarks().invalidateNewerThan(timestamp); + habit.getStreaks().invalidateNewerThan(timestamp); return rep; } } diff --git a/app/src/main/java/org/isoron/uhabits/models/Score.java b/app/src/main/java/org/isoron/uhabits/models/Score.java index 5943950fe..2ad7b5aae 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Score.java +++ b/app/src/main/java/org/isoron/uhabits/models/Score.java @@ -35,12 +35,12 @@ public class Score * Timestamp of the day to which this score applies. Time of day should be * midnight (UTC). */ - private Long timestamp; + private final Long timestamp; /** * Value of the score. */ - private Integer value; + private final Integer value; /** * Maximum score value attainable by any habit. @@ -86,6 +86,11 @@ public class Score return score; } + public int compareNewer(Score other) + { + return Long.signum(this.getTimestamp() - other.getTimestamp()); + } + public Habit getHabit() { return habit; diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java index 7b0749a54..ec4ae3d81 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -19,19 +19,17 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.activeandroid.Cache; - import org.isoron.uhabits.utils.DateUtils; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; -import java.util.Date; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -55,39 +53,7 @@ public abstract class ScoreList observable = new ModelObservable(); } - /** - * Returns the values of all the scores, from day of the first repetition - * until today, grouped in chunks of specified size. - *

- * If the group size is one, then the value of each score is returned - * individually. If the group is, for example, seven, then the days are - * grouped in groups of seven consecutive days. - *

- * The values are returned in an array of integers, with one entry for each - * group of days in the interval. This value corresponds to the average of - * the scores for the days inside the group. The first entry corresponds to - * the ending of the interval (that is, the most recent group of days). The - * last entry corresponds to the beginning of the interval. As usual, the - * time of the day for the timestamps should be midnight (UTC). The - * endpoints of the interval are included. - *

- * The values are returned in an integer array. There is one entry for each - * day inside the interval. The first entry corresponds to today, while the - * last entry corresponds to the day of the oldest repetition. - * - * @param divisor the size of the groups - * @return array of values, with one entry for each group of days - */ - @NonNull - public int[] getAllValues(long divisor) - { - Repetition oldestRep = habit.getRepetitions().getOldest(); - if (oldestRep == null) return new int[0]; - - long fromTimestamp = oldestRep.getTimestamp(); - long toTimestamp = DateUtils.getStartOfToday(); - return getValues(fromTimestamp, toTimestamp, divisor); - } + public abstract List getAll(); public ModelObservable getObservable() { @@ -112,6 +78,14 @@ public abstract class ScoreList */ public abstract int getValue(long timestamp); + public List groupBy(DateUtils.TruncateField field) + { + HashMap> groups = getGroupedValues(field); + List scores = groupsToAvgScores(groups); + Collections.sort(scores, (s1, s2) -> s2.compareNewer(s1)); + return scores; + } + /** * Marks all scores that have timestamp equal to or newer than the given * timestamp as invalid. Any following getValue calls will trigger the @@ -124,29 +98,15 @@ public abstract class ScoreList public void writeCSV(Writer out) throws IOException { computeAll(); - SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); - String query = - "select timestamp, score from score where habit = ? order by timestamp"; - String params[] = {habit.getId().toString()}; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if (!cursor.moveToFirst()) return; - - do + for (Score s : getAll()) { - String timestamp = dateFormat.format(new Date(cursor.getLong(0))); - String score = String.format("%.4f", - ((float) cursor.getInt(1)) / Score.MAX_VALUE); + String timestamp = dateFormat.format(s.getTimestamp()); + String score = + String.format("%.4f", ((float) s.getValue()) / Score.MAX_VALUE); out.write(String.format("%s,%s\n", timestamp, score)); - - } while (cursor.moveToNext()); - - cursor.close(); - out.close(); + } } protected abstract void add(List scores); @@ -175,7 +135,7 @@ public abstract class ScoreList long newestTimestamp = 0; Score newest = getNewestComputed(); - if(newest != null) + if (newest != null) { newestValue = newest.getValue(); newestTimestamp = newest.getTimestamp(); @@ -218,10 +178,29 @@ public abstract class ScoreList * @param timestamp the timestamp for the day * @return the score for the day */ + @Nullable protected abstract Score get(long timestamp); + @NonNull + private HashMap> getGroupedValues(DateUtils.TruncateField field) + { + HashMap> groups = new HashMap<>(); + + for (Score s : getAll()) + { + long groupTimestamp = DateUtils.truncate(field, s.getTimestamp()); + + if (!groups.containsKey(groupTimestamp)) + groups.put(groupTimestamp, new ArrayList<>()); + + groups.get(groupTimestamp).add((long) s.getValue()); + } + + return groups; + } + /** - * Returns the most recent score that was already computed. + * Returns the most recent score that has already been computed. *

* If no score has been computed yet, returns null. * @@ -230,13 +209,23 @@ public abstract class ScoreList @Nullable protected abstract Score getNewestComputed(); - /** - * Same as getAllValues(long), but using a specified interval. - * - * @param from beginning of the interval (included) - * @param to end of the interval (included) - * @param divisor size of the groups - * @return array of values, with one entry for each group of days - */ - protected abstract int[] getValues(long from, long to, long divisor); + + @NonNull + private List groupsToAvgScores(HashMap> groups) + { + List scores = new LinkedList<>(); + + for (Long timestamp : groups.keySet()) + { + long meanValue = 0L; + ArrayList groupValues = groups.get(timestamp); + + for (Long v : groupValues) meanValue += v; + meanValue /= groupValues.size(); + + scores.add(new Score(habit, timestamp, (int) meanValue)); + } + + return scores; + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/StreakList.java b/app/src/main/java/org/isoron/uhabits/models/StreakList.java index 19fc4718c..870a4f16e 100644 --- a/app/src/main/java/org/isoron/uhabits/models/StreakList.java +++ b/app/src/main/java/org/isoron/uhabits/models/StreakList.java @@ -48,6 +48,7 @@ public abstract class StreakList public abstract List getAll(); + @NonNull public List getBest(int limit) { List streaks = getAll(); @@ -57,8 +58,10 @@ public abstract class StreakList return streaks; } + @Nullable public abstract Streak getNewestComputed(); + @NonNull public ModelObservable getObservable() { return observable; @@ -89,7 +92,7 @@ public abstract class StreakList * @return the list of streaks. */ @NonNull - protected List checkmarksToStreaks(Long beginning, int[] checks) + protected List checkmarksToStreaks(long beginning, int[] checks) { ArrayList transitions = getTransitions(beginning, checks); @@ -130,7 +133,7 @@ public abstract class StreakList * @return the list of transitions */ @NonNull - protected ArrayList getTransitions(Long beginning, int[] checks) + protected ArrayList getTransitions(long beginning, int[] checks) { long day = DateUtils.millisecondsInOneDay; long current = beginning; @@ -152,7 +155,7 @@ public abstract class StreakList return list; } - protected abstract void insert(List streaks); + protected abstract void insert(@NonNull List streaks); protected abstract void removeNewestComputed(); } diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java index 389d7f59e..baef4ae40 100644 --- a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java @@ -50,7 +50,7 @@ public class MemoryModelFactory implements ModelFactory @Override public ScoreList buildScoreList(Habit habit) { - return null; + return new MemoryScoreList(habit); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java new file mode 100644 index 000000000..4dac68e3a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java @@ -0,0 +1,96 @@ +/* + * 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.models.memory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Score; +import org.isoron.uhabits.models.ScoreList; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class MemoryScoreList extends ScoreList +{ + List list; + + public MemoryScoreList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public int getValue(long timestamp) + { + Score s = get(timestamp); + if (s != null) return s.getValue(); + return 0; + } + + @Override + public void invalidateNewerThan(long timestamp) + { + List discard = new LinkedList<>(); + + for (Score s : list) + if (s.getTimestamp() >= timestamp) discard.add(s); + + list.removeAll(discard); + } + + @Override + @NonNull + public List getAll() + { + computeAll(); + return new LinkedList<>(list); + } + + @Override + protected void add(List scores) + { + list.addAll(scores); + Collections.sort(list, + (s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp())); + } + + @Override + @Nullable + protected Score get(long timestamp) + { + computeAll(); + for (Score s : list) + if (s.getTimestamp() == timestamp) return s; + + return null; + } + + @Nullable + @Override + protected Score getNewestComputed() + { + if(list.isEmpty()) return null; + return list.get(0); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java index f3647e4d6..ba059e938 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java @@ -106,6 +106,7 @@ public class SQLiteCheckmarkList extends CheckmarkList .limit(1) .executeSingle(); + if(record == null) return null; return record.toCheckmark(); } diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java index 2a8f33b91..34e97104e 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java @@ -54,7 +54,7 @@ public class SQLiteHabitList extends HabitList } @Override - public void add(Habit habit) + public void add(@NonNull Habit habit) { if(cache.containsValue(habit)) throw new RuntimeException("habit already in cache"); @@ -132,7 +132,7 @@ public class SQLiteHabitList extends HabitList } @Override - public int indexOf(Habit h) + public int indexOf(@NonNull Habit h) { HabitRecord record = HabitRecord.get(h.getId()); if (record == null) return -1; diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java index e8d1603f3..63b68922c 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java @@ -19,7 +19,6 @@ package org.isoron.uhabits.models.sqlite; -import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import android.support.annotation.NonNull; @@ -32,11 +31,10 @@ import com.activeandroid.query.Select; import com.activeandroid.util.SQLiteUtils; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Repetition; import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.ScoreList; -import org.isoron.uhabits.utils.DateUtils; +import java.util.LinkedList; import java.util.List; /** @@ -73,11 +71,25 @@ public class SQLiteScoreList extends ScoreList .execute(); } + @Override + @NonNull + public List getAll() + { + List records = select().execute(); + List scores = new LinkedList<>(); + + for(ScoreRecord rec : records) + scores.add(rec.toScore()); + + return scores; + } + @Nullable @Override protected Score getNewestComputed() { ScoreRecord record = select().limit(1).executeSingle(); + if(record == null) return null; return record.toScore(); } @@ -85,55 +97,15 @@ public class SQLiteScoreList extends ScoreList @Nullable protected Score get(long timestamp) { - Repetition oldestRep = habit.getRepetitions().getOldest(); - if (oldestRep == null) return null; - compute(oldestRep.getTimestamp(), timestamp); + computeAll(); ScoreRecord record = select().where("timestamp = ?", timestamp).executeSingle(); + if(record == null) return null; return record.toScore(); } - @Override - @NonNull - protected int[] getValues(long from, long to, long divisor) - { - compute(from, to); - - divisor *= DateUtils.millisecondsInOneDay; - Long offset = to + divisor; - - String query = - "select ((timestamp - ?) / ?) as time, avg(score) from Score " + - "where habit = ? and timestamp >= ? and timestamp <= ? " + - "group by time order by time desc"; - - String params[] = { - offset.toString(), - Long.toString(divisor), - habit.getId().toString(), - Long.toString(from), - Long.toString(to) - }; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if (!cursor.moveToFirst()) return new int[0]; - - int k = 0; - int[] scores = new int[cursor.getCount()]; - - do - { - scores[k++] = (int) cursor.getFloat(1); - } while (cursor.moveToNext()); - - cursor.close(); - return scores; - } - @Override protected void add(List scores) { diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java index e44daa300..410a1288e 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java @@ -19,6 +19,9 @@ package org.isoron.uhabits.models.sqlite; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + import com.activeandroid.query.Delete; import com.activeandroid.query.Select; @@ -57,8 +60,9 @@ public class SQLiteStreakList extends StreakList @Override public Streak getNewestComputed() { - rebuild(); - return getNewestRecord().toStreak(); + StreakRecord newestRecord = getNewestRecord(); + if(newestRecord == null) return null; + return newestRecord.toStreak(); } @Override @@ -73,6 +77,7 @@ public class SQLiteStreakList extends StreakList observable.notifyListeners(); } + @Nullable private StreakRecord getNewestRecord() { return new Select() @@ -84,7 +89,7 @@ public class SQLiteStreakList extends StreakList } @Override - protected void insert(List streaks) + protected void insert(@NonNull List streaks) { DatabaseUtils.executeAsTransaction(() -> { for (Streak streak : streaks) @@ -96,6 +101,7 @@ public class SQLiteStreakList extends StreakList }); } + @NonNull private List recordsToStreaks(List records) { LinkedList streaks = new LinkedList<>(); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java index aecb2a5e4..07d50342b 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java @@ -36,6 +36,7 @@ import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.commands.CommandRunner; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.utils.ColorUtils; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.Preferences; @@ -50,17 +51,23 @@ import butterknife.OnItemSelected; public abstract class BaseDialogFragment extends AppCompatDialogFragment { + @Nullable protected Habit originalHabit; + @Nullable protected Habit modifiedHabit; + @Nullable protected BaseDialogHelper helper; @Inject - Preferences prefs; + protected Preferences prefs; @Inject - CommandRunner commandRunner; + protected CommandRunner commandRunner; + + @Inject + protected HabitList habitList; @Override public View onCreateView(LayoutInflater inflater, diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java index 2541100d6..d4e9beb76 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java @@ -21,20 +21,13 @@ package org.isoron.uhabits.ui.habits.edit; import android.os.Bundle; -import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.EditHabitCommand; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.HabitList; - -import javax.inject.Inject; public class EditHabitDialogFragment extends BaseDialogFragment { - @Inject - HabitList habitList; - public static EditHabitDialogFragment newInstance(long habitId) { EditHabitDialogFragment frag = new EditHabitDialogFragment(); @@ -53,8 +46,6 @@ public class EditHabitDialogFragment extends BaseDialogFragment @Override protected void initializeHabits() { - HabitsApplication.getComponent().inject(this); - Long habitId = (Long) getArguments().get("habitId"); if (habitId == null) throw new IllegalArgumentException("habitId must be specified"); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitScoreView.java index 0b7cf2611..377edb83a 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitScoreView.java @@ -27,8 +27,10 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; +import android.util.Log; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; @@ -42,6 +44,8 @@ import org.isoron.uhabits.utils.InterfaceUtils; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.LinkedList; +import java.util.List; import java.util.Random; public class HabitScoreView extends ScrollableDataView @@ -86,7 +90,7 @@ public class HabitScoreView extends ScrollableDataView private int gridColor; @Nullable - private int[] scores; + private List scores; private int primaryColor; @@ -134,7 +138,11 @@ public class HabitScoreView extends ScrollableDataView else { if (habit == null) return; - scores = habit.getScores().getAllValues(bucketSize); + if (bucketSize == 1) + scores = habit.getScores().getAll(); + else + scores = habit.getScores().groupBy(getTruncateField()); + createColors(); } @@ -285,14 +293,20 @@ public class HabitScoreView extends ScrollableDataView private void generateRandomData() { Random random = new Random(); - scores = new int[100]; - scores[0] = Score.MAX_VALUE / 2; + scores = new LinkedList<>(); + + int previous = Score.MAX_VALUE / 2; + long timestamp = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; for (int i = 1; i < 100; i++) { int step = Score.MAX_VALUE / 10; - scores[i] = scores[i - 1] + random.nextInt(step * 2) - step; - scores[i] = Math.max(0, Math.min(Score.MAX_VALUE, scores[i])); + int current = previous + random.nextInt(step * 2) - step; + current = Math.max(0, Math.min(Score.MAX_VALUE, current)); + scores.add(new Score(habit, timestamp, current)); + previous = current; + timestamp -= day; } } @@ -326,6 +340,38 @@ public class HabitScoreView extends ScrollableDataView return maxMonthWidth; } + @NonNull + private DateUtils.TruncateField getTruncateField() + { + DateUtils.TruncateField field; + + switch (bucketSize) + { + case 7: + field = DateUtils.TruncateField.WEEK_NUMBER; + break; + + case 365: + field = DateUtils.TruncateField.YEAR; + break; + + case 92: + field = DateUtils.TruncateField.QUARTER; + break; + + default: + Log.e("HabitScoreView", + String.format("Unknown bucket size: %d", bucketSize)); + // continue to case 31 + + case 31: + field = DateUtils.TruncateField.MONTH; + break; + } + + return field; + } + private void init() { createPaints(); @@ -413,7 +459,7 @@ public class HabitScoreView extends ScrollableDataView { int score = 0; int offset = nColumns - k - 1 + getDataOffset(); - if (offset < scores.length) score = scores[offset]; + if (offset < scores.size()) score = scores.get(offset).getValue(); double relativeScore = ((double) score) / Score.MAX_VALUE; int height = (int) (columnHeight * relativeScore); diff --git a/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java b/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java index a93a38c8b..c28128387 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java @@ -33,58 +33,23 @@ import java.util.TimeZone; public abstract class DateUtils { - public static long millisecondsInOneDay = 24 * 60 * 60 * 1000; public static int ALL_WEEK_DAYS = 127; private static Long fixedLocalTime = null; - public static long getLocalTime() - { - if(fixedLocalTime != null) return fixedLocalTime; - - TimeZone tz = TimeZone.getDefault(); - long now = new Date().getTime(); - return now + tz.getOffset(now); - } - - public static void setFixedLocalTime(Long timestamp) - { - fixedLocalTime = timestamp; - } - - public static long toLocalTime(long timestamp) - { - TimeZone tz = TimeZone.getDefault(); - long now = new Date(timestamp).getTime(); - return now + tz.getOffset(now); - } - - public static long getStartOfDay(long timestamp) - { - return (timestamp / millisecondsInOneDay) * millisecondsInOneDay; - } - - public static GregorianCalendar getStartOfTodayCalendar() - { - return getCalendar(getStartOfToday()); - } - - public static GregorianCalendar getCalendar(long timestamp) - { - GregorianCalendar day = new GregorianCalendar(TimeZone.getTimeZone("GMT")); - day.setTimeInMillis(timestamp); - return day; - } + /** + * Number of milliseconds in one day. + */ + public static long millisecondsInOneDay = 24 * 60 * 60 * 1000; - public static int getWeekday(long timestamp) + public static String formatHeaderDate(GregorianCalendar day) { - GregorianCalendar day = getCalendar(timestamp); - return day.get(GregorianCalendar.DAY_OF_WEEK) % 7; - } + String dayOfMonth = + Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)); + String dayOfWeek = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, + GregorianCalendar.SHORT, Locale.getDefault()); - public static long getStartOfToday() - { - return getStartOfDay(DateUtils.getLocalTime()); + return dayOfWeek + "\n" + dayOfMonth; } public static String formatTime(Context context, int hours, int minutes) @@ -98,74 +63,77 @@ public abstract class DateUtils return df.format(date); } - public static SimpleDateFormat getDateFormat(String skeleton) + public static String formatWeekdayList(Context context, boolean weekday[]) { - String pattern; - Locale locale = Locale.getDefault(); + String shortDayNames[] = getShortDayNames(); + String longDayNames[] = getLongDayNames(); + StringBuilder buffer = new StringBuilder(); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) - pattern = DateFormat.getBestDateTimePattern(locale, skeleton); - else - pattern = skeleton; + int count = 0; + int first = 0; + boolean isFirst = true; + for (int i = 0; i < 7; i++) + { + if (weekday[i]) + { + if (isFirst) first = i; + else buffer.append(", "); - SimpleDateFormat format = new SimpleDateFormat(pattern, locale); - format.setTimeZone(TimeZone.getTimeZone("UTC")); + buffer.append(shortDayNames[i]); + isFirst = false; + count++; + } + } - return format; + if (count == 1) return longDayNames[first]; + if (count == 2 && weekday[0] && weekday[1]) + return context.getString(R.string.weekends); + if (count == 5 && !weekday[0] && !weekday[1]) + return context.getString(R.string.any_weekday); + if (count == 7) return context.getString(R.string.any_day); + return buffer.toString(); } - public static SimpleDateFormat getCSVDateFormat() + public static SimpleDateFormat getBackupDateFormat() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + SimpleDateFormat dateFormat = + new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat; } - public static SimpleDateFormat getBackupDateFormat() + public static SimpleDateFormat getCSVDateFormat() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US); + SimpleDateFormat dateFormat = + new SimpleDateFormat("yyyy-MM-dd", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat; } - public static String formatHeaderDate(GregorianCalendar day) - { - String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)); - String dayOfWeek = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, - GregorianCalendar.SHORT, Locale.getDefault()); - - return dayOfWeek + "\n" + dayOfMonth; - } - - public static int differenceInDays(Date from, Date to) + public static GregorianCalendar getCalendar(long timestamp) { - long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime()); - return (int) (milliseconds / millisecondsInOneDay); + GregorianCalendar day = + new GregorianCalendar(TimeZone.getTimeZone("GMT")); + day.setTimeInMillis(timestamp); + return day; } - public static String[] getShortDayNames() + public static SimpleDateFormat getDateFormat(String skeleton) { - return getDayNames(GregorianCalendar.SHORT); - } + String pattern; + Locale locale = Locale.getDefault(); - public static String[] getLongDayNames() - { - return getDayNames(GregorianCalendar.LONG); - } + if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) + pattern = DateFormat.getBestDateTimePattern(locale, skeleton); + else pattern = skeleton; + SimpleDateFormat format = new SimpleDateFormat(pattern, locale); + format.setTimeZone(TimeZone.getTimeZone("UTC")); - /** - * Throughout the code, it is assumed that the weekdays are numbered from 0 (Saturday) to 6 - * (Friday). In the Java Calendar they are numbered from 1 (Sunday) to 7 (Saturday). This - * function converts from Java to our internal representation. - * - * @return weekday number in the internal interpretation - */ - public static int javaWeekdayToLoopWeekday(int number) - { - return number % 7; + return format; } public static String[] getDayNames(int format) @@ -178,13 +146,22 @@ public abstract class DateUtils for (int i = 0; i < wdays.length; i++) { wdays[i] = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, - Locale.getDefault()); + Locale.getDefault()); day.add(GregorianCalendar.DAY_OF_MONTH, 1); } return wdays; } + public static long getLocalTime() + { + if (fixedLocalTime != null) return fixedLocalTime; + + TimeZone tz = TimeZone.getDefault(); + long now = new Date().getTime(); + return now + tz.getOffset(now); + } + /** * @return array with weekday names starting according to locale settings, * e.g. [Mo,Di,Mi,Do,Fr,Sa,So] in Europe @@ -194,10 +171,12 @@ public abstract class DateUtils String[] days = new String[7]; Calendar calendar = new GregorianCalendar(); - calendar.set(GregorianCalendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + calendar.set(GregorianCalendar.DAY_OF_WEEK, + calendar.getFirstDayOfWeek()); for (int i = 0; i < days.length; i++) { - days[i] = calendar.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, + days[i] = + calendar.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, Locale.getDefault()); calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); } @@ -206,14 +185,15 @@ public abstract class DateUtils } /** - * @return array with week days numbers starting according to locale settings, - * e.g. [2,3,4,5,6,7,1] in Europe + * @return array with week days numbers starting according to locale + * settings, e.g. [2,3,4,5,6,7,1] in Europe */ public static Integer[] getLocaleWeekdayList() { Integer[] dayNumbers = new Integer[7]; Calendar calendar = new GregorianCalendar(); - calendar.set(GregorianCalendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + calendar.set(GregorianCalendar.DAY_OF_WEEK, + calendar.getFirstDayOfWeek()); for (int i = 0; i < dayNumbers.length; i++) { dayNumbers[i] = calendar.get(GregorianCalendar.DAY_OF_WEEK); @@ -222,33 +202,48 @@ public abstract class DateUtils return dayNumbers; } - public static String formatWeekdayList(Context context, boolean weekday[]) + public static String[] getLongDayNames() { - String shortDayNames[] = getShortDayNames(); - String longDayNames[] = getLongDayNames(); - StringBuilder buffer = new StringBuilder(); + return getDayNames(GregorianCalendar.LONG); + } - int count = 0; - int first = 0; - boolean isFirst = true; - for(int i = 0; i < 7; i++) - { - if(weekday[i]) - { - if(isFirst) first = i; - else buffer.append(", "); + public static String[] getShortDayNames() + { + return getDayNames(GregorianCalendar.SHORT); + } - buffer.append(shortDayNames[i]); - isFirst = false; - count++; - } - } + public static long getStartOfDay(long timestamp) + { + return (timestamp / millisecondsInOneDay) * millisecondsInOneDay; + } - if(count == 1) return longDayNames[first]; - if(count == 2 && weekday[0] && weekday[1]) return context.getString(R.string.weekends); - if(count == 5 && !weekday[0] && !weekday[1]) return context.getString(R.string.any_weekday); - if(count == 7) return context.getString(R.string.any_day); - return buffer.toString(); + public static long getStartOfToday() + { + return getStartOfDay(DateUtils.getLocalTime()); + } + + public static GregorianCalendar getStartOfTodayCalendar() + { + return getCalendar(getStartOfToday()); + } + + public static int getWeekday(long timestamp) + { + GregorianCalendar day = getCalendar(timestamp); + return day.get(GregorianCalendar.DAY_OF_WEEK) % 7; + } + + /** + * Throughout the code, it is assumed that the weekdays are numbered from 0 + * (Saturday) to 6 (Friday). In the Java Calendar they are numbered from 1 + * (Sunday) to 7 (Saturday). This function converts from Java to our + * internal representation. + * + * @return weekday number in the internal interpretation + */ + public static int javaWeekdayToLoopWeekday(int number) + { + return number % 7; } public static Integer packWeekdayList(boolean weekday[]) @@ -256,26 +251,77 @@ public abstract class DateUtils int list = 0; int current = 1; - for(int i = 0; i < 7; i++) + for (int i = 0; i < 7; i++) { - if(weekday[i]) list |= current; + if (weekday[i]) list |= current; current = current << 1; } return list; } + public static void setFixedLocalTime(Long timestamp) + { + fixedLocalTime = timestamp; + } + + public static long toLocalTime(long timestamp) + { + TimeZone tz = TimeZone.getDefault(); + long now = new Date(timestamp).getTime(); + return now + tz.getOffset(now); + } + + public static Long truncate(TruncateField field, long timestamp) + { + GregorianCalendar cal = DateUtils.getCalendar(timestamp); + + switch (field) + { + case MONTH: + cal.set(Calendar.DAY_OF_MONTH, 1); + return cal.getTimeInMillis(); + + case WEEK_NUMBER: + int firstWeekday = cal.getFirstDayOfWeek(); + int weekday = cal.get(Calendar.DAY_OF_WEEK); + int delta = weekday - firstWeekday; + if (delta < 0) delta += 7; + cal.add(Calendar.DAY_OF_YEAR, -delta); + return cal.getTimeInMillis(); + + case QUARTER: + int quarter = cal.get(Calendar.MONTH) / 3; + cal.set(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.MONTH, quarter * 3); + return cal.getTimeInMillis(); + + case YEAR: + cal.set(Calendar.MONTH, Calendar.JANUARY); + cal.set(Calendar.DAY_OF_MONTH, 1); + return cal.getTimeInMillis(); + + default: + throw new IllegalArgumentException(); + } + } + public static boolean[] unpackWeekdayList(int list) { boolean[] weekday = new boolean[7]; int current = 1; - for(int i = 0; i < 7; i++) + for (int i = 0; i < 7; i++) { - if((list & current) != 0) weekday[i] = true; + if ((list & current) != 0) weekday[i] = true; current = current << 1; } return weekday; } + + public enum TruncateField + { + MONTH, WEEK_NUMBER, YEAR, QUARTER + } } diff --git a/app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java b/app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java new file mode 100644 index 000000000..9fdc2d48a --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/models/ScoreListTest.java @@ -0,0 +1,195 @@ +/* + * 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.models; + +import org.isoron.uhabits.BaseUnitTest; +import org.isoron.uhabits.utils.DateUtils; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; + +public class ScoreListTest extends BaseUnitTest +{ + private Habit habit; + + @Override + @Before + public void setUp() + { + super.setUp(); + habit = fixtures.createEmptyHabit(); + } + + @Test + public void test_getAll() + { + toggleRepetitions(0, 20); + + int expectedValues[] = { + 12629351, + 12266245, + 11883254, + 11479288, + 11053198, + 10603773, + 10129735, + 9629735, + 9102352, + 8546087, + 7959357, + 7340494, + 6687738, + 5999234, + 5273023, + 4507040, + 3699107, + 2846927, + 1948077, + 1000000 + }; + + int actualValues[] = new int[expectedValues.length]; + + int i = 0; + for (Score s : habit.getScores().getAll()) + actualValues[i++] = s.getValue(); + + assertThat(actualValues, equalTo(expectedValues)); + } + + @Test + public void test_getTodayValue() + { + toggleRepetitions(0, 20); + assertThat(habit.getScores().getTodayValue(), equalTo(12629351)); + } + + @Test + public void test_getValue() + { + toggleRepetitions(0, 20); + + int expectedValues[] = { + 12629351, + 12266245, + 11883254, + 11479288, + 11053198, + 10603773, + 10129735, + 9629735, + 9102352, + 8546087, + 7959357, + 7340494, + 6687738, + 5999234, + 5273023, + 4507040, + 3699107, + 2846927, + 1948077, + 1000000 + }; + + long current = DateUtils.getStartOfToday(); + for (int expectedValue : expectedValues) + { + assertThat(habit.getScores().getValue(current), + equalTo(expectedValue)); + current -= DateUtils.millisecondsInOneDay; + } + } + + @Test + public void test_groupBy() + { + Habit habit = fixtures.createLongHabit(); + List list = + habit.getScores().groupBy(DateUtils.TruncateField.MONTH); + + assertThat(list.size(), equalTo(5)); + assertThat(list.get(0).getValue(), equalTo(14634077)); + assertThat(list.get(1).getValue(), equalTo(12969133)); + assertThat(list.get(2).getValue(), equalTo(10595391)); + } + + @Test + public void test_invalidateNewerThan() + { + assertThat(habit.getScores().getTodayValue(), equalTo(0)); + + toggleRepetitions(0, 2); + assertThat(habit.getScores().getTodayValue(), equalTo(1948077)); + + habit.setFreqNum(1); + habit.setFreqDen(2); + habit.getScores().invalidateNewerThan(0); + + assertThat(habit.getScores().getTodayValue(), equalTo(1974654)); + } + + @Test + public void test_writeCSV() throws IOException + { + Habit habit = fixtures.createShortHabit(); + + String expectedCSV = "2015-01-25,0.2649\n" + + "2015-01-24,0.2205\n" + + "2015-01-23,0.2283\n" + + "2015-01-22,0.2364\n" + + "2015-01-21,0.1909\n" + + "2015-01-20,0.1439\n" + + "2015-01-19,0.0952\n" + + "2015-01-18,0.0986\n" + + "2015-01-17,0.1021\n" + + "2015-01-16,0.0519\n"; + + StringWriter writer = new StringWriter(); + habit.getScores().writeCSV(writer); + + assertThat(writer.toString(), equalTo(expectedCSV)); + } + + private void log(List list) + { + SimpleDateFormat df = DateUtils.getCSVDateFormat(); + for (Score s : list) + log("%s %d", df.format(new Date(s.getTimestamp())), s.getValue()); + } + + private void toggleRepetitions(final int from, final int to) + { + RepetitionList reps = habit.getRepetitions(); + long today = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; + + for (int i = from; i < to; i++) + reps.toggleTimestamp(today - i * day); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java b/app/src/test/java/org/isoron/uhabits/models/ScoreTest.java similarity index 71% rename from app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java rename to app/src/test/java/org/isoron/uhabits/models/ScoreTest.java index bc0a31c31..34b400150 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java +++ b/app/src/test/java/org/isoron/uhabits/models/ScoreTest.java @@ -17,24 +17,16 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.models; +package org.isoron.uhabits.models; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseAndroidTest; -import org.isoron.uhabits.models.Checkmark; -import org.isoron.uhabits.models.Score; +import org.isoron.uhabits.BaseUnitTest; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; -@RunWith(AndroidJUnit4.class) -@SmallTest -public class ScoreTest extends BaseAndroidTest +public class ScoreTest extends BaseUnitTest { @Override @Before @@ -50,20 +42,22 @@ public class ScoreTest extends BaseAndroidTest assertThat(Score.compute(1, 0, checkmark), equalTo(0)); assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387)); assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775)); - assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478)); + assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), + equalTo(18259478)); checkmark = Checkmark.CHECKED_IMPLICITLY; assertThat(Score.compute(1, 0, checkmark), equalTo(0)); assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387)); assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775)); - assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478)); + assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), + equalTo(18259478)); checkmark = Checkmark.CHECKED_EXPLICITLY; assertThat(Score.compute(1, 0, checkmark), equalTo(1000000)); assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387)); assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775)); - assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo( - Score.MAX_VALUE)); + assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), + equalTo(Score.MAX_VALUE)); } @Test @@ -71,15 +65,19 @@ public class ScoreTest extends BaseAndroidTest { int checkmark = Checkmark.CHECKED_EXPLICITLY; assertThat(Score.compute(1 / 3.0, 0, checkmark), equalTo(1000000)); - assertThat(Score.compute(1 / 3.0, 5000000, checkmark), equalTo(5916180)); - assertThat(Score.compute(1 / 3.0, 10000000, checkmark), equalTo(10832360)); - assertThat(Score.compute(1 / 3.0, Score.MAX_VALUE, checkmark), equalTo( - Score.MAX_VALUE)); + assertThat(Score.compute(1 / 3.0, 5000000, checkmark), + equalTo(5916180)); + assertThat(Score.compute(1 / 3.0, 10000000, checkmark), + equalTo(10832360)); + assertThat(Score.compute(1 / 3.0, Score.MAX_VALUE, checkmark), + equalTo(Score.MAX_VALUE)); assertThat(Score.compute(1 / 7.0, 0, checkmark), equalTo(1000000)); - assertThat(Score.compute(1 / 7.0, 5000000, checkmark), equalTo(5964398)); - assertThat(Score.compute(1 / 7.0, 10000000, checkmark), equalTo(10928796)); - assertThat(Score.compute(1 / 7.0, Score.MAX_VALUE, checkmark), equalTo( - Score.MAX_VALUE)); + assertThat(Score.compute(1 / 7.0, 5000000, checkmark), + equalTo(5964398)); + assertThat(Score.compute(1 / 7.0, 10000000, checkmark), + equalTo(10928796)); + assertThat(Score.compute(1 / 7.0, Score.MAX_VALUE, checkmark), + equalTo(Score.MAX_VALUE)); } } diff --git a/app/src/test/java/org/isoron/uhabits/utils/DateUtilsTest.java b/app/src/test/java/org/isoron/uhabits/utils/DateUtilsTest.java new file mode 100644 index 000000000..b3b26ecbc --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/utils/DateUtilsTest.java @@ -0,0 +1,146 @@ +/* + * 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.utils; + +import org.isoron.uhabits.BaseUnitTest; +import org.junit.Test; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; + +public class DateUtilsTest extends BaseUnitTest +{ + @Test + public void testTruncate_dayOfWeek() + { + DateUtils.TruncateField field = DateUtils.TruncateField.WEEK_NUMBER; + + long expected = timestamp(2015, Calendar.JANUARY, 11); + long t0 = timestamp(2015, Calendar.JANUARY, 11); + long t1 = timestamp(2015, Calendar.JANUARY, 16); + long t2 = timestamp(2015, Calendar.JANUARY, 17); + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + + expected = timestamp(2015, Calendar.JANUARY, 18); + t0 = timestamp(2015, Calendar.JANUARY, 18); + t1 = timestamp(2015, Calendar.JANUARY, 19); + t2 = timestamp(2015, Calendar.JANUARY, 24); + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + } + + @Test + public void testTruncate_month() + { + long expected = timestamp(2016, Calendar.JUNE, 1); + long t0 = timestamp(2016, Calendar.JUNE, 1); + long t1 = timestamp(2016, Calendar.JUNE, 15); + long t2 = timestamp(2016, Calendar.JUNE, 20); + + DateUtils.TruncateField field = DateUtils.TruncateField.MONTH; + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + + expected = timestamp(2016, Calendar.DECEMBER, 1); + t0 = timestamp(2016, Calendar.DECEMBER, 1); + t1 = timestamp(2016, Calendar.DECEMBER, 15); + t2 = timestamp(2016, Calendar.DECEMBER, 31); + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + } + + @Test + public void testTruncate_quarter() + { + DateUtils.TruncateField field = DateUtils.TruncateField.QUARTER; + + long expected = timestamp(2016, Calendar.JANUARY, 1); + long t0 = timestamp(2016, Calendar.JANUARY, 20); + long t1 = timestamp(2016, Calendar.FEBRUARY, 15); + long t2 = timestamp(2016, Calendar.MARCH, 30); + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + + expected = timestamp(2016, Calendar.APRIL, 1); + t0 = timestamp(2016, Calendar.APRIL, 1); + t1 = timestamp(2016, Calendar.MAY, 30); + t2 = timestamp(2016, Calendar.JUNE, 20); + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + } + + @Test + public void testTruncate_year() + { + DateUtils.TruncateField field = DateUtils.TruncateField.YEAR; + + long expected = timestamp(2016, Calendar.JANUARY, 1); + long t0 = timestamp(2016, Calendar.JANUARY, 1); + long t1 = timestamp(2016, Calendar.FEBRUARY, 25); + long t2 = timestamp(2016, Calendar.DECEMBER, 31); + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + + expected = timestamp(2017, Calendar.JANUARY, 1); + t0 = timestamp(2017, Calendar.JANUARY, 1); + t1 = timestamp(2017, Calendar.MAY, 30); + t2 = timestamp(2017, Calendar.DECEMBER, 31); + + assertThat(DateUtils.truncate(field, t0), equalTo(expected)); + assertThat(DateUtils.truncate(field, t1), equalTo(expected)); + assertThat(DateUtils.truncate(field, t2), equalTo(expected)); + } + + private void log(long timestamp) + { + DateFormat df = SimpleDateFormat.getDateTimeInstance(); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + log("%s", df.format(new Date(timestamp))); + } + + public long timestamp(int year, int month, int day) + { + GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); + cal.set(year, month, day); + return cal.getTimeInMillis(); + } +}