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();
+ }
+}