Implement alternative checkmark algorithm

Fix failing tests
pull/87/merge
Alinson S. Xavier 8 years ago
parent 96e1771c25
commit ed9066f393

@ -1,3 +1,3 @@
#!/bin/bash
find app/build/outputs/failed/test-screenshots -name '*.expected*' -delete
rsync -av app/build/outputs/failed/test-screenshots/ app/src/androidTest/assets/
find uhabits-android/build/outputs/failed/test-screenshots -name '*.expected*' -delete
rsync -av uhabits-android/build/outputs/failed/test-screenshots/ uhabits-android/src/androidTest/assets/

@ -5,7 +5,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-alpha2'
classpath 'com.android.tools.build:gradle:3.0.0-alpha3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

@ -96,9 +96,10 @@ dependencies {
testAnnotationProcessor 'com.google.auto.factory:auto-factory:1.0-beta3'
testAnnotationProcessor 'com.google.dagger:dagger-compiler:2.9'
testAnnotationProcessor 'com.jakewharton:butterknife-compiler:8.6.1-SNAPSHOT'
testCompile 'com.google.dagger:dagger:2.9'
testCompile "org.mockito:mockito-core:2.8.9"
testCompile "org.mockito:mockito-inline:2.8.9"
testImplementation 'com.google.dagger:dagger:2.9'
testImplementation "org.mockito:mockito-core:2.8.9"
testImplementation "org.mockito:mockito-inline:2.8.9"
testImplementation 'junit:junit:4+'
implementation('com.opencsv:opencsv:3.9') {
exclude group: 'commons-logging', module: 'commons-logging'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@ -1,127 +0,0 @@
/*
* 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.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteCheckmarkListTest extends BaseAndroidTest
{
private Habit habit;
private CheckmarkList checkmarks;
private long today;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
checkmarks = habit.getCheckmarks();
checkmarks.getToday(); // compute checkmarks
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testAdd()
{
checkmarks.invalidateNewerThan(0);
List<Checkmark> list = new LinkedList<>();
list.add(new Checkmark(0, 0));
list.add(new Checkmark(1, 1));
list.add(new Checkmark(2, 2));
checkmarks.add(list);
List<CheckmarkRecord> records = getAllRecords();
assertThat(records.size(), equalTo(3));
assertThat(records.get(0).timestamp, equalTo(2L));
}
@Test
public void testGetByInterval()
{
long from = today - 10 * day;
long to = today - 3 * day;
List<Checkmark> list = checkmarks.getByInterval(from, to);
assertThat(list.size(), equalTo(8));
assertThat(list.get(0).getTimestamp(), equalTo(today - 3 * day));
assertThat(list.get(3).getTimestamp(), equalTo(today - 6 * day));
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testGetByInterval_withLongInterval()
{
long from = today - 200 * day;
long to = today;
List<Checkmark> list = checkmarks.getByInterval(from, to);
assertThat(list.size(), equalTo(201));
}
@Test
public void testInvalidateNewerThan()
{
List<CheckmarkRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
checkmarks.invalidateNewerThan(today - 20 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(100));
assertThat(records.get(0).timestamp, equalTo(today - 21 * day));
}
private List<CheckmarkRecord> getAllRecords()
{
return new Select()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc")
.execute();
}
}

@ -1,136 +0,0 @@
/*
* 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.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteScoreListTest extends BaseAndroidTest
{
private Habit habit;
private ScoreList scores;
private long today;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
scores = habit.getScores();
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testGetAll()
{
List<Score> list = scores.toList();
assertThat(list.size(), equalTo(121));
assertThat(list.get(0).getTimestamp(), equalTo(today));
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testInvalidateNewerThan()
{
scores.getTodayValue(); // force recompute
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
scores.invalidateNewerThan(today - 10 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(110));
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
}
@Test
public void testAdd()
{
new Delete().from(ScoreRecord.class).execute();
List<Score> list = new LinkedList<>();
list.add(new Score(today, 0));
list.add(new Score(today - day, 0));
list.add(new Score(today - 2 * day, 0));
scores.add(list);
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(3));
assertThat(records.get(0).timestamp, equalTo(today));
}
@Test
public void testGetByInterval()
{
long from = today - 10 * day;
long to = today - 3 * day;
List<Score> list = scores.getByInterval(from, to);
assertThat(list.size(), equalTo(8));
assertThat(list.get(0).getTimestamp(), equalTo(today - 3 * day));
assertThat(list.get(3).getTimestamp(), equalTo(today - 6 * day));
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testGetByInterval_withLongInterval()
{
long from = today - 200 * day;
long to = today;
List<Score> list = scores.getByInterval(from, to);
assertThat(list.size(), equalTo(201));
}
private List<ScoreRecord> getAllRecords()
{
return new Select()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc")
.execute();
}
}

@ -19,7 +19,6 @@
package org.isoron.uhabits.activities.habits.list;
import android.content.*;
import android.os.*;
import org.isoron.androidbase.activities.*;
@ -28,7 +27,6 @@ import org.isoron.uhabits.activities.habits.list.model.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.sync.*;
/**
* Activity that allows the user to see and modify the list of habits.
@ -85,8 +83,8 @@ public class ListHabitsActivity extends BaseActivity
screen.setSelectionMenu(selectionMenu);
rootView.setController(controller, selectionMenu);
if(prefs.isSyncEnabled())
startService(new Intent(this, SyncService.class));
// if(prefs.isSyncEnabled())
// startService(new Intent(this, SyncService.class));
setScreen(screen);
controller.onStartup();

@ -188,9 +188,6 @@ public class SQLiteHabitList extends HabitList
@Override
public void removeAll()
{
sqlite.query("delete from checkmarks", null);
sqlite.query("delete from score", null);
sqlite.query("delete from streak", null);
sqlite.query("delete from repetitions", null);
sqlite.query("delete from habits", null);
}

@ -6,29 +6,23 @@ apply plugin: 'java'
apply plugin: 'jacoco'
dependencies {
apt 'com.google.auto.factory:auto-factory:1.0-beta3'
apt 'com.google.dagger:dagger:2.11-rc2'
compileOnly 'javax.annotation:jsr250-api:1.0'
compileOnly 'com.google.auto.factory:auto-factory:1.0-beta3'
compileOnly 'com.google.dagger:dagger:2.11-rc2'
implementation 'com.android.support:support-annotations:25.3.1'
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'org.apache.commons:commons-lang3:3.5'
implementation 'com.google.code.gson:gson:2.7'
testImplementation 'junit:junit:4+'
testImplementation 'org.hamcrest:hamcrest-library:1.4-atlassian-1'
testImplementation 'org.apache.commons:commons-io:1.3.2'
testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.json:json:20160810'
implementation ('com.opencsv:opencsv:3.9') {
implementation('com.opencsv:opencsv:3.9') {
exclude group: 'commons-logging', module: 'commons-logging'
}
compile 'junit:junit:4.12'
}
jacocoTestReport {

@ -56,10 +56,10 @@ public final class Checkmark
/**
* The value of the checkmark.
*
* <p>
* For boolean habits, this equals either UNCHECKED, CHECKED_EXPLICITLY,
* or CHECKED_IMPLICITLY.
*
* <p>
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is stored as 1500.
*/
@ -76,6 +76,21 @@ public final class Checkmark
return Long.signum(this.getTimestamp() - other.getTimestamp());
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Checkmark checkmark = (Checkmark) o;
return new EqualsBuilder()
.append(timestamp, checkmark.timestamp)
.append(value, checkmark.value)
.isEquals();
}
public long getTimestamp()
{
return timestamp;
@ -86,6 +101,15 @@ public final class Checkmark
return value;
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(timestamp)
.append(value)
.toHashCode();
}
@Override
public String toString()
{

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.models;
import android.support.annotation.*;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
@ -29,7 +30,10 @@ import java.util.*;
import javax.annotation.concurrent.*;
import static org.isoron.uhabits.core.models.Checkmark.*;
import static org.isoron.uhabits.core.models.Checkmark.CHECKED_EXPLICITLY;
import static org.isoron.uhabits.core.models.Checkmark.CHECKED_IMPLICITLY;
import static org.isoron.uhabits.core.models.Checkmark.UNCHECKED;
/**
* The collection of {@link Checkmark}s belonging to a habit.
*/
@ -46,6 +50,104 @@ public abstract class CheckmarkList
this.observable = new ModelObservable();
}
@NonNull
static List<Checkmark> buildCheckmarksFromIntervals(Repetition[] reps,
ArrayList<Interval> intervals)
{
if (reps.length == 0) throw new IllegalArgumentException();
long day = DateUtils.millisecondsInOneDay;
long today = DateUtils.getStartOfToday();
long begin = reps[0].getTimestamp();
if (intervals.size() > 0)
begin = Long.min(begin, intervals.get(0).begin);
int nDays = (int) ((today - begin) / day) + 1;
List<Checkmark> checkmarks = new ArrayList<>(nDays);
for (int i = 0; i < nDays; i++)
checkmarks.add(new Checkmark(today - day * i, UNCHECKED));
for (Interval interval : intervals)
{
for (long date = interval.begin; date <= interval.end; date += day)
{
if (date > today) continue;
int offset = (int) ((today - date) / day);
checkmarks.set(offset, new Checkmark(date, CHECKED_IMPLICITLY));
}
}
for (Repetition rep : reps)
{
long date = rep.getTimestamp();
int offset = (int) ((today - date) / day);
checkmarks.set(offset, new Checkmark(date, CHECKED_EXPLICITLY));
}
return checkmarks;
}
/**
* For non-daily habits, some groups of repetitions generate many
* checkmarks. For example, for weekly habits, each repetition generates
* seven checkmarks. For twice-a-week habits, two repetitions that are close
* enough together also generate seven checkmarks.
* <p>
* This group of generated checkmarks, for a given set of repetition, is
* represented by an interval. This function computes the list of intervals
* for a given list of repetitions. It tries to build the intervals as far
* away in the future as possible.
*/
@NonNull
static ArrayList<Interval> buildIntervals(@NonNull Frequency freq,
@NonNull Repetition[] reps)
{
long day = DateUtils.millisecondsInOneDay;
int num = freq.getNumerator();
int den = freq.getDenominator();
ArrayList<Interval> intervals = new ArrayList<>();
for (int i = 0; i < reps.length - num + 1; i++)
{
Repetition first = reps[i];
Repetition last = reps[i + num - 1];
long distance = (last.getTimestamp() - first.getTimestamp()) / day;
if (distance >= den) continue;
long begin = first.getTimestamp();
long center = last.getTimestamp();
long end = begin + (den - 1) * day;
intervals.add(new Interval(begin, center, end));
}
return intervals;
}
/**
* Starting from the oldest interval, this function tries to slide the
* intervals backwards into the past, so that gaps are eliminated and
* streaks are maximized. When it detects that sliding an interval
* would not help fixing any gap, it leaves the interval unchanged.
*/
static void snapIntervalsTogether(@NonNull ArrayList<Interval> intervals)
{
long day = DateUtils.millisecondsInOneDay;
for (int i = 1; i < intervals.size(); i++)
{
Interval curr = intervals.get(i);
Interval prev = intervals.get(i - 1);
long distance = curr.begin - prev.end - day;
if (distance <= 0 || curr.end - distance < curr.center) continue;
intervals.set(i, new Interval(curr.begin - distance, curr.center,
curr.end - distance));
}
}
/**
* Adds all the given checkmarks to the list.
* <p>
@ -104,8 +206,9 @@ public abstract class CheckmarkList
@Nullable
public synchronized final Checkmark getToday()
{
computeAll();
return getNewestComputed();
compute();
long today = DateUtils.getStartOfToday();
return getByInterval(today, today).get(0);
}
/**
@ -117,7 +220,7 @@ public abstract class CheckmarkList
{
Checkmark today = getToday();
if (today != null) return today.getValue();
else return Checkmark.UNCHECKED;
else return UNCHECKED;
}
/**
@ -135,7 +238,7 @@ public abstract class CheckmarkList
*/
public final int[] getValues(long from, long to)
{
if(from > to) return new int[0];
if (from > to) return new int[0];
List<Checkmark> checkmarks = getByInterval(from, to);
int values[] = new int[checkmarks.size()];
@ -168,7 +271,7 @@ public abstract class CheckmarkList
synchronized (this)
{
computeAll();
compute();
values = getAllValues();
}
@ -184,118 +287,126 @@ public abstract class CheckmarkList
}
/**
* Computes and stores one checkmark for each day that falls inside the
* specified interval of time. Days that already have a corresponding
* checkmark are skipped.
*
* This method assumes the list of computed checkmarks has no holes. That
* is, if there is a checkmark computed at time t1 and another at time t2,
* then every checkmark between t1 and t2 is also computed.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
* Computes and stores one checkmark for each day, from the first habit
* repetition to today. If this list is already computed, does nothing.
*/
protected final synchronized void compute(long from, long to)
protected final synchronized void compute()
{
final long day = DateUtils.millisecondsInOneDay;
final long today = DateUtils.getStartOfToday();
Checkmark newest = getNewestComputed();
Checkmark oldest = getOldestComputed();
if(newest != null && newest.getTimestamp() == today) return;
invalidateNewerThan(0);
if (newest == null || oldest == null)
{
forceRecompute(from, to);
}
else
{
forceRecompute(from, oldest.getTimestamp() - day);
forceRecompute(newest.getTimestamp() + day, to);
}
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return;
final long from = oldestRep.getTimestamp();
Repetition reps[] = habit
.getRepetitions()
.getByInterval(from, today)
.toArray(new Repetition[0]);
if (habit.isNumerical()) computeNumerical(reps);
else computeYesNo(reps);
}
/**
* Returns oldest checkmark that has already been computed.
* Returns newest checkmark that has already been computed.
*
* @return oldest checkmark already computed
* @return newest checkmark already computed
*/
@Nullable
protected abstract Checkmark getOldestComputed();
protected abstract Checkmark getNewestComputed();
/**
* Computes and stores one checkmark for each day that falls inside the
* specified interval of time.
*
* This method does not check if the checkmarks have already been
* computed or not. If they have, then duplicate checkmarks will
* be stored, which is a bad thing.
* Returns oldest checkmark that has already been computed.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
* @return oldest checkmark already computed
*/
private synchronized void forceRecompute(long from, long to)
{
if (from > to) return;
@Nullable
protected abstract Checkmark getOldestComputed();
final long day = DateUtils.millisecondsInOneDay;
Frequency freq = habit.getFrequency();
private void computeNumerical(Repetition[] reps)
{
if (reps.length == 0) throw new IllegalArgumentException();
long fromExtended = from - (long) (freq.getDenominator()) * day;
List<Repetition> reps =
habit.getRepetitions().getByInterval(fromExtended, to);
long day = DateUtils.millisecondsInOneDay;
long today = DateUtils.getStartOfToday();
long begin = reps[0].getTimestamp();
final int nDays = (int) ((to - from) / day) + 1;
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
final int checks[] = new int[nDaysExtended];
int nDays = (int) ((today - begin) / day) + 1;
List<Checkmark> checkmarks = new ArrayList<>(nDays);
for (int i = 0; i < nDays; i++)
checkmarks.add(new Checkmark(today - day * i, 0));
for (Repetition rep : reps)
{
int offset = (int) ((rep.getTimestamp() - fromExtended) / day);
checks[nDaysExtended - offset - 1] = rep.getValue();
long date = rep.getTimestamp();
int offset = (int) ((today - date) / day);
checkmarks.set(offset, new Checkmark(date, rep.getValue()));
}
for (int i = 0; i < nDays; i++)
{
int counter = 0;
add(checkmarks);
}
for (int j = 0; j < freq.getDenominator(); j++)
if (checks[i + j] == CHECKED_EXPLICITLY) counter++;
private void computeYesNo(Repetition[] reps)
{
ArrayList<Interval> intervals;
intervals = buildIntervals(habit.getFrequency(), reps);
snapIntervalsTogether(intervals);
add(buildCheckmarksFromIntervals(reps, intervals));
}
if (counter >= freq.getNumerator())
if (checks[i] != CHECKED_EXPLICITLY)
checks[i] = CHECKED_IMPLICITLY;
}
static class Interval
{
final long begin;
List<Checkmark> checkmarks = new LinkedList<>();
final long center;
for (int i = 0; i < nDays; i++)
final long end;
Interval(long begin, long center, long end)
{
int value = checks[i];
long timestamp = to - i * day;
checkmarks.add(new Checkmark(timestamp, value));
this.begin = begin;
this.center = center;
this.end = end;
}
add(checkmarks);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
/**
* Computes and stores one checkmark for each day, since the first
* repetition of the habit until today. Days that already have a
* corresponding checkmark are skipped.
*/
private synchronized void computeAll()
{
Repetition oldest = habit.getRepetitions().getOldest();
if (oldest == null) return;
if (o == null || getClass() != o.getClass()) return false;
Long today = DateUtils.getStartOfToday();
compute(oldest.getTimestamp(), today);
}
Interval interval = (Interval) o;
/**
* Returns newest checkmark that has already been computed.
*
* @return newest checkmark already computed
*/
@Nullable
protected abstract Checkmark getNewestComputed();
return new EqualsBuilder()
.append(begin, interval.begin)
.append(center, interval.center)
.append(end, interval.end)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(begin)
.append(center)
.append(end)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("begin", begin)
.append("center", center)
.append("end", end)
.toString();
}
}
}

@ -33,11 +33,9 @@ public final class Repetition
/**
* The value of the repetition.
*
* For boolean habits, this equals either Checkmark.UNCHECKED,
* Checkmark.CHECKED_EXPLICITLY, or Checkmark.CHECKED_IMPLICITLY.
*
* For boolean habits, this always equals Checkmark.CHECKED_EXPLICITLY.
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is stored as 1500.
* is, if the user enters value 1.50 on the app, it is here stored as 1500.
*/
private final int value;

@ -77,7 +77,6 @@ public abstract class RepetitionList
* @param toTimestamp timestamp of the end of the interval
* @return list of repetitions within given time interval
*/
// TODO: Change order timestamp desc
public abstract List<Repetition> getByInterval(long fromTimestamp,
long toTimestamp);

@ -25,17 +25,19 @@ import org.isoron.uhabits.core.models.*;
import java.util.*;
import static org.isoron.uhabits.core.utils.DateUtils.*;
/**
* In-memory implementation of {@link CheckmarkList}.
*/
public class MemoryCheckmarkList extends CheckmarkList
{
LinkedList<Checkmark> list;
ArrayList<Checkmark> list;
public MemoryCheckmarkList(Habit habit)
{
super(habit);
list = new LinkedList<>();
list = new ArrayList<>();
}
@Override
@ -49,13 +51,27 @@ public class MemoryCheckmarkList extends CheckmarkList
@Override
public List<Checkmark> getByInterval(long fromTimestamp, long toTimestamp)
{
compute(fromTimestamp, toTimestamp);
compute();
List<Checkmark> filtered = new LinkedList<>();
long newestTimestamp = Long.MIN_VALUE;
long oldestTimestamp = Long.MAX_VALUE;
Checkmark newest = getNewestComputed();
Checkmark oldest = getOldestComputed();
if(newest != null) newestTimestamp = newest.getTimestamp();
if(oldest != null) oldestTimestamp = oldest.getTimestamp();
for (Checkmark c : list)
if (c.getTimestamp() >= fromTimestamp &&
c.getTimestamp() <= toTimestamp) filtered.add(c);
List<Checkmark> filtered = new LinkedList<>();
for(long time = toTimestamp; time >= fromTimestamp; time -= millisecondsInOneDay)
{
if(time > newestTimestamp || time < oldestTimestamp)
filtered.add(new Checkmark(time, Checkmark.UNCHECKED));
else
{
int offset = (int) ((newestTimestamp - time) / millisecondsInOneDay);
filtered.add(list.get(offset));
}
}
return filtered;
}
@ -63,12 +79,8 @@ public class MemoryCheckmarkList extends CheckmarkList
@Override
public void invalidateNewerThan(long timestamp)
{
LinkedList<Checkmark> invalid = new LinkedList<>();
for (Checkmark c : list)
if (c.getTimestamp() >= timestamp) invalid.add(c);
list.removeAll(invalid);
list.clear();
observable.notifyListeners();
}
@Override
@ -76,7 +88,7 @@ public class MemoryCheckmarkList extends CheckmarkList
protected Checkmark getOldestComputed()
{
if(list.isEmpty()) return null;
return list.getLast();
return list.get(list.size()-1);
}
@Override
@ -84,7 +96,7 @@ public class MemoryCheckmarkList extends CheckmarkList
protected Checkmark getNewestComputed()
{
if(list.isEmpty()) return null;
return list.getFirst();
return list.get(0);
}
}

@ -24,33 +24,153 @@ import org.isoron.uhabits.core.utils.*;
import org.junit.*;
import java.io.*;
import java.util.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.*;
import static org.isoron.uhabits.core.models.Checkmark.*;
import static org.isoron.uhabits.core.models.Checkmark.CHECKED_EXPLICITLY;
import static org.isoron.uhabits.core.models.Checkmark.CHECKED_IMPLICITLY;
import static org.isoron.uhabits.core.models.Checkmark.UNCHECKED;
public class CheckmarkListTest extends BaseUnitTest
{
// 8:00am, January 25th, 2015 (UTC)
private long fixed_local_time = 1422172800000L;
private long dayLength;
private long today;
private Habit nonDailyHabit;
private Habit emptyHabit;
private Habit numericalHabit;
@Override
public void setUp()
{
super.setUp();
DateUtils.setFixedLocalTime(fixed_local_time);
dayLength = DateUtils.millisecondsInOneDay;
today = DateUtils.getStartOfToday();
fixtures.createShortHabit();
nonDailyHabit = fixtures.createShortHabit();
habitList.add(nonDailyHabit);
emptyHabit = fixtures.createEmptyHabit();
habitList.add(emptyHabit);
numericalHabit = fixtures.createNumericalHabit();
habitList.add(numericalHabit);
}
@Test
public void test_buildCheckmarksFromIntervals_1() throws Exception
{
Repetition reps[] = new Repetition[]{
new Repetition(day(10), CHECKED_EXPLICITLY),
new Repetition(day(5), CHECKED_EXPLICITLY),
new Repetition(day(2), CHECKED_EXPLICITLY),
new Repetition(day(1), CHECKED_EXPLICITLY),
};
ArrayList<CheckmarkList.Interval> intervals = new ArrayList<>();
intervals.add(new CheckmarkList.Interval(day(10), day(8), day(8)));
intervals.add(new CheckmarkList.Interval(day(6), day(5), day(4)));
intervals.add(new CheckmarkList.Interval(day(2), day(2), day(1)));
List<Checkmark> expected = new ArrayList<>();
expected.add(new Checkmark(day(0), UNCHECKED));
expected.add(new Checkmark(day(1), CHECKED_EXPLICITLY));
expected.add(new Checkmark(day(2), CHECKED_EXPLICITLY));
expected.add(new Checkmark(day(3), UNCHECKED));
expected.add(new Checkmark(day(4), CHECKED_IMPLICITLY));
expected.add(new Checkmark(day(5), CHECKED_EXPLICITLY));
expected.add(new Checkmark(day(6), CHECKED_IMPLICITLY));
expected.add(new Checkmark(day(7), UNCHECKED));
expected.add(new Checkmark(day(8), CHECKED_IMPLICITLY));
expected.add(new Checkmark(day(9), CHECKED_IMPLICITLY));
expected.add(new Checkmark(day(10), CHECKED_EXPLICITLY));
List<Checkmark> actual =
CheckmarkList.buildCheckmarksFromIntervals(reps, intervals);
assertThat(actual, equalTo(expected));
}
@Test
public void test_buildCheckmarksFromIntervals_2() throws Exception
{
Repetition reps[] = new Repetition[]{
new Repetition(day(0), CHECKED_EXPLICITLY),
};
ArrayList<CheckmarkList.Interval> intervals = new ArrayList<>();
intervals.add(new CheckmarkList.Interval(day(0), day(0), day(-10)));
List<Checkmark> expected = new ArrayList<>();
expected.add(new Checkmark(day(0), CHECKED_EXPLICITLY));
List<Checkmark> actual =
CheckmarkList.buildCheckmarksFromIntervals(reps, intervals);
assertThat(actual, equalTo(expected));
}
@Test
public void test_buildIntervals_1() throws Exception
{
Repetition reps[] = new Repetition[]{
new Repetition(day(23), CHECKED_EXPLICITLY),
new Repetition(day(18), CHECKED_EXPLICITLY),
new Repetition(day(8), CHECKED_EXPLICITLY),
};
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
expected.add(new CheckmarkList.Interval(day(23), day(23), day(17)));
expected.add(new CheckmarkList.Interval(day(18), day(18), day(12)));
expected.add(new CheckmarkList.Interval(day(8), day(8), day(2)));
ArrayList<CheckmarkList.Interval> actual;
actual = CheckmarkList.buildIntervals(Frequency.WEEKLY, reps);
assertThat(actual, equalTo(expected));
}
@Test
public void test_buildIntervals_2() throws Exception
{
Repetition reps[] = new Repetition[]{
new Repetition(day(23), CHECKED_EXPLICITLY),
new Repetition(day(18), CHECKED_EXPLICITLY),
new Repetition(day(8), CHECKED_EXPLICITLY),
};
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
expected.add(new CheckmarkList.Interval(day(23), day(23), day(23)));
expected.add(new CheckmarkList.Interval(day(18), day(18), day(18)));
expected.add(new CheckmarkList.Interval(day(8), day(8), day(8)));
ArrayList<CheckmarkList.Interval> actual;
actual = CheckmarkList.buildIntervals(Frequency.DAILY, reps);
assertThat(actual, equalTo(expected));
}
@Test
public void test_buildIntervals_3() throws Exception
{
Repetition reps[] = new Repetition[]{
new Repetition(day(23), CHECKED_EXPLICITLY),
new Repetition(day(22), CHECKED_EXPLICITLY),
new Repetition(day(18), CHECKED_EXPLICITLY),
new Repetition(day(15), CHECKED_EXPLICITLY),
new Repetition(day(8), CHECKED_EXPLICITLY),
};
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
expected.add(new CheckmarkList.Interval(day(23), day(22), day(17)));
expected.add(new CheckmarkList.Interval(day(22), day(18), day(16)));
expected.add(new CheckmarkList.Interval(day(18), day(15), day(12)));
ArrayList<CheckmarkList.Interval> actual;
actual =
CheckmarkList.buildIntervals(Frequency.TWO_TIMES_PER_WEEK, reps);
assertThat(actual, equalTo(expected));
}
@Test
@ -62,7 +182,7 @@ public class CheckmarkListTest extends BaseUnitTest
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
UNCHECKED,
CHECKED_IMPLICITLY,
CHECKED_IMPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY
@ -73,7 +193,6 @@ public class CheckmarkListTest extends BaseUnitTest
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getAllValues_moveForwardInTime()
{
@ -89,7 +208,7 @@ public class CheckmarkListTest extends BaseUnitTest
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
UNCHECKED,
CHECKED_IMPLICITLY,
CHECKED_IMPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY
@ -119,7 +238,7 @@ public class CheckmarkListTest extends BaseUnitTest
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
UNCHECKED,
CHECKED_IMPLICITLY,
CHECKED_IMPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY
@ -130,20 +249,33 @@ public class CheckmarkListTest extends BaseUnitTest
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getByInterval_withNumericalHabits() throws Exception
{
CheckmarkList checkmarks = numericalHabit.getCheckmarks();
List<Checkmark> expected =
Arrays.asList(new Checkmark(day(1), 200), new Checkmark(day(2), 0),
new Checkmark(day(3), 300), new Checkmark(day(4), 0),
new Checkmark(day(5), 400));
List<Checkmark> actual = checkmarks.getByInterval(day(5), day(1));
assertThat(actual, equalTo(expected));
}
@Test
public void test_getTodayValue()
{
CheckmarkList checkmarks = nonDailyHabit.getCheckmarks();
travelInTime(-1);
assertThat(nonDailyHabit.getCheckmarks().getTodayValue(),
equalTo(UNCHECKED));
assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED));
travelInTime(0);
assertThat(nonDailyHabit.getCheckmarks().getTodayValue(),
equalTo(CHECKED_EXPLICITLY));
assertThat(checkmarks.getTodayValue(), equalTo(CHECKED_EXPLICITLY));
travelInTime(1);
assertThat(nonDailyHabit.getCheckmarks().getTodayValue(),
equalTo(UNCHECKED));
assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED));
}
@Test
@ -156,14 +288,12 @@ public class CheckmarkListTest extends BaseUnitTest
@Test
public void test_getValues_withValidInterval()
{
long from =
DateUtils.getStartOfToday() - 15 * DateUtils.millisecondsInOneDay;
long to =
DateUtils.getStartOfToday() - 5 * DateUtils.millisecondsInOneDay;
long from = today - 15 * dayLength;
long to = today - 5 * dayLength;
int[] expectedValues = {
CHECKED_EXPLICITLY,
UNCHECKED,
CHECKED_IMPLICITLY,
CHECKED_IMPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
@ -180,18 +310,31 @@ public class CheckmarkListTest extends BaseUnitTest
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_snapIntervalsTogether_1() throws Exception
{
ArrayList<CheckmarkList.Interval> original = new ArrayList<>();
original.add(new CheckmarkList.Interval(day(40), day(40), day(34)));
original.add(new CheckmarkList.Interval(day(25), day(25), day(19)));
original.add(new CheckmarkList.Interval(day(16), day(16), day(10)));
original.add(new CheckmarkList.Interval(day(8), day(8), day(2)));
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
expected.add(new CheckmarkList.Interval(day(40), day(40), day(34)));
expected.add(new CheckmarkList.Interval(day(25), day(25), day(19)));
expected.add(new CheckmarkList.Interval(day(18), day(16), day(12)));
expected.add(new CheckmarkList.Interval(day(11), day(8), day(5)));
CheckmarkList.snapIntervalsTogether(original);
assertThat(original, equalTo(expected));
}
@Test
public void test_writeCSV() throws IOException
{
String expectedCSV = "2015-01-25,2\n" +
"2015-01-24,0\n" +
"2015-01-23,1\n" +
"2015-01-22,2\n" +
"2015-01-21,2\n" +
"2015-01-20,2\n" +
"2015-01-19,0\n" +
"2015-01-18,1\n" +
"2015-01-17,2\n" +
String expectedCSV = "2015-01-25,2\n2015-01-24,0\n2015-01-23,1\n" +
"2015-01-22,2\n2015-01-21,2\n2015-01-20,2\n" +
"2015-01-19,1\n2015-01-18,1\n2015-01-17,2\n" +
"2015-01-16,2\n";
@ -201,9 +344,15 @@ public class CheckmarkListTest extends BaseUnitTest
assertThat(writer.toString(), equalTo(expectedCSV));
}
private long day(int offset)
{
return DateUtils.getStartOfToday() -
offset * DateUtils.millisecondsInOneDay;
}
private void travelInTime(int days)
{
DateUtils.setFixedLocalTime(
fixed_local_time + days * DateUtils.millisecondsInOneDay);
FIXED_LOCAL_TIME + days * DateUtils.millisecondsInOneDay);
}
}

@ -125,6 +125,28 @@ public class ScoreListTest extends BaseUnitTest
}
}
@Test
public void test_getValues()
{
toggleRepetitions(0, 20);
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
long from = today - 4 * day;
long to = today - 2 * day;
double[] expected = {
0.617008, 0.596033, 0.573909,
};
double[] actual = habit.getScores().getValues(from, to);
assertThat(actual.length, equalTo(expected.length));
for (int i = 0; i < actual.length; i++)
assertThat(actual[i], closeTo(expected[i], E));
}
@Test
public void test_groupBy()
{
@ -133,9 +155,9 @@ public class ScoreListTest extends BaseUnitTest
habit.getScores().groupBy(DateUtils.TruncateField.MONTH);
assertThat(list.size(), equalTo(5));
assertThat(list.get(0).getValue(), closeTo(0.549096, E));
assertThat(list.get(1).getValue(), closeTo(0.480098, E));
assertThat(list.get(2).getValue(), closeTo(0.377885, E));
assertThat(list.get(0).getValue(), closeTo(0.653659, E));
assertThat(list.get(1).getValue(), closeTo(0.622715, E));
assertThat(list.get(2).getValue(), closeTo(0.520997, E));
}
@Test
@ -157,16 +179,11 @@ public class ScoreListTest extends BaseUnitTest
{
Habit habit = fixtures.createShortHabit();
String expectedCSV = "2015-01-25,0.2372\n" +
"2015-01-24,0.2096\n" +
"2015-01-23,0.2172\n" +
"2015-01-22,0.1889\n" +
"2015-01-21,0.1595\n" +
"2015-01-20,0.1291\n" +
"2015-01-19,0.0976\n" +
"2015-01-18,0.1011\n" +
"2015-01-17,0.0686\n" +
"2015-01-16,0.0349\n";
String expectedCSV = "2015-01-25,0.2654\n2015-01-24,0.2389\n" +
"2015-01-23,0.2475\n2015-01-22,0.2203\n" +
"2015-01-21,0.1921\n2015-01-20,0.1628\n" +
"2015-01-19,0.1325\n2015-01-18,0.1011\n" +
"2015-01-17,0.0686\n2015-01-16,0.0349\n";
StringWriter writer = new StringWriter();
habit.getScores().writeCSV(writer);
@ -174,30 +191,6 @@ public class ScoreListTest extends BaseUnitTest
assertThat(writer.toString(), equalTo(expectedCSV));
}
@Test
public void test_getValues()
{
toggleRepetitions(0, 20);
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
long from = today - 4 * day;
long to = today - 2 * day;
double[] expected = {
0.617008,
0.596033,
0.573909,
};
double[] actual = habit.getScores().getValues(from, to);
assertThat(actual.length, equalTo(expected.length));
for(int i = 0; i < actual.length; i++)
assertThat(actual[i], closeTo(expected[i], E));
}
private void toggleRepetitions(final int from, final int to)
{
RepetitionList reps = habit.getRepetitions();

@ -99,6 +99,7 @@ public class ListHabitsBehaviorTest extends BaseUnitTest
when(system.getCSVOutputDir()).thenReturn(outputDir);
behavior.onExportCSV();
verify(screen).showMessage(COULD_NOT_EXPORT);
assertTrue(outputDir.delete());
}
@Test

Loading…
Cancel
Save