Update score calculation

The value of a score is now a double. For boolean habits, this number goes from zero
to one and corresponds to the percentage. For numerical habits, it now corresponds
to a weighted average of the checkmark values. Also, for non-daily boolean habits, the
score now increases with implicit checkmarks.
pull/157/merge
Alinson S. Xavier 9 years ago
parent d3b540199c
commit 5653651c0d

@ -12,7 +12,7 @@ android {
minSdkVersion 15
targetSdkVersion 25
buildConfigField "Integer", "databaseVersion", "16"
buildConfigField "Integer", "databaseVersion", "17"
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

@ -51,7 +51,7 @@ public class CheckmarkWidgetViewTest extends BaseViewTest
view = new CheckmarkWidgetView(targetContext);
int color = ColorUtils.getAndroidTestColor(habit.getColor());
double score = habit.getScores().getTodayValue();
float percentage = (float) score / Score.MAX_VALUE;
float percentage = (float) score;
view.setActiveColor(color);
view.setCheckmarkValue(habit.getCheckmarks().getTodayValue());

@ -0,0 +1,5 @@
DROP TABLE Score;
CREATE TABLE Score (Id INTEGER PRIMARY KEY AUTOINCREMENT, habit INTEGER REFERENCES Habits(Id), score REAL, timestamp INTEGER);
CREATE INDEX idx_score_habit_timestamp on score(habit, timestamp);
delete from Streak;
delete from Checkmarks;

@ -108,15 +108,15 @@ public class ScoreChart extends ScrollableChart
Random random = new Random();
scores = new LinkedList<>();
int previous = Score.MAX_VALUE / 2;
double previous = 0.5f;
long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
for (int i = 1; i < 100; i++)
{
int step = Score.MAX_VALUE / 10;
int current = previous + random.nextInt(step * 2) - step;
current = Math.max(0, Math.min(Score.MAX_VALUE, current));
double step = 0.1f;
double current = previous + random.nextDouble() * step * 2 - step;
current = Math.max(0, Math.min(1.0f, current));
scores.add(new Score(timestamp, current));
previous = current;
timestamp -= day;
@ -190,8 +190,7 @@ public class ScoreChart extends ScrollableChart
double score = scores.get(offset).getValue();
long timestamp = scores.get(offset).getTimestamp();
double relativeScore = score / Score.MAX_VALUE;
int height = (int) (columnHeight * relativeScore);
int height = (int) (columnHeight * score);
rect.set(0, 0, baseSize, baseSize);
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,

@ -140,7 +140,7 @@ public class HabitCardView extends FrameLayout
public void setScore(double score)
{
float percentage = (float) score / Score.MAX_VALUE;
float percentage = (float) score;
scoreRing.setPercentage(percentage);
scoreRing.setPrecision(1.0f / 16);
postInvalidate();

@ -105,9 +105,9 @@ public class OverviewCard extends HabitCard
private void initEditMode()
{
color = ColorUtils.getAndroidTestColor(1);
cache.todayScore = Score.MAX_VALUE * 0.6f;
cache.lastMonthScore = Score.MAX_VALUE * 0.42f;
cache.lastYearScore = Score.MAX_VALUE * 0.75f;
cache.todayScore = 0.6f;
cache.lastMonthScore = 0.42f;
cache.lastYearScore = 0.75f;
refreshColors();
refreshScore();
}
@ -121,11 +121,9 @@ public class OverviewCard extends HabitCard
private void refreshScore()
{
float todayPercentage = cache.todayScore / Score.MAX_VALUE;
float monthDiff =
todayPercentage - (cache.lastMonthScore / Score.MAX_VALUE);
float yearDiff =
todayPercentage - (cache.lastYearScore / Score.MAX_VALUE);
float todayPercentage = cache.todayScore;
float monthDiff = todayPercentage - cache.lastMonthScore;
float yearDiff = todayPercentage - cache.lastYearScore;
scoreRing.setPercentage(todayPercentage);
scoreLabel.setText(String.format("%.0f%%", todayPercentage * 100));

@ -202,7 +202,7 @@ public class HabitsCSVExporter
checksWriter.write(String.valueOf(checkmarks.get(j)[i]));
checksWriter.write(DELIMITER);
String score =
String.format("%.4f", ((float) scores.get(j)[i]) / Score.MAX_VALUE);
String.format("%.4f", ((float) scores.get(j)[i]));
scoresWriter.write(score);
scoresWriter.write(DELIMITER);
}

@ -21,16 +21,13 @@ package org.isoron.uhabits.models;
import org.apache.commons.lang3.builder.*;
import static java.lang.Math.*;
/**
* Represents how strong a habit is at a certain date.
*/
public final class Score
{
/**
* Maximum score value attainable by any habit.
*/
public static final int MAX_VALUE = 19259478;
/**
* Timestamp of the day to which this score applies. Time of day should be
* midnight (UTC).
@ -55,27 +52,20 @@ public final class Score
* 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.
* <p>
* 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,
double previousScore,
int checkmarkValue)
public static double compute(double frequency,
double previousScore,
int checkmarkValue)
{
double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1));
int score = (int) (previousScore * multiplier);
double multiplier = pow(0.5, frequency / 13.0);
if (checkmarkValue == Checkmark.CHECKED_EXPLICITLY)
{
score += 1000000;
score = Math.min(score, Score.MAX_VALUE);
}
double score = previousScore * multiplier;
score += checkmarkValue * (1 - multiplier);
return score;
}

@ -132,7 +132,7 @@ public abstract class ScoreList implements Iterable<Score>
public List<Score> groupBy(DateUtils.TruncateField field)
{
computeAll();
HashMap<Long, ArrayList<Long>> groups = getGroupedValues(field);
HashMap<Long, ArrayList<Double>> groups = getGroupedValues(field);
List<Score> scores = groupsToAvgScores(groups);
Collections.sort(scores, (s1, s2) -> s2.compareNewer(s1));
return scores;
@ -173,7 +173,7 @@ public abstract class ScoreList implements Iterable<Score>
{
String timestamp = dateFormat.format(s.getTimestamp());
String score =
String.format("%.4f", ((float) s.getValue()) / Score.MAX_VALUE);
String.format("%.4f", s.getValue());
out.write(String.format("%s,%s\n", timestamp, score));
}
}
@ -277,6 +277,8 @@ public abstract class ScoreList implements Iterable<Score>
for (int i = 0; i < checkmarkValues.length; i++)
{
int value = checkmarkValues[checkmarkValues.length - i - 1];
if(!habit.isNumerical() && value > 0) value = 1;
previousValue = Score.compute(freq, previousValue, value);
scores.add(new Score(from + day * i, previousValue));
}
@ -285,9 +287,9 @@ public abstract class ScoreList implements Iterable<Score>
}
@NonNull
private HashMap<Long, ArrayList<Long>> getGroupedValues(DateUtils.TruncateField field)
private HashMap<Long, ArrayList<Double>> getGroupedValues(DateUtils.TruncateField field)
{
HashMap<Long, ArrayList<Long>> groups = new HashMap<>();
HashMap<Long, ArrayList<Double>> groups = new HashMap<>();
for (Score s : this)
{
@ -296,26 +298,26 @@ public abstract class ScoreList implements Iterable<Score>
if (!groups.containsKey(groupTimestamp))
groups.put(groupTimestamp, new ArrayList<>());
groups.get(groupTimestamp).add((long) s.getValue());
groups.get(groupTimestamp).add(s.getValue());
}
return groups;
}
@NonNull
private List<Score> groupsToAvgScores(HashMap<Long, ArrayList<Long>> groups)
private List<Score> groupsToAvgScores(HashMap<Long, ArrayList<Double>> groups)
{
List<Score> scores = new LinkedList<>();
for (Long timestamp : groups.keySet())
{
long meanValue = 0L;
ArrayList<Long> groupValues = groups.get(timestamp);
double meanValue = 0.0;
ArrayList<Double> groupValues = groups.get(timestamp);
for (Long v : groupValues) meanValue += v;
for (Double v : groupValues) meanValue += v;
meanValue /= groupValues.size();
scores.add(new Score(timestamp, (int) meanValue));
scores.add(new Score(timestamp, meanValue));
}
return scores;

@ -53,7 +53,7 @@ public class CheckmarkWidget extends BaseWidget
CheckmarkWidgetView view = (CheckmarkWidgetView) v;
int color = ColorUtils.getColor(getContext(), habit.getColor());
double score = habit.getScores().getTodayValue();
float percentage = (float) score / Score.MAX_VALUE;
float percentage = (float) score;
int checkmark = habit.getCheckmarks().getTodayValue();
view.setPercentage(percentage);

@ -87,7 +87,7 @@ public class EditHabitCommandTest extends BaseUnitTest
command.execute();
assertThat(habit.getName(), equalTo("modified"));
assertThat(habit.getScores().getTodayValue(),
greaterThan(originalScore));
lessThan(originalScore));
command.undo();
assertThat(habit.getName(), equalTo("original"));
@ -96,6 +96,6 @@ public class EditHabitCommandTest extends BaseUnitTest
command.execute();
assertThat(habit.getName(), equalTo("modified"));
assertThat(habit.getScores().getTodayValue(),
greaterThan(originalScore));
lessThan(originalScore));
}
}

@ -28,9 +28,12 @@ import java.util.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.number.IsCloseTo.closeTo;
public class ScoreListTest extends BaseUnitTest
{
private static final double E = 1e-6;
private Habit habit;
@Override
@ -47,42 +50,39 @@ public class ScoreListTest extends BaseUnitTest
toggleRepetitions(0, 20);
double expectedValues[] = {
12629351,
12266245,
11883254,
11479288,
11053198,
10603773,
10129735,
9629735,
9102352,
8546087,
7959357,
7340494,
6687738,
5999234,
5273023,
4507040,
3699107,
2846927,
1948077,
1000000
0.655747,
0.636894,
0.617008,
0.596033,
0.573910,
0.550574,
0.525961,
0.500000,
0.472617,
0.443734,
0.413270,
0.381137,
0.347244,
0.311495,
0.273788,
0.234017,
0.192067,
0.147820,
0.101149,
0.051922,
};
double actualValues[] = new double[expectedValues.length];
int i = 0;
for (Score s : habit.getScores())
actualValues[i++] = s.getValue();
assertThat(actualValues, equalTo(expectedValues));
assertThat(s.getValue(), closeTo(expectedValues[i++], E));
}
@Test
public void test_getTodayValue()
{
toggleRepetitions(0, 20);
assertThat(habit.getScores().getTodayValue(), equalTo(12629351.0));
double actual = habit.getScores().getTodayValue();
assertThat(actual, closeTo(0.655747, E));
}
@Test
@ -91,36 +91,36 @@ public class ScoreListTest extends BaseUnitTest
toggleRepetitions(0, 20);
double expectedValues[] = {
12629351,
12266245,
11883254,
11479288,
11053198,
10603773,
10129735,
9629735,
9102352,
8546087,
7959357,
7340494,
6687738,
5999234,
5273023,
4507040,
3699107,
2846927,
1948077,
1000000,
0,
0,
0
0.655747,
0.636894,
0.617008,
0.596033,
0.573910,
0.550574,
0.525961,
0.500000,
0.472617,
0.443734,
0.413270,
0.381137,
0.347244,
0.311495,
0.273788,
0.234017,
0.192067,
0.147820,
0.101149,
0.051922,
0.000000,
0.000000,
0.000000
};
ScoreList scores = habit.getScores();
long current = DateUtils.getStartOfToday();
for (double expectedValue : expectedValues)
{
assertThat(scores.getValue(current), equalTo(expectedValue));
assertThat(scores.getValue(current), closeTo(expectedValue, E));
current -= DateUtils.millisecondsInOneDay;
}
}
@ -133,23 +133,23 @@ public class ScoreListTest extends BaseUnitTest
habit.getScores().groupBy(DateUtils.TruncateField.MONTH);
assertThat(list.size(), equalTo(5));
assertThat(list.get(0).getValue(), equalTo(14634077.0));
assertThat(list.get(1).getValue(), equalTo(12969133.0));
assertThat(list.get(2).getValue(), equalTo(10595391.0));
assertThat(list.get(0).getValue(), closeTo(0.549096, E));
assertThat(list.get(1).getValue(), closeTo(0.480098, E));
assertThat(list.get(2).getValue(), closeTo(0.377885, E));
}
@Test
public void test_invalidateNewerThan()
{
assertThat(habit.getScores().getTodayValue(), equalTo(0.0));
assertThat(habit.getScores().getTodayValue(), closeTo(0.0, E));
toggleRepetitions(0, 2);
assertThat(habit.getScores().getTodayValue(), equalTo(1948077.0));
assertThat(habit.getScores().getTodayValue(), closeTo(0.101149, E));
habit.setFrequency(new Frequency(1, 2));
habit.getScores().invalidateNewerThan(0);
assertThat(habit.getScores().getTodayValue(), equalTo(1974654.0));
assertThat(habit.getScores().getTodayValue(), closeTo(0.051922, E));
}
@Test
@ -157,16 +157,16 @@ public class ScoreListTest extends BaseUnitTest
{
Habit habit = fixtures.createShortHabit();
String expectedCSV = "2015-01-25,0.2649\n" +
"2015-01-24,0.2205\n" +
"2015-01-23,0.2283\n" +
"2015-01-22,0.2364\n" +
"2015-01-21,0.1909\n" +
"2015-01-20,0.1439\n" +
"2015-01-19,0.0952\n" +
"2015-01-18,0.0986\n" +
"2015-01-17,0.1021\n" +
"2015-01-16,0.0519\n";
String expectedCSV = "2015-01-25,0.2372\n" +
"2015-01-24,0.2096\n" +
"2015-01-23,0.2172\n" +
"2015-01-22,0.1889\n" +
"2015-01-21,0.1595\n" +
"2015-01-20,0.1291\n" +
"2015-01-19,0.0976\n" +
"2015-01-18,0.1011\n" +
"2015-01-17,0.0686\n" +
"2015-01-16,0.0349\n";
StringWriter writer = new StringWriter();
habit.getScores().writeCSV(writer);
@ -186,13 +186,16 @@ public class ScoreListTest extends BaseUnitTest
long to = today - 2 * day;
double[] expected = {
11883254,
11479288,
11053198,
0.617008,
0.596033,
0.573909,
};
double[] actual = habit.getScores().getValues(from, to);
assertThat(actual, equalTo(expected));
assertThat(actual.length, equalTo(expected.length));
for(int i = 0; i < actual.length; i++)
assertThat(actual[i], closeTo(expected[i], E));
}
private void toggleRepetitions(final int from, final int to)

@ -19,15 +19,17 @@
package org.isoron.uhabits.models;
import org.isoron.uhabits.BaseUnitTest;
import org.junit.Before;
import org.junit.Test;
import org.isoron.uhabits.*;
import org.junit.*;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.hamcrest.number.IsCloseTo.*;
import static org.isoron.uhabits.models.Score.*;
import static org.junit.Assert.*;
public class ScoreTest extends BaseUnitTest
{
private static final double E = 1e-6;
@Override
@Before
public void setUp()
@ -38,46 +40,30 @@ public class ScoreTest extends BaseUnitTest
@Test
public void test_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));
int check = 1;
double freq = 1.0;
assertThat(compute(freq, 0, check), closeTo(0.051922, E));
assertThat(compute(freq, 0.5, check), closeTo(0.525961, E));
assertThat(compute(freq, 0.75, check), closeTo(0.762981, E));
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));
check = 0;
assertThat(compute(freq, 0, check), closeTo(0, E));
assertThat(compute(freq, 0.5, check), closeTo(0.474039, E));
assertThat(compute(freq, 0.75, check), closeTo(0.711058, E));
}
@Test
public void test_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));
int check = 1;
double freq = 1 / 3.0;
assertThat(compute(freq, 0, check), closeTo(0.017616, E));
assertThat(compute(freq, 0.5, check), closeTo(0.508808, E));
assertThat(compute(freq, 0.75, check), closeTo(0.754404, E));
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));
check = 0;
assertThat(compute(freq, 0, check), closeTo(0.0, E));
assertThat(compute(freq, 0.5, check), closeTo(0.491192, E));
assertThat(compute(freq, 0.75, check), closeTo(0.736788, E));
}
}

Loading…
Cancel
Save