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()
|
static Habit createEmptyHabit()
|
||||||
{
|
{
|
||||||
Habit habit = new Habit();
|
Habit habit = new Habit();
|
||||||
|
habit.freqNum = 1;
|
||||||
|
habit.freqDen = 1;
|
||||||
habit.save();
|
habit.save();
|
||||||
return habit;
|
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.checkmarks.deleteNewerThan(0);
|
||||||
habit.streaks.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.checkmarks.deleteNewerThan(0);
|
||||||
habit.streaks.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);
|
RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing);
|
||||||
scoreRing.setColor(habit.color);
|
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)
|
private void updateHeaders(View view)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ public class CSVExporter
|
|||||||
|
|
||||||
public String formatScore(int score)
|
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
|
private void writeScores(String dirPath, Habit habit) throws IOException
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ public class HabitListLoader
|
|||||||
if (isCancelled()) return null;
|
if (isCancelled()) return null;
|
||||||
|
|
||||||
Long id = h.getId();
|
Long id = h.getId();
|
||||||
newScores.put(id, h.scores.getNewestValue());
|
newScores.put(id, h.scores.getTodayValue());
|
||||||
newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
||||||
|
|
||||||
publishProgress(current++, newHabits.size());
|
publishProgress(current++, newHabits.size());
|
||||||
@@ -213,7 +213,7 @@ public class HabitListLoader
|
|||||||
|
|
||||||
Habit h = Habit.get(id);
|
Habit h = Habit.get(id);
|
||||||
habits.put(id, h);
|
habits.put(id, h);
|
||||||
scores.put(id, h.scores.getNewestValue());
|
scores.put(id, h.scores.getTodayValue());
|
||||||
checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -117,21 +117,25 @@ public class Habit extends Model
|
|||||||
/**
|
/**
|
||||||
* List of streaks belonging to this habit.
|
* List of streaks belonging to this habit.
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
public StreakList streaks;
|
public StreakList streaks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of scores belonging to this habit.
|
* List of scores belonging to this habit.
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
public ScoreList scores;
|
public ScoreList scores;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of repetitions belonging to this habit.
|
* List of repetitions belonging to this habit.
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
public RepetitionList repetitions;
|
public RepetitionList repetitions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of checkmarks belonging to this habit.
|
* List of checkmarks belonging to this habit.
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
public CheckmarkList checkmarks;
|
public CheckmarkList checkmarks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,7 +146,11 @@ public class Habit extends Model
|
|||||||
public Habit(Habit model)
|
public Habit(Habit model)
|
||||||
{
|
{
|
||||||
copyAttributes(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.archived = 0;
|
||||||
this.freqDen = 7;
|
this.freqDen = 7;
|
||||||
this.freqNum = 3;
|
this.freqNum = 3;
|
||||||
initializeLists();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeLists()
|
checkmarks = new CheckmarkList(this);
|
||||||
{
|
|
||||||
streaks = new StreakList(this);
|
streaks = new StreakList(this);
|
||||||
scores = new ScoreList(this);
|
scores = new ScoreList(this);
|
||||||
repetitions = new RepetitionList(this);
|
repetitions = new RepetitionList(this);
|
||||||
checkmarks = new CheckmarkList(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ public class RepetitionList
|
|||||||
rep.save();
|
rep.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
habit.scores.deleteNewerThan(timestamp);
|
habit.scores.invalidateNewerThan(timestamp);
|
||||||
habit.checkmarks.deleteNewerThan(timestamp);
|
habit.checkmarks.deleteNewerThan(timestamp);
|
||||||
habit.streaks.deleteNewerThan(timestamp);
|
habit.streaks.deleteNewerThan(timestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,94 @@ import com.activeandroid.annotation.Table;
|
|||||||
@Table(name = "Score")
|
@Table(name = "Score")
|
||||||
public class Score extends Model
|
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 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")
|
@Column(name = "habit")
|
||||||
public Habit habit;
|
public Habit habit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp of the day to which this score applies. Time of day should be midnight (UTC).
|
||||||
|
*/
|
||||||
@Column(name = "timestamp")
|
@Column(name = "timestamp")
|
||||||
public Long timestamp;
|
public Long timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value of the score.
|
||||||
|
*/
|
||||||
@Column(name = "score")
|
@Column(name = "score")
|
||||||
public Integer 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.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import com.activeandroid.ActiveAndroid;
|
import com.activeandroid.ActiveAndroid;
|
||||||
import com.activeandroid.Cache;
|
import com.activeandroid.Cache;
|
||||||
import com.activeandroid.query.Delete;
|
import com.activeandroid.query.Delete;
|
||||||
|
import com.activeandroid.query.From;
|
||||||
import com.activeandroid.query.Select;
|
import com.activeandroid.query.Select;
|
||||||
|
|
||||||
|
import org.isoron.helpers.ActiveAndroidHelper;
|
||||||
import org.isoron.helpers.DateHelper;
|
import org.isoron.helpers.DateHelper;
|
||||||
|
|
||||||
public class ScoreList
|
public class ScoreList
|
||||||
{
|
{
|
||||||
|
@NonNull
|
||||||
private Habit habit;
|
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;
|
this.habit = habit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCurrentStarStatus()
|
protected From select()
|
||||||
{
|
{
|
||||||
int score = getNewestValue();
|
return new Select()
|
||||||
|
.from(Score.class)
|
||||||
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)
|
|
||||||
.where("habit = ?", habit.getId())
|
.where("habit = ?", habit.getId())
|
||||||
.orderBy("timestamp desc")
|
.orderBy("timestamp desc");
|
||||||
.limit(1)
|
|
||||||
.executeSingle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
new Delete().from(Score.class)
|
||||||
.where("habit = ?", habit.getId())
|
.where("habit = ?", habit.getId())
|
||||||
@@ -64,79 +95,137 @@ public class ScoreList
|
|||||||
.execute();
|
.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;
|
final long day = DateHelper.millisecondsInOneDay;
|
||||||
long beginningTime;
|
final double freq = ((double) habit.freqNum) / habit.freqDen;
|
||||||
|
|
||||||
long today = DateHelper.getStartOfDay(DateHelper.getLocalTime());
|
int newestScoreValue = findNewestValue();
|
||||||
long day = DateHelper.millisecondsInOneDay;
|
Score newestScore = findNewest();
|
||||||
|
|
||||||
double freq = ((double) habit.freqNum) / habit.freqDen;
|
if(newestScore != null)
|
||||||
double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1));
|
from = newestScore.timestamp + day;
|
||||||
|
|
||||||
Score newestScore = getNewest();
|
final int checkmarkValues[] = habit.checkmarks.getValues(from, to);
|
||||||
if (newestScore == null)
|
final int firstScore = newestScoreValue;
|
||||||
|
final long beginning = from;
|
||||||
|
|
||||||
|
ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command()
|
||||||
{
|
{
|
||||||
Repetition oldestRep = habit.repetitions.getOldest();
|
@Override
|
||||||
if (oldestRep == null) return 0;
|
public void execute()
|
||||||
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++)
|
|
||||||
{
|
{
|
||||||
Score s = new Score();
|
int lastScore = firstScore;
|
||||||
s.habit = habit;
|
|
||||||
s.timestamp = beginningTime + day * i;
|
for (int i = 0; i < checkmarkValues.length; i++)
|
||||||
s.score = (int) (lastScore * multiplier);
|
|
||||||
if (reps[reps.length - i - 1] == 2)
|
|
||||||
{
|
{
|
||||||
s.score += 1000000;
|
int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1];
|
||||||
s.score = Math.min(s.score, Score.MAX_SCORE);
|
|
||||||
|
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
|
Repetition oldestRep = habit.repetitions.getOldest();
|
||||||
getNewestValue();
|
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 " +
|
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";
|
"group by time order by time desc";
|
||||||
|
|
||||||
String params[] = { offset.toString(), divisor.toString(), habit.getId().toString(),
|
String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(),
|
||||||
fromTimestamp.toString(), toTimestamp.toString()};
|
Long.toString(from), Long.toString(to) };
|
||||||
|
|
||||||
SQLiteDatabase db = Cache.openDatabase();
|
SQLiteDatabase db = Cache.openDatabase();
|
||||||
Cursor cursor = db.rawQuery(query, params);
|
Cursor cursor = db.rawQuery(query, params);
|
||||||
@@ -148,22 +237,45 @@ public class ScoreList
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
scores[k++] = (int) cursor.getLong(1);
|
scores[k++] = (int) cursor.getFloat(1);
|
||||||
}
|
}
|
||||||
while (cursor.moveToNext());
|
while (cursor.moveToNext());
|
||||||
|
|
||||||
cursor.close();
|
cursor.close();
|
||||||
return scores;
|
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();
|
return get(DateHelper.getStartOfToday());
|
||||||
if(oldestRep == null) return new int[0];
|
}
|
||||||
|
|
||||||
long fromTimestamp = oldestRep.timestamp;
|
/**
|
||||||
long toTimestamp = DateHelper.getStartOfToday();
|
* Returns the value of the score for today.
|
||||||
return getAllValues(fromTimestamp, toTimestamp, divisor);
|
*
|
||||||
|
* @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)
|
public void setHabit(Habit habit)
|
||||||
{
|
{
|
||||||
this.check_status = habit.checkmarks.getTodayValue();
|
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.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), Color.blue(habit.color));
|
||||||
this.label = habit.name;
|
this.label = habit.name;
|
||||||
updateLabel();
|
updateLabel();
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (habit == null) return;
|
if (habit == null) return;
|
||||||
scores = habit.scores.getAllValues(BUCKET_SIZE * DateHelper.millisecondsInOneDay);
|
scores = habit.scores.getAllValues(BUCKET_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -181,13 +181,13 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
|
|||||||
{
|
{
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
scores = new int[100];
|
scores = new int[100];
|
||||||
scores[0] = Score.MAX_SCORE / 2;
|
scores[0] = Score.MAX_VALUE / 2;
|
||||||
|
|
||||||
for(int i = 1; i < 100; i++)
|
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] = 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();
|
int offset = nColumns - k - 1 + getDataOffset();
|
||||||
if(offset < scores.length) score = scores[offset];
|
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);
|
int height = (int) (columnHeight * sRelative);
|
||||||
|
|
||||||
rect.set(0, 0, baseSize, baseSize);
|
rect.set(0, 0, baseSize, baseSize);
|
||||||
|
|||||||
Reference in New Issue
Block a user