mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-14 04:58:52 -06:00
All scores, frequency, history, etc. corrected for the skip days features.
This commit is contained in:
@@ -51,6 +51,7 @@ import org.isoron.uhabits.core.models.HabitType
|
|||||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.models.Reminder
|
import org.isoron.uhabits.core.models.Reminder
|
||||||
|
import org.isoron.uhabits.core.models.SkipDays
|
||||||
import org.isoron.uhabits.core.models.WeekdayList
|
import org.isoron.uhabits.core.models.WeekdayList
|
||||||
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
|
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
|
||||||
import org.isoron.uhabits.utils.ColorUtils
|
import org.isoron.uhabits.utils.ColorUtils
|
||||||
@@ -106,8 +107,8 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
color = habit.color
|
color = habit.color
|
||||||
freqNum = habit.frequency.numerator
|
freqNum = habit.frequency.numerator
|
||||||
freqDen = habit.frequency.denominator
|
freqDen = habit.frequency.denominator
|
||||||
isSkipDays = habit.skipDays
|
isSkipDays = habit.skipDays.isSkipDays
|
||||||
listSkipDays = habit.skipDaysList
|
listSkipDays = habit.skipDays.days
|
||||||
targetType = habit.targetType
|
targetType = habit.targetType
|
||||||
habit.reminder?.let {
|
habit.reminder?.let {
|
||||||
reminderHour = it.hour
|
reminderHour = it.hour
|
||||||
@@ -302,8 +303,7 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
habit.frequency = Frequency(freqNum, freqDen)
|
habit.frequency = Frequency(freqNum, freqDen)
|
||||||
habit.skipDays = isSkipDays
|
habit.skipDays = SkipDays(isSkipDays, listSkipDays)
|
||||||
habit.skipDaysList = listSkipDays
|
|
||||||
if (habitType == HabitType.NUMERICAL) {
|
if (habitType == HabitType.NUMERICAL) {
|
||||||
habit.targetValue = binding.targetInput.text.toString().toDouble()
|
habit.targetValue = binding.targetInput.text.toString().toDouble()
|
||||||
habit.targetType = targetType
|
habit.targetType = targetType
|
||||||
@@ -358,6 +358,12 @@ class EditHabitActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun populateSkipDays() {
|
private fun populateSkipDays() {
|
||||||
|
val preferences = (application as HabitsApplication).component.preferences
|
||||||
|
if (preferences.isSkipEnabled || isSkipDays) {
|
||||||
|
binding.skipDaysOuterBox.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.skipDaysOuterBox.visibility = View.GONE
|
||||||
|
}
|
||||||
if (isSkipDays) {
|
if (isSkipDays) {
|
||||||
binding.skipDaysPicker.text = listSkipDays.toFormattedString(this)
|
binding.skipDaysPicker.text = listSkipDays.toFormattedString(this)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
|||||||
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
|
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils
|
import org.isoron.uhabits.utils.InterfaceUtils
|
||||||
import org.isoron.uhabits.utils.formatTime
|
import org.isoron.uhabits.utils.formatTime
|
||||||
|
import org.isoron.uhabits.utils.toFormattedString
|
||||||
|
|
||||||
class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||||
|
|
||||||
@@ -78,6 +79,12 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
|
|||||||
if (state.question.isEmpty()) {
|
if (state.question.isEmpty()) {
|
||||||
binding.questionLabel.visibility = View.GONE
|
binding.questionLabel.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
if (state.skipDays.isSkipDays) {
|
||||||
|
binding.skipLabel.visibility = View.VISIBLE
|
||||||
|
binding.skipLabel.text = context.getString(R.string.skip_day) + " " + state.skipDays.days.toFormattedString(context)
|
||||||
|
} else {
|
||||||
|
binding.skipLabel.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
postInvalidate()
|
postInvalidate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,7 @@
|
|||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
android:id="@+id/skipDaysOuterBox"
|
||||||
style="@style/FormOuterBox"
|
style="@style/FormOuterBox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|||||||
@@ -76,6 +76,17 @@
|
|||||||
android:text="@string/every_day"
|
android:text="@string/every_day"
|
||||||
android:textColor="?attr/contrast60"
|
android:textColor="?attr/contrast60"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textSize="@dimen/smallTextSize" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/skipLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="1dp"
|
||||||
|
android:textColor="?attr/contrast60"
|
||||||
|
android:text=""
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:textSize="@dimen/smallTextSize" />
|
android:textSize="@dimen/smallTextSize" />
|
||||||
|
|
||||||
@@ -95,6 +106,7 @@
|
|||||||
android:textColor="?attr/contrast60"
|
android:textColor="?attr/contrast60"
|
||||||
android:text="8:00 AM"
|
android:text="8:00 AM"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
android:textSize="@dimen/smallTextSize" />
|
android:textSize="@dimen/smallTextSize" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, skip_days integer, skip_days_list integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer )
|
create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer highlight integer, name text, position integer, reminder_hour integer, reminder_min integer )
|
||||||
create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer )
|
create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer )
|
||||||
create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer )
|
create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer )
|
||||||
create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer )
|
create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer )
|
||||||
|
|||||||
2
uhabits-core-legacy/assets/main/migrations/024.sql
Normal file
2
uhabits-core-legacy/assets/main/migrations/024.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
alter table Habits add column skip_days integer not null default 0
|
||||||
|
alter table Habits add column skip_days_list integer not null default 0
|
||||||
@@ -42,8 +42,8 @@ open class EntryList {
|
|||||||
* skip days are enabled and that day is to be skipped
|
* skip days are enabled and that day is to be skipped
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
open fun get(timestamp: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): Entry {
|
open fun get(timestamp: Timestamp, skipDays: SkipDays = SkipDays.NONE): Entry {
|
||||||
return entriesByTimestamp[timestamp] ?: if (skipDays && skipDaysList.isDayTrue(timestamp.weekday)) Entry(timestamp, SKIP) else Entry(timestamp, UNKNOWN)
|
return if (skipDays.isDaySkipped(timestamp)) Entry(timestamp, SKIP) else entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,12 +52,12 @@ open class EntryList {
|
|||||||
* included.
|
* included.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
open fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): List<Entry> {
|
open fun getByInterval(from: Timestamp, to: Timestamp, skipDays: SkipDays = SkipDays.NONE): List<Entry> {
|
||||||
val result = mutableListOf<Entry>()
|
val result = mutableListOf<Entry>()
|
||||||
if (from.isNewerThan(to)) return result
|
if (from.isNewerThan(to)) return result
|
||||||
var current = to
|
var current = to
|
||||||
while (current >= from) {
|
while (current >= from) {
|
||||||
result.add(get(current, skipDays, skipDaysList))
|
result.add(get(current, skipDays))
|
||||||
current = current.minus(1)
|
current = current.minus(1)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -91,16 +91,18 @@ open class EntryList {
|
|||||||
open fun recomputeFrom(
|
open fun recomputeFrom(
|
||||||
originalEntries: EntryList,
|
originalEntries: EntryList,
|
||||||
frequency: Frequency,
|
frequency: Frequency,
|
||||||
isNumerical: Boolean
|
isNumerical: Boolean,
|
||||||
|
skipDays: SkipDays
|
||||||
) {
|
) {
|
||||||
clear()
|
clear()
|
||||||
val original = originalEntries.getKnown()
|
val original = originalEntries.getKnown()
|
||||||
if (isNumerical) {
|
if (isNumerical) {
|
||||||
original.forEach { add(it) }
|
val computed = addEntriesWithSkipDays(original, skipDays)
|
||||||
|
computed.forEach { add(it) }
|
||||||
} else {
|
} else {
|
||||||
val intervals = buildIntervals(frequency, original)
|
val intervals = buildIntervals(frequency, original)
|
||||||
snapIntervalsTogether(intervals)
|
snapIntervalsTogether(intervals)
|
||||||
val computed = buildEntriesFromInterval(original, intervals)
|
val computed = buildEntriesFromInterval(original, intervals, skipDays)
|
||||||
computed.filter { it.value != UNKNOWN || it.notes.isNotEmpty() }.forEach { add(it) }
|
computed.filter { it.value != UNKNOWN || it.notes.isNotEmpty() }.forEach { add(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,10 +131,10 @@ open class EntryList {
|
|||||||
fun computeWeekdayFrequency(isNumerical: Boolean): HashMap<Timestamp, Array<Int>> {
|
fun computeWeekdayFrequency(isNumerical: Boolean): HashMap<Timestamp, Array<Int>> {
|
||||||
val entries = getKnown()
|
val entries = getKnown()
|
||||||
val map = hashMapOf<Timestamp, Array<Int>>()
|
val map = hashMapOf<Timestamp, Array<Int>>()
|
||||||
for ((originalTimestamp, value) in entries) {
|
for ((computedTimestamp, value) in entries) {
|
||||||
val weekday = originalTimestamp.weekday
|
val weekday = computedTimestamp.weekday
|
||||||
val truncatedTimestamp = Timestamp(
|
val truncatedTimestamp = Timestamp(
|
||||||
originalTimestamp.toCalendar().apply {
|
computedTimestamp.toCalendar().apply {
|
||||||
set(Calendar.DAY_OF_MONTH, 1)
|
set(Calendar.DAY_OF_MONTH, 1)
|
||||||
}.timeInMillis
|
}.timeInMillis
|
||||||
)
|
)
|
||||||
@@ -143,7 +145,7 @@ open class EntryList {
|
|||||||
map[truncatedTimestamp] = list
|
map[truncatedTimestamp] = list
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNumerical) {
|
if (isNumerical && value != SKIP) {
|
||||||
list[weekday] += value
|
list[weekday] += value
|
||||||
} else if (value == YES_MANUAL) {
|
} else if (value == YES_MANUAL) {
|
||||||
list[weekday] += 1
|
list[weekday] += 1
|
||||||
@@ -168,7 +170,8 @@ open class EntryList {
|
|||||||
*/
|
*/
|
||||||
fun buildEntriesFromInterval(
|
fun buildEntriesFromInterval(
|
||||||
original: List<Entry>,
|
original: List<Entry>,
|
||||||
intervals: List<Interval>
|
intervals: List<Interval>,
|
||||||
|
skipDays: SkipDays
|
||||||
): List<Entry> {
|
): List<Entry> {
|
||||||
val result = arrayListOf<Entry>()
|
val result = arrayListOf<Entry>()
|
||||||
if (original.isEmpty()) return result
|
if (original.isEmpty()) return result
|
||||||
@@ -197,15 +200,17 @@ open class EntryList {
|
|||||||
current = interval.end
|
current = interval.end
|
||||||
while (current >= interval.begin) {
|
while (current >= interval.begin) {
|
||||||
val offset = current.daysUntil(to)
|
val offset = current.daysUntil(to)
|
||||||
result[offset] = Entry(current, YES_AUTO)
|
result[offset] = if (skipDays.isDaySkipped(current)) Entry(current, SKIP) else Entry(current, YES_AUTO)
|
||||||
current = current.minus(1)
|
current = current.minus(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy original entries
|
// Copy original entries except for the skipped days
|
||||||
original.forEach { entry ->
|
original.forEach { entry ->
|
||||||
val offset = entry.timestamp.daysUntil(to)
|
val offset = entry.timestamp.daysUntil(to)
|
||||||
val value = if (
|
val value = if (skipDays.isDaySkipped(entry)) {
|
||||||
|
SKIP
|
||||||
|
} else if (
|
||||||
result[offset].value == UNKNOWN ||
|
result[offset].value == UNKNOWN ||
|
||||||
entry.value == SKIP ||
|
entry.value == SKIP ||
|
||||||
entry.value == YES_MANUAL
|
entry.value == YES_MANUAL
|
||||||
@@ -272,6 +277,29 @@ open class EntryList {
|
|||||||
}
|
}
|
||||||
return intervals
|
return intervals
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addEntriesWithSkipDays(original: List<Entry>, skipDays: SkipDays): List<Entry> {
|
||||||
|
if (original.isEmpty()) return original
|
||||||
|
val earliest = original.last().timestamp
|
||||||
|
val today = DateUtils.getTodayWithOffset()
|
||||||
|
val computed = mutableListOf<Entry>()
|
||||||
|
var current = today
|
||||||
|
var offset = 0
|
||||||
|
while (current >= earliest) {
|
||||||
|
if (current == original[offset].timestamp) {
|
||||||
|
if (!skipDays.isDaySkipped(current)) {
|
||||||
|
computed.add(original[offset])
|
||||||
|
} else {
|
||||||
|
computed.add(Entry(current, SKIP))
|
||||||
|
}
|
||||||
|
offset++
|
||||||
|
} else if (skipDays.isDaySkipped(current)) {
|
||||||
|
computed.add(Entry(current, SKIP))
|
||||||
|
}
|
||||||
|
current = current.minus(1)
|
||||||
|
}
|
||||||
|
return computed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,10 +352,12 @@ fun List<Entry>.groupedSum(
|
|||||||
*/
|
*/
|
||||||
fun List<Entry>.countSkippedDays(
|
fun List<Entry>.countSkippedDays(
|
||||||
truncateField: DateUtils.TruncateField,
|
truncateField: DateUtils.TruncateField,
|
||||||
firstWeekday: Int = Calendar.SATURDAY
|
firstWeekday: Int = Calendar.SATURDAY,
|
||||||
|
skipDays: SkipDays
|
||||||
): List<Entry> {
|
): List<Entry> {
|
||||||
|
val thisIntervalStart = DateUtils.getTodayWithOffset().truncate(truncateField, firstWeekday)
|
||||||
return this.map { (timestamp, value) ->
|
return this.map { (timestamp, value) ->
|
||||||
if (value == SKIP) {
|
if (value == SKIP || skipDays.isDaySkipped(timestamp)) {
|
||||||
Entry(timestamp, 1)
|
Entry(timestamp, 1)
|
||||||
} else {
|
} else {
|
||||||
Entry(timestamp, 0)
|
Entry(timestamp, 0)
|
||||||
@@ -341,5 +371,5 @@ fun List<Entry>.countSkippedDays(
|
|||||||
Entry(timestamp, entries.sumOf { it.value })
|
Entry(timestamp, entries.sumOf { it.value })
|
||||||
}.sortedBy { (timestamp, _) ->
|
}.sortedBy { (timestamp, _) ->
|
||||||
-timestamp.unixTime
|
-timestamp.unixTime
|
||||||
}
|
}.filter { it.timestamp == thisIntervalStart }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ data class Habit(
|
|||||||
var color: PaletteColor = PaletteColor(8),
|
var color: PaletteColor = PaletteColor(8),
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
var frequency: Frequency = Frequency.DAILY,
|
var frequency: Frequency = Frequency.DAILY,
|
||||||
var skipDays: Boolean = false,
|
var skipDays: SkipDays = SkipDays.NONE,
|
||||||
var skipDaysList: WeekdayList = WeekdayList.NO_DAY,
|
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
var isArchived: Boolean = false,
|
var isArchived: Boolean = false,
|
||||||
var name: String = "",
|
var name: String = "",
|
||||||
@@ -80,7 +79,8 @@ data class Habit(
|
|||||||
computedEntries.recomputeFrom(
|
computedEntries.recomputeFrom(
|
||||||
originalEntries = originalEntries,
|
originalEntries = originalEntries,
|
||||||
frequency = frequency,
|
frequency = frequency,
|
||||||
isNumerical = isNumerical
|
isNumerical = isNumerical,
|
||||||
|
skipDays = skipDays
|
||||||
)
|
)
|
||||||
|
|
||||||
val today = DateUtils.getTodayWithOffset()
|
val today = DateUtils.getTodayWithOffset()
|
||||||
@@ -93,7 +93,6 @@ data class Habit(
|
|||||||
frequency = frequency,
|
frequency = frequency,
|
||||||
isNumerical = isNumerical,
|
isNumerical = isNumerical,
|
||||||
skipDays = skipDays,
|
skipDays = skipDays,
|
||||||
skipDaysList = skipDaysList,
|
|
||||||
numericalHabitType = targetType,
|
numericalHabitType = targetType,
|
||||||
targetValue = targetValue,
|
targetValue = targetValue,
|
||||||
computedEntries = computedEntries,
|
computedEntries = computedEntries,
|
||||||
@@ -113,7 +112,6 @@ data class Habit(
|
|||||||
this.description = other.description
|
this.description = other.description
|
||||||
this.frequency = other.frequency
|
this.frequency = other.frequency
|
||||||
this.skipDays = other.skipDays
|
this.skipDays = other.skipDays
|
||||||
this.skipDaysList = other.skipDaysList
|
|
||||||
// this.id should not be copied
|
// this.id should not be copied
|
||||||
this.isArchived = other.isArchived
|
this.isArchived = other.isArchived
|
||||||
this.name = other.name
|
this.name = other.name
|
||||||
@@ -135,7 +133,6 @@ data class Habit(
|
|||||||
if (description != other.description) return false
|
if (description != other.description) return false
|
||||||
if (frequency != other.frequency) return false
|
if (frequency != other.frequency) return false
|
||||||
if (skipDays != other.skipDays) return false
|
if (skipDays != other.skipDays) return false
|
||||||
if (skipDaysList != other.skipDaysList) return false
|
|
||||||
if (id != other.id) return false
|
if (id != other.id) return false
|
||||||
if (isArchived != other.isArchived) return false
|
if (isArchived != other.isArchived) return false
|
||||||
if (name != other.name) return false
|
if (name != other.name) return false
|
||||||
@@ -155,7 +152,7 @@ data class Habit(
|
|||||||
var result = color.hashCode()
|
var result = color.hashCode()
|
||||||
result = 31 * result + description.hashCode()
|
result = 31 * result + description.hashCode()
|
||||||
result = 31 * result + frequency.hashCode()
|
result = 31 * result + frequency.hashCode()
|
||||||
result = 31 * result + skipDaysList.hashCode()
|
result = 31 * result + skipDays.hashCode()
|
||||||
result = 31 * result + (id?.hashCode() ?: 0)
|
result = 31 * result + (id?.hashCode() ?: 0)
|
||||||
result = 31 * result + isArchived.hashCode()
|
result = 31 * result + isArchived.hashCode()
|
||||||
result = 31 * result + name.hashCode()
|
result = 31 * result + name.hashCode()
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ class ScoreList {
|
|||||||
fun recompute(
|
fun recompute(
|
||||||
frequency: Frequency,
|
frequency: Frequency,
|
||||||
isNumerical: Boolean,
|
isNumerical: Boolean,
|
||||||
skipDays: Boolean,
|
skipDays: SkipDays,
|
||||||
skipDaysList: WeekdayList,
|
|
||||||
numericalHabitType: NumericalHabitType,
|
numericalHabitType: NumericalHabitType,
|
||||||
targetValue: Double,
|
targetValue: Double,
|
||||||
computedEntries: EntryList,
|
computedEntries: EntryList,
|
||||||
@@ -81,7 +80,7 @@ class ScoreList {
|
|||||||
var numerator = frequency.numerator
|
var numerator = frequency.numerator
|
||||||
var denominator = frequency.denominator
|
var denominator = frequency.denominator
|
||||||
val freq = frequency.toDouble()
|
val freq = frequency.toDouble()
|
||||||
val values = computedEntries.getByInterval(from, to, skipDays, skipDaysList).map { it.value }.toIntArray()
|
val values = computedEntries.getByInterval(from, to, skipDays).map { it.value }.toIntArray()
|
||||||
val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST
|
val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST
|
||||||
|
|
||||||
// For non-daily boolean habits, we double the numerator and the denominator to smooth
|
// For non-daily boolean habits, we double the numerator and the denominator to smooth
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* 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.core.models
|
||||||
|
|
||||||
|
data class SkipDays(
|
||||||
|
val isSkipDays: Boolean,
|
||||||
|
val days: WeekdayList
|
||||||
|
) {
|
||||||
|
fun isDaySkipped(day: Int): Boolean {
|
||||||
|
return isSkipDays && days.isDayTrue(day)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDaySkipped(day: Timestamp): Boolean {
|
||||||
|
return isSkipDays && days.isDayTrue(day.weekday)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDaySkipped(entry: Entry): Boolean {
|
||||||
|
return isSkipDays && days.isDayTrue(entry.timestamp.weekday)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val NONE = SkipDays(false, WeekdayList(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import org.isoron.uhabits.core.database.Repository
|
|||||||
import org.isoron.uhabits.core.models.Entry
|
import org.isoron.uhabits.core.models.Entry
|
||||||
import org.isoron.uhabits.core.models.EntryList
|
import org.isoron.uhabits.core.models.EntryList
|
||||||
import org.isoron.uhabits.core.models.Frequency
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
|
import org.isoron.uhabits.core.models.SkipDays
|
||||||
import org.isoron.uhabits.core.models.Timestamp
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
import org.isoron.uhabits.core.models.WeekdayList
|
import org.isoron.uhabits.core.models.WeekdayList
|
||||||
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
|
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
|
||||||
@@ -44,13 +45,13 @@ class SQLiteEntryList(database: Database) : EntryList() {
|
|||||||
isLoaded = true
|
isLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(timestamp: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): Entry {
|
override fun get(timestamp: Timestamp, skipDays: SkipDays): Entry {
|
||||||
loadRecords()
|
loadRecords()
|
||||||
return super.get(timestamp, skipDays, skipDaysList)
|
return super.get(timestamp, skipDays)
|
||||||
}
|
}
|
||||||
override fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): List<Entry> {
|
override fun getByInterval(from: Timestamp, to: Timestamp, skipDays: SkipDays): List<Entry> {
|
||||||
loadRecords()
|
loadRecords()
|
||||||
return super.getByInterval(from, to, skipDays, skipDaysList)
|
return super.getByInterval(from, to, skipDays)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(entry: Entry) {
|
override fun add(entry: Entry) {
|
||||||
@@ -78,7 +79,7 @@ class SQLiteEntryList(database: Database) : EntryList() {
|
|||||||
return super.getKnown()
|
return super.getKnown()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) {
|
override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean, skipDays: SkipDays) {
|
||||||
throw UnsupportedOperationException()
|
throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.isoron.uhabits.core.models.HabitType
|
|||||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.models.Reminder
|
import org.isoron.uhabits.core.models.Reminder
|
||||||
|
import org.isoron.uhabits.core.models.SkipDays
|
||||||
import org.isoron.uhabits.core.models.WeekdayList
|
import org.isoron.uhabits.core.models.WeekdayList
|
||||||
import java.util.Objects.requireNonNull
|
import java.util.Objects.requireNonNull
|
||||||
|
|
||||||
@@ -111,8 +112,8 @@ class HabitRecord {
|
|||||||
val (numerator, denominator) = model.frequency
|
val (numerator, denominator) = model.frequency
|
||||||
freqNum = numerator
|
freqNum = numerator
|
||||||
freqDen = denominator
|
freqDen = denominator
|
||||||
skipDays = if (model.skipDays) 1 else 0
|
skipDays = if (model.skipDays.isSkipDays) 1 else 0
|
||||||
skipDaysList = model.skipDaysList.toInteger()
|
skipDaysList = model.skipDays.days.toInteger()
|
||||||
reminderDays = 0
|
reminderDays = 0
|
||||||
reminderMin = null
|
reminderMin = null
|
||||||
reminderHour = null
|
reminderHour = null
|
||||||
@@ -130,8 +131,7 @@ class HabitRecord {
|
|||||||
habit.description = description!!
|
habit.description = description!!
|
||||||
habit.question = question!!
|
habit.question = question!!
|
||||||
habit.frequency = Frequency(freqNum!!, freqDen!!)
|
habit.frequency = Frequency(freqNum!!, freqDen!!)
|
||||||
habit.skipDays = (skipDays!! == 1)
|
habit.skipDays = SkipDays(skipDays!! == 1, WeekdayList(skipDaysList!!))
|
||||||
habit.skipDaysList = WeekdayList(skipDaysList!!)
|
|
||||||
habit.color = PaletteColor(color!!)
|
habit.color = PaletteColor(color!!)
|
||||||
habit.isArchived = archived != 0
|
habit.isArchived = archived != 0
|
||||||
habit.type = HabitType.fromInt(type!!)
|
habit.type = HabitType.fromInt(type!!)
|
||||||
|
|||||||
@@ -312,8 +312,7 @@ class HabitCardListCache @Inject constructor(
|
|||||||
val list: MutableList<Int> = ArrayList()
|
val list: MutableList<Int> = ArrayList()
|
||||||
val notes: MutableList<String> = ArrayList()
|
val notes: MutableList<String> = ArrayList()
|
||||||
val skipDays = habit.skipDays
|
val skipDays = habit.skipDays
|
||||||
val skipDaysList = habit.skipDaysList
|
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today, skipDays)) {
|
||||||
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today, skipDays, skipDaysList)) {
|
|
||||||
list.add(value)
|
list.add(value)
|
||||||
notes.add(note)
|
notes.add(note)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list
|
|||||||
|
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
||||||
|
import org.isoron.uhabits.core.models.Entry
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
import org.isoron.uhabits.core.models.HabitList
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
@@ -53,10 +54,11 @@ open class ListHabitsBehavior @Inject constructor(
|
|||||||
|
|
||||||
fun onEdit(habit: Habit, timestamp: Timestamp?) {
|
fun onEdit(habit: Habit, timestamp: Timestamp?) {
|
||||||
val entry = habit.computedEntries.get(timestamp!!)
|
val entry = habit.computedEntries.get(timestamp!!)
|
||||||
|
if (habit.skipDays.isDaySkipped(timestamp)) return
|
||||||
if (habit.type == HabitType.NUMERICAL) {
|
if (habit.type == HabitType.NUMERICAL) {
|
||||||
val oldValue = entry.value.toDouble() / 1000
|
val oldValue = entry.value.toDouble() / 1000
|
||||||
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float ->
|
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float ->
|
||||||
val value = if (habit.skipDays && habit.skipDaysList.isDayTrue(timestamp.weekday)) 3 else (newValue * 1000).roundToInt()
|
val value = if (habit.skipDays.isDaySkipped(timestamp)) Entry.SKIP else (newValue * 1000).roundToInt()
|
||||||
|
|
||||||
if (newValue != oldValue) {
|
if (newValue != oldValue) {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class FrequencyCardPresenter {
|
|||||||
) = FrequencyCardState(
|
) = FrequencyCardState(
|
||||||
color = habit.color,
|
color = habit.color,
|
||||||
isNumerical = habit.isNumerical,
|
isNumerical = habit.isNumerical,
|
||||||
frequency = habit.originalEntries.computeWeekdayFrequency(
|
frequency = habit.computedEntries.computeWeekdayFrequency(
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
),
|
),
|
||||||
firstWeekday = firstWeekday,
|
firstWeekday = firstWeekday,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class HistoryCardPresenter(
|
|||||||
override fun onDateLongPress(date: LocalDate) {
|
override fun onDateLongPress(date: LocalDate) {
|
||||||
val timestamp = Timestamp.fromLocalDate(date)
|
val timestamp = Timestamp.fromLocalDate(date)
|
||||||
screen.showFeedback()
|
screen.showFeedback()
|
||||||
|
if (habit.skipDays.isDaySkipped(timestamp)) return
|
||||||
if (habit.isNumerical) {
|
if (habit.isNumerical) {
|
||||||
showNumberPopup(timestamp)
|
showNumberPopup(timestamp)
|
||||||
} else {
|
} else {
|
||||||
@@ -81,6 +82,7 @@ class HistoryCardPresenter(
|
|||||||
override fun onDateShortPress(date: LocalDate) {
|
override fun onDateShortPress(date: LocalDate) {
|
||||||
val timestamp = Timestamp.fromLocalDate(date)
|
val timestamp = Timestamp.fromLocalDate(date)
|
||||||
screen.showFeedback()
|
screen.showFeedback()
|
||||||
|
if (habit.skipDays.isDaySkipped(timestamp)) return
|
||||||
if (habit.isNumerical) {
|
if (habit.isNumerical) {
|
||||||
showNumberPopup(timestamp)
|
showNumberPopup(timestamp)
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +163,7 @@ class HistoryCardPresenter(
|
|||||||
): HistoryCardState {
|
): HistoryCardState {
|
||||||
val today = DateUtils.getTodayWithOffset()
|
val today = DateUtils.getTodayWithOffset()
|
||||||
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
||||||
val entries = habit.computedEntries.getByInterval(oldest, today)
|
val entries = habit.computedEntries.getByInterval(oldest, today, habit.skipDays)
|
||||||
val series = if (habit.isNumerical) {
|
val series = if (habit.isNumerical) {
|
||||||
entries.map {
|
entries.map {
|
||||||
when {
|
when {
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ import org.isoron.uhabits.core.models.Habit
|
|||||||
import org.isoron.uhabits.core.models.NumericalHabitType
|
import org.isoron.uhabits.core.models.NumericalHabitType
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
import org.isoron.uhabits.core.models.Reminder
|
import org.isoron.uhabits.core.models.Reminder
|
||||||
|
import org.isoron.uhabits.core.models.SkipDays
|
||||||
import org.isoron.uhabits.core.ui.views.Theme
|
import org.isoron.uhabits.core.ui.views.Theme
|
||||||
|
|
||||||
data class SubtitleCardState(
|
data class SubtitleCardState(
|
||||||
val color: PaletteColor,
|
val color: PaletteColor,
|
||||||
val frequency: Frequency,
|
val frequency: Frequency,
|
||||||
val isNumerical: Boolean,
|
val isNumerical: Boolean,
|
||||||
|
val skipDays: SkipDays,
|
||||||
val question: String,
|
val question: String,
|
||||||
val reminder: Reminder?,
|
val reminder: Reminder?,
|
||||||
val targetValue: Double = 0.0,
|
val targetValue: Double = 0.0,
|
||||||
@@ -47,6 +49,7 @@ class SubtitleCardPresenter {
|
|||||||
color = habit.color,
|
color = habit.color,
|
||||||
frequency = habit.frequency,
|
frequency = habit.frequency,
|
||||||
isNumerical = habit.isNumerical,
|
isNumerical = habit.isNumerical,
|
||||||
|
skipDays = habit.skipDays,
|
||||||
question = habit.question,
|
question = habit.question,
|
||||||
reminder = habit.reminder,
|
reminder = habit.reminder,
|
||||||
targetValue = habit.targetValue,
|
targetValue = habit.targetValue,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views
|
|||||||
|
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
import org.isoron.uhabits.core.models.PaletteColor
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
import org.isoron.uhabits.core.models.countSkippedDays
|
import org.isoron.uhabits.core.models.countSkippedDays
|
||||||
import org.isoron.uhabits.core.models.groupedSum
|
import org.isoron.uhabits.core.models.groupedSum
|
||||||
import org.isoron.uhabits.core.ui.views.Theme
|
import org.isoron.uhabits.core.ui.views.Theme
|
||||||
@@ -45,54 +46,61 @@ class TargetCardPresenter {
|
|||||||
theme: Theme
|
theme: Theme
|
||||||
): TargetCardState {
|
): TargetCardState {
|
||||||
val today = DateUtils.getTodayWithOffset()
|
val today = DateUtils.getTodayWithOffset()
|
||||||
|
val (yearBegin, yearEnd) = getYearRange(firstWeekday)
|
||||||
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
||||||
val entries = habit.computedEntries.getByInterval(oldest, today)
|
val entriesForSkip = habit.computedEntries.getByInterval(yearBegin, yearEnd, habit.skipDays)
|
||||||
|
val entriesForSum = habit.computedEntries.getByInterval(oldest, today)
|
||||||
|
|
||||||
val valueToday = entries.groupedSum(
|
val valueToday = entriesForSum.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.DAY,
|
truncateField = DateUtils.TruncateField.DAY,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val skippedDayToday = entries.countSkippedDays(
|
val skippedDayToday = entriesForSkip.countSkippedDays(
|
||||||
truncateField = DateUtils.TruncateField.DAY
|
truncateField = DateUtils.TruncateField.DAY,
|
||||||
|
skipDays = habit.skipDays,
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisWeek = entries.groupedSum(
|
val valueThisWeek = entriesForSum.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
||||||
firstWeekday = firstWeekday,
|
firstWeekday = firstWeekday,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val skippedDaysThisWeek = entries.countSkippedDays(
|
val skippedDaysThisWeek = entriesForSkip.countSkippedDays(
|
||||||
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
|
||||||
firstWeekday = firstWeekday
|
firstWeekday = firstWeekday,
|
||||||
|
skipDays = habit.skipDays,
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisMonth = entries.groupedSum(
|
val valueThisMonth = entriesForSum.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.MONTH,
|
truncateField = DateUtils.TruncateField.MONTH,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val skippedDaysThisMonth = entries.countSkippedDays(
|
val skippedDaysThisMonth = entriesForSkip.countSkippedDays(
|
||||||
truncateField = DateUtils.TruncateField.MONTH
|
truncateField = DateUtils.TruncateField.MONTH,
|
||||||
|
skipDays = habit.skipDays,
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisQuarter = entries.groupedSum(
|
val valueThisQuarter = entriesForSum.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.QUARTER,
|
truncateField = DateUtils.TruncateField.QUARTER,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val skippedDaysThisQuarter = entries.countSkippedDays(
|
val skippedDaysThisQuarter = entriesForSkip.countSkippedDays(
|
||||||
truncateField = DateUtils.TruncateField.QUARTER
|
truncateField = DateUtils.TruncateField.QUARTER,
|
||||||
|
skipDays = habit.skipDays,
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val valueThisYear = entries.groupedSum(
|
val valueThisYear = entriesForSum.groupedSum(
|
||||||
truncateField = DateUtils.TruncateField.YEAR,
|
truncateField = DateUtils.TruncateField.YEAR,
|
||||||
isNumerical = habit.isNumerical
|
isNumerical = habit.isNumerical
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val skippedDaysThisYear = entries.countSkippedDays(
|
val skippedDaysThisYear = entriesForSkip.countSkippedDays(
|
||||||
truncateField = DateUtils.TruncateField.YEAR
|
truncateField = DateUtils.TruncateField.YEAR,
|
||||||
|
skipDays = habit.skipDays,
|
||||||
).firstOrNull()?.value ?: 0
|
).firstOrNull()?.value ?: 0
|
||||||
|
|
||||||
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
|
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
|
||||||
@@ -165,5 +173,16 @@ class TargetCardPresenter {
|
|||||||
theme = theme
|
theme = theme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getYearRange(firstWeekday: Int): Pair<Timestamp, Timestamp> {
|
||||||
|
val today = DateUtils.getTodayWithOffset()
|
||||||
|
val yearBegin = today.truncate(DateUtils.TruncateField.YEAR, firstWeekday)
|
||||||
|
val cali = yearBegin.toCalendar()
|
||||||
|
cali.add(Calendar.YEAR, 1)
|
||||||
|
var newest = Timestamp(cali)
|
||||||
|
val thisWeek = today.truncate(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday)
|
||||||
|
if (thisWeek.daysUntil(newest) < 7) newest = thisWeek.plus(7)
|
||||||
|
return Pair(yearBegin, newest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ create table Habits (
|
|||||||
description text,
|
description text,
|
||||||
freq_den integer,
|
freq_den integer,
|
||||||
freq_num integer,
|
freq_num integer,
|
||||||
skip_days integer,
|
|
||||||
skip_days_list integer,
|
|
||||||
highlight integer,
|
highlight integer,
|
||||||
name text,
|
name text,
|
||||||
position integer,
|
position integer,
|
||||||
|
|||||||
2
uhabits-core/src/jvmMain/resources/migrations/26.sql
Normal file
2
uhabits-core/src/jvmMain/resources/migrations/26.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
alter table Habits add column skip_days integer not null default 0;
|
||||||
|
alter table Habits add column skip_days_list integer not null default 0;
|
||||||
Reference in New Issue
Block a user