diff --git a/build.gradle.kts b/build.gradle.kts index c5279d865..750093865 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { val kotlinVersion = "1.9.22" - id("com.android.application") version "8.4.0" apply (false) + id("com.android.application") version "8.5.1" apply (false) id("org.jetbrains.kotlin.android") version kotlinVersion apply (false) id("org.jetbrains.kotlin.kapt") version kotlinVersion apply (false) id("org.jetbrains.kotlin.multiplatform") version kotlinVersion apply (false) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17655d0ef..48c0a02ca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/uhabits-android/build.gradle.kts b/uhabits-android/build.gradle.kts index 894c0b4ec..893191dd7 100644 --- a/uhabits-android/build.gradle.kts +++ b/uhabits-android/build.gradle.kts @@ -19,7 +19,7 @@ plugins { id("com.github.triplet.play") version "3.8.6" - id("com.android.application") version "8.4.0" + id("com.android.application") version "8.5.1" id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.kapt") id("org.jlleitschuh.gradle.ktlint") diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt index 3acc19e1c..eb51f8e8c 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt @@ -61,6 +61,31 @@ class ScoreList { return result } + /** + * Returns the number of skips after the offset in the interval used to calculate + * the percentage of completed days. + * + * If skips are found in the interval, it is expanded until the interval has the size of the + * sum of the denominator and the number of skips within the interval. + */ + @Synchronized + fun getNumberOfSkipsByInterval( + values: IntArray, + firstIndexToCheck: Int, + lastIndexToCheck: Int + ): Int { + if (lastIndexToCheck < firstIndexToCheck) return 0 + var nbOfSkips = 0 + var nextLastIndex = lastIndexToCheck + for (i in firstIndexToCheck..lastIndexToCheck) { + if (values[i] == Entry.SKIP) { + nbOfSkips++ + if (lastIndexToCheck + nbOfSkips < values.size) nextLastIndex++ + } + } + return nbOfSkips + getNumberOfSkipsByInterval(values, lastIndexToCheck + 1, nextLastIndex) + } + /** * Recomputes all scores between the provided [from] and [to] timestamps. */ @@ -125,8 +150,12 @@ class ScoreList { rollingSum += 1.0 } if (offset + denominator < values.size) { - if (values[offset + denominator] == Entry.YES_MANUAL) { - rollingSum -= 1.0 + val nbOfSkips = + getNumberOfSkipsByInterval(values, offset, offset + denominator) + if (offset + denominator + nbOfSkips < values.size) { + if (values[offset + denominator + nbOfSkips] == Entry.YES_MANUAL) { + if (values[offset] != Entry.SKIP) rollingSum -= 1.0 + } } } if (values[offset] != Entry.SKIP) { diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt index 9b653cd4a..6ff2bb60e 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.models +import junit.framework.Assert.assertTrue import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.number.IsCloseTo import org.hamcrest.number.OrderingComparison @@ -27,6 +28,7 @@ import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.junit.Before import org.junit.Test import java.util.ArrayList +import kotlin.test.assertEquals import kotlin.test.assertTrue open class BaseScoreListTest : BaseUnitTest() { @@ -145,6 +147,93 @@ class YesNoScoreListTest : BaseScoreListTest() { checkScoreValues(expectedValues) } + @Test + fun test_getNumberOfSkipsByInterval_NoSkips() { + val vars = intArrayOf(-1, -1, -1, -1, 3, 2, 2, -1, 2, 2, -1, 2, 2, -1, 3, 2) + val nbOfSkips = habit.scores.getNumberOfSkipsByInterval(vars, 5, 13) + assertEquals(0, nbOfSkips) + } + + @Test + fun test_getNumberOfSkipsByInterval_SkipsOnlyInInitialInterval() { + val vars = intArrayOf(-1, -1, -1, -1, 3, 2, 2, -1, 3, 3, -1, 2, 2, -1, 3, 2) + val nbOfSkips = habit.scores.getNumberOfSkipsByInterval(vars, 4, 9) + assertEquals(3, nbOfSkips) + } + + @Test + fun test_getNumberOfSkipsByInterval_SkipsInSubsequentIntervals() { + val vars = intArrayOf(-1, -1, -1, -1, 3, 2, 2, -1, 3, 3, -1, 2, 2, -1, 3, 2) + val nbOfSkips = habit.scores.getNumberOfSkipsByInterval(vars, 4, 11) + assertEquals(4, nbOfSkips) + } + + @Test + fun test_getValueNonDailyWithSkip() { + habit.frequency = Frequency(6, 7) + check(0, 18) + addSkip(10) + addSkip(11) + addSkip(12) + habit.recompute() + val expectedValues = doubleArrayOf( + 0.365222, + 0.333100, + 0.299354, + 0.263899, + 0.226651, + 0.191734, + 0.159268, + 0.129375, + 0.102187, + 0.077839, + 0.056477, + 0.056477, + 0.056477, + 0.056477, + 0.038251, + 0.023319, + 0.011848, + 0.004014, + 0.000000, + 0.000000, + 0.000000 + ) + checkScoreValues(expectedValues) + } + + @Test + fun test_perfectDailyWithSkips() { + // If the habit is performed daily and the user always either completes or + // skips the habit, score should converge to 100%. + habit.frequency = Frequency(1, 1) + val values = ArrayList() + check(0, 500) + for (k in 0..99) { + addSkip(7 * k + 5) + addSkip(7 * k + 6) + } + habit.recompute() + check(values) + assertThat(habit.scores[today].value, IsCloseTo.closeTo(1.0, E)) + } + + @Test + fun test_perfectNonDailyWithSkips() { + // If the habit is performed six times per week and the user always either completes or + // skips the habit, score should converge to 100%. + habit.frequency = Frequency(6, 7) + val values = ArrayList() + check(0, 500) + for (k in 0..99) { + addSkip(7 * k + 5) + addSkip(7 * k + 6) + } + habit.recompute() + check(values) + assertThat(habit.scores[today].value, IsCloseTo.closeTo(1.0, E)) + } + @Test fun test_imperfectNonDaily() { // If the habit should be performed 3 times per week and the user misses 1 repetition