mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 01:08:50 -06:00
Refactor and write docs for Score and ScoreList
This commit is contained in:
@@ -48,6 +48,8 @@ public class HabitFixtures
|
||||
static Habit createEmptyHabit()
|
||||
{
|
||||
Habit habit = new Habit();
|
||||
habit.freqNum = 1;
|
||||
habit.freqDen = 1;
|
||||
habit.save();
|
||||
return habit;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.unit.models;
|
||||
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
|
||||
import org.isoron.helpers.ActiveAndroidHelper;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
import org.isoron.uhabits.models.Score;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class ScoreListTest
|
||||
{
|
||||
private Habit habit;
|
||||
|
||||
@Before
|
||||
public void prepare()
|
||||
{
|
||||
HabitFixtures.purgeHabits();
|
||||
DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME);
|
||||
habit = HabitFixtures.createEmptyHabit();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown()
|
||||
{
|
||||
DateHelper.setFixedLocalTime(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidateNewerThan()
|
||||
{
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(0));
|
||||
|
||||
toggleRepetitions(0, 2);
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(1948077));
|
||||
|
||||
habit.freqNum = 1;
|
||||
habit.freqDen = 2;
|
||||
habit.scores.invalidateNewerThan(0);
|
||||
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(1974654));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTodayStarValue()
|
||||
{
|
||||
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.EMPTY_STAR));
|
||||
|
||||
int k = 0;
|
||||
while(habit.scores.getTodayValue() < Score.HALF_STAR_CUTOFF) toggleRepetitions(k, ++k);
|
||||
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.HALF_STAR));
|
||||
|
||||
while(habit.scores.getTodayValue() < Score.FULL_STAR_CUTOFF) toggleRepetitions(k, ++k);
|
||||
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.FULL_STAR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTodayValue()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(12629351));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getValue()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
|
||||
int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
|
||||
10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
|
||||
4507040, 3699107, 2846927, 1948077, 1000000 };
|
||||
|
||||
long current = DateHelper.getStartOfToday();
|
||||
for(int expectedValue : expectedValues)
|
||||
{
|
||||
assertThat(habit.scores.getValue(current), equalTo(expectedValue));
|
||||
current -= DateHelper.millisecondsInOneDay;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllValues_withoutGroups()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
|
||||
int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
|
||||
10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
|
||||
4507040, 3699107, 2846927, 1948077, 1000000 };
|
||||
|
||||
int actualValues[] = habit.scores.getAllValues(1);
|
||||
assertThat(actualValues, equalTo(expectedValues));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllValues_withGroups()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
|
||||
int expectedValues[] = { 12629351, 11006461, 7272612, 2800230 };
|
||||
|
||||
int actualValues[] = habit.scores.getAllValues(7);
|
||||
assertThat(actualValues, equalTo(expectedValues));
|
||||
}
|
||||
|
||||
private void toggleRepetitions(final int from, final int to)
|
||||
{
|
||||
ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command()
|
||||
{
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
long today = DateHelper.getStartOfToday();
|
||||
for (int i = from; i < to; i++)
|
||||
habit.repetitions.toggle(today - i * DateHelper.millisecondsInOneDay);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.unit.models;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.hamcrest.core.IsNot.not;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.isoron.uhabits.models.Score;
|
||||
import org.isoron.uhabits.models.Repetition;
|
||||
import org.isoron.uhabits.models.Checkmark;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class ScoreTest
|
||||
{
|
||||
@Test
|
||||
public void compute_withDailyHabit()
|
||||
{
|
||||
int checkmark = Checkmark.UNCHECKED;
|
||||
assertThat(Score.compute(1, 0, checkmark), equalTo(0));
|
||||
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
|
||||
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
|
||||
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
|
||||
|
||||
checkmark = Checkmark.CHECKED_IMPLICITLY;
|
||||
assertThat(Score.compute(1, 0, checkmark), equalTo(0));
|
||||
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
|
||||
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
|
||||
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
|
||||
|
||||
checkmark = Checkmark.CHECKED_EXPLICITLY;
|
||||
assertThat(Score.compute(1, 0, checkmark), equalTo(1000000));
|
||||
assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387));
|
||||
assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775));
|
||||
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_withNonDailyHabit()
|
||||
{
|
||||
int checkmark = Checkmark.CHECKED_EXPLICITLY;
|
||||
assertThat(Score.compute(1/3.0, 0, checkmark), equalTo(1000000));
|
||||
assertThat(Score.compute(1/3.0, 5000000, checkmark), equalTo(5916180));
|
||||
assertThat(Score.compute(1/3.0, 10000000, checkmark), equalTo(10832360));
|
||||
assertThat(Score.compute(1/3.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
|
||||
|
||||
assertThat(Score.compute(1/7.0, 0, checkmark), equalTo(1000000));
|
||||
assertThat(Score.compute(1/7.0, 5000000, checkmark), equalTo(5964398));
|
||||
assertThat(Score.compute(1/7.0, 10000000, checkmark), equalTo(10928796));
|
||||
assertThat(Score.compute(1/7.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getStarStatus()
|
||||
{
|
||||
Score s = new Score();
|
||||
|
||||
s.score = Score.FULL_STAR_CUTOFF + 1;
|
||||
assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
|
||||
|
||||
s.score = Score.FULL_STAR_CUTOFF;
|
||||
assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
|
||||
|
||||
s.score = Score.FULL_STAR_CUTOFF - 1;
|
||||
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
|
||||
|
||||
s.score = Score.HALF_STAR_CUTOFF + 1;
|
||||
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
|
||||
|
||||
s.score = Score.HALF_STAR_CUTOFF;
|
||||
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
|
||||
|
||||
s.score = Score.HALF_STAR_CUTOFF - 1;
|
||||
assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
|
||||
|
||||
s.score = 0;
|
||||
assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.isoron.helpers;
|
||||
|
||||
import com.activeandroid.ActiveAndroid;
|
||||
|
||||
public class ActiveAndroidHelper
|
||||
{
|
||||
public interface Command
|
||||
{
|
||||
void execute();
|
||||
}
|
||||
|
||||
public static void executeAsTransaction(Command command)
|
||||
{
|
||||
ActiveAndroid.beginTransaction();
|
||||
try
|
||||
{
|
||||
command.execute();
|
||||
ActiveAndroid.setTransactionSuccessful();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class EditHabitCommand extends Command
|
||||
{
|
||||
habit.checkmarks.deleteNewerThan(0);
|
||||
habit.streaks.deleteNewerThan(0);
|
||||
habit.scores.deleteNewerThan(0);
|
||||
habit.scores.invalidateNewerThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class EditHabitCommand extends Command
|
||||
{
|
||||
habit.checkmarks.deleteNewerThan(0);
|
||||
habit.streaks.deleteNewerThan(0);
|
||||
habit.scores.deleteNewerThan(0);
|
||||
habit.scores.invalidateNewerThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ public class ShowHabitFragment extends Fragment
|
||||
{
|
||||
RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing);
|
||||
scoreRing.setColor(habit.color);
|
||||
scoreRing.setPercentage((float) habit.scores.getNewestValue() / Score.MAX_SCORE);
|
||||
scoreRing.setPercentage((float) habit.scores.getTodayValue() / Score.MAX_VALUE);
|
||||
}
|
||||
|
||||
private void updateHeaders(View view)
|
||||
|
||||
@@ -75,7 +75,7 @@ public class CSVExporter
|
||||
|
||||
public String formatScore(int score)
|
||||
{
|
||||
return String.format("%.2f", ((float) score) / Score.MAX_SCORE);
|
||||
return String.format("%.2f", ((float) score) / Score.MAX_VALUE);
|
||||
}
|
||||
|
||||
private void writeScores(String dirPath, Habit habit) throws IOException
|
||||
|
||||
@@ -144,7 +144,7 @@ public class HabitListLoader
|
||||
if (isCancelled()) return null;
|
||||
|
||||
Long id = h.getId();
|
||||
newScores.put(id, h.scores.getNewestValue());
|
||||
newScores.put(id, h.scores.getTodayValue());
|
||||
newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
||||
|
||||
publishProgress(current++, newHabits.size());
|
||||
@@ -213,7 +213,7 @@ public class HabitListLoader
|
||||
|
||||
Habit h = Habit.get(id);
|
||||
habits.put(id, h);
|
||||
scores.put(id, h.scores.getNewestValue());
|
||||
scores.put(id, h.scores.getTodayValue());
|
||||
checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
||||
|
||||
return null;
|
||||
|
||||
@@ -117,21 +117,25 @@ public class Habit extends Model
|
||||
/**
|
||||
* List of streaks belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public StreakList streaks;
|
||||
|
||||
/**
|
||||
* List of scores belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public ScoreList scores;
|
||||
|
||||
/**
|
||||
* List of repetitions belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public RepetitionList repetitions;
|
||||
|
||||
/**
|
||||
* List of checkmarks belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public CheckmarkList checkmarks;
|
||||
|
||||
/**
|
||||
@@ -142,7 +146,11 @@ public class Habit extends Model
|
||||
public Habit(Habit model)
|
||||
{
|
||||
copyAttributes(model);
|
||||
initializeLists();
|
||||
|
||||
checkmarks = new CheckmarkList(this);
|
||||
streaks = new StreakList(this);
|
||||
scores = new ScoreList(this);
|
||||
repetitions = new RepetitionList(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,15 +165,11 @@ public class Habit extends Model
|
||||
this.archived = 0;
|
||||
this.freqDen = 7;
|
||||
this.freqNum = 3;
|
||||
initializeLists();
|
||||
}
|
||||
|
||||
private void initializeLists()
|
||||
{
|
||||
checkmarks = new CheckmarkList(this);
|
||||
streaks = new StreakList(this);
|
||||
scores = new ScoreList(this);
|
||||
repetitions = new RepetitionList(this);
|
||||
checkmarks = new CheckmarkList(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,7 +107,7 @@ public class RepetitionList
|
||||
rep.save();
|
||||
}
|
||||
|
||||
habit.scores.deleteNewerThan(timestamp);
|
||||
habit.scores.invalidateNewerThan(timestamp);
|
||||
habit.checkmarks.deleteNewerThan(timestamp);
|
||||
habit.streaks.deleteNewerThan(timestamp);
|
||||
}
|
||||
|
||||
@@ -26,16 +26,94 @@ import com.activeandroid.annotation.Table;
|
||||
@Table(name = "Score")
|
||||
public class Score extends Model
|
||||
{
|
||||
/**
|
||||
* Minimum score value required to earn half a star.
|
||||
*/
|
||||
public static final int HALF_STAR_CUTOFF = 9629750;
|
||||
public static final int FULL_STAR_CUTOFF = 15407600;
|
||||
public static final int MAX_SCORE = 19259500;
|
||||
|
||||
/**
|
||||
* Minimum score value required to earn a full star.
|
||||
*/
|
||||
public static final int FULL_STAR_CUTOFF = 15407600;
|
||||
|
||||
/**
|
||||
* Maximum score value attainable by any habit.
|
||||
*/
|
||||
public static final int MAX_VALUE = 19259478;
|
||||
|
||||
/**
|
||||
* Status indicating that the habit has not earned any star.
|
||||
*/
|
||||
public static final int EMPTY_STAR = 0;
|
||||
|
||||
/**
|
||||
* Status indicating that the habit has earned half a star.
|
||||
*/
|
||||
public static final int HALF_STAR = 1;
|
||||
|
||||
/**
|
||||
* Status indicating that the habit has earned a full star.
|
||||
*/
|
||||
public static final int FULL_STAR = 2;
|
||||
|
||||
/**
|
||||
* Habit to which this score belongs to.
|
||||
*/
|
||||
@Column(name = "habit")
|
||||
public Habit habit;
|
||||
|
||||
/**
|
||||
* Timestamp of the day to which this score applies. Time of day should be midnight (UTC).
|
||||
*/
|
||||
@Column(name = "timestamp")
|
||||
public Long timestamp;
|
||||
|
||||
/**
|
||||
* Value of the score.
|
||||
*/
|
||||
@Column(name = "score")
|
||||
public Integer score;
|
||||
|
||||
/**
|
||||
* Given the frequency of the habit, the previous score, and the value of the current checkmark,
|
||||
* computes the current score for the habit.
|
||||
*
|
||||
* The frequency of the habit is the number of repetitions divided by the length of the
|
||||
* interval. For example, a habit that should be repeated 3 times in 8 days has frequency 3.0 /
|
||||
* 8.0 = 0.375.
|
||||
*
|
||||
* The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or CHECK_EXPLICITLY.
|
||||
*
|
||||
* @param frequency the frequency of the habit
|
||||
* @param previousScore the previous score of the habit
|
||||
* @param checkmarkValue the value of the current checkmark
|
||||
*
|
||||
* @return the current score
|
||||
*/
|
||||
public static int compute(double frequency, int previousScore, int checkmarkValue)
|
||||
{
|
||||
double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1));
|
||||
int score = (int) (previousScore * multiplier);
|
||||
|
||||
if (checkmarkValue == Checkmark.CHECKED_EXPLICITLY)
|
||||
{
|
||||
score += 1000000;
|
||||
score = Math.min(score, Score.MAX_VALUE);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current star status for the habit, which can one of EMPTY_STAR, HALF_STAR or
|
||||
* FULL_STAR.
|
||||
*
|
||||
* @return current star status
|
||||
*/
|
||||
public int getStarStatus()
|
||||
{
|
||||
if(score >= Score.FULL_STAR_CUTOFF) return Score.FULL_STAR;
|
||||
if(score >= Score.HALF_STAR_CUTOFF) return Score.HALF_STAR;
|
||||
return Score.EMPTY_STAR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,42 +21,73 @@ package org.isoron.uhabits.models;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.activeandroid.ActiveAndroid;
|
||||
import com.activeandroid.Cache;
|
||||
import com.activeandroid.query.Delete;
|
||||
import com.activeandroid.query.From;
|
||||
import com.activeandroid.query.Select;
|
||||
|
||||
import org.isoron.helpers.ActiveAndroidHelper;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
|
||||
public class ScoreList
|
||||
{
|
||||
@NonNull
|
||||
private Habit habit;
|
||||
|
||||
public ScoreList(Habit habit)
|
||||
/**
|
||||
* Constructs a new ScoreList associated with the given habit.
|
||||
*
|
||||
* @param habit the habit this list should be associated with
|
||||
*/
|
||||
public ScoreList(@NonNull Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
}
|
||||
|
||||
public int getCurrentStarStatus()
|
||||
protected From select()
|
||||
{
|
||||
int score = getNewestValue();
|
||||
|
||||
if(score >= Score.FULL_STAR_CUTOFF) return 2;
|
||||
else if(score >= Score.HALF_STAR_CUTOFF) return 1;
|
||||
else return 0;
|
||||
}
|
||||
|
||||
public Score getNewest()
|
||||
{
|
||||
return new Select().from(Score.class)
|
||||
return new Select()
|
||||
.from(Score.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.orderBy("timestamp desc")
|
||||
.limit(1)
|
||||
.executeSingle();
|
||||
.orderBy("timestamp desc");
|
||||
}
|
||||
|
||||
public void deleteNewerThan(long timestamp)
|
||||
/**
|
||||
* Returns the most recent score already computed. If no score has been computed yet, returns
|
||||
* null.
|
||||
*
|
||||
* @return newest score, or null if none exist
|
||||
*/
|
||||
@Nullable
|
||||
protected Score findNewest()
|
||||
{
|
||||
return select().limit(1).executeSingle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the most recent score that was already computed. If no score has been
|
||||
* computed yet, returns zero.
|
||||
*
|
||||
* @return value of newest score, or zero if none exist
|
||||
*/
|
||||
protected int findNewestValue()
|
||||
{
|
||||
Score newest = findNewest();
|
||||
if(newest == null) return 0;
|
||||
else return newest.score;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 void invalidateNewerThan(long timestamp)
|
||||
{
|
||||
new Delete().from(Score.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
@@ -64,79 +95,137 @@ public class ScoreList
|
||||
.execute();
|
||||
}
|
||||
|
||||
public Integer getNewestValue()
|
||||
/**
|
||||
* Computes and saves the scores that are missing inside a given time interval. 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.
|
||||
*
|
||||
* This function assumes that there are no gaps on the scores. That is, if the newest score has
|
||||
* timestamp t, then every score with timestamp lower than t has already been computed.
|
||||
*
|
||||
* @param from timestamp of the beginning of the interval
|
||||
* @param to timestamp of the end of the time interval
|
||||
*/
|
||||
protected void compute(long from, long to)
|
||||
{
|
||||
int beginningScore;
|
||||
long beginningTime;
|
||||
final long day = DateHelper.millisecondsInOneDay;
|
||||
final double freq = ((double) habit.freqNum) / habit.freqDen;
|
||||
|
||||
long today = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
||||
long day = DateHelper.millisecondsInOneDay;
|
||||
int newestScoreValue = findNewestValue();
|
||||
Score newestScore = findNewest();
|
||||
|
||||
double freq = ((double) habit.freqNum) / habit.freqDen;
|
||||
double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1));
|
||||
if(newestScore != null)
|
||||
from = newestScore.timestamp + day;
|
||||
|
||||
Score newestScore = getNewest();
|
||||
if (newestScore == null)
|
||||
final int checkmarkValues[] = habit.checkmarks.getValues(from, to);
|
||||
final int firstScore = newestScoreValue;
|
||||
final long beginning = from;
|
||||
|
||||
ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command()
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if (oldestRep == null) return 0;
|
||||
beginningTime = oldestRep.timestamp;
|
||||
beginningScore = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
beginningTime = newestScore.timestamp + day;
|
||||
beginningScore = newestScore.score;
|
||||
}
|
||||
|
||||
long nDays = (today - beginningTime) / day;
|
||||
if (nDays < 0) return newestScore.score;
|
||||
|
||||
int reps[] = habit.checkmarks.getValues(beginningTime, today);
|
||||
|
||||
ActiveAndroid.beginTransaction();
|
||||
int lastScore = beginningScore;
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < reps.length; i++)
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
Score s = new Score();
|
||||
s.habit = habit;
|
||||
s.timestamp = beginningTime + day * i;
|
||||
s.score = (int) (lastScore * multiplier);
|
||||
if (reps[reps.length - i - 1] == 2)
|
||||
int lastScore = firstScore;
|
||||
|
||||
for (int i = 0; i < checkmarkValues.length; i++)
|
||||
{
|
||||
s.score += 1000000;
|
||||
s.score = Math.min(s.score, Score.MAX_SCORE);
|
||||
int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1];
|
||||
|
||||
Score s = new Score();
|
||||
s.habit = habit;
|
||||
s.timestamp = beginning + day * i;
|
||||
s.score = lastScore = Score.compute(freq, lastScore, checkmarkValue);
|
||||
s.save();
|
||||
}
|
||||
s.save();
|
||||
|
||||
lastScore = s.score;
|
||||
}
|
||||
|
||||
ActiveAndroid.setTransactionSuccessful();
|
||||
} finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
}
|
||||
|
||||
return lastScore;
|
||||
});
|
||||
}
|
||||
|
||||
public int[] getAllValues(Long fromTimestamp, Long toTimestamp, Long divisor)
|
||||
/**
|
||||
* Returns the score for a certain day.
|
||||
*
|
||||
* @param timestamp the timestamp for the day
|
||||
* @return the score for the day
|
||||
*/
|
||||
@Nullable
|
||||
protected Score get(long timestamp)
|
||||
{
|
||||
// Force rebuild of the score table
|
||||
getNewestValue();
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if(oldestRep == null) return null;
|
||||
|
||||
Long offset = toTimestamp - (divisor - 1) * DateHelper.millisecondsInOneDay;
|
||||
compute(oldestRep.timestamp, timestamp);
|
||||
|
||||
return select().where("timestamp = ?", timestamp).executeSingle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the score for a given day.
|
||||
*
|
||||
* @param timestamp the timestamp of a day
|
||||
* @return score for that day
|
||||
*/
|
||||
public int getValue(long timestamp)
|
||||
{
|
||||
Score s = get(timestamp);
|
||||
if(s == null) return 0;
|
||||
else return s.score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values of all the scores, from day of the first repetition until today, grouped
|
||||
* in chunks of specified size.
|
||||
*
|
||||
* If the group size is one, then the value of each score is returned individually. If the group
|
||||
* is, for example, seven, then the days are grouped in groups of seven consecutive days.
|
||||
*
|
||||
* The values are returned in an array of integers, with one entry for each group of days in the
|
||||
* interval. This value corresponds to the average of the scores for the days inside the group.
|
||||
* The first entry corresponds to the ending of the interval (that is, the most recent group of
|
||||
* days). The last entry corresponds to the beginning of the interval. As usual, the time of the
|
||||
* day for the timestamps should be midnight (UTC). The endpoints of the interval are included.
|
||||
*
|
||||
* The values are returned in an integer array. There is one entry for each day inside the
|
||||
* interval. The first entry corresponds to today, while the last entry corresponds to the
|
||||
* day of the oldest repetition.
|
||||
*
|
||||
* @param divisor the size of the groups
|
||||
* @return array of values, with one entry for each group of days
|
||||
*/
|
||||
@NonNull
|
||||
public int[] getAllValues(long divisor)
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if(oldestRep == null) return new int[0];
|
||||
|
||||
long fromTimestamp = oldestRep.timestamp;
|
||||
long toTimestamp = DateHelper.getStartOfToday();
|
||||
return getValues(fromTimestamp, toTimestamp, divisor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as getAllValues(long), but using a specified interval.
|
||||
*
|
||||
* @param from beginning of the interval (included)
|
||||
* @param to end of the interval (included)
|
||||
* @param divisor size of the groups
|
||||
* @return array of values, with one entry for each group of days
|
||||
*/
|
||||
@NonNull
|
||||
protected int[] getValues(long from, long to, long divisor)
|
||||
{
|
||||
compute(from, to);
|
||||
|
||||
divisor *= DateHelper.millisecondsInOneDay;
|
||||
Long offset = to + divisor - 1;
|
||||
|
||||
String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " +
|
||||
"where habit = ? and timestamp > ? and timestamp <= ? " +
|
||||
"where habit = ? and timestamp >= ? and timestamp <= ? " +
|
||||
"group by time order by time desc";
|
||||
|
||||
String params[] = { offset.toString(), divisor.toString(), habit.getId().toString(),
|
||||
fromTimestamp.toString(), toTimestamp.toString()};
|
||||
String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(),
|
||||
Long.toString(from), Long.toString(to) };
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
Cursor cursor = db.rawQuery(query, params);
|
||||
@@ -148,22 +237,45 @@ public class ScoreList
|
||||
|
||||
do
|
||||
{
|
||||
scores[k++] = (int) cursor.getLong(1);
|
||||
scores[k++] = (int) cursor.getFloat(1);
|
||||
}
|
||||
while (cursor.moveToNext());
|
||||
|
||||
cursor.close();
|
||||
return scores;
|
||||
|
||||
}
|
||||
|
||||
public int[] getAllValues(long divisor)
|
||||
/**
|
||||
* Returns the score for today.
|
||||
*
|
||||
* @return score for today
|
||||
*/
|
||||
@Nullable
|
||||
protected Score getToday()
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if(oldestRep == null) return new int[0];
|
||||
return get(DateHelper.getStartOfToday());
|
||||
}
|
||||
|
||||
long fromTimestamp = oldestRep.timestamp;
|
||||
long toTimestamp = DateHelper.getStartOfToday();
|
||||
return getAllValues(fromTimestamp, toTimestamp, divisor);
|
||||
/**
|
||||
* Returns the value of the score for today.
|
||||
*
|
||||
* @return value of today's score
|
||||
*/
|
||||
public int getTodayValue()
|
||||
{
|
||||
return getValue(DateHelper.getStartOfToday());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the star status for today. The returned value is either Score.EMPTY_STAR,
|
||||
* Score.HALF_STAR or Score.FULL_STAR.
|
||||
*
|
||||
* @return star status for today
|
||||
*/
|
||||
public int getTodayStarStatus()
|
||||
{
|
||||
Score score = getToday();
|
||||
if(score != null) return score.getStarStatus();
|
||||
else return Score.EMPTY_STAR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public class CheckmarkView extends View
|
||||
public void setHabit(Habit habit)
|
||||
{
|
||||
this.check_status = habit.checkmarks.getTodayValue();
|
||||
this.star_status = habit.scores.getCurrentStarStatus();
|
||||
this.star_status = habit.scores.getTodayStarStatus();
|
||||
this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), Color.blue(habit.color));
|
||||
this.label = habit.name;
|
||||
updateLabel();
|
||||
|
||||
@@ -171,7 +171,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
|
||||
else
|
||||
{
|
||||
if (habit == null) return;
|
||||
scores = habit.scores.getAllValues(BUCKET_SIZE * DateHelper.millisecondsInOneDay);
|
||||
scores = habit.scores.getAllValues(BUCKET_SIZE);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
@@ -181,13 +181,13 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
|
||||
{
|
||||
Random random = new Random();
|
||||
scores = new int[100];
|
||||
scores[0] = Score.MAX_SCORE / 2;
|
||||
scores[0] = Score.MAX_VALUE / 2;
|
||||
|
||||
for(int i = 1; i < 100; i++)
|
||||
{
|
||||
int step = Score.MAX_SCORE / 10;
|
||||
int step = Score.MAX_VALUE / 10;
|
||||
scores[i] = scores[i - 1] + random.nextInt(step * 2) - step;
|
||||
scores[i] = Math.max(0, Math.min(Score.MAX_SCORE, scores[i]));
|
||||
scores[i] = Math.max(0, Math.min(Score.MAX_VALUE, scores[i]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
|
||||
int offset = nColumns - k - 1 + getDataOffset();
|
||||
if(offset < scores.length) score = scores[offset];
|
||||
|
||||
double sRelative = ((double) score) / Score.MAX_SCORE;
|
||||
double sRelative = ((double) score) / Score.MAX_VALUE;
|
||||
int height = (int) (columnHeight * sRelative);
|
||||
|
||||
rect.set(0, 0, baseSize, baseSize);
|
||||
|
||||
Reference in New Issue
Block a user