Reduce time required to form non-daily habits; smooth out irregular schedules

pull/655/head
Alinson S. Xavier 5 years ago
parent ef186d55c6
commit 20142d5f94

@ -63,7 +63,7 @@ public final class Score
double previousScore, double previousScore,
double checkmarkValue) double checkmarkValue)
{ {
double multiplier = pow(0.5, frequency / 13.0); double multiplier = pow(0.5, sqrt(frequency) / 13.0);
double score = previousScore * multiplier; double score = previousScore * multiplier;
score += checkmarkValue * (1 - multiplier); score += checkmarkValue * (1 - multiplier);

@ -275,6 +275,15 @@ public abstract class ScoreList implements Iterable<Score>
final double freq = habit.getFrequency().toDouble(); final double freq = habit.getFrequency().toDouble();
final int[] checkmarkValues = habit.getCheckmarks().getValues(from, to); final int[] checkmarkValues = habit.getCheckmarks().getValues(from, to);
// For non-daily boolean habits, we double the numerator and the denominator to smooth
// out irregular repetition schedules (for example, weekly habits performed on different
// days of the week)
if (!habit.isNumerical() && freq < 1.0)
{
numerator *= 2;
denominator *= 2;
}
List<Score> scores = new LinkedList<>(); List<Score> scores = new LinkedList<>();
for (int i = 0; i < checkmarkValues.length; i++) for (int i = 0; i < checkmarkValues.length; i++)

@ -20,6 +20,7 @@
package org.isoron.uhabits.core.models; package org.isoron.uhabits.core.models;
import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.test.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.junit.*; import org.junit.*;
@ -29,6 +30,7 @@ import java.util.*;
import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.*; import static org.hamcrest.core.IsEqual.*;
import static org.hamcrest.number.IsCloseTo.*; import static org.hamcrest.number.IsCloseTo.*;
import static org.hamcrest.number.OrderingComparison.*;
import static org.isoron.uhabits.core.models.Checkmark.*; import static org.isoron.uhabits.core.models.Checkmark.*;
public class ScoreListTest extends BaseUnitTest public class ScoreListTest extends BaseUnitTest
@ -48,7 +50,7 @@ public class ScoreListTest extends BaseUnitTest
@Test @Test
public void test_getAll() public void test_getAll()
{ {
toggleRepetitions(0, 20); toggle(0, 20);
double expectedValues[] = { double expectedValues[] = {
0.655747, 0.655747,
@ -81,7 +83,7 @@ public class ScoreListTest extends BaseUnitTest
@Test @Test
public void test_getTodayValue() public void test_getTodayValue()
{ {
toggleRepetitions(0, 20); toggle(0, 20);
double actual = habit.getScores().getTodayValue(); double actual = habit.getScores().getTodayValue();
assertThat(actual, closeTo(0.655747, E)); assertThat(actual, closeTo(0.655747, E));
} }
@ -89,7 +91,7 @@ public class ScoreListTest extends BaseUnitTest
@Test @Test
public void test_getValue() public void test_getValue()
{ {
toggleRepetitions(0, 20); toggle(0, 20);
double expectedValues[] = { double expectedValues[] = {
0.655747, 0.655747,
@ -172,7 +174,7 @@ public class ScoreListTest extends BaseUnitTest
@Test @Test
public void test_getValues() public void test_getValues()
{ {
toggleRepetitions(0, 20); toggle(0, 20);
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();
Timestamp from = today.minus(4); Timestamp from = today.minus(4);
@ -192,6 +194,8 @@ public class ScoreListTest extends BaseUnitTest
@Test @Test
public void test_imperfectNonDaily() public void test_imperfectNonDaily()
{ {
// If the habit should be performed 3 times per week and the user misses 1 repetition
// each week, score should converge to 66%.
habit.setFrequency(new Frequency(3, 7)); habit.setFrequency(new Frequency(3, 7));
ArrayList<Integer> values = new ArrayList<>(); ArrayList<Integer> values = new ArrayList<>();
for (int k = 0; k < 100; k++) for (int k = 0; k < 100; k++)
@ -207,10 +211,63 @@ public class ScoreListTest extends BaseUnitTest
toggle(values); toggle(values);
assertThat(habit.getScores().getTodayValue(), closeTo(2/3.0, E)); assertThat(habit.getScores().getTodayValue(), closeTo(2/3.0, E));
// Missing 2 repetitions out of 4 per week, the score should converge to 50%
habit.setFrequency(new Frequency(4, 7)); habit.setFrequency(new Frequency(4, 7));
assertThat(habit.getScores().getTodayValue(), closeTo(0.5, E)); assertThat(habit.getScores().getTodayValue(), closeTo(0.5, E));
} }
@Test
public void test_irregularNonDaily()
{
// If the user performs habit perfectly each week, but on different weekdays,
// score should still converge to 100%
habit.setFrequency(new Frequency(1, 7));
ArrayList<Integer> values = new ArrayList<>();
for (int k = 0; k < 100; k++)
{
// Week 0
values.add(YES_MANUAL);
values.add(NO);
values.add(NO);
values.add(NO);
values.add(NO);
values.add(NO);
values.add(NO);
// Week 1
values.add(NO);
values.add(NO);
values.add(NO);
values.add(NO);
values.add(NO);
values.add(NO);
values.add(YES_MANUAL);
}
toggle(values);
assertThat(habit.getScores().getTodayValue(), closeTo(1.0, 1e-3));
}
@Test
public void shouldAchieveHighScoreInReasonableTime()
{
// Daily habits should achieve at least 99% in 3 months
habit = fixtures.createEmptyHabit();
habit.setFrequency(Frequency.DAILY);
for (int i = 0; i < 90; i++) toggle(i);
assertThat(habit.getScores().getTodayValue(), greaterThan(0.99));
// Weekly habits should achieve at least 99% in 9 months
habit = fixtures.createEmptyHabit();
habit.setFrequency(Frequency.WEEKLY);
for (int i = 0; i < 39; i++) toggle(7 * i);
assertThat(habit.getScores().getTodayValue(), greaterThan(0.99));
// Monthly habits should achieve at least 99% in 18 months
habit.setFrequency(new Frequency(1, 30));
for (int i = 0; i < 18; i++) toggle(30 * i);
assertThat(habit.getScores().getTodayValue(), greaterThan(0.99));
}
@Test @Test
public void test_groupBy() public void test_groupBy()
{ {
@ -219,9 +276,9 @@ public class ScoreListTest extends BaseUnitTest
habit.getScores().groupBy(DateUtils.TruncateField.MONTH, Calendar.SATURDAY); habit.getScores().groupBy(DateUtils.TruncateField.MONTH, Calendar.SATURDAY);
assertThat(list.size(), equalTo(5)); assertThat(list.size(), equalTo(5));
assertThat(list.get(0).getValue(), closeTo(0.601508, E)); assertThat(list.get(0).getValue(), closeTo(0.644120, E));
assertThat(list.get(1).getValue(), closeTo(0.580580, E)); assertThat(list.get(1).getValue(), closeTo(0.713651, E));
assertThat(list.get(2).getValue(), closeTo(0.474609, E)); assertThat(list.get(2).getValue(), closeTo(0.571922, E));
} }
@Test @Test
@ -229,13 +286,13 @@ public class ScoreListTest extends BaseUnitTest
{ {
assertThat(habit.getScores().getTodayValue(), closeTo(0.0, E)); assertThat(habit.getScores().getTodayValue(), closeTo(0.0, E));
toggleRepetitions(0, 2); toggle(0, 2);
assertThat(habit.getScores().getTodayValue(), closeTo(0.101149, E)); assertThat(habit.getScores().getTodayValue(), closeTo(0.101149, E));
habit.setFrequency(new Frequency(1, 2)); habit.setFrequency(new Frequency(1, 2));
habit.getScores().invalidateNewerThan(new Timestamp(0)); habit.getScores().invalidateNewerThan(new Timestamp(0));
assertThat(habit.getScores().getTodayValue(), closeTo(0.051922, E)); assertThat(habit.getScores().getTodayValue(), closeTo(0.054816, E));
} }
@Test @Test
@ -244,16 +301,16 @@ public class ScoreListTest extends BaseUnitTest
Habit habit = fixtures.createShortHabit(); Habit habit = fixtures.createShortHabit();
String expectedCSV = String expectedCSV =
"2015-01-25,0.2234\n" + "2015-01-25,0.2557\n" +
"2015-01-24,0.2134\n" + "2015-01-24,0.2226\n" +
"2015-01-23,0.2031\n" + "2015-01-23,0.1991\n" +
"2015-01-22,0.1742\n" + "2015-01-22,0.1746\n" +
"2015-01-21,0.1443\n" + "2015-01-21,0.1379\n" +
"2015-01-20,0.1134\n" + "2015-01-20,0.0995\n" +
"2015-01-19,0.0994\n" + "2015-01-19,0.0706\n" +
"2015-01-18,0.0849\n" + "2015-01-18,0.0515\n" +
"2015-01-17,0.0518\n" + "2015-01-17,0.0315\n" +
"2015-01-16,0.0175\n"; "2015-01-16,0.0107\n";
StringWriter writer = new StringWriter(); StringWriter writer = new StringWriter();
habit.getScores().writeCSV(writer); habit.getScores().writeCSV(writer);
@ -261,7 +318,14 @@ public class ScoreListTest extends BaseUnitTest
assertThat(writer.toString(), equalTo(expectedCSV)); assertThat(writer.toString(), equalTo(expectedCSV));
} }
private void toggleRepetitions(final int from, final int to) private void toggle(final int offset)
{
RepetitionList reps = habit.getRepetitions();
Timestamp today = DateUtils.getToday();
reps.toggle(today.minus(offset));
}
private void toggle(final int from, final int to)
{ {
RepetitionList reps = habit.getRepetitions(); RepetitionList reps = habit.getRepetitions();
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();

@ -59,14 +59,14 @@ public class ScoreTest extends BaseUnitTest
{ {
int check = 1; int check = 1;
double freq = 1 / 3.0; double freq = 1 / 3.0;
assertThat(compute(freq, 0, check), closeTo(0.017616, E)); assertThat(compute(freq, 0, check), closeTo(0.030314, E));
assertThat(compute(freq, 0.5, check), closeTo(0.508808, E)); assertThat(compute(freq, 0.5, check), closeTo(0.515157, E));
assertThat(compute(freq, 0.75, check), closeTo(0.754404, E)); assertThat(compute(freq, 0.75, check), closeTo(0.757578, E));
check = 0; check = 0;
assertThat(compute(freq, 0, check), closeTo(0.0, E)); assertThat(compute(freq, 0, check), closeTo(0.0, E));
assertThat(compute(freq, 0.5, check), closeTo(0.491192, E)); assertThat(compute(freq, 0.5, check), closeTo(0.484842, E));
assertThat(compute(freq, 0.75, check), closeTo(0.736788, E)); assertThat(compute(freq, 0.75, check), closeTo(0.727263, E));
} }

Loading…
Cancel
Save