/*
* 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();
}