diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt index 81ea9674a..c7a338954 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Habit.kt @@ -83,12 +83,12 @@ data class Habit( if (from.isNewerThan(to)) from = to scores.recompute( - this.frequency, - this.isNumerical, - this.targetValue, - this.computedEntries, - from, - to + frequency = frequency, + isNumerical = isNumerical, + targetValue = targetValue, + computedEntries = computedEntries, + from = from, + to = to, ) } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.kt index e1bc9e9ea..6f05074b3 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/Score.kt @@ -25,12 +25,6 @@ data class Score( val value: Double, ) { - fun compareNewer(other: Score): Int { - return this.timestamp.compareTo(other.timestamp) - } - - fun compareOlder(other: Score) = -compareNewer(other) - companion object { /** * Given the frequency of the habit, the previous score, and the value of @@ -39,11 +33,6 @@ data 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. - * - * @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 */ @JvmStatic fun compute( diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ScoreList.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ScoreList.java deleted file mode 100644 index b2ecb84ca..000000000 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ScoreList.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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.core.models; - -import androidx.annotation.*; - -import java.util.*; - -import static org.isoron.uhabits.core.models.Entry.*; - -public class ScoreList -{ - private final HashMap list = new HashMap<>(); - - /** - * Returns the score for a given day. If the timestamp given happens before the first - * repetition of the habit or after the last computed score, returns a score with value zero. - */ - public final synchronized Score get(Timestamp timestamp) - { - if (list.containsKey(timestamp)) return list.get(timestamp); - return new Score(timestamp, 0); - } - - /** - * Returns the list of scores that fall within the given interval. - *

- * There is exactly one score per day in the interval. The endpoints of - * the interval are included. The list is ordered by timestamp (decreasing). - * That is, the first score corresponds to the newest timestamp, and the - * last score corresponds to the oldest timestamp. - * - * @param fromTimestamp timestamp of the beginning of the interval. - * @param toTimestamp timestamp of the end of the interval. - * @return the list of scores within the interval. - */ - @NonNull - public List getByInterval(@NonNull Timestamp fromTimestamp, - @NonNull Timestamp toTimestamp) - { - List result = new LinkedList<>(); - if (fromTimestamp.isNewerThan(toTimestamp)) return result; - Timestamp current = toTimestamp; - while(!current.isOlderThan(fromTimestamp)) - { - result.add(get(current)); - current = current.minus(1); - } - return result; - } - - public void recompute( - Frequency frequency, - boolean isNumerical, - double targetValue, - EntryList computedEntries, - Timestamp from, - Timestamp to) - { - list.clear(); - if (computedEntries.getKnown().isEmpty()) return; - if (from.isNewerThan(to)) return; - - double rollingSum = 0.0; - int numerator = frequency.getNumerator(); - int denominator = frequency.getDenominator(); - final double freq = frequency.toDouble(); - final Integer[] values = computedEntries - .getByInterval(from, to) - .stream() - .map(Entry::getValue) - .toArray(Integer[]::new); - - // 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 (!isNumerical && freq < 1.0) - { - numerator *= 2; - denominator *= 2; - } - - double previousValue = 0; - for (int i = 0; i < values.length; i++) - { - int offset = values.length - i - 1; - if (isNumerical) - { - rollingSum += values[offset]; - if (offset + denominator < values.length) - { - rollingSum -= values[offset + denominator]; - } - double percentageCompleted = Math.min(1, rollingSum / 1000 / targetValue); - previousValue = Score.compute(freq, previousValue, percentageCompleted); - } - else - { - if (values[offset] == YES_MANUAL) - rollingSum += 1.0; - if (offset + denominator < values.length) - if (values[offset + denominator] == YES_MANUAL) - rollingSum -= 1.0; - if (values[offset] != SKIP) - { - double percentageCompleted = Math.min(1, rollingSum / numerator); - previousValue = Score.compute(freq, previousValue, percentageCompleted); - } - } - Timestamp timestamp = from.plus(i); - list.put(timestamp, new Score(timestamp, previousValue)); - } - } -} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ScoreList.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ScoreList.kt new file mode 100644 index 000000000..4ca19bd07 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/models/ScoreList.kt @@ -0,0 +1,114 @@ +/* + * 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.core.models + +import org.isoron.uhabits.core.models.Score.Companion.compute +import java.util.* +import kotlin.math.* + +class ScoreList { + + private val map = HashMap() + + /** + * Returns the score for a given day. If the timestamp given happens before the first + * repetition of the habit or after the last computed score, returns a score with value zero. + */ + operator fun get(timestamp: Timestamp): Score { + return map[timestamp] ?: Score(timestamp, 0.0) + } + + /** + * Returns the list of scores that fall within the given interval. + * + * There is exactly one score per day in the interval. The endpoints of the interval are + * included. The list is ordered by timestamp (decreasing). That is, the first score + * corresponds to the newest timestamp, and the last score corresponds to the oldest timestamp. + */ + fun getByInterval( + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + ): List { + val result: MutableList = ArrayList() + if (fromTimestamp.isNewerThan(toTimestamp)) return result + var current = toTimestamp + while (!current.isOlderThan(fromTimestamp)) { + result.add(get(current)) + current = current.minus(1) + } + return result + } + + /** + * Recomputes all scores between the provided [from] and [to] timestamps. + */ + fun recompute( + frequency: Frequency, + isNumerical: Boolean, + targetValue: Double, + computedEntries: EntryList, + from: Timestamp, + to: Timestamp, + ) { + map.clear() + if (computedEntries.getKnown().isEmpty()) return + if (from.isNewerThan(to)) return + var rollingSum = 0.0 + var numerator = frequency.numerator + var denominator = frequency.denominator + val freq = frequency.toDouble() + val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray() + + // 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 (!isNumerical && freq < 1.0) { + numerator *= 2 + denominator *= 2 + } + + var previousValue = 0.0 + for (i in values.indices) { + val offset = values.size - i - 1 + if (isNumerical) { + rollingSum += values[offset] + if (offset + denominator < values.size) { + rollingSum -= values[offset + denominator] + } + val percentageCompleted = min(1.0, rollingSum / 1000 / targetValue) + previousValue = compute(freq, previousValue, percentageCompleted) + } else { + if (values[offset] == Entry.YES_MANUAL) { + rollingSum += 1.0 + } + if (offset + denominator < values.size) { + if (values[offset + denominator] == Entry.YES_MANUAL) { + rollingSum -= 1.0 + } + } + if (values[offset] != Entry.SKIP) { + val percentageCompleted = Math.min(1.0, rollingSum / numerator) + previousValue = compute(freq, previousValue, percentageCompleted) + } + } + val timestamp = from.plus(i) + map[timestamp] = Score(timestamp, previousValue) + } + } +} \ No newline at end of file