mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Move remaining model classes
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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.*;
|
||||
|
||||
import javax.annotation.concurrent.*;
|
||||
|
||||
import static org.isoron.uhabits.models.Checkmark.*;
|
||||
/**
|
||||
* The collection of {@link Checkmark}s belonging to a habit.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public abstract class CheckmarkList
|
||||
{
|
||||
protected final Habit habit;
|
||||
|
||||
public final ModelObservable observable;
|
||||
|
||||
public CheckmarkList(Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
this.observable = new ModelObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 synchronized 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 synchronized final Checkmark getToday()
|
||||
{
|
||||
computeAll();
|
||||
return getNewestComputed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of today's checkmark.
|
||||
*
|
||||
* @return value of today's checkmark
|
||||
*/
|
||||
public synchronized 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
|
||||
{
|
||||
int values[];
|
||||
|
||||
synchronized (this)
|
||||
{
|
||||
computeAll();
|
||||
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] = rep.getValue();
|
||||
}
|
||||
|
||||
for (int i = 0; i < nDays; i++)
|
||||
{
|
||||
int counter = 0;
|
||||
|
||||
for (int j = 0; j < freq.getDenominator(); j++)
|
||||
if (checks[i + j] == CHECKED_EXPLICITLY) counter++;
|
||||
|
||||
if (counter >= freq.getNumerator())
|
||||
if (checks[i] != CHECKED_EXPLICITLY)
|
||||
checks[i] = 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.
|
||||
*/
|
||||
private synchronized 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();
|
||||
}
|
||||
450
uhabits-core/src/main/java/org/isoron/uhabits/models/Habit.java
Normal file
450
uhabits-core/src/main/java/org/isoron/uhabits/models/Habit.java
Normal file
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
* 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.apache.commons.lang3.builder.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.annotation.concurrent.*;
|
||||
import javax.inject.*;
|
||||
|
||||
import static org.isoron.uhabits.models.Checkmark.*;
|
||||
|
||||
/**
|
||||
* The thing that the user wants to track.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public class Habit
|
||||
{
|
||||
public static final int AT_LEAST = 0;
|
||||
|
||||
public static final int AT_MOST = 1;
|
||||
|
||||
public static final String HABIT_URI_FORMAT =
|
||||
"content://org.isoron.uhabits/habit/%d";
|
||||
|
||||
public static final int NUMBER_HABIT = 1;
|
||||
|
||||
public static final int YES_NO_HABIT = 0;
|
||||
|
||||
@Nullable
|
||||
public Long id;
|
||||
|
||||
@NonNull
|
||||
private HabitData data;
|
||||
|
||||
@NonNull
|
||||
private StreakList streaks;
|
||||
|
||||
@NonNull
|
||||
private ScoreList scores;
|
||||
|
||||
@NonNull
|
||||
private RepetitionList repetitions;
|
||||
|
||||
@NonNull
|
||||
private CheckmarkList checkmarks;
|
||||
|
||||
private ModelObservable observable = new ModelObservable();
|
||||
|
||||
/**
|
||||
* Constructs a habit with default data.
|
||||
* <p>
|
||||
* The habit is not archived, not highlighted, has no reminders and is
|
||||
* placed in the last position of the list of habits.
|
||||
*/
|
||||
@Inject
|
||||
Habit(@NonNull ModelFactory factory)
|
||||
{
|
||||
this.data = new HabitData();
|
||||
checkmarks = factory.buildCheckmarkList(this);
|
||||
streaks = factory.buildStreakList(this);
|
||||
scores = factory.buildScoreList(this);
|
||||
repetitions = factory.buildRepetitionList(this);
|
||||
}
|
||||
|
||||
Habit(@NonNull ModelFactory factory, @NonNull HabitData data)
|
||||
{
|
||||
this.data = new HabitData(data);
|
||||
checkmarks = factory.buildCheckmarkList(this);
|
||||
streaks = factory.buildStreakList(this);
|
||||
scores = factory.buildScoreList(this);
|
||||
repetitions = factory.buildRepetitionList(this);
|
||||
observable = new ModelObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the reminder for a habit.
|
||||
*/
|
||||
public synchronized void clearReminder()
|
||||
{
|
||||
data.reminder = null;
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all the attributes of the specified habit into this habit
|
||||
*
|
||||
* @param model the model whose attributes should be copied from
|
||||
*/
|
||||
public synchronized void copyFrom(@NonNull Habit model)
|
||||
{
|
||||
this.data = new HabitData(model.data);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of checkmarks belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized CheckmarkList getCheckmarks()
|
||||
{
|
||||
return checkmarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color of the habit.
|
||||
* <p>
|
||||
* This number is not an android.graphics.Color, but an index to the
|
||||
* activity color palette, which changes according to the theme. To convert
|
||||
* this color into an android.graphics.Color, use ColorHelper.getColor(context,
|
||||
* habit.color).
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Integer getColor()
|
||||
{
|
||||
return data.color;
|
||||
}
|
||||
|
||||
public synchronized void setColor(@NonNull Integer color)
|
||||
{
|
||||
data.color = color;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized String getDescription()
|
||||
{
|
||||
return data.description;
|
||||
}
|
||||
|
||||
public synchronized void setDescription(@NonNull String description)
|
||||
{
|
||||
data.description = description;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized Frequency getFrequency()
|
||||
{
|
||||
return data.frequency;
|
||||
}
|
||||
|
||||
public synchronized void setFrequency(@NonNull Frequency frequency)
|
||||
{
|
||||
data.frequency = frequency;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized Long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public synchronized void setId(@Nullable Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized String getName()
|
||||
{
|
||||
return data.name;
|
||||
}
|
||||
|
||||
public synchronized void setName(@NonNull String name)
|
||||
{
|
||||
data.name = name;
|
||||
}
|
||||
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reminder for this habit.
|
||||
* <p>
|
||||
* Before calling this method, you should call {@link #hasReminder()} to
|
||||
* verify that a reminder does exist, otherwise an exception will be
|
||||
* thrown.
|
||||
*
|
||||
* @return the reminder for this habit
|
||||
* @throws IllegalStateException if habit has no reminder
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Reminder getReminder()
|
||||
{
|
||||
if (data.reminder == null) throw new IllegalStateException();
|
||||
return data.reminder;
|
||||
}
|
||||
|
||||
public synchronized void setReminder(@Nullable Reminder reminder)
|
||||
{
|
||||
data.reminder = reminder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public RepetitionList getRepetitions()
|
||||
{
|
||||
return repetitions;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ScoreList getScores()
|
||||
{
|
||||
return scores;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public StreakList getStreaks()
|
||||
{
|
||||
return streaks;
|
||||
}
|
||||
|
||||
public synchronized int getTargetType()
|
||||
{
|
||||
return data.targetType;
|
||||
}
|
||||
|
||||
public synchronized void setTargetType(int targetType)
|
||||
{
|
||||
if (targetType != AT_LEAST && targetType != AT_MOST)
|
||||
throw new IllegalArgumentException();
|
||||
data.targetType = targetType;
|
||||
}
|
||||
|
||||
public synchronized double getTargetValue()
|
||||
{
|
||||
return data.targetValue;
|
||||
}
|
||||
|
||||
public synchronized void setTargetValue(double targetValue)
|
||||
{
|
||||
if (targetValue < 0) throw new IllegalArgumentException();
|
||||
data.targetValue = targetValue;
|
||||
}
|
||||
|
||||
public synchronized int getType()
|
||||
{
|
||||
return data.type;
|
||||
}
|
||||
|
||||
public synchronized void setType(int type)
|
||||
{
|
||||
if (type != YES_NO_HABIT && type != NUMBER_HABIT)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
data.type = type;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized String getUnit()
|
||||
{
|
||||
return data.unit;
|
||||
}
|
||||
|
||||
public synchronized void setUnit(@NonNull String unit)
|
||||
{
|
||||
data.unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public URI that identifies this habit
|
||||
*
|
||||
* @return the URI
|
||||
*/
|
||||
public String getUriString()
|
||||
{
|
||||
return String.format(Locale.US, HABIT_URI_FORMAT, getId());
|
||||
}
|
||||
|
||||
public synchronized boolean hasId()
|
||||
{
|
||||
return getId() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the habit has a reminder.
|
||||
*
|
||||
* @return true if habit has reminder, false otherwise
|
||||
*/
|
||||
public synchronized boolean hasReminder()
|
||||
{
|
||||
return data.reminder != null;
|
||||
}
|
||||
|
||||
public void invalidateNewerThan(long timestamp)
|
||||
{
|
||||
getScores().invalidateNewerThan(timestamp);
|
||||
getCheckmarks().invalidateNewerThan(timestamp);
|
||||
getStreaks().invalidateNewerThan(timestamp);
|
||||
}
|
||||
|
||||
public synchronized boolean isArchived()
|
||||
{
|
||||
return data.archived;
|
||||
}
|
||||
|
||||
public synchronized void setArchived(boolean archived)
|
||||
{
|
||||
data.archived = archived;
|
||||
}
|
||||
|
||||
public synchronized boolean isCompletedToday()
|
||||
{
|
||||
int todayCheckmark = getCheckmarks().getTodayValue();
|
||||
if (isNumerical()) return todayCheckmark >= data.targetValue;
|
||||
else return (todayCheckmark != UNCHECKED);
|
||||
}
|
||||
|
||||
public synchronized boolean isNumerical()
|
||||
{
|
||||
return data.type == NUMBER_HABIT;
|
||||
}
|
||||
|
||||
public HabitData getData()
|
||||
{
|
||||
return new HabitData(data);
|
||||
}
|
||||
|
||||
public static class HabitData
|
||||
{
|
||||
@NonNull
|
||||
public String name;
|
||||
|
||||
@NonNull
|
||||
public String description;
|
||||
|
||||
@NonNull
|
||||
public Frequency frequency;
|
||||
|
||||
public int color;
|
||||
|
||||
public boolean archived;
|
||||
|
||||
public int targetType;
|
||||
|
||||
public double targetValue;
|
||||
|
||||
public int type;
|
||||
|
||||
@NonNull
|
||||
public String unit;
|
||||
|
||||
@Nullable
|
||||
public Reminder reminder;
|
||||
|
||||
public HabitData()
|
||||
{
|
||||
this.color = 5;
|
||||
this.archived = false;
|
||||
this.frequency = new Frequency(3, 7);
|
||||
this.type = YES_NO_HABIT;
|
||||
this.name = "";
|
||||
this.description = "";
|
||||
this.targetType = AT_LEAST;
|
||||
this.targetValue = 100;
|
||||
this.unit = "";
|
||||
}
|
||||
|
||||
public HabitData(@NonNull HabitData model)
|
||||
{
|
||||
this.name = model.name;
|
||||
this.description = model.description;
|
||||
this.frequency = model.frequency;
|
||||
this.color = model.color;
|
||||
this.archived = model.archived;
|
||||
this.targetType = model.targetType;
|
||||
this.targetValue = model.targetValue;
|
||||
this.type = model.type;
|
||||
this.unit = model.unit;
|
||||
this.reminder = model.reminder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this)
|
||||
.append("name", name)
|
||||
.append("description", description)
|
||||
.append("frequency", frequency)
|
||||
.append("color", color)
|
||||
.append("archived", archived)
|
||||
.append("targetType", targetType)
|
||||
.append("targetValue", targetValue)
|
||||
.append("type", type)
|
||||
.append("unit", unit)
|
||||
.append("reminder", reminder)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
HabitData habitData = (HabitData) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.append(color, habitData.color)
|
||||
.append(archived, habitData.archived)
|
||||
.append(targetType, habitData.targetType)
|
||||
.append(targetValue, habitData.targetValue)
|
||||
.append(type, habitData.type)
|
||||
.append(name, habitData.name)
|
||||
.append(description, habitData.description)
|
||||
.append(frequency, habitData.frequency)
|
||||
.append(unit, habitData.unit)
|
||||
.append(reminder, habitData.reminder)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.append(name)
|
||||
.append(description)
|
||||
.append(frequency)
|
||||
.append(color)
|
||||
.append(archived)
|
||||
.append(targetType)
|
||||
.append(targetValue)
|
||||
.append(type)
|
||||
.append(unit)
|
||||
.append(reminder)
|
||||
.toHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* 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 com.opencsv.*;
|
||||
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.annotation.concurrent.*;
|
||||
|
||||
/**
|
||||
* An ordered collection of {@link Habit}s.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public abstract class HabitList implements Iterable<Habit>
|
||||
{
|
||||
private final ModelObservable observable;
|
||||
|
||||
@NonNull
|
||||
protected final HabitMatcher filter;
|
||||
|
||||
/**
|
||||
* Creates a new HabitList.
|
||||
* <p>
|
||||
* Depending on the implementation, this list can either be empty or be
|
||||
* populated by some pre-existing habits, for example, from a certain
|
||||
* database.
|
||||
*/
|
||||
public HabitList()
|
||||
{
|
||||
observable = new ModelObservable();
|
||||
filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
|
||||
}
|
||||
|
||||
protected HabitList(@NonNull HabitMatcher filter)
|
||||
{
|
||||
observable = new ModelObservable();
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new habit in the list.
|
||||
* <p>
|
||||
* If the id of the habit is null, the list will assign it a new id, which
|
||||
* is guaranteed to be unique in the scope of the list. If id is not null,
|
||||
* the caller should make sure that the list does not already contain
|
||||
* another habit with same id, otherwise a RuntimeException will be thrown.
|
||||
*
|
||||
* @param habit the habit to be inserted
|
||||
* @throws IllegalArgumentException if the habit is already on the list.
|
||||
*/
|
||||
public abstract void add(@NonNull Habit habit)
|
||||
throws IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Returns the habit with specified id.
|
||||
*
|
||||
* @param id the id of the habit
|
||||
* @return the habit, or null if none exist
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Habit getById(long id);
|
||||
|
||||
/**
|
||||
* Returns the habit that occupies a certain position.
|
||||
*
|
||||
* @param position the position of the desired habit
|
||||
* @return the habit at that position
|
||||
* @throws IndexOutOfBoundsException when the position is invalid
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Habit getByPosition(int position);
|
||||
|
||||
/**
|
||||
* Returns the list of habits that match a given condition.
|
||||
*
|
||||
* @param matcher the matcher that checks the condition
|
||||
* @return the list of matching habits
|
||||
*/
|
||||
@NonNull
|
||||
public abstract HabitList getFiltered(HabitMatcher matcher);
|
||||
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
public abstract Order getOrder();
|
||||
|
||||
/**
|
||||
* Changes the order of the elements on the list.
|
||||
*
|
||||
* @param order the new order criterea
|
||||
*/
|
||||
public abstract void setOrder(@NonNull Order order);
|
||||
|
||||
/**
|
||||
* Returns the index of the given habit in the list, or -1 if the list does
|
||||
* not contain the habit.
|
||||
*
|
||||
* @param h the habit
|
||||
* @return the index of the habit, or -1 if not in the list
|
||||
*/
|
||||
public abstract int indexOf(@NonNull Habit h);
|
||||
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return size() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given habit from the list.
|
||||
* <p>
|
||||
* If the given habit is not in the list, does nothing.
|
||||
*
|
||||
* @param h the habit to be removed.
|
||||
*/
|
||||
public abstract void remove(@NonNull Habit h);
|
||||
|
||||
/**
|
||||
* Removes all the habits from the list.
|
||||
*/
|
||||
public void removeAll()
|
||||
{
|
||||
List<Habit> copy = new LinkedList<>();
|
||||
for (Habit h : this) copy.add(h);
|
||||
for (Habit h : copy) remove(h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the position of a habit in the list.
|
||||
*
|
||||
* @param from the habit that should be moved
|
||||
* @param to the habit that currently occupies the desired position
|
||||
*/
|
||||
public abstract void reorder(Habit from, Habit to);
|
||||
|
||||
public void repair()
|
||||
{
|
||||
for (Habit h : this)
|
||||
{
|
||||
h.getCheckmarks().invalidateNewerThan(0);
|
||||
h.getStreaks().invalidateNewerThan(0);
|
||||
h.getScores().invalidateNewerThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of habits in this list.
|
||||
*
|
||||
* @return number of habits
|
||||
*/
|
||||
public abstract int size();
|
||||
|
||||
/**
|
||||
* Notifies the list that a certain list of habits has been modified.
|
||||
* <p>
|
||||
* Depending on the implementation, this operation might trigger a write to
|
||||
* disk, or do nothing at all. To make sure that the habits get persisted,
|
||||
* this operation must be called.
|
||||
*
|
||||
* @param habits the list of habits that have been modified.
|
||||
*/
|
||||
public abstract void update(List<Habit> habits);
|
||||
|
||||
/**
|
||||
* Notifies the list that a certain habit has been modified.
|
||||
* <p>
|
||||
* See {@link #update(List)} for more details.
|
||||
*
|
||||
* @param habit the habit that has been modified.
|
||||
*/
|
||||
public void update(@NonNull Habit habit)
|
||||
{
|
||||
update(Collections.singletonList(habit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the list of habits to the given writer, in CSV format. There is
|
||||
* one line for each habit, containing the fields name, description,
|
||||
* frequency numerator, frequency denominator and color. The color is
|
||||
* written in HTML format (#000000).
|
||||
*
|
||||
* @param out the writer that will receive the result
|
||||
* @throws IOException if write operations fail
|
||||
*/
|
||||
public void writeCSV(@NonNull Writer out) throws IOException
|
||||
{
|
||||
String header[] = {
|
||||
"Position",
|
||||
"Name",
|
||||
"Description",
|
||||
"NumRepetitions",
|
||||
"Interval",
|
||||
"Color"
|
||||
};
|
||||
|
||||
CSVWriter csv = new CSVWriter(out);
|
||||
csv.writeNext(header, false);
|
||||
|
||||
for (Habit habit : this)
|
||||
{
|
||||
Frequency freq = habit.getFrequency();
|
||||
|
||||
String[] cols = {
|
||||
String.format("%03d", indexOf(habit) + 1),
|
||||
habit.getName(),
|
||||
habit.getDescription(),
|
||||
Integer.toString(freq.getNumerator()),
|
||||
Integer.toString(freq.getDenominator()),
|
||||
ColorConstants.CSV_PALETTE[habit.getColor()]
|
||||
};
|
||||
|
||||
csv.writeNext(cols, false);
|
||||
}
|
||||
|
||||
csv.close();
|
||||
}
|
||||
|
||||
public enum Order
|
||||
{
|
||||
BY_NAME,
|
||||
BY_COLOR,
|
||||
BY_SCORE,
|
||||
BY_POSITION
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 java.util.*;
|
||||
|
||||
public class HabitMatcher
|
||||
{
|
||||
public static final HabitMatcher WITH_ALARM = new HabitMatcherBuilder()
|
||||
.setArchivedAllowed(true)
|
||||
.setReminderRequired(true)
|
||||
.build();
|
||||
|
||||
private final boolean archivedAllowed;
|
||||
|
||||
private final boolean reminderRequired;
|
||||
|
||||
private final boolean completedAllowed;
|
||||
|
||||
private final List<Integer> allowedColors;
|
||||
|
||||
public HabitMatcher(boolean allowArchived,
|
||||
boolean reminderRequired,
|
||||
boolean completedAllowed,
|
||||
@NonNull List<Integer> allowedColors)
|
||||
{
|
||||
this.archivedAllowed = allowArchived;
|
||||
this.reminderRequired = reminderRequired;
|
||||
this.completedAllowed = completedAllowed;
|
||||
this.allowedColors = allowedColors;
|
||||
}
|
||||
|
||||
public List<Integer> getAllowedColors()
|
||||
{
|
||||
return allowedColors;
|
||||
}
|
||||
|
||||
public boolean isArchivedAllowed()
|
||||
{
|
||||
return archivedAllowed;
|
||||
}
|
||||
|
||||
public boolean isCompletedAllowed()
|
||||
{
|
||||
return completedAllowed;
|
||||
}
|
||||
|
||||
public boolean isReminderRequired()
|
||||
{
|
||||
return reminderRequired;
|
||||
}
|
||||
|
||||
public boolean matches(Habit habit)
|
||||
{
|
||||
if (!isArchivedAllowed() && habit.isArchived()) return false;
|
||||
if (isReminderRequired() && !habit.hasReminder()) return false;
|
||||
if (!isCompletedAllowed() && habit.isCompletedToday()) return false;
|
||||
if (!allowedColors.contains(habit.getColor())) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class HabitMatcherBuilder
|
||||
{
|
||||
private boolean archivedAllowed = false;
|
||||
|
||||
private boolean reminderRequired = false;
|
||||
|
||||
private boolean completedAllowed = true;
|
||||
|
||||
private List<Integer> allowedColors = allColors();
|
||||
|
||||
private static List<Integer> allColors()
|
||||
{
|
||||
List<Integer> colors = new ArrayList<>();
|
||||
for(int i = 0; i < ColorConstants.CSV_PALETTE.length; i++)
|
||||
colors.add(i);
|
||||
return colors;
|
||||
}
|
||||
|
||||
public HabitMatcher build()
|
||||
{
|
||||
return new HabitMatcher(archivedAllowed, reminderRequired,
|
||||
completedAllowed, allowedColors);
|
||||
}
|
||||
|
||||
public HabitMatcherBuilder setArchivedAllowed(boolean archivedAllowed)
|
||||
{
|
||||
this.archivedAllowed = archivedAllowed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HabitMatcherBuilder setAllowedColors(List<Integer> allowedColors)
|
||||
{
|
||||
this.allowedColors = allowedColors;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HabitMatcherBuilder setCompletedAllowed(boolean completedAllowed)
|
||||
{
|
||||
this.completedAllowed = completedAllowed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HabitMatcherBuilder setReminderRequired(boolean reminderRequired)
|
||||
{
|
||||
this.reminderRequired = reminderRequired;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
public class HabitNotFoundException extends RuntimeException {
|
||||
public HabitNotFoundException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public HabitNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HabitNotFoundException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public HabitNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Interface implemented by factories that provide concrete implementations of
|
||||
* the core model classes.
|
||||
*/
|
||||
public interface ModelFactory
|
||||
{
|
||||
CheckmarkList buildCheckmarkList(Habit habit);
|
||||
|
||||
default Habit buildHabit()
|
||||
{
|
||||
return new Habit(this);
|
||||
}
|
||||
|
||||
default Habit buildHabit(Habit.HabitData data)
|
||||
{
|
||||
return new Habit(this, data);
|
||||
}
|
||||
|
||||
HabitList buildHabitList();
|
||||
|
||||
RepetitionList buildRepetitionList(Habit habit);
|
||||
|
||||
ScoreList buildScoreList(Habit habit);
|
||||
|
||||
StreakList buildStreakList(Habit habit);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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.util.*;
|
||||
|
||||
/**
|
||||
* The collection of {@link Repetition}s belonging to a habit.
|
||||
*/
|
||||
public abstract class RepetitionList
|
||||
{
|
||||
@NonNull
|
||||
protected final Habit habit;
|
||||
|
||||
@NonNull
|
||||
protected final ModelObservable observable;
|
||||
|
||||
public RepetitionList(@NonNull Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
this.observable = new ModelObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a repetition to the list.
|
||||
* <p>
|
||||
* Any implementation of this method must call observable.notifyListeners()
|
||||
* after the repetition has been added.
|
||||
*
|
||||
* @param repetition the repetition to be added.
|
||||
*/
|
||||
public abstract void add(Repetition repetition);
|
||||
|
||||
/**
|
||||
* Returns true if the list contains a repetition that has the given
|
||||
* timestamp.
|
||||
*
|
||||
* @param timestamp the timestamp to find.
|
||||
* @return true if list contains repetition with given timestamp, false
|
||||
* otherwise.
|
||||
*/
|
||||
public boolean containsTimestamp(long timestamp)
|
||||
{
|
||||
return (getByTimestamp(timestamp) != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of repetitions that happened within the given time
|
||||
* interval.
|
||||
* <p>
|
||||
* The list is sorted by timestamp in increasing order. That is, the first
|
||||
* element corresponds to oldest timestamp, while the last element
|
||||
* corresponds to the newest. The endpoints of the interval are included.
|
||||
*
|
||||
* @param fromTimestamp timestamp of the beginning of the interval
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Returns the repetition that has the given timestamp, or null if none
|
||||
* exists.
|
||||
*
|
||||
* @param timestamp the repetition timestamp.
|
||||
* @return the repetition that has the given timestamp.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Repetition getByTimestamp(long timestamp);
|
||||
|
||||
@NonNull
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the oldest repetition in the list.
|
||||
* <p>
|
||||
* If the list is empty, returns null. Repetitions in the future are
|
||||
* discarded.
|
||||
*
|
||||
* @return oldest repetition in the list, or null if list is empty.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Repetition getOldest();
|
||||
@Nullable
|
||||
/**
|
||||
* Returns the newest repetition in the list.
|
||||
* <p>
|
||||
* If the list is empty, returns null. Repetitions in the past are
|
||||
* discarded.
|
||||
*
|
||||
* @return newest repetition in the list, or null if list is empty.
|
||||
*/
|
||||
public abstract Repetition getNewest();
|
||||
|
||||
/**
|
||||
* Returns the total number of repetitions for each month, from the first
|
||||
* repetition until today, grouped by day of week.
|
||||
* <p>
|
||||
* The repetitions are returned in a HashMap. The key is the timestamp for
|
||||
* the first day of the month, at midnight (00:00). The value is an integer
|
||||
* array with 7 entries. The first entry contains the total number of
|
||||
* repetitions during the specified month that occurred on a Saturday. The
|
||||
* second entry corresponds to Sunday, and so on. If there are no
|
||||
* repetitions during a certain month, the value is null.
|
||||
*
|
||||
* @return total number of repetitions by month versus day of week
|
||||
*/
|
||||
@NonNull
|
||||
public HashMap<Long, Integer[]> getWeekdayFrequency()
|
||||
{
|
||||
List<Repetition> reps = getByInterval(0, DateUtils.getStartOfToday());
|
||||
HashMap<Long, Integer[]> map = new HashMap<>();
|
||||
|
||||
for (Repetition r : reps)
|
||||
{
|
||||
Calendar date = DateUtils.getCalendar(r.getTimestamp());
|
||||
int weekday = DateUtils.getWeekday(r.getTimestamp());
|
||||
date.set(Calendar.DAY_OF_MONTH, 1);
|
||||
|
||||
long timestamp = date.getTimeInMillis();
|
||||
Integer[] list = map.get(timestamp);
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
list = new Integer[7];
|
||||
Arrays.fill(list, 0);
|
||||
map.put(timestamp, list);
|
||||
}
|
||||
|
||||
list[weekday]++;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given repetition from the list.
|
||||
* <p>
|
||||
* If the list does not contain the repetition, it is unchanged.
|
||||
* <p>
|
||||
* Any implementation of this method must call observable.notifyListeners()
|
||||
* after the repetition has been added.
|
||||
*
|
||||
* @param repetition the repetition to be removed
|
||||
*/
|
||||
public abstract void remove(@NonNull Repetition repetition);
|
||||
|
||||
/**
|
||||
* Adds or remove a repetition at a certain timestamp.
|
||||
* <p>
|
||||
* If there exists a repetition on the list with the given timestamp, the
|
||||
* method removes this repetition from the list and returns it. If there are
|
||||
* no repetitions with the given timestamp, creates and adds one to the
|
||||
* list, then returns it.
|
||||
*
|
||||
* @param timestamp the timestamp for the timestamp that should be added or
|
||||
* removed.
|
||||
* @return the repetition that has been added or removed.
|
||||
*/
|
||||
@NonNull
|
||||
public Repetition toggleTimestamp(long timestamp)
|
||||
{
|
||||
timestamp = DateUtils.getStartOfDay(timestamp);
|
||||
Repetition rep = getByTimestamp(timestamp);
|
||||
|
||||
if (rep != null) remove(rep);
|
||||
else
|
||||
{
|
||||
rep = new Repetition(timestamp, Checkmark.CHECKED_EXPLICITLY);
|
||||
add(rep);
|
||||
}
|
||||
|
||||
habit.invalidateNewerThan(timestamp);
|
||||
return rep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of all repetitions
|
||||
*
|
||||
* @return number of all repetitions
|
||||
*/
|
||||
@NonNull
|
||||
public abstract long getTotalCount();
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* 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.*;
|
||||
|
||||
public abstract class ScoreList implements Iterable<Score>
|
||||
{
|
||||
protected final Habit habit;
|
||||
|
||||
protected ModelObservable observable;
|
||||
|
||||
protected Double todayValue = null;
|
||||
|
||||
/**
|
||||
* Creates a new ScoreList for the given habit.
|
||||
* <p>
|
||||
* The list is populated automatically according to the repetitions that the
|
||||
* habit has.
|
||||
*
|
||||
* @param habit the habit to which the scores belong.
|
||||
*/
|
||||
public ScoreList(Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
observable = new ModelObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given scores to the list.
|
||||
* <p>
|
||||
* This method should not be called by the application, since the scores are
|
||||
* computed automatically from the list of repetitions.
|
||||
*
|
||||
* @param scores the scores to add.
|
||||
*/
|
||||
public abstract void add(List<Score> scores);
|
||||
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the score for today.
|
||||
*
|
||||
* @return value of today's score
|
||||
*/
|
||||
public double getTodayValue()
|
||||
{
|
||||
if(todayValue == null) todayValue = getValue(DateUtils.getStartOfToday());
|
||||
return todayValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the score for a given day.
|
||||
* <p>
|
||||
* If the timestamp given happens before the first repetition of the habit
|
||||
* then returns zero.
|
||||
*
|
||||
* @param timestamp the timestamp of a day
|
||||
* @return score value for that day
|
||||
*/
|
||||
public final double getValue(long timestamp)
|
||||
{
|
||||
compute(timestamp, timestamp);
|
||||
Score s = getComputedByTimestamp(timestamp);
|
||||
if(s == null) throw new IllegalStateException();
|
||||
return s.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of scores that fall within the given interval.
|
||||
* <p>
|
||||
* There is exactly one score per day in the interval. The endpoints of
|
||||
* the interval are included. The list is ordered by timestamp (decreasing).
|
||||
* That is, the first score corresponds to the newest timestamp, and the
|
||||
* last score 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 scores within the interval.
|
||||
*/
|
||||
@NonNull
|
||||
public abstract List<Score> getByInterval(long fromTimestamp,
|
||||
long toTimestamp);
|
||||
|
||||
/**
|
||||
* Returns the values of the scores 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 score
|
||||
* @param to timestamp for the newest score
|
||||
* @return values for the scores inside the given interval
|
||||
*/
|
||||
public final double[] getValues(long from, long to)
|
||||
{
|
||||
List<Score> scores = getByInterval(from, to);
|
||||
double[] values = new double[scores.size()];
|
||||
|
||||
for(int i = 0; i < values.length; i++)
|
||||
values[i] = scores.get(i).getValue();
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public List<Score> groupBy(DateUtils.TruncateField field)
|
||||
{
|
||||
computeAll();
|
||||
HashMap<Long, ArrayList<Double>> 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
|
||||
* scores to be recomputed.
|
||||
*
|
||||
* @param timestamp the oldest timestamp that should be invalidated
|
||||
*/
|
||||
public abstract void invalidateNewerThan(long timestamp);
|
||||
|
||||
@Override
|
||||
public Iterator<Score> iterator()
|
||||
{
|
||||
return toList().iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Java list of scores, containing one score for each day, from
|
||||
* the first repetition of the habit until today.
|
||||
* <p>
|
||||
* The scores are sorted by decreasing timestamp. The first score
|
||||
* corresponds to today.
|
||||
*
|
||||
* @return list of scores
|
||||
*/
|
||||
public abstract List<Score> toList();
|
||||
|
||||
public void writeCSV(Writer out) throws IOException
|
||||
{
|
||||
computeAll();
|
||||
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
|
||||
|
||||
for (Score s : this)
|
||||
{
|
||||
String timestamp = dateFormat.format(s.getTimestamp());
|
||||
String score =
|
||||
String.format("%.4f", s.getValue());
|
||||
out.write(String.format("%s,%s\n", timestamp, score));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and stores one score for each day inside the given interval.
|
||||
* <p>
|
||||
* Scores that have already been computed are skipped, therefore there is no
|
||||
* harm in calling this function more times, or with larger intervals, than
|
||||
* strictly needed. The endpoints of the interval are included.
|
||||
* <p>
|
||||
* This method assumes the list of computed scores has no holes. That is, if
|
||||
* there is a score computed at time t1 and another at time t2, then every
|
||||
* score between t1 and t2 is also computed.
|
||||
*
|
||||
* @param from timestamp of the beginning of the interval
|
||||
* @param to timestamp of the end of the time interval
|
||||
*/
|
||||
protected synchronized void compute(long from, long to)
|
||||
{
|
||||
final long day = DateUtils.millisecondsInOneDay;
|
||||
|
||||
Score newest = getNewestComputed();
|
||||
Score oldest = getOldestComputed();
|
||||
|
||||
if (newest == null)
|
||||
{
|
||||
Repetition oldestRep = habit.getRepetitions().getOldest();
|
||||
if (oldestRep != null)
|
||||
from = Math.min(from, oldestRep.getTimestamp());
|
||||
forceRecompute(from, to, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (oldest == null) throw new IllegalStateException();
|
||||
forceRecompute(from, oldest.getTimestamp() - day, 0);
|
||||
forceRecompute(newest.getTimestamp() + day, to,
|
||||
newest.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and saves the scores that are missing since the first repetition
|
||||
* of the habit.
|
||||
*/
|
||||
protected void computeAll()
|
||||
{
|
||||
Repetition oldestRep = habit.getRepetitions().getOldest();
|
||||
if (oldestRep == null) return;
|
||||
|
||||
long today = DateUtils.getStartOfToday();
|
||||
compute(oldestRep.getTimestamp(), today);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the score that has the given timestamp, if it has already been
|
||||
* computed. If that score has not been computed yet, returns null.
|
||||
*
|
||||
* @param timestamp the timestamp of the score
|
||||
* @return the score with given timestamp, or null not yet computed.
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Score getComputedByTimestamp(long timestamp);
|
||||
|
||||
/**
|
||||
* Returns the most recent score that has already been computed. If no score
|
||||
* has been computed yet, returns null.
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Score getNewestComputed();
|
||||
|
||||
/**
|
||||
* Returns oldest score already computed. If no score has been computed yet,
|
||||
* returns null.
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Score getOldestComputed();
|
||||
|
||||
/**
|
||||
* Computes and stores one score for each day inside the given interval.
|
||||
* <p>
|
||||
* This function does not check if the scores have already been computed. If
|
||||
* they have, then it stores duplicate scores, which is a bad thing.
|
||||
*
|
||||
* @param from timestamp of the beginning of the interval
|
||||
* @param to timestamp of the end of the interval
|
||||
* @param previousValue value of the score on the day immediately before the
|
||||
* interval begins
|
||||
*/
|
||||
private void forceRecompute(long from, long to, double previousValue)
|
||||
{
|
||||
if(from > to) return;
|
||||
|
||||
final long day = DateUtils.millisecondsInOneDay;
|
||||
|
||||
final double freq = habit.getFrequency().toDouble();
|
||||
final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to);
|
||||
|
||||
List<Score> scores = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < checkmarkValues.length; i++)
|
||||
{
|
||||
double value = checkmarkValues[checkmarkValues.length - i - 1];
|
||||
|
||||
if(habit.isNumerical())
|
||||
{
|
||||
value /= 1000;
|
||||
value /= habit.getTargetValue();
|
||||
value = Math.min(1, value);
|
||||
}
|
||||
|
||||
if(!habit.isNumerical() && value > 0)
|
||||
value = 1;
|
||||
|
||||
previousValue = Score.compute(freq, previousValue, value);
|
||||
scores.add(new Score(from + day * i, previousValue));
|
||||
}
|
||||
|
||||
add(scores);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private HashMap<Long, ArrayList<Double>> getGroupedValues(DateUtils.TruncateField field)
|
||||
{
|
||||
HashMap<Long, ArrayList<Double>> groups = new HashMap<>();
|
||||
|
||||
for (Score s : this)
|
||||
{
|
||||
long groupTimestamp = DateUtils.truncate(field, s.getTimestamp());
|
||||
|
||||
if (!groups.containsKey(groupTimestamp))
|
||||
groups.put(groupTimestamp, new ArrayList<>());
|
||||
|
||||
groups.get(groupTimestamp).add(s.getValue());
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<Score> groupsToAvgScores(HashMap<Long, ArrayList<Double>> groups)
|
||||
{
|
||||
List<Score> scores = new LinkedList<>();
|
||||
|
||||
for (Long timestamp : groups.keySet())
|
||||
{
|
||||
double meanValue = 0.0;
|
||||
ArrayList<Double> groupValues = groups.get(timestamp);
|
||||
|
||||
for (Double v : groupValues) meanValue += v;
|
||||
meanValue /= groupValues.size();
|
||||
|
||||
scores.add(new Score(timestamp, meanValue));
|
||||
}
|
||||
|
||||
return scores;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.apache.commons.lang3.builder.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
public final class Streak
|
||||
{
|
||||
private final long start;
|
||||
|
||||
private final long end;
|
||||
|
||||
public Streak(long start, long end)
|
||||
{
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public int compareLonger(Streak other)
|
||||
{
|
||||
if (this.getLength() != other.getLength())
|
||||
return Long.signum(this.getLength() - other.getLength());
|
||||
|
||||
return Long.signum(this.getEnd() - other.getEnd());
|
||||
}
|
||||
|
||||
public int compareNewer(Streak other)
|
||||
{
|
||||
return Long.signum(this.getEnd() - other.getEnd());
|
||||
}
|
||||
|
||||
public long getEnd()
|
||||
{
|
||||
return end;
|
||||
}
|
||||
|
||||
public long getLength()
|
||||
{
|
||||
return (end - start) / DateUtils.millisecondsInOneDay + 1;
|
||||
}
|
||||
|
||||
public long getStart()
|
||||
{
|
||||
return start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this)
|
||||
.append("start", start)
|
||||
.append("end", end)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.util.*;
|
||||
|
||||
/**
|
||||
* The collection of {@link Streak}s that belong to a habit.
|
||||
* <p>
|
||||
* This list is populated automatically from the list of repetitions.
|
||||
*/
|
||||
public abstract class StreakList
|
||||
{
|
||||
protected final Habit habit;
|
||||
|
||||
protected ModelObservable observable;
|
||||
|
||||
protected StreakList(Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
observable = new ModelObservable();
|
||||
}
|
||||
|
||||
public abstract List<Streak> getAll();
|
||||
|
||||
@NonNull
|
||||
public List<Streak> getBest(int limit)
|
||||
{
|
||||
List<Streak> streaks = getAll();
|
||||
Collections.sort(streaks, (s1, s2) -> s2.compareLonger(s1));
|
||||
streaks = streaks.subList(0, Math.min(streaks.size(), limit));
|
||||
Collections.sort(streaks, (s1, s2) -> s2.compareNewer(s1));
|
||||
return streaks;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public abstract Streak getNewestComputed();
|
||||
|
||||
@NonNull
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
public abstract void invalidateNewerThan(long timestamp);
|
||||
|
||||
public synchronized void rebuild()
|
||||
{
|
||||
long today = DateUtils.getStartOfToday();
|
||||
|
||||
Long beginning = findBeginning();
|
||||
if (beginning == null || beginning > today) return;
|
||||
|
||||
int checks[] = habit.getCheckmarks().getValues(beginning, today);
|
||||
List<Streak> streaks = checkmarksToStreaks(beginning, checks);
|
||||
|
||||
removeNewestComputed();
|
||||
add(streaks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of checkmark values to a list of streaks.
|
||||
*
|
||||
* @param beginning the timestamp corresponding to the first checkmark
|
||||
* value.
|
||||
* @param checks the checkmarks values, ordered by decreasing timestamp.
|
||||
* @return the list of streaks.
|
||||
*/
|
||||
@NonNull
|
||||
protected List<Streak> checkmarksToStreaks(long beginning, int[] checks)
|
||||
{
|
||||
ArrayList<Long> transitions = getTransitions(beginning, checks);
|
||||
|
||||
List<Streak> streaks = new LinkedList<>();
|
||||
for (int i = 0; i < transitions.size(); i += 2)
|
||||
{
|
||||
long start = transitions.get(i);
|
||||
long end = transitions.get(i + 1);
|
||||
streaks.add(new Streak(start, end));
|
||||
}
|
||||
|
||||
return streaks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the place where we should start when recomputing the streaks.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
protected Long findBeginning()
|
||||
{
|
||||
Streak newestStreak = getNewestComputed();
|
||||
if (newestStreak != null) return newestStreak.getStart();
|
||||
|
||||
Repetition oldestRep = habit.getRepetitions().getOldest();
|
||||
if (oldestRep != null) return oldestRep.getTimestamp();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamps where there was a transition from performing a
|
||||
* habit to not performing a habit, and vice-versa.
|
||||
*
|
||||
* @param beginning the timestamp for the first checkmark
|
||||
* @param checks the checkmarks, ordered by decresing timestamp
|
||||
* @return the list of transitions
|
||||
*/
|
||||
@NonNull
|
||||
protected ArrayList<Long> getTransitions(long beginning, int[] checks)
|
||||
{
|
||||
long day = DateUtils.millisecondsInOneDay;
|
||||
long current = beginning;
|
||||
|
||||
ArrayList<Long> list = new ArrayList<>();
|
||||
list.add(current);
|
||||
|
||||
for (int i = 1; i < checks.length; i++)
|
||||
{
|
||||
current += day;
|
||||
int j = checks.length - i - 1;
|
||||
|
||||
if ((checks[j + 1] == 0 && checks[j] > 0)) list.add(current);
|
||||
if ((checks[j + 1] > 0 && checks[j] == 0)) list.add(current - day);
|
||||
}
|
||||
|
||||
if (list.size() % 2 == 1) list.add(current);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
protected abstract void add(@NonNull List<Streak> streaks);
|
||||
|
||||
protected abstract void removeNewestComputed();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* In-memory implementation of {@link CheckmarkList}.
|
||||
*/
|
||||
public class MemoryCheckmarkList extends CheckmarkList
|
||||
{
|
||||
LinkedList<Checkmark> list;
|
||||
|
||||
public MemoryCheckmarkList(Habit habit)
|
||||
{
|
||||
super(habit);
|
||||
list = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(List<Checkmark> checkmarks)
|
||||
{
|
||||
list.addAll(checkmarks);
|
||||
Collections.sort(list, (c1, c2) -> c2.compareNewer(c1));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Checkmark> getByInterval(long fromTimestamp, long toTimestamp)
|
||||
{
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
|
||||
List<Checkmark> filtered = new LinkedList<>();
|
||||
|
||||
for (Checkmark c : list)
|
||||
if (c.getTimestamp() >= fromTimestamp &&
|
||||
c.getTimestamp() <= toTimestamp) filtered.add(c);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Checkmark getOldestComputed()
|
||||
{
|
||||
if(list.isEmpty()) return null;
|
||||
return list.getLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Checkmark getNewestComputed()
|
||||
{
|
||||
if(list.isEmpty()) return null;
|
||||
return list.getFirst();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.uhabits.models.HabitList.Order.*;
|
||||
|
||||
/**
|
||||
* In-memory implementation of {@link HabitList}.
|
||||
*/
|
||||
public class MemoryHabitList extends HabitList
|
||||
{
|
||||
@NonNull
|
||||
private LinkedList<Habit> list;
|
||||
|
||||
private Comparator<Habit> comparator = null;
|
||||
|
||||
@NonNull
|
||||
private Order order;
|
||||
|
||||
public MemoryHabitList()
|
||||
{
|
||||
super();
|
||||
list = new LinkedList<>();
|
||||
order = Order.BY_POSITION;
|
||||
}
|
||||
|
||||
protected MemoryHabitList(@NonNull HabitMatcher matcher)
|
||||
{
|
||||
super(matcher);
|
||||
list = new LinkedList<>();
|
||||
order = Order.BY_POSITION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(@NonNull Habit habit) throws IllegalArgumentException
|
||||
{
|
||||
if (list.contains(habit))
|
||||
throw new IllegalArgumentException("habit already added");
|
||||
|
||||
Long id = habit.getId();
|
||||
if (id != null && getById(id) != null)
|
||||
throw new RuntimeException("duplicate id");
|
||||
|
||||
if (id == null) habit.setId((long) list.size());
|
||||
list.addLast(habit);
|
||||
resort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Habit getById(long id)
|
||||
{
|
||||
for (Habit h : list)
|
||||
{
|
||||
if (h.getId() == null) continue;
|
||||
if (h.getId() == id) return h;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Habit getByPosition(int position)
|
||||
{
|
||||
return list.get(position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public HabitList getFiltered(HabitMatcher matcher)
|
||||
{
|
||||
MemoryHabitList habits = new MemoryHabitList(matcher);
|
||||
habits.comparator = comparator;
|
||||
for (Habit h : this) if (matcher.matches(h)) habits.add(h);
|
||||
return habits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Order getOrder()
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(@NonNull Habit h)
|
||||
{
|
||||
return list.indexOf(h);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Habit> iterator()
|
||||
{
|
||||
return Collections.unmodifiableCollection(list).iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NonNull Habit habit)
|
||||
{
|
||||
list.remove(habit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reorder(Habit from, Habit to)
|
||||
{
|
||||
int toPos = indexOf(to);
|
||||
list.remove(from);
|
||||
list.add(toPos, from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOrder(@NonNull Order order)
|
||||
{
|
||||
this.order = order;
|
||||
this.comparator = getComparatorByOrder(order);
|
||||
resort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(List<Habit> habits)
|
||||
{
|
||||
// NOP
|
||||
}
|
||||
|
||||
private Comparator<Habit> getComparatorByOrder(Order order)
|
||||
{
|
||||
Comparator<Habit> nameComparator =
|
||||
(h1, h2) -> h1.getName().compareTo(h2.getName());
|
||||
|
||||
Comparator<Habit> colorComparator = (h1, h2) -> {
|
||||
Integer c1 = h1.getColor();
|
||||
Integer c2 = h2.getColor();
|
||||
if (c1.equals(c2)) return nameComparator.compare(h1, h2);
|
||||
else return c1.compareTo(c2);
|
||||
};
|
||||
|
||||
Comparator<Habit> scoreComparator = (h1, h2) -> {
|
||||
double s1 = h1.getScores().getTodayValue();
|
||||
double s2 = h2.getScores().getTodayValue();
|
||||
return Double.compare(s2, s1);
|
||||
};
|
||||
|
||||
if (order == BY_POSITION) return null;
|
||||
if (order == BY_NAME) return nameComparator;
|
||||
if (order == BY_COLOR) return colorComparator;
|
||||
if (order == BY_SCORE) return scoreComparator;
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
private void resort()
|
||||
{
|
||||
if (comparator != null) Collections.sort(list, comparator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.models.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
@Module
|
||||
public class MemoryModelFactory implements ModelFactory
|
||||
{
|
||||
@Provides
|
||||
@AppScope
|
||||
public static HabitList provideHabitList()
|
||||
{
|
||||
return new MemoryHabitList();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
public static ModelFactory provideModelFactory()
|
||||
{
|
||||
return new MemoryModelFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CheckmarkList buildCheckmarkList(Habit habit)
|
||||
{
|
||||
return new MemoryCheckmarkList(habit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HabitList buildHabitList()
|
||||
{
|
||||
return new MemoryHabitList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepetitionList buildRepetitionList(Habit habit)
|
||||
{
|
||||
return new MemoryRepetitionList(habit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScoreList buildScoreList(Habit habit)
|
||||
{
|
||||
return new MemoryScoreList(habit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreakList buildStreakList(Habit habit)
|
||||
{
|
||||
return new MemoryStreakList(habit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* In-memory implementation of {@link RepetitionList}.
|
||||
*/
|
||||
public class MemoryRepetitionList extends RepetitionList
|
||||
{
|
||||
LinkedList<Repetition> list;
|
||||
|
||||
public MemoryRepetitionList(Habit habit)
|
||||
{
|
||||
super(habit);
|
||||
list = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(Repetition repetition)
|
||||
{
|
||||
list.add(repetition);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp)
|
||||
{
|
||||
LinkedList<Repetition> filtered = new LinkedList<>();
|
||||
|
||||
for (Repetition r : list)
|
||||
{
|
||||
long t = r.getTimestamp();
|
||||
if (t >= fromTimestamp && t <= toTimestamp) filtered.add(r);
|
||||
}
|
||||
|
||||
Collections.sort(filtered,
|
||||
(r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp()));
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Repetition getByTimestamp(long timestamp)
|
||||
{
|
||||
for (Repetition r : list)
|
||||
if (r.getTimestamp() == timestamp) return r;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Repetition getOldest()
|
||||
{
|
||||
long oldestTime = Long.MAX_VALUE;
|
||||
Repetition oldestRep = null;
|
||||
|
||||
for (Repetition rep : list)
|
||||
{
|
||||
if (rep.getTimestamp() < oldestTime)
|
||||
{
|
||||
oldestRep = rep;
|
||||
oldestTime = rep.getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
return oldestRep;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Repetition getNewest()
|
||||
{
|
||||
long newestTime = -1;
|
||||
Repetition newestRep = null;
|
||||
|
||||
for (Repetition rep : list)
|
||||
{
|
||||
if (rep.getTimestamp() > newestTime)
|
||||
{
|
||||
newestRep = rep;
|
||||
newestTime = rep.getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
return newestRep;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NonNull Repetition repetition)
|
||||
{
|
||||
list.remove(repetition);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalCount()
|
||||
{
|
||||
return list.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.*;
|
||||
|
||||
import org.isoron.uhabits.models.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MemoryScoreList extends ScoreList
|
||||
{
|
||||
LinkedList<Score> list;
|
||||
|
||||
public MemoryScoreList(Habit habit)
|
||||
{
|
||||
super(habit);
|
||||
list = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(List<Score> scores)
|
||||
{
|
||||
list.addAll(scores);
|
||||
Collections.sort(list,
|
||||
(s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp()));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Score> getByInterval(long fromTimestamp, long toTimestamp)
|
||||
{
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
|
||||
List<Score> filtered = new LinkedList<>();
|
||||
|
||||
for (Score s : list)
|
||||
if (s.getTimestamp() >= fromTimestamp &&
|
||||
s.getTimestamp() <= toTimestamp) filtered.add(s);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Score getComputedByTimestamp(long timestamp)
|
||||
{
|
||||
for (Score s : list)
|
||||
if (s.getTimestamp() == timestamp) return s;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
todayValue = null;
|
||||
getObservable().notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Score> toList()
|
||||
{
|
||||
computeAll();
|
||||
return new LinkedList<>(list);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Score getNewestComputed()
|
||||
{
|
||||
if (list.isEmpty()) return null;
|
||||
return list.getFirst();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Score getOldestComputed()
|
||||
{
|
||||
if (list.isEmpty()) return null;
|
||||
return list.getLast();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 org.isoron.uhabits.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MemoryStreakList extends StreakList
|
||||
{
|
||||
LinkedList<Streak> list;
|
||||
|
||||
public MemoryStreakList(Habit habit)
|
||||
{
|
||||
super(habit);
|
||||
list = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Streak getNewestComputed()
|
||||
{
|
||||
Streak newest = null;
|
||||
|
||||
for (Streak s : list)
|
||||
if (newest == null || s.getEnd() > newest.getEnd()) newest = s;
|
||||
|
||||
return newest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateNewerThan(long timestamp)
|
||||
{
|
||||
LinkedList<Streak> discard = new LinkedList<>();
|
||||
|
||||
for (Streak s : list)
|
||||
if (s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay)
|
||||
discard.add(s);
|
||||
|
||||
list.removeAll(discard);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void add(List<Streak> streaks)
|
||||
{
|
||||
list.addAll(streaks);
|
||||
Collections.sort(list, (s1, s2) -> s2.compareNewer(s1));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeNewestComputed()
|
||||
{
|
||||
Streak newest = getNewestComputed();
|
||||
if (newest != null) list.remove(newest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Streak> getAll()
|
||||
{
|
||||
rebuild();
|
||||
return new LinkedList<>(list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides in-memory implementation of core models.
|
||||
*/
|
||||
package org.isoron.uhabits.models.memory;
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 Licenses along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides core models classes, such as {@link org.isoron.uhabits.models.Habit}
|
||||
* and {@link org.isoron.uhabits.models.Repetition}.
|
||||
*/
|
||||
package org.isoron.uhabits.models;
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.isoron.uhabits.utils;
|
||||
|
||||
public class ColorConstants
|
||||
{
|
||||
public static String[] CSV_PALETTE = {
|
||||
"#D32F2F", // 0 red
|
||||
"#E64A19", // 1 orange
|
||||
"#F9A825", // 2 yellow
|
||||
"#AFB42B", // 3 light green
|
||||
"#388E3C", // 4 dark green
|
||||
"#00897B", // 5 teal
|
||||
"#00ACC1", // 6 cyan
|
||||
"#039BE5", // 7 blue
|
||||
"#5E35B1", // 8 deep purple
|
||||
"#8E24AA", // 9 purple
|
||||
"#D81B60", // 10 pink
|
||||
"#303030", // 11 dark grey
|
||||
"#aaaaaa" // 12 light grey
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 android.support.annotation.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
public class DateFormats
|
||||
{
|
||||
@NonNull
|
||||
public static SimpleDateFormat fromSkeleton(@NonNull String skeleton,
|
||||
@NonNull Locale locale)
|
||||
{
|
||||
SimpleDateFormat df = new SimpleDateFormat(skeleton, locale);
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return df;
|
||||
}
|
||||
|
||||
public static SimpleDateFormat getBackupDateFormat()
|
||||
{
|
||||
return fromSkeleton("yyyy-MM-dd HHmmss", Locale.US);
|
||||
}
|
||||
|
||||
public static SimpleDateFormat getCSVDateFormat()
|
||||
{
|
||||
return fromSkeleton("yyyy-MM-dd", Locale.US);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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 java.util.*;
|
||||
|
||||
import static java.util.Calendar.*;
|
||||
|
||||
public abstract class DateUtils
|
||||
{
|
||||
private static Long fixedLocalTime = null;
|
||||
|
||||
private static TimeZone fixedTimeZone = null;
|
||||
|
||||
/**
|
||||
* Number of milliseconds in one day.
|
||||
*/
|
||||
public static long millisecondsInOneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
public static long applyTimezone(long localTimestamp)
|
||||
{
|
||||
TimeZone tz = getTimezone();
|
||||
long now = new Date(localTimestamp).getTime();
|
||||
return now - tz.getOffset(now);
|
||||
}
|
||||
|
||||
public static String formatHeaderDate(GregorianCalendar day)
|
||||
{
|
||||
Locale locale = Locale.getDefault();
|
||||
String dayOfMonth = Integer.toString(day.get(DAY_OF_MONTH));
|
||||
String dayOfWeek = day.getDisplayName(DAY_OF_WEEK, SHORT, locale);
|
||||
return dayOfWeek + "\n" + dayOfMonth;
|
||||
}
|
||||
|
||||
public static GregorianCalendar getCalendar(long timestamp)
|
||||
{
|
||||
GregorianCalendar day =
|
||||
new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||
day.setTimeInMillis(timestamp);
|
||||
return day;
|
||||
}
|
||||
|
||||
public static String[] getDayNames(int format)
|
||||
{
|
||||
String[] wdays = new String[7];
|
||||
|
||||
Calendar day = new GregorianCalendar();
|
||||
day.set(DAY_OF_WEEK, Calendar.SATURDAY);
|
||||
|
||||
for (int i = 0; i < wdays.length; i++)
|
||||
{
|
||||
wdays[i] =
|
||||
day.getDisplayName(DAY_OF_WEEK, format, Locale.getDefault());
|
||||
day.add(DAY_OF_MONTH, 1);
|
||||
}
|
||||
|
||||
return wdays;
|
||||
}
|
||||
|
||||
public static long getLocalTime()
|
||||
{
|
||||
if (fixedLocalTime != null) return fixedLocalTime;
|
||||
|
||||
TimeZone tz = getTimezone();
|
||||
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
|
||||
*/
|
||||
public static String[] getLocaleDayNames(int format)
|
||||
{
|
||||
String[] days = new String[7];
|
||||
|
||||
Calendar calendar = new GregorianCalendar();
|
||||
calendar.set(DAY_OF_WEEK, calendar.getFirstDayOfWeek());
|
||||
for (int i = 0; i < days.length; i++)
|
||||
{
|
||||
days[i] = calendar.getDisplayName(DAY_OF_WEEK, format,
|
||||
Locale.getDefault());
|
||||
calendar.add(DAY_OF_MONTH, 1);
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(DAY_OF_WEEK, calendar.getFirstDayOfWeek());
|
||||
for (int i = 0; i < dayNumbers.length; i++)
|
||||
{
|
||||
dayNumbers[i] = calendar.get(DAY_OF_WEEK);
|
||||
calendar.add(DAY_OF_MONTH, 1);
|
||||
}
|
||||
return dayNumbers;
|
||||
}
|
||||
|
||||
public static String[] getLongDayNames()
|
||||
{
|
||||
return getDayNames(GregorianCalendar.LONG);
|
||||
}
|
||||
|
||||
public static String[] getShortDayNames()
|
||||
{
|
||||
return getDayNames(SHORT);
|
||||
}
|
||||
|
||||
public static long getStartOfDay(long timestamp)
|
||||
{
|
||||
return (timestamp / millisecondsInOneDay) * millisecondsInOneDay;
|
||||
}
|
||||
|
||||
public static long getStartOfToday()
|
||||
{
|
||||
return getStartOfDay(DateUtils.getLocalTime());
|
||||
}
|
||||
|
||||
public static long millisecondsUntilTomorrow()
|
||||
{
|
||||
return getStartOfToday() + millisecondsInOneDay - getLocalTime();
|
||||
}
|
||||
|
||||
public static GregorianCalendar getStartOfTodayCalendar()
|
||||
{
|
||||
return getCalendar(getStartOfToday());
|
||||
}
|
||||
|
||||
public static TimeZone getTimezone()
|
||||
{
|
||||
if(fixedTimeZone != null) return fixedTimeZone;
|
||||
return TimeZone.getDefault();
|
||||
}
|
||||
|
||||
public static void setFixedTimeZone(TimeZone tz)
|
||||
{
|
||||
fixedTimeZone = tz;
|
||||
}
|
||||
|
||||
public static int getWeekday(long timestamp)
|
||||
{
|
||||
GregorianCalendar day = getCalendar(timestamp);
|
||||
return javaWeekdayToLoopWeekday(day.get(DAY_OF_WEEK));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 long removeTimezone(long timestamp)
|
||||
{
|
||||
TimeZone tz = getTimezone();
|
||||
long now = new Date(timestamp).getTime();
|
||||
return now + tz.getOffset(now);
|
||||
}
|
||||
|
||||
public static void setFixedLocalTime(Long timestamp)
|
||||
{
|
||||
fixedLocalTime = timestamp;
|
||||
}
|
||||
|
||||
public static Long truncate(TruncateField field, long timestamp)
|
||||
{
|
||||
GregorianCalendar cal = DateUtils.getCalendar(timestamp);
|
||||
|
||||
switch (field)
|
||||
{
|
||||
case MONTH:
|
||||
cal.set(DAY_OF_MONTH, 1);
|
||||
return cal.getTimeInMillis();
|
||||
|
||||
case WEEK_NUMBER:
|
||||
int firstWeekday = cal.getFirstDayOfWeek();
|
||||
int weekday = cal.get(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(DAY_OF_MONTH, 1);
|
||||
cal.set(Calendar.MONTH, quarter * 3);
|
||||
return cal.getTimeInMillis();
|
||||
|
||||
case YEAR:
|
||||
cal.set(Calendar.MONTH, Calendar.JANUARY);
|
||||
cal.set(DAY_OF_MONTH, 1);
|
||||
return cal.getTimeInMillis();
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
public enum TruncateField
|
||||
{
|
||||
MONTH, WEEK_NUMBER, YEAR, QUARTER
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of days between two timestamps (exclusively).
|
||||
*
|
||||
* @param t1 the first timestamp to use in milliseconds
|
||||
* @param t2 the second timestamp to use in milliseconds
|
||||
* @return the number of days between the two timestamps
|
||||
*/
|
||||
public static int getDaysBetween(long t1, long t2)
|
||||
{
|
||||
Date d1 = new Date(t1);
|
||||
Date d2 = new Date(t2);
|
||||
return (int) (Math.abs((d2.getTime() - d1.getTime()) / millisecondsInOneDay));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user