diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt index cf472122a..bbe27fd3b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ModelObservable.kt @@ -68,7 +68,7 @@ class ModelObservable { * Interface implemented by objects that want to be notified when the model * changes. */ - interface Listener { + fun interface Listener { /** * Called whenever the model associated to this observable has been * modified. diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java deleted file mode 100644 index b755c7588..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * 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 . - */ - -package org.isoron.uhabits.core.models.memory; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -import static org.isoron.uhabits.core.models.HabitList.Order.*; - -/** - * In-memory implementation of {@link HabitList}. - */ -public class MemoryHabitList extends HabitList -{ - @NonNull - private LinkedList list = new LinkedList<>(); - - @NonNull - private Order primaryOrder = Order.BY_POSITION; - - @NonNull - private Order secondaryOrder = Order.BY_NAME_ASC; - - private Comparator comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder); - - @Nullable - private MemoryHabitList parent = null; - - public MemoryHabitList() - { - super(); - } - - protected MemoryHabitList(@NonNull HabitMatcher matcher, - Comparator comparator, - @NonNull MemoryHabitList parent) - { - super(matcher); - this.parent = parent; - this.comparator = comparator; - this.primaryOrder = parent.primaryOrder; - this.secondaryOrder = parent.secondaryOrder; - parent.getObservable().addListener(this::loadFromParent); - loadFromParent(); - } - - @Override - public synchronized void add(@NonNull Habit habit) - throws IllegalArgumentException - { - throwIfHasParent(); - if (list.contains(habit)) - throw new IllegalArgumentException("habit already added"); - - Long id = habit.getId(); - if (id != null && getById(id) != null) - throw new RuntimeException("duplicate id"); - - if (id == null) habit.setId((long) list.size()); - list.addLast(habit); - resort(); - } - - @Override - public synchronized Habit getById(long id) - { - for (Habit h : list) - { - if (h.getId() == null) throw new IllegalStateException(); - if (h.getId() == id) return h; - } - return null; - } - - @Override - public synchronized Habit getByUUID(String uuid) - { - for (Habit h : list) if (Objects.requireNonNull(h.getUuid()).equals(uuid)) return h; - return null; - } - - @NonNull - @Override - public synchronized Habit getByPosition(int position) - { - return list.get(position); - } - - @NonNull - @Override - public synchronized HabitList getFiltered(HabitMatcher matcher) - { - return new MemoryHabitList(matcher, comparator, this); - } - - @NotNull - @Override - public synchronized Order getPrimaryOrder() - { - return primaryOrder; - } - - @NotNull - @Override - public synchronized Order getSecondaryOrder() - { - return secondaryOrder; - } - - @Override - public synchronized void setPrimaryOrder(@NonNull Order order) - { - this.primaryOrder = order; - this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder); - resort(); - } - - @Override - public void setSecondaryOrder(@NonNull Order order) - { - this.secondaryOrder = order; - this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder); - resort(); - } - - private Comparator getComposedComparatorByOrder(Order firstOrder, Order secondOrder) - { - return (h1, h2) -> { - int firstResult = getComparatorByOrder(firstOrder).compare(h1, h2); - - if (firstResult != 0 || secondOrder == null) { - return firstResult; - } - - return getComparatorByOrder(secondOrder).compare(h1, h2); - }; - } - - private Comparator getComparatorByOrder(Order order) { - Comparator nameComparatorAsc = (h1, h2) -> - h1.getName().compareTo(h2.getName()); - - Comparator nameComparatorDesc = (h1, h2) -> - nameComparatorAsc.compare(h2, h1); - - Comparator colorComparatorAsc = (h1, h2) -> - h1.getColor().compareTo(h2.getColor()); - - Comparator colorComparatorDesc = (h1, h2) -> - colorComparatorAsc.compare(h2, h1); - - Comparator scoreComparatorDesc = (h1, h2) -> - { - Timestamp today = DateUtils.getTodayWithOffset(); - return Double.compare( - h1.getScores().get(today).getValue(), - h2.getScores().get(today).getValue()); - }; - - Comparator scoreComparatorAsc = (h1, h2) -> - scoreComparatorDesc.compare(h2, h1); - - Comparator positionComparator = (h1, h2) -> - Integer.compare(h1.getPosition(), h2.getPosition()); - - Comparator statusComparatorDesc = (h1, h2) -> - { - if (h1.isCompletedToday() != h2.isCompletedToday()) { - return h1.isCompletedToday() ? -1 : 1; - } - - if (h1.isNumerical() != h2.isNumerical()) { - return h1.isNumerical() ? -1 : 1; - } - - Timestamp today = DateUtils.getTodayWithOffset(); - Integer v1 = h1.getComputedEntries().get(today).getValue(); - Integer v2 = h2.getComputedEntries().get(today).getValue(); - - return v2.compareTo(v1); - }; - - Comparator statusComparatorAsc = (h1, h2) -> statusComparatorDesc.compare(h2, h1); - - if (order == BY_POSITION) return positionComparator; - if (order == BY_NAME_ASC) return nameComparatorAsc; - if (order == BY_NAME_DESC) return nameComparatorDesc; - if (order == BY_COLOR_ASC) return colorComparatorAsc; - if (order == BY_COLOR_DESC) return colorComparatorDesc; - if (order == BY_SCORE_DESC) return scoreComparatorDesc; - if (order == BY_SCORE_ASC) return scoreComparatorAsc; - if (order == BY_STATUS_DESC) return statusComparatorDesc; - if (order == BY_STATUS_ASC) return statusComparatorAsc; - throw new IllegalStateException(); - } - - @Override - public synchronized int indexOf(@NonNull Habit h) - { - return list.indexOf(h); - } - - @NonNull - @Override - public synchronized Iterator iterator() - { - return new ArrayList<>(list).iterator(); - } - - @Override - public synchronized void remove(@NonNull Habit habit) - { - throwIfHasParent(); - list.remove(habit); - getObservable().notifyListeners(); - } - - @Override - public synchronized void reorder(@NonNull Habit from, @NonNull Habit to) - { - throwIfHasParent(); - if (primaryOrder != BY_POSITION) throw new IllegalStateException( - "cannot reorder automatically sorted list"); - - if (indexOf(from) < 0) throw new IllegalArgumentException( - "list does not contain (from) habit"); - - int toPos = indexOf(to); - if (toPos < 0) throw new IllegalArgumentException( - "list does not contain (to) habit"); - - list.remove(from); - list.add(toPos, from); - - int position = 0; - for(Habit h : list) - h.setPosition(position++); - - getObservable().notifyListeners(); - } - - @Override - public synchronized int size() - { - return list.size(); - } - - @Override - public synchronized void update(List habits) - { - resort(); - } - - private void throwIfHasParent() - { - if (parent != null) throw new IllegalStateException( - "Filtered lists cannot be modified directly. " + - "You should modify the parent list instead."); - } - - private synchronized void loadFromParent() - { - if (parent == null) throw new IllegalStateException(); - - list.clear(); - for (Habit h : parent) if (filter.matches(h)) list.add(h); - resort(); - } - - public synchronized void resort() - { - if (comparator != null) list.sort(comparator); - getObservable().notifyListeners(); - } -} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt new file mode 100644 index 000000000..72447862f --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/memory/MemoryHabitList.kt @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * 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 . + */ +package org.isoron.uhabits.core.models.memory + +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import java.util.ArrayList +import java.util.Comparator +import java.util.LinkedList +import java.util.Objects + +/** + * In-memory implementation of [HabitList]. + */ +class MemoryHabitList : HabitList { + private val list = LinkedList() + + @get:Synchronized + override var primaryOrder = Order.BY_POSITION + set(value) { + field = value + comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder) + resort() + } + + @get:Synchronized + override var secondaryOrder = Order.BY_NAME_ASC + set(value) { + field = value + comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder) + resort() + } + + private var comparator: Comparator? = + getComposedComparatorByOrder(primaryOrder, secondaryOrder) + private var parent: MemoryHabitList? = null + + constructor() : super() + constructor( + matcher: HabitMatcher, + comparator: Comparator?, + parent: MemoryHabitList + ) : super(matcher) { + this.parent = parent + this.comparator = comparator + primaryOrder = parent.primaryOrder + secondaryOrder = parent.secondaryOrder + parent.observable.addListener { loadFromParent() } + loadFromParent() + } + + @Synchronized + @Throws(IllegalArgumentException::class) + override fun add(habit: Habit) { + throwIfHasParent() + require(!list.contains(habit)) { "habit already added" } + val id = habit.id + if (id != null && getById(id) != null) throw RuntimeException("duplicate id") + if (id == null) habit.id = list.size.toLong() + list.addLast(habit) + resort() + } + + @Synchronized + override fun getById(id: Long): Habit? { + for (h in list) { + checkNotNull(h.id) + if (h.id == id) return h + } + return null + } + + @Synchronized + override fun getByUUID(uuid: String?): Habit? { + for (h in list) if (Objects.requireNonNull(h.uuid) == uuid) return h + return null + } + + @Synchronized + override fun getByPosition(position: Int): Habit { + return list[position] + } + + @Synchronized + override fun getFiltered(matcher: HabitMatcher?): HabitList { + return MemoryHabitList(matcher!!, comparator, this) + } + + private fun getComposedComparatorByOrder( + firstOrder: Order, + secondOrder: Order? + ): Comparator { + return Comparator { h1: Habit, h2: Habit -> + val firstResult = getComparatorByOrder(firstOrder).compare(h1, h2) + if (firstResult != 0 || secondOrder == null) { + return@Comparator firstResult + } + getComparatorByOrder(secondOrder).compare(h1, h2) + } + } + + private fun getComparatorByOrder(order: Order): Comparator { + val nameComparatorAsc = Comparator { habit1, habit2 -> + habit1.name.compareTo( + habit2.name + ) + } + val nameComparatorDesc = + Comparator { h1: Habit, h2: Habit -> nameComparatorAsc.compare(h2, h1) } + val colorComparatorAsc = Comparator { (color1), (color2) -> + color1.compareTo( + color2 + ) + } + val colorComparatorDesc = + Comparator { h1: Habit, h2: Habit -> colorComparatorAsc.compare(h2, h1) } + val scoreComparatorDesc = + Comparator { habit1, habit2 -> + val today = getTodayWithOffset() + habit1.scores[today].value.compareTo(habit2.scores[today].value) + } + val scoreComparatorAsc = + Comparator { h1: Habit, h2: Habit -> scoreComparatorDesc.compare(h2, h1) } + val positionComparator = + Comparator { habit1, habit2 -> + habit1.position.compareTo(habit2.position) + } + val statusComparatorDesc = Comparator { h1: Habit, h2: Habit -> + if (h1.isCompletedToday() != h2.isCompletedToday()) { + return@Comparator if (h1.isCompletedToday()) -1 else 1 + } + if (h1.isNumerical != h2.isNumerical) { + return@Comparator if (h1.isNumerical) -1 else 1 + } + val today = getTodayWithOffset() + val v1 = h1.computedEntries.get(today).value + val v2 = h2.computedEntries.get(today).value + v2.compareTo(v1) + } + val statusComparatorAsc = + Comparator { h1: Habit, h2: Habit -> statusComparatorDesc.compare(h2, h1) } + if (order === Order.BY_POSITION) return positionComparator + if (order === Order.BY_NAME_ASC) return nameComparatorAsc + if (order === Order.BY_NAME_DESC) return nameComparatorDesc + if (order === Order.BY_COLOR_ASC) return colorComparatorAsc + if (order === Order.BY_COLOR_DESC) return colorComparatorDesc + if (order === Order.BY_SCORE_DESC) return scoreComparatorDesc + if (order === Order.BY_SCORE_ASC) return scoreComparatorAsc + if (order === Order.BY_STATUS_DESC) return statusComparatorDesc + if (order === Order.BY_STATUS_ASC) return statusComparatorAsc + throw IllegalStateException() + } + + @Synchronized + override fun indexOf(h: Habit): Int { + return list.indexOf(h) + } + + @Synchronized + override fun iterator(): Iterator { + return ArrayList(list).iterator() + } + + @Synchronized + override fun remove(h: Habit) { + throwIfHasParent() + list.remove(h) + observable.notifyListeners() + } + + @Synchronized + override fun reorder(from: Habit, to: Habit) { + throwIfHasParent() + check(!(primaryOrder !== Order.BY_POSITION)) { "cannot reorder automatically sorted list" } + require(indexOf(from) >= 0) { "list does not contain (from) habit" } + val toPos = indexOf(to) + require(toPos >= 0) { "list does not contain (to) habit" } + list.remove(from) + list.add(toPos, from) + var position = 0 + for (h in list) h.position = position++ + observable.notifyListeners() + } + + @Synchronized + override fun size(): Int { + return list.size + } + + @Synchronized + override fun update(habits: List?) { + resort() + } + + private fun throwIfHasParent() { + check(parent == null) { + "Filtered lists cannot be modified directly. " + + "You should modify the parent list instead." + } + } + + @Synchronized + private fun loadFromParent() { + checkNotNull(parent) + list.clear() + for (h in parent!!) if (filter.matches(h)) list.add(h) + resort() + } + + @Synchronized + override fun resort() { + if (comparator != null) list.sortWith(comparator!!) + observable.notifyListeners() + } +}