Migrate Scores to pure Kotlin; display correct score on main screen

pull/498/head
Alinson S. Xavier 7 years ago
parent 6d527a31d7
commit c16a0ecd65

@ -43,6 +43,8 @@ class Backend(databaseName: String,
val checkmarks = mutableMapOf<Habit, CheckmarkList>() val checkmarks = mutableMapOf<Habit, CheckmarkList>()
val scores = mutableMapOf<Habit, ScoreList>()
val mainScreenDataSource: MainScreenDataSource val mainScreenDataSource: MainScreenDataSource
val strings = localeHelper.getStringsForCurrentLocale() val strings = localeHelper.getStringsForCurrentLocale()
@ -68,11 +70,13 @@ class Backend(databaseName: String,
val checks = checkmarkRepository.findAll(key) val checks = checkmarkRepository.findAll(key)
checkmarks[habit] = CheckmarkList(habit.frequency, habit.type) checkmarks[habit] = CheckmarkList(habit.frequency, habit.type)
checkmarks[habit]?.setManualCheckmarks(checks) checkmarks[habit]?.setManualCheckmarks(checks)
scores[habit] = ScoreList(checkmarks[habit]!!)
} }
} }
mainScreenDataSource = MainScreenDataSource(preferences, mainScreenDataSource = MainScreenDataSource(preferences,
habits, habits,
checkmarks, checkmarks,
scores,
taskRunner) taskRunner)
} }

@ -27,14 +27,15 @@ import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
class MainScreenDataSource(val preferences: Preferences, class MainScreenDataSource(val preferences: Preferences,
val habits: MutableMap<Int, Habit>, val habits: MutableMap<Int, Habit>,
val checkmarks: MutableMap<Habit, CheckmarkList>, val checkmarks: MutableMap<Habit, CheckmarkList>,
val scores: MutableMap<Habit, ScoreList>,
val taskRunner: TaskRunner) { val taskRunner: TaskRunner) {
val maxNumberOfButtons = 60 val maxNumberOfButtons = 60
private val today = LocalDate(2019, 3, 30) /* TODO */ private val today = LocalDate(2019, 3, 30) /* TODO */
data class Data(val habits: List<Habit>, data class Data(val habits: List<Habit>,
val currentScore: Map<Habit, Double>, val scores: Map<Habit, Score>,
val checkmarkValues: Map<Habit, List<Int>>) val checkmarks: Map<Habit, List<Checkmark>>)
val observable = Observable<Listener>() val observable = Observable<Listener>()
@ -50,26 +51,26 @@ class MainScreenDataSource(val preferences: Preferences,
filtered = filtered.filter { !it.isArchived } filtered = filtered.filter { !it.isArchived }
} }
val recentCheckmarks = filtered.associate { habit -> val checkmarks = filtered.associate { habit ->
val allValues = checkmarks.getValue(habit).getValuesUntil(today) val allValues = checkmarks.getValue(habit).getUntil(today)
if (allValues.size <= maxNumberOfButtons) habit to allValues if (allValues.size <= maxNumberOfButtons) habit to allValues
else habit to allValues.subList(0, maxNumberOfButtons) else habit to allValues.subList(0, maxNumberOfButtons)
} }
if (!preferences.showCompleted) { if (!preferences.showCompleted) {
filtered = filtered.filter { habit -> filtered = filtered.filter { habit ->
(habit.type == HabitType.BOOLEAN_HABIT && recentCheckmarks.getValue(habit)[0] == UNCHECKED) || (habit.type == HabitType.BOOLEAN_HABIT && checkmarks.getValue(habit)[0].value == UNCHECKED) ||
(habit.type == HabitType.NUMERICAL_HABIT && recentCheckmarks.getValue(habit)[0] * 1000 < habit.target) (habit.type == HabitType.NUMERICAL_HABIT && checkmarks.getValue(habit)[0].value * 1000 < habit.target)
} }
} }
val currentScores = filtered.associate { val scores = filtered.associate { habit ->
it to 0.0 /* TODO */ habit to scores[habit]!!.getAt(today)
} }
taskRunner.runInForeground { taskRunner.runInForeground {
observable.notifyListeners { listener -> observable.notifyListeners { listener ->
val data = Data(filtered, currentScores, recentCheckmarks) val data = Data(filtered, scores, checkmarks)
listener.onDataChanged(data) listener.onDataChanged(data)
} }
} }

@ -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.CHECKED_MANUAL
import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED import org.isoron.uhabits.models.Checkmark.Companion.UNCHECKED
class CheckmarkList(private val frequency: Frequency, class CheckmarkList(val frequency: Frequency,
private val habitType: HabitType) { val habitType: HabitType) {
private val manualCheckmarks = mutableListOf<Checkmark>() 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 * 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<Checkmark>) { fun setManualCheckmarks(checks: List<Checkmark>) {
manualCheckmarks.clear() manualCheckmarks.clear()
automaticCheckmarks.clear() computedCheckmarks.clear()
manualCheckmarks.addAll(checks) manualCheckmarks.addAll(checks)
if (habitType == HabitType.NUMERICAL_HABIT) { if (habitType == HabitType.NUMERICAL_HABIT) {
automaticCheckmarks.addAll(checks) computedCheckmarks.addAll(checks)
} else { } else {
val computed = computeAutomaticCheckmarks(checks, frequency) val computed = computeCheckmarks(checks, frequency)
automaticCheckmarks.addAll(computed) 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 * That is, the first element of the returned list corresponds to the date
* provided. * provided.
*/ */
fun getValuesUntil(date: LocalDate): List<Int> { fun getUntil(date: LocalDate): List<Checkmark> {
if (automaticCheckmarks.isEmpty()) return listOf() if (computedCheckmarks.isEmpty()) return listOf()
val result = mutableListOf<Int>() val result = mutableListOf<Checkmark>()
val newest = automaticCheckmarks.first().date val newest = computedCheckmarks.first().date
val distToNewest = newest.distanceTo(date) val distToNewest = newest.distanceTo(date)
var k = 0
var fromIndex = 0 var fromIndex = 0
val toIndex = automaticCheckmarks.size val toIndex = computedCheckmarks.size
if (newest.isOlderThan(date)) { if (newest.isOlderThan(date)) {
repeat(distToNewest) { result.add(UNCHECKED) } repeat(distToNewest) { result.add(Checkmark(date.minus(k++), UNCHECKED)) }
} else { } else {
fromIndex = distToNewest fromIndex = distToNewest
} }
val subList = automaticCheckmarks.subList(fromIndex, toIndex) val subList = computedCheckmarks.subList(fromIndex, toIndex)
result.addAll(subList.map { it.value }) result.addAll(subList.map { Checkmark(date.minus(k++), it.value) })
return result return result
} }
@ -77,9 +78,9 @@ class CheckmarkList(private val frequency: Frequency,
/** /**
* Computes the list of automatic checkmarks a list of manual ones. * Computes the list of automatic checkmarks a list of manual ones.
*/ */
fun computeAutomaticCheckmarks(checks: List<Checkmark>, fun computeCheckmarks(checks: List<Checkmark>,
frequency: Frequency frequency: Frequency
): MutableList<Checkmark> { ): MutableList<Checkmark> {
val intervals = buildIntervals(checks, frequency) val intervals = buildIntervals(checks, frequency)
snapIntervalsTogether(intervals) snapIntervalsTogether(intervals)

@ -21,6 +21,11 @@ package org.isoron.uhabits.models
data class Frequency(val numerator: Int, data class Frequency(val numerator: Int,
val denominator: Int) { val denominator: Int) {
fun toDouble(): Double {
return numerator.toDouble() / denominator
}
companion object { companion object {
val WEEKLY = Frequency(1, 7) val WEEKLY = Frequency(1, 7)
val DAILY = Frequency(1, 1) val DAILY = Frequency(1, 1)

@ -20,10 +20,9 @@
package org.isoron.uhabits.models package org.isoron.uhabits.models
import org.isoron.platform.time.* import org.isoron.platform.time.*
import kotlin.math.*
class ScoreList(private val frequency: Frequency, class ScoreList(private val checkmarkList: CheckmarkList) {
private val checkmarkList: CheckmarkList) {
/** /**
* Returns a list of all scores, from the beginning of the habit history * Returns a list of all scores, from the beginning of the habit history
* until the specified date. * 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 * That is, the first element of the returned list corresponds to the date
* provided. * provided.
*/ */
fun getValuesUntil(date: LocalDate): List<Double> { fun getUntil(date: LocalDate): List<Score> {
TODO() 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
}
} }
} }

@ -114,8 +114,7 @@ class CheckmarkListTest : BaseTest() {
Checkmark(day(8), CHECKED_AUTOMATIC), Checkmark(day(8), CHECKED_AUTOMATIC),
Checkmark(day(9), CHECKED_AUTOMATIC), Checkmark(day(9), CHECKED_AUTOMATIC),
Checkmark(day(10), CHECKED_MANUAL)) Checkmark(day(10), CHECKED_MANUAL))
val actual = CheckmarkList.buildCheckmarksFromIntervals(checks, val actual = CheckmarkList.buildCheckmarksFromIntervals(checks, intervals)
intervals)
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@ -129,8 +128,7 @@ class CheckmarkListTest : BaseTest() {
Checkmark(day(3), CHECKED_AUTOMATIC), Checkmark(day(3), CHECKED_AUTOMATIC),
Checkmark(day(4), CHECKED_AUTOMATIC), Checkmark(day(4), CHECKED_AUTOMATIC),
Checkmark(day(5), CHECKED_AUTOMATIC)) Checkmark(day(5), CHECKED_AUTOMATIC))
val actual = CheckmarkList.buildCheckmarksFromIntervals(reps, val actual = CheckmarkList.buildCheckmarksFromIntervals(reps, intervals)
intervals)
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@ -152,38 +150,37 @@ class CheckmarkListTest : BaseTest() {
Checkmark(day(8), CHECKED_AUTOMATIC), Checkmark(day(8), CHECKED_AUTOMATIC),
Checkmark(day(9), CHECKED_AUTOMATIC), Checkmark(day(9), CHECKED_AUTOMATIC),
Checkmark(day(10), CHECKED_MANUAL)) Checkmark(day(10), CHECKED_MANUAL))
val actual = CheckmarkList.computeAutomaticCheckmarks(checks, val actual = CheckmarkList.computeCheckmarks(checks, Frequency(1, 3))
Frequency(1, 3))
assertEquals(expected, actual) assertEquals(expected, actual)
} }
@Test @Test
fun testGetValuesUntil() { fun testGetUntil() {
val list = CheckmarkList(Frequency(1, 2), HabitType.BOOLEAN_HABIT) val list = CheckmarkList(Frequency(1, 2), HabitType.BOOLEAN_HABIT)
list.setManualCheckmarks(listOf(Checkmark(day(4), CHECKED_MANUAL), list.setManualCheckmarks(listOf(Checkmark(day(4), CHECKED_MANUAL),
Checkmark(day(7), CHECKED_MANUAL))) Checkmark(day(7), CHECKED_MANUAL)))
val expected = listOf(UNCHECKED, val expected = listOf(Checkmark(day(0), UNCHECKED),
UNCHECKED, Checkmark(day(1), UNCHECKED),
UNCHECKED, Checkmark(day(2), UNCHECKED),
CHECKED_AUTOMATIC, Checkmark(day(3), CHECKED_AUTOMATIC),
CHECKED_MANUAL, Checkmark(day(4), CHECKED_MANUAL),
UNCHECKED, Checkmark(day(5), UNCHECKED),
CHECKED_AUTOMATIC, Checkmark(day(6), CHECKED_AUTOMATIC),
CHECKED_MANUAL) Checkmark(day(7), CHECKED_MANUAL))
assertEquals(expected, list.getValuesUntil(day(0))) assertEquals(expected, list.getUntil(day(0)))
val expected2 = listOf(CHECKED_AUTOMATIC, val expected2 = listOf(Checkmark(day(3), CHECKED_AUTOMATIC),
CHECKED_MANUAL, Checkmark(day(4), CHECKED_MANUAL),
UNCHECKED, Checkmark(day(5), UNCHECKED),
CHECKED_AUTOMATIC, Checkmark(day(6), CHECKED_AUTOMATIC),
CHECKED_MANUAL) Checkmark(day(7), CHECKED_MANUAL))
assertEquals(expected2, list.getValuesUntil(day(3))) assertEquals(expected2, list.getUntil(day(3)))
} }
@Test @Test
fun testGetValuesUntil2() { fun testGetValuesUntil2() {
val list = CheckmarkList(Frequency(1, 2), HabitType.BOOLEAN_HABIT) val list = CheckmarkList(Frequency(1, 2), HabitType.BOOLEAN_HABIT)
val expected = listOf<Int>() val expected = listOf<Checkmark>()
assertEquals(expected, list.getValuesUntil(day(0))) assertEquals(expected, list.getUntil(day(0)))
} }
} }

@ -0,0 +1,94 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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)
}
}

@ -31,7 +31,7 @@ class MainScreenCell : UITableViewCell {
fatalError() 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 { if buttons.count != nButtons {
buttons.removeAll() buttons.removeAll()
for v in contentView.subviews { v.removeFromSuperview() } for v in contentView.subviews { v.removeFromSuperview() }
@ -68,7 +68,7 @@ class MainScreenCell : UITableViewCell {
label.text = habit.name label.text = habit.name
label.textColor = color.uicolor label.textColor = color.uicolor
ring.component = Ring(color: color, ring.component = Ring(color: color,
percentage: Double.random(in: 0...1), percentage: score.value,
thickness: 2.5, thickness: 2.5,
radius: 7, radius: 7,
theme: theme, theme: theme,
@ -78,12 +78,12 @@ class MainScreenCell : UITableViewCell {
for i in 0..<buttons.count { for i in 0..<buttons.count {
if habit.type == .numerical { if habit.type == .numerical {
buttons[i].component = NumberButton(color: color, buttons[i].component = NumberButton(color: color,
value: Double(truncating: values[i]) / 1000.0, value: Double(checkmarks[i].value) / 1000.0,
threshold: habit.target, threshold: habit.target,
units: habit.unit, units: habit.unit,
theme: theme) theme: theme)
} else { } else {
buttons[i].component = CheckmarkButton(value: Int32(truncating: values[i]), buttons[i].component = CheckmarkButton(value: checkmarks[i].value,
color: color, color: color,
theme: theme) theme: theme)
} }
@ -148,7 +148,11 @@ class MainScreenController: UITableViewController, MainScreenDataSourceListener
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MainScreenCell let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MainScreenCell
let habit = data!.habits[indexPath.row] 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 return cell
} }

Loading…
Cancel
Save