Move remaining model tests to JVM; simplify SQLite implementation

pull/145/head
Alinson S. Xavier 9 years ago
parent 78d4f86cab
commit 2b23b36e36

@ -35,6 +35,17 @@ android {
targetCompatibility 1.8 targetCompatibility 1.8
sourceCompatibility 1.8 sourceCompatibility 1.8
} }
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
} }
dependencies { dependencies {

@ -23,174 +23,11 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest; 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 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) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class ScoreListTest extends BaseAndroidTest 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);
});
}
} }

@ -57,8 +57,6 @@ public interface BaseComponent
void inject(ToggleRepetitionTask toggleRepetitionTask); void inject(ToggleRepetitionTask toggleRepetitionTask);
void inject(BaseDialogFragment baseDialogFragment);
void inject(HabitCardListCache habitCardListCache); void inject(HabitCardListCache habitCardListCache);
void inject(HabitBroadcastReceiver habitBroadcastReceiver); void inject(HabitBroadcastReceiver habitBroadcastReceiver);
@ -100,4 +98,6 @@ public interface BaseComponent
void inject(AbstractImporter abstractImporter); void inject(AbstractImporter abstractImporter);
void inject(HabitsCSVExporter habitsCSVExporter); void inject(HabitsCSVExporter habitsCSVExporter);
void inject(BaseDialogFragment baseDialogFragment);
} }

@ -49,11 +49,11 @@ public class Checkmark
*/ */
public static final int UNCHECKED = 0; 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) public Checkmark(Habit habit, long timestamp, int value)
{ {
@ -62,6 +62,11 @@ public class Checkmark
this.value = value; this.value = value;
} }
public Habit getHabit()
{
return habit;
}
public long getTimestamp() public long getTimestamp()
{ {
return timestamp; return timestamp;

@ -55,7 +55,7 @@ public abstract class HabitList
* *
* @param habit the habit to be inserted * @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. * Returns the total number of unarchived habits.
@ -87,6 +87,7 @@ public abstract class HabitList
* @param id the id of the habit * @param id the id of the habit
* @return the habit, or null if none exist * @return the habit, or null if none exist
*/ */
@Nullable
public abstract Habit getById(long id); public abstract Habit getById(long id);
/** /**
@ -136,7 +137,7 @@ public abstract class HabitList
* @param h the habit * @param h the habit
* @return the index of the habit, or -1 if not in the list * @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. * Removes the given habit from the list.
@ -173,7 +174,7 @@ public abstract class HabitList
* *
* @param habit the habit that has been modified. * @param habit the habit that has been modified.
*/ */
public void update(Habit habit) public void update(@NonNull Habit habit)
{ {
update(Collections.singletonList(habit)); update(Collections.singletonList(habit));
} }
@ -187,7 +188,7 @@ public abstract class HabitList
* @param out the writer that will receive the result * @param out the writer that will receive the result
* @throws IOException if write operations fail * @throws IOException if write operations fail
*/ */
public void writeCSV(Writer out) throws IOException public void writeCSV(@NonNull Writer out) throws IOException
{ {
String header[] = { String header[] = {
"Position", "Position",

@ -189,9 +189,9 @@ public abstract class RepetitionList
add(rep); add(rep);
} }
// habit.getScores().invalidateNewerThan(timestamp); habit.getScores().invalidateNewerThan(timestamp);
// habit.getCheckmarks().invalidateNewerThan(timestamp); habit.getCheckmarks().invalidateNewerThan(timestamp);
// habit.getStreaks().invalidateNewerThan(timestamp); habit.getStreaks().invalidateNewerThan(timestamp);
return rep; return rep;
} }
} }

@ -35,12 +35,12 @@ public class Score
* Timestamp of the day to which this score applies. Time of day should be * Timestamp of the day to which this score applies. Time of day should be
* midnight (UTC). * midnight (UTC).
*/ */
private Long timestamp; private final Long timestamp;
/** /**
* Value of the score. * Value of the score.
*/ */
private Integer value; private final Integer value;
/** /**
* Maximum score value attainable by any habit. * Maximum score value attainable by any habit.
@ -86,6 +86,11 @@ public class Score
return score; return score;
} }
public int compareNewer(Score other)
{
return Long.signum(this.getTimestamp() - other.getTimestamp());
}
public Habit getHabit() public Habit getHabit()
{ {
return habit; return habit;

@ -19,19 +19,17 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.DateUtils;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.text.SimpleDateFormat; 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.LinkedList;
import java.util.List; import java.util.List;
@ -55,39 +53,7 @@ public abstract class ScoreList
observable = new ModelObservable(); observable = new ModelObservable();
} }
/** public abstract List<Score> getAll();
* Returns the values of all the scores, from day of the first repetition
* until today, grouped in chunks of specified size.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 ModelObservable getObservable() public ModelObservable getObservable()
{ {
@ -112,6 +78,14 @@ public abstract class ScoreList
*/ */
public abstract int getValue(long timestamp); public abstract int getValue(long timestamp);
public List<Score> groupBy(DateUtils.TruncateField field)
{
HashMap<Long, ArrayList<Long>> groups = getGroupedValues(field);
List<Score> 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 * Marks all scores that have timestamp equal to or newer than the given
* timestamp as invalid. Any following getValue calls will trigger the * 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 public void writeCSV(Writer out) throws IOException
{ {
computeAll(); computeAll();
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
String query = for (Score s : getAll())
"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
{ {
String timestamp = dateFormat.format(new Date(cursor.getLong(0))); String timestamp = dateFormat.format(s.getTimestamp());
String score = String.format("%.4f", String score =
((float) cursor.getInt(1)) / Score.MAX_VALUE); String.format("%.4f", ((float) s.getValue()) / Score.MAX_VALUE);
out.write(String.format("%s,%s\n", timestamp, score)); out.write(String.format("%s,%s\n", timestamp, score));
}
} while (cursor.moveToNext());
cursor.close();
out.close();
} }
protected abstract void add(List<Score> scores); protected abstract void add(List<Score> scores);
@ -175,7 +135,7 @@ public abstract class ScoreList
long newestTimestamp = 0; long newestTimestamp = 0;
Score newest = getNewestComputed(); Score newest = getNewestComputed();
if(newest != null) if (newest != null)
{ {
newestValue = newest.getValue(); newestValue = newest.getValue();
newestTimestamp = newest.getTimestamp(); newestTimestamp = newest.getTimestamp();
@ -218,10 +178,29 @@ public abstract class ScoreList
* @param timestamp the timestamp for the day * @param timestamp the timestamp for the day
* @return the score for the day * @return the score for the day
*/ */
@Nullable
protected abstract Score get(long timestamp); protected abstract Score get(long timestamp);
@NonNull
private HashMap<Long, ArrayList<Long>> getGroupedValues(DateUtils.TruncateField field)
{
HashMap<Long, ArrayList<Long>> 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.
* <p> * <p>
* If no score has been computed yet, returns null. * If no score has been computed yet, returns null.
* *
@ -230,13 +209,23 @@ public abstract class ScoreList
@Nullable @Nullable
protected abstract Score getNewestComputed(); protected abstract Score getNewestComputed();
/**
* Same as getAllValues(long), but using a specified interval. @NonNull
* private List<Score> groupsToAvgScores(HashMap<Long, ArrayList<Long>> groups)
* @param from beginning of the interval (included) {
* @param to end of the interval (included) List<Score> scores = new LinkedList<>();
* @param divisor size of the groups
* @return array of values, with one entry for each group of days for (Long timestamp : groups.keySet())
*/ {
protected abstract int[] getValues(long from, long to, long divisor); long meanValue = 0L;
ArrayList<Long> groupValues = groups.get(timestamp);
for (Long v : groupValues) meanValue += v;
meanValue /= groupValues.size();
scores.add(new Score(habit, timestamp, (int) meanValue));
}
return scores;
}
} }

@ -48,6 +48,7 @@ public abstract class StreakList
public abstract List<Streak> getAll(); public abstract List<Streak> getAll();
@NonNull
public List<Streak> getBest(int limit) public List<Streak> getBest(int limit)
{ {
List<Streak> streaks = getAll(); List<Streak> streaks = getAll();
@ -57,8 +58,10 @@ public abstract class StreakList
return streaks; return streaks;
} }
@Nullable
public abstract Streak getNewestComputed(); public abstract Streak getNewestComputed();
@NonNull
public ModelObservable getObservable() public ModelObservable getObservable()
{ {
return observable; return observable;
@ -89,7 +92,7 @@ public abstract class StreakList
* @return the list of streaks. * @return the list of streaks.
*/ */
@NonNull @NonNull
protected List<Streak> checkmarksToStreaks(Long beginning, int[] checks) protected List<Streak> checkmarksToStreaks(long beginning, int[] checks)
{ {
ArrayList<Long> transitions = getTransitions(beginning, checks); ArrayList<Long> transitions = getTransitions(beginning, checks);
@ -130,7 +133,7 @@ public abstract class StreakList
* @return the list of transitions * @return the list of transitions
*/ */
@NonNull @NonNull
protected ArrayList<Long> getTransitions(Long beginning, int[] checks) protected ArrayList<Long> getTransitions(long beginning, int[] checks)
{ {
long day = DateUtils.millisecondsInOneDay; long day = DateUtils.millisecondsInOneDay;
long current = beginning; long current = beginning;
@ -152,7 +155,7 @@ public abstract class StreakList
return list; return list;
} }
protected abstract void insert(List<Streak> streaks); protected abstract void insert(@NonNull List<Streak> streaks);
protected abstract void removeNewestComputed(); protected abstract void removeNewestComputed();
} }

@ -50,7 +50,7 @@ public class MemoryModelFactory implements ModelFactory
@Override @Override
public ScoreList buildScoreList(Habit habit) public ScoreList buildScoreList(Habit habit)
{ {
return null; return new MemoryScoreList(habit);
} }
@Override @Override

@ -0,0 +1,96 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Score> 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<Score> discard = new LinkedList<>();
for (Score s : list)
if (s.getTimestamp() >= timestamp) discard.add(s);
list.removeAll(discard);
}
@Override
@NonNull
public List<Score> getAll()
{
computeAll();
return new LinkedList<>(list);
}
@Override
protected void add(List<Score> 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);
}
}

@ -106,6 +106,7 @@ public class SQLiteCheckmarkList extends CheckmarkList
.limit(1) .limit(1)
.executeSingle(); .executeSingle();
if(record == null) return null;
return record.toCheckmark(); return record.toCheckmark();
} }

@ -54,7 +54,7 @@ public class SQLiteHabitList extends HabitList
} }
@Override @Override
public void add(Habit habit) public void add(@NonNull Habit habit)
{ {
if(cache.containsValue(habit)) if(cache.containsValue(habit))
throw new RuntimeException("habit already in cache"); throw new RuntimeException("habit already in cache");
@ -132,7 +132,7 @@ public class SQLiteHabitList extends HabitList
} }
@Override @Override
public int indexOf(Habit h) public int indexOf(@NonNull Habit h)
{ {
HabitRecord record = HabitRecord.get(h.getId()); HabitRecord record = HabitRecord.get(h.getId());
if (record == null) return -1; if (record == null) return -1;

@ -19,7 +19,6 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement; import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -32,11 +31,10 @@ import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils; import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.ScoreList; import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.LinkedList;
import java.util.List; import java.util.List;
/** /**
@ -73,11 +71,25 @@ public class SQLiteScoreList extends ScoreList
.execute(); .execute();
} }
@Override
@NonNull
public List<Score> getAll()
{
List<ScoreRecord> records = select().execute();
List<Score> scores = new LinkedList<>();
for(ScoreRecord rec : records)
scores.add(rec.toScore());
return scores;
}
@Nullable @Nullable
@Override @Override
protected Score getNewestComputed() protected Score getNewestComputed()
{ {
ScoreRecord record = select().limit(1).executeSingle(); ScoreRecord record = select().limit(1).executeSingle();
if(record == null) return null;
return record.toScore(); return record.toScore();
} }
@ -85,55 +97,15 @@ public class SQLiteScoreList extends ScoreList
@Nullable @Nullable
protected Score get(long timestamp) protected Score get(long timestamp)
{ {
Repetition oldestRep = habit.getRepetitions().getOldest(); computeAll();
if (oldestRep == null) return null;
compute(oldestRep.getTimestamp(), timestamp);
ScoreRecord record = ScoreRecord record =
select().where("timestamp = ?", timestamp).executeSingle(); select().where("timestamp = ?", timestamp).executeSingle();
if(record == null) return null;
return record.toScore(); 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 @Override
protected void add(List<Score> scores) protected void add(List<Score> scores)
{ {

@ -19,6 +19,9 @@
package org.isoron.uhabits.models.sqlite; 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.Delete;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
@ -57,8 +60,9 @@ public class SQLiteStreakList extends StreakList
@Override @Override
public Streak getNewestComputed() public Streak getNewestComputed()
{ {
rebuild(); StreakRecord newestRecord = getNewestRecord();
return getNewestRecord().toStreak(); if(newestRecord == null) return null;
return newestRecord.toStreak();
} }
@Override @Override
@ -73,6 +77,7 @@ public class SQLiteStreakList extends StreakList
observable.notifyListeners(); observable.notifyListeners();
} }
@Nullable
private StreakRecord getNewestRecord() private StreakRecord getNewestRecord()
{ {
return new Select() return new Select()
@ -84,7 +89,7 @@ public class SQLiteStreakList extends StreakList
} }
@Override @Override
protected void insert(List<Streak> streaks) protected void insert(@NonNull List<Streak> streaks)
{ {
DatabaseUtils.executeAsTransaction(() -> { DatabaseUtils.executeAsTransaction(() -> {
for (Streak streak : streaks) for (Streak streak : streaks)
@ -96,6 +101,7 @@ public class SQLiteStreakList extends StreakList
}); });
} }
@NonNull
private List<Streak> recordsToStreaks(List<StreakRecord> records) private List<Streak> recordsToStreaks(List<StreakRecord> records)
{ {
LinkedList<Streak> streaks = new LinkedList<>(); LinkedList<Streak> streaks = new LinkedList<>();

@ -36,6 +36,7 @@ import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.CommandRunner; import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.utils.ColorUtils; import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.Preferences; import org.isoron.uhabits.utils.Preferences;
@ -50,17 +51,23 @@ import butterknife.OnItemSelected;
public abstract class BaseDialogFragment extends AppCompatDialogFragment public abstract class BaseDialogFragment extends AppCompatDialogFragment
{ {
@Nullable
protected Habit originalHabit; protected Habit originalHabit;
@Nullable
protected Habit modifiedHabit; protected Habit modifiedHabit;
@Nullable
protected BaseDialogHelper helper; protected BaseDialogHelper helper;
@Inject @Inject
Preferences prefs; protected Preferences prefs;
@Inject @Inject
CommandRunner commandRunner; protected CommandRunner commandRunner;
@Inject
protected HabitList habitList;
@Override @Override
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,

@ -21,20 +21,13 @@ package org.isoron.uhabits.ui.habits.edit;
import android.os.Bundle; import android.os.Bundle;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.EditHabitCommand; import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import javax.inject.Inject;
public class EditHabitDialogFragment extends BaseDialogFragment public class EditHabitDialogFragment extends BaseDialogFragment
{ {
@Inject
HabitList habitList;
public static EditHabitDialogFragment newInstance(long habitId) public static EditHabitDialogFragment newInstance(long habitId)
{ {
EditHabitDialogFragment frag = new EditHabitDialogFragment(); EditHabitDialogFragment frag = new EditHabitDialogFragment();
@ -53,8 +46,6 @@ public class EditHabitDialogFragment extends BaseDialogFragment
@Override @Override
protected void initializeHabits() protected void initializeHabits()
{ {
HabitsApplication.getComponent().inject(this);
Long habitId = (Long) getArguments().get("habitId"); Long habitId = (Long) getArguments().get("habitId");
if (habitId == null) if (habitId == null)
throw new IllegalArgumentException("habitId must be specified"); throw new IllegalArgumentException("habitId must be specified");

@ -27,8 +27,10 @@ import android.graphics.Paint;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode; import android.graphics.PorterDuffXfermode;
import android.graphics.RectF; import android.graphics.RectF;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
@ -42,6 +44,8 @@ import org.isoron.uhabits.utils.InterfaceUtils;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;
import java.util.Random; import java.util.Random;
public class HabitScoreView extends ScrollableDataView public class HabitScoreView extends ScrollableDataView
@ -86,7 +90,7 @@ public class HabitScoreView extends ScrollableDataView
private int gridColor; private int gridColor;
@Nullable @Nullable
private int[] scores; private List<Score> scores;
private int primaryColor; private int primaryColor;
@ -134,7 +138,11 @@ public class HabitScoreView extends ScrollableDataView
else else
{ {
if (habit == null) return; if (habit == null) return;
scores = habit.getScores().getAllValues(bucketSize); if (bucketSize == 1)
scores = habit.getScores().getAll();
else
scores = habit.getScores().groupBy(getTruncateField());
createColors(); createColors();
} }
@ -285,14 +293,20 @@ public class HabitScoreView extends ScrollableDataView
private void generateRandomData() private void generateRandomData()
{ {
Random random = new Random(); Random random = new Random();
scores = new int[100]; scores = new LinkedList<>();
scores[0] = Score.MAX_VALUE / 2;
int previous = Score.MAX_VALUE / 2;
long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
for (int i = 1; i < 100; i++) for (int i = 1; i < 100; i++)
{ {
int step = Score.MAX_VALUE / 10; int step = Score.MAX_VALUE / 10;
scores[i] = scores[i - 1] + random.nextInt(step * 2) - step; int current = previous + random.nextInt(step * 2) - step;
scores[i] = Math.max(0, Math.min(Score.MAX_VALUE, scores[i])); 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; 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() private void init()
{ {
createPaints(); createPaints();
@ -413,7 +459,7 @@ public class HabitScoreView extends ScrollableDataView
{ {
int score = 0; int score = 0;
int offset = nColumns - k - 1 + getDataOffset(); 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; double relativeScore = ((double) score) / Score.MAX_VALUE;
int height = (int) (columnHeight * relativeScore); int height = (int) (columnHeight * relativeScore);

@ -33,58 +33,23 @@ import java.util.TimeZone;
public abstract class DateUtils public abstract class DateUtils
{ {
public static long millisecondsInOneDay = 24 * 60 * 60 * 1000;
public static int ALL_WEEK_DAYS = 127; public static int ALL_WEEK_DAYS = 127;
private static Long fixedLocalTime = null; private static Long fixedLocalTime = null;
public static long getLocalTime() /**
{ * Number of milliseconds in one day.
if(fixedLocalTime != null) return fixedLocalTime; */
public static long millisecondsInOneDay = 24 * 60 * 60 * 1000;
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;
}
public static int getWeekday(long timestamp) public static String formatHeaderDate(GregorianCalendar day)
{ {
GregorianCalendar day = getCalendar(timestamp); String dayOfMonth =
return day.get(GregorianCalendar.DAY_OF_WEEK) % 7; 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 dayOfWeek + "\n" + dayOfMonth;
{
return getStartOfDay(DateUtils.getLocalTime());
} }
public static String formatTime(Context context, int hours, int minutes) public static String formatTime(Context context, int hours, int minutes)
@ -98,74 +63,77 @@ public abstract class DateUtils
return df.format(date); return df.format(date);
} }
public static SimpleDateFormat getDateFormat(String skeleton) public static String formatWeekdayList(Context context, boolean weekday[])
{ {
String pattern; String shortDayNames[] = getShortDayNames();
Locale locale = Locale.getDefault(); String longDayNames[] = getLongDayNames();
StringBuilder buffer = new StringBuilder();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) int count = 0;
pattern = DateFormat.getBestDateTimePattern(locale, skeleton); int first = 0;
else boolean isFirst = true;
pattern = skeleton; for (int i = 0; i < 7; i++)
{
if (weekday[i])
{
if (isFirst) first = i;
else buffer.append(", ");
SimpleDateFormat format = new SimpleDateFormat(pattern, locale); buffer.append(shortDayNames[i]);
format.setTimeZone(TimeZone.getTimeZone("UTC")); 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")); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat; 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")); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat; return dateFormat;
} }
public static String formatHeaderDate(GregorianCalendar day) public static GregorianCalendar getCalendar(long timestamp)
{
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)
{ {
long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime()); GregorianCalendar day =
return (int) (milliseconds / millisecondsInOneDay); 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() if (android.os.Build.VERSION.SDK_INT >=
{ android.os.Build.VERSION_CODES.JELLY_BEAN_MR2)
return getDayNames(GregorianCalendar.LONG); pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
} else pattern = skeleton;
SimpleDateFormat format = new SimpleDateFormat(pattern, locale);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
/** return format;
* 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 String[] getDayNames(int format) public static String[] getDayNames(int format)
@ -178,13 +146,22 @@ public abstract class DateUtils
for (int i = 0; i < wdays.length; i++) for (int i = 0; i < wdays.length; i++)
{ {
wdays[i] = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, wdays[i] = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format,
Locale.getDefault()); Locale.getDefault());
day.add(GregorianCalendar.DAY_OF_MONTH, 1); day.add(GregorianCalendar.DAY_OF_MONTH, 1);
} }
return wdays; 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, * @return array with weekday names starting according to locale settings,
* e.g. [Mo,Di,Mi,Do,Fr,Sa,So] in Europe * e.g. [Mo,Di,Mi,Do,Fr,Sa,So] in Europe
@ -194,10 +171,12 @@ public abstract class DateUtils
String[] days = new String[7]; String[] days = new String[7];
Calendar calendar = new GregorianCalendar(); 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++) 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()); Locale.getDefault());
calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); 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, * @return array with week days numbers starting according to locale
* e.g. [2,3,4,5,6,7,1] in Europe * settings, e.g. [2,3,4,5,6,7,1] in Europe
*/ */
public static Integer[] getLocaleWeekdayList() public static Integer[] getLocaleWeekdayList()
{ {
Integer[] dayNumbers = new Integer[7]; Integer[] dayNumbers = new Integer[7];
Calendar calendar = new GregorianCalendar(); 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++) for (int i = 0; i < dayNumbers.length; i++)
{ {
dayNumbers[i] = calendar.get(GregorianCalendar.DAY_OF_WEEK); dayNumbers[i] = calendar.get(GregorianCalendar.DAY_OF_WEEK);
@ -222,33 +202,48 @@ public abstract class DateUtils
return dayNumbers; return dayNumbers;
} }
public static String formatWeekdayList(Context context, boolean weekday[]) public static String[] getLongDayNames()
{ {
String shortDayNames[] = getShortDayNames(); return getDayNames(GregorianCalendar.LONG);
String longDayNames[] = getLongDayNames(); }
StringBuilder buffer = new StringBuilder();
int count = 0; public static String[] getShortDayNames()
int first = 0; {
boolean isFirst = true; return getDayNames(GregorianCalendar.SHORT);
for(int i = 0; i < 7; i++) }
{
if(weekday[i])
{
if(isFirst) first = i;
else buffer.append(", ");
buffer.append(shortDayNames[i]); public static long getStartOfDay(long timestamp)
isFirst = false; {
count++; return (timestamp / millisecondsInOneDay) * millisecondsInOneDay;
} }
}
if(count == 1) return longDayNames[first]; public static long getStartOfToday()
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); return getStartOfDay(DateUtils.getLocalTime());
if(count == 7) return context.getString(R.string.any_day); }
return buffer.toString();
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[]) public static Integer packWeekdayList(boolean weekday[])
@ -256,26 +251,77 @@ public abstract class DateUtils
int list = 0; int list = 0;
int current = 1; 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; current = current << 1;
} }
return list; 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) public static boolean[] unpackWeekdayList(int list)
{ {
boolean[] weekday = new boolean[7]; boolean[] weekday = new boolean[7];
int current = 1; 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; current = current << 1;
} }
return weekday; return weekday;
} }
public enum TruncateField
{
MONTH, WEEK_NUMBER, YEAR, QUARTER
}
} }

@ -0,0 +1,195 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Score> 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<Score> 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);
}
}

@ -17,24 +17,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.unit.models; package org.isoron.uhabits.models;
import android.support.test.runner.AndroidJUnit4; import org.isoron.uhabits.BaseUnitTest;
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.junit.Before; import org.junit.Before;
import org.junit.Test; 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; import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class) public class ScoreTest extends BaseUnitTest
@SmallTest
public class ScoreTest extends BaseAndroidTest
{ {
@Override @Override
@Before @Before
@ -50,20 +42,22 @@ public class ScoreTest extends BaseAndroidTest
assertThat(Score.compute(1, 0, checkmark), equalTo(0)); assertThat(Score.compute(1, 0, checkmark), equalTo(0));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387)); assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775)); 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; checkmark = Checkmark.CHECKED_IMPLICITLY;
assertThat(Score.compute(1, 0, checkmark), equalTo(0)); assertThat(Score.compute(1, 0, checkmark), equalTo(0));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387)); assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775)); 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; checkmark = Checkmark.CHECKED_EXPLICITLY;
assertThat(Score.compute(1, 0, checkmark), equalTo(1000000)); assertThat(Score.compute(1, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387)); assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775)); assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo( assertThat(Score.compute(1, Score.MAX_VALUE, checkmark),
Score.MAX_VALUE)); equalTo(Score.MAX_VALUE));
} }
@Test @Test
@ -71,15 +65,19 @@ public class ScoreTest extends BaseAndroidTest
{ {
int checkmark = Checkmark.CHECKED_EXPLICITLY; int checkmark = Checkmark.CHECKED_EXPLICITLY;
assertThat(Score.compute(1 / 3.0, 0, checkmark), equalTo(1000000)); 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, 5000000, checkmark),
assertThat(Score.compute(1 / 3.0, 10000000, checkmark), equalTo(10832360)); equalTo(5916180));
assertThat(Score.compute(1 / 3.0, Score.MAX_VALUE, checkmark), equalTo( assertThat(Score.compute(1 / 3.0, 10000000, checkmark),
Score.MAX_VALUE)); 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, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1 / 7.0, 5000000, checkmark), equalTo(5964398)); assertThat(Score.compute(1 / 7.0, 5000000, checkmark),
assertThat(Score.compute(1 / 7.0, 10000000, checkmark), equalTo(10928796)); equalTo(5964398));
assertThat(Score.compute(1 / 7.0, Score.MAX_VALUE, checkmark), equalTo( assertThat(Score.compute(1 / 7.0, 10000000, checkmark),
Score.MAX_VALUE)); equalTo(10928796));
assertThat(Score.compute(1 / 7.0, Score.MAX_VALUE, checkmark),
equalTo(Score.MAX_VALUE));
} }
} }

@ -0,0 +1,146 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
Loading…
Cancel
Save