/*
* 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 android.support.annotation.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import java.text.*;
import java.util.*;
/**
* The collection of {@link Checkmark}s belonging to a habit.
*/
public abstract class CheckmarkList
{
protected Habit habit;
public ModelObservable observable = new ModelObservable();
public CheckmarkList(Habit habit)
{
this.habit = habit;
}
/**
* Adds all the given checkmarks to the list.
* <p>
* This should never be called by the application, since the checkmarks are
* computed automatically from the list of repetitions.
*
* @param checkmarks the checkmarks to be added.
*/
public abstract void add(List<Checkmark> checkmarks);
/**
* 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 final int[] getAllValues()
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0];
Long fromTimestamp = oldestRep.getTimestamp();
Long toTimestamp = DateUtils.getStartOfToday();
return getValues(fromTimestamp, toTimestamp);
}
/**
* Returns the list of checkmarks that fall within the given interval.
* <p>
* There is exactly one checkmark per day in the interval. The endpoints of
* the interval are included. The list is ordered by timestamp (decreasing).
* That is, the first checkmark corresponds to the newest timestamp, and the
* last checkmark corresponds to the oldest timestamp.
*
* @param fromTimestamp timestamp of the beginning of the interval.
* @param toTimestamp timestamp of the end of the interval.
* @return the list of checkmarks within the interval.
*/
@NonNull
public abstract List<Checkmark> getByInterval(long fromTimestamp,
long toTimestamp);
/**
* Returns the checkmark for today.
*
* @return checkmark for today
*/
@Nullable
public final Checkmark getToday()
{
computeAll();
return getNewestComputed();
}
/**
* Returns the value of today's checkmark.
*
* @return value of today's checkmark
*/
public final int getTodayValue()
{
Checkmark today = getToday();
if (today != null) return today.getValue();
else return Checkmark.UNCHECKED;
}
/**
* Returns the values of the checkmarks that fall inside a certain interval
* of time.
* <p>
* The values are returned in an array containing one integer value 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 values for the checkmarks inside the given interval
*/
public final int[] getValues(long from, long to)
{
if(from > to) return new int[0];
List<Checkmark> checkmarks = getByInterval(from, to);
int values[] = new int[checkmarks.size()];
int i = 0;
for (Checkmark c : checkmarks)
values[i++] = c.getValue();
return values;
}
/**
* Marks as invalid every checkmark that has timestamp either equal or newer
* than a given timestamp. These checkmarks will be recomputed at the next
* time they are queried.
*
* @param timestamp the timestamp
*/
public abstract void invalidateNewerThan(long timestamp);
/**
* Writes the entire list of checkmarks to the given writer, in CSV format.
*
* @param out the writer where the CSV will be output
* @throws IOException in case write operations fail
*/
public final void writeCSV(Writer out) throws IOException
{
computeAll();
int values[] = getAllValues();
long timestamp = DateUtils.getStartOfToday();
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
for (int value : values)
{
String date = dateFormat.format(new Date(timestamp));
out.write(String.format("%s,%d\n", date, value));
timestamp -= DateUtils.millisecondsInOneDay;
}
}
/**
* 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
*/
protected final synchronized void compute(long from, long to)
{
final long day = DateUtils.millisecondsInOneDay;
Checkmark newest = getNewestComputed();
Checkmark oldest = getOldestComputed();
if (newest == null)
{
forceRecompute(from, to);
}
else
{
forceRecompute(from, oldest.getTimestamp() - day);
forceRecompute(newest.getTimestamp() + day, to);
}
}
/**
* Returns oldest checkmark that has already been computed.
*
* @return oldest checkmark already computed
*/
protected abstract Checkmark getOldestComputed();
/**
* 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.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
*/
private synchronized void forceRecompute(long from, long to)
{
if (from > to) return;
final long day = DateUtils.millisecondsInOneDay;
Frequency freq = habit.getFrequency();
long fromExtended = from - (long) (freq.getDenominator()) * day;
List<Repetition> reps =
habit.getRepetitions().getByInterval(fromExtended, to);
final int nDays = (int) ((to - from) / day) + 1;
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
final int checks[] = new int[nDaysExtended];
for (Repetition rep : reps)
{
int offset = (int) ((rep.getTimestamp() - fromExtended) / day);
checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY;
}
for (int i = 0; i < nDays; i++)
{
int counter = 0;
for (int j = 0; j < freq.getDenominator(); j++)
if (checks[i + j] == 2) counter++;
if (counter >= freq.getNumerator())
if (checks[i] != Checkmark.CHECKED_EXPLICITLY)
checks[i] = Checkmark.CHECKED_IMPLICITLY;
}
List<Checkmark> checkmarks = new LinkedList<>();
for (int i = 0; i < nDays; i++)
{
int value = checks[i];
long timestamp = to - i * day;
checkmarks.add(new Checkmark(timestamp, value));
}
add(checkmarks);
}
/**
* 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.
*/
protected final void computeAll()
{
Repetition oldest = habit.getRepetitions().getOldest();
if (oldest == null) return;
Long today = DateUtils.getStartOfToday();
compute(oldest.getTimestamp(), today);
}
/**
* Returns newest checkmark that has already been computed.
*
* @return newest checkmark already computed
*/
protected abstract Checkmark getNewestComputed();
}