Replace JVM stdlib imports with Kotlin stdlib for KMP migration

This commit is contained in:
Alinson S. Xavier 2026-04-05 16:41:55 -05:00
parent 110f594d09
commit 7a48277478
27 changed files with 176 additions and 166 deletions

View File

@ -378,6 +378,8 @@ main() {
build)
shift; _parse_opts "$@"
clean
log_info "Formatting code..."
gradle_run ktlintFormat || fail
core_build
android_build
;;

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2016-2025 Á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.platform.io
expect fun format(format: String, arg: String): String
expect fun format(format: String, arg: Int): String
expect fun format(format: String, arg: Double): String

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2016-2025 Á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.platform.io
actual fun format(format: String, arg: String): String =
String.format(format, arg)
actual fun format(format: String, arg: Int): String =
String.format(format, arg)
actual fun format(format: String, arg: Double): String =
String.format(format, arg)

View File

@ -22,14 +22,13 @@ import me.tatarka.inject.annotations.Inject
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner
import java.util.LinkedList
@AppScope
@Inject
open class CommandRunner(
private val taskRunner: TaskRunner
) {
private val listeners: LinkedList<Listener> = LinkedList()
private val listeners: MutableList<Listener> = mutableListOf()
open fun run(command: Command) {
taskRunner.execute(

View File

@ -26,14 +26,10 @@ import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import java.util.ArrayList
import java.util.Calendar
import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
@ThreadSafe
open class EntryList {
private val entriesByDate: HashMap<LocalDate, Entry> = HashMap()
@ -304,7 +300,7 @@ private fun truncateDate(
*/
fun List<Entry>.groupedSum(
truncateField: TruncateField,
firstWeekday: Int = Calendar.SATURDAY,
firstWeekday: Int = 7,
isNumerical: Boolean
): List<Entry> {
val firstWeekdayEnum = DayOfWeek.values()[firstWeekday - 1]
@ -332,7 +328,7 @@ fun List<Entry>.groupedSum(
*/
fun List<Entry>.countSkippedDays(
truncateField: TruncateField,
firstWeekday: Int = Calendar.SATURDAY
firstWeekday: Int = 7
): List<Entry> {
val firstWeekdayEnum = DayOfWeek.values()[firstWeekday - 1]
return this.map { (date, value) ->

View File

@ -19,8 +19,10 @@
package org.isoron.uhabits.core.models
import org.isoron.platform.time.getToday
import java.util.UUID
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalUuidApi::class)
data class Habit(
var color: PaletteColor = PaletteColor(8),
var description: String = "",
@ -42,7 +44,7 @@ data class Habit(
val streaks: StreakList
) {
init {
if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "")
if (uuid == null) this.uuid = Uuid.random().toHexString()
}
var observable = ModelObservable()

View File

@ -19,10 +19,9 @@
package org.isoron.uhabits.core.models
import com.opencsv.CSVWriter
import org.isoron.platform.io.format
import java.io.IOException
import java.io.Writer
import java.util.LinkedList
import java.util.Locale
import javax.annotation.concurrent.ThreadSafe
/**
@ -125,8 +124,7 @@ abstract class HabitList : Iterable<Habit> {
* Removes all the habits from the list.
*/
open fun removeAll() {
val copy: MutableList<Habit> = LinkedList()
for (h in this) copy.add(h)
val copy = toList()
for (h in copy) remove(h)
observable.notifyListeners()
}
@ -199,7 +197,7 @@ abstract class HabitList : Iterable<Habit> {
for (habit in this) {
val (numerator, denominator) = habit.frequency
val cols = arrayOf(
String.format(Locale.US, "%03d", indexOf(habit) + 1),
format("%03d", indexOf(habit) + 1),
habit.name,
habit.type.name,
habit.question,

View File

@ -1,7 +1,5 @@
package org.isoron.uhabits.core.models
import java.lang.IllegalStateException
enum class HabitType(val value: Int) {
YES_NO(0), NUMERICAL(1);

View File

@ -18,14 +18,10 @@
*/
package org.isoron.uhabits.core.models
import java.util.LinkedList
import javax.annotation.concurrent.ThreadSafe
/**
* A ModelObservable allows objects to subscribe themselves to it and receive
* notifications whenever the model is changed.
*/
@ThreadSafe
class ModelObservable {
private val listeners: MutableList<Listener>
@ -80,6 +76,6 @@ class ModelObservable {
* Creates a new ModelObservable with no listeners.
*/
init {
listeners = LinkedList()
listeners = mutableListOf()
}
}

View File

@ -1,7 +1,5 @@
package org.isoron.uhabits.core.models
import java.lang.IllegalStateException
enum class NumericalHabitType(val value: Int) {
AT_LEAST(0), AT_MOST(1);

View File

@ -20,16 +20,12 @@ package org.isoron.uhabits.core.models
import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.models.Score.Companion.compute
import java.util.ArrayList
import java.util.HashMap
import javax.annotation.concurrent.ThreadSafe
import kotlin.math.max
import kotlin.math.min
@ThreadSafe
class ScoreList {
private val map = HashMap<LocalDate, Score>()
private val map = mutableMapOf<LocalDate, Score>()
/**
* Returns the score for a given day. If the date given happens before the first
@ -52,7 +48,7 @@ class ScoreList {
from: LocalDate,
to: LocalDate
): List<Score> {
val result: MutableList<Score> = ArrayList()
val result: MutableList<Score> = mutableListOf()
if (from.isNewerThan(to)) return result
var current = to
while (!current.isOlderThan(from)) {

View File

@ -18,10 +18,6 @@
*/
package org.isoron.uhabits.core.models
import org.apache.commons.lang3.builder.EqualsBuilder
import org.apache.commons.lang3.builder.HashCodeBuilder
import java.util.Arrays
class WeekdayList {
private val weekdays: BooleanArray
@ -35,7 +31,7 @@ class WeekdayList {
}
constructor(weekdays: BooleanArray?) {
this.weekdays = Arrays.copyOf(weekdays, 7)
this.weekdays = weekdays!!.copyOf(7)
}
val isEmpty: Boolean
@ -60,13 +56,12 @@ class WeekdayList {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val that = other as WeekdayList
return EqualsBuilder().append(weekdays, that.weekdays).isEquals
if (other !is WeekdayList) return false
return weekdays.contentEquals(other.weekdays)
}
override fun hashCode(): Int {
return HashCodeBuilder(17, 37).append(weekdays).toHashCode()
return weekdays.contentHashCode()
}
override fun toString() = "{weekdays: [${weekdays.joinToString(separator = ",")}]}"

View File

@ -21,16 +21,12 @@ 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 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<Habit>()
private val list = mutableListOf<Habit>()
@get:Synchronized
override var primaryOrder = Order.BY_POSITION
@ -74,7 +70,7 @@ class MemoryHabitList : HabitList {
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)
list.add(habit)
resort()
}
@ -89,7 +85,7 @@ class MemoryHabitList : HabitList {
@Synchronized
override fun getByUUID(uuid: String?): Habit? {
for (h in list) if (Objects.requireNonNull(h.uuid) == uuid) return h
for (h in list) if (h.uuid!! == uuid) return h
return null
}
@ -171,7 +167,7 @@ class MemoryHabitList : HabitList {
@Synchronized
override fun iterator(): Iterator<Habit> {
return ArrayList(list).iterator()
return list.toMutableList().iterator()
}
@Synchronized

View File

@ -27,7 +27,6 @@ import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList
import java.util.Objects.requireNonNull
/**
* The SQLite database record corresponding to a [Habit].
@ -110,7 +109,7 @@ class HabitRecord {
reminderHour = null
if (model.hasReminder()) {
val reminder = model.reminder
reminderHour = requireNonNull(reminder)!!.hour
reminderHour = reminder!!.hour
reminderMin = reminder!!.minute
reminderDays = reminder.days.toInteger()
}

View File

@ -25,7 +25,6 @@ import org.isoron.platform.utils.StringUtils.Companion.joinLongs
import org.isoron.platform.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.ui.ThemeSwitcher
import java.util.LinkedList
import kotlin.math.max
import kotlin.math.min
@ -282,7 +281,7 @@ open class Preferences(private val storage: Storage) {
}
init {
listeners = LinkedList()
listeners = mutableListOf()
storage.onAttached(this)
}
}

View File

@ -29,8 +29,6 @@ import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.utils.DateUtils
import java.util.Locale
import java.util.Objects
@AppScope
@Inject
@ -57,19 +55,11 @@ class ReminderScheduler(
sys.log("ReminderScheduler", "habit=" + habit.id + " has no reminder. Skipping.")
return
}
var reminderTime = Objects.requireNonNull(habit.reminder)!!.timeInMillis
var reminderTime = habit.reminder!!.timeInMillis
val snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.id!!)
if (snoozeReminderTime != 0L) {
val now = DateUtils.applyTimezone(DateUtils.getLocalTime())
sys.log(
"ReminderScheduler",
String.format(
Locale.US,
"Habit %d has been snoozed until %d",
habit.id,
snoozeReminderTime
)
)
sys.log("ReminderScheduler", "Habit ${habit.id} has been snoozed until $snoozeReminderTime")
if (snoozeReminderTime > now) {
sys.log("ReminderScheduler", "Snooze time is in the future. Accepting.")
reminderTime = snoozeReminderTime
@ -93,16 +83,7 @@ class ReminderScheduler(
return
}
val timestamp = DateUtils.getStartOfDayWithOffset(DateUtils.removeTimezone(reminderTime), 0, 0)
sys.log(
"ReminderScheduler",
String.format(
Locale.US,
"reminderTime=%d removeTimezone=%d timestamp=%d",
reminderTime,
DateUtils.removeTimezone(reminderTime),
timestamp
)
)
sys.log("ReminderScheduler", "reminderTime=$reminderTime removeTimezone=${DateUtils.removeTimezone(reminderTime)} timestamp=$timestamp")
sys.scheduleShowReminder(reminderTime, habit, timestamp)
}

View File

@ -18,13 +18,11 @@
*/
package org.isoron.uhabits.core.tasks
import java.util.LinkedList
class SingleThreadTaskRunner : TaskRunner {
override val activeTaskCount: Int
get() = 0
private val listeners: MutableList<TaskRunner.Listener> = LinkedList()
private val listeners: MutableList<TaskRunner.Listener> = mutableListOf()
override fun addListener(listener: TaskRunner.Listener) {
listeners.add(listener)
}

View File

@ -30,9 +30,6 @@ import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner
import java.util.HashMap
import java.util.Locale
import java.util.Objects
@AppScope
@Inject
@ -42,7 +39,7 @@ class NotificationTray(
private val preferences: Preferences,
private val systemTray: SystemTray
) : CommandRunner.Listener, Preferences.Listener {
private val active: HashMap<Habit, NotificationData> = HashMap()
private val active: MutableMap<Habit, NotificationData> = mutableMapOf()
fun cancel(habit: Habit) {
val notificationId = getNotificationId(habit)
systemTray.removeNotification(notificationId)
@ -123,43 +120,19 @@ class NotificationTray(
override fun onPostExecute() {
systemTray.log("Showing notification for habit=" + habit.id)
if (isCompleted && habit.targetType != NumericalHabitType.AT_MOST) {
systemTray.log(
String.format(
Locale.US,
"Habit %d already checked. Skipping.",
habit.id
)
)
systemTray.log("Habit ${habit.id} already checked. Skipping.")
return
}
if (!habit.hasReminder()) {
systemTray.log(
String.format(
Locale.US,
"Habit %d does not have a reminder. Skipping.",
habit.id
)
)
systemTray.log("Habit ${habit.id} does not have a reminder. Skipping.")
return
}
if (habit.isArchived) {
systemTray.log(
String.format(
Locale.US,
"Habit %d is archived. Skipping.",
habit.id
)
)
systemTray.log("Habit ${habit.id} is archived. Skipping.")
return
}
if (!shouldShowReminderToday()) {
systemTray.log(
String.format(
Locale.US,
"Habit %d not supposed to run today. Skipping.",
habit.id
)
)
systemTray.log("Habit ${habit.id} not supposed to run today. Skipping.")
return
}
systemTray.showNotification(
@ -173,7 +146,7 @@ class NotificationTray(
private fun shouldShowReminderToday(): Boolean {
if (!habit.hasReminder()) return false
val reminder = habit.reminder
val reminderDays = Objects.requireNonNull(reminder)!!.days.toArray()
val reminderDays = reminder!!.days.toArray()
val weekday = (date.dayOfWeek.daysSinceSunday + 1) % 7
return reminderDays[weekday]
}

View File

@ -19,7 +19,6 @@
package org.isoron.uhabits.core.ui.screens.habits.list
import me.tatarka.inject.annotations.Inject
import org.apache.commons.lang3.ArrayUtils
import org.isoron.platform.time.getToday
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.commands.Command
@ -32,11 +31,6 @@ import org.isoron.uhabits.core.models.HabitList.Order
import org.isoron.uhabits.core.models.HabitMatcher
import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner
import java.util.ArrayList
import java.util.Arrays
import java.util.HashMap
import java.util.LinkedList
import java.util.TreeSet
/**
* A HabitCardListCache fetches and keeps a cache of all the data necessary to
@ -210,11 +204,11 @@ class HabitCardListCache(
}
private inner class CacheData {
val idToHabit: HashMap<Long?, Habit> = HashMap()
val idToHabit: MutableMap<Long?, Habit> = mutableMapOf()
val habits: MutableList<Habit>
val checkmarks: HashMap<Long?, IntArray>
val scores: HashMap<Long?, Double>
val notes: HashMap<Long?, Array<String>>
val checkmarks: MutableMap<Long?, IntArray>
val scores: MutableMap<Long?, Double>
val notes: MutableMap<Long?, Array<String>>
@Synchronized
fun copyCheckmarksFrom(oldData: CacheData) {
@ -267,10 +261,10 @@ class HabitCardListCache(
* Creates a new CacheData without any content.
*/
init {
habits = LinkedList()
checkmarks = HashMap()
scores = HashMap()
notes = HashMap()
habits = mutableListOf()
checkmarks = mutableMapOf()
scores = mutableMapOf()
notes = mutableMapOf()
}
}
@ -310,15 +304,14 @@ class HabitCardListCache(
val habit = newData.habits[position]
if (targetId != null && targetId != habit.id) continue
newData.scores[habit.id] = habit.scores[today].value
val list: MutableList<Int> = ArrayList()
val notes: MutableList<String> = ArrayList()
val checkmarkList = mutableListOf<Int>()
val noteList = mutableListOf<String>()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
list.add(value)
notes.add(note)
checkmarkList.add(value)
noteList.add(note)
}
val entries = list.toTypedArray()
newData.checkmarks[habit.id] = ArrayUtils.toPrimitive(entries)
newData.notes[habit.id] = notes.toTypedArray()
newData.checkmarks[habit.id] = checkmarkList.toIntArray()
newData.notes[habit.id] = noteList.toTypedArray()
runner!!.publishProgress(this, position)
}
}
@ -380,8 +373,8 @@ class HabitCardListCache(
val newNoteIndicators = newData.notes[id]!!
var unchanged = true
if (oldScore != newScore) unchanged = false
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false
if (!Arrays.equals(oldNoteIndicators, newNoteIndicators)) unchanged = false
if (!oldCheckmarks.contentEquals(newCheckmarks)) unchanged = false
if (!oldNoteIndicators.contentEquals(newNoteIndicators)) unchanged = false
if (unchanged) return
data.scores[id] = newScore
data.checkmarks[id] = newCheckmarks
@ -413,8 +406,7 @@ class HabitCardListCache(
private fun processRemovedHabits() {
val before: Set<Long?> = data.idToHabit.keys
val after: Set<Long?> = newData.idToHabit.keys
val removed: MutableSet<Long?> = TreeSet(before)
removed.removeAll(after)
val removed = before - after
for (id in removed) remove(id!!)
}
}

View File

@ -30,9 +30,12 @@ import org.isoron.uhabits.core.tasks.ExportCSVTask
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import java.io.File
import java.util.Random
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
class ShowHabitMenuPresenter(
private val commandRunner: CommandRunner,
@ -86,16 +89,15 @@ class ShowHabitMenuPresenter(
}
fun onRandomize() {
val random = Random()
habit.originalEntries.clear()
var strength = 50.0
for (i in 0 until 365 * 5) {
if (i % 7 == 0) strength = max(0.0, min(100.0, strength + 10 * random.nextGaussian()))
if (random.nextInt(100) > strength) continue
if (i % 7 == 0) strength = max(0.0, min(100.0, strength + 10 * nextGaussian()))
if (kotlin.random.Random.nextInt(100) > strength) continue
var value = Entry.YES_MANUAL
if (habit.isNumerical) {
value =
(1000 + 250 * random.nextGaussian() * strength / 100).toInt() * 1000
(1000 + 250 * nextGaussian() * strength / 100).toInt() * 1000
}
habit.originalEntries.add(Entry(getToday().minus(i), value))
}
@ -103,6 +105,12 @@ class ShowHabitMenuPresenter(
screen.refresh()
}
private fun nextGaussian(): Double {
val u1 = kotlin.random.Random.nextDouble()
val u2 = kotlin.random.Random.nextDouble()
return sqrt(-2.0 * ln(u1)) * cos(2.0 * PI * u2)
}
enum class Message {
COULD_NOT_EXPORT,
HABIT_ARCHIVED,

View File

@ -24,8 +24,6 @@ import org.isoron.platform.time.LocalDate
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.views.Theme
import java.util.HashMap
data class FrequencyCardState(
val color: PaletteColor,
val firstWeekday: DayOfWeek,

View File

@ -26,7 +26,6 @@ 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 java.util.ArrayList
import kotlin.math.max
data class TargetCardState(
@ -135,21 +134,21 @@ class TargetCardPresenter {
targetThisQuarter = max(0.0, targetThisQuarter - dailyTarget * skippedDaysThisQuarter)
targetThisYear = max(0.0, targetThisYear - dailyTarget * skippedDaysThisYear)
val values = ArrayList<Double>()
val values = mutableListOf<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek / 1e3)
values.add(valueThisMonth / 1e3)
values.add(valueThisQuarter / 1e3)
values.add(valueThisYear / 1e3)
val targets = ArrayList<Double>()
val targets = mutableListOf<Double>()
if (habit.frequency.denominator <= 1) targets.add(targetToday)
if (habit.frequency.denominator <= 7) targets.add(targetThisWeek)
targets.add(targetThisMonth)
targets.add(targetThisQuarter)
targets.add(targetThisYear)
val intervals = ArrayList<Int>()
val intervals = mutableListOf<Int>()
if (habit.frequency.denominator <= 1) intervals.add(1)
if (habit.frequency.denominator <= 7) intervals.add(7)
intervals.add(30)

View File

@ -23,26 +23,25 @@ import org.isoron.platform.gui.Canvas
import org.isoron.platform.gui.Color
import org.isoron.platform.gui.Font
import org.isoron.platform.gui.View
import java.lang.String.format
import kotlin.math.round
fun Double.toShortString(): String = when {
this >= 1e9 -> format("%.1fG", this / 1e9)
this >= 1e8 -> format("%.0fM", this / 1e6)
this >= 1e7 -> format("%.1fM", this / 1e6)
this >= 1e6 -> format("%.1fM", this / 1e6)
this >= 1e5 -> format("%.0fk", this / 1e3)
this >= 1e4 -> format("%.1fk", this / 1e3)
this >= 1e3 -> format("%.1fk", this / 1e3)
this >= 1e2 -> format("%.0f", this)
this >= 1e9 -> "%.1fG".format(this / 1e9)
this >= 1e8 -> "%.0fM".format(this / 1e6)
this >= 1e7 -> "%.1fM".format(this / 1e6)
this >= 1e6 -> "%.1fM".format(this / 1e6)
this >= 1e5 -> "%.0fk".format(this / 1e3)
this >= 1e4 -> "%.1fk".format(this / 1e3)
this >= 1e3 -> "%.1fk".format(this / 1e3)
this >= 1e2 -> "%.0f".format(this)
this >= 1e1 -> when {
round(this) == this -> format("%.0f", this)
else -> format("%.1f", this)
round(this) == this -> "%.0f".format(this)
else -> "%.1f".format(this)
}
else -> when {
round(this) == this -> format("%.0f", this)
round(this * 10) == this * 10 -> format("%.1f", this)
else -> format("%.2f", this)
round(this) == this -> "%.0f".format(this)
round(this * 10) == this * 10 -> "%.1f".format(this)
else -> "%.2f".format(this)
}
}

View File

@ -22,7 +22,6 @@ package org.isoron.uhabits.core.ui.views
import org.isoron.platform.gui.Canvas
import org.isoron.platform.gui.Color
import org.isoron.platform.gui.View
import java.lang.String.format
import kotlin.math.max
import kotlin.math.min
@ -52,7 +51,7 @@ class Ring(
if (label) {
canvas.setColor(color)
canvas.setFontSize(radius * 0.4)
canvas.drawText(format("%.0f%%", percentage * 100), width / 2, height / 2)
canvas.drawText("%.0f%%".format(percentage * 100), width / 2, height / 2)
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2016-2025 Á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.platform.io
import kotlin.test.Test
import kotlin.test.assertEquals
class StringsTest {
@Test
fun testFormat() {
assertEquals("hello world!", format("hello %s!", "world"))
assertEquals(" 5", format("%3d", 5))
assertEquals("005", format("%03d", 5))
assertEquals(" 45", format("%3d", 45))
assertEquals("145", format("%3d", 145))
assertEquals(" 13.42", format("%8.2f", 13.419187263))
assertEquals("00013.42", format("%08.2f", 13.419187263))
assertEquals("13.42 ", format("%-8.2f", 13.419187263))
}
}

View File

@ -28,7 +28,7 @@ import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.junit.Test
import java.util.Random
import kotlin.random.Random
import kotlin.test.assertEquals
class EntryListTest {

View File

@ -27,7 +27,6 @@ import org.isoron.uhabits.core.BaseUnitTest
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.junit.Before
import org.junit.Test
import java.util.ArrayList
import kotlin.test.assertTrue
open class BaseScoreListTest : BaseUnitTest() {
@ -151,7 +150,7 @@ class YesNoScoreListTest : BaseScoreListTest() {
// If the habit should be performed 3 times per week and the user misses 1 repetition
// each week, score should converge to 66%.
habit.frequency = Frequency(3, 7)
val values = ArrayList<Int>()
val values = mutableListOf<Int>()
for (k in 0..99) {
values.add(Entry.YES_MANUAL)
values.add(Entry.YES_MANUAL)
@ -175,7 +174,7 @@ class YesNoScoreListTest : BaseScoreListTest() {
// If the user performs habit perfectly each week, but on different weekdays,
// score should still converge to 100%
habit.frequency = Frequency(1, 7)
val values = ArrayList<Int>()
val values = mutableListOf<Int>()
for (k in 0..99) {
// Week 0
values.add(Entry.YES_MANUAL)
@ -256,7 +255,7 @@ class YesNoScoreListTest : BaseScoreListTest() {
habit.recompute()
}
private fun check(values: ArrayList<Int>) {
private fun check(values: MutableList<Int>) {
val entries = habit.originalEntries
for (i in values.indices) if (values[i] == Entry.YES_MANUAL) {
entries.add(