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
sourceCompatibility 1.8
}
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
dependencies {

@ -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);
});
}
}

@ -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);
}

@ -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;

@ -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",

@ -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;
}
}

@ -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;

@ -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.
* <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 abstract List<Score> getAll();
public ModelObservable getObservable()
{
@ -112,6 +78,14 @@ public abstract class ScoreList
*/
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
* 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<Score> scores);
@ -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<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>
* 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<Score> groupsToAvgScores(HashMap<Long, ArrayList<Long>> groups)
{
List<Score> scores = new LinkedList<>();
for (Long timestamp : groups.keySet())
{
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();
@NonNull
public List<Streak> getBest(int limit)
{
List<Streak> 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<Streak> checkmarksToStreaks(Long beginning, int[] checks)
protected List<Streak> checkmarksToStreaks(long beginning, int[] checks)
{
ArrayList<Long> transitions = getTransitions(beginning, checks);
@ -130,7 +133,7 @@ public abstract class StreakList
* @return the list of transitions
*/
@NonNull
protected ArrayList<Long> getTransitions(Long beginning, int[] checks)
protected ArrayList<Long> 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<Streak> streaks);
protected abstract void insert(@NonNull List<Streak> streaks);
protected abstract void removeNewestComputed();
}

@ -50,7 +50,7 @@ public class MemoryModelFactory implements ModelFactory
@Override
public ScoreList buildScoreList(Habit habit)
{
return null;
return new MemoryScoreList(habit);
}
@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)
.executeSingle();
if(record == null) return null;
return record.toCheckmark();
}

@ -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;

@ -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<Score> getAll()
{
List<ScoreRecord> records = select().execute();
List<Score> 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<Score> scores)
{

@ -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<Streak> streaks)
protected void insert(@NonNull List<Streak> streaks)
{
DatabaseUtils.executeAsTransaction(() -> {
for (Streak streak : streaks)
@ -96,6 +101,7 @@ public class SQLiteStreakList extends StreakList
});
}
@NonNull
private List<Streak> recordsToStreaks(List<StreakRecord> records)
{
LinkedList<Streak> streaks = new LinkedList<>();

@ -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,

@ -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");

@ -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<Score> 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);

@ -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();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2)
pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
else
pattern = skeleton;
String shortDayNames[] = getShortDayNames();
String longDayNames[] = getLongDayNames();
StringBuilder buffer = new StringBuilder();
SimpleDateFormat format = new SimpleDateFormat(pattern, locale);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
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(", ");
return format;
buffer.append(shortDayNames[i]);
isFirst = false;
count++;
}
}
public static SimpleDateFormat getCSVDateFormat()
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat;
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 getBackupDateFormat()
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US);
SimpleDateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat;
}
public static String formatHeaderDate(GregorianCalendar day)
public static SimpleDateFormat getCSVDateFormat()
{
String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH));
String dayOfWeek = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK,
GregorianCalendar.SHORT, Locale.getDefault());
SimpleDateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dayOfWeek + "\n" + dayOfMonth;
return dateFormat;
}
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)
@ -185,6 +153,15 @@ public abstract class DateUtils
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++)
public static String[] getShortDayNames()
{
if(weekday[i])
return getDayNames(GregorianCalendar.SHORT);
}
public static long getStartOfDay(long timestamp)
{
if(isFirst) first = i;
else buffer.append(", ");
return (timestamp / millisecondsInOneDay) * millisecondsInOneDay;
}
buffer.append(shortDayNames[i]);
isFirst = false;
count++;
public static long getStartOfToday()
{
return getStartOfDay(DateUtils.getLocalTime());
}
public static GregorianCalendar getStartOfTodayCalendar()
{
return getCalendar(getStartOfToday());
}
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 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[])
@ -265,6 +260,52 @@ public abstract class DateUtils
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];
@ -278,4 +319,9 @@ public abstract class DateUtils
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/>.
*/
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));
}
}

@ -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