diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java index 3cbea7a0d..a8c706a2c 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java @@ -48,6 +48,8 @@ public class HabitFixtures static Habit createEmptyHabit() { Habit habit = new Habit(); + habit.freqNum = 1; + habit.freqDen = 1; habit.save(); return habit; } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java new file mode 100644 index 000000000..f9b85b083 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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); + } + }); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java new file mode 100644 index 000000000..d666ff1df --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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)); + } +} diff --git a/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java b/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java new file mode 100644 index 000000000..8e89bb7e8 --- /dev/null +++ b/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java @@ -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(); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java index aaca085d6..e816227cc 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -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); } } diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java index ba6c82d41..4be27ec21 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java @@ -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) diff --git a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java index c1a2c9599..d5690e0a1 100644 --- a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java @@ -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 diff --git a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java index 8a0d93661..2890c872d 100644 --- a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java +++ b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java @@ -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; diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 362fea02a..d52a63fa0 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -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); } /** diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index c7b91351a..475f6e1fa 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -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); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Score.java b/app/src/main/java/org/isoron/uhabits/models/Score.java index 2c3cd1b9d..5eba480e9 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Score.java +++ b/app/src/main/java/org/isoron/uhabits/models/Score.java @@ -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; + + /** + * Minimum score value required to earn a full star. + */ public static final int FULL_STAR_CUTOFF = 15407600; - public static final int MAX_SCORE = 19259500; + /** + * 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; + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java index c703e393a..f432151fb 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -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(); + return new Select() + .from(Score.class) + .where("habit = ?", habit.getId()) + .orderBy("timestamp desc"); + } - if(score >= Score.FULL_STAR_CUTOFF) return 2; - else if(score >= Score.HALF_STAR_CUTOFF) return 1; - else return 0; + /** + * 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(); } - public Score getNewest() + /** + * 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() { - return new Select().from(Score.class) - .where("habit = ?", habit.getId()) - .orderBy("timestamp desc") - .limit(1) - .executeSingle(); + Score newest = findNewest(); + if(newest == null) return 0; + else return newest.score; } - public void deleteNewerThan(long timestamp) + /** + * 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; - - long today = DateHelper.getStartOfDay(DateHelper.getLocalTime()); - long day = DateHelper.millisecondsInOneDay; - - double freq = ((double) habit.freqNum) / habit.freqDen; - double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1)); - - Score newestScore = getNewest(); - if (newestScore == null) - { - Repetition oldestRep = habit.repetitions.getOldest(); - if (oldestRep == null) return 0; - beginningTime = oldestRep.timestamp; - beginningScore = 0; - } - else - { - beginningTime = newestScore.timestamp + day; - beginningScore = newestScore.score; - } + final long day = DateHelper.millisecondsInOneDay; + final double freq = ((double) habit.freqNum) / habit.freqDen; - long nDays = (today - beginningTime) / day; - if (nDays < 0) return newestScore.score; + int newestScoreValue = findNewestValue(); + Score newestScore = findNewest(); - int reps[] = habit.checkmarks.getValues(beginningTime, today); + if(newestScore != null) + from = newestScore.timestamp + day; - ActiveAndroid.beginTransaction(); - int lastScore = beginningScore; + final int checkmarkValues[] = habit.checkmarks.getValues(from, to); + final int firstScore = newestScoreValue; + final long beginning = from; - try + ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() { - 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); - } - s.save(); + int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1]; - lastScore = s.score; + Score s = new Score(); + s.habit = habit; + s.timestamp = beginning + day * i; + s.score = lastScore = Score.compute(freq, lastScore, checkmarkValue); + s.save(); + } } + }); + } - ActiveAndroid.setTransactionSuccessful(); - } finally - { - ActiveAndroid.endTransaction(); - } + /** + * 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) + { + Repetition oldestRep = habit.repetitions.getOldest(); + if(oldestRep == null) return null; - return lastScore; + compute(oldestRep.timestamp, timestamp); + + return select().where("timestamp = ?", timestamp).executeSingle(); } - public int[] getAllValues(Long fromTimestamp, Long toTimestamp, Long divisor) + /** + * 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) { - // Force rebuild of the score table - getNewestValue(); + Score s = get(timestamp); + if(s == null) return 0; + else return s.score; + } - Long offset = toTimestamp - (divisor - 1) * DateHelper.millisecondsInOneDay; + /** + * 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; + } + /** + * Returns the score for today. + * + * @return score for today + */ + @Nullable + protected Score getToday() + { + return get(DateHelper.getStartOfToday()); } - public int[] getAllValues(long divisor) + /** + * Returns the value of the score for today. + * + * @return value of today's score + */ + public int getTodayValue() { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return new int[0]; + return getValue(DateHelper.getStartOfToday()); + } - long fromTimestamp = oldestRep.timestamp; - long toTimestamp = DateHelper.getStartOfToday(); - return getAllValues(fromTimestamp, toTimestamp, divisor); + /** + * 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; } } diff --git a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java index ba310c70f..6de4cffeb 100644 --- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java +++ b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java @@ -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(); diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java index c64a6101b..355c6ba08 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java @@ -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);