mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-07 01:28:52 -06:00
Merge branch 'dev' into feature/sync2
This commit is contained in:
11
uhabits-core/assets/test/habitbull3.csv
Normal file
11
uhabits-core/assets/test/habitbull3.csv
Normal file
@@ -0,0 +1,11 @@
|
||||
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
|
||||
Pushups,,Fitness,2021-09-01,30,
|
||||
Pushups,,Fitness,2022-01-08,100,
|
||||
Pushups,,Fitness,2022-01-09,100,
|
||||
Pushups,,Fitness,2022-01-10,100,
|
||||
Pushups,,Fitness,2022-01-11,100,
|
||||
Pushups,,Fitness,2022-01-12,100,
|
||||
Pushups,,Fitness,2022-01-13,100,
|
||||
run,,Fitness,2022-01-03,1,
|
||||
run,,Fitness,2022-01-18,1,
|
||||
run,,Fitness,2022-01-19,1,
|
||||
|
@@ -43,13 +43,13 @@ kotlin {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
compileOnly("com.google.dagger:dagger:2.40.3")
|
||||
implementation("com.google.guava:guava:31.0.1-android")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2")
|
||||
implementation("androidx.annotation:annotation:1.3.0")
|
||||
compileOnly("com.google.dagger:dagger:2.43.2")
|
||||
implementation("com.google.guava:guava:31.1-android")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.10")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4")
|
||||
implementation("androidx.annotation:annotation:1.4.0")
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
implementation("com.opencsv:opencsv:5.5.2")
|
||||
implementation("com.opencsv:opencsv:5.6")
|
||||
implementation("commons-codec:commons-codec:1.15")
|
||||
implementation("org.apache.commons:commons-lang3:3.12.0")
|
||||
}
|
||||
@@ -59,7 +59,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation("org.xerial:sqlite-jdbc:3.36.0.3")
|
||||
implementation("org.xerial:sqlite-jdbc:3.39.2.1")
|
||||
implementation("org.hamcrest:hamcrest:2.2")
|
||||
implementation("org.apache.commons:commons-io:1.3.2")
|
||||
implementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
|
||||
@@ -29,6 +29,11 @@ enum class Font {
|
||||
FONT_AWESOME
|
||||
}
|
||||
|
||||
data class ScreenLocation(
|
||||
val x: Double,
|
||||
val y: Double,
|
||||
)
|
||||
|
||||
interface Canvas {
|
||||
fun setColor(color: Color)
|
||||
fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double)
|
||||
|
||||
@@ -25,6 +25,12 @@ class StringUtils {
|
||||
|
||||
fun joinLongs(values: LongArray): String = values.joinToString(separator = ",")
|
||||
|
||||
fun splitLongs(str: String): LongArray = str.split(",").map { it.toLong() }.toLongArray()
|
||||
fun splitLongs(str: String): LongArray {
|
||||
return try {
|
||||
str.split(",").map { it.toLong() }.toLongArray()
|
||||
} catch (e: NumberFormatException) {
|
||||
LongArray(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.isoron.platform.time
|
||||
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar.DAY_OF_MONTH
|
||||
import java.util.Calendar.DAY_OF_WEEK
|
||||
import java.util.Calendar.HOUR_OF_DAY
|
||||
@@ -66,4 +67,10 @@ class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter {
|
||||
val cal = date.toGregorianCalendar()
|
||||
return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale)
|
||||
}
|
||||
|
||||
fun longFormat(date: LocalDate): String {
|
||||
val df = DateFormat.getDateInstance(DateFormat.LONG, locale)
|
||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return df.format(date.toGregorianCalendar().time)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,6 @@ data class EditHabitCommand(
|
||||
habitList.update(habit)
|
||||
habit.observable.notifyListeners()
|
||||
habit.recompute()
|
||||
habitList.resort()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.HabitType
|
||||
import org.isoron.uhabits.core.models.ModelFactory
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import java.io.BufferedReader
|
||||
@@ -50,7 +51,7 @@ class HabitBullCSVImporter
|
||||
logging: Logging,
|
||||
) : AbstractImporter() {
|
||||
|
||||
val logger = logging.getLogger("HabitBullCSVImporter")
|
||||
private val logger = logging.getLogger("HabitBullCSVImporter")
|
||||
|
||||
override fun canHandle(file: File): Boolean {
|
||||
val reader = BufferedReader(FileReader(file))
|
||||
@@ -77,10 +78,16 @@ class HabitBullCSVImporter
|
||||
logger.info("Creating habit: $name")
|
||||
}
|
||||
val notes = cols[5] ?: ""
|
||||
if (parseInt(cols[4]) == 1) {
|
||||
h.originalEntries.add(Entry(timestamp, Entry.YES_MANUAL, notes))
|
||||
} else {
|
||||
h.originalEntries.add(Entry(timestamp, Entry.NO, notes))
|
||||
when (val value = parseInt(cols[4])) {
|
||||
0 -> h.originalEntries.add(Entry(timestamp, Entry.NO, notes))
|
||||
1 -> h.originalEntries.add(Entry(timestamp, Entry.YES_MANUAL, notes))
|
||||
else -> {
|
||||
if (value > 1 && h.type != HabitType.NUMERICAL) {
|
||||
logger.info("Found a value of $value, considering this habit as numerical.")
|
||||
h.type = HabitType.NUMERICAL
|
||||
}
|
||||
h.originalEntries.add(Entry(timestamp, value, notes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,13 @@ package org.isoron.uhabits.core.io
|
||||
|
||||
import org.isoron.uhabits.core.AppScope
|
||||
import org.isoron.uhabits.core.DATABASE_VERSION
|
||||
import org.isoron.uhabits.core.commands.Command
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.commands.CreateHabitCommand
|
||||
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
||||
import org.isoron.uhabits.core.commands.EditHabitCommand
|
||||
import org.isoron.uhabits.core.database.DatabaseOpener
|
||||
import org.isoron.uhabits.core.database.MigrationHelper
|
||||
import org.isoron.uhabits.core.database.Repository
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.ModelFactory
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
@@ -81,34 +80,33 @@ class LoopDBImporter
|
||||
var habit = habitList.getByUUID(habitRecord.uuid)
|
||||
val entryRecords = entryRepository.findAll("where habit = ?", habitRecord.id.toString())
|
||||
|
||||
var command: Command
|
||||
if (habit == null) {
|
||||
habit = modelFactory.buildHabit()
|
||||
habitRecord.id = null
|
||||
habitRecord.copyTo(habit)
|
||||
command = CreateHabitCommand(modelFactory, habitList, habit)
|
||||
command.run()
|
||||
CreateHabitCommand(modelFactory, habitList, habit).run()
|
||||
} else {
|
||||
val modified = modelFactory.buildHabit()
|
||||
habitRecord.id = habit.id
|
||||
habitRecord.copyTo(modified)
|
||||
command = EditHabitCommand(habitList, habit.id!!, modified)
|
||||
command.run()
|
||||
EditHabitCommand(habitList, habit.id!!, modified).run()
|
||||
}
|
||||
|
||||
// Reload saved version of the habit
|
||||
habit = habitList.getByUUID(habitRecord.uuid)
|
||||
habit = habitList.getByUUID(habitRecord.uuid)!!
|
||||
val entries = habit.originalEntries
|
||||
|
||||
// Import entries
|
||||
for (r in entryRecords) {
|
||||
val t = Timestamp(r.timestamp!!)
|
||||
val (_, value, notes) = habit!!.originalEntries.get(t)
|
||||
val oldNotes = r.notes ?: ""
|
||||
if (value != r.value || notes != oldNotes) CreateRepetitionCommand(habitList, habit, t, r.value!!, oldNotes).run()
|
||||
val (_, value, notes) = entries.get(t)
|
||||
if (value != r.value || notes != r.notes) {
|
||||
entries.add(Entry(t, r.value!!, r.notes ?: ""))
|
||||
}
|
||||
}
|
||||
|
||||
runner.notifyListeners(command)
|
||||
habit.recompute()
|
||||
}
|
||||
|
||||
habitList.resort()
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +276,8 @@ open class EntryList {
|
||||
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
|
||||
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
|
||||
*
|
||||
* SKIP values are converted to zero (if they weren't, each SKIP day would count as 0.003).
|
||||
*
|
||||
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
|
||||
* coming last. If the original list has gaps in it (for example, weeks or months without any
|
||||
* entries), then the list produced by this method will also have gaps.
|
||||
@@ -289,7 +291,10 @@ fun List<Entry>.groupedSum(
|
||||
): List<Entry> {
|
||||
return this.map { (timestamp, value) ->
|
||||
if (isNumerical) {
|
||||
Entry(timestamp, max(0, value))
|
||||
if (value == SKIP)
|
||||
Entry(timestamp, 0)
|
||||
else
|
||||
Entry(timestamp, max(0, value))
|
||||
} else {
|
||||
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
|
||||
}
|
||||
@@ -301,6 +306,31 @@ fun List<Entry>.groupedSum(
|
||||
}.entries.map { (timestamp, entries) ->
|
||||
Entry(timestamp, entries.sumOf { it.value })
|
||||
}.sortedBy { (timestamp, _) ->
|
||||
- timestamp.unixTime
|
||||
-timestamp.unixTime
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of days with vaLue SKIP in the given period.
|
||||
*/
|
||||
fun List<Entry>.countSkippedDays(
|
||||
truncateField: DateUtils.TruncateField,
|
||||
firstWeekday: Int = Calendar.SATURDAY
|
||||
): List<Entry> {
|
||||
return this.map { (timestamp, value) ->
|
||||
if (value == SKIP) {
|
||||
Entry(timestamp, 1)
|
||||
} else {
|
||||
Entry(timestamp, 0)
|
||||
}
|
||||
}.groupBy { entry ->
|
||||
entry.timestamp.truncate(
|
||||
truncateField,
|
||||
firstWeekday,
|
||||
)
|
||||
}.entries.map { (timestamp, entries) ->
|
||||
Entry(timestamp, entries.sumOf { it.value })
|
||||
}.sortedBy { (timestamp, _) ->
|
||||
-timestamp.unixTime
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,20 +100,25 @@ class ScoreList {
|
||||
}
|
||||
|
||||
val normalizedRollingSum = rollingSum / 1000
|
||||
val percentageCompleted = if (!isAtMost) {
|
||||
if (targetValue > 0)
|
||||
min(1.0, normalizedRollingSum / targetValue)
|
||||
else
|
||||
1.0
|
||||
} else {
|
||||
if (targetValue > 0) {
|
||||
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(0.0, 1.0)
|
||||
if (values[offset] != Entry.SKIP) {
|
||||
val percentageCompleted = if (!isAtMost) {
|
||||
if (targetValue > 0)
|
||||
min(1.0, normalizedRollingSum / targetValue)
|
||||
else
|
||||
1.0
|
||||
} else {
|
||||
if (normalizedRollingSum > 0) 0.0 else 1.0
|
||||
if (targetValue > 0) {
|
||||
(1 - ((normalizedRollingSum - targetValue) / targetValue)).coerceIn(
|
||||
0.0,
|
||||
1.0
|
||||
)
|
||||
} else {
|
||||
if (normalizedRollingSum > 0) 0.0 else 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousValue = compute(freq, previousValue, percentageCompleted)
|
||||
previousValue = compute(freq, previousValue, percentageCompleted)
|
||||
}
|
||||
} else {
|
||||
if (values[offset] == Entry.YES_MANUAL) {
|
||||
rollingSum += 1.0
|
||||
|
||||
@@ -152,19 +152,10 @@ open class Preferences(private val storage: Storage) {
|
||||
for (l in listeners) l.onNotificationsChanged()
|
||||
}
|
||||
|
||||
fun setNotificationsLed(enabled: Boolean) {
|
||||
storage.putBoolean("pref_led_notifications", enabled)
|
||||
for (l in listeners) l.onNotificationsChanged()
|
||||
}
|
||||
|
||||
fun shouldMakeNotificationsSticky(): Boolean {
|
||||
return storage.getBoolean("pref_sticky_notifications", false)
|
||||
}
|
||||
|
||||
fun shouldMakeNotificationsLed(): Boolean {
|
||||
return storage.getBoolean("pref_led_notifications", false)
|
||||
}
|
||||
|
||||
open var isCheckmarkSequenceReversed: Boolean
|
||||
get() {
|
||||
if (shouldReverseCheckmarks == null) shouldReverseCheckmarks =
|
||||
|
||||
@@ -79,8 +79,8 @@ class HabitCardListCache @Inject constructor(
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getNoteIndicators(habitId: Long): BooleanArray {
|
||||
return data.notesIndicators[habitId]!!
|
||||
fun getNotes(habitId: Long): Array<String> {
|
||||
return data.notes[habitId]!!
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -168,7 +168,7 @@ class HabitCardListCache @Inject constructor(
|
||||
data.habits.removeAt(position)
|
||||
data.idToHabit.remove(id)
|
||||
data.checkmarks.remove(id)
|
||||
data.notesIndicators.remove(id)
|
||||
data.notes.remove(id)
|
||||
data.scores.remove(id)
|
||||
listener.onItemRemoved(position)
|
||||
}
|
||||
@@ -213,7 +213,7 @@ class HabitCardListCache @Inject constructor(
|
||||
val habits: MutableList<Habit>
|
||||
val checkmarks: HashMap<Long?, IntArray>
|
||||
val scores: HashMap<Long?, Double>
|
||||
val notesIndicators: HashMap<Long?, BooleanArray>
|
||||
val notes: HashMap<Long?, Array<String>>
|
||||
|
||||
@Synchronized
|
||||
fun copyCheckmarksFrom(oldData: CacheData) {
|
||||
@@ -226,10 +226,10 @@ class HabitCardListCache @Inject constructor(
|
||||
|
||||
@Synchronized
|
||||
fun copyNoteIndicatorsFrom(oldData: CacheData) {
|
||||
val empty = BooleanArray(checkmarkCount)
|
||||
val empty = (0..checkmarkCount).map { "" }.toTypedArray()
|
||||
for (id in idToHabit.keys) {
|
||||
if (oldData.notesIndicators.containsKey(id)) notesIndicators[id] =
|
||||
oldData.notesIndicators[id]!! else notesIndicators[id] = empty
|
||||
if (oldData.notes.containsKey(id)) notes[id] =
|
||||
oldData.notes[id]!! else notes[id] = empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ class HabitCardListCache @Inject constructor(
|
||||
habits = LinkedList()
|
||||
checkmarks = HashMap()
|
||||
scores = HashMap()
|
||||
notesIndicators = HashMap()
|
||||
notes = HashMap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,14 +298,14 @@ class HabitCardListCache @Inject constructor(
|
||||
if (targetId != null && targetId != habit.id) continue
|
||||
newData.scores[habit.id] = habit.scores[today].value
|
||||
val list: MutableList<Int> = ArrayList()
|
||||
val notesIndicators: MutableList<Boolean> = ArrayList()
|
||||
val notes: MutableList<String> = ArrayList()
|
||||
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
|
||||
list.add(value)
|
||||
notesIndicators.add(note.isNotEmpty())
|
||||
notes.add(note)
|
||||
}
|
||||
val entries = list.toTypedArray()
|
||||
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries)
|
||||
newData.notesIndicators[habit.id] = notesIndicators.toBooleanArray()
|
||||
newData.notes[habit.id] = notes.toTypedArray()
|
||||
runner!!.publishProgress(this, position)
|
||||
}
|
||||
}
|
||||
@@ -333,7 +333,7 @@ class HabitCardListCache @Inject constructor(
|
||||
data.idToHabit[id] = habit
|
||||
data.scores[id] = newData.scores[id]!!
|
||||
data.checkmarks[id] = newData.checkmarks[id]!!
|
||||
data.notesIndicators[id] = newData.notesIndicators[id]!!
|
||||
data.notes[id] = newData.notes[id]!!
|
||||
listener.onItemInserted(position)
|
||||
}
|
||||
|
||||
@@ -361,10 +361,10 @@ class HabitCardListCache @Inject constructor(
|
||||
private fun performUpdate(id: Long, position: Int) {
|
||||
val oldScore = data.scores[id]!!
|
||||
val oldCheckmarks = data.checkmarks[id]
|
||||
val oldNoteIndicators = data.notesIndicators[id]
|
||||
val oldNoteIndicators = data.notes[id]
|
||||
val newScore = newData.scores[id]!!
|
||||
val newCheckmarks = newData.checkmarks[id]!!
|
||||
val newNoteIndicators = newData.notesIndicators[id]!!
|
||||
val newNoteIndicators = newData.notes[id]!!
|
||||
var unchanged = true
|
||||
if (oldScore != newScore) unchanged = false
|
||||
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
|
||||
@@ -372,7 +372,7 @@ class HabitCardListCache @Inject constructor(
|
||||
if (unchanged) return
|
||||
data.scores[id] = newScore
|
||||
data.checkmarks[id] = newCheckmarks
|
||||
data.notesIndicators[id] = newNoteIndicators
|
||||
data.notes[id] = newNoteIndicators
|
||||
listener.onItemChanged(position)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,21 +51,15 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
fun onEdit(habit: Habit, timestamp: Timestamp?) {
|
||||
val entry = habit.computedEntries.get(timestamp!!)
|
||||
if (habit.type == HabitType.NUMERICAL) {
|
||||
val oldValue = entry.value.toDouble()
|
||||
screen.showNumberPicker(
|
||||
oldValue / 1000,
|
||||
habit.unit,
|
||||
entry.notes,
|
||||
timestamp.toDialogDateString(),
|
||||
) { newValue: Double, newNotes: String, ->
|
||||
val oldValue = entry.value.toDouble() / 1000
|
||||
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
|
||||
val value = (newValue * 1000).roundToInt()
|
||||
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes))
|
||||
}
|
||||
} else {
|
||||
screen.showCheckmarkDialog(
|
||||
screen.showCheckmarkPopup(
|
||||
entry.value,
|
||||
entry.notes,
|
||||
timestamp.toDialogDateString(),
|
||||
habit.color,
|
||||
) { newValue, newNotes ->
|
||||
commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes))
|
||||
@@ -119,8 +113,7 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
if (prefs.isFirstRun) onFirstRun()
|
||||
}
|
||||
|
||||
fun onToggle(habit: Habit, timestamp: Timestamp?, value: Int) {
|
||||
val notes = habit.computedEntries.get(timestamp!!).notes
|
||||
fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String) {
|
||||
commandRunner.run(
|
||||
CreateRepetitionCommand(habitList, habit, timestamp, value, notes)
|
||||
)
|
||||
@@ -160,21 +153,17 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
fun showHabitScreen(h: Habit)
|
||||
fun showIntroScreen()
|
||||
fun showMessage(m: Message)
|
||||
fun showNumberPicker(
|
||||
fun showNumberPopup(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
callback: NumberPickerCallback
|
||||
)
|
||||
fun showCheckmarkDialog(
|
||||
value: Int,
|
||||
fun showCheckmarkPopup(
|
||||
selectedValue: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
color: PaletteColor,
|
||||
callback: CheckMarkDialogCallback
|
||||
)
|
||||
|
||||
fun showSendBugReportToDeveloperScreen(log: String)
|
||||
fun showSendFileScreen(filename: String)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ data class FrequencyCardState(
|
||||
val firstWeekday: Int,
|
||||
val frequency: HashMap<Timestamp, Array<Int>>,
|
||||
val theme: Theme,
|
||||
val isNumerical: Boolean
|
||||
)
|
||||
|
||||
class FrequencyCardPresenter {
|
||||
@@ -40,6 +41,7 @@ class FrequencyCardPresenter {
|
||||
theme: Theme
|
||||
) = FrequencyCardState(
|
||||
color = habit.color,
|
||||
isNumerical = habit.isNumerical,
|
||||
frequency = habit.originalEntries.computeWeekdayFrequency(
|
||||
isNumerical = habit.isNumerical
|
||||
),
|
||||
|
||||
@@ -29,16 +29,21 @@ import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType.AT_LEAST
|
||||
import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.DIMMED
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.GREY
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.HATCHED
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.OFF
|
||||
import org.isoron.uhabits.core.ui.views.HistoryChart.Square.ON
|
||||
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class HistoryCardState(
|
||||
@@ -63,23 +68,10 @@ class HistoryCardPresenter(
|
||||
val timestamp = Timestamp.fromLocalDate(date)
|
||||
screen.showFeedback()
|
||||
if (habit.isNumerical) {
|
||||
showNumberPicker(timestamp)
|
||||
showNumberPopup(timestamp)
|
||||
} else {
|
||||
val entry = habit.computedEntries.get(timestamp)
|
||||
val nextValue = Entry.nextToggleValue(
|
||||
value = entry.value,
|
||||
isSkipEnabled = preferences.isSkipEnabled,
|
||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||
)
|
||||
commandRunner.run(
|
||||
CreateRepetitionCommand(
|
||||
habitList,
|
||||
habit,
|
||||
timestamp,
|
||||
nextValue,
|
||||
entry.notes,
|
||||
),
|
||||
)
|
||||
if (preferences.isShortToggleEnabled) showCheckmarkPopup(timestamp)
|
||||
else toggle(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,37 +79,58 @@ class HistoryCardPresenter(
|
||||
val timestamp = Timestamp.fromLocalDate(date)
|
||||
screen.showFeedback()
|
||||
if (habit.isNumerical) {
|
||||
showNumberPicker(timestamp)
|
||||
showNumberPopup(timestamp)
|
||||
} else {
|
||||
val entry = habit.computedEntries.get(timestamp)
|
||||
screen.showCheckmarkDialog(
|
||||
entry.value,
|
||||
entry.notes,
|
||||
timestamp.toDialogDateString(),
|
||||
preferences,
|
||||
habit.color,
|
||||
) { newValue, newNotes ->
|
||||
commandRunner.run(
|
||||
CreateRepetitionCommand(
|
||||
habitList,
|
||||
habit,
|
||||
timestamp,
|
||||
newValue,
|
||||
newNotes,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (preferences.isShortToggleEnabled) toggle(timestamp)
|
||||
else showCheckmarkPopup(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNumberPicker(timestamp: Timestamp) {
|
||||
private fun showCheckmarkPopup(timestamp: Timestamp) {
|
||||
val entry = habit.computedEntries.get(timestamp)
|
||||
screen.showCheckmarkPopup(
|
||||
entry.value,
|
||||
entry.notes,
|
||||
preferences,
|
||||
habit.color,
|
||||
) { newValue, newNotes ->
|
||||
commandRunner.run(
|
||||
CreateRepetitionCommand(
|
||||
habitList,
|
||||
habit,
|
||||
timestamp,
|
||||
newValue,
|
||||
newNotes,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggle(timestamp: Timestamp) {
|
||||
val entry = habit.computedEntries.get(timestamp)
|
||||
val nextValue = Entry.nextToggleValue(
|
||||
value = entry.value,
|
||||
isSkipEnabled = preferences.isSkipEnabled,
|
||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||
)
|
||||
commandRunner.run(
|
||||
CreateRepetitionCommand(
|
||||
habitList,
|
||||
habit,
|
||||
timestamp,
|
||||
nextValue,
|
||||
entry.notes,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNumberPopup(timestamp: Timestamp) {
|
||||
val entry = habit.computedEntries.get(timestamp)
|
||||
val oldValue = entry.value
|
||||
screen.showNumberPicker(
|
||||
oldValue / 1000.0,
|
||||
habit.unit,
|
||||
entry.notes,
|
||||
timestamp.toDialogDateString(),
|
||||
screen.showNumberPopup(
|
||||
value = oldValue / 1000.0,
|
||||
notes = entry.notes,
|
||||
preferences = preferences,
|
||||
) { newValue: Double, newNotes: String ->
|
||||
val thousands = (newValue * 1000).roundToInt()
|
||||
commandRunner.run(
|
||||
@@ -146,36 +159,25 @@ class HistoryCardPresenter(
|
||||
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
||||
val entries = habit.computedEntries.getByInterval(oldest, today)
|
||||
val series = if (habit.isNumerical) {
|
||||
if (habit.targetType == NumericalHabitType.AT_LEAST) {
|
||||
entries.map {
|
||||
when (max(0, it.value)) {
|
||||
0 -> HistoryChart.Square.OFF
|
||||
else -> HistoryChart.Square.ON
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entries.map {
|
||||
when {
|
||||
max(0.0, it.value / 1000.0) <= habit.targetValue -> HistoryChart.Square.ON
|
||||
else -> HistoryChart.Square.OFF
|
||||
}
|
||||
entries.map {
|
||||
when {
|
||||
it.value == Entry.UNKNOWN -> OFF
|
||||
it.value == SKIP -> HATCHED
|
||||
(habit.targetType == AT_MOST) && (it.value / 1000.0 <= habit.targetValue) -> ON
|
||||
(habit.targetType == AT_LEAST) && (it.value / 1000.0 >= habit.targetValue) -> ON
|
||||
else -> GREY
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entries.map {
|
||||
when (it.value) {
|
||||
YES_MANUAL -> HistoryChart.Square.ON
|
||||
YES_AUTO -> HistoryChart.Square.DIMMED
|
||||
SKIP -> HistoryChart.Square.HATCHED
|
||||
else -> HistoryChart.Square.OFF
|
||||
YES_MANUAL -> ON
|
||||
YES_AUTO -> DIMMED
|
||||
SKIP -> HATCHED
|
||||
else -> OFF
|
||||
}
|
||||
}
|
||||
}
|
||||
val defaultSquare = if (habit.isNumerical && habit.targetType == NumericalHabitType.AT_MOST)
|
||||
HistoryChart.Square.ON
|
||||
else
|
||||
HistoryChart.Square.OFF
|
||||
|
||||
val notesIndicators = entries.map {
|
||||
when (it.notes) {
|
||||
"" -> false
|
||||
@@ -189,7 +191,7 @@ class HistoryCardPresenter(
|
||||
today = today.toLocalDate(),
|
||||
theme = theme,
|
||||
series = series,
|
||||
defaultSquare = defaultSquare,
|
||||
defaultSquare = OFF,
|
||||
notesIndicators = notesIndicators,
|
||||
)
|
||||
}
|
||||
@@ -198,17 +200,15 @@ class HistoryCardPresenter(
|
||||
interface Screen {
|
||||
fun showHistoryEditorDialog(listener: OnDateClickedListener)
|
||||
fun showFeedback()
|
||||
fun showNumberPicker(
|
||||
fun showNumberPopup(
|
||||
value: Double,
|
||||
unit: String,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
preferences: Preferences,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
||||
)
|
||||
fun showCheckmarkDialog(
|
||||
value: Int,
|
||||
fun showCheckmarkPopup(
|
||||
selectedValue: Int,
|
||||
notes: String,
|
||||
dateString: String,
|
||||
preferences: Preferences,
|
||||
color: PaletteColor,
|
||||
callback: ListHabitsBehavior.CheckMarkDialogCallback,
|
||||
|
||||
@@ -21,11 +21,13 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
|
||||
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.countSkippedDays
|
||||
import org.isoron.uhabits.core.models.groupedSum
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import java.util.ArrayList
|
||||
import java.util.Calendar
|
||||
import kotlin.math.max
|
||||
|
||||
data class TargetCardState(
|
||||
val color: PaletteColor,
|
||||
@@ -51,37 +53,88 @@ class TargetCardPresenter {
|
||||
isNumerical = habit.isNumerical
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val skippedDayToday = entries.countSkippedDays(
|
||||
truncateField = DateUtils.TruncateField.DAY
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val valueThisWeek = entries.groupedSum(
|
||||
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
||||
firstWeekday = firstWeekday,
|
||||
isNumerical = habit.isNumerical
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val skippedDaysThisWeek = entries.countSkippedDays(
|
||||
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
||||
firstWeekday = firstWeekday
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val valueThisMonth = entries.groupedSum(
|
||||
truncateField = DateUtils.TruncateField.MONTH,
|
||||
isNumerical = habit.isNumerical
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val skippedDaysThisMonth = entries.countSkippedDays(
|
||||
truncateField = DateUtils.TruncateField.MONTH,
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val valueThisQuarter = entries.groupedSum(
|
||||
truncateField = DateUtils.TruncateField.QUARTER,
|
||||
isNumerical = habit.isNumerical
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val skippedDaysThisQuarter = entries.countSkippedDays(
|
||||
truncateField = DateUtils.TruncateField.QUARTER
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val valueThisYear = entries.groupedSum(
|
||||
truncateField = DateUtils.TruncateField.YEAR,
|
||||
isNumerical = habit.isNumerical
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val skippedDaysThisYear = entries.countSkippedDays(
|
||||
truncateField = DateUtils.TruncateField.YEAR
|
||||
).firstOrNull()?.value ?: 0
|
||||
|
||||
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
|
||||
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
|
||||
val daysInWeek = 7
|
||||
val daysInQuarter = 91
|
||||
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
|
||||
val weeksInMonth = daysInMonth / 7
|
||||
val weeksInQuarter = 13
|
||||
val weeksInYear = 52
|
||||
val monthsInQuarter = 3
|
||||
val monthsInYear = 12
|
||||
|
||||
val targetToday = habit.targetValue / habit.frequency.denominator
|
||||
val targetThisWeek = targetToday * 7
|
||||
val targetThisMonth = targetToday * daysInMonth
|
||||
val targetThisQuarter = targetToday * daysInQuarter
|
||||
val targetThisYear = targetToday * daysInYear
|
||||
val denominator = habit.frequency.denominator
|
||||
val dailyTarget = habit.targetValue / habit.frequency.denominator
|
||||
|
||||
var targetToday = dailyTarget
|
||||
var targetThisWeek = when (denominator) {
|
||||
7 -> habit.targetValue
|
||||
else -> dailyTarget * daysInWeek
|
||||
}
|
||||
var targetThisMonth = when (denominator) {
|
||||
30 -> habit.targetValue
|
||||
7 -> habit.targetValue * weeksInMonth
|
||||
else -> dailyTarget * daysInMonth
|
||||
}
|
||||
var targetThisQuarter = when (denominator) {
|
||||
30 -> habit.targetValue * monthsInQuarter
|
||||
7 -> habit.targetValue * weeksInQuarter
|
||||
else -> dailyTarget * daysInQuarter
|
||||
}
|
||||
var targetThisYear = when (denominator) {
|
||||
30 -> habit.targetValue * monthsInYear
|
||||
7 -> habit.targetValue * weeksInYear
|
||||
else -> dailyTarget * daysInYear
|
||||
}
|
||||
|
||||
targetToday = max(0.0, targetToday - dailyTarget * skippedDayToday)
|
||||
targetThisWeek = max(0.0, targetThisWeek - dailyTarget * skippedDaysThisWeek)
|
||||
targetThisMonth = max(0.0, targetThisMonth - dailyTarget * skippedDaysThisMonth)
|
||||
targetThisQuarter = max(0.0, targetThisQuarter - dailyTarget * skippedDaysThisQuarter)
|
||||
targetThisYear = max(0.0, targetThisYear - dailyTarget * skippedDaysThisYear)
|
||||
|
||||
val values = ArrayList<Double>()
|
||||
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
|
||||
|
||||
@@ -92,7 +92,7 @@ class BarChart(
|
||||
val r = round(barWidth * 0.15)
|
||||
if (2 * r < barHeight) {
|
||||
canvas.fillRect(x, y + r, barWidth, barHeight - r)
|
||||
canvas.fillRect(x + r, y, barWidth - 2 * r, r)
|
||||
canvas.fillRect(x + r, y, barWidth - 2 * r, r + 1)
|
||||
canvas.fillCircle(x + r, y + r, r)
|
||||
canvas.fillCircle(x + barWidth - r, y + r, r)
|
||||
} else {
|
||||
|
||||
@@ -53,6 +53,7 @@ class HistoryChart(
|
||||
enum class Square {
|
||||
ON,
|
||||
OFF,
|
||||
GREY,
|
||||
DIMMED,
|
||||
HATCHED,
|
||||
}
|
||||
@@ -86,7 +87,7 @@ class HistoryChart(
|
||||
val col = ((x - padding) / squareSize).toInt()
|
||||
val row = ((y - padding) / squareSize).toInt()
|
||||
val offset = col * 7 + (row - 1)
|
||||
if (row == 0 || col == nColumns) return
|
||||
if (x - padding < 0 || row == 0 || row > 7 || col == nColumns) return
|
||||
val clickedDate = topLeftDate.plus(offset)
|
||||
if (clickedDate.isNewerThan(today)) return
|
||||
if (isLongClick) {
|
||||
@@ -216,6 +217,9 @@ class HistoryChart(
|
||||
Square.OFF -> {
|
||||
theme.lowContrastTextColor
|
||||
}
|
||||
Square.GREY -> {
|
||||
theme.mediumContrastTextColor
|
||||
}
|
||||
Square.DIMMED, Square.HATCHED -> {
|
||||
color.blendWith(theme.cardBackgroundColor, 0.5)
|
||||
}
|
||||
@@ -254,7 +258,7 @@ class HistoryChart(
|
||||
|
||||
if (hasNotes) {
|
||||
circleColor = when (value) {
|
||||
Square.ON -> theme.lowContrastTextColor
|
||||
Square.ON, Square.GREY -> theme.lowContrastTextColor
|
||||
else -> color
|
||||
}
|
||||
canvas.setColor(circleColor)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package org.isoron.uhabits.core.utils
|
||||
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import java.time.YearMonth
|
||||
import java.util.Calendar
|
||||
import java.util.Calendar.DAY_OF_MONTH
|
||||
import java.util.Calendar.DAY_OF_WEEK
|
||||
@@ -178,6 +179,26 @@ abstract class DateUtils {
|
||||
return getWeekdayNames(GregorianCalendar.SHORT, firstWeekday)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a vector of Int representing the frequency of each weekday in a given month.
|
||||
*
|
||||
* @param startOfMonth a Timestamp representing the beginning of the month.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getWeekdaysInMonth(startOfMonth: Timestamp): Array<Int> {
|
||||
val month = startOfMonth.toCalendar()[Calendar.MONTH] + 1
|
||||
val year = startOfMonth.toCalendar()[Calendar.YEAR]
|
||||
val weekday = startOfMonth.weekday
|
||||
val monthLength = YearMonth.of(year, month).lengthOfMonth()
|
||||
|
||||
val freq = Array(7) { 0 }
|
||||
for (day in weekday until weekday + monthLength) {
|
||||
freq[day % 7] += 1
|
||||
}
|
||||
|
||||
return freq
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getToday(): Timestamp = Timestamp(getStartOfToday())
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.isoron.uhabits.core.BaseUnitTest
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Frequency
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitType
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime
|
||||
@@ -73,6 +74,30 @@ class ImportTest : BaseUnitTest() {
|
||||
assertTrue(isNotesEqual(habit, 2019, 6, 14, "Habit 3 notes"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun testHabitBullCSV3() {
|
||||
importFromFile("habitbull3.csv")
|
||||
assertThat(habitList.size(), equalTo(2))
|
||||
|
||||
val habit = habitList.getByPosition(0)
|
||||
assertThat(habit.name, equalTo("Pushups"))
|
||||
assertThat(habit.type, equalTo(HabitType.NUMERICAL))
|
||||
assertThat(habit.description, equalTo(""))
|
||||
assertThat(habit.frequency, equalTo(Frequency.DAILY))
|
||||
assertThat(getValue(habit, 2021, 9, 1), equalTo(30))
|
||||
assertThat(getValue(habit, 2022, 1, 8), equalTo(100))
|
||||
|
||||
val habit2 = habitList.getByPosition(1)
|
||||
assertThat(habit2.name, equalTo("run"))
|
||||
assertThat(habit2.type, equalTo(HabitType.YES_NO))
|
||||
assertThat(habit2.description, equalTo(""))
|
||||
assertThat(habit2.frequency, equalTo(Frequency.DAILY))
|
||||
assertTrue(isChecked(habit2, 2022, 1, 3))
|
||||
assertTrue(isChecked(habit2, 2022, 1, 18))
|
||||
assertTrue(isChecked(habit2, 2022, 1, 19))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun testLoopDB() {
|
||||
@@ -124,10 +149,14 @@ class ImportTest : BaseUnitTest() {
|
||||
}
|
||||
|
||||
private fun isChecked(h: Habit, year: Int, month: Int, day: Int): Boolean {
|
||||
return getValue(h, year, month, day) == Entry.YES_MANUAL
|
||||
}
|
||||
|
||||
private fun getValue(h: Habit, year: Int, month: Int, day: Int): Int {
|
||||
val date = getStartOfTodayCalendar()
|
||||
date.set(year, month - 1, day)
|
||||
val timestamp = Timestamp(date)
|
||||
return h.originalEntries.get(timestamp).value == Entry.YES_MANUAL
|
||||
return h.originalEntries.get(timestamp).value
|
||||
}
|
||||
|
||||
private fun isNotesEqual(h: Habit, year: Int, month: Int, day: Int, notes: String): Boolean {
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.hamcrest.MatcherAssert.assertThat
|
||||
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.utils.DateUtils.Companion.getToday
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -381,6 +382,66 @@ class NumericalAtLeastScoreListTest : NumericalScoreListTest() {
|
||||
}
|
||||
}
|
||||
|
||||
class NumericalAtLeastScoreListWithSkipTest : NumericalScoreListTest() {
|
||||
@Before
|
||||
@Throws(Exception::class)
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
habit = fixtures.createEmptyNumericalHabit(NumericalHabitType.AT_LEAST)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_getValue() {
|
||||
addEntries(0, 10, 2000)
|
||||
addEntries(10, 11, SKIP)
|
||||
addEntries(11, 15, 2000)
|
||||
addEntries(15, 16, SKIP)
|
||||
addEntries(16, 20, 2000)
|
||||
val expectedValues = doubleArrayOf(
|
||||
0.617008,
|
||||
0.596033,
|
||||
0.573910,
|
||||
0.550574,
|
||||
0.525961,
|
||||
0.500000,
|
||||
0.472617,
|
||||
0.443734,
|
||||
0.413270,
|
||||
0.381137,
|
||||
0.347244, // skipped day should have the same score as the previous day
|
||||
0.347244,
|
||||
0.311495,
|
||||
0.273788,
|
||||
0.234017,
|
||||
0.192067, // skipped day should have the same score as the previous day
|
||||
0.192067,
|
||||
0.147820,
|
||||
0.101149,
|
||||
0.051922,
|
||||
0.000000,
|
||||
0.000000,
|
||||
0.000000
|
||||
)
|
||||
checkScoreValues(expectedValues)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skipsShouldNotAffectScore() {
|
||||
addEntries(0, 500, 1000)
|
||||
val initialScore = habit.scores[today].value
|
||||
|
||||
addEntries(500, 1000, SKIP)
|
||||
assertThat(habit.scores[today].value, IsCloseTo.closeTo(initialScore, E))
|
||||
|
||||
addEntries(0, 300, 1000)
|
||||
addEntries(300, 500, SKIP)
|
||||
addEntries(500, 700, 1000)
|
||||
|
||||
// skipped days should be treated as if they never existed
|
||||
assertThat(habit.scores[today].value, IsCloseTo.closeTo(initialScore, E))
|
||||
}
|
||||
}
|
||||
|
||||
class NumericalAtMostScoreListTest : NumericalScoreListTest() {
|
||||
@Before
|
||||
@Throws(Exception::class)
|
||||
|
||||
@@ -116,9 +116,6 @@ class PreferencesTest : BaseUnitTest() {
|
||||
assertFalse(prefs.shouldMakeNotificationsSticky())
|
||||
prefs.setNotificationsSticky(true)
|
||||
assertTrue(prefs.shouldMakeNotificationsSticky())
|
||||
assertFalse(prefs.shouldMakeNotificationsLed())
|
||||
prefs.setNotificationsLed(true)
|
||||
assertTrue(prefs.shouldMakeNotificationsLed())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -79,7 +79,11 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
||||
@Test
|
||||
fun testOnEdit() {
|
||||
behavior.onEdit(habit2, getToday())
|
||||
verify(screen).showNumberPicker(eq(0.1), eq("miles"), eq(""), eq("Jan 25, 2015"), picker.capture())
|
||||
verify(screen).showNumberPopup(
|
||||
eq(0.1),
|
||||
eq(""),
|
||||
picker.capture()
|
||||
)
|
||||
picker.lastValue.onNumberPicked(100.0, "")
|
||||
val today = getTodayWithOffset()
|
||||
assertThat(habit2.computedEntries.get(today).value, equalTo(100000))
|
||||
@@ -160,7 +164,12 @@ class ListHabitsBehaviorTest : BaseUnitTest() {
|
||||
@Test
|
||||
fun testOnToggle() {
|
||||
assertTrue(habit1.isCompletedToday())
|
||||
behavior.onToggle(habit1, getToday(), Entry.NO)
|
||||
behavior.onToggle(
|
||||
habit = habit1,
|
||||
timestamp = getToday(),
|
||||
value = Entry.NO,
|
||||
notes = ""
|
||||
)
|
||||
assertFalse(habit1.isCompletedToday())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,7 @@ class HistoryChartTest {
|
||||
else -> OFF
|
||||
}
|
||||
},
|
||||
notesIndicators = MutableList(85) {
|
||||
index: Int ->
|
||||
notesIndicators = MutableList(85) { index: Int ->
|
||||
index % 3 == 0
|
||||
}
|
||||
)
|
||||
|
||||
@@ -118,6 +118,31 @@ class DateUtilsTest : BaseUnitTest() {
|
||||
assertThat(arrayOf("Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"), equalTo(longWeekdayNames))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getWeekdaysInMonth() {
|
||||
val february = GregorianCalendar(2018, Calendar.FEBRUARY, 1)
|
||||
val leapFebruary = GregorianCalendar(2020, Calendar.FEBRUARY, 1)
|
||||
val month = GregorianCalendar(2020, Calendar.APRIL, 1)
|
||||
val longMonth = GregorianCalendar(2020, Calendar.AUGUST, 1)
|
||||
|
||||
assertThat(
|
||||
arrayOf(4, 4, 4, 4, 4, 4, 4),
|
||||
equalTo(DateUtils.getWeekdaysInMonth(Timestamp(february)))
|
||||
)
|
||||
assertThat(
|
||||
arrayOf(5, 4, 4, 4, 4, 4, 4),
|
||||
equalTo(DateUtils.getWeekdaysInMonth(Timestamp(leapFebruary)))
|
||||
)
|
||||
assertThat(
|
||||
arrayOf(4, 4, 4, 4, 5, 5, 4),
|
||||
equalTo(DateUtils.getWeekdaysInMonth(Timestamp(month)))
|
||||
)
|
||||
assertThat(
|
||||
arrayOf(5, 5, 5, 4, 4, 4, 4),
|
||||
equalTo(DateUtils.getWeekdaysInMonth(Timestamp(longMonth)))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetToday() {
|
||||
setFixedLocalTime(FIXED_LOCAL_TIME)
|
||||
|
||||
Reference in New Issue
Block a user