Create HabitGroup and HabitGroup list classes

pull/2020/head
Dharanish 1 year ago
parent 053bfe116a
commit 340bde9f69

@ -107,6 +107,10 @@ data class Habit(
) )
} }
fun firstEntryDate(): Timestamp {
return computedEntries.getKnown().lastOrNull()?.timestamp ?: DateUtils.getTodayWithOffset()
}
fun copyFrom(other: Habit) { fun copyFrom(other: Habit) {
this.color = other.color this.color = other.color
this.description = other.description this.description = other.description

@ -0,0 +1,123 @@
package org.isoron.uhabits.core.models
import org.isoron.uhabits.core.utils.DateUtils
import java.util.UUID
data class HabitGroup(
var color: PaletteColor = PaletteColor(8),
var description: String = "",
var id: Long? = null,
var isArchived: Boolean = false,
var name: String = "",
var position: Int = 0,
var question: String = "",
var reminder: Reminder? = null,
var unit: String = "",
var uuid: String? = null,
var habitList: HabitList,
var habitGroupList: HabitGroupList,
val scores: ScoreList,
val streaks: StreakList
) {
init {
if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "")
}
var observable = ModelObservable()
val uriString: String
get() = "content://org.isoron.uhabits/habit/$id"
fun hasReminder(): Boolean = reminder != null
fun isCompletedToday(): Boolean {
return habitList.all { it.isCompletedToday() } && habitGroupList.all { it.isCompletedToday() }
}
fun isEnteredToday(): Boolean {
return habitList.all { it.isEnteredToday() } && habitGroupList.all { it.isEnteredToday() }
}
fun firstEntryDate(): Timestamp {
val today = DateUtils.getTodayWithOffset()
var earliest = today
for (h in habitList) {
val first = h.firstEntryDate()
if (earliest.isNewerThan(first)) earliest = first
}
for (hgr in habitGroupList) {
val first = hgr.firstEntryDate()
if (earliest.isNewerThan(first)) earliest = first
}
return earliest
}
fun recompute() {
for (h in habitList) h.recompute()
for (hgr in habitGroupList) hgr.recompute()
val today = DateUtils.getTodayWithOffset()
val to = today.plus(30)
var from = firstEntryDate()
if (from.isNewerThan(to)) from = to
scores.combineFrom(
habitList = habitList,
habitGroupList = habitGroupList,
from = from,
to = to
)
streaks.combineFrom(
habitList = habitList,
habitGroupList = habitGroupList,
from = from,
to = to
)
}
fun copyFrom(other: Habit) {
this.color = other.color
this.description = other.description
// this.id should not be copied
this.isArchived = other.isArchived
this.name = other.name
this.position = other.position
this.question = other.question
this.reminder = other.reminder
this.unit = other.unit
this.uuid = other.uuid
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Habit) return false
if (color != other.color) return false
if (description != other.description) return false
if (id != other.id) return false
if (isArchived != other.isArchived) return false
if (name != other.name) return false
if (position != other.position) return false
if (question != other.question) return false
if (reminder != other.reminder) return false
if (unit != other.unit) return false
if (uuid != other.uuid) return false
return true
}
override fun hashCode(): Int {
var result = color.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + isArchived.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + position
result = 31 * result + question.hashCode()
result = 31 * result + (reminder?.hashCode() ?: 0)
result = 31 * result + unit.hashCode()
result = 31 * result + (uuid?.hashCode() ?: 0)
return result
}
}

@ -0,0 +1,200 @@
package org.isoron.uhabits.core.models
import com.opencsv.CSVWriter
import java.io.IOException
import java.io.Writer
import java.util.LinkedList
import javax.annotation.concurrent.ThreadSafe
/**
* An ordered collection of [HabitGroup]s.
*/
@ThreadSafe
abstract class HabitGroupList : Iterable<HabitGroup> {
val observable: ModelObservable
@JvmField
protected val filter: HabitMatcher
/**
* Creates a new HabitList.
*
* Depending on the implementation, this list can either be empty or be
* populated by some pre-existing habits, for example, from a certain
* database.
*/
constructor() {
observable = ModelObservable()
filter = HabitMatcher(isArchivedAllowed = true)
}
protected constructor(filter: HabitMatcher) {
observable = ModelObservable()
this.filter = filter
}
/**
* Inserts a new habit in the list.
*
* If the id of the habit is null, the list will assign it a new id, which
* is guaranteed to be unique in the scope of the list. If id is not null,
* the caller should make sure that the list does not already contain
* another habit with same id, otherwise a RuntimeException will be thrown.
*
* @param habitGroup the habit to be inserted
* @throws IllegalArgumentException if the habit is already on the list.
*/
@Throws(IllegalArgumentException::class)
abstract fun add(habitGroup: HabitGroup)
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
*/
abstract fun getById(id: Long): HabitGroup?
/**
* Returns the habit with specified UUID.
*
* @param uuid the UUID of the habit
* @return the habit, or null if none exist
*/
abstract fun getByUUID(uuid: String?): HabitGroup?
/**
* Returns the habit that occupies a certain position.
*
* @param position the position of the desired habit
* @return the habit at that position
* @throws IndexOutOfBoundsException when the position is invalid
*/
abstract fun getByPosition(position: Int): HabitGroup
/**
* Returns the list of habits that match a given condition.
*
* @param matcher the matcher that checks the condition
* @return the list of matching habits
*/
abstract fun getFiltered(matcher: HabitMatcher?): HabitGroupList
abstract var primaryOrder: Order
abstract var secondaryOrder: Order
/**
* Returns the index of the given habit in the list, or -1 if the list does
* not contain the habit.
*
* @param h the habit
* @return the index of the habit, or -1 if not in the list
*/
abstract fun indexOf(h: HabitGroup): Int
val isEmpty: Boolean
get() = size() == 0
/**
* Removes the given habit from the list.
*
* If the given habit is not in the list, does nothing.
*
* @param h the habit to be removed.
*/
abstract fun remove(h: HabitGroup)
/**
* Removes all the habits from the list.
*/
open fun removeAll() {
val copy: MutableList<HabitGroup> = LinkedList()
for (h in this) copy.add(h)
for (h in copy) remove(h)
observable.notifyListeners()
}
/**
* Changes the position of a habit in the list.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
abstract fun reorder(from: HabitGroup, to: HabitGroup)
open fun repair() {}
/**
* Returns the number of habits in this list.
*
* @return number of habits
*/
abstract fun size(): Int
/**
* Notifies the list that a certain list of habits has been modified.
*
* Depending on the implementation, this operation might trigger a write to
* disk, or do nothing at all. To make sure that the habits get persisted,
* this operation must be called.
*
* @param habitGroups the list of habits that have been modified.
*/
abstract fun update(habitGroups: List<HabitGroup>)
/**
* Notifies the list that a certain habit has been modified.
*
* See [.update] for more details.
*
* @param habitGroup the habit that has been modified.
*/
fun update(habitGroup: HabitGroup) {
update(listOf(habitGroup))
}
/**
* Writes the list of habits to the given writer, in CSV format. There is
* one line for each habit, containing the fields name, description,
* frequency numerator, frequency denominator and color. The color is
* written in HTML format (#000000).
*
* @param out the writer that will receive the result
* @throws IOException if write operations fail
*/
@Throws(IOException::class)
fun writeCSV(out: Writer) {
val header = arrayOf(
"Position",
"Name",
"Question",
"Description",
"NumRepetitions",
"Interval",
"Color"
)
val csv = CSVWriter(out)
csv.writeNext(header, false)
for (habit in this) {
val cols = arrayOf(
String.format("%03d", indexOf(habit) + 1),
habit.name,
habit.question,
habit.description,
habit.color.toCsvColor()
)
csv.writeNext(cols, false)
}
csv.close()
}
abstract fun resort()
enum class Order {
BY_NAME_ASC,
BY_NAME_DESC,
BY_COLOR_ASC,
BY_COLOR_DESC,
BY_SCORE_ASC,
BY_SCORE_DESC,
BY_STATUS_ASC,
BY_STATUS_DESC,
BY_POSITION
}
}

@ -19,8 +19,6 @@
package org.isoron.uhabits.core.models package org.isoron.uhabits.core.models
import org.isoron.uhabits.core.models.Score.Companion.compute import org.isoron.uhabits.core.models.Score.Companion.compute
import java.util.ArrayList
import java.util.HashMap
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -139,4 +137,21 @@ class ScoreList {
map[timestamp] = Score(timestamp, previousValue) map[timestamp] = Score(timestamp, previousValue)
} }
} }
@Synchronized
fun combineFrom(
habitList: HabitList,
habitGroupList: HabitGroupList,
from: Timestamp,
to: Timestamp
) {
var current = to
while (current >= from) {
val habitScores = habitList.map { it.scores[current].value }
val groupScores = habitGroupList.map { it.scores[current].value }
val averageScore = (habitScores + groupScores).average()
map[current] = Score(current, averageScore)
current = current.minus(1)
}
}
} }

@ -38,4 +38,8 @@ data class Streak(
val length: Int val length: Int
get() = start.daysUntil(end) + 1 get() = start.daysUntil(end) + 1
fun isInStreak(timestamp: Timestamp): Boolean {
return timestamp in start..end
}
} }

@ -62,4 +62,35 @@ class StreakList {
} }
list.add(Streak(begin, end)) list.add(Streak(begin, end))
} }
@Synchronized
fun isInStreaks(timestamp: Timestamp): Boolean {
return list.any { it.isInStreak(timestamp) }
}
@Synchronized
fun combineFrom(
habitList: HabitList,
habitGroupList: HabitGroupList,
from: Timestamp,
to: Timestamp
) {
var current = from
var streakRunning = false
var streakStart = from
while (current <= to) {
if (habitList.all { it.streaks.isInStreaks(current) } &&
habitGroupList.all { it.streaks.isInStreaks(current) } &&
!streakRunning
) {
streakStart = current
streakRunning = true
} else if (streakRunning) {
val streakEnd = current.minus(1)
list.add(Streak(streakStart, streakEnd))
streakRunning = false
}
current = current.plus(1)
}
}
} }

Loading…
Cancel
Save