From 393880a04715ddb208da4236371b43f94885b558 Mon Sep 17 00:00:00 2001 From: Maxet1000 Date: Sat, 27 May 2023 19:51:38 +0200 Subject: [PATCH 1/4] Fixed score when using skips on non-daily habit --- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- uhabits-android/build.gradle.kts | 2 +- .../isoron/uhabits/core/models/ScoreList.kt | 33 ++++++- .../uhabits/core/models/ScoreListTest.kt | 89 +++++++++++++++++++ 5 files changed, 123 insertions(+), 5 deletions(-) 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 From 6ba27f26d8564f6c1815c910db6eba78e36afdb0 Mon Sep 17 00:00:00 2001 From: Maxet1000 Date: Sun, 3 Dec 2023 15:49:59 +0100 Subject: [PATCH 2/4] Made the edits requested by Hiqua --- .../isoron/uhabits/core/models/ScoreList.kt | 23 ++++++++++--------- .../uhabits/core/models/ScoreListTest.kt | 11 ++++++--- 2 files changed, 20 insertions(+), 14 deletions(-) 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 eb51f8e8c..65b85ea6b 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 @@ -65,25 +65,25 @@ class ScoreList { * 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. + * If skips are found in the interval, it expands the interval by the number of skips found + * and repeats this process for the expanded part until no skips are found in an expanded part. */ @Synchronized fun getNumberOfSkipsByInterval( values: IntArray, - firstIndexToCheck: Int, - lastIndexToCheck: Int + firstIndexCurrentInterval: Int, + lastIndexCurrentInterval: Int ): Int { - if (lastIndexToCheck < firstIndexToCheck) return 0 + if (lastIndexCurrentInterval < firstIndexCurrentInterval) return 0 var nbOfSkips = 0 - var nextLastIndex = lastIndexToCheck - for (i in firstIndexToCheck..lastIndexToCheck) { + var nextLastIndex = lastIndexCurrentInterval + for (i in firstIndexCurrentInterval..lastIndexCurrentInterval) { if (values[i] == Entry.SKIP) { nbOfSkips++ - if (lastIndexToCheck + nbOfSkips < values.size) nextLastIndex++ + if (lastIndexCurrentInterval + nbOfSkips < values.size) nextLastIndex++ } } - return nbOfSkips + getNumberOfSkipsByInterval(values, lastIndexToCheck + 1, nextLastIndex) + return nbOfSkips + getNumberOfSkipsByInterval(values, lastIndexCurrentInterval + 1, nextLastIndex) } /** @@ -152,8 +152,9 @@ class ScoreList { if (offset + denominator < values.size) { val nbOfSkips = getNumberOfSkipsByInterval(values, offset, offset + denominator) - if (offset + denominator + nbOfSkips < values.size) { - if (values[offset + denominator + nbOfSkips] == Entry.YES_MANUAL) { + val lastIndexForRollingSum = offset + denominator + nbOfSkips + if (lastIndexForRollingSum < values.size) { + if (values[lastIndexForRollingSum] == Entry.YES_MANUAL) { if (values[offset] != Entry.SKIP) rollingSum -= 1.0 } } 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 6ff2bb60e..9e128f81d 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 @@ -24,6 +24,8 @@ import org.hamcrest.number.IsCloseTo import org.hamcrest.number.OrderingComparison import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.Entry.Companion.SKIP +import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN +import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.junit.Before import org.junit.Test @@ -149,21 +151,24 @@ class YesNoScoreListTest : BaseScoreListTest() { @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 vars = intArrayOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN, SKIP, YES_MANUAL, YES_MANUAL, + UNKNOWN, YES_MANUAL, YES_MANUAL, UNKNOWN, YES_MANUAL, YES_MANUAL, UNKNOWN, SKIP, YES_MANUAL) 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 vars = intArrayOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN, SKIP, YES_MANUAL, YES_MANUAL, + UNKNOWN, SKIP, SKIP, UNKNOWN, YES_MANUAL, YES_MANUAL, UNKNOWN, SKIP, YES_MANUAL) 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 vars = intArrayOf(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN, SKIP, YES_MANUAL, YES_MANUAL, + UNKNOWN, SKIP, SKIP, UNKNOWN, YES_MANUAL, YES_MANUAL, UNKNOWN, SKIP, YES_MANUAL) val nbOfSkips = habit.scores.getNumberOfSkipsByInterval(vars, 4, 11) assertEquals(4, nbOfSkips) } From e7c3de84deeef1ead3a9345e44512dc2858b703f Mon Sep 17 00:00:00 2001 From: Maxet1000 Date: Sun, 14 Jul 2024 15:11:41 +0200 Subject: [PATCH 3/4] made tailrec --- .../java/org/isoron/uhabits/core/models/ScoreList.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 65b85ea6b..c2e1e9d11 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 @@ -69,12 +69,13 @@ class ScoreList { * and repeats this process for the expanded part until no skips are found in an expanded part. */ @Synchronized - fun getNumberOfSkipsByInterval( + tailrec fun getNumberOfSkipsByInterval( values: IntArray, firstIndexCurrentInterval: Int, - lastIndexCurrentInterval: Int + lastIndexCurrentInterval: Int, + numberOfSkipsIntermediate: Int = 0 ): Int { - if (lastIndexCurrentInterval < firstIndexCurrentInterval) return 0 + if (lastIndexCurrentInterval < firstIndexCurrentInterval) return numberOfSkipsIntermediate var nbOfSkips = 0 var nextLastIndex = lastIndexCurrentInterval for (i in firstIndexCurrentInterval..lastIndexCurrentInterval) { @@ -83,7 +84,7 @@ class ScoreList { if (lastIndexCurrentInterval + nbOfSkips < values.size) nextLastIndex++ } } - return nbOfSkips + getNumberOfSkipsByInterval(values, lastIndexCurrentInterval + 1, nextLastIndex) + return getNumberOfSkipsByInterval(values, lastIndexCurrentInterval + 1, nextLastIndex, nbOfSkips) } /** From e51f32d5749b125b2aa733a09d88d177c152d440 Mon Sep 17 00:00:00 2001 From: Maxet1000 Date: Sun, 14 Jul 2024 16:18:37 +0200 Subject: [PATCH 4/4] made tailrec --- .../java/org/isoron/uhabits/core/models/ScoreList.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 c2e1e9d11..41d336fa4 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 @@ -73,9 +73,9 @@ class ScoreList { values: IntArray, firstIndexCurrentInterval: Int, lastIndexCurrentInterval: Int, - numberOfSkipsIntermediate: Int = 0 + nbSkipsIntermedSol: Int = 0 ): Int { - if (lastIndexCurrentInterval < firstIndexCurrentInterval) return numberOfSkipsIntermediate + if (lastIndexCurrentInterval < firstIndexCurrentInterval) return nbSkipsIntermedSol var nbOfSkips = 0 var nextLastIndex = lastIndexCurrentInterval for (i in firstIndexCurrentInterval..lastIndexCurrentInterval) { @@ -84,7 +84,7 @@ class ScoreList { if (lastIndexCurrentInterval + nbOfSkips < values.size) nextLastIndex++ } } - return getNumberOfSkipsByInterval(values, lastIndexCurrentInterval + 1, nextLastIndex, nbOfSkips) + return getNumberOfSkipsByInterval(values, lastIndexCurrentInterval + 1, nextLastIndex, nbSkipsIntermedSol + nbOfSkips) } /**