|
|
@ -19,8 +19,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
package org.isoron.uhabits.core.models;
|
|
|
|
package org.isoron.uhabits.core.models;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.*;
|
|
|
|
import androidx.annotation.*;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
import org.apache.commons.lang3.builder.*;
|
|
|
|
import org.apache.commons.lang3.builder.*;
|
|
|
|
import org.isoron.uhabits.core.utils.*;
|
|
|
|
import org.isoron.uhabits.core.utils.*;
|
|
|
|
|
|
|
|
|
|
|
@ -33,58 +35,67 @@ import javax.annotation.concurrent.*;
|
|
|
|
import static org.isoron.uhabits.core.models.Checkmark.*;
|
|
|
|
import static org.isoron.uhabits.core.models.Checkmark.*;
|
|
|
|
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
|
|
|
|
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* The collection of {@link Checkmark}s belonging to a habit.
|
|
|
|
* The collection of {@link Checkmark}s belonging to a habit.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@ThreadSafe
|
|
|
|
@ThreadSafe
|
|
|
|
public abstract class CheckmarkList
|
|
|
|
public abstract class CheckmarkList {
|
|
|
|
{
|
|
|
|
|
|
|
|
protected final Habit habit;
|
|
|
|
protected final Habit habit;
|
|
|
|
|
|
|
|
|
|
|
|
public final ModelObservable observable;
|
|
|
|
public final ModelObservable observable;
|
|
|
|
|
|
|
|
|
|
|
|
public CheckmarkList(Habit habit)
|
|
|
|
|
|
|
|
{
|
|
|
|
public CheckmarkList(Habit habit) {
|
|
|
|
this.habit = habit;
|
|
|
|
this.habit = habit;
|
|
|
|
this.observable = new ModelObservable();
|
|
|
|
this.observable = new ModelObservable();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
@NonNull
|
|
|
|
static List<Checkmark> buildCheckmarksFromIntervals(Repetition[] reps,
|
|
|
|
static List<Checkmark> buildCheckmarksFromIntervals(
|
|
|
|
ArrayList<Interval> intervals)
|
|
|
|
Repetition[] reps,
|
|
|
|
{
|
|
|
|
ArrayList<Interval> intervals
|
|
|
|
if (reps.length == 0) throw new IllegalArgumentException();
|
|
|
|
) {
|
|
|
|
|
|
|
|
if (reps.length == 0) {
|
|
|
|
|
|
|
|
throw new IllegalArgumentException();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Timestamp today = DateUtils.getToday();
|
|
|
|
Timestamp today = DateUtils.getToday();
|
|
|
|
Timestamp begin = reps[0].getTimestamp();
|
|
|
|
Timestamp begin = reps[0].getTimestamp();
|
|
|
|
if (intervals.size() > 0) begin = Timestamp.oldest(begin, intervals.get(0).begin);
|
|
|
|
if (intervals.size() > 0) {
|
|
|
|
|
|
|
|
begin = Timestamp.oldest(begin, intervals.get(0).begin);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int nDays = begin.daysUntil(today) + 1;
|
|
|
|
int nDays = begin.daysUntil(today) + 1;
|
|
|
|
List<Checkmark> checkmarks = new ArrayList<>(nDays);
|
|
|
|
List<Checkmark> checkmarks = new ArrayList<>(nDays);
|
|
|
|
for (int i = 0; i < nDays; i++)
|
|
|
|
for (int i = 0; i < nDays; i++) {
|
|
|
|
checkmarks.add(new Checkmark(today.minus(i), NO));
|
|
|
|
checkmarks.add(new Checkmark(today.minus(i), NO, false));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (Interval interval : intervals)
|
|
|
|
for (Interval interval : intervals) {
|
|
|
|
{
|
|
|
|
for (int i = 0; i < interval.length(); i++) {
|
|
|
|
for (int i = 0; i < interval.length(); i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
Timestamp date = interval.begin.plus(i);
|
|
|
|
Timestamp date = interval.begin.plus(i);
|
|
|
|
int offset = date.daysUntil(today);
|
|
|
|
int offset = date.daysUntil(today);
|
|
|
|
if (offset < 0) continue;
|
|
|
|
if (offset < 0) {
|
|
|
|
checkmarks.set(offset, new Checkmark(date, YES_AUTO));
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
checkmarks.set(offset, new Checkmark(date, YES, false));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (Repetition rep : reps)
|
|
|
|
for (Repetition rep : reps) {
|
|
|
|
{
|
|
|
|
|
|
|
|
Timestamp date = rep.getTimestamp();
|
|
|
|
Timestamp date = rep.getTimestamp();
|
|
|
|
int offset = date.daysUntil(today);
|
|
|
|
int offset = date.daysUntil(today);
|
|
|
|
checkmarks.set(offset, new Checkmark(date, rep.getValue()));
|
|
|
|
checkmarks.set(offset, new Checkmark(date, rep.getValue(), rep.isManualInput()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return checkmarks;
|
|
|
|
return checkmarks;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* For non-daily habits, some groups of repetitions generate many
|
|
|
|
* For non-daily habits, some groups of repetitions generate many
|
|
|
|
* checkmarks. For example, for weekly habits, each repetition generates
|
|
|
|
* checkmarks. For example, for weekly habits, each repetition generates
|
|
|
@ -97,25 +108,29 @@ public abstract class CheckmarkList
|
|
|
|
* away in the future as possible.
|
|
|
|
* away in the future as possible.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@NonNull
|
|
|
|
@NonNull
|
|
|
|
static ArrayList<Interval> buildIntervals(@NonNull Frequency freq,
|
|
|
|
static ArrayList<Interval> buildIntervals(
|
|
|
|
@NonNull Repetition[] reps)
|
|
|
|
@NonNull Frequency freq,
|
|
|
|
{
|
|
|
|
@NonNull Repetition[] reps
|
|
|
|
|
|
|
|
) {
|
|
|
|
ArrayList<Repetition> filteredReps = new ArrayList<>();
|
|
|
|
ArrayList<Repetition> filteredReps = new ArrayList<>();
|
|
|
|
for (Repetition r : reps)
|
|
|
|
for (Repetition r : reps) {
|
|
|
|
if (r.getValue() == YES_MANUAL)
|
|
|
|
if (r.getValue() == YES && r.isManualInput()) {
|
|
|
|
filteredReps.add(r);
|
|
|
|
filteredReps.add(r);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int num = freq.getNumerator();
|
|
|
|
int num = freq.getNumerator();
|
|
|
|
int den = freq.getDenominator();
|
|
|
|
int den = freq.getDenominator();
|
|
|
|
|
|
|
|
|
|
|
|
ArrayList<Interval> intervals = new ArrayList<>();
|
|
|
|
ArrayList<Interval> intervals = new ArrayList<>();
|
|
|
|
for (int i = 0; i < filteredReps.size() - num + 1; i++)
|
|
|
|
for (int i = 0; i < filteredReps.size() - num + 1; i++) {
|
|
|
|
{
|
|
|
|
|
|
|
|
Repetition first = filteredReps.get(i);
|
|
|
|
Repetition first = filteredReps.get(i);
|
|
|
|
Repetition last = filteredReps.get(i + num - 1);
|
|
|
|
Repetition last = filteredReps.get(i + num - 1);
|
|
|
|
|
|
|
|
|
|
|
|
long distance = first.getTimestamp().daysUntil(last.getTimestamp());
|
|
|
|
long distance = first.getTimestamp().daysUntil(last.getTimestamp());
|
|
|
|
if (distance >= den) continue;
|
|
|
|
if (distance >= den) {
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Timestamp begin = first.getTimestamp();
|
|
|
|
Timestamp begin = first.getTimestamp();
|
|
|
|
Timestamp center = last.getTimestamp();
|
|
|
|
Timestamp center = last.getTimestamp();
|
|
|
@ -126,32 +141,33 @@ public abstract class CheckmarkList
|
|
|
|
return intervals;
|
|
|
|
return intervals;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Starting from the second newest interval, this function tries to slide the
|
|
|
|
* Starting from the second newest interval, this function tries to slide the
|
|
|
|
* intervals backwards into the past, so that gaps are eliminated and
|
|
|
|
* intervals backwards into the past, so that gaps are eliminated and
|
|
|
|
* streaks are maximized.
|
|
|
|
* streaks are maximized.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
static void snapIntervalsTogether(@NonNull ArrayList<Interval> intervals)
|
|
|
|
static void snapIntervalsTogether(@NonNull ArrayList<Interval> intervals) {
|
|
|
|
{
|
|
|
|
|
|
|
|
int n = intervals.size();
|
|
|
|
int n = intervals.size();
|
|
|
|
for (int i = n - 2; i >= 0; i--)
|
|
|
|
for (int i = n - 2; i >= 0; i--) {
|
|
|
|
{
|
|
|
|
|
|
|
|
Interval curr = intervals.get(i);
|
|
|
|
Interval curr = intervals.get(i);
|
|
|
|
Interval next = intervals.get(i + 1);
|
|
|
|
Interval next = intervals.get(i + 1);
|
|
|
|
|
|
|
|
|
|
|
|
int gapNextToCurrent = next.begin.daysUntil(curr.end);
|
|
|
|
int gapNextToCurrent = next.begin.daysUntil(curr.end);
|
|
|
|
int gapCenterToEnd = curr.center.daysUntil(curr.end);
|
|
|
|
int gapCenterToEnd = curr.center.daysUntil(curr.end);
|
|
|
|
|
|
|
|
|
|
|
|
if (gapNextToCurrent >= 0)
|
|
|
|
if (gapNextToCurrent >= 0) {
|
|
|
|
{
|
|
|
|
|
|
|
|
int shift = Math.min(gapCenterToEnd, gapNextToCurrent + 1);
|
|
|
|
int shift = Math.min(gapCenterToEnd, gapNextToCurrent + 1);
|
|
|
|
intervals.set(i, new Interval(curr.begin.minus(shift),
|
|
|
|
intervals.set(i, new Interval(
|
|
|
|
curr.center,
|
|
|
|
curr.begin.minus(shift),
|
|
|
|
curr.end.minus(shift)));
|
|
|
|
curr.center,
|
|
|
|
|
|
|
|
curr.end.minus(shift)
|
|
|
|
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Adds all the given checkmarks to the list.
|
|
|
|
* Adds all the given checkmarks to the list.
|
|
|
|
* <p>
|
|
|
|
* <p>
|
|
|
@ -162,6 +178,7 @@ public abstract class CheckmarkList
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
public abstract void add(List<Checkmark> checkmarks);
|
|
|
|
public abstract void add(List<Checkmark> checkmarks);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns the values for all the checkmarks, since the oldest repetition of
|
|
|
|
* Returns the values for all the checkmarks, since the oldest repetition of
|
|
|
|
* the habit until today.
|
|
|
|
* the habit until today.
|
|
|
@ -175,10 +192,11 @@ public abstract class CheckmarkList
|
|
|
|
* @return values for the checkmarks in the interval
|
|
|
|
* @return values for the checkmarks in the interval
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@NonNull
|
|
|
|
@NonNull
|
|
|
|
public synchronized final int[] getAllValues()
|
|
|
|
public synchronized final int[] getAllValues() {
|
|
|
|
{
|
|
|
|
|
|
|
|
Repetition oldestRep = habit.getRepetitions().getOldest();
|
|
|
|
Repetition oldestRep = habit.getRepetitions().getOldest();
|
|
|
|
if (oldestRep == null) return new int[0];
|
|
|
|
if (oldestRep == null) {
|
|
|
|
|
|
|
|
return new int[0];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Timestamp fromTimestamp = oldestRep.getTimestamp();
|
|
|
|
Timestamp fromTimestamp = oldestRep.getTimestamp();
|
|
|
|
Timestamp toTimestamp = DateUtils.getToday();
|
|
|
|
Timestamp toTimestamp = DateUtils.getToday();
|
|
|
@ -186,6 +204,33 @@ public abstract class CheckmarkList
|
|
|
|
return getValues(fromTimestamp, toTimestamp);
|
|
|
|
return getValues(fromTimestamp, toTimestamp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Returns the values for all the checkmarks, since the oldest repetition of
|
|
|
|
|
|
|
|
* the habit until today.
|
|
|
|
|
|
|
|
* <p>
|
|
|
|
|
|
|
|
* If there are no repetitions at all, returns an empty array. The values
|
|
|
|
|
|
|
|
* are returned in an array containing one integer value for each day since
|
|
|
|
|
|
|
|
* the first repetition of the habit until today. The first entry
|
|
|
|
|
|
|
|
* corresponds to today, the second entry corresponds to yesterday, and so
|
|
|
|
|
|
|
|
* on.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @return values for the checkmarks in the interval
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
|
|
|
|
public synchronized final CheckmarkState[] getAllStates() {
|
|
|
|
|
|
|
|
Repetition oldestRep = habit.getRepetitions().getOldest();
|
|
|
|
|
|
|
|
if (oldestRep == null) {
|
|
|
|
|
|
|
|
return new CheckmarkState[0];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Timestamp fromTimestamp = oldestRep.getTimestamp();
|
|
|
|
|
|
|
|
Timestamp toTimestamp = DateUtils.getToday();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return getCheckmarkStates(fromTimestamp, toTimestamp);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns the list of checkmarks that fall within the given interval.
|
|
|
|
* Returns the list of checkmarks that fall within the given interval.
|
|
|
|
* <p>
|
|
|
|
* <p>
|
|
|
@ -196,11 +241,15 @@ public abstract class CheckmarkList
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param fromTimestamp timestamp of the beginning of the interval.
|
|
|
|
* @param fromTimestamp timestamp of the beginning of the interval.
|
|
|
|
* @param toTimestamp timestamp of the end of the interval.
|
|
|
|
* @param toTimestamp timestamp of the end of the interval.
|
|
|
|
|
|
|
|
*
|
|
|
|
* @return the list of checkmarks within the interval.
|
|
|
|
* @return the list of checkmarks within the interval.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@NonNull
|
|
|
|
@NonNull
|
|
|
|
public abstract List<Checkmark> getByInterval(Timestamp fromTimestamp,
|
|
|
|
public abstract List<Checkmark> getByInterval(
|
|
|
|
Timestamp toTimestamp);
|
|
|
|
Timestamp fromTimestamp,
|
|
|
|
|
|
|
|
Timestamp toTimestamp
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns the checkmark for today.
|
|
|
|
* Returns the checkmark for today.
|
|
|
@ -208,54 +257,72 @@ public abstract class CheckmarkList
|
|
|
|
* @return checkmark for today
|
|
|
|
* @return checkmark for today
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
@Nullable
|
|
|
|
@Nullable
|
|
|
|
public synchronized final Checkmark getToday()
|
|
|
|
public synchronized final Checkmark getToday() {
|
|
|
|
{
|
|
|
|
|
|
|
|
compute();
|
|
|
|
compute();
|
|
|
|
Timestamp today = DateUtils.getToday();
|
|
|
|
Timestamp today = DateUtils.getToday();
|
|
|
|
return getByInterval(today, today).get(0);
|
|
|
|
return getByInterval(today, today).get(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns the value of today's checkmark.
|
|
|
|
* Returns the value of today's checkmark.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @return value of today's checkmark
|
|
|
|
* @return value of today's checkmark
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
public synchronized int getTodayValue()
|
|
|
|
public synchronized int getTodayValue() {
|
|
|
|
{
|
|
|
|
|
|
|
|
Checkmark today = getToday();
|
|
|
|
Checkmark today = getToday();
|
|
|
|
if (today != null) return today.getValue();
|
|
|
|
if (today != null) {
|
|
|
|
else return NO;
|
|
|
|
return today.getValue();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return NO;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public synchronized int getThisWeekValue(int firstWeekday)
|
|
|
|
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Returns the value of today's checkmark.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @return value of today's checkmark
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
public synchronized CheckmarkState getTodayState() {
|
|
|
|
|
|
|
|
Checkmark today = getToday();
|
|
|
|
|
|
|
|
if (today != null) {
|
|
|
|
|
|
|
|
return new CheckmarkState(today.getValue(), today.isManualInput());
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return new CheckmarkState(NO, false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public synchronized int getThisWeekValue(int firstWeekday) {
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday);
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public synchronized int getThisMonthValue()
|
|
|
|
|
|
|
|
{
|
|
|
|
public synchronized int getThisMonthValue() {
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.MONTH, Calendar.SATURDAY);
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.MONTH, Calendar.SATURDAY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public synchronized int getThisQuarterValue()
|
|
|
|
public synchronized int getThisQuarterValue() {
|
|
|
|
{
|
|
|
|
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.QUARTER, Calendar.SATURDAY);
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.QUARTER, Calendar.SATURDAY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public synchronized int getThisYearValue()
|
|
|
|
public synchronized int getThisYearValue() {
|
|
|
|
{
|
|
|
|
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.YEAR, Calendar.SATURDAY);
|
|
|
|
return getThisIntervalValue(DateUtils.TruncateField.YEAR, Calendar.SATURDAY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private int getThisIntervalValue(DateUtils.TruncateField truncateField, int firstWeekday)
|
|
|
|
|
|
|
|
{
|
|
|
|
private int getThisIntervalValue(DateUtils.TruncateField truncateField, int firstWeekday) {
|
|
|
|
List<Checkmark> groups = habit.getCheckmarks().groupBy(truncateField, firstWeekday, 1);
|
|
|
|
List<Checkmark> groups = habit.getCheckmarks().groupBy(truncateField, firstWeekday, 1);
|
|
|
|
if (groups.isEmpty()) return 0;
|
|
|
|
if (groups.isEmpty()) {
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
return groups.get(0).getValue();
|
|
|
|
return groups.get(0).getValue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns the values of the checkmarks that fall inside a certain interval
|
|
|
|
* Returns the values of the checkmarks that fall inside a certain interval
|
|
|
|
* of time.
|
|
|
|
* of time.
|
|
|
@ -267,22 +334,54 @@ public abstract class CheckmarkList
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param from timestamp for the oldest checkmark
|
|
|
|
* @param from timestamp for the oldest checkmark
|
|
|
|
* @param to timestamp for the newest checkmark
|
|
|
|
* @param to timestamp for the newest checkmark
|
|
|
|
|
|
|
|
*
|
|
|
|
* @return values for the checkmarks inside the given interval
|
|
|
|
* @return values for the checkmarks inside the given interval
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
public final int[] getValues(Timestamp from, Timestamp to)
|
|
|
|
public final int[] getValues(Timestamp from, Timestamp to) {
|
|
|
|
{
|
|
|
|
if (from.isNewerThan(to)) {
|
|
|
|
if (from.isNewerThan(to)) return new int[0];
|
|
|
|
return new int[0];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
List<Checkmark> checkmarks = getByInterval(from, to);
|
|
|
|
List<Checkmark> checkmarks = getByInterval(from, to);
|
|
|
|
int values[] = new int[checkmarks.size()];
|
|
|
|
int values[] = new int[checkmarks.size()];
|
|
|
|
|
|
|
|
|
|
|
|
int i = 0;
|
|
|
|
int i = 0;
|
|
|
|
for (Checkmark c : checkmarks)
|
|
|
|
for (Checkmark c : checkmarks) {
|
|
|
|
values[i++] = c.getValue();
|
|
|
|
values[i++] = c.getValue();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return values;
|
|
|
|
return values;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Returns the checkmarkStates that fall inside a certain interval
|
|
|
|
|
|
|
|
* of time.
|
|
|
|
|
|
|
|
* <p>
|
|
|
|
|
|
|
|
* The values are returned in an array containing the checkmark for each
|
|
|
|
|
|
|
|
* day of the interval. The first entry corresponds to the most recent day
|
|
|
|
|
|
|
|
* in the interval. Each subsequent entry corresponds to one day older than
|
|
|
|
|
|
|
|
* the previous entry. The boundaries of the time interval are included.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @param from timestamp for the oldest checkmark
|
|
|
|
|
|
|
|
* @param to timestamp for the newest checkmark
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* @return the checkmarks inside the given interval
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
public final CheckmarkState[] getCheckmarkStates(Timestamp from, Timestamp to) {
|
|
|
|
|
|
|
|
if (from.isNewerThan(to)) {
|
|
|
|
|
|
|
|
return new CheckmarkState[0];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final List<Checkmark> checkmarks = getByInterval(from, to);
|
|
|
|
|
|
|
|
final CheckmarkState[] checkmarkStates = new CheckmarkState[checkmarks.size()];
|
|
|
|
|
|
|
|
for (int i = 0; i < checkmarks.size(); i++) {
|
|
|
|
|
|
|
|
checkmarkStates[i] = checkmarks.get(i).getState();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return checkmarkStates;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Marks as invalid every checkmark that has timestamp either equal or newer
|
|
|
|
* Marks as invalid every checkmark that has timestamp either equal or newer
|
|
|
|
* than a given timestamp. These checkmarks will be recomputed at the next
|
|
|
|
* than a given timestamp. These checkmarks will be recomputed at the next
|
|
|
@ -292,47 +391,50 @@ public abstract class CheckmarkList
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
public abstract void invalidateNewerThan(Timestamp timestamp);
|
|
|
|
public abstract void invalidateNewerThan(Timestamp timestamp);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Writes the entire list of checkmarks to the given writer, in CSV format.
|
|
|
|
* Writes the entire list of checkmarks to the given writer, in CSV format.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param out the writer where the CSV will be output
|
|
|
|
* @param out the writer where the CSV will be output
|
|
|
|
|
|
|
|
*
|
|
|
|
* @throws IOException in case write operations fail
|
|
|
|
* @throws IOException in case write operations fail
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
public final void writeCSV(Writer out) throws IOException
|
|
|
|
public final void writeCSV(Writer out) throws IOException {
|
|
|
|
{
|
|
|
|
CheckmarkState[] states;
|
|
|
|
int values[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
synchronized (this)
|
|
|
|
synchronized (this) {
|
|
|
|
{
|
|
|
|
|
|
|
|
compute();
|
|
|
|
compute();
|
|
|
|
values = getAllValues();
|
|
|
|
states = getAllStates();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Timestamp timestamp = DateUtils.getToday();
|
|
|
|
Timestamp timestamp = DateUtils.getToday();
|
|
|
|
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
|
|
|
|
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
|
|
|
|
|
|
|
|
|
|
|
|
for (int value : values)
|
|
|
|
for (CheckmarkState state : states) {
|
|
|
|
{
|
|
|
|
|
|
|
|
String date = dateFormat.format(timestamp.toJavaDate());
|
|
|
|
String date = dateFormat.format(timestamp.toJavaDate());
|
|
|
|
out.write(String.format("%s,%d\n", date, value));
|
|
|
|
out.write(String.format("%s,%d,%d\n", date, state.getValue(), state.isManualInput() ? 1 : 0));
|
|
|
|
timestamp = timestamp.minus(1);
|
|
|
|
timestamp = timestamp.minus(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Computes and stores one checkmark for each day, from the first habit
|
|
|
|
* Computes and stores one checkmark for each day, from the first habit
|
|
|
|
* repetition to today. If this list is already computed, does nothing.
|
|
|
|
* repetition to today. If this list is already computed, does nothing.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
protected final synchronized void compute()
|
|
|
|
protected final synchronized void compute() {
|
|
|
|
{
|
|
|
|
|
|
|
|
final Timestamp today = DateUtils.getToday();
|
|
|
|
final Timestamp today = DateUtils.getToday();
|
|
|
|
|
|
|
|
|
|
|
|
Checkmark newest = getNewestComputed();
|
|
|
|
Checkmark newest = getNewestComputed();
|
|
|
|
if (newest != null && newest.getTimestamp().equals(today)) return;
|
|
|
|
if (newest != null && newest.getTimestamp().equals(today)) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
invalidateNewerThan(Timestamp.ZERO);
|
|
|
|
invalidateNewerThan(Timestamp.ZERO);
|
|
|
|
|
|
|
|
|
|
|
|
Repetition oldestRep = habit.getRepetitions().getOldest();
|
|
|
|
Repetition oldestRep = habit.getRepetitions().getOldest();
|
|
|
|
if (oldestRep == null) return;
|
|
|
|
if (oldestRep == null) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
final Timestamp from = oldestRep.getTimestamp();
|
|
|
|
final Timestamp from = oldestRep.getTimestamp();
|
|
|
|
|
|
|
|
|
|
|
|
Repetition reps[] = habit
|
|
|
|
Repetition reps[] = habit
|
|
|
@ -340,10 +442,14 @@ public abstract class CheckmarkList
|
|
|
|
.getByInterval(from, today)
|
|
|
|
.getByInterval(from, today)
|
|
|
|
.toArray(new Repetition[0]);
|
|
|
|
.toArray(new Repetition[0]);
|
|
|
|
|
|
|
|
|
|
|
|
if (habit.isNumerical()) computeNumerical(reps);
|
|
|
|
if (habit.isNumerical()) {
|
|
|
|
else computeYesNo(reps);
|
|
|
|
computeNumerical(reps);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
computeYesNo(reps);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Returns newest checkmark that has already been computed.
|
|
|
|
* Returns newest checkmark that has already been computed.
|
|
|
|
*
|
|
|
|
*
|
|
|
@ -360,66 +466,76 @@ public abstract class CheckmarkList
|
|
|
|
@Nullable
|
|
|
|
@Nullable
|
|
|
|
protected abstract Checkmark getOldestComputed();
|
|
|
|
protected abstract Checkmark getOldestComputed();
|
|
|
|
|
|
|
|
|
|
|
|
private void computeNumerical(Repetition[] reps)
|
|
|
|
|
|
|
|
{
|
|
|
|
private void computeNumerical(Repetition[] reps) {
|
|
|
|
if (reps.length == 0) return;
|
|
|
|
if (reps.length == 0) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Timestamp today = DateUtils.getToday();
|
|
|
|
Timestamp today = DateUtils.getToday();
|
|
|
|
Timestamp begin = reps[0].getTimestamp();
|
|
|
|
Timestamp begin = reps[0].getTimestamp();
|
|
|
|
|
|
|
|
|
|
|
|
int nDays = begin.daysUntil(today) + 1;
|
|
|
|
int nDays = begin.daysUntil(today) + 1;
|
|
|
|
List<Checkmark> checkmarks = new ArrayList<>(nDays);
|
|
|
|
List<Checkmark> checkmarks = new ArrayList<>(nDays);
|
|
|
|
for (int i = 0; i < nDays; i++)
|
|
|
|
for (int i = 0; i < nDays; i++) {
|
|
|
|
checkmarks.add(new Checkmark(today.minus(i), 0));
|
|
|
|
checkmarks.add(new Checkmark(today.minus(i), 0, false));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (Repetition rep : reps)
|
|
|
|
for (Repetition rep : reps) {
|
|
|
|
{
|
|
|
|
|
|
|
|
int offset = rep.getTimestamp().daysUntil(today);
|
|
|
|
int offset = rep.getTimestamp().daysUntil(today);
|
|
|
|
checkmarks.set(offset, new Checkmark(rep.getTimestamp(), rep.getValue()));
|
|
|
|
checkmarks.set(offset, new Checkmark(rep.getTimestamp(), rep.getValue(), rep.isManualInput()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
add(checkmarks);
|
|
|
|
add(checkmarks);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void computeYesNo(Repetition[] reps)
|
|
|
|
|
|
|
|
{
|
|
|
|
private void computeYesNo(Repetition[] reps) {
|
|
|
|
ArrayList<Interval> intervals;
|
|
|
|
ArrayList<Interval> intervals;
|
|
|
|
intervals = buildIntervals(habit.getFrequency(), reps);
|
|
|
|
intervals = buildIntervals(habit.getFrequency(), reps);
|
|
|
|
snapIntervalsTogether(intervals);
|
|
|
|
snapIntervalsTogether(intervals);
|
|
|
|
add(buildCheckmarksFromIntervals(reps, intervals));
|
|
|
|
add(buildCheckmarksFromIntervals(reps, intervals));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public List<Checkmark> getAll() {
|
|
|
|
public List<Checkmark> getAll() {
|
|
|
|
Repetition oldest = habit.getRepetitions().getOldest();
|
|
|
|
Repetition oldest = habit.getRepetitions().getOldest();
|
|
|
|
if(oldest == null) return new ArrayList<>();
|
|
|
|
if (oldest == null) {
|
|
|
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
|
|
|
|
}
|
|
|
|
return getByInterval(oldest.getTimestamp(), DateUtils.getToday());
|
|
|
|
return getByInterval(oldest.getTimestamp(), DateUtils.getToday());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static final class Interval
|
|
|
|
|
|
|
|
{
|
|
|
|
static final class Interval {
|
|
|
|
final Timestamp begin;
|
|
|
|
final Timestamp begin;
|
|
|
|
|
|
|
|
|
|
|
|
final Timestamp center;
|
|
|
|
final Timestamp center;
|
|
|
|
|
|
|
|
|
|
|
|
final Timestamp end;
|
|
|
|
final Timestamp end;
|
|
|
|
|
|
|
|
|
|
|
|
Interval(Timestamp begin, Timestamp center, Timestamp end)
|
|
|
|
|
|
|
|
{
|
|
|
|
Interval(Timestamp begin, Timestamp center, Timestamp end) {
|
|
|
|
this.begin = begin;
|
|
|
|
this.begin = begin;
|
|
|
|
this.center = center;
|
|
|
|
this.center = center;
|
|
|
|
this.end = end;
|
|
|
|
this.end = end;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public int length() {
|
|
|
|
public int length() {
|
|
|
|
return begin.daysUntil(end) + 1;
|
|
|
|
return begin.daysUntil(end) + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
@Override
|
|
|
|
public boolean equals(Object o)
|
|
|
|
public boolean equals(Object o) {
|
|
|
|
{
|
|
|
|
if (this == o) {
|
|
|
|
if (this == o) return true;
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
|
|
if (o == null || getClass() != o.getClass()) {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Interval interval = (Interval) o;
|
|
|
|
Interval interval = (Interval) o;
|
|
|
|
|
|
|
|
|
|
|
@ -430,9 +546,9 @@ public abstract class CheckmarkList
|
|
|
|
.isEquals();
|
|
|
|
.isEquals();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
@Override
|
|
|
|
public int hashCode()
|
|
|
|
public int hashCode() {
|
|
|
|
{
|
|
|
|
|
|
|
|
return new HashCodeBuilder(17, 37)
|
|
|
|
return new HashCodeBuilder(17, 37)
|
|
|
|
.append(begin)
|
|
|
|
.append(begin)
|
|
|
|
.append(center)
|
|
|
|
.append(center)
|
|
|
@ -440,9 +556,9 @@ public abstract class CheckmarkList
|
|
|
|
.toHashCode();
|
|
|
|
.toHashCode();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
@Override
|
|
|
|
public String toString()
|
|
|
|
public String toString() {
|
|
|
|
{
|
|
|
|
|
|
|
|
return new ToStringBuilder(this, defaultToStringStyle())
|
|
|
|
return new ToStringBuilder(this, defaultToStringStyle())
|
|
|
|
.append("begin", begin)
|
|
|
|
.append("begin", begin)
|
|
|
|
.append("center", center)
|
|
|
|
.append("center", center)
|
|
|
@ -451,44 +567,47 @@ public abstract class CheckmarkList
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
@NonNull
|
|
|
|
public List<Checkmark> groupBy(DateUtils.TruncateField field, int firstWeekday)
|
|
|
|
public List<Checkmark> groupBy(DateUtils.TruncateField field, int firstWeekday) {
|
|
|
|
{
|
|
|
|
|
|
|
|
return groupBy(field, firstWeekday, 0);
|
|
|
|
return groupBy(field, firstWeekday, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
@NonNull
|
|
|
|
public List<Checkmark> groupBy(DateUtils.TruncateField field,
|
|
|
|
public List<Checkmark> groupBy(
|
|
|
|
int firstWeekday,
|
|
|
|
DateUtils.TruncateField field,
|
|
|
|
int maxGroups)
|
|
|
|
int firstWeekday,
|
|
|
|
{
|
|
|
|
int maxGroups
|
|
|
|
|
|
|
|
) {
|
|
|
|
List<Checkmark> checks = getAll();
|
|
|
|
List<Checkmark> checks = getAll();
|
|
|
|
|
|
|
|
|
|
|
|
int count = 0;
|
|
|
|
int count = 0;
|
|
|
|
Timestamp[] truncatedTimestamps = new Timestamp[checks.size()];
|
|
|
|
Timestamp[] truncatedTimestamps = new Timestamp[checks.size()];
|
|
|
|
int[] values = new int[checks.size()];
|
|
|
|
int[] values = new int[checks.size()];
|
|
|
|
|
|
|
|
boolean[] manualInputs = new boolean[checks.size()];
|
|
|
|
|
|
|
|
|
|
|
|
for (Checkmark rep : checks)
|
|
|
|
for (Checkmark rep : checks) {
|
|
|
|
{
|
|
|
|
|
|
|
|
Timestamp tt = rep.getTimestamp().truncate(field, firstWeekday);
|
|
|
|
Timestamp tt = rep.getTimestamp().truncate(field, firstWeekday);
|
|
|
|
if (count == 0 || !truncatedTimestamps[count - 1].equals(tt))
|
|
|
|
if (count == 0 || !truncatedTimestamps[count - 1].equals(tt)) {
|
|
|
|
{
|
|
|
|
if (maxGroups > 0 && count >= maxGroups) {
|
|
|
|
if (maxGroups > 0 && count >= maxGroups) break;
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
truncatedTimestamps[count++] = tt;
|
|
|
|
truncatedTimestamps[count++] = tt;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if(habit.isNumerical())
|
|
|
|
if (habit.isNumerical()) {
|
|
|
|
values[count - 1] += rep.getValue();
|
|
|
|
values[count - 1] += rep.getValue();
|
|
|
|
else if(rep.getValue() == Checkmark.YES_MANUAL)
|
|
|
|
} else if (rep.getValue() == Checkmark.YES && rep.isManualInput()) {
|
|
|
|
values[count - 1] += 1000;
|
|
|
|
values[count - 1] += 1000;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
manualInputs[count - 1] = rep.isManualInput();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ArrayList<Checkmark> groupedCheckmarks = new ArrayList<>();
|
|
|
|
ArrayList<Checkmark> groupedCheckmarks = new ArrayList<>();
|
|
|
|
for (int i = 0; i < count; i++)
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
{
|
|
|
|
Checkmark rep = new Checkmark(truncatedTimestamps[i], values[i], manualInputs[i]);
|
|
|
|
Checkmark rep = new Checkmark(truncatedTimestamps[i], values[i]);
|
|
|
|
|
|
|
|
groupedCheckmarks.add(rep);
|
|
|
|
groupedCheckmarks.add(rep);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|