diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt index c1ca28607..a81fb37f3 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/Backend.kt @@ -43,6 +43,8 @@ class Backend(databaseName: String, val checkmarks = mutableMapOf() + val scores = mutableMapOf() + val mainScreenDataSource: MainScreenDataSource val strings = localeHelper.getStringsForCurrentLocale() @@ -68,11 +70,13 @@ class Backend(databaseName: String, val checks = checkmarkRepository.findAll(key) checkmarks[habit] = CheckmarkList(habit.frequency, habit.type) checkmarks[habit]?.setManualCheckmarks(checks) + scores[habit] = ScoreList(checkmarks[habit]!!) } } mainScreenDataSource = MainScreenDataSource(preferences, habits, checkmarks, + scores, taskRunner) } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt index 07f487932..5243fda0c 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/backend/MainScreenDataSource.kt @@ -27,14 +27,15 @@ import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED class MainScreenDataSource(val preferences: Preferences, val habits: MutableMap, val checkmarks: MutableMap, + val scores: MutableMap, val taskRunner: TaskRunner) { val maxNumberOfButtons = 60 private val today = LocalDate(2019, 3, 30) /* TODO */ data class Data(val habits: List, - val currentScore: Map, - val checkmarkValues: Map>) + val scores: Map, + val checkmarks: Map>) val observable = Observable() @@ -50,26 +51,26 @@ class MainScreenDataSource(val preferences: Preferences, filtered = filtered.filter { !it.isArchived } } - val recentCheckmarks = filtered.associate { habit -> - val allValues = checkmarks.getValue(habit).getValuesUntil(today) + val checkmarks = filtered.associate { habit -> + val allValues = checkmarks.getValue(habit).getUntil(today) if (allValues.size <= maxNumberOfButtons) habit to allValues else habit to allValues.subList(0, maxNumberOfButtons) } if (!preferences.showCompleted) { filtered = filtered.filter { habit -> - (habit.type == HabitType.BOOLEAN_HABIT && recentCheckmarks.getValue(habit)[0] == UNCHECKED) || - (habit.type == HabitType.NUMERICAL_HABIT && recentCheckmarks.getValue(habit)[0] * 1000 < habit.target) + (habit.type == HabitType.BOOLEAN_HABIT && checkmarks.getValue(habit)[0].value == UNCHECKED) || + (habit.type == HabitType.NUMERICAL_HABIT && checkmarks.getValue(habit)[0].value * 1000 < habit.target) } } - val currentScores = filtered.associate { - it to 0.0 /* TODO */ + val scores = filtered.associate { habit -> + habit to scores[habit]!!.getAt(today) } taskRunner.runInForeground { observable.notifyListeners { listener -> - val data = Data(filtered, currentScores, recentCheckmarks) + val data = Data(filtered, scores, checkmarks) listener.onDataChanged(data) } } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt index 87ea7fc51..55eda4ae8 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/CheckmarkList.kt @@ -24,11 +24,11 @@ import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_AUTOMATIC import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_MANUAL import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED -class CheckmarkList(private val frequency: Frequency, - private val habitType: HabitType) { +class CheckmarkList(val frequency: Frequency, + val habitType: HabitType) { private val manualCheckmarks = mutableListOf() - private val automaticCheckmarks = mutableListOf() + private val computedCheckmarks = mutableListOf() /** * Replaces the entire list of manual checkmarks by the ones provided. The @@ -36,13 +36,13 @@ class CheckmarkList(private val frequency: Frequency, */ fun setManualCheckmarks(checks: List) { manualCheckmarks.clear() - automaticCheckmarks.clear() + computedCheckmarks.clear() manualCheckmarks.addAll(checks) if (habitType == HabitType.NUMERICAL_HABIT) { - automaticCheckmarks.addAll(checks) + computedCheckmarks.addAll(checks) } else { - val computed = computeAutomaticCheckmarks(checks, frequency) - automaticCheckmarks.addAll(computed) + val computed = computeCheckmarks(checks, frequency) + computedCheckmarks.addAll(computed) } } @@ -54,22 +54,23 @@ class CheckmarkList(private val frequency: Frequency, * That is, the first element of the returned list corresponds to the date * provided. */ - fun getValuesUntil(date: LocalDate): List { - if (automaticCheckmarks.isEmpty()) return listOf() + fun getUntil(date: LocalDate): List { + if (computedCheckmarks.isEmpty()) return listOf() - val result = mutableListOf() - val newest = automaticCheckmarks.first().date + val result = mutableListOf() + val newest = computedCheckmarks.first().date val distToNewest = newest.distanceTo(date) + var k = 0 var fromIndex = 0 - val toIndex = automaticCheckmarks.size + val toIndex = computedCheckmarks.size if (newest.isOlderThan(date)) { - repeat(distToNewest) { result.add(UNCHECKED) } + repeat(distToNewest) { result.add(Checkmark(date.minus(k++), UNCHECKED)) } } else { fromIndex = distToNewest } - val subList = automaticCheckmarks.subList(fromIndex, toIndex) - result.addAll(subList.map { it.value }) + val subList = computedCheckmarks.subList(fromIndex, toIndex) + result.addAll(subList.map { Checkmark(date.minus(k++), it.value) }) return result } @@ -77,9 +78,9 @@ class CheckmarkList(private val frequency: Frequency, /** * Computes the list of automatic checkmarks a list of manual ones. */ - fun computeAutomaticCheckmarks(checks: List, - frequency: Frequency - ): MutableList { + fun computeCheckmarks(checks: List, + frequency: Frequency + ): MutableList { val intervals = buildIntervals(checks, frequency) snapIntervalsTogether(intervals) diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt index e07ef0e38..f09a32a51 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/Frequency.kt @@ -21,6 +21,11 @@ package org.isoron.uhabits.models data class Frequency(val numerator: Int, val denominator: Int) { + + fun toDouble(): Double { + return numerator.toDouble() / denominator + } + companion object { val WEEKLY = Frequency(1, 7) val DAILY = Frequency(1, 1) diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/ScoreList.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/ScoreList.kt index 91b2b0c3f..46fb993d7 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/models/ScoreList.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/ScoreList.kt @@ -20,10 +20,9 @@ package org.isoron.uhabits.models import org.isoron.platform.time.* +import kotlin.math.* -class ScoreList(private val frequency: Frequency, - private val checkmarkList: CheckmarkList) { - +class ScoreList(private val checkmarkList: CheckmarkList) { /** * Returns a list of all scores, from the beginning of the habit history * until the specified date. @@ -32,7 +31,40 @@ class ScoreList(private val frequency: Frequency, * That is, the first element of the returned list corresponds to the date * provided. */ - fun getValuesUntil(date: LocalDate): List { - TODO() + fun getUntil(date: LocalDate): List { + val frequency = checkmarkList.frequency + val checks = checkmarkList.getUntil(date) + val scores = mutableListOf() + val type = checkmarkList.habitType + + var currentScore = 0.0 + checks.reversed().forEach { check -> + val value = if (type == HabitType.BOOLEAN_HABIT) { + min(1, check.value) + } else { + check.value + } + currentScore = compute(frequency, currentScore, value) + scores.add(Score(check.date, currentScore)) + } + return scores.reversed() + } + + fun getAt(date: LocalDate): Score { + return getUntil(date)[0] + } + + companion object { + /** + * Given the frequency of the habit, the previous score, and the value of + * the current checkmark, computes the current score for the habit. + */ + fun compute(frequency: Frequency, + previousScore: Double, + checkmarkValue: Int): Double { + val multiplier = 0.5.pow(frequency.toDouble() / 13.0) + val score = previousScore * multiplier + checkmarkValue * (1 - multiplier) + return floor(score * 1e6) / 1e6 + } } } \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt index fdb5e19e6..9da1185ed 100644 --- a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/CheckmarkListTest.kt @@ -114,8 +114,7 @@ class CheckmarkListTest : BaseTest() { Checkmark(day(8), CHECKED_AUTOMATIC), Checkmark(day(9), CHECKED_AUTOMATIC), Checkmark(day(10), CHECKED_MANUAL)) - val actual = CheckmarkList.buildCheckmarksFromIntervals(checks, - intervals) + val actual = CheckmarkList.buildCheckmarksFromIntervals(checks, intervals) assertEquals(expected, actual) } @@ -129,8 +128,7 @@ class CheckmarkListTest : BaseTest() { Checkmark(day(3), CHECKED_AUTOMATIC), Checkmark(day(4), CHECKED_AUTOMATIC), Checkmark(day(5), CHECKED_AUTOMATIC)) - val actual = CheckmarkList.buildCheckmarksFromIntervals(reps, - intervals) + val actual = CheckmarkList.buildCheckmarksFromIntervals(reps, intervals) assertEquals(expected, actual) } @@ -152,38 +150,37 @@ class CheckmarkListTest : BaseTest() { Checkmark(day(8), CHECKED_AUTOMATIC), Checkmark(day(9), CHECKED_AUTOMATIC), Checkmark(day(10), CHECKED_MANUAL)) - val actual = CheckmarkList.computeAutomaticCheckmarks(checks, - Frequency(1, 3)) + val actual = CheckmarkList.computeCheckmarks(checks, Frequency(1, 3)) assertEquals(expected, actual) } @Test - fun testGetValuesUntil() { + fun testGetUntil() { val list = CheckmarkList(Frequency(1, 2), HabitType.BOOLEAN_HABIT) list.setManualCheckmarks(listOf(Checkmark(day(4), CHECKED_MANUAL), Checkmark(day(7), CHECKED_MANUAL))) - val expected = listOf(UNCHECKED, - UNCHECKED, - UNCHECKED, - CHECKED_AUTOMATIC, - CHECKED_MANUAL, - UNCHECKED, - CHECKED_AUTOMATIC, - CHECKED_MANUAL) - assertEquals(expected, list.getValuesUntil(day(0))) + val expected = listOf(Checkmark(day(0), UNCHECKED), + Checkmark(day(1), UNCHECKED), + Checkmark(day(2), UNCHECKED), + Checkmark(day(3), CHECKED_AUTOMATIC), + Checkmark(day(4), CHECKED_MANUAL), + Checkmark(day(5), UNCHECKED), + Checkmark(day(6), CHECKED_AUTOMATIC), + Checkmark(day(7), CHECKED_MANUAL)) + assertEquals(expected, list.getUntil(day(0))) - val expected2 = listOf(CHECKED_AUTOMATIC, - CHECKED_MANUAL, - UNCHECKED, - CHECKED_AUTOMATIC, - CHECKED_MANUAL) - assertEquals(expected2, list.getValuesUntil(day(3))) + val expected2 = listOf(Checkmark(day(3), CHECKED_AUTOMATIC), + Checkmark(day(4), CHECKED_MANUAL), + Checkmark(day(5), UNCHECKED), + Checkmark(day(6), CHECKED_AUTOMATIC), + Checkmark(day(7), CHECKED_MANUAL)) + assertEquals(expected2, list.getUntil(day(3))) } @Test fun testGetValuesUntil2() { val list = CheckmarkList(Frequency(1, 2), HabitType.BOOLEAN_HABIT) - val expected = listOf() - assertEquals(expected, list.getValuesUntil(day(0))) + val expected = listOf() + assertEquals(expected, list.getUntil(day(0))) } } \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/models/ScoreListTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/ScoreListTest.kt new file mode 100644 index 000000000..a971d405c --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/models/ScoreListTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2016-2019 Á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.models + +import org.isoron.platform.time.* +import org.isoron.uhabits.models.Checkmark.Companion.CHECKED_MANUAL +import org.isoron.uhabits.models.Frequency.Companion.DAILY +import org.isoron.uhabits.models.HabitType.* +import org.isoron.uhabits.models.ScoreList.Companion.compute +import org.junit.Assert.* +import org.junit.Test +import java.lang.Math.* +import kotlin.test.* + +class ScoreListTest { + val epsilon = 1e-6 + val today = LocalDate(2019, 1, 1) + + @Test + fun `compute with daily habit`() { + val freq = DAILY + var check = 1 + assertEquals(compute(freq, 0.0, check), 0.051922, epsilon) + assertEquals(compute(freq, 0.5, check), 0.525961, epsilon) + assertEquals(compute(freq, 0.75, check), 0.762980, epsilon) + + check = 0 + assertEquals(compute(freq, 0.0, check), 0.0, epsilon) + assertEquals(compute(freq, 0.5, check), 0.474039, epsilon) + assertEquals(compute(freq, 0.75, check), 0.711058, epsilon) + } + + @Test + fun `compute with non-daily habit`() { + var check = 1 + val freq = Frequency(1, 3) + assertEquals(compute(freq, 0.0, check), 0.017615, epsilon) + assertEquals(compute(freq, 0.5, check), 0.508807, epsilon) + assertEquals(compute(freq, 0.75, check), 0.754404, epsilon) + + check = 0 + assertEquals(compute(freq, 0.0, check), 0.0, epsilon) + assertEquals(compute(freq, 0.5, check), 0.491192, epsilon) + assertEquals(compute(freq, 0.75, check), 0.736788, epsilon) + } + + @Test + fun `getValueUntil with boolean habit`() { + val checks = CheckmarkList(DAILY, BOOLEAN_HABIT) + checks.setManualCheckmarks((0..19).map { + Checkmark(today.minus(it), CHECKED_MANUAL) + }) + val scoreList = ScoreList(checks) + val actual = scoreList.getUntil(today) + val expected = listOf(Score(today.minus(0), 0.655741), + Score(today.minus(1), 0.636888), + Score(today.minus(2), 0.617002), + Score(today.minus(3), 0.596027), + Score(today.minus(4), 0.573903), + Score(today.minus(5), 0.550568), + Score(today.minus(6), 0.525955), + Score(today.minus(7), 0.499994), + Score(today.minus(8), 0.472611), + Score(today.minus(9), 0.443729), + Score(today.minus(10), 0.413265), + Score(today.minus(11), 0.381132), + Score(today.minus(12), 0.347240), + Score(today.minus(13), 0.311491), + Score(today.minus(14), 0.273785), + Score(today.minus(15), 0.234014), + Score(today.minus(16), 0.192065), + Score(today.minus(17), 0.147818), + Score(today.minus(18), 0.101148), + Score(today.minus(19), 0.051922)) + assertEquals(expected, actual) + } +} \ No newline at end of file diff --git a/ios/Application/Frontend/MainScreenController.swift b/ios/Application/Frontend/MainScreenController.swift index 45157a8d3..dddfa9c2c 100644 --- a/ios/Application/Frontend/MainScreenController.swift +++ b/ios/Application/Frontend/MainScreenController.swift @@ -31,7 +31,7 @@ class MainScreenCell : UITableViewCell { fatalError() } - func update(habit: Habit, values: [KotlinInt], theme: Theme, nButtons: Int) { + func update(habit: Habit, checkmarks: [Checkmark], score: Score, theme: Theme, nButtons: Int) { if buttons.count != nButtons { buttons.removeAll() for v in contentView.subviews { v.removeFromSuperview() } @@ -68,7 +68,7 @@ class MainScreenCell : UITableViewCell { label.text = habit.name label.textColor = color.uicolor ring.component = Ring(color: color, - percentage: Double.random(in: 0...1), + percentage: score.value, thickness: 2.5, radius: 7, theme: theme, @@ -78,12 +78,12 @@ class MainScreenCell : UITableViewCell { for i in 0.. UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MainScreenCell let habit = data!.habits[indexPath.row] - cell.update(habit: habit, values: data!.checkmarkValues[habit]!, theme: theme, nButtons: nButtons) + cell.update(habit: habit, + checkmarks: data!.checkmarks[habit]!, + score: data!.scores[habit]!, + theme: theme, + nButtons: nButtons) return cell }