Skip to content

Commit

Permalink
Migrate Scores to pure Kotlin; display correct score on main screen
Browse files Browse the repository at this point in the history
  • Loading branch information
iSoron committed Apr 6, 2019
1 parent 6d527a3 commit c16a0ec
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class Backend(databaseName: String,

val checkmarks = mutableMapOf<Habit, CheckmarkList>()

val scores = mutableMapOf<Habit, ScoreList>()

val mainScreenDataSource: MainScreenDataSource

val strings = localeHelper.getStringsForCurrentLocale()
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
class MainScreenDataSource(val preferences: Preferences,
val habits: MutableMap<Int, Habit>,
val checkmarks: MutableMap<Habit, CheckmarkList>,
val scores: MutableMap<Habit, ScoreList>,
val taskRunner: TaskRunner) {

val maxNumberOfButtons = 60
private val today = LocalDate(2019, 3, 30) /* TODO */

data class Data(val habits: List<Habit>,
val currentScore: Map<Habit, Double>,
val checkmarkValues: Map<Habit, List<Int>>)
val scores: Map<Habit, Score>,
val checkmarks: Map<Habit, List<Checkmark>>)

val observable = Observable<Listener>()

Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,25 @@ 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<Checkmark>()
private val automaticCheckmarks = mutableListOf<Checkmark>()
private val computedCheckmarks = mutableListOf<Checkmark>()

/**
* Replaces the entire list of manual checkmarks by the ones provided. The
* list of automatic checkmarks will be automatically updated.
*/
fun setManualCheckmarks(checks: List<Checkmark>) {
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)
}
}

Expand All @@ -54,32 +54,33 @@ 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<Int> {
if (automaticCheckmarks.isEmpty()) return listOf()
fun getUntil(date: LocalDate): List<Checkmark> {
if (computedCheckmarks.isEmpty()) return listOf()

val result = mutableListOf<Int>()
val newest = automaticCheckmarks.first().date
val result = mutableListOf<Checkmark>()
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
}

companion object {
/**
* Computes the list of automatic checkmarks a list of manual ones.
*/
fun computeAutomaticCheckmarks(checks: List<Checkmark>,
frequency: Frequency
): MutableList<Checkmark> {
fun computeCheckmarks(checks: List<Checkmark>,
frequency: Frequency
): MutableList<Checkmark> {

val intervals = buildIntervals(checks, frequency)
snapIntervalsTogether(intervals)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 37 additions & 5 deletions core/src/commonMain/kotlin/org/isoron/uhabits/models/ScoreList.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Double> {
TODO()
fun getUntil(date: LocalDate): List<Score> {
val frequency = checkmarkList.frequency
val checks = checkmarkList.getUntil(date)
val scores = mutableListOf<Score>()
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand All @@ -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<Int>()
assertEquals(expected, list.getValuesUntil(day(0)))
val expected = listOf<Checkmark>()
assertEquals(expected, list.getUntil(day(0)))
}
}
94 changes: 94 additions & 0 deletions core/src/jvmTest/kotlin/org/isoron/uhabits/models/ScoreListTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/

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)
}
}
Loading

0 comments on commit c16a0ec

Please sign in to comment.