From 0544166124bb676dba524b28c46b38e093550dd7 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Sun, 5 Apr 2026 12:52:47 -0500 Subject: [PATCH] Replace Timestamp with LocalDate --- .gitignore | 1 + build.sh | 61 +- .../org/isoron/uhabits/BaseAndroidTest.kt | 18 +- .../isoron/uhabits/BaseUserInterfaceTest.kt | 2 +- .../java/org/isoron/uhabits/HabitFixtures.kt | 20 +- .../habits/list/views/EntryPanelViewTest.kt | 14 +- .../habits/list/views/HabitCardViewTest.kt | 8 +- .../habits/list/views/NumberPanelViewTest.kt | 14 +- .../show/views/FrequencyCardViewTest.kt | 3 +- .../uhabits/performance/PerformanceTest.kt | 8 +- .../uhabits/widgets/CheckmarkWidgetTest.kt | 8 +- .../uhabits/widgets/FrequencyWidgetTest.kt | 4 +- .../widgets/views/CheckmarkWidgetViewTest.kt | 4 +- .../org/isoron/uhabits/HabitsApplication.kt | 9 +- .../common/dialogs/HistoryEditorDialog.kt | 4 +- .../common/dialogs/WeekdayPickerDialog.kt | 7 +- .../activities/common/views/FrequencyChart.kt | 94 +- .../activities/common/views/ScoreChart.kt | 63 +- .../common/views/ScrollableChart.kt | 4 +- .../activities/common/views/StreakChart.kt | 25 +- .../habits/list/ListHabitsActivity.kt | 9 +- .../habits/list/ListHabitsSelectionMenu.kt | 4 +- .../habits/list/views/CheckmarkButtonView.kt | 2 +- .../habits/list/views/CheckmarkPanelView.kt | 16 +- .../habits/list/views/HabitCardView.kt | 42 +- .../habits/list/views/HeaderView.kt | 18 +- .../habits/list/views/NumberButtonView.kt | 2 +- .../habits/list/views/NumberPanelView.kt | 12 +- .../habits/show/views/BarCardView.kt | 2 +- .../activities/settings/SettingsFragment.kt | 9 +- .../uhabits/automation/FireSettingReceiver.kt | 14 +- .../isoron/uhabits/intents/IntentParser.kt | 27 +- .../uhabits/intents/PendingIntentFactory.kt | 22 +- .../notifications/AndroidNotificationTray.kt | 16 +- .../uhabits/receivers/ReminderController.kt | 10 +- .../uhabits/receivers/ReminderReceiver.kt | 12 +- .../uhabits/receivers/WidgetReceiver.kt | 21 +- .../org/isoron/uhabits/utils/DatabaseUtils.kt | 5 +- .../isoron/uhabits/utils/DateExtensions.kt | 9 +- .../isoron/uhabits/utils/ViewExtensions.kt | 2 +- .../isoron/uhabits/widgets/CheckmarkWidget.kt | 6 +- .../isoron/uhabits/widgets/FrequencyWidget.kt | 3 +- .../widgets/FrequencyWidgetProvider.kt | 2 +- .../isoron/uhabits/widgets/HistoryWidget.kt | 4 +- .../uhabits/widgets/StackWidgetService.kt | 7 +- .../isoron/uhabits/widgets/StackWidgetType.kt | 8 +- .../isoron/uhabits/widgets/WidgetUpdater.kt | 6 +- .../org/isoron/uhabits/BaseAndroidJVMTest.kt | 10 +- .../receivers/ReminderControllerTest.kt | 7 +- .../kotlin/org/isoron/platform/time/Dates.kt | 87 +- .../org/isoron/platform/time/JavaDates.kt | 25 +- .../java/org/isoron/platform/time/JvmDates.kt | 14 + .../core/commands/CreateRepetitionCommand.kt | 6 +- .../uhabits/core/io/HabitBullCSVImporter.kt | 52 +- .../uhabits/core/io/HabitsCSVExporter.kt | 40 +- .../isoron/uhabits/core/io/LoopDBImporter.kt | 8 +- .../uhabits/core/io/RewireDBImporter.kt | 15 +- .../uhabits/core/io/TickmateDBImporter.kt | 7 +- .../org/isoron/uhabits/core/models/Entry.kt | 4 +- .../isoron/uhabits/core/models/EntryList.kt | 147 +-- .../org/isoron/uhabits/core/models/Habit.kt | 10 +- .../org/isoron/uhabits/core/models/Score.kt | 4 +- .../isoron/uhabits/core/models/ScoreList.kt | 33 +- .../org/isoron/uhabits/core/models/Streak.kt | 10 +- .../isoron/uhabits/core/models/StreakList.kt | 19 +- .../isoron/uhabits/core/models/Timestamp.kt | 135 -- .../core/models/memory/MemoryHabitList.kt | 5 +- .../core/models/sqlite/SQLiteEntryList.kt | 10 +- .../core/models/sqlite/records/EntryRecord.kt | 8 +- .../uhabits/core/preferences/Preferences.kt | 20 +- .../core/reminders/ReminderScheduler.kt | 13 +- .../isoron/uhabits/core/test/HabitFixtures.kt | 11 +- .../uhabits/core/ui/NotificationTray.kt | 16 +- .../ui/callbacks/OnToggleCheckmarkListener.kt | 4 +- .../screens/habits/list/HabitCardListCache.kt | 4 +- .../core/ui/screens/habits/list/HintList.kt | 11 +- .../screens/habits/list/ListHabitsBehavior.kt | 16 +- .../core/ui/screens/habits/show/ShowHabit.kt | 2 +- .../habits/show/ShowHabitMenuPresenter.kt | 4 +- .../ui/screens/habits/show/views/BarCard.kt | 6 +- .../habits/show/views/FrequencyCard.kt | 9 +- .../screens/habits/show/views/HistoryCard.kt | 41 +- .../screens/habits/show/views/OverviewCard.kt | 4 +- .../ui/screens/habits/show/views/ScoreCard.kt | 50 +- .../screens/habits/show/views/TargetCard.kt | 33 +- .../isoron/uhabits/core/ui/views/BarChart.kt | 2 +- .../uhabits/core/ui/widgets/WidgetBehavior.kt | 36 +- .../isoron/uhabits/core/utils/DateFormats.kt | 6 - .../isoron/uhabits/core/utils/DateUtils.kt | 414 ++---- .../uhabits/core/utils/MidnightTimer.kt | 11 +- .../java/org/isoron/platform/gui/DatesTest.kt | 186 ++- .../gui/JavaLocalDateFormatterTest.kt | 88 ++ .../org/isoron/uhabits/core/BaseUnitTest.kt | 24 +- .../commands/CreateRepetitionCommandTest.kt | 6 +- .../core/commands/EditHabitCommandTest.kt | 8 +- .../org/isoron/uhabits/core/io/ImportTest.kt | 15 +- .../uhabits/core/models/EntryListTest.kt | 82 +- .../isoron/uhabits/core/models/HabitTest.kt | 2 +- .../uhabits/core/models/ScoreListTest.kt | 5 +- .../uhabits/core/models/StreakListTest.kt | 5 +- .../uhabits/core/models/TimestampTest.kt | 67 - .../core/models/sqlite/SQLiteEntryListTest.kt | 17 +- .../models/sqlite/records/EntryRecordTest.kt | 4 +- .../core/preferences/PreferencesTest.kt | 9 +- .../core/reminders/ReminderSchedulerTest.kt | 25 +- .../habits/list/HabitCardListCacheTest.kt | 4 +- .../ui/screens/habits/list/HintListTest.kt | 12 +- .../habits/list/ListHabitsBehaviorTest.kt | 9 +- .../core/ui/widgets/WidgetBehaviorTest.kt | 8 +- .../uhabits/core/utils/DateUtilsTest.kt | 1109 +++++++---------- .../uhabits/core/utils/MidnightTimerTest.kt | 12 +- 111 files changed, 1723 insertions(+), 2004 deletions(-) create mode 100644 uhabits-core/src/jvmMain/java/org/isoron/platform/time/JvmDates.kt delete mode 100644 uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt create mode 100644 uhabits-core/src/jvmTest/java/org/isoron/platform/gui/JavaLocalDateFormatterTest.kt delete mode 100644 uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/TimestampTest.kt diff --git a/.gitignore b/.gitignore index 8dac6827..0d0456ce 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ node_modules *.sketch crowdin.yml kotlin-js-store +*.md diff --git a/build.sh b/build.sh index f077534d..aba44ea0 100755 --- a/build.sh +++ b/build.sh @@ -22,7 +22,8 @@ ANDROID_OUTPUTS_DIR="uhabits-android/build/outputs" AVDMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager" AVD_PREFIX="uhabitsTest" EMULATOR="${ANDROID_HOME}/emulator/emulator" -GRADLE="./gradlew --stacktrace --quiet" +GRADLE="./gradlew --stacktrace --quiet --console=plain" +GRADLE_LOG="build/gradle-output.log" PACKAGE_NAME=org.isoron.uhabits SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" VERSION=$(grep versionName uhabits-android/build.gradle.kts | sed -e 's/.*"\([^"]*\)".*/\1/g') @@ -63,14 +64,24 @@ fail() { exit 1 } +gradle_run() { + mkdir -p build + if ! $GRADLE "$@" > "$GRADLE_LOG" 2>&1; then + log_error "Gradle command failed: $*" + grep -E "^e:|^w:|^FAILURE|^> " "$GRADLE_LOG" | head -40 + log_error "Full log: $GRADLE_LOG" + return 1 + fi +} + # Core # ----------------------------------------------------------------------------- core_build() { log_info "Building uhabits-core..." - $GRADLE ktlintCheck || fail - $GRADLE lintDebug || fail - $GRADLE :uhabits-core:build || fail + gradle_run ktlintCheck || fail + gradle_run lintDebug || fail + gradle_run :uhabits-core:build || fail } # Android @@ -268,32 +279,32 @@ android_build() { fi log_info "Removing old APKs..." - rm -vf uhabits-android/build/*.apk + rm -f uhabits-android/build/*.apk if [ -n "$RELEASE" ]; then log_info "Building release APK..." - $GRADLE updateTranslators - $GRADLE :uhabits-android:assembleRelease + gradle_run updateTranslators + gradle_run :uhabits-android:assembleRelease cp -v \ uhabits-android/build/outputs/apk/release/uhabits-android-release.apk \ uhabits-android/build/loop-"$VERSION"-release.apk fi log_info "Building debug APK..." - $GRADLE :uhabits-android:assembleDebug --stacktrace || fail + gradle_run :uhabits-android:assembleDebug || fail cp -v \ uhabits-android/build/outputs/apk/debug/uhabits-android-debug.apk \ uhabits-android/build/loop-"$VERSION"-debug.apk log_info "Building instrumentation APK..." if [ -n "$RELEASE" ]; then - $GRADLE :uhabits-android:assembleAndroidTest \ + gradle_run :uhabits-android:assembleAndroidTest \ -Pandroid.injected.signing.store.file="$LOOP_KEY_STORE" \ -Pandroid.injected.signing.store.password="$LOOP_STORE_PASSWORD" \ -Pandroid.injected.signing.key.alias="$LOOP_KEY_ALIAS" \ -Pandroid.injected.signing.key.password="$LOOP_KEY_PASSWORD" || fail else - $GRADLE assembleAndroidTest || fail + gradle_run assembleAndroidTest || fail fi return 0 @@ -345,21 +356,21 @@ END } clean() { - rm -rfv uhabits-android/.gradle - rm -rfv uhabits-android/android-pickers/build - rm -rfv uhabits-android/build - rm -rfv uhabits-android/uhabits-android/build - rm -rfv uhabits-core-legacy/.gradle - rm -rfv uhabits-core-legacy/build - rm -rfv uhabits-core/.gradle - rm -rfv uhabits-core/build - rm -rfv uhabits-server/.gradle - rm -rfv uhabits-server/build - rm -rfv uhabits-web/build - rm -rfv uhabits-web/node_modules - rm -rfv uhabits-web/node_modules/core-js/build - rm -rfv uhabits-web/node_modules/upath/build - rm -rfv .gradle + rm -rf uhabits-android/.gradle + rm -rf uhabits-android/android-pickers/build + rm -rf uhabits-android/build + rm -rf uhabits-android/uhabits-android/build + rm -rf uhabits-core-legacy/.gradle + rm -rf uhabits-core-legacy/build + rm -rf uhabits-core/.gradle + rm -rf uhabits-core/build + rm -rf uhabits-server/.gradle + rm -rf uhabits-server/build + rm -rf uhabits-web/build + rm -rf uhabits-web/node_modules + rm -rf uhabits-web/node_modules/core-js/build + rm -rf uhabits-web/node_modules/upath/build + rm -rf .gradle } main() { diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt index 3b1215b3..aac323f4 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.kt @@ -29,14 +29,14 @@ import androidx.test.uiautomator.UiDevice import junit.framework.TestCase import org.hamcrest.CoreMatchers.hasItems import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.computeToday +import org.isoron.platform.time.getToday +import org.isoron.platform.time.setToday import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.ModelFactory -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.tasks.TaskRunner -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime -import org.isoron.uhabits.core.utils.DateUtils.Companion.setStartDayOffset import org.isoron.uhabits.inject.ActivityContextModule import org.isoron.uhabits.inject.AppContextModule import org.isoron.uhabits.inject.HabitsModule @@ -75,8 +75,6 @@ abstract class BaseAndroidTest : TestCase() { public override fun setUp() { if (Looper.myLooper() == null) Looper.prepare() device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - setFixedLocalTime(FIXED_LOCAL_TIME) - setStartDayOffset(0, 0) setResolution(2.0f) setTheme(R.style.AppBaseTheme) setLocale("en", "US") @@ -92,6 +90,7 @@ abstract class BaseAndroidTest : TestCase() { prefs = appComponent.preferences habitList = appComponent.habitList taskRunner = appComponent.taskRunner + setToday(computeToday(appComponent.preferences.midnightDelayHours, 0)) modelFactory = appComponent.modelFactory prefs.clear() fixtures = HabitFixtures(modelFactory, habitList) @@ -143,7 +142,7 @@ abstract class BaseAndroidTest : TestCase() { } } - protected fun day(offset: Int): Timestamp { + protected fun day(offset: Int): LocalDate { return getToday().minus(offset) } @@ -211,8 +210,5 @@ abstract class BaseAndroidTest : TestCase() { setSystemTime(savedCalendar) } - companion object { - // 8:00am, January 25th, 2015 (UTC) - const val FIXED_LOCAL_TIME = 1422172800000L - } + companion object } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt index c86b2706..e3be84f0 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseUserInterfaceTest.kt @@ -24,11 +24,11 @@ import android.content.Intent import androidx.test.core.app.ApplicationProvider import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.isoron.uhabits.inject.HabitsApplicationComponent import org.junit.After import org.junit.Before diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt index 325e8e0f..7299239b 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/HabitFixtures.kt @@ -18,6 +18,8 @@ */ package org.isoron.uhabits +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Frequency @@ -28,8 +30,6 @@ import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday class HabitFixtures(private val modelFactory: ModelFactory, private val habitList: HabitList) { var LONG_HABIT_ENTRIES = booleanArrayOf( @@ -55,7 +55,7 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis val habit = createEmptyHabit() habit.frequency = Frequency(3, 7) habit.color = PaletteColor(7) - val today: Timestamp = getToday() + val today: LocalDate = getToday() val marks = intArrayOf( 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, @@ -70,7 +70,7 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis val habit = createEmptyHabit() habit.frequency = Frequency(1, 2) habit.color = PaletteColor(11) - val today: Timestamp = getToday() + val today: LocalDate = getToday() val marks = intArrayOf( 0, 3, 5, 6, 7, 10, 13, 14, 15, 18, 21, 22, 23, 24, 27, 28, 30, 31, 34, 37, 39, 42, 43, 46, 47, 48, 51, 52, 54, 55, 57, 59, 62, 65, 68, 71, 73, 76, 79, @@ -108,10 +108,10 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis unit = "pages" } habitList.add(habit) - var timestamp: Timestamp = getToday() + var date: LocalDate = getToday() for (value in LONG_NUMERICAL_HABIT_ENTRIES) { - habit.originalEntries.add(Entry(timestamp, value)) - timestamp = timestamp.minus(1) + habit.originalEntries.add(Entry(date, value)) + date = date.minus(1) } habit.recompute() return habit @@ -124,10 +124,10 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis frequency = Frequency(2, 3) } habitList.add(habit) - var timestamp: Timestamp = getToday() + var date: LocalDate = getToday() for (c in LONG_HABIT_ENTRIES) { - if (c) habit.originalEntries.add(Entry(timestamp, YES_MANUAL)) - timestamp = timestamp.minus(1) + if (c) habit.originalEntries.add(Entry(date, YES_MANUAL)) + date = date.minus(1) } habit.recompute() return habit diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryPanelViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryPanelViewTest.kt index fea9a85b..0e739c12 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryPanelViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/EntryPanelViewTest.kt @@ -23,11 +23,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.core.models.Entry.Companion.NO 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.Timestamp import org.isoron.uhabits.utils.PaletteUtils import org.junit.After import org.junit.Before @@ -76,22 +76,22 @@ class EntryPanelViewTest : BaseViewTest() { @Test fun testToggle() { - val timestamps = mutableListOf() - view.onToggle = { t, _, _ -> timestamps.add(t) } + val dates = mutableListOf() + view.onToggle = { t, _, _ -> dates.add(t) } view.buttons[0].performLongClick() view.buttons[2].performLongClick() view.buttons[3].performLongClick() - assertThat(timestamps, equalTo(listOf(day(0), day(2), day(3)))) + assertThat(dates, equalTo(listOf(day(0), day(2), day(3)))) } @Test fun testToggle_withOffset() { - val timestamps = mutableListOf() + val dates = mutableListOf() view.dataOffset = 3 - view.onToggle = { t, _, _ -> timestamps += t } + view.onToggle = { t, _, _ -> dates += t } view.buttons[0].performLongClick() view.buttons[2].performLongClick() view.buttons[3].performLongClick() - assertThat(timestamps, equalTo(listOf(day(3), day(5), day(6)))) + assertThat(dates, equalTo(listOf(day(3), day(5), day(6)))) } } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt index d6faf683..0ca25f50 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.kt @@ -21,12 +21,12 @@ package org.isoron.uhabits.activities.habits.list.views import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.PaletteColor -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils import org.junit.Test import org.junit.runner.RunWith @@ -38,7 +38,7 @@ class HabitCardViewTest : BaseViewTest() { private lateinit var view: HabitCardView private lateinit var habit1: Habit private lateinit var habit2: Habit - private lateinit var today: Timestamp + private lateinit var today: LocalDate override fun setUp() { super.setUp() @@ -46,7 +46,7 @@ class HabitCardViewTest : BaseViewTest() { habit1 = fixtures.createLongHabit() habit2 = fixtures.createLongNumericalHabit() - today = DateUtils.getTodayWithOffset() + today = getToday() val entries = habit1 .computedEntries diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt index 9e9a7001..c66b22e6 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelViewTest.kt @@ -23,9 +23,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.core.models.NumericalHabitType -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.utils.PaletteUtils import org.junit.After import org.junit.Before @@ -75,22 +75,22 @@ class NumberPanelViewTest : BaseViewTest() { @Test fun testEdit() { - val timestamps = mutableListOf() - view.onEdit = { t -> timestamps.plusAssign(t) } + val dates = mutableListOf() + view.onEdit = { t -> dates.plusAssign(t) } view.buttons[0].performLongClick() view.buttons[2].performLongClick() view.buttons[3].performLongClick() - assertThat(timestamps, equalTo(listOf(day(0), day(2), day(3)))) + assertThat(dates, equalTo(listOf(day(0), day(2), day(3)))) } @Test fun testEdit_withOffset() { - val timestamps = mutableListOf() + val dates = mutableListOf() view.dataOffset = 3 - view.onEdit = { t -> timestamps += t } + view.onEdit = { t -> dates += t } view.buttons[0].performLongClick() view.buttons[2].performLongClick() view.buttons[3].performLongClick() - assertThat(timestamps, equalTo(listOf(day(3), day(5), day(6)))) + assertThat(dates, equalTo(listOf(day(3), day(5), day(6)))) } } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCardViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCardViewTest.kt index 47a19393..40b80f9d 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCardViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCardViewTest.kt @@ -22,6 +22,7 @@ import android.view.LayoutInflater import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest +import org.isoron.platform.time.DayOfWeek import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter @@ -47,7 +48,7 @@ class FrequencyCardViewTest : BaseViewTest() { view.setState( FrequencyCardPresenter.buildState( habit = habit, - firstWeekday = 0, + firstWeekday = DayOfWeek.SUNDAY, theme = LightTheme() ) ) diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt index fade863c..1ad3e759 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/performance/PerformanceTest.kt @@ -20,12 +20,11 @@ package org.isoron.uhabits.performance import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.BaseAndroidTest import org.isoron.uhabits.core.commands.CreateHabitCommand import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.models.Timestamp.Companion.DAY_LENGTH import org.isoron.uhabits.core.models.sqlite.SQLModelFactory import org.junit.Ignore import org.junit.Test @@ -59,9 +58,10 @@ class PerformanceTest : BaseAndroidTest() { val db = (modelFactory as SQLModelFactory).database db.beginTransaction() val habit = fixtures.createEmptyHabit() + var date = LocalDate(2000, 1, 1) for (i in 0..4999) { - val timestamp: Timestamp = Timestamp(i * DAY_LENGTH) - CreateRepetitionCommand(habitList, habit, timestamp, 1, "").run() + CreateRepetitionCommand(habitList, habit, date, 1, "").run() + date = date.plus(1) } db.setTransactionSuccessful() db.endTransaction() diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt index a6127a15..3f80a466 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.kt @@ -27,13 +27,13 @@ import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.junit.Test import org.junit.runner.RunWith @@ -43,12 +43,12 @@ class CheckmarkWidgetTest : BaseViewTest() { private lateinit var habit: Habit private lateinit var entries: EntryList private lateinit var view: FrameLayout - private lateinit var today: Timestamp + private lateinit var today: LocalDate override fun setUp() { super.setUp() setTheme(R.style.WidgetTheme) - today = getTodayWithOffset() + today = getToday() prefs.widgetOpacity = 255 prefs.isSkipEnabled = true habit = fixtures.createVeryLongHabit() diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt index 1abc588d..d1f93958 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.kt @@ -21,12 +21,12 @@ package org.isoron.uhabits.widgets import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest +import org.isoron.platform.time.DayOfWeek import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Habit import org.junit.Test import org.junit.runner.RunWith -import java.util.Calendar @RunWith(AndroidJUnit4::class) @MediumTest @@ -38,7 +38,7 @@ class FrequencyWidgetTest : BaseViewTest() { setTheme(R.style.WidgetTheme) prefs.widgetOpacity = 255 habit = fixtures.createVeryLongHabit() - val widget = FrequencyWidget(targetContext, 0, habit, Calendar.SUNDAY) + val widget = FrequencyWidget(targetContext, 0, habit, DayOfWeek.SUNDAY) view = convertToView(widget, 400, 400) } diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt index c60ab25d..abd2428a 100644 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.kt @@ -20,9 +20,9 @@ package org.isoron.uhabits.widgets.views import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest +import org.isoron.platform.time.getToday import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.R -import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor import org.junit.Before import org.junit.Ignore @@ -43,7 +43,7 @@ class CheckmarkWidgetViewTest : BaseViewTest() { val habit = fixtures.createShortHabit() val computedEntries = habit.computedEntries val scores = habit.scores - val today = getTodayWithOffset() + val today = getToday() val score = scores[today].value view = CheckmarkWidgetView(targetContext).apply { activeColor = getAndroidTestColor(0) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt index cf481d39..56ad774f 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/HabitsApplication.kt @@ -21,10 +21,11 @@ package org.isoron.uhabits import android.app.Application import android.content.Context +import org.isoron.platform.time.computeToday +import org.isoron.platform.time.setToday import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.ui.NotificationTray -import org.isoron.uhabits.core.utils.DateUtils.Companion.setStartDayOffset import org.isoron.uhabits.inject.AppContextModule import org.isoron.uhabits.inject.DaggerHabitsApplicationComponent import org.isoron.uhabits.inject.HabitsApplicationComponent @@ -70,11 +71,7 @@ class HabitsApplication : Application() { val prefs = component.preferences prefs.lastAppVersion = BuildConfig.VERSION_CODE - if (prefs.isMidnightDelayEnabled) { - setStartDayOffset(3, 0) - } else { - setStartDayOffset(0, 0) - } + setToday(computeToday(component.preferences.midnightDelayHours, 0)) val habitList = component.habitList for (h in habitList) h.recompute() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt index 75de6212..3b987b05 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.kt @@ -24,6 +24,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import org.isoron.platform.gui.AndroidDataView import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.getToday import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R import org.isoron.uhabits.activities.AndroidThemeSwitcher @@ -35,7 +36,6 @@ import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter import org.isoron.uhabits.core.ui.views.HistoryChart import org.isoron.uhabits.core.ui.views.LightTheme import org.isoron.uhabits.core.ui.views.OnDateClickedListener -import org.isoron.uhabits.core.utils.DateUtils import java.util.Locale import kotlin.math.min @@ -67,7 +67,7 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener { defaultSquare = HistoryChart.Square.OFF, notesIndicators = emptyList(), theme = themeSwitcher.currentTheme, - today = DateUtils.getTodayWithOffset().toLocalDate(), + today = getToday(), onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {}, padding = 10.0 ) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.kt index fc1489ec..b175d5c8 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.kt @@ -24,10 +24,11 @@ import android.content.DialogInterface.OnMultiChoiceClickListener import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDialogFragment +import org.isoron.platform.time.DayOfWeek +import org.isoron.platform.time.JavaLocalDateFormatter import org.isoron.uhabits.R import org.isoron.uhabits.core.models.WeekdayList -import org.isoron.uhabits.core.utils.DateUtils -import java.util.Calendar +import java.util.Locale /** * Dialog that allows the user to pick one or more days of the week. @@ -65,7 +66,7 @@ class WeekdayPickerDialog : builder .setTitle(R.string.select_weekdays) .setMultiChoiceItems( - DateUtils.getLongWeekdayNames(Calendar.SATURDAY), + JavaLocalDateFormatter(Locale.getDefault()).longWeekdayNames(DayOfWeek.SATURDAY), selectedDays, this ) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt index 5b05e77c..6812b024 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.kt @@ -23,19 +23,15 @@ import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF import android.util.AttributeSet +import org.isoron.platform.time.DayOfWeek +import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.countWeekdayOccurrencesInMonth +import org.isoron.platform.time.getToday +import org.isoron.platform.time.getWeekdaySequence import org.isoron.uhabits.R -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset -import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence -import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaysInMonth import org.isoron.uhabits.utils.ColorUtils.mixColors import org.isoron.uhabits.utils.StyledResources -import org.isoron.uhabits.utils.toSimpleDataFormat -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.GregorianCalendar import java.util.Locale import java.util.Random import kotlin.collections.HashMap @@ -46,8 +42,7 @@ import kotlin.math.roundToInt class FrequencyChart : ScrollableChart { private var pGrid: Paint? = null private var em = 0f - private var dfMonth: SimpleDateFormat? = null - private var dfYear: SimpleDateFormat? = null + private var dateFormatter: JavaLocalDateFormatter? = null private var pText: Paint? = null private var pGraph: Paint? = null private var rect: RectF? = null @@ -62,9 +57,9 @@ class FrequencyChart : ScrollableChart { private lateinit var colors: IntArray private var primaryColor = 0 private var isBackgroundTransparent = false - private lateinit var frequency: HashMap> + private lateinit var frequency: HashMap> private var maxFreq = 0 - private var firstWeekday = Calendar.SUNDAY + private var firstWeekday: DayOfWeek = DayOfWeek.SUNDAY private var isNumerical: Boolean = false constructor(context: Context?) : super(context) { @@ -87,18 +82,18 @@ class FrequencyChart : ScrollableChart { postInvalidate() } - fun setFrequency(frequency: java.util.HashMap>) { + fun setFrequency(frequency: java.util.HashMap>) { this.frequency = frequency maxFreq = getMaxFreq(frequency) postInvalidate() } - fun setFirstWeekday(firstWeekday: Int) { + fun setFirstWeekday(firstWeekday: DayOfWeek) { this.firstWeekday = firstWeekday postInvalidate() } - private fun getMaxFreq(frequency: HashMap>): Int { + private fun getMaxFreq(frequency: HashMap>): Int { var maxValue = 1 for (values in frequency.values) for (value in values) maxValue = max( value, @@ -131,15 +126,14 @@ class FrequencyChart : ScrollableChart { pText!!.color = textColor pGraph!!.color = primaryColor prevRect!!.setEmpty() - val currentDate: GregorianCalendar = - getStartOfTodayCalendarWithOffset() - currentDate[Calendar.DAY_OF_MONTH] = 1 - currentDate.add(Calendar.MONTH, -nColumns + 2 - dataOffset) + val today = getToday() + var currentDate = LocalDate(today.year, today.month, 1) + currentDate = stepMonth(currentDate, -nColumns + 2 - dataOffset) for (i in 0 until nColumns - 1) { rect!![0f, 0f, columnWidth] = columnHeight.toFloat() rect!!.offset(i * columnWidth, 0f) drawColumn(canvas, rect, currentDate) - currentDate.add(Calendar.MONTH, 1) + currentDate = stepMonth(currentDate, 1) } } @@ -171,16 +165,16 @@ class FrequencyChart : ScrollableChart { internalPaddingTop = 0 } - private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { - val values = frequency[Timestamp(date)] - val weekDaysInMonth = getWeekdaysInMonth(Timestamp(date)) + private fun drawColumn(canvas: Canvas, rect: RectF?, date: LocalDate) { + val values = frequency[date] + val weekDaysInMonth = countWeekdayOccurrencesInMonth(date) val rowHeight = rect!!.height() / 8.0f prevRect!!.set(rect) - val localeWeekdayList: Array = getWeekdaySequence(firstWeekday) + val localeWeekdayList = getWeekdaySequence(firstWeekday) for (j in localeWeekdayList.indices) { rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat() rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j) - val i = localeWeekdayList[j] % 7 + val i = (localeWeekdayList[j].daysSinceSunday + 1) % 7 if (values != null) { drawMarker(canvas, rect, values[i], weekDaysInMonth[i]) } @@ -189,17 +183,17 @@ class FrequencyChart : ScrollableChart { drawFooter(canvas, rect, date) } - private fun drawFooter(canvas: Canvas, rect: RectF?, date: GregorianCalendar) { - val time = date.time + private fun drawFooter(canvas: Canvas, rect: RectF?, date: LocalDate) { + val df = dateFormatter ?: return canvas.drawText( - dfMonth!!.format(time), + df.shortMonthName(date), rect!!.centerX(), rect.centerY() - 0.1f * em, pText!! ) - if (date[Calendar.MONTH] == 1) { + if (date.month == 2) { canvas.drawText( - dfYear!!.format(time), + date.year.toString(), rect.centerX(), rect.centerY() + 0.9f * em, pText!! @@ -213,7 +207,8 @@ class FrequencyChart : ScrollableChart { pText!!.textAlign = Paint.Align.LEFT pText!!.color = textColor pGrid!!.color = gridColor - for (day in getShortWeekdayNames(firstWeekday)) { + val df = dateFormatter ?: return + for (day in df.shortWeekdayNames(firstWeekday)) { canvas.drawText( day, rGrid.right - columnWidth, @@ -251,12 +246,11 @@ class FrequencyChart : ScrollableChart { private val maxMonthWidth: Float get() { + val df = dateFormatter ?: return 0f var maxMonthWidth = 0f - val day: GregorianCalendar = - getStartOfTodayCalendarWithOffset() - for (i in 0..11) { - day[Calendar.MONTH] = i - val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + for (i in 1..12) { + val date = LocalDate(2020, i, 1) + val monthWidth = pText!!.measureText(df.shortMonthName(date)) maxMonthWidth = max(maxMonthWidth, monthWidth) } return maxMonthWidth @@ -281,13 +275,7 @@ class FrequencyChart : ScrollableChart { } private fun initDateFormats() { - if (isInEditMode) { - dfMonth = SimpleDateFormat("MMM", Locale.getDefault()) - dfYear = SimpleDateFormat("yyyy", Locale.getDefault()) - } else { - dfMonth = "MMM".toSimpleDataFormat() - dfYear = "yyyy".toSimpleDataFormat() - } + dateFormatter = JavaLocalDateFormatter(Locale.getDefault()) } private fun initRects() { @@ -295,15 +283,23 @@ class FrequencyChart : ScrollableChart { prevRect = RectF() } + private fun stepMonth(date: LocalDate, months: Int): LocalDate { + var y = date.year + var m = date.month + months + while (m < 1) { m += 12; y -= 1 } + while (m > 12) { m -= 12; y += 1 } + return LocalDate(y, m, 1) + } + fun populateWithRandomData() { - val date: GregorianCalendar = getStartOfTodayCalendar() - date[Calendar.DAY_OF_MONTH] = 1 + val today = getToday() + var date = LocalDate(today.year, today.month, 1) val rand = Random() frequency.clear() for (i in 0..39) { val values = IntArray(7) { rand.nextInt(5) }.toTypedArray() - frequency[Timestamp(date)] = values - date.add(Calendar.MONTH, -1) + frequency[date] = values + date = stepMonth(date, -1) } maxFreq = getMaxFreq(frequency) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt index 89a09e37..73ab828c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScoreChart.kt @@ -27,18 +27,14 @@ import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.RectF import android.util.AttributeSet +import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Score -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.StyledResources -import org.isoron.uhabits.utils.toSimpleDataFormat -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.GregorianCalendar import java.util.LinkedList import java.util.Locale import java.util.Random @@ -48,9 +44,7 @@ import kotlin.math.min class ScoreChart : ScrollableChart { private var pGrid: Paint? = null private var em = 0f - private var dfMonth: SimpleDateFormat? = null - private var dfDay: SimpleDateFormat? = null - private var dfYear: SimpleDateFormat? = null + private var dateFormatter: JavaLocalDateFormatter? = null private var pText: Paint? = null private var pGraph: Paint? = null private var rect: RectF? = null @@ -87,12 +81,12 @@ class ScoreChart : ScrollableChart { val random = Random() val newScores = LinkedList() var previous = 0.5 - val timestamp: Timestamp = getToday() + val today = getToday() for (i in 1..99) { val step = 0.1 var current = previous + random.nextDouble() * step * 2 - step current = max(0.0, min(1.0, current)) - newScores.add(Score(timestamp.minus(i), current)) + newScores.add(Score(today.minus(i), current)) previous = current } scores = newScores @@ -142,7 +136,7 @@ class ScoreChart : ScrollableChart { val offset = nColumns - k - 1 + dataOffset if (offset >= scores!!.size) continue val score = scores!![offset].value - val timestamp = scores!![offset].timestamp + val date = scores!![offset].date val height = (columnHeight * score).toInt() rect!![0f, 0f, baseSize.toFloat()] = baseSize.toFloat() rect!!.offset( @@ -159,7 +153,7 @@ class ScoreChart : ScrollableChart { prevRect!!.set(rect!!) rect!![0f, 0f, columnWidth] = columnHeight.toFloat() rect!!.offset(k * columnWidth, internalPaddingTop.toFloat()) - drawFooter(activeCanvas, rect, timestamp) + drawFooter(activeCanvas, rect, date) } if (activeCanvas !== canvas) canvas.drawBitmap(internalDrawingCache!!, 0f, 0f, null) } @@ -199,16 +193,15 @@ class ScoreChart : ScrollableChart { if (isTransparencyEnabled) initCache(width, height) } - private fun drawFooter(canvas: Canvas?, rect: RectF?, currentDate: Timestamp) { - val yearText = dfYear!!.format(currentDate.toJavaDate()) - val monthText = dfMonth!!.format(currentDate.toJavaDate()) - val dayText = dfDay!!.format(currentDate.toJavaDate()) - val calendar = currentDate.toCalendar() + private fun drawFooter(canvas: Canvas?, rect: RectF?, date: LocalDate) { + val df = dateFormatter ?: return + val yearText = date.year.toString() + val monthText = df.shortMonthName(date) + val dayText = date.day.toString() val text: String - val year = calendar[Calendar.YEAR] var shouldPrintYear = true if (yearText == previousYearText) shouldPrintYear = false - if (bucketSize >= 365 && year % 2 != 0) shouldPrintYear = false + if (bucketSize >= 365 && date.year % 2 != 0) shouldPrintYear = false if (skipYear > 0) { skipYear-- shouldPrintYear = false @@ -294,24 +287,22 @@ class ScoreChart : ScrollableChart { private val maxDayWidth: Float private get() { + val df = dateFormatter ?: return 0f var maxDayWidth = 0f - val day: GregorianCalendar = - getStartOfTodayCalendarWithOffset() - for (i in 0..27) { - day[Calendar.DAY_OF_MONTH] = i - val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + for (i in 1..12) { + val date = LocalDate(2020, i, 1) + val monthWidth = pText!!.measureText(df.shortMonthName(date)) maxDayWidth = max(maxDayWidth, monthWidth) } return maxDayWidth } private val maxMonthWidth: Float private get() { + val df = dateFormatter ?: return 0f var maxMonthWidth = 0f - val day: GregorianCalendar = - getStartOfTodayCalendarWithOffset() - for (i in 0..11) { - day[Calendar.MONTH] = i - val monthWidth = pText!!.measureText(dfMonth!!.format(day.time)) + for (i in 1..12) { + val date = LocalDate(2020, i, 1) + val monthWidth = pText!!.measureText(df.shortMonthName(date)) maxMonthWidth = max(maxMonthWidth, monthWidth) } return maxMonthWidth @@ -340,15 +331,7 @@ class ScoreChart : ScrollableChart { } private fun initDateFormats() { - if (isInEditMode) { - dfMonth = SimpleDateFormat("MMM", Locale.getDefault()) - dfYear = SimpleDateFormat("yyyy", Locale.getDefault()) - dfDay = SimpleDateFormat("d", Locale.getDefault()) - } else { - dfMonth = "MMM".toSimpleDataFormat() - dfYear = "yyyy".toSimpleDataFormat() - dfDay = "d".toSimpleDataFormat() - } + dateFormatter = JavaLocalDateFormatter(Locale.getDefault()) } private fun initPaints() { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt index 37080091..2ea34010 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/ScrollableChart.kt @@ -31,8 +31,6 @@ import android.widget.Scroller import kotlin.math.abs import kotlin.math.max import kotlin.math.min -import org.isoron.uhabits.core.utils.DateUtils.Companion.getMonthsSince1970 -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar abstract class ScrollableChart : View, GestureDetector.OnGestureListener, AnimatorUpdateListener { var dataOffset = 0 @@ -43,7 +41,7 @@ abstract class ScrollableChart : View, GestureDetector.OnGestureListener, Animat private lateinit var scroller: Scroller private lateinit var scrollAnimator: ValueAnimator private lateinit var scrollController: ScrollController - private var maxDataOffset = getMonthsSince1970(getStartOfTodayCalendar()) + private var maxDataOffset = 12 * 200 constructor(context: Context?) : super(context) { init(context) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt index 25676353..d3355b03 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.kt @@ -26,17 +26,16 @@ import android.graphics.RectF import android.util.AttributeSet import android.view.View import android.view.ViewGroup +import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.getToday import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Streak -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels import org.isoron.uhabits.utils.InterfaceUtils.getDimension import org.isoron.uhabits.utils.StyledResources -import java.text.DateFormat import java.util.LinkedList +import java.util.Locale import java.util.Random -import java.util.TimeZone import kotlin.math.floor import kotlin.math.max import kotlin.math.min @@ -52,7 +51,7 @@ class StreakChart : View { private var primaryColor = 0 private var streaks: List? = null private var isBackgroundTransparent = false - private var dateFormat: DateFormat? = null + private var dateFormatter: JavaLocalDateFormatter? = null private var internalWidth = 0 private var em = 0f private var maxLabelWidth = 0f @@ -78,7 +77,7 @@ class StreakChart : View { get() = floor((measuredHeight / baseSize).toDouble()).toInt() fun populateWithRandomData() { - var start: Timestamp = getToday() + var start = getToday() val streaks: MutableList = LinkedList() for (i in 0..9) { val length = Random().nextInt(100) @@ -177,8 +176,9 @@ class StreakChart : View { paint!! ) if (shouldShowLabels) { - val startLabel = dateFormat!!.format(streak.start.toJavaDate()) - val endLabel = dateFormat!!.format(streak.end.toJavaDate()) + val df = dateFormatter!! + val startLabel = df.longFormat(streak.start) + val endLabel = df.longFormat(streak.end) paint!!.color = textColors[1] paint!!.textAlign = Paint.Align.RIGHT canvas.drawText(startLabel, gap - textMargin, yOffset, paint!!) @@ -191,9 +191,7 @@ class StreakChart : View { initPaints() initColors() streaks = emptyList() - val newDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM) - if (!isInEditMode) newDateFormat.timeZone = TimeZone.getTimeZone("GMT") - dateFormat = newDateFormat + dateFormatter = JavaLocalDateFormatter(Locale.getDefault()) rect = RectF() baseSize = resources.getDimensionPixelSize(R.dimen.baseSize) } @@ -234,11 +232,12 @@ class StreakChart : View { maxLength = 0 minLength = Long.MAX_VALUE shouldShowLabels = true + val df = dateFormatter ?: return for (s in streaks!!) { maxLength = max(maxLength, s.length.toLong()) minLength = min(minLength, s.length.toLong()) - val lw1 = paint!!.measureText(dateFormat!!.format(s.start.toJavaDate())) - val lw2 = paint!!.measureText(dateFormat!!.format(s.end.toJavaDate())) + val lw1 = paint!!.measureText(df.longFormat(s.start)) + val lw2 = paint!!.measureText(df.longFormat(s.end)) maxLabelWidth = max(maxLabelWidth, max(lw1, lw2)) } if (internalWidth - 2 * maxLabelWidth < internalWidth * 0.25f) { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt index 31e98274..20eacb18 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.kt @@ -32,10 +32,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat.checkSelfPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.BaseExceptionHandler import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK @@ -177,10 +177,11 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener { if (intent == null) return if (intent.action == ACTION_EDIT) { val habitId = intent.extras?.getLong("habit") - val timestamp = intent.extras?.getLong("timestamp") - if (habitId != null && timestamp != null) { + val timestampMillis = intent.extras?.getLong("timestamp") + if (habitId != null && timestampMillis != null) { val habit = appComponent.habitList.getById(habitId)!! - component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp), 0f, 0f) + val date = LocalDate.fromUnixTime(timestampMillis) + component.listHabitsBehavior.onEdit(habit, date, 0f, 0f) } } intent = null diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt index 8bb61f83..826b01eb 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.kt @@ -25,6 +25,7 @@ import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import dagger.Lazy +import org.isoron.platform.time.getToday import org.isoron.uhabits.R import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter import org.isoron.uhabits.activities.habits.list.views.HabitCardListController @@ -32,7 +33,6 @@ import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityScope import javax.inject.Inject @@ -118,7 +118,7 @@ class ListHabitsSelectionMenu @Inject constructor( R.id.action_notify -> { for (h in listAdapter.selected) - notificationTray.show(h, DateUtils.getToday(), 0) + notificationTray.show(h, getToday(), 0) return true } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt index 4d4c5315..0213b1d3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.kt @@ -198,7 +198,7 @@ class CheckmarkButtonView( canvas = canvas, color = color, size = em, - notes = notes, + notes = notes ) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt index aca3dc79..cae55074 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.kt @@ -20,10 +20,10 @@ package org.isoron.uhabits.activities.habits.list.views import android.content.Context +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.inject.ActivityContext import javax.inject.Inject @@ -60,13 +60,13 @@ class CheckmarkPanelView( setupButtons() } - var onToggle: (Timestamp, Int, String) -> Unit = { _, _, _ -> } + var onToggle: (LocalDate, Int, String) -> Unit = { _, _, _ -> } set(value) { field = value setupButtons() } - var onEdit: (Timestamp) -> Unit = { _ -> } + var onEdit: (LocalDate) -> Unit = { _ -> } set(value) { field = value setupButtons() @@ -76,10 +76,10 @@ class CheckmarkPanelView( @Synchronized override fun setupButtons() { - val today = DateUtils.getTodayWithOffset() + val today = getToday() buttons.forEachIndexed { index, button -> - val timestamp = today.minus(index + dataOffset) + val date = today.minus(index + dataOffset) button.value = when { index + dataOffset < values.size -> values[index + dataOffset] else -> UNKNOWN @@ -89,8 +89,8 @@ class CheckmarkPanelView( else -> "" } button.color = color - button.onToggle = { value, notes -> onToggle(timestamp, value, notes) } - button.onEdit = { onEdit(timestamp) } + button.onToggle = { value, notes -> onToggle(date, value, notes) } + button.onEdit = { onEdit(date) } } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt index 66404440..6c635694 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.kt @@ -35,13 +35,13 @@ import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import org.isoron.platform.gui.toInt +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.R import org.isoron.uhabits.activities.common.views.RingView import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.ModelObservable -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.utils.currentTheme import org.isoron.uhabits.utils.dp @@ -153,13 +153,13 @@ class HabitCardView( } checkmarkPanel = checkmarkPanelFactory.create().apply { - onToggle = { timestamp, value, notes -> - triggerRipple(timestamp) - val location = getAbsoluteButtonLocation(timestamp) + onToggle = { date, value, notes -> + triggerRipple(date) + val location = getAbsoluteButtonLocation(date) habit?.let { behavior.onToggle( it, - timestamp, + date, value, notes, location.x, @@ -167,19 +167,19 @@ class HabitCardView( ) } } - onEdit = { timestamp -> - triggerRipple(timestamp) - val location = getAbsoluteButtonLocation(timestamp) - habit?.let { behavior.onEdit(it, timestamp, location.x, location.y) } + onEdit = { date -> + triggerRipple(date) + val location = getAbsoluteButtonLocation(date) + habit?.let { behavior.onEdit(it, date, location.x, location.y) } } } numberPanel = numberPanelFactory.create().apply { visibility = GONE - onEdit = { timestamp -> - triggerRipple(timestamp) - val location = getAbsoluteButtonLocation(timestamp) - habit?.let { behavior.onEdit(it, timestamp, location.x, location.y) } + onEdit = { date -> + triggerRipple(date) + val location = getAbsoluteButtonLocation(date) + habit?.let { behavior.onEdit(it, date, location.x, location.y) } } } @@ -218,14 +218,14 @@ class HabitCardView( updateBackground(isSelected) } - fun triggerRipple(timestamp: Timestamp) { - val location = getRelativeButtonLocation(timestamp) + fun triggerRipple(date: LocalDate) { + val location = getRelativeButtonLocation(date) triggerRipple(location.x, location.y) } - private fun getRelativeButtonLocation(timestamp: Timestamp): PointF { - val today = DateUtils.getTodayWithOffset() - val offset = timestamp.daysUntil(today) - dataOffset + private fun getRelativeButtonLocation(date: LocalDate): PointF { + val today = getToday() + val offset = date.daysUntil(today) - dataOffset val panel = when (habit!!.isNumerical) { true -> numberPanel false -> checkmarkPanel @@ -236,10 +236,10 @@ class HabitCardView( return PointF(x, y) } - private fun getAbsoluteButtonLocation(timestamp: Timestamp): PointF { + private fun getAbsoluteButtonLocation(date: LocalDate): PointF { val containerLocation = IntArray(2) this.getLocationInWindow(containerLocation) - val relButtonLocation = getRelativeButtonLocation(timestamp) + val relButtonLocation = getRelativeButtonLocation(date) val windowInsets = rootWindowInsets val xInset = windowInsets?.displayCutout?.safeInsetLeft ?: 0 val yInset = if (SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) { diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt index 1fb5cf56..4c13df95 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.kt @@ -27,17 +27,18 @@ import android.graphics.RectF import android.graphics.Typeface import android.text.TextPaint import android.view.View.MeasureSpec.EXACTLY +import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.getToday import org.isoron.uhabits.R import org.isoron.uhabits.activities.common.views.ScrollableChart import org.isoron.uhabits.core.preferences.Preferences -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.MidnightTimer import org.isoron.uhabits.utils.dim import org.isoron.uhabits.utils.dp import org.isoron.uhabits.utils.isRTL import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.toMeasureSpec -import java.util.GregorianCalendar +import java.util.Locale class HeaderView( context: Context, @@ -102,6 +103,7 @@ class HeaderView( private inner class Drawer { private val rect = RectF() + private val dateFormatter = JavaLocalDateFormatter(Locale.getDefault()) private val paint = TextPaint().apply { color = Color.BLACK isAntiAlias = true @@ -112,12 +114,11 @@ class HeaderView( } fun draw(canvas: Canvas) { - val day = DateUtils.getStartOfTodayCalendarWithOffset() + val today = getToday() val width = dim(R.dimen.checkmarkWidth) val height = dim(R.dimen.checkmarkHeight) val isReversed = prefs.isCheckmarkSequenceReversed - day.add(GregorianCalendar.DAY_OF_MONTH, -dataOffset) val em = paint.measureText("m") repeat(buttonCount) { index -> @@ -139,12 +140,13 @@ class HeaderView( ) } + val date = today.minus(index + dataOffset) + val dayOfWeek = dateFormatter.shortWeekdayName(date).uppercase() + val dayOfMonth = date.day.toString() val y1 = rect.centerY() - 0.25 * em val y2 = rect.centerY() + 1.25 * em - val lines = DateUtils.formatHeaderDate(day).uppercase().split("\n") - canvas.drawText(lines[0], rect.centerX(), y1.toFloat(), paint) - canvas.drawText(lines[1], rect.centerX(), y2.toFloat(), paint) - day.add(GregorianCalendar.DAY_OF_MONTH, -1) + canvas.drawText(dayOfWeek, rect.centerX(), y1.toFloat(), paint) + canvas.drawText(dayOfMonth, rect.centerX(), y2.toFloat(), paint) } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt index 959e9e1c..1d81f3f5 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberButtonView.kt @@ -241,7 +241,7 @@ class NumberButtonView( canvas = canvas, color = color, size = em, - notes = notes, + notes = notes ) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt index eb4f8d59..b63f45b9 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/NumberPanelView.kt @@ -20,10 +20,10 @@ package org.isoron.uhabits.activities.habits.list.views import android.content.Context +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.NumericalHabitType -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.inject.ActivityContext import javax.inject.Inject @@ -78,7 +78,7 @@ class NumberPanelView( setupButtons() } - var onEdit: (Timestamp) -> Unit = { _ -> } + var onEdit: (LocalDate) -> Unit = { _ -> } set(value) { field = value setupButtons() @@ -88,10 +88,10 @@ class NumberPanelView( @Synchronized override fun setupButtons() { - val today = DateUtils.getTodayWithOffset() + val today = getToday() buttons.forEachIndexed { index, button -> - val timestamp = today.minus(index + dataOffset) + val date = today.minus(index + dataOffset) button.value = when { index + dataOffset < values.size -> values[index + dataOffset] else -> 0.0 @@ -104,7 +104,7 @@ class NumberPanelView( button.targetType = targetType button.threshold = threshold button.units = units - button.onEdit = { onEdit(timestamp) } + button.onEdit = { onEdit(date) } } } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt index c89db82b..2ee9eb98 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt @@ -41,7 +41,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.getDefault())).apply { series = mutableListOf(state.entries.map { it.value / 1000.0 }) colors = mutableListOf(theme.color(state.color.paletteIndex)) - axis = state.entries.map { it.timestamp.toLocalDate() } + axis = state.entries.map { it.date } } binding.chart.resetDataOffset() binding.chart.postInvalidate() diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt index a6cb3442..63f06679 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.kt @@ -36,6 +36,8 @@ import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import androidx.recyclerview.widget.RecyclerView +import org.isoron.platform.time.DayOfWeek +import org.isoron.platform.time.JavaLocalDateFormatter import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R import org.isoron.uhabits.activities.habits.list.RESULT_BUG_REPORT @@ -45,14 +47,13 @@ import org.isoron.uhabits.activities.habits.list.RESULT_IMPORT_DATA import org.isoron.uhabits.activities.habits.list.RESULT_REPAIR_DB import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.NotificationTray -import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel import org.isoron.uhabits.notifications.RingtoneManager import org.isoron.uhabits.utils.StyledResources import org.isoron.uhabits.utils.applyBottomInset import org.isoron.uhabits.utils.startActivitySafely import org.isoron.uhabits.widgets.WidgetUpdater -import java.util.Calendar +import java.util.Locale class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener { private var sharedPrefs: SharedPreferences? = null @@ -114,7 +115,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis override fun onCreateRecyclerView( inflater: LayoutInflater?, parent: ViewGroup?, - savedInstanceState: Bundle?, + savedInstanceState: Bundle? ): RecyclerView? { return super.onCreateRecyclerView(inflater, parent, savedInstanceState) .also { it.applyBottomInset() } @@ -172,7 +173,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis private fun updateWeekdayPreference() { val weekdayPref = findPreference("pref_first_weekday") as ListPreference val currentFirstWeekday = prefs.firstWeekday.daysSinceSunday + 1 - val dayNames = getLongWeekdayNames(Calendar.SATURDAY) + val dayNames = JavaLocalDateFormatter(Locale.getDefault()).longWeekdayNames(DayOfWeek.SATURDAY) val dayValues = arrayOf("7", "1", "2", "3", "4", "5", "6") weekdayPref.entries = dayNames weekdayPref.entryValues = dayValues diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.kt b/uhabits-android/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.kt index 6cceb964..ad4d7a71 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.kt @@ -23,10 +23,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import dagger.Component +import org.isoron.platform.time.getToday import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.ui.widgets.WidgetBehavior -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.inject.HabitsApplicationComponent import org.isoron.uhabits.receivers.ReceiverScope @@ -51,15 +51,15 @@ class FireSettingReceiver : BroadcastReceiver() { .build() allHabits = app.component.habitList val args = SettingUtils.parseIntent(intent, allHabits) ?: return - val timestamp = DateUtils.getTodayWithOffset() + val today = getToday() val controller = component.widgetController when (args.action) { - ACTION_CHECK -> controller.onAddRepetition(args.habit, timestamp) - ACTION_UNCHECK -> controller.onRemoveRepetition(args.habit, timestamp) - ACTION_TOGGLE -> controller.onToggleRepetition(args.habit, timestamp) - ACTION_INCREMENT -> controller.onIncrement(args.habit, timestamp, 1000) - ACTION_DECREMENT -> controller.onDecrement(args.habit, timestamp, 1000) + ACTION_CHECK -> controller.onAddRepetition(args.habit, today) + ACTION_UNCHECK -> controller.onRemoveRepetition(args.habit, today) + ACTION_TOGGLE -> controller.onToggleRepetition(args.habit, today) + ACTION_INCREMENT -> controller.onIncrement(args.habit, today, 1000) + ACTION_DECREMENT -> controller.onDecrement(args.habit, today, 1000) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentParser.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentParser.kt index bddb1967..dc3fe5a8 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentParser.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/IntentParser.kt @@ -22,25 +22,28 @@ package org.isoron.uhabits.intents import android.content.ContentUris.parseId import android.content.Intent import android.net.Uri +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils import javax.inject.Inject @AppScope class IntentParser -@Inject constructor(private val habits: HabitList) { +@Inject constructor( + private val habits: HabitList +) { fun parseCheckmarkIntent(intent: Intent): CheckmarkIntentData { val uri = intent.data ?: throw IllegalArgumentException("uri is null") - return CheckmarkIntentData(parseHabit(uri), parseTimestamp(intent)) + return CheckmarkIntentData(parseHabit(uri), parseDate(intent)) } fun copyIntentData(source: Intent, destination: Intent) { destination.data = source.data - destination.putExtra("timestamp", source.getLongExtra("timestamp", DateUtils.getTodayWithOffset().unixTime)) + val todayMillis = getToday().unixTime + destination.putExtra("timestamp", source.getLongExtra("timestamp", todayMillis)) } private fun parseHabit(uri: Uri): Habit { @@ -48,17 +51,17 @@ class IntentParser ?: throw IllegalArgumentException("habit not found") } - private fun parseTimestamp(intent: Intent): Timestamp { - val today = DateUtils.getTodayWithOffset().unixTime - var timestamp = intent.getLongExtra("timestamp", today) - timestamp = DateUtils.getStartOfDay(timestamp) + private fun parseDate(intent: Intent): LocalDate { + val todayMillis = getToday().unixTime + var timestamp = intent.getLongExtra("timestamp", todayMillis) + timestamp = LocalDate.fromUnixTime(timestamp).unixTime - if (timestamp < 0 || timestamp > today) { + if (timestamp < 0 || timestamp > todayMillis) { throw IllegalArgumentException("timestamp is not valid") } - return Timestamp(timestamp) + return LocalDate.fromUnixTime(timestamp) } - class CheckmarkIntentData(var habit: Habit, var timestamp: Timestamp) + class CheckmarkIntentData(var habit: Habit, var date: LocalDate) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt b/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt index 30ab5b7a..d9fe11b4 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.kt @@ -29,11 +29,11 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.activities.habits.list.ListHabitsActivity import org.isoron.uhabits.activities.habits.show.ShowHabitActivity import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.inject.AppContext import org.isoron.uhabits.receivers.ReminderReceiver import org.isoron.uhabits.receivers.WidgetReceiver @@ -46,14 +46,14 @@ class PendingIntentFactory private val intentFactory: IntentFactory ) { - fun addCheckmark(habit: Habit, timestamp: Timestamp?): PendingIntent = + fun addCheckmark(habit: Habit, date: LocalDate?): PendingIntent = getBroadcast( context, 1, Intent(context, WidgetReceiver::class.java).apply { data = Uri.parse(habit.uriString) action = WidgetReceiver.ACTION_ADD_REPETITION - if (timestamp != null) putExtra("timestamp", timestamp.unixTime) + if (date != null) putExtra("timestamp", date.unixTime) }, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT ) @@ -69,14 +69,14 @@ class PendingIntentFactory FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT ) - fun removeRepetition(habit: Habit, timestamp: Timestamp?): PendingIntent = + fun removeRepetition(habit: Habit, date: LocalDate?): PendingIntent = getBroadcast( context, 3, Intent(context, WidgetReceiver::class.java).apply { action = WidgetReceiver.ACTION_REMOVE_REPETITION data = Uri.parse(habit.uriString) - if (timestamp != null) putExtra("timestamp", timestamp.unixTime) + if (date != null) putExtra("timestamp", date.unixTime) }, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT ) @@ -156,14 +156,14 @@ class PendingIntentFactory FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT ) - fun showNumberPicker(habit: Habit, timestamp: Timestamp): PendingIntent? { + fun showNumberPicker(habit: Habit, date: LocalDate): PendingIntent? { return getActivity( context, (habit.id!! % Integer.MAX_VALUE).toInt() + 1, Intent(context, ListHabitsActivity::class.java).apply { action = ListHabitsActivity.ACTION_EDIT putExtra("habit", habit.id) - putExtra("timestamp", timestamp.unixTime) + putExtra("timestamp", date.unixTime) }, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT ) @@ -180,9 +180,9 @@ class PendingIntentFactory ) } - fun showNumberPickerFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply { + fun showNumberPickerFillIn(habit: Habit, date: LocalDate) = Intent().apply { putExtra("habit", habit.id) - putExtra("timestamp", timestamp.unixTime) + putExtra("timestamp", date.unixTime) } private fun getIntentTemplateFlags(): Int { @@ -203,8 +203,8 @@ class PendingIntentFactory getIntentTemplateFlags() ) - fun toggleCheckmarkFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply { + fun toggleCheckmarkFillIn(habit: Habit, date: LocalDate) = Intent().apply { data = Uri.parse(habit.uriString) - putExtra("timestamp", timestamp.unixTime) + putExtra("timestamp", date.unixTime) } } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt index bb3b675d..6a53a25e 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/notifications/AndroidNotificationTray.kt @@ -32,10 +32,10 @@ import androidx.core.app.NotificationCompat.Action import androidx.core.app.NotificationCompat.Builder import androidx.core.app.NotificationCompat.WearableExtender import androidx.core.app.NotificationManagerCompat +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.R import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.NotificationTray import org.isoron.uhabits.inject.AppContext @@ -65,11 +65,11 @@ class AndroidNotificationTray override fun showNotification( habit: Habit, notificationId: Int, - timestamp: Timestamp, + date: LocalDate, reminderTime: Long ) { val notificationManager = NotificationManagerCompat.from(context) - val notification = buildNotification(habit, reminderTime, timestamp) + val notification = buildNotification(habit, reminderTime, date) createAndroidNotificationChannel(context) try { notificationManager.notify(notificationId, notification) @@ -82,7 +82,7 @@ class AndroidNotificationTray val n = buildNotification( habit, reminderTime, - timestamp, + date, disableSound = true ) notificationManager.notify(notificationId, n) @@ -93,25 +93,25 @@ class AndroidNotificationTray fun buildNotification( habit: Habit, reminderTime: Long, - timestamp: Timestamp, + date: LocalDate, disableSound: Boolean = false ): Notification { val addRepetitionAction = Action( R.drawable.ic_action_check, context.getString(R.string.yes), - pendingIntents.addCheckmark(habit, timestamp) + pendingIntents.addCheckmark(habit, date) ) val removeRepetitionAction = Action( R.drawable.ic_action_cancel, context.getString(R.string.no), - pendingIntents.removeRepetition(habit, timestamp) + pendingIntents.removeRepetition(habit, date) ) val enterAction = Action( R.drawable.ic_action_check, context.getString(R.string.enter), - pendingIntents.showNumberPicker(habit, timestamp) + pendingIntents.showNumberPicker(habit, date) ) val wearableBg = decodeResource(context.resources, R.drawable.stripe) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt index 9b820992..3acc7261 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderController.kt @@ -21,13 +21,13 @@ package org.isoron.uhabits.receivers import android.content.Context import android.content.Intent import android.net.Uri +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.ui.NotificationTray -import org.isoron.uhabits.core.utils.DateUtils.Companion.getUpcomingTimeInMillis +import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.notifications.SnoozeDelayPickerActivity import javax.inject.Inject @@ -43,10 +43,10 @@ class ReminderController @Inject constructor( fun onShowReminder( habit: Habit, - timestamp: Timestamp, + date: LocalDate, reminderTime: Long ) { - notificationTray.show(habit, timestamp, reminderTime) + notificationTray.show(habit, date, reminderTime) reminderScheduler.scheduleAll() } @@ -60,7 +60,7 @@ class ReminderController @Inject constructor( } fun onSnoozeTimePicked(habit: Habit?, hour: Int, minute: Int) { - val time: Long = getUpcomingTimeInMillis(hour, minute) + val time: Long = DateUtils.getUpcomingTimeInMillis(hour, minute) reminderScheduler.scheduleAtTime(habit!!, time) notificationTray.cancel(habit) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt index a3eb22e5..0b3cb366 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.kt @@ -25,10 +25,10 @@ import android.content.Intent import android.os.Build import android.os.Build.VERSION.SDK_INT import android.util.Log +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayWithOffset /** * The Android BroadcastReceiver for Loop Habit Tracker. @@ -47,11 +47,11 @@ class ReminderReceiver : BroadcastReceiver() { val reminderController = appComponent.reminderController Log.i(TAG, String.format("Received intent: %s", intent.toString())) var habit: Habit? = null - val today: Long = getStartOfTodayWithOffset() + val todayMillis = getToday().unixTime val data = intent.data if (data != null) habit = habits.getById(ContentUris.parseId(data)) - val timestamp = intent.getLongExtra("timestamp", today) - val reminderTime = intent.getLongExtra("reminderTime", today) + val timestamp = intent.getLongExtra("timestamp", todayMillis) + val reminderTime = intent.getLongExtra("reminderTime", todayMillis) try { when (intent.action) { ACTION_SHOW_REMINDER -> { @@ -67,7 +67,7 @@ class ReminderReceiver : BroadcastReceiver() { ) reminderController.onShowReminder( habit, - Timestamp(timestamp), + LocalDate.fromUnixTime(timestamp), reminderTime ) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.kt b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.kt index 99f7225a..da0dd6c3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.kt @@ -23,6 +23,8 @@ import android.content.Context import android.content.Intent import android.util.Log import dagger.Component +import org.isoron.platform.time.computeToday +import org.isoron.platform.time.setToday import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.core.ui.widgets.WidgetBehavior import org.isoron.uhabits.inject.HabitsApplicationComponent @@ -57,45 +59,46 @@ class WidgetReceiver : BroadcastReceiver() { Log.d( TAG, String.format( - "onAddRepetition habit=%d timestamp=%d", + "onAddRepetition habit=%d date=%s", data!!.habit.id, - data.timestamp.unixTime + data.date ) ) controller.onAddRepetition( data.habit, - data.timestamp + data.date ) } ACTION_TOGGLE_REPETITION -> { Log.d( TAG, String.format( - "onToggleRepetition habit=%d timestamp=%d", + "onToggleRepetition habit=%d date=%s", data!!.habit.id, - data.timestamp.unixTime + data.date ) ) controller.onToggleRepetition( data.habit, - data.timestamp + data.date ) } ACTION_REMOVE_REPETITION -> { Log.d( TAG, String.format( - "onRemoveRepetition habit=%d timestamp=%d", + "onRemoveRepetition habit=%d date=%s", data!!.habit.id, - data.timestamp.unixTime + data.date ) ) controller.onRemoveRepetition( data.habit, - data.timestamp + data.date ) } ACTION_UPDATE_WIDGETS_VALUE -> { + setToday(computeToday(prefs.midnightDelayHours, 0)) widgetUpdater.updateWidgets() widgetUpdater.scheduleStartDayWidgetUpdate() } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.kt b/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.kt index de2f1b7b..c780a652 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.kt @@ -27,7 +27,6 @@ import org.isoron.uhabits.HabitsDatabaseOpener import org.isoron.uhabits.core.DATABASE_FILENAME import org.isoron.uhabits.core.DATABASE_VERSION import org.isoron.uhabits.core.utils.DateFormats.Companion.getBackupDateFormat -import org.isoron.uhabits.core.utils.DateUtils.Companion.getLocalTime import java.io.File import java.io.FileInputStream import java.io.IOException @@ -62,7 +61,7 @@ object DatabaseUtils { @Throws(IOException::class) fun saveDatabaseCopy(context: Context, dir: File): String { val dateFormat: SimpleDateFormat = getBackupDateFormat() - val date = dateFormat.format(getLocalTime()) + val date = dateFormat.format(System.currentTimeMillis()) val filename = "${dir.absolutePath}/Loop Habits Backup $date.db" Log.i("DatabaseUtils", "Writing: $filename") val db = getDatabaseFile(context) @@ -75,7 +74,7 @@ object DatabaseUtils { @Throws(IOException::class) fun saveDatabaseCopy(context: Context, dir: DocumentFile): String { val dateFormat: SimpleDateFormat = getBackupDateFormat() - val date = dateFormat.format(getLocalTime()) + val date = dateFormat.format(System.currentTimeMillis()) val file = dir.createFile("application/octet-stream", "Loop Habits Backup $date.db") ?: throw IOException("Unable to create backup file") Log.i("DatabaseUtils", "Writing: ${file.uri}") diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/DateExtensions.kt b/uhabits-android/src/main/java/org/isoron/uhabits/utils/DateExtensions.kt index 63708608..aa49117d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/DateExtensions.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/DateExtensions.kt @@ -21,12 +21,12 @@ package org.isoron.uhabits.utils import android.content.Context import android.text.format.DateFormat +import org.isoron.platform.time.DayOfWeek +import org.isoron.platform.time.JavaLocalDateFormatter import org.isoron.uhabits.R import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.utils.DateFormats -import org.isoron.uhabits.core.utils.DateUtils import java.text.SimpleDateFormat -import java.util.Calendar import java.util.Date import java.util.Locale import java.util.TimeZone @@ -37,8 +37,9 @@ fun String.toSimpleDataFormat(): SimpleDateFormat { } fun WeekdayList.toFormattedString(context: Context): String { - val shortDayNames = DateUtils.getShortWeekdayNames(Calendar.SATURDAY) - val longDayNames = DateUtils.getLongWeekdayNames(Calendar.SATURDAY) + val formatter = JavaLocalDateFormatter(Locale.getDefault()) + val shortDayNames = formatter.shortWeekdayNames(DayOfWeek.SATURDAY) + val longDayNames = formatter.longWeekdayNames(DayOfWeek.SATURDAY) val buffer = StringBuilder() var count = 0 var first = 0 diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt index 3d39093f..91d349fb 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/utils/ViewExtensions.kt @@ -215,7 +215,7 @@ fun View.drawNotesIndicator( canvas: Canvas, color: Int, size: Float, - notes: String, + notes: String ) { if (notes.isBlank()) return diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt index b4b0ecdc..8a4b8316 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.kt @@ -23,10 +23,10 @@ import android.app.PendingIntent import android.content.Context import android.view.View import org.isoron.platform.gui.toInt +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.ui.views.WidgetTheme -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.widgets.views.CheckmarkWidgetView open class CheckmarkWidget( @@ -41,7 +41,7 @@ open class CheckmarkWidget( override fun getOnClickPendingIntent(context: Context): PendingIntent? { return if (habit.isNumerical) { - pendingIntentFactory.showNumberPicker(habit, DateUtils.getTodayWithOffset()) + pendingIntentFactory.showNumberPicker(habit, getToday()) } else { pendingIntentFactory.toggleCheckmark(habit, null) } @@ -49,7 +49,7 @@ open class CheckmarkWidget( override fun refreshData(widgetView: View) { (widgetView as CheckmarkWidgetView).apply { - val today = DateUtils.getTodayWithOffset() + val today = getToday() setBackgroundAlpha(preferedBackgroundAlpha) activeColor = WidgetTheme().color(habit.color).toInt() name = habit.name diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt index 65658ac9..ed39534d 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.kt @@ -23,6 +23,7 @@ import android.app.PendingIntent import android.content.Context import android.view.View import org.isoron.platform.gui.toInt +import org.isoron.platform.time.DayOfWeek import org.isoron.uhabits.activities.common.views.FrequencyChart import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.ui.views.WidgetTheme @@ -32,7 +33,7 @@ class FrequencyWidget( context: Context, widgetId: Int, private val habit: Habit, - private val firstWeekday: Int, + private val firstWeekday: DayOfWeek, stacked: Boolean = false ) : BaseWidget(context, widgetId, stacked) { override val defaultHeight: Int = 200 diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt index cca380ff..2015ea2c 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.kt @@ -29,7 +29,7 @@ class FrequencyWidgetProvider : BaseWidgetProvider() { context, id, habits[0], - preferences.firstWeekdayInt + preferences.firstWeekday ) } else { StackWidget(context, id, StackWidgetType.FREQUENCY, habits) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt index 435ee6ad..ac998d26 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.kt @@ -24,11 +24,11 @@ import android.content.Context import android.view.View import org.isoron.platform.gui.AndroidDataView import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter import org.isoron.uhabits.core.ui.views.HistoryChart import org.isoron.uhabits.core.ui.views.WidgetTheme -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.widgets.views.GraphWidgetView import java.util.Locale @@ -68,7 +68,7 @@ class HistoryWidget( context, AndroidDataView(context).apply { view = HistoryChart( - today = DateUtils.getTodayWithOffset().toLocalDate(), + today = getToday(), paletteColor = habit.color, theme = WidgetTheme(), dateFormatter = JavaLocalDateFormatter(Locale.getDefault()), diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetService.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetService.kt index 8185b4ad..b18873c3 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetService.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetService.kt @@ -27,13 +27,13 @@ import android.util.Log import android.widget.RemoteViews import android.widget.RemoteViewsService import android.widget.RemoteViewsService.RemoteViewsFactory +import org.isoron.platform.time.getToday import org.isoron.platform.utils.StringUtils.Companion.splitLongs import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitNotFoundException import org.isoron.uhabits.core.preferences.Preferences -import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.PendingIntentFactory import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels @@ -101,7 +101,8 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int val landscapeViews = widget.landscapeRemoteViews val portraitViews = widget.portraitRemoteViews val factory = PendingIntentFactory(context, IntentFactory()) - val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getTodayWithOffset()) + val today = getToday() + val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, today) landscapeViews.setOnClickFillInIntent(R.id.button, intent) portraitViews.setOnClickFillInIntent(R.id.button, intent) val remoteViews = RemoteViews(landscapeViews, portraitViews) @@ -119,7 +120,7 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int context, widgetId, habit, - prefs.firstWeekdayInt, + prefs.firstWeekday, true ) StackWidgetType.SCORE -> ScoreWidget(context, widgetId, habit, true) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt index 02e81d4f..f76a8555 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/StackWidgetType.kt @@ -20,9 +20,9 @@ package org.isoron.uhabits.widgets import android.app.PendingIntent import android.content.Intent +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.R import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.intents.PendingIntentFactory import java.lang.IllegalStateException @@ -100,14 +100,14 @@ enum class StackWidgetType(val value: Int) { widgetType: StackWidgetType, habit: Habit, allHabitsInStackWidget: List, - timestamp: Timestamp + today: LocalDate ): Intent { val containsNumerical = allHabitsInStackWidget.any { it.isNumerical } return when (widgetType) { CHECKMARK -> if (containsNumerical) { - factory.showNumberPickerFillIn(habit, timestamp) + factory.showNumberPickerFillIn(habit, today) } else { - factory.toggleCheckmarkFillIn(habit, timestamp) + factory.toggleCheckmarkFillIn(habit, today) } FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitFillIn(habit) } diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt index 59769cb4..d875156a 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.kt @@ -26,6 +26,7 @@ import android.content.Intent import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand +import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.WidgetPreferences import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.utils.DateUtils @@ -43,7 +44,8 @@ class WidgetUpdater private val commandRunner: CommandRunner, private val taskRunner: TaskRunner, private val widgetPrefs: WidgetPreferences, - private val intentScheduler: IntentScheduler + private val intentScheduler: IntentScheduler, + private val preferences: Preferences ) : CommandRunner.Listener { override fun onCommandFinished(command: Command) { @@ -72,7 +74,7 @@ class WidgetUpdater } fun scheduleStartDayWidgetUpdate() { - val timestamp = DateUtils.getStartOfTomorrowWithOffset() + val timestamp = DateUtils.getStartOfTomorrowWithOffset(preferences.midnightDelayHours, 0) intentScheduler.scheduleWidgetUpdate(timestamp) } diff --git a/uhabits-android/src/test/java/org/isoron/uhabits/BaseAndroidJVMTest.kt b/uhabits-android/src/test/java/org/isoron/uhabits/BaseAndroidJVMTest.kt index aa8c42e6..7a640d45 100644 --- a/uhabits-android/src/test/java/org/isoron/uhabits/BaseAndroidJVMTest.kt +++ b/uhabits-android/src/test/java/org/isoron/uhabits/BaseAndroidJVMTest.kt @@ -18,13 +18,13 @@ */ package org.isoron.uhabits +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.setToday import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.memory.MemoryModelFactory import org.isoron.uhabits.core.tasks.SingleThreadTaskRunner import org.isoron.uhabits.core.test.HabitFixtures -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime -import org.isoron.uhabits.core.utils.DateUtils.Companion.setStartDayOffset import org.junit.After import org.junit.Before import org.junit.Test @@ -42,9 +42,7 @@ open class BaseAndroidJVMTest { @Before open fun setUp() { - val fixedLocalTime = 1422172800000L - setFixedLocalTime(fixedLocalTime) - setStartDayOffset(0, 0) + setToday(LocalDate(2015, 1, 25)) modelFactory = MemoryModelFactory() habitList = spy(modelFactory.buildHabitList()) fixtures = HabitFixtures(modelFactory, habitList) @@ -54,8 +52,6 @@ open class BaseAndroidJVMTest { @After fun tearDown() { - setFixedLocalTime(null) - setStartDayOffset(0, 0) } @Test diff --git a/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.kt b/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.kt index caa6befe..16913052 100644 --- a/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.kt +++ b/uhabits-android/src/test/java/org/isoron/uhabits/receivers/ReminderControllerTest.kt @@ -18,9 +18,9 @@ */ package org.isoron.uhabits.receivers +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.BaseAndroidJVMTest import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.reminders.ReminderScheduler import org.isoron.uhabits.core.ui.NotificationTray @@ -58,8 +58,9 @@ class ReminderControllerTest : BaseAndroidJVMTest() { @Throws(Exception::class) fun testOnShowReminder() { val habit: Habit = mock() - controller.onShowReminder(habit, Timestamp.ZERO.plus(100), 456) - verify(notificationTray).show(habit, Timestamp.ZERO.plus(100), 456) + val date = LocalDate(2015, 1, 25) + controller.onShowReminder(habit, date, 456) + verify(notificationTray).show(habit, date, 456) verify(reminderScheduler).scheduleAll() } diff --git a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt index d30edc3a..049ccd41 100644 --- a/uhabits-core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt +++ b/uhabits-core/src/commonMain/kotlin/org/isoron/platform/time/Dates.kt @@ -19,9 +19,21 @@ package org.isoron.platform.time -import kotlin.math.abs import kotlin.math.ceil +private var currentToday: LocalDate? = null + +fun getToday(): LocalDate = + currentToday ?: error("getToday() called before setToday()") + +fun setToday(date: LocalDate) { + currentToday = date +} + +fun resetToday() { + currentToday = null +} + enum class DayOfWeek(val daysSinceSunday: Int) { SUNDAY(0), MONDAY(1), @@ -32,7 +44,9 @@ enum class DayOfWeek(val daysSinceSunday: Int) { SATURDAY(6) } -data class LocalDate(val daysSince2000: Int) { +data class LocalDate(val daysSince2000: Int) : Comparable { + override fun compareTo(other: LocalDate): Int = + daysSince2000.compareTo(other.daysSince2000) var yearCache = -1 var monthCache = -1 @@ -43,7 +57,9 @@ data class LocalDate(val daysSince2000: Int) { val dayOfWeek: DayOfWeek get() { - return when (daysSince2000 % 7) { + val rem = daysSince2000 % 7 + val mod = if (rem < 0) rem + 7 else rem + return when (mod) { 0 -> DayOfWeek.SATURDAY 1 -> DayOfWeek.SUNDAY 2 -> DayOfWeek.MONDAY @@ -79,6 +95,9 @@ data class LocalDate(val daysSince2000: Int) { else -> 31 } + val yearLength: Int + get() = if (isLeapYear(year)) 366 else 365 + private fun updateYearMonthDayCache() { var currYear = 2000 var currDay = 0 @@ -126,19 +145,57 @@ data class LocalDate(val daysSince2000: Int) { return LocalDate(daysSince2000 - days) } - fun distanceTo(other: LocalDate): Int { - return abs(daysSince2000 - other.daysSince2000) + fun daysUntil(other: LocalDate): Int { + return other.daysSince2000 - this.daysSince2000 + } + + val unixTime: Long + get() = EPOCH_2000_MILLIS + daysSince2000.toLong() * MILLIS_PER_DAY + + fun startOfWeek(firstWeekday: DayOfWeek): LocalDate { + var delta = dayOfWeek.daysSinceSunday - firstWeekday.daysSinceSunday + if (delta < 0) delta += 7 + return minus(delta) + } + + fun startOfMonth(): LocalDate = LocalDate(year, month, 1) + + fun startOfQuarter(): LocalDate { + val quarterMonth = ((month - 1) / 3) * 3 + 1 + return LocalDate(year, quarterMonth, 1) + } + + fun startOfYear(): LocalDate = LocalDate(year, 1, 1) + + fun toCSVString(): String { + val y = year.toString().padStart(4, '0') + val m = month.toString().padStart(2, '0') + val d = day.toString().padStart(2, '0') + return "$y-$m-$d" } override fun toString(): String { return "LocalDate($year-$month-$day)" } + + companion object { + private const val EPOCH_2000_MILLIS = 946684800000L + private const val MILLIS_PER_DAY = 86400000L + + fun fromUnixTime(millis: Long): LocalDate { + val diff = millis - EPOCH_2000_MILLIS + val days = if (diff >= 0) diff / MILLIS_PER_DAY else (diff - MILLIS_PER_DAY + 1) / MILLIS_PER_DAY + return LocalDate(days.toInt()) + } + } } interface LocalDateFormatter { fun shortWeekdayName(weekday: DayOfWeek): String fun shortWeekdayName(date: LocalDate): String fun shortMonthName(date: LocalDate): String + fun longWeekdayName(weekday: DayOfWeek): String + fun longMonthName(date: LocalDate): String } private fun isLeapYear(year: Int): Boolean { @@ -167,3 +224,23 @@ private fun daysSince2000(year: Int, month: Int, day: Int): Int { result += (day - 1) return result } + +enum class TruncateField { + DAY, WEEK_NUMBER, MONTH, QUARTER, YEAR +} + +fun getWeekdaySequence(firstWeekday: DayOfWeek): List { + val allDays = DayOfWeek.entries + return (0 until 7).map { offset -> + allDays[(firstWeekday.daysSinceSunday + offset) % 7] + } +} + +fun countWeekdayOccurrencesInMonth(startOfMonth: LocalDate): Array { + val weekday = (startOfMonth.dayOfWeek.daysSinceSunday + 1) % 7 + val freq = Array(7) { 0 } + for (day in weekday until weekday + startOfMonth.monthLength) { + freq[day % 7] += 1 + } + return freq +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/platform/time/JavaDates.kt b/uhabits-core/src/jvmMain/java/org/isoron/platform/time/JavaDates.kt index 1a5c99bb..b447b9fc 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/platform/time/JavaDates.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/platform/time/JavaDates.kt @@ -47,6 +47,10 @@ fun LocalDate.toGregorianCalendar(): GregorianCalendar { return cal } +fun getFirstWeekdayNumberAccordingToLocale(): Int { + return GregorianCalendar(Locale.getDefault()).firstDayOfWeek +} + class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter { override fun shortMonthName(date: LocalDate): String { val cal = date.toGregorianCalendar() @@ -59,7 +63,7 @@ class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter { override fun shortWeekdayName(weekday: DayOfWeek): String { val cal = GregorianCalendar() - cal.set(DAY_OF_WEEK, weekday.daysSinceSunday - 1) + cal.set(DAY_OF_WEEK, weekday.daysSinceSunday + 1) return shortWeekdayName(LocalDate(cal.get(YEAR), cal.get(MONTH) + 1, cal.get(DAY_OF_MONTH))) } @@ -68,9 +72,28 @@ class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter { return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale) } + override fun longWeekdayName(weekday: DayOfWeek): String { + val cal = GregorianCalendar() + cal.set(DAY_OF_WEEK, weekday.daysSinceSunday + 1) + return cal.getDisplayName(DAY_OF_WEEK, LONG, locale) + } + + override fun longMonthName(date: LocalDate): String { + val cal = date.toGregorianCalendar() + return cal.getDisplayName(MONTH, LONG, locale) + } + fun longFormat(date: LocalDate): String { val df = DateFormat.getDateInstance(DateFormat.LONG, locale) df.timeZone = TimeZone.getTimeZone("UTC") return df.format(date.toGregorianCalendar().time) } + + fun shortWeekdayNames(firstWeekday: DayOfWeek): Array { + return getWeekdaySequence(firstWeekday).map { shortWeekdayName(it) }.toTypedArray() + } + + fun longWeekdayNames(firstWeekday: DayOfWeek): Array { + return getWeekdaySequence(firstWeekday).map { longWeekdayName(it) }.toTypedArray() + } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/platform/time/JvmDates.kt b/uhabits-core/src/jvmMain/java/org/isoron/platform/time/JvmDates.kt new file mode 100644 index 00000000..a8bb2ecc --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/platform/time/JvmDates.kt @@ -0,0 +1,14 @@ +package org.isoron.platform.time + +import java.util.TimeZone + +fun computeToday(hourOffset: Int = 0, minuteOffset: Int = 0): LocalDate { + val nowMillis = System.currentTimeMillis() + val tz = TimeZone.getDefault() + val localMillis = nowMillis + tz.getOffset(nowMillis) + val offsetMillis = hourOffset * 3600000L + minuteOffset * 60000L + val adjustedMillis = localMillis - offsetMillis + val daysSinceEpoch = Math.floorDiv(adjustedMillis, 86400000L) + val daysSince2000 = (daysSinceEpoch - 10957).toInt() + return LocalDate(daysSince2000) +} diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateRepetitionCommand.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateRepetitionCommand.kt index 4ed84db9..52919880 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateRepetitionCommand.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/commands/CreateRepetitionCommand.kt @@ -18,21 +18,21 @@ */ package org.isoron.uhabits.core.commands +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList -import org.isoron.uhabits.core.models.Timestamp data class CreateRepetitionCommand( val habitList: HabitList, val habit: Habit, - val timestamp: Timestamp, + val date: LocalDate, val value: Int, val notes: String ) : Command { override fun run() { val entries = habit.originalEntries - entries.add(Entry(timestamp, value, notes)) + entries.add(Entry(date, value, notes)) habit.recompute() habitList.resort() } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt index da7df49c..8782c221 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt @@ -19,26 +19,16 @@ package org.isoron.uhabits.core.io import com.opencsv.CSVReader +import org.isoron.platform.time.LocalDate 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 import java.io.File import java.io.FileReader -import java.text.DateFormat -import java.text.ParseException -import java.text.SimpleDateFormat -import java.util.Calendar.DAY_OF_MONTH -import java.util.Calendar.MONTH -import java.util.Calendar.YEAR -import java.util.Date -import java.util.GregorianCalendar -import java.util.HashMap -import java.util.Locale import javax.inject.Inject /** @@ -66,7 +56,7 @@ class HabitBullCSVImporter val name = cols[0] if (name == "HabitName") continue val description = cols[1] - val timestamp = parseTimestamp(cols[3]) + val date = parseDate(cols[3]) var h = map[name] if (h == null) { h = modelFactory.buildHabit() @@ -79,14 +69,14 @@ class HabitBullCSVImporter } val notes = cols[5] ?: "" 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)) + 0 -> h.originalEntries.add(Entry(date, Entry.NO, notes)) + 1 -> h.originalEntries.add(Entry(date, 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 * 1000, notes)) + h.originalEntries.add(Entry(date, value * 1000, notes)) } } } @@ -94,30 +84,18 @@ class HabitBullCSVImporter map.forEach { (_, habit) -> habit.recompute() } } - private fun parseTimestamp(rawValue: String): Timestamp { - val formats = listOf( - DateFormat.getDateInstance(DateFormat.SHORT), - SimpleDateFormat("yyyy-MM-dd", Locale.US), - SimpleDateFormat("MM/dd/yyyy", Locale.US) - ) - var parsedDate: Date? = null - for (fmt in formats) { - try { - parsedDate = fmt.parse(rawValue) - } catch (e: ParseException) { - // ignored - } + private fun parseDate(rawValue: String): LocalDate { + if (rawValue.contains("-")) { + // yyyy-MM-dd + val parts = rawValue.split("-") + return LocalDate(parts[0].toInt(), parts[1].toInt(), parts[2].toInt()) } - if (parsedDate == null) { - throw Exception("Unrecognized date format: $rawValue") + if (rawValue.contains("/")) { + // M/d/yyyy + val parts = rawValue.split("/") + return LocalDate(parts[2].toInt(), parts[0].toInt(), parts[1].toInt()) } - val parsedCalendar = GregorianCalendar() - parsedCalendar.time = parsedDate - return Timestamp.from( - parsedCalendar[YEAR], - parsedCalendar[MONTH], - parsedCalendar[DAY_OF_MONTH] - ) + throw Exception("Unrecognized date format: $rawValue") } private fun parseInt(rawValue: String): Int { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt index a9b97373..04fe9ea4 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt @@ -19,14 +19,13 @@ package org.isoron.uhabits.core.io import com.opencsv.CSVWriter +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.Score -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateFormats -import org.isoron.uhabits.core.utils.DateUtils import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -104,17 +103,16 @@ class HabitsCSVExporter( val path = habitDirName + "Scores.csv" val out = FileWriter(exportDirName + path) generatedFilenames.add(path) - val dateFormat = DateFormats.getCSVDateFormat() - val today = DateUtils.getTodayWithOffset() + val today = getToday() var oldest = today val known = habit.computedEntries.getKnown() - if (known.isNotEmpty()) oldest = known[known.size - 1].timestamp + if (known.isNotEmpty()) oldest = known[known.size - 1].date val csv = CSVWriter(out) csv.writeNext(arrayOf("Date", "Score"), false) - for ((timestamp1, value) in habit.scores.getByInterval(oldest, today)) { - val timestamp = dateFormat.format(timestamp1.unixTime) - val score = String.format(Locale.US, "%.4f", value) - csv.writeNext(arrayOf(timestamp, score), false) + for (s in habit.scores.getByInterval(oldest, today)) { + val date = s.date.toCSVString() + val score = String.format(Locale.US, "%.4f", s.value) + csv.writeNext(arrayOf(date, score), false) } csv.close() out.close() @@ -124,11 +122,10 @@ class HabitsCSVExporter( val filename = habitDirName + "Checkmarks.csv" val out = FileWriter(exportDirName + filename) generatedFilenames.add(filename) - val dateFormat = DateFormats.getCSVDateFormat() val csv = CSVWriter(out) csv.writeNext(arrayOf("Date", "Value", "Notes"), false) for (entry in entries.getKnown()) { - val date = dateFormat.format(entry.timestamp.toJavaDate()) + val date = entry.date.toCSVString() csv.writeNext( arrayOf( date, @@ -162,7 +159,7 @@ class HabitsCSVExporter( val timeframe = getTimeframe() val oldest = timeframe[0] - val newest = DateUtils.getTodayWithOffset() + val newest = getToday() val checkmarks: MutableList> = ArrayList() val scores: MutableList> = ArrayList() for (habit in selectedHabits) { @@ -171,10 +168,8 @@ class HabitsCSVExporter( } val days = oldest.daysUntil(newest) - val dateFormat = DateFormats.getCSVDateFormat() for (i in 0..days) { - val day = newest.minus(i).toJavaDate() - val date = dateFormat.format(day) + val date = newest.minus(i).toCSVString() val sb = StringBuilder() sb.append(date).append(delimiter) checksWriter.write(sb.toString()) @@ -218,14 +213,14 @@ class HabitsCSVExporter( * * @return the timeframe containing the oldest timestamp and the newest timestamp */ - private fun getTimeframe(): Array { - var oldest = Timestamp.ZERO.plus(1000000) - var newest = Timestamp.ZERO + private fun getTimeframe(): Array { + var oldest = LocalDate(1000000) + var newest = LocalDate(0) for (habit in selectedHabits) { val entries = habit.originalEntries.getKnown() if (entries.isEmpty()) continue - val currNew = entries[0].timestamp - val currOld = entries[entries.size - 1].timestamp + val currNew = entries[0].date + val currOld = entries[entries.size - 1].date oldest = if (currOld.isOlderThan(oldest)) currOld else oldest newest = if (currNew.isNewerThan(newest)) currNew else newest } @@ -233,8 +228,7 @@ class HabitsCSVExporter( } private fun writeZipFile(): String { - val dateFormat = DateFormats.getCSVDateFormat() - val date = dateFormat.format(DateUtils.getStartOfToday()) + val date = getToday().toCSVString() val zipFilename = String.format("%s/Loop Habits CSV %s.zip", exportDirName, date) val fos = FileOutputStream(zipFilename) val zos = ZipOutputStream(fos) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt index 07a49fe8..f43842f3 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/LoopDBImporter.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.io +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.DATABASE_VERSION import org.isoron.uhabits.core.commands.CommandRunner @@ -29,7 +30,6 @@ 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 import org.isoron.uhabits.core.models.sqlite.records.EntryRecord import org.isoron.uhabits.core.models.sqlite.records.HabitRecord import org.isoron.uhabits.core.utils.isSQLite3File @@ -98,10 +98,10 @@ class LoopDBImporter // Import entries for (r in entryRecords) { - val t = Timestamp(r.timestamp!!) - val (_, value, notes) = entries.get(t) + val date = LocalDate.fromUnixTime(r.timestamp!!) + val (_, value, notes) = entries.get(date) if (value != r.value || notes != r.notes) { - entries.add(Entry(t, r.value!!, r.notes ?: "")) + entries.add(Entry(date, r.value!!, r.notes ?: "")) } } habit.recompute() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/RewireDBImporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/RewireDBImporter.kt index 4e148d84..f0e420bd 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/RewireDBImporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/RewireDBImporter.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.io +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.database.Cursor import org.isoron.uhabits.core.database.Database import org.isoron.uhabits.core.database.DatabaseOpener @@ -27,9 +28,7 @@ import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.Reminder -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.WeekdayList -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.isSQLite3File import java.io.File import javax.inject.Inject @@ -129,13 +128,11 @@ class RewireDBImporter ) if (!c.moveToNext()) return do { - val date = c.getString(0) - val year = date!!.substring(0, 4).toInt() - val month = date.substring(4, 6).toInt() - val day = date.substring(6, 8).toInt() - val cal = DateUtils.getStartOfTodayCalendar() - cal[year, month - 1] = day - habit.originalEntries.add(Entry(Timestamp(cal), Entry.YES_MANUAL)) + val dateStr = c.getString(0) + val year = dateStr!!.substring(0, 4).toInt() + val month = dateStr.substring(4, 6).toInt() + val day = dateStr.substring(6, 8).toInt() + habit.originalEntries.add(Entry(LocalDate(year, month, day), Entry.YES_MANUAL)) } while (c.moveToNext()) } finally { c?.close() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt index cc122c1b..a8fa49b1 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.io +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.database.Cursor import org.isoron.uhabits.core.database.Database import org.isoron.uhabits.core.database.DatabaseOpener @@ -26,8 +27,6 @@ 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.ModelFactory -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.isSQLite3File import java.io.File import javax.inject.Inject @@ -79,9 +78,7 @@ class TickmateDBImporter @Inject constructor( val year = c.getInt(0)!! val month = c.getInt(1)!! val day = c.getInt(2)!! - val cal = DateUtils.getStartOfTodayCalendar() - cal[year, month] = day - habit.originalEntries.add(Entry(Timestamp(cal), Entry.YES_MANUAL)) + habit.originalEntries.add(Entry(LocalDate(year, month + 1, day), Entry.YES_MANUAL)) } while (c.moveToNext()) } finally { c?.close() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Entry.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Entry.kt index 5b3da225..f5c6c541 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Entry.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Entry.kt @@ -18,8 +18,10 @@ */ package org.isoron.uhabits.core.models +import org.isoron.platform.time.LocalDate + data class Entry( - val timestamp: Timestamp, + val date: LocalDate, val value: Int, val notes: String = "" ) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index 53b92541..756766ab 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -19,11 +19,13 @@ package org.isoron.uhabits.core.models +import org.isoron.platform.time.DayOfWeek +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.TruncateField 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 org.isoron.uhabits.core.utils.DateUtils import java.util.ArrayList import java.util.Calendar import javax.annotation.concurrent.ThreadSafe @@ -34,15 +36,15 @@ import kotlin.math.min @ThreadSafe open class EntryList { - private val entriesByTimestamp: HashMap = HashMap() + private val entriesByDate: HashMap = HashMap() /** - * Returns the entry corresponding to the given timestamp. If no entry with such timestamp - * has been previously added, returns Entry(timestamp, UNKNOWN). + * Returns the entry corresponding to the given date. If no entry with such date + * has been previously added, returns Entry(date, UNKNOWN). */ @Synchronized - open fun get(timestamp: Timestamp): Entry { - return entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) + open fun get(date: LocalDate): Entry { + return entriesByDate[date] ?: Entry(date, UNKNOWN) } /** @@ -51,7 +53,7 @@ open class EntryList { * included. */ @Synchronized - open fun getByInterval(from: Timestamp, to: Timestamp): List { + open fun getByInterval(from: LocalDate, to: LocalDate): List { val result = mutableListOf() if (from.isNewerThan(to)) return result var current = to @@ -63,21 +65,21 @@ open class EntryList { } /** - * Adds the given entry to the list. If another entry with the same timestamp already exists, + * Adds the given entry to the list. If another entry with the same date already exists, * replaces it. */ @Synchronized open fun add(entry: Entry) { - entriesByTimestamp[entry.timestamp] = entry + entriesByDate[entry.date] = entry } /** - * Returns all entries whose values are known, sorted by timestamp. The first element + * Returns all entries whose values are known, sorted by date. The first element * corresponds to the newest entry, and the last element corresponds to the oldest. */ @Synchronized open fun getKnown(): List { - return entriesByTimestamp.values.sortedBy { it.timestamp }.reversed() + return entriesByDate.values.sortedBy { it.date }.reversed() } /** @@ -109,7 +111,7 @@ open class EntryList { */ @Synchronized open fun clear() { - entriesByTimestamp.clear() + entriesByDate.clear() } /** @@ -125,33 +127,29 @@ open class EntryList { * @return total number of checkmarks by month versus day of week */ @Synchronized - fun computeWeekdayFrequency(isNumerical: Boolean): HashMap> { + fun computeWeekdayFrequency(isNumerical: Boolean): HashMap> { val entries = getKnown() - val map = hashMapOf>() - for ((originalTimestamp, value) in entries) { - val weekday = originalTimestamp.weekday - val truncatedTimestamp = Timestamp( - originalTimestamp.toCalendar().apply { - set(Calendar.DAY_OF_MONTH, 1) - }.timeInMillis - ) + val map = hashMapOf>() + for (entry in entries) { + val weekday = (entry.date.dayOfWeek.daysSinceSunday + 1) % 7 + val monthStart = entry.date.startOfMonth() - var list = map[truncatedTimestamp] + var list = map[monthStart] if (list == null) { list = arrayOf(0, 0, 0, 0, 0, 0, 0) - map[truncatedTimestamp] = list + map[monthStart] = list } if (isNumerical) { - list[weekday] += value - } else if (value == YES_MANUAL) { + list[weekday] += entry.value + } else if (entry.value == YES_MANUAL) { list[weekday] += 1 } } return map } - data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) { + data class Interval(val begin: LocalDate, val center: LocalDate, val end: LocalDate) { val length: Int get() = begin.daysUntil(end) + 1 } @@ -162,7 +160,7 @@ open class EntryList { * interval receive value UNKNOWN. Entries that fall within an interval but do not appear * in [original] receive value YES_AUTO. Entries provided in [original] are copied over. * - * The intervals should be sorted by timestamp. The first element in the list should + * The intervals should be sorted by date. The first element in the list should * correspond to the newest interval. */ fun buildEntriesFromInterval( @@ -172,12 +170,12 @@ open class EntryList { val result = arrayListOf() if (original.isEmpty()) return result - var from = original[0].timestamp - var to = original[0].timestamp + var from = original[0].date + var to = original[0].date for (e in original) { - if (e.timestamp < from) from = e.timestamp - if (e.timestamp > to) to = e.timestamp + if (e.date < from) from = e.date + if (e.date > to) to = e.date } for (interval in intervals) { if (interval.begin < from) from = interval.begin @@ -203,7 +201,7 @@ open class EntryList { // Copy original entries original.forEach { entry -> - val offset = entry.timestamp.daysUntil(to) + val offset = entry.date.daysUntil(to) val value = if ( result[offset].value == UNKNOWN || entry.value == SKIP || @@ -213,7 +211,7 @@ open class EntryList { } else { YES_AUTO } - result[offset] = Entry(entry.timestamp, value, entry.notes) + result[offset] = Entry(entry.date, value, entry.notes) } return result @@ -224,7 +222,7 @@ open class EntryList { * intervals backwards into the past, so that gaps are eliminated and * streaks are maximized. * - * The intervals should be sorted by timestamp. The first element in the list should + * The intervals should be sorted by date. The first element in the list should * correspond to the newest interval. */ fun snapIntervalsTogether(intervals: ArrayList) { @@ -253,15 +251,14 @@ open class EntryList { val den = freq.denominator val intervals = arrayListOf() for (i in num - 1 until filtered.size) { - val (begin, _) = filtered[i] - val (center, _) = filtered[i - num + 1] + val begin = filtered[i].date + val center = filtered[i - num + 1].date var size = den if (den == 30 || den == 31) { - val beginDate = begin.toLocalDate() - size = if (beginDate.day == beginDate.monthLength) { - beginDate.plus(1).monthLength + size = if (begin.day == begin.monthLength) { + begin.plus(1).monthLength } else { - beginDate.monthLength + begin.monthLength } } if (begin.daysUntil(center) < size) { @@ -274,10 +271,24 @@ open class EntryList { } } +private fun truncateDate( + date: LocalDate, + field: TruncateField, + firstWeekday: DayOfWeek +): LocalDate { + return when (field) { + TruncateField.DAY -> date + TruncateField.WEEK_NUMBER -> date.startOfWeek(firstWeekday) + TruncateField.MONTH -> date.startOfMonth() + TruncateField.QUARTER -> date.startOfQuarter() + TruncateField.YEAR -> date.startOfYear() + } +} + /** - * Given a list of entries, truncates the timestamp of each entry (according to the field given), - * groups the entries according to this truncated timestamp, then creates a new entry (t,v) for - * each group, where t is the truncated timestamp and v is the sum of the values of all entries in + * Given a list of entries, truncates the date of each entry (according to the field given), + * groups the entries according to this truncated date, then creates a new entry (d,v) for + * each group, where d is the truncated date and v is the sum of the values of all entries in * the group. * * For numerical habits, non-positive entry values are converted to zero. For boolean habits, each @@ -285,60 +296,56 @@ open class EntryList { * * 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 + * The returned list is sorted by date, 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. * * The argument [firstWeekday] is only relevant when truncating by week. */ fun List.groupedSum( - truncateField: DateUtils.TruncateField, + truncateField: TruncateField, firstWeekday: Int = Calendar.SATURDAY, isNumerical: Boolean ): List { - return this.map { (timestamp, value) -> + val firstWeekdayEnum = DayOfWeek.values()[firstWeekday - 1] + return this.map { (date, value) -> if (isNumerical) { if (value == SKIP) { - Entry(timestamp, 0) + Entry(date, 0) } else { - Entry(timestamp, max(0, value)) + Entry(date, max(0, value)) } } else { - Entry(timestamp, if (value == YES_MANUAL) 1000 else 0) + Entry(date, if (value == YES_MANUAL) 1000 else 0) } }.groupBy { entry -> - entry.timestamp.truncate( - truncateField, - firstWeekday - ) - }.entries.map { (timestamp, entries) -> - Entry(timestamp, entries.sumOf { it.value }) - }.sortedBy { (timestamp, _) -> - -timestamp.unixTime + truncateDate(entry.date, truncateField, firstWeekdayEnum) + }.entries.map { (date, entries) -> + Entry(date, entries.sumOf { it.value }) + }.sortedBy { (date, _) -> + -date.daysSince2000 } } /** - * Counts the number of days with vaLue SKIP in the given period. + * Counts the number of days with value SKIP in the given period. */ fun List.countSkippedDays( - truncateField: DateUtils.TruncateField, + truncateField: TruncateField, firstWeekday: Int = Calendar.SATURDAY ): List { - return this.map { (timestamp, value) -> + val firstWeekdayEnum = DayOfWeek.values()[firstWeekday - 1] + return this.map { (date, value) -> if (value == SKIP) { - Entry(timestamp, 1) + Entry(date, 1) } else { - Entry(timestamp, 0) + Entry(date, 0) } }.groupBy { entry -> - entry.timestamp.truncate( - truncateField, - firstWeekday - ) - }.entries.map { (timestamp, entries) -> - Entry(timestamp, entries.sumOf { it.value }) - }.sortedBy { (timestamp, _) -> - -timestamp.unixTime + truncateDate(entry.date, truncateField, firstWeekdayEnum) + }.entries.map { (date, entries) -> + Entry(date, entries.sumOf { it.value }) + }.sortedBy { (date, _) -> + -date.daysSince2000 } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index 0be2d012..8dac38e6 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -18,7 +18,7 @@ */ package org.isoron.uhabits.core.models -import org.isoron.uhabits.core.utils.DateUtils +import org.isoron.platform.time.getToday import java.util.UUID data class Habit( @@ -56,7 +56,7 @@ data class Habit( fun hasReminder(): Boolean = reminder != null fun isCompletedToday(): Boolean { - val today = DateUtils.getTodayWithOffset() + val today = getToday() val value = computedEntries.get(today).value return if (isNumerical) { when (targetType) { @@ -69,7 +69,7 @@ data class Habit( } fun isEnteredToday(): Boolean { - val today = DateUtils.getTodayWithOffset() + val today = getToday() val value = computedEntries.get(today).value return value != Entry.UNKNOWN } @@ -81,10 +81,10 @@ data class Habit( isNumerical = isNumerical ) - val today = DateUtils.getTodayWithOffset() + val today = getToday() val to = today.plus(30) val entries = computedEntries.getKnown() - var from = entries.lastOrNull()?.timestamp ?: today + var from = entries.lastOrNull()?.date ?: today if (from.isNewerThan(to)) from = to scores.recompute( diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Score.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Score.kt index 0375f244..c8053d46 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Score.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Score.kt @@ -18,14 +18,14 @@ */ package org.isoron.uhabits.core.models +import org.isoron.platform.time.LocalDate import kotlin.math.pow import kotlin.math.sqrt data class Score( - val timestamp: Timestamp, + val date: LocalDate, val value: Double ) { - companion object { /** * Given the frequency of the habit, the previous score, and the value of diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt index 824d8a4a..35ca3150 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt @@ -18,6 +18,7 @@ */ 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 @@ -28,33 +29,33 @@ import kotlin.math.min @ThreadSafe class ScoreList { - private val map = HashMap() + private val map = HashMap() /** - * Returns the score for a given day. If the timestamp given happens before the first + * Returns the score for a given day. If the date given happens before the first * repetition of the habit or after the last computed score, returns a score with value zero. */ @Synchronized - operator fun get(timestamp: Timestamp): Score { - return map[timestamp] ?: Score(timestamp, 0.0) + operator fun get(date: LocalDate): Score { + return map[date] ?: Score(date, 0.0) } /** * Returns the list of scores that fall within the given interval. * * There is exactly one score per day in the interval. The endpoints of the interval are - * included. The list is ordered by timestamp (decreasing). That is, the first score - * corresponds to the newest timestamp, and the last score corresponds to the oldest timestamp. + * included. The list is ordered by date (decreasing). That is, the first score + * corresponds to the newest date, and the last score corresponds to the oldest date. */ @Synchronized fun getByInterval( - fromTimestamp: Timestamp, - toTimestamp: Timestamp + from: LocalDate, + to: LocalDate ): List { val result: MutableList = ArrayList() - if (fromTimestamp.isNewerThan(toTimestamp)) return result - var current = toTimestamp - while (!current.isOlderThan(fromTimestamp)) { + if (from.isNewerThan(to)) return result + var current = to + while (!current.isOlderThan(from)) { result.add(get(current)) current = current.minus(1) } @@ -62,7 +63,7 @@ class ScoreList { } /** - * Recomputes all scores between the provided [from] and [to] timestamps. + * Recomputes all scores between the provided [from] and [to] dates. */ @Synchronized fun recompute( @@ -71,8 +72,8 @@ class ScoreList { numericalHabitType: NumericalHabitType, targetValue: Double, computedEntries: EntryList, - from: Timestamp, - to: Timestamp + from: LocalDate, + to: LocalDate ) { map.clear() var rollingSum = 0.0 @@ -134,8 +135,8 @@ class ScoreList { previousValue = compute(freq, previousValue, percentageCompleted) } } - val timestamp = from.plus(i) - map[timestamp] = Score(timestamp, previousValue) + val date = from.plus(i) + map[date] = Score(date, previousValue) } } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt index b033642e..72e1c4ea 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Streak.kt @@ -18,22 +18,22 @@ */ package org.isoron.uhabits.core.models -import java.lang.Long.signum +import org.isoron.platform.time.LocalDate data class Streak( - val start: Timestamp, - val end: Timestamp + val start: LocalDate, + val end: LocalDate ) { fun compareLonger(other: Streak): Int { return if (length != other.length) { - signum(length - other.length.toLong()) + length - other.length } else { compareNewer(other) } } fun compareNewer(other: Streak): Int { - return end.compareTo(other.end) + return end.daysSince2000 - other.end.daysSince2000 } val length: Int diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt index d0a82ae7..dc83f79e 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/StreakList.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.models +import org.isoron.platform.time.LocalDate import javax.annotation.concurrent.ThreadSafe import kotlin.math.min @@ -36,14 +37,14 @@ class StreakList { @Synchronized fun recompute( computedEntries: EntryList, - from: Timestamp, - to: Timestamp, + from: LocalDate, + to: LocalDate, isNumerical: Boolean, targetValue: Double, targetType: NumericalHabitType ) { list.clear() - val timestamps = computedEntries + val dates = computedEntries .getByInterval(from, to) .filter { val value = it.value @@ -56,15 +57,15 @@ class StreakList { value > 0 } } - .map { it.timestamp } + .map { it.date } .toTypedArray() - if (timestamps.isEmpty()) return + if (dates.isEmpty()) return - var begin = timestamps[0] - var end = timestamps[0] - for (i in 1 until timestamps.size) { - val current = timestamps[i] + var begin = dates[0] + var end = dates[0] + for (i in 1 until dates.size) { + val current = dates[i] if (current == begin.minus(1)) { begin = current } else { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt deleted file mode 100644 index dc9c4aea..00000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Timestamp.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2016-2025 Á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 - -import org.isoron.platform.time.LocalDate -import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat -import org.isoron.uhabits.core.utils.DateFormats.Companion.getDialogDateFormat -import org.isoron.uhabits.core.utils.DateUtils -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar -import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate -import java.util.Calendar -import java.util.Date -import java.util.GregorianCalendar -import java.util.TimeZone - -data class Timestamp(var unixTime: Long) : Comparable { - - constructor(cal: GregorianCalendar) : this(cal.timeInMillis) - - fun toLocalDate(): LocalDate { - val millisSince2000 = unixTime - 946684800000L - val daysSince2000 = (millisSince2000 / 86400000).toInt() - return LocalDate(daysSince2000) - } - - /** - * Returns -1 if this timestamp is older than the given timestamp, 1 if this - * timestamp is newer, or zero if they are equal. - */ - override fun compareTo(other: Timestamp): Int { - return java.lang.Long.signum(unixTime - other.unixTime) - } - - operator fun minus(days: Int): Timestamp { - return plus(-days) - } - - operator fun plus(days: Int): Timestamp { - return Timestamp(unixTime + DAY_LENGTH * days) - } - - /** - * Returns the number of days between this timestamp and the given one. If - * the other timestamp equals this one, returns zero. If the other timestamp - * is older than this one, returns a negative number. - */ - fun daysUntil(other: Timestamp): Int { - return ((other.unixTime - unixTime) / DAY_LENGTH).toInt() - } - - fun isNewerThan(other: Timestamp): Boolean { - return compareTo(other) > 0 - } - - fun isOlderThan(other: Timestamp): Boolean { - return compareTo(other) < 0 - } - - fun toJavaDate(): Date { - return Date(unixTime) - } - - fun toCalendar(): GregorianCalendar { - val day = GregorianCalendar(TimeZone.getTimeZone("GMT")) - day.timeInMillis = unixTime - return day - } - - fun toDialogDateString(): String { - return getDialogDateFormat().format(Date(unixTime)) - } - - override fun toString(): String { - return getCSVDateFormat().format(Date(unixTime)) - } - - /** - * Returns an integer corresponding to the day of the week. Saturday maps - * to 0, Sunday maps to 1, and so on. - */ - val weekday: Int - get() = toCalendar()[Calendar.DAY_OF_WEEK] % 7 - - fun truncate(field: DateUtils.TruncateField?, firstWeekday: Int): Timestamp { - return Timestamp( - truncate( - field!!, - unixTime, - firstWeekday - ) - ) - } - - companion object { - const val DAY_LENGTH: Long = 86400000 - val ZERO = Timestamp(0) - fun fromLocalDate(date: LocalDate): Timestamp { - return Timestamp(946684800000L + date.daysSince2000 * 86400000L) - } - - fun from(year: Int, javaMonth: Int, day: Int): Timestamp { - val cal = getStartOfTodayCalendar() - cal[year, javaMonth, day, 0, 0] = 0 - return Timestamp(cal.timeInMillis) - } - - /** - * Given two timestamps, returns whichever timestamp is the oldest one. - */ - fun oldest(first: Timestamp, second: Timestamp): Timestamp { - return if (first.unixTime < second.unixTime) first else second - } - } - - init { - require(unixTime >= 0) { "Invalid unix time: $unixTime" } - if (unixTime % DAY_LENGTH != 0L) unixTime = unixTime / DAY_LENGTH * DAY_LENGTH - } -} 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 index c1812858..f077db14 100644 --- 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 @@ -21,7 +21,6 @@ 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 @@ -130,7 +129,7 @@ class MemoryHabitList : HabitList { Comparator { h1: Habit, h2: Habit -> colorComparatorAsc.compare(h2, h1) } val scoreComparatorDesc = Comparator { habit1, habit2 -> - val today = getTodayWithOffset() + val today = org.isoron.platform.time.getToday() habit1.scores[today].value.compareTo(habit2.scores[today].value) } val scoreComparatorAsc = @@ -144,7 +143,7 @@ class MemoryHabitList : HabitList { if (h1.isNumerical != h2.isNumerical) { return@Comparator if (h1.isNumerical) -1 else 1 } - val today = getTodayWithOffset() + val today = org.isoron.platform.time.getToday() val v1 = h1.computedEntries.get(today).value val v2 = h2.computedEntries.get(today).value v2.compareTo(v1) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt index 911f93f5..f4f9556e 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt @@ -19,12 +19,12 @@ package org.isoron.uhabits.core.models.sqlite +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.database.Database import org.isoron.uhabits.core.database.Repository import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.Frequency -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.sqlite.records.EntryRecord class SQLiteEntryList(database: Database) : EntryList() { @@ -43,12 +43,12 @@ class SQLiteEntryList(database: Database) : EntryList() { isLoaded = true } - override fun get(timestamp: Timestamp): Entry { + override fun get(date: LocalDate): Entry { loadRecords() - return super.get(timestamp) + return super.get(date) } - override fun getByInterval(from: Timestamp, to: Timestamp): List { + override fun getByInterval(from: LocalDate, to: LocalDate): List { loadRecords() return super.getByInterval(from, to) } @@ -61,7 +61,7 @@ class SQLiteEntryList(database: Database) : EntryList() { repository.execSQL( "delete from repetitions where habit = ? and timestamp = ?", habitId.toString(), - entry.timestamp.unixTime.toString() + entry.date.unixTime.toString() ) // Add new row diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt index f30318e6..2d5c7917 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecord.kt @@ -18,10 +18,10 @@ */ package org.isoron.uhabits.core.models.sqlite.records +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.database.Column import org.isoron.uhabits.core.database.Table import org.isoron.uhabits.core.models.Entry -import org.isoron.uhabits.core.models.Timestamp /** * The SQLite database record corresponding to a [Entry]. @@ -44,14 +44,14 @@ class EntryRecord { @field:Column var notes: String? = null + fun copyFrom(entry: Entry) { - timestamp = entry.timestamp.unixTime + timestamp = entry.date.unixTime value = entry.value notes = entry.notes } fun toEntry(): Entry { - val notes = notes ?: "" - return Entry(Timestamp(timestamp!!), value!!, notes) + return Entry(LocalDate.fromUnixTime(timestamp!!), value!!, notes ?: "") } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt index 491790c8..eb1fc11a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/preferences/Preferences.kt @@ -19,12 +19,12 @@ package org.isoron.uhabits.core.preferences import org.isoron.platform.time.DayOfWeek +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getFirstWeekdayNumberAccordingToLocale 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.models.Timestamp import org.isoron.uhabits.core.ui.ThemeSwitcher -import org.isoron.uhabits.core.utils.DateUtils.Companion.getFirstWeekdayNumberAccordingToLocale import java.util.LinkedList import kotlin.math.max import kotlin.math.min @@ -86,11 +86,12 @@ open class Preferences(private val storage: Storage) { } val lastHintNumber: Int get() = storage.getInt("last_hint_number", -1) - open val lastHintTimestamp: Timestamp? + open val lastHintDate: LocalDate? get() { val unixTime = storage.getLong("last_hint_timestamp", -1) - return if (unixTime < 0) null else Timestamp(unixTime) + return if (unixTime < 0) null else LocalDate.fromUnixTime(unixTime) } + var showArchived: Boolean get() = storage.getBoolean("pref_show_archived", false) set(showArchived) { @@ -183,9 +184,16 @@ open class Preferences(private val storage: Storage) { for (l in listeners) l.onCheckmarkSequenceChanged() } - fun updateLastHint(number: Int, timestamp: Timestamp) { + val midnightDelayHours: Int + get() = if (isMidnightDelayEnabled) MIDNIGHT_DELAY_HOURS else 0 + + companion object { + const val MIDNIGHT_DELAY_HOURS = 3 + } + + fun updateLastHint(number: Int, date: LocalDate) { storage.putInt("last_hint_number", number) - storage.putLong("last_hint_timestamp", timestamp.unixTime) + storage.putLong("last_hint_timestamp", date.unixTime) } var lastAppVersion: Int diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt index 7dfa55b9..e60d60ea 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/reminders/ReminderScheduler.kt @@ -27,10 +27,7 @@ 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.preferences.WidgetPreferences -import org.isoron.uhabits.core.utils.DateUtils.Companion.applyTimezone -import org.isoron.uhabits.core.utils.DateUtils.Companion.getLocalTime -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfDayWithOffset -import org.isoron.uhabits.core.utils.DateUtils.Companion.removeTimezone +import org.isoron.uhabits.core.utils.DateUtils import java.util.Locale import java.util.Objects import javax.inject.Inject @@ -62,7 +59,7 @@ class ReminderScheduler @Inject constructor( var reminderTime = Objects.requireNonNull(habit.reminder)!!.timeInMillis val snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.id!!) if (snoozeReminderTime != 0L) { - val now = applyTimezone(getLocalTime()) + val now = DateUtils.applyTimezone(DateUtils.getLocalTime()) sys.log( "ReminderScheduler", String.format( @@ -94,14 +91,14 @@ class ReminderScheduler @Inject constructor( sys.log("ReminderScheduler", "habit=" + habit.id + " is archived. Skipping.") return } - val timestamp = getStartOfDayWithOffset(removeTimezone(reminderTime)) + val timestamp = DateUtils.getStartOfDayWithOffset(DateUtils.removeTimezone(reminderTime), 0, 0) sys.log( "ReminderScheduler", String.format( Locale.US, "reminderTime=%d removeTimezone=%d timestamp=%d", reminderTime, - removeTimezone(reminderTime), + DateUtils.removeTimezone(reminderTime), timestamp ) ) @@ -132,7 +129,7 @@ class ReminderScheduler @Inject constructor( @Synchronized fun snoozeReminder(habit: Habit, minutes: Long) { - val now = applyTimezone(getLocalTime()) + val now = DateUtils.applyTimezone(DateUtils.getLocalTime()) val snoozedUntil = now + minutes * 60 * 1000 widgetPreferences.setSnoozeTime(habit.id!!, snoozedUntil) schedule(habit) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt index 25543a34..505b4d23 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/test/HabitFixtures.kt @@ -18,6 +18,8 @@ */ package org.isoron.uhabits.core.test +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Habit @@ -26,11 +28,12 @@ import org.isoron.uhabits.core.models.HabitType import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.NumericalHabitType import org.isoron.uhabits.core.models.PaletteColor -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.sqlite.SQLiteEntryList -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday -class HabitFixtures(private val modelFactory: ModelFactory, private val habitList: HabitList) { +class HabitFixtures( + private val modelFactory: ModelFactory, + private val habitList: HabitList +) { private var NON_DAILY_HABIT_CHECKS = booleanArrayOf( true, false, false, true, true, true, false, false, true, true ) @@ -103,7 +106,7 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis return habit } - fun createLongNumericalHabit(reference: Timestamp): Habit { + fun createLongNumericalHabit(reference: LocalDate): Habit { val habit = modelFactory.buildHabit() habit.type = HabitType.NUMERICAL habit.name = "Walk" diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt index 84522358..462b3492 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/NotificationTray.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.ui +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.commands.Command import org.isoron.uhabits.core.commands.CommandRunner @@ -25,7 +26,6 @@ import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.NumericalHabitType -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.tasks.Task import org.isoron.uhabits.core.tasks.TaskRunner @@ -63,8 +63,8 @@ class NotificationTray @Inject constructor( reshowAll() } - fun show(habit: Habit, timestamp: Timestamp, reminderTime: Long) { - val data = NotificationData(timestamp, reminderTime) + fun show(habit: Habit, date: LocalDate, reminderTime: Long) { + val data = NotificationData(date, reminderTime) active[habit] = data taskRunner.execute(ShowNotificationTask(habit, data)) } @@ -101,18 +101,18 @@ class NotificationTray @Inject constructor( fun showNotification( habit: Habit, notificationId: Int, - timestamp: Timestamp, + date: LocalDate, reminderTime: Long ) fun log(msg: String) } - internal class NotificationData(val timestamp: Timestamp, val reminderTime: Long) + internal class NotificationData(val date: LocalDate, val reminderTime: Long) private inner class ShowNotificationTask(private val habit: Habit, data: NotificationData) : Task { var isCompleted = false - private val timestamp: Timestamp = data.timestamp + private val date: LocalDate = data.date private val reminderTime: Long = data.reminderTime override fun doInBackground() { @@ -164,7 +164,7 @@ class NotificationTray @Inject constructor( systemTray.showNotification( habit, getNotificationId(habit), - timestamp, + date, reminderTime ) } @@ -173,7 +173,7 @@ class NotificationTray @Inject constructor( if (!habit.hasReminder()) return false val reminder = habit.reminder val reminderDays = Objects.requireNonNull(reminder)!!.days.toArray() - val weekday = timestamp.weekday + val weekday = (date.dayOfWeek.daysSinceSunday + 1) % 7 return reminderDays[weekday] } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnToggleCheckmarkListener.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnToggleCheckmarkListener.kt index 6c280ec3..9ceabb80 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnToggleCheckmarkListener.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/callbacks/OnToggleCheckmarkListener.kt @@ -19,8 +19,8 @@ package org.isoron.uhabits.core.ui.callbacks -import org.isoron.uhabits.core.models.Timestamp +import org.isoron.platform.time.LocalDate interface OnToggleCheckmarkListener { - fun onToggleEntry(timestamp: Timestamp, value: Int) {} + fun onToggleEntry(date: LocalDate, value: Int) {} } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index b2400890..ca72994d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -19,6 +19,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list 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 import org.isoron.uhabits.core.commands.CommandRunner @@ -30,7 +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 org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import java.util.ArrayList import java.util.Arrays import java.util.HashMap @@ -301,7 +301,7 @@ class HabitCardListCache @Inject constructor( newData.copyScoresFrom(data) newData.copyCheckmarksFrom(data) newData.copyNoteIndicatorsFrom(data) - val today = getTodayWithOffset() + val today = getToday() val dateFrom = today.minus(checkmarkCount - 1) if (runner != null) runner!!.publishProgress(this, -1) for (position in newData.habits.indices) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt index df783890..96cf9f73 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HintList.kt @@ -18,14 +18,17 @@ */ package org.isoron.uhabits.core.ui.screens.habits.list +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.preferences.Preferences -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday /** * Provides a list of hints to be shown at the application startup, and takes * care of deciding when a new hint should be shown. */ -open class HintList(private val prefs: Preferences, private val hints: Array) { +open class HintList( + private val prefs: Preferences, + private val hints: Array +) { /** * Returns a new hint to be shown to the user. * @@ -50,7 +53,7 @@ open class HintList(private val prefs: Preferences, private val hints: Array @@ -65,7 +65,7 @@ open class ListHabitsBehavior @Inject constructor( screen.showConfetti(habit.color, x, y) } } - commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, value, newNotes)) + commandRunner.run(CreateRepetitionCommand(habitList, habit, date, value, newNotes)) } } else { screen.showCheckmarkPopup( @@ -74,7 +74,7 @@ open class ListHabitsBehavior @Inject constructor( habit.color ) { newValue: Int, newNotes: String -> if (newValue != entry.value && newValue == YES_MANUAL) screen.showConfetti(habit.color, x, y) - commandRunner.run(CreateRepetitionCommand(habitList, habit, timestamp, newValue, newNotes)) + commandRunner.run(CreateRepetitionCommand(habitList, habit, date, newValue, newNotes)) } } } @@ -129,9 +129,9 @@ open class ListHabitsBehavior @Inject constructor( if (prefs.isFirstRun) onFirstRun() } - fun onToggle(habit: Habit, timestamp: Timestamp, value: Int, notes: String, x: Float, y: Float) { + fun onToggle(habit: Habit, date: LocalDate, value: Int, notes: String, x: Float, y: Float) { commandRunner.run( - CreateRepetitionCommand(habitList, habit, timestamp, value, notes) + CreateRepetitionCommand(habitList, habit, date, value, notes) ) if (value == YES_MANUAL) screen.showConfetti(habit.color, x, y) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt index 95c0dd02..2b662c83 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabit.kt @@ -124,7 +124,7 @@ class ShowHabitPresenter( ), frequency = FrequencyCardPresenter.buildState( habit = habit, - firstWeekday = preferences.firstWeekdayInt, + firstWeekday = preferences.firstWeekday, theme = theme ), history = HistoryCardPresenter.buildState( diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenter.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenter.kt index 34b9e22e..feb3e981 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenter.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/ShowHabitMenuPresenter.kt @@ -18,6 +18,7 @@ */ package org.isoron.uhabits.core.ui.screens.habits.show +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.commands.ArchiveHabitsCommand import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.DeleteHabitsCommand @@ -28,7 +29,6 @@ import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.tasks.ExportCSVTask import org.isoron.uhabits.core.tasks.TaskRunner import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback -import org.isoron.uhabits.core.utils.DateUtils import java.io.File import java.util.Random import kotlin.math.max @@ -97,7 +97,7 @@ class ShowHabitMenuPresenter( value = (1000 + 250 * random.nextGaussian() * strength / 100).toInt() * 1000 } - habit.originalEntries.add(Entry(DateUtils.getToday().minus(i), value)) + habit.originalEntries.add(Entry(getToday().minus(i), value)) } habit.recompute() screen.refresh() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt index 54ec9349..c587ff29 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt @@ -19,13 +19,13 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.views.Theme -import org.isoron.uhabits.core.utils.DateUtils data class BarCardState( val theme: Theme, @@ -57,8 +57,8 @@ class BarCardPresenter( } else { boolBucketSizes[boolSpinnerPosition] } - val today = DateUtils.getTodayWithOffset() - val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today + val today = getToday() + val oldest = habit.computedEntries.getKnown().lastOrNull()?.date ?: today val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum( truncateField = ScoreCardPresenter.getTruncateField(bucketSize), firstWeekday = firstWeekday, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt index abde130f..354c4536 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/FrequencyCard.kt @@ -19,16 +19,17 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views +import org.isoron.platform.time.DayOfWeek +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.models.Timestamp import org.isoron.uhabits.core.ui.views.Theme import java.util.HashMap data class FrequencyCardState( val color: PaletteColor, - val firstWeekday: Int, - val frequency: HashMap>, + val firstWeekday: DayOfWeek, + val frequency: HashMap>, val theme: Theme, val isNumerical: Boolean ) @@ -37,7 +38,7 @@ class FrequencyCardPresenter { companion object { fun buildState( habit: Habit, - firstWeekday: Int, + firstWeekday: DayOfWeek, theme: Theme ) = FrequencyCardState( color = habit.color, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt index efee048d..a3099f0a 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/HistoryCard.kt @@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.platform.time.DayOfWeek import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.models.Entry @@ -32,7 +33,6 @@ import org.isoron.uhabits.core.models.HabitList 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 @@ -43,7 +43,6 @@ 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.roundToInt data class HistoryCardState( @@ -65,35 +64,33 @@ class HistoryCardPresenter( ) : OnDateClickedListener { override fun onDateLongPress(date: LocalDate) { - val timestamp = Timestamp.fromLocalDate(date) screen.showFeedback() if (habit.isNumerical) { - showNumberPopup(timestamp) + showNumberPopup(date) } else { if (preferences.isShortToggleEnabled) { - showCheckmarkPopup(timestamp) + showCheckmarkPopup(date) } else { - toggle(timestamp) + toggle(date) } } } override fun onDateShortPress(date: LocalDate) { - val timestamp = Timestamp.fromLocalDate(date) screen.showFeedback() if (habit.isNumerical) { - showNumberPopup(timestamp) + showNumberPopup(date) } else { if (preferences.isShortToggleEnabled) { - toggle(timestamp) + toggle(date) } else { - showCheckmarkPopup(timestamp) + showCheckmarkPopup(date) } } } - private fun showCheckmarkPopup(timestamp: Timestamp) { - val entry = habit.computedEntries.get(timestamp) + private fun showCheckmarkPopup(date: LocalDate) { + val entry = habit.computedEntries.get(date) screen.showCheckmarkPopup( entry.value, entry.notes, @@ -103,7 +100,7 @@ class HistoryCardPresenter( CreateRepetitionCommand( habitList, habit, - timestamp, + date, newValue, newNotes ) @@ -111,8 +108,8 @@ class HistoryCardPresenter( } } - private fun toggle(timestamp: Timestamp) { - val entry = habit.computedEntries.get(timestamp) + private fun toggle(date: LocalDate) { + val entry = habit.computedEntries.get(date) val nextValue = Entry.nextToggleValue( value = entry.value, isSkipEnabled = preferences.isSkipEnabled, @@ -122,15 +119,15 @@ class HistoryCardPresenter( CreateRepetitionCommand( habitList, habit, - timestamp, + date, nextValue, entry.notes ) ) } - private fun showNumberPopup(timestamp: Timestamp) { - val entry = habit.computedEntries.get(timestamp) + private fun showNumberPopup(date: LocalDate) { + val entry = habit.computedEntries.get(date) val oldValue = entry.value screen.showNumberPopup( value = oldValue / 1000.0, @@ -141,7 +138,7 @@ class HistoryCardPresenter( CreateRepetitionCommand( habitList, habit, - timestamp, + date, thousands, newNotes ) @@ -159,8 +156,8 @@ class HistoryCardPresenter( firstWeekday: DayOfWeek, theme: Theme ): HistoryCardState { - val today = DateUtils.getTodayWithOffset() - val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today + val today = getToday() + val oldest = habit.computedEntries.getKnown().lastOrNull()?.date ?: today val entries = habit.computedEntries.getByInterval(oldest, today) val series = if (habit.isNumerical) { entries.map { @@ -192,7 +189,7 @@ class HistoryCardPresenter( return HistoryCardState( color = habit.color, firstWeekday = firstWeekday, - today = today.toLocalDate(), + today = today, theme = theme, series = series, defaultSquare = OFF, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt index c5f749a2..cfc80e13 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/OverviewCard.kt @@ -19,11 +19,11 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.ui.views.Theme -import org.isoron.uhabits.core.utils.DateUtils data class OverviewCardState( val color: PaletteColor, @@ -37,7 +37,7 @@ data class OverviewCardState( class OverviewCardPresenter { companion object { fun buildState(habit: Habit, theme: Theme): OverviewCardState { - val today = DateUtils.getTodayWithOffset() + val today = getToday() val lastMonth = today.minus(30) val lastYear = today.minus(365) val scores = habit.scores diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt index 8cfeae89..f0df01a9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/ScoreCard.kt @@ -19,12 +19,14 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.TruncateField +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.Score import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.views.Theme -import org.isoron.uhabits.core.utils.DateUtils data class ScoreCardState( val scores: List, @@ -40,14 +42,14 @@ class ScoreCardPresenter( ) { companion object { val BUCKET_SIZES = intArrayOf(1, 7, 31, 92, 365) - fun getTruncateField(bucketSize: Int): DateUtils.TruncateField { + fun getTruncateField(bucketSize: Int): TruncateField { return when (bucketSize) { - 1 -> DateUtils.TruncateField.DAY - 7 -> DateUtils.TruncateField.WEEK_NUMBER - 31 -> DateUtils.TruncateField.MONTH - 92 -> DateUtils.TruncateField.QUARTER - 365 -> DateUtils.TruncateField.YEAR - else -> DateUtils.TruncateField.MONTH + 1 -> TruncateField.DAY + 7 -> TruncateField.WEEK_NUMBER + 31 -> TruncateField.MONTH + 92 -> TruncateField.QUARTER + 365 -> TruncateField.YEAR + else -> TruncateField.MONTH } } @@ -58,21 +60,20 @@ class ScoreCardPresenter( theme: Theme ): ScoreCardState { val bucketSize = BUCKET_SIZES[spinnerPosition] - val today = DateUtils.getTodayWithOffset() - val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today + val today = getToday() + val oldest = habit.computedEntries.getKnown().lastOrNull()?.date ?: today - val field = getTruncateField(bucketSize) - val scores = habit.scores.getByInterval(oldest, today).groupBy { - DateUtils.truncate(field, it.timestamp, firstWeekday) - }.map { (timestamp, scores) -> + val scores = habit.scores.getByInterval(oldest, today).groupBy { score -> + truncateDate(getTruncateField(bucketSize), score.date, firstWeekday) + }.map { (date, scores) -> Score( - timestamp, + date, scores.map { it.value }.average() ) }.sortedBy { - it.timestamp + it.date }.reversed() return ScoreCardState( @@ -83,6 +84,23 @@ class ScoreCardPresenter( theme = theme ) } + + private fun truncateDate( + field: TruncateField, + date: LocalDate, + firstWeekday: Int + ): LocalDate { + // firstWeekday: 1=Sunday, 2=Monday, ..., 7=Saturday + // DayOfWeek enum: SUNDAY(0), MONDAY(1), ..., SATURDAY(6) + val firstWeekdayDow = org.isoron.platform.time.DayOfWeek.entries[firstWeekday - 1] + return when (field) { + TruncateField.WEEK_NUMBER -> date.startOfWeek(firstWeekdayDow) + TruncateField.MONTH -> date.startOfMonth() + TruncateField.QUARTER -> date.startOfQuarter() + TruncateField.YEAR -> date.startOfYear() + else -> date + } + } } fun onSpinnerPosition(position: Int) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt index fb275627..b79730d8 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/TargetCard.kt @@ -19,14 +19,14 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views +import org.isoron.platform.time.TruncateField +import org.isoron.platform.time.getToday 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( @@ -44,62 +44,61 @@ class TargetCardPresenter { firstWeekday: Int, theme: Theme ): TargetCardState { - val today = DateUtils.getTodayWithOffset() - val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today + val today = getToday() + val oldest = habit.computedEntries.getKnown().lastOrNull()?.date ?: today val entries = habit.computedEntries.getByInterval(oldest, today) val valueToday = entries.groupedSum( - truncateField = DateUtils.TruncateField.DAY, + truncateField = TruncateField.DAY, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 val skippedDayToday = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.DAY + truncateField = TruncateField.DAY ).firstOrNull()?.value ?: 0 val valueThisWeek = entries.groupedSum( - truncateField = DateUtils.TruncateField.WEEK_NUMBER, + truncateField = TruncateField.WEEK_NUMBER, firstWeekday = firstWeekday, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 val skippedDaysThisWeek = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.WEEK_NUMBER, + truncateField = TruncateField.WEEK_NUMBER, firstWeekday = firstWeekday ).firstOrNull()?.value ?: 0 val valueThisMonth = entries.groupedSum( - truncateField = DateUtils.TruncateField.MONTH, + truncateField = TruncateField.MONTH, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 val skippedDaysThisMonth = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.MONTH + truncateField = TruncateField.MONTH ).firstOrNull()?.value ?: 0 val valueThisQuarter = entries.groupedSum( - truncateField = DateUtils.TruncateField.QUARTER, + truncateField = TruncateField.QUARTER, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 val skippedDaysThisQuarter = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.QUARTER + truncateField = TruncateField.QUARTER ).firstOrNull()?.value ?: 0 val valueThisYear = entries.groupedSum( - truncateField = DateUtils.TruncateField.YEAR, + truncateField = TruncateField.YEAR, isNumerical = habit.isNumerical ).firstOrNull()?.value ?: 0 val skippedDaysThisYear = entries.countSkippedDays( - truncateField = DateUtils.TruncateField.YEAR + truncateField = TruncateField.YEAR ).firstOrNull()?.value ?: 0 - val cal = DateUtils.getStartOfTodayCalendarWithOffset() - val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH) + val daysInMonth = today.monthLength val daysInWeek = 7 val daysInQuarter = 91 - val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR) + val daysInYear = today.yearLength val weeksInMonth = daysInMonth / 7 val weeksInQuarter = 13 val weeksInYear = 52 diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt index 42a5d270..ae6d0d71 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt @@ -139,7 +139,7 @@ class BarChart( canvas.setFontSize(theme.smallTextSize) var prevMonth = -1 var prevYear = -1 - val isLargeInterval = axis.size < 2 || (axis[0].distanceTo(axis[1]) > 300) + val isLargeInterval = axis.size < 2 || (axis[0].daysUntil(axis[1]) > 300) for (c in 0 until nColumns) { val x = barGroupOffset(c) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt index 3509e4ae..868769cd 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/widgets/WidgetBehavior.kt @@ -18,13 +18,13 @@ */ package org.isoron.uhabits.core.ui.widgets +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry.Companion.nextToggleValue import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.HabitList -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.NotificationTray import javax.inject.Inject @@ -35,47 +35,47 @@ class WidgetBehavior @Inject constructor( private val notificationTray: NotificationTray, private val preferences: Preferences ) { - fun onAddRepetition(habit: Habit, timestamp: Timestamp?) { + fun onAddRepetition(habit: Habit, date: LocalDate) { notificationTray.cancel(habit) - val entry = habit.originalEntries.get(timestamp!!) - setValue(habit, timestamp, Entry.YES_MANUAL, entry.notes) + val entry = habit.originalEntries.get(date) + setValue(habit, date, Entry.YES_MANUAL, entry.notes) } - fun onRemoveRepetition(habit: Habit, timestamp: Timestamp?) { + fun onRemoveRepetition(habit: Habit, date: LocalDate) { notificationTray.cancel(habit) - val entry = habit.originalEntries.get(timestamp!!) - setValue(habit, timestamp, Entry.NO, entry.notes) + val entry = habit.originalEntries.get(date) + setValue(habit, date, Entry.NO, entry.notes) } - fun onToggleRepetition(habit: Habit, timestamp: Timestamp) { - val entry = habit.originalEntries.get(timestamp) + fun onToggleRepetition(habit: Habit, date: LocalDate) { + val entry = habit.originalEntries.get(date) val currentValue = entry.value val newValue = nextToggleValue( value = currentValue, isSkipEnabled = preferences.isSkipEnabled, areQuestionMarksEnabled = preferences.areQuestionMarksEnabled ) - setValue(habit, timestamp, newValue, entry.notes) + setValue(habit, date, newValue, entry.notes) notificationTray.cancel(habit) } - fun onIncrement(habit: Habit, timestamp: Timestamp, amount: Int) { - val entry = habit.computedEntries.get(timestamp) + fun onIncrement(habit: Habit, date: LocalDate, amount: Int) { + val entry = habit.computedEntries.get(date) val currentValue = entry.value - setValue(habit, timestamp, currentValue + amount, entry.notes) + setValue(habit, date, currentValue + amount, entry.notes) notificationTray.cancel(habit) } - fun onDecrement(habit: Habit, timestamp: Timestamp, amount: Int) { - val entry = habit.computedEntries.get(timestamp) + fun onDecrement(habit: Habit, date: LocalDate, amount: Int) { + val entry = habit.computedEntries.get(date) val currentValue = entry.value - setValue(habit, timestamp, currentValue - amount, entry.notes) + setValue(habit, date, currentValue - amount, entry.notes) notificationTray.cancel(habit) } - fun setValue(habit: Habit, timestamp: Timestamp?, newValue: Int, notes: String) { + fun setValue(habit: Habit, date: LocalDate, newValue: Int, notes: String) { commandRunner.run( - CreateRepetitionCommand(habitList, habit, timestamp!!, newValue, notes) + CreateRepetitionCommand(habitList, habit, date, newValue, notes) ) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateFormats.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateFormats.kt index 88c39beb..348f0e34 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateFormats.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateFormats.kt @@ -38,11 +38,5 @@ class DateFormats { @JvmStatic fun getBackupDateFormat(): SimpleDateFormat = fromSkeleton("yyyy-MM-dd HHmmss", Locale.US) - - @JvmStatic fun getCSVDateFormat(): SimpleDateFormat = - fromSkeleton("yyyy-MM-dd", Locale.US) - - @JvmStatic fun getDialogDateFormat(): SimpleDateFormat = - fromSkeleton("MMM dd, yyyy", Locale.US) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt index e2552a14..1f039454 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/DateUtils.kt @@ -18,343 +18,99 @@ */ 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 -import java.util.Calendar.SHORT import java.util.Date import java.util.GregorianCalendar -import java.util.Locale import java.util.TimeZone -abstract class DateUtils { - companion object { - private var fixedLocalTime: Long? = null - private var fixedTimeZone: TimeZone? = null - private var fixedLocale: Locale? = null - private var startDayHourOffset: Int = 0 - private var startDayMinuteOffset: Int = 0 +object DateUtils { + const val SECOND_LENGTH: Long = 1000 + const val MINUTE_LENGTH: Long = 60 * SECOND_LENGTH + const val HOUR_LENGTH: Long = 60 * MINUTE_LENGTH + const val DAY_LENGTH: Long = 24 * HOUR_LENGTH - /** - * Number of milliseconds in one second. - */ - const val SECOND_LENGTH: Long = 1000 + private var fixedLocalTime: Long? = null - /** - * Number of milliseconds in one minute. - */ - const val MINUTE_LENGTH: Long = 60 * SECOND_LENGTH - - /** - * Number of milliseconds in one hour. - */ - const val HOUR_LENGTH: Long = 60 * MINUTE_LENGTH - - /** - * Number of milliseconds in one day. - */ - const val DAY_LENGTH: Long = 24 * HOUR_LENGTH - - @JvmStatic - fun applyTimezone(localTimestamp: Long): Long { - val tz: TimeZone = getTimeZone() - return localTimestamp - tz.getOffset(localTimestamp - tz.getOffset(localTimestamp)) - } - - @JvmStatic - fun formatHeaderDate(day: GregorianCalendar): String { - val locale = getLocale() - val dayOfMonth: String = day.get(DAY_OF_MONTH).toString() - val dayOfWeek = day.getDisplayName(DAY_OF_WEEK, SHORT, locale) - return dayOfWeek + "\n" + dayOfMonth - } - - private fun getCalendar(timestamp: Long): GregorianCalendar { - val day = GregorianCalendar(TimeZone.getTimeZone("GMT"), getLocale()) - day.timeInMillis = timestamp - return day - } - - @JvmStatic - fun getLocalTime(utcTimeInMillis: Long? = null): Long { - if (fixedLocalTime != null) return fixedLocalTime as Long - - val tz = getTimeZone() - val now = utcTimeInMillis ?: Date().time - return now + tz.getOffset(now) - } - - /** - * Returns an array of strings with the names for each day of the week, - * in either SHORT or LONG format. The first entry corresponds to the - * first day of the week, according to the provided argument. - * - * @param format Either GregorianCalendar.SHORT or LONG - * @param firstWeekDay An integer representing the first day of the week, - * following java.util.Calendar conventions. That is, - * Saturday corresponds to 7, and Sunday corresponds - * to 1. - */ - private fun getWeekdayNames( - format: Int, - firstWeekDay: Int - ): Array { - val calendar = GregorianCalendar(getLocale()) - calendar.set(DAY_OF_WEEK, firstWeekDay) - - val daysNullable = ArrayList() - for (i in 1..7) { - daysNullable.add( - calendar.getDisplayName( - DAY_OF_WEEK, - format, - getLocale() - ) - ) - - calendar.add(DAY_OF_MONTH, 1) - } - - return daysNullable.toTypedArray() - } - - /** - * Returns a vector of exactly seven integers, where the first integer is - * the provided firstWeekday number, and each subsequent number is the - * previous number plus 1, wrapping back to 1 after 7. For example, - * providing 3 as firstWeekday returns {3,4,5,6,7,1,2} - * - * This function is supposed to be used to construct a sequence of weekday - * number following java.util.Calendar conventions. - */ - @JvmStatic - fun getWeekdaySequence(firstWeekday: Int): Array { - return arrayOf( - (firstWeekday - 1) % 7 + 1, - (firstWeekday) % 7 + 1, - (firstWeekday + 1) % 7 + 1, - (firstWeekday + 2) % 7 + 1, - (firstWeekday + 3) % 7 + 1, - (firstWeekday + 4) % 7 + 1, - (firstWeekday + 5) % 7 + 1 - ) - } - - /** - * @return An integer representing the first day of the week, according to - * the current locale. Sunday corresponds to 1, Monday to 2, and so on, - * until Saturday, which is represented by 7. This is consistent - * with java.util.Calendar constants. - */ - @JvmStatic - fun getFirstWeekdayNumberAccordingToLocale(): Int { - return GregorianCalendar(getLocale()).firstDayOfWeek - } - - /** - * @return A vector of strings with the long names for the week days, - * according to the current locale. The first entry corresponds to Saturday, - * the second entry corresponds to Monday, and so on. - * - * @param firstWeekday Either Calendar.SATURDAY, Calendar.MONDAY, or other - * weekdays defined in this class. - */ - @JvmStatic - fun getLongWeekdayNames(firstWeekday: Int): Array { - return getWeekdayNames(GregorianCalendar.LONG, firstWeekday) - } - - /** - * Returns a vector of strings with the short names for the week days, - * according to the current locale. The first entry corresponds to Saturday, - * the second entry corresponds to Monday, and so on. - * - * @param firstWeekday Either Calendar.SATURDAY, Calendar.MONDAY, or other - * weekdays defined in this class. - */ - @JvmStatic - fun getShortWeekdayNames(firstWeekday: Int): Array { - 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 { - 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 getMonthsSince1970(today: GregorianCalendar): Int { - val start = GregorianCalendar(TimeZone.getTimeZone("GMT")) - start.set(1970, Calendar.JANUARY, 1, 0, 0, 0) - start.set(Calendar.MILLISECOND, 0) - - val years = today.get(Calendar.YEAR) - start.get(Calendar.YEAR) - 2 - val months = today.get(Calendar.MONTH) - start.get(Calendar.MONTH) - - return years * 12 + months - } - - @JvmStatic - fun getToday(): Timestamp = Timestamp(getStartOfToday()) - - @JvmStatic - fun getTodayWithOffset(): Timestamp = Timestamp(getStartOfTodayWithOffset()) - - @JvmStatic - fun getStartOfDay(timestamp: Long): Long = (timestamp / DAY_LENGTH) * DAY_LENGTH - - @JvmStatic - fun getStartOfDayWithOffset(timestamp: Long): Long { - val offset = startDayHourOffset * HOUR_LENGTH + startDayMinuteOffset * MINUTE_LENGTH - return getStartOfDay(timestamp - offset) - } - - @JvmStatic - fun getStartOfToday(): Long = getStartOfDay(getLocalTime()) - - @JvmStatic - fun getStartOfTomorrowWithOffset(): Long = getUpcomingTimeInMillis( - startDayHourOffset, - startDayMinuteOffset - ) - - @JvmStatic - fun getStartOfTodayWithOffset(): Long = getStartOfDayWithOffset(getLocalTime()) - - @JvmStatic - fun millisecondsUntilTomorrowWithOffset(): Long = getStartOfTomorrowWithOffset() - applyTimezone(getLocalTime()) - - @JvmStatic - fun getStartOfTodayCalendar(): GregorianCalendar = getCalendar(getStartOfToday()) - - @JvmStatic - fun getStartOfTodayCalendarWithOffset(): GregorianCalendar = getCalendar(getStartOfTodayWithOffset()) - - private fun getTimeZone(): TimeZone { - return fixedTimeZone ?: TimeZone.getDefault() - } - - @JvmStatic - fun removeTimezone(timestamp: Long): Long { - val tz = getTimeZone() - return timestamp + tz.getOffset(timestamp) - } - - @JvmStatic - fun setStartDayOffset(hourOffset: Int, minuteOffset: Int) { - startDayHourOffset = hourOffset - startDayMinuteOffset = minuteOffset - } - - private fun getLocale(): Locale { - return fixedLocale ?: Locale.getDefault() - } - - @JvmStatic - fun truncate( - field: TruncateField, - timestamp: Timestamp, - firstWeekday: Int - ): Timestamp { - return Timestamp( - truncate( - field, - timestamp.unixTime, - firstWeekday - ) - ) - } - - @JvmStatic - fun truncate( - field: TruncateField, - timestamp: Long, - firstWeekday: Int - ): Long { - val cal = getCalendar(timestamp) - - return when (field) { - TruncateField.DAY -> { cal.timeInMillis } - - TruncateField.MONTH -> { - cal.set(DAY_OF_MONTH, 1) - cal.timeInMillis - } - - TruncateField.WEEK_NUMBER -> { - val weekDay = cal.get(DAY_OF_WEEK) - var delta = weekDay - firstWeekday - if (delta < 0) { delta += 7 } - cal.add(Calendar.DAY_OF_YEAR, -delta) - cal.timeInMillis - } - - TruncateField.QUARTER -> { - val quarter = cal.get(Calendar.MONTH) / 3 - cal.set(DAY_OF_MONTH, 1) - cal.set(Calendar.MONTH, quarter * 3) - cal.timeInMillis - } - - TruncateField.YEAR -> { - cal.set(Calendar.MONTH, Calendar.JANUARY) - cal.set(DAY_OF_MONTH, 1) - cal.timeInMillis - } - } - } - - @JvmStatic - fun getUpcomingTimeInMillis( - hour: Int, - minute: Int - ): Long { - val calendar = getStartOfTodayCalendar() - calendar.set(Calendar.HOUR_OF_DAY, hour) - calendar.set(Calendar.MINUTE, minute) - calendar.set(Calendar.SECOND, 0) - var time = calendar.timeInMillis - - if (getLocalTime() > time) { - time += DAY_LENGTH - } - - return applyTimezone(time) - } - - @JvmStatic - fun setFixedLocalTime(newFixedLocalTime: Long?) { - this.fixedLocalTime = newFixedLocalTime - } - - @JvmStatic - fun setFixedTimeZone(newTimeZone: TimeZone?) { - this.fixedTimeZone = newTimeZone - } - - @JvmStatic - fun setFixedLocale(newLocale: Locale?) { - this.fixedLocale = newLocale - } + @JvmStatic + fun setFixedLocalTime(value: Long?) { + fixedLocalTime = value } - enum class TruncateField { - DAY, MONTH, WEEK_NUMBER, YEAR, QUARTER + @JvmStatic + var fixedTimeZone: TimeZone? = null + private set + + fun setFixedTimeZone(value: TimeZone?) { + fixedTimeZone = value + } + + fun applyTimezone( + localTimestamp: Long, + tz: TimeZone = fixedTimeZone ?: TimeZone.getDefault() + ): Long { + return localTimestamp - tz.getOffset(localTimestamp - tz.getOffset(localTimestamp)) + } + + fun removeTimezone( + timestamp: Long, + tz: TimeZone = fixedTimeZone ?: TimeZone.getDefault() + ): Long { + return timestamp + tz.getOffset(timestamp) + } + + fun getLocalTime( + tz: TimeZone = fixedTimeZone ?: TimeZone.getDefault(), + utcTimeInMillis: Long? = null + ): Long { + fixedLocalTime?.let { return it } + val now = utcTimeInMillis ?: Date().time + return now + tz.getOffset(now) + } + + fun getStartOfDay(timestamp: Long): Long = (timestamp / DAY_LENGTH) * DAY_LENGTH + + fun getStartOfDayWithOffset( + timestamp: Long, + hourOffset: Int, + minuteOffset: Int + ): Long { + val offset = hourOffset * HOUR_LENGTH + minuteOffset * MINUTE_LENGTH + return getStartOfDay(timestamp - offset) + } + + fun getStartOfTomorrowWithOffset( + hourOffset: Int, + minuteOffset: Int, + tz: TimeZone = fixedTimeZone ?: TimeZone.getDefault() + ): Long = getUpcomingTimeInMillis(hourOffset, minuteOffset, tz) + + fun millisecondsUntilTomorrowWithOffset( + hourOffset: Int, + minuteOffset: Int, + tz: TimeZone = fixedTimeZone ?: TimeZone.getDefault() + ): Long { + return getStartOfTomorrowWithOffset(hourOffset, minuteOffset, tz) - + applyTimezone(getLocalTime(tz), tz) + } + + fun getUpcomingTimeInMillis( + hour: Int, + minute: Int, + tz: TimeZone = fixedTimeZone ?: TimeZone.getDefault() + ): Long { + val localTime = getLocalTime(tz) + val startOfToday = getStartOfDay(localTime) + val calendar = GregorianCalendar(TimeZone.getTimeZone("GMT")) + calendar.timeInMillis = startOfToday + calendar.set(Calendar.HOUR_OF_DAY, hour) + calendar.set(Calendar.MINUTE, minute) + calendar.set(Calendar.SECOND, 0) + var time = calendar.timeInMillis + + if (localTime > time) { + time += DAY_LENGTH + } + + return applyTimezone(time, tz) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt index 548c0104..7991bc94 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/utils/MidnightTimer.kt @@ -18,8 +18,11 @@ */ package org.isoron.uhabits.core.utils +import org.isoron.platform.time.computeToday +import org.isoron.platform.time.setToday import org.isoron.uhabits.core.AppScope import org.isoron.uhabits.core.io.Logging +import org.isoron.uhabits.core.preferences.Preferences import java.util.LinkedList import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -30,7 +33,10 @@ import javax.inject.Inject * A class that emits events when a new day starts. */ @AppScope -open class MidnightTimer @Inject constructor(logging: Logging) { +open class MidnightTimer @Inject constructor( + logging: Logging, + private val preferences: Preferences +) { private val listeners: MutableList = LinkedList() private lateinit var executor: ScheduledExecutorService private val logger = logging.getLogger("MidnightTimer") @@ -52,7 +58,7 @@ open class MidnightTimer @Inject constructor(logging: Logging) { testExecutor: ScheduledExecutorService? = null ) { executor = testExecutor ?: Executors.newSingleThreadScheduledExecutor() - val initialDelay = DateUtils.millisecondsUntilTomorrowWithOffset() + delayOffsetInMillis + val initialDelay = DateUtils.millisecondsUntilTomorrowWithOffset(preferences.midnightDelayHours, 0) + delayOffsetInMillis logger.info("Scheduling refresh for $initialDelay ms from now") executor.scheduleAtFixedRate( { notifyListeners() }, @@ -68,6 +74,7 @@ open class MidnightTimer @Inject constructor(logging: Logging) { @Synchronized private fun notifyListeners() { logger.info("Midnight refresh") + setToday(computeToday(preferences.midnightDelayHours, 0)) for (l in listeners) { l.atMidnight() } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/platform/gui/DatesTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/platform/gui/DatesTest.kt index 43562c7c..c2aeff1a 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/platform/gui/DatesTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/platform/gui/DatesTest.kt @@ -19,16 +19,194 @@ package org.isoron.platform.gui +import org.isoron.platform.time.DayOfWeek import org.isoron.platform.time.LocalDate -import org.junit.Assert.assertEquals +import org.isoron.platform.time.computeToday +import org.isoron.platform.time.countWeekdayOccurrencesInMonth +import org.isoron.platform.time.getToday +import org.isoron.platform.time.getWeekdaySequence +import org.isoron.platform.time.resetToday +import org.isoron.platform.time.setToday +import org.junit.Assert.assertArrayEquals import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue class DatesTest { @Test fun testDatesBefore2000() { val date = LocalDate(-1) - assertEquals(date.day, 31) - assertEquals(date.month, 12) - assertEquals(date.year, 1999) + assertEquals(31, date.day) + assertEquals(12, date.month) + assertEquals(1999, date.year) + } + + @Test + fun testDayOfWeekBefore2000() { + // 1999-12-31 (daysSince2000 = -1) was a Friday + assertEquals(DayOfWeek.FRIDAY, LocalDate(1999, 12, 31).dayOfWeek) + // 1999-12-30 (daysSince2000 = -2) was a Thursday + assertEquals(DayOfWeek.THURSDAY, LocalDate(1999, 12, 30).dayOfWeek) + // 1999-12-25 (Saturday) + assertEquals(DayOfWeek.SATURDAY, LocalDate(1999, 12, 25).dayOfWeek) + // 1999-12-26 (Sunday) + assertEquals(DayOfWeek.SUNDAY, LocalDate(1999, 12, 26).dayOfWeek) + } + + @Test + fun testFromUnixTimeBefore2000() { + val epoch2000 = 946684800000L + val msPerDay = 86400000L + // One millisecond before 2000-01-01 should be 1999-12-31 + val justBefore2000 = LocalDate.fromUnixTime(epoch2000 - 1) + assertEquals(LocalDate(1999, 12, 31), justBefore2000) + // One full day before 2000-01-01 should be 1999-12-31 + val oneDayBefore = LocalDate.fromUnixTime(epoch2000 - msPerDay) + assertEquals(LocalDate(1999, 12, 31), oneDayBefore) + // One full day + 1ms before should be 1999-12-30 + val oneDayAndOneMsBefore = LocalDate.fromUnixTime(epoch2000 - msPerDay - 1) + assertEquals(LocalDate(1999, 12, 30), oneDayAndOneMsBefore) + } + + @Test + fun testGetWeekdaySequence() { + val seq = getWeekdaySequence(DayOfWeek.TUESDAY) + assertEquals( + listOf( + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY + ), + seq + ) + val seqSun = getWeekdaySequence(DayOfWeek.SUNDAY) + assertEquals( + listOf( + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY + ), + seqSun + ) + } + + @Test + fun testCountWeekdayOccurrencesInMonth() { + // February 2018 (28 days, starts on Thursday) + assertArrayEquals( + arrayOf(4, 4, 4, 4, 4, 4, 4), + countWeekdayOccurrencesInMonth(LocalDate(2018, 2, 1)) + ) + // February 2020 (leap, 29 days, starts on Saturday) + assertArrayEquals( + arrayOf(5, 4, 4, 4, 4, 4, 4), + countWeekdayOccurrencesInMonth(LocalDate(2020, 2, 1)) + ) + // April 2020 (30 days, starts on Wednesday) + assertArrayEquals( + arrayOf(4, 4, 4, 4, 5, 5, 4), + countWeekdayOccurrencesInMonth(LocalDate(2020, 4, 1)) + ) + // August 2020 (31 days, starts on Saturday) + assertArrayEquals( + arrayOf(5, 5, 5, 4, 4, 4, 4), + countWeekdayOccurrencesInMonth(LocalDate(2020, 8, 1)) + ) + } + + @Test + fun testStartOfWeek() { + // Wednesday 2015-01-28 + val wed = LocalDate(2015, 1, 28) + + // firstWeekday = SUNDAY -> start is Sunday 2015-01-25 + assertEquals(LocalDate(2015, 1, 25), wed.startOfWeek(DayOfWeek.SUNDAY)) + + // firstWeekday = MONDAY -> start is Monday 2015-01-26 + assertEquals(LocalDate(2015, 1, 26), wed.startOfWeek(DayOfWeek.MONDAY)) + + // firstWeekday = WEDNESDAY -> start is Wednesday 2015-01-28 (same day) + assertEquals(LocalDate(2015, 1, 28), wed.startOfWeek(DayOfWeek.WEDNESDAY)) + + // firstWeekday = SATURDAY -> start is Saturday 2015-01-24 + assertEquals(LocalDate(2015, 1, 24), wed.startOfWeek(DayOfWeek.SATURDAY)) + } + + @Test + fun testStartOfMonth() { + assertEquals(LocalDate(2015, 1, 1), LocalDate(2015, 1, 25).startOfMonth()) + assertEquals(LocalDate(2015, 2, 1), LocalDate(2015, 2, 28).startOfMonth()) + assertEquals(LocalDate(2024, 2, 1), LocalDate(2024, 2, 29).startOfMonth()) + } + + @Test + fun testStartOfQuarter() { + assertEquals(LocalDate(2015, 1, 1), LocalDate(2015, 1, 15).startOfQuarter()) + assertEquals(LocalDate(2015, 1, 1), LocalDate(2015, 3, 31).startOfQuarter()) + assertEquals(LocalDate(2015, 4, 1), LocalDate(2015, 4, 1).startOfQuarter()) + assertEquals(LocalDate(2015, 4, 1), LocalDate(2015, 6, 15).startOfQuarter()) + assertEquals(LocalDate(2015, 7, 1), LocalDate(2015, 9, 30).startOfQuarter()) + assertEquals(LocalDate(2015, 10, 1), LocalDate(2015, 12, 31).startOfQuarter()) + } + + @Test + fun testStartOfYear() { + assertEquals(LocalDate(2015, 1, 1), LocalDate(2015, 6, 15).startOfYear()) + assertEquals(LocalDate(2024, 1, 1), LocalDate(2024, 12, 31).startOfYear()) + } + + @Test + fun testToCSVString() { + assertEquals("2015-01-25", LocalDate(2015, 1, 25).toCSVString()) + assertEquals("2024-02-29", LocalDate(2024, 2, 29).toCSVString()) + assertEquals("1999-12-31", LocalDate(1999, 12, 31).toCSVString()) + } + + @Test + fun testComparisons() { + val jan1 = LocalDate(2015, 1, 1) + val jan2 = LocalDate(2015, 1, 2) + val jan1b = LocalDate(2015, 1, 1) + + assert(jan1 < jan2) + assert(jan2 > jan1) + assert(jan1 <= jan1b) + assert(jan1 >= jan1b) + assert(jan1 == jan1b) + } + + @Test + fun testGetTodayBeforeSetTodayThrows() { + resetToday() + try { + assertFailsWith { getToday() } + } finally { + setToday(LocalDate(2015, 1, 25)) + } + } + + @Test + fun testSetTodayUpdatable() { + setToday(LocalDate(2015, 1, 25)) + assertEquals(LocalDate(2015, 1, 25), getToday()) + setToday(LocalDate(2020, 6, 15)) + assertEquals(LocalDate(2020, 6, 15), getToday()) + } + + @Test + fun testComputeTodayReturnsSensibleDate() { + val today = computeToday() + // Should be somewhere around the current date (after 2020, before 2100) + assertTrue(today.year >= 2020) + assertTrue(today.year <= 2100) } } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/platform/gui/JavaLocalDateFormatterTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/platform/gui/JavaLocalDateFormatterTest.kt new file mode 100644 index 00000000..cc6416bc --- /dev/null +++ b/uhabits-core/src/jvmTest/java/org/isoron/platform/gui/JavaLocalDateFormatterTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016-2025 Á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.platform.gui + +import org.isoron.platform.time.DayOfWeek +import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.getFirstWeekdayNumberAccordingToLocale +import org.junit.Test +import java.util.Locale +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class JavaLocalDateFormatterTest { + + @Test + fun testShortWeekdayNames_us() { + val formatter = JavaLocalDateFormatter(Locale.US) + assertContentEquals( + arrayOf("Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"), + formatter.shortWeekdayNames(DayOfWeek.SATURDAY) + ) + } + + @Test + fun testShortWeekdayNames_germany() { + val formatter = JavaLocalDateFormatter(Locale.GERMANY) + assertContentEquals( + arrayOf("Sa.", "So.", "Mo.", "Di.", "Mi.", "Do.", "Fr."), + formatter.shortWeekdayNames(DayOfWeek.SATURDAY) + ) + } + + @Test + fun testLongWeekdayNames_us() { + val formatter = JavaLocalDateFormatter(Locale.US) + assertContentEquals( + arrayOf("Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"), + formatter.longWeekdayNames(DayOfWeek.SATURDAY) + ) + } + + @Test + fun testLongWeekdayNames_germany() { + val formatter = JavaLocalDateFormatter(Locale.GERMANY) + assertContentEquals( + arrayOf("Samstag", "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"), + formatter.longWeekdayNames(DayOfWeek.SATURDAY) + ) + } + + @Test + fun testGetFirstWeekdayNumberAccordingToLocale_us() { + val saved = Locale.getDefault() + try { + Locale.setDefault(Locale.US) + assertEquals(1, getFirstWeekdayNumberAccordingToLocale()) + } finally { + Locale.setDefault(saved) + } + } + + @Test + fun testGetFirstWeekdayNumberAccordingToLocale_germany() { + val saved = Locale.getDefault() + try { + Locale.setDefault(Locale.GERMANY) + assertEquals(2, getFirstWeekdayNumberAccordingToLocale()) + } finally { + Locale.setDefault(saved) + } + } +} diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt index 8737ecc9..efe08a78 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/BaseUnitTest.kt @@ -19,6 +19,8 @@ package org.isoron.uhabits.core import org.apache.commons.io.IOUtils +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.setToday import org.isoron.uhabits.core.commands.CommandRunner import org.isoron.uhabits.core.database.Database import org.isoron.uhabits.core.database.DatabaseOpener @@ -26,13 +28,9 @@ import org.isoron.uhabits.core.database.JdbcDatabase import org.isoron.uhabits.core.database.MigrationHelper import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.ModelFactory -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.memory.MemoryModelFactory import org.isoron.uhabits.core.tasks.SingleThreadTaskRunner import org.isoron.uhabits.core.test.HabitFixtures -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime -import org.isoron.uhabits.core.utils.DateUtils.Companion.setStartDayOffset import org.junit.After import org.junit.Before import org.junit.Test @@ -48,6 +46,8 @@ import java.io.InputStream import java.nio.file.Paths import java.sql.DriverManager import java.sql.SQLException +import java.util.GregorianCalendar +import java.util.TimeZone @RunWith(MockitoJUnitRunner::class) open class BaseUnitTest { @@ -76,8 +76,7 @@ open class BaseUnitTest { @Before @Throws(Exception::class) open fun setUp() { - setFixedLocalTime(FIXED_LOCAL_TIME) - setStartDayOffset(0, 0) + setToday(LocalDate(2015, 1, 25)) val memoryModelFactory = MemoryModelFactory() habitList = spy(memoryModelFactory.buildHabitList()) fixtures = HabitFixtures(memoryModelFactory, habitList) @@ -90,8 +89,6 @@ open class BaseUnitTest { @Throws(Exception::class) open fun tearDown() { validateMockitoUsage() - setFixedLocalTime(null) - setStartDayOffset(0, 0) } fun unixTime(year: Int, month: Int, day: Int): Long { @@ -99,15 +96,12 @@ open class BaseUnitTest { } open fun unixTime(year: Int, month: Int, day: Int, hour: Int, minute: Int, milliseconds: Long = 0): Long { - val cal = getStartOfTodayCalendar() - cal.set(year, month, day, hour, minute) + val cal = GregorianCalendar(TimeZone.getTimeZone("GMT")) + cal.set(year, month, day, hour, minute, 0) + cal.set(GregorianCalendar.MILLISECOND, 0) return cal.timeInMillis + milliseconds } - fun timestamp(year: Int, month: Int, day: Int): Timestamp { - return Timestamp(unixTime(year, month, day)) - } - @Test fun nothing() { } @@ -139,8 +133,6 @@ open class BaseUnitTest { } companion object { - // 8:00am, January 25th, 2015 (UTC) - const val FIXED_LOCAL_TIME = 1422172800000L fun buildMemoryDatabase(): Database { return try { val db: Database = JdbcDatabase( diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/CreateRepetitionCommandTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/CreateRepetitionCommandTest.kt index 4e6efd3a..c19f140a 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/CreateRepetitionCommandTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/CreateRepetitionCommandTest.kt @@ -18,11 +18,11 @@ */ package org.isoron.uhabits.core.commands +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -30,7 +30,7 @@ import kotlin.test.assertEquals class CreateRepetitionCommandTest : BaseUnitTest() { private lateinit var command: CreateRepetitionCommand private lateinit var habit: Habit - private lateinit var today: Timestamp + private lateinit var today: LocalDate @Before @Throws(Exception::class) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt index 60e9bc0a..cb4f6774 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/commands/EditHabitCommandTest.kt @@ -20,11 +20,11 @@ package org.isoron.uhabits.core.commands import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.junit.Before import org.junit.Test @@ -32,7 +32,7 @@ class EditHabitCommandTest : BaseUnitTest() { private lateinit var command: EditHabitCommand private lateinit var habit: Habit private lateinit var modified: Habit - private lateinit var today: Timestamp + private lateinit var today: LocalDate @Before @Throws(Exception::class) @@ -47,7 +47,7 @@ class EditHabitCommandTest : BaseUnitTest() { modified.copyFrom(habit) modified.name = "modified" habitList.add(modified) - today = getTodayWithOffset() + today = getToday() } @Test diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/io/ImportTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/io/ImportTest.kt index 9fa5103f..2801f943 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/io/ImportTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/io/ImportTest.kt @@ -20,14 +20,12 @@ package org.isoron.uhabits.core.io import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo +import org.isoron.platform.time.LocalDate 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 import org.junit.Before import org.junit.Test import java.io.File @@ -40,7 +38,6 @@ class ImportTest : BaseUnitTest() { @Throws(Exception::class) override fun setUp() { super.setUp() - setFixedLocalTime(null) } @Test @@ -168,17 +165,11 @@ class ImportTest : BaseUnitTest() { } 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 + return h.originalEntries.get(LocalDate(year, month, day)).value } private fun isNotesEqual(h: Habit, year: Int, month: Int, day: Int, notes: String): Boolean { - val date = getStartOfTodayCalendar() - date.set(year, month - 1, day) - val timestamp = Timestamp(date) - return h.originalEntries.get(timestamp).notes == notes + return h.originalEntries.get(LocalDate(year, month, day)).notes == notes } @Throws(IOException::class) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt index f494ebab..fd5a16b2 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/EntryListTest.kt @@ -21,13 +21,13 @@ package org.isoron.uhabits.core.models import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.TruncateField import org.isoron.uhabits.core.models.Entry.Companion.NO 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.isoron.uhabits.core.utils.DateUtils import org.junit.Test -import java.util.Calendar import java.util.Random import kotlin.test.assertEquals @@ -35,7 +35,7 @@ class EntryListTest { @Test fun testEmptyList() { val entries = EntryList() - val today = DateUtils.getToday() + val today = LocalDate(2015, 1, 25) assertEquals(Entry(today.minus(0), UNKNOWN), entries.get(today.minus(0))) assertEquals(Entry(today.minus(2), UNKNOWN), entries.get(today.minus(2))) @@ -67,7 +67,7 @@ class EntryListTest { @Test fun testComputeBoolean() { - val today = DateUtils.getToday() + val today = LocalDate(2015, 1, 25) val original = EntryList() original.add(Entry(today.minus(4), YES_MANUAL)) @@ -97,7 +97,7 @@ class EntryListTest { @Test fun testComputeNumerical() { - val today = DateUtils.getToday() + val today = LocalDate(2015, 1, 25) val original = EntryList() original.add(Entry(today.minus(4), 100)) @@ -136,37 +136,37 @@ class EntryListTest { 370, 187, 208, 231, 341, 312 ) - val reference = Timestamp.from(2014, Calendar.JUNE, 1) + val reference = LocalDate(2014, 6, 1) val entries = EntryList() offsets.indices.forEach { entries.add(Entry(reference.minus(offsets[it]), values[it])) } val byMonth = entries.getKnown().groupedSum( - truncateField = DateUtils.TruncateField.MONTH, + truncateField = TruncateField.MONTH, isNumerical = true ) assertThat(byMonth.size, equalTo(17)) - assertThat(byMonth[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 230))) - assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 1988))) - assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 1271))) + assertThat(byMonth[0], equalTo(Entry(LocalDate(2014, 6, 1), 230))) + assertThat(byMonth[6], equalTo(Entry(LocalDate(2013, 12, 1), 1988))) + assertThat(byMonth[12], equalTo(Entry(LocalDate(2013, 5, 1), 1271))) val byQuarter = entries.getKnown().groupedSum( - truncateField = DateUtils.TruncateField.QUARTER, + truncateField = TruncateField.QUARTER, isNumerical = true ) assertThat(byQuarter.size, equalTo(6)) - assertThat(byQuarter[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 3263))) - assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 3838))) - assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 4975))) + assertThat(byQuarter[0], equalTo(Entry(LocalDate(2014, 4, 1), 3263))) + assertThat(byQuarter[3], equalTo(Entry(LocalDate(2013, 7, 1), 3838))) + assertThat(byQuarter[5], equalTo(Entry(LocalDate(2013, 1, 1), 4975))) val byYear = entries.getKnown().groupedSum( - truncateField = DateUtils.TruncateField.YEAR, + truncateField = TruncateField.YEAR, isNumerical = true ) assertThat(byYear.size, equalTo(2)) - assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 8227))) - assertThat(byYear[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 16172))) + assertThat(byYear[0], equalTo(Entry(LocalDate(2014, 1, 1), 8227))) + assertThat(byYear[1], equalTo(Entry(LocalDate(2013, 1, 1), 16172))) } @Test @@ -180,37 +180,37 @@ class EntryListTest { 455, 460, 462, 465, 470, 471, 479, 481, 485, 489, 494, 495, 500, 501, 503, 507 ) - val reference = Timestamp.from(2014, Calendar.JUNE, 1) + val reference = LocalDate(2014, 6, 1) val entries = EntryList() offsets.indices.forEach { entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL)) } val byMonth = entries.getKnown().groupedSum( - truncateField = DateUtils.TruncateField.MONTH, + truncateField = TruncateField.MONTH, isNumerical = false ) assertThat(byMonth.size, equalTo(17)) - assertThat(byMonth[0], equalTo(Entry(Timestamp.from(2014, Calendar.JUNE, 1), 1_000))) - assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 7_000))) - assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 6_000))) + assertThat(byMonth[0], equalTo(Entry(LocalDate(2014, 6, 1), 1_000))) + assertThat(byMonth[6], equalTo(Entry(LocalDate(2013, 12, 1), 7_000))) + assertThat(byMonth[12], equalTo(Entry(LocalDate(2013, 5, 1), 6_000))) val byQuarter = entries.getKnown().groupedSum( - truncateField = DateUtils.TruncateField.QUARTER, + truncateField = TruncateField.QUARTER, isNumerical = false ) assertThat(byQuarter.size, equalTo(6)) - assertThat(byQuarter[0], equalTo(Entry(Timestamp.from(2014, Calendar.APRIL, 1), 15_000))) - assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 17_000))) - assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 20_000))) + assertThat(byQuarter[0], equalTo(Entry(LocalDate(2014, 4, 1), 15_000))) + assertThat(byQuarter[3], equalTo(Entry(LocalDate(2013, 7, 1), 17_000))) + assertThat(byQuarter[5], equalTo(Entry(LocalDate(2013, 1, 1), 20_000))) val byYear = entries.getKnown().groupedSum( - truncateField = DateUtils.TruncateField.YEAR, + truncateField = TruncateField.YEAR, isNumerical = false ) assertThat(byYear.size, equalTo(2)) - assertThat(byYear[0], equalTo(Entry(Timestamp.from(2014, Calendar.JANUARY, 1), 34_000))) - assertThat(byYear[1], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 66_000))) + assertThat(byYear[0], equalTo(Entry(LocalDate(2014, 1, 1), 34_000))) + assertThat(byYear[1], equalTo(Entry(LocalDate(2013, 1, 1), 66_000))) } @Test @@ -348,31 +348,33 @@ class EntryListTest { val random = Random(123L) val weekdayCount = Array(12) { Array(7) { 0 } } val monthCount = Array(12) { 0 } - val day = DateUtils.getStartOfTodayCalendar() // Add repetitions randomly from January to December - day.set(2015, Calendar.JANUARY, 1, 0, 0, 0) + var day = LocalDate(2015, 1, 1) for (i in 0..364) { if (random.nextBoolean()) { - val month = day[Calendar.MONTH] - val week = day[Calendar.DAY_OF_WEEK] % 7 + val month = day.month - 1 + val weekday = (day.dayOfWeek.daysSinceSunday + 1) % 7 // Leave the month of March empty, to check that it returns null - if (month == Calendar.MARCH) continue + if (month == 2) { + day = day.plus(1) + continue + } - entries.add(Entry(Timestamp(day), YES_MANUAL)) - weekdayCount[month][week]++ + entries.add(Entry(day, YES_MANUAL)) + weekdayCount[month][weekday]++ monthCount[month]++ } - day.add(Calendar.DAY_OF_YEAR, 1) + day = day.plus(1) } val freq = entries.computeWeekdayFrequency(isNumerical = false) // Repetitions should be counted correctly for (month in 0..11) { - day.set(2015, month, 1, 0, 0, 0) - val actualCount = freq[Timestamp(day)] + val monthStart = LocalDate(2015, month + 1, 1) + val actualCount = freq[monthStart] if (monthCount[month] == 0) { assertThat(actualCount, equalTo(null)) } else { @@ -381,5 +383,5 @@ class EntryListTest { } } - fun day(offset: Int) = DateUtils.getToday().minus(offset) + fun day(offset: Int): LocalDate = LocalDate(2015, 1, 25).minus(offset) } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/HabitTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/HabitTest.kt index 866a4758..efd03ac3 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/HabitTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/HabitTest.kt @@ -21,8 +21,8 @@ package org.isoron.uhabits.core.models import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.BaseUnitTest -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.junit.Assert.assertNotEquals import org.junit.Test import kotlin.test.assertEquals diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt index 723a3c23..1a136f9a 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/ScoreListTest.kt @@ -21,9 +21,10 @@ package org.isoron.uhabits.core.models import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.number.IsCloseTo import org.hamcrest.number.OrderingComparison +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday 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 import java.util.ArrayList @@ -31,7 +32,7 @@ import kotlin.test.assertTrue open class BaseScoreListTest : BaseUnitTest() { protected lateinit var habit: Habit - protected lateinit var today: Timestamp + protected lateinit var today: LocalDate @Before @Throws(Exception::class) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/StreakListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/StreakListTest.kt index 1ef271ec..4291b8f0 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/StreakListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/StreakListTest.kt @@ -20,14 +20,15 @@ package org.isoron.uhabits.core.models import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.BaseUnitTest -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.junit.Test class StreakListTest : BaseUnitTest() { private lateinit var habit: Habit private lateinit var streaks: StreakList - private lateinit var today: Timestamp + private lateinit var today: LocalDate @Throws(Exception::class) override fun setUp() { diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/TimestampTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/TimestampTest.kt deleted file mode 100644 index cf5a3ce1..00000000 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/TimestampTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2016-2025 Á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 - -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.greaterThan -import org.hamcrest.Matchers.lessThan -import org.isoron.uhabits.core.BaseUnitTest -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday -import org.junit.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class TimestampTest : BaseUnitTest() { - @Test - @Throws(Exception::class) - fun testCompare() { - val t1 = getToday() - val t2 = t1.minus(1) - val t3 = t1.plus(3) - assertThat(t1.compareTo(t2), greaterThan(0)) - assertThat(t1.compareTo(t1), equalTo(0)) - assertThat(t1.compareTo(t3), lessThan(0)) - assertTrue(t1.isNewerThan(t2)) - assertFalse(t1.isNewerThan(t1)) - assertFalse(t2.isNewerThan(t1)) - assertTrue(t2.isOlderThan(t1)) - assertFalse(t1.isOlderThan(t2)) - } - - @Test - @Throws(Exception::class) - fun testDaysUntil() { - val t = getToday() - assertThat(t.daysUntil(t), equalTo(0)) - assertThat(t.daysUntil(t.plus(1)), equalTo(1)) - assertThat(t.daysUntil(t.plus(3)), equalTo(3)) - assertThat(t.daysUntil(t.plus(300)), equalTo(300)) - assertThat(t.daysUntil(t.minus(1)), equalTo(-1)) - assertThat(t.daysUntil(t.minus(3)), equalTo(-3)) - assertThat(t.daysUntil(t.minus(300)), equalTo(-300)) - } - - @Test - @Throws(Exception::class) - fun testInexact() { - val t = Timestamp(1578054764000L) - assertThat(t.unixTime, equalTo(1578009600000L)) - } -} diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryListTest.kt index 7907cbf8..092d3741 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryListTest.kt @@ -19,13 +19,12 @@ package org.isoron.uhabits.core.models.sqlite +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.BaseUnitTest.Companion.buildMemoryDatabase import org.isoron.uhabits.core.database.Repository import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.sqlite.records.EntryRecord -import org.isoron.uhabits.core.utils.DateUtils import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -36,7 +35,7 @@ class SQLiteEntryListTest { private val database = buildMemoryDatabase() private val repository = Repository(EntryRecord::class.java, database) private val entries = SQLiteEntryList(database) - private val today = DateUtils.getToday() + private val today = LocalDate(2015, 1, 25) @Before fun setUp() { @@ -50,7 +49,7 @@ class SQLiteEntryListTest { @Test fun testLoad() { - val today = DateUtils.getToday() + val today = LocalDate(2015, 1, 25) repository.save( EntryRecord().apply { habitId = entries.habitId @@ -66,15 +65,15 @@ class SQLiteEntryListTest { } ) assertEquals( - Entry(timestamp = today, value = 500), + Entry(date = today, value = 500), entries.get(today) ) assertEquals( - Entry(timestamp = today.minus(1), value = UNKNOWN), + Entry(date = today.minus(1), value = UNKNOWN), entries.get(today.minus(1)) ) assertEquals( - Entry(timestamp = today.minus(5), value = 300), + Entry(date = today.minus(5), value = 300), entries.get(today.minus(5)) ) } @@ -96,11 +95,11 @@ class SQLiteEntryListTest { assertEquals(replacement, retrieved2.toEntry()) } - private fun getByTimestamp(habitId: Int, timestamp: Timestamp): EntryRecord? { + private fun getByTimestamp(habitId: Int, date: LocalDate): EntryRecord? { return repository.findFirst( "where habit = ? and timestamp = ?", habitId.toString(), - timestamp.unixTime.toString() + date.unixTime.toString() ) } } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecordTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecordTest.kt index 444ed0b8..eb2c2b9e 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecordTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/models/sqlite/records/EntryRecordTest.kt @@ -20,16 +20,16 @@ package org.isoron.uhabits.core.models.sqlite.records import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.Entry -import org.isoron.uhabits.core.models.Timestamp import org.junit.Test class EntryRecordTest : BaseUnitTest() { @Test @Throws(Exception::class) fun testRecord() { - val check = Entry(Timestamp.ZERO.plus(100), 50) + val check = Entry(LocalDate(100), 50) val record = EntryRecord() record.copyFrom(check) assertThat(check, equalTo(record.toEntry())) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt index 6c3a4cfc..c5f13965 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/preferences/PreferencesTest.kt @@ -20,9 +20,9 @@ package org.isoron.uhabits.core.preferences import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.HabitList -import org.isoron.uhabits.core.models.Timestamp.Companion.ZERO import org.isoron.uhabits.core.ui.ThemeSwitcher import org.junit.Before import org.junit.Test @@ -93,10 +93,11 @@ class PreferencesTest : BaseUnitTest() { @Throws(Exception::class) fun testLastHint() { assertThat(prefs.lastHintNumber, equalTo(-1)) - assertNull(prefs.lastHintTimestamp) - prefs.updateLastHint(34, ZERO.plus(100)) + assertNull(prefs.lastHintDate) + val date = LocalDate(2015, 3, 15) + prefs.updateLastHint(34, date) assertThat(prefs.lastHintNumber, equalTo(34)) - assertThat(prefs.lastHintTimestamp, equalTo(ZERO.plus(100))) + assertThat(prefs.lastHintDate, equalTo(date)) } @Test diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.kt index 5165d5bd..4a0ba09b 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/reminders/ReminderSchedulerTest.kt @@ -23,11 +23,11 @@ import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Reminder import org.isoron.uhabits.core.models.WeekdayList import org.isoron.uhabits.core.preferences.WidgetPreferences -import org.isoron.uhabits.core.utils.DateUtils.Companion.applyTimezone -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar -import org.isoron.uhabits.core.utils.DateUtils.Companion.removeTimezone -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedTimeZone +import org.isoron.uhabits.core.utils.DateUtils +import org.isoron.uhabits.core.utils.DateUtils.removeTimezone +import org.isoron.uhabits.core.utils.DateUtils.setFixedLocalTime +import org.isoron.uhabits.core.utils.DateUtils.setFixedTimeZone +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -37,7 +37,6 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.Calendar import java.util.TimeZone @RunWith(MockitoJUnitRunner::class) @@ -60,6 +59,13 @@ class ReminderSchedulerTest : BaseUnitTest() { setFixedTimeZone(TimeZone.getTimeZone("GMT-4")) } + @After + override fun tearDown() { + super.tearDown() + setFixedLocalTime(null) + setFixedTimeZone(null) + } + @Test fun testScheduleAll() { val now = unixTime(2015, 1, 26, 13, 0) @@ -100,7 +106,7 @@ class ReminderSchedulerTest : BaseUnitTest() { setFixedLocalTime(now) val snoozeTimeInFuture = unixTime(2015, 1, 1, 21, 0) val snoozeTimeInPast = unixTime(2015, 1, 1, 7, 0) - val regularReminderTime = applyTimezone(unixTime(2015, 1, 2, 8, 30)) + val regularReminderTime = DateUtils.applyTimezone(unixTime(2015, 1, 2, 8, 30)) val todayCheckmarkTime = unixTime(2015, 1, 1, 0, 0) val tomorrowCheckmarkTime = unixTime(2015, 1, 2, 0, 0) habit.reminder = Reminder(8, 30, WeekdayList.EVERY_DAY) @@ -139,8 +145,9 @@ class ReminderSchedulerTest : BaseUnitTest() { } override fun unixTime(year: Int, month: Int, day: Int, hour: Int, minute: Int, milliseconds: Long): Long { - val cal: Calendar = getStartOfTodayCalendar() - cal[year, month, day, hour] = minute + val cal = java.util.GregorianCalendar(TimeZone.getTimeZone("GMT")) + cal.set(year, month, day, hour, minute, 0) + cal.set(java.util.GregorianCalendar.MILLISECOND, 0) return cal.timeInMillis } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt index 9811300f..e6a9f280 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCacheTest.kt @@ -20,11 +20,11 @@ package org.isoron.uhabits.core.ui.screens.habits.list import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.isoron.platform.time.LocalDate import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.commands.DeleteHabitsCommand import org.isoron.uhabits.core.models.Entry -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.reset @@ -34,7 +34,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions class HabitCardListCacheTest : BaseUnitTest() { private lateinit var cache: HabitCardListCache private lateinit var listener: HabitCardListCache.Listener - var today = getToday() + var today = LocalDate(2015, 1, 25) @Throws(Exception::class) override fun setUp() { diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HintListTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HintListTest.kt index 1142bb42..d606f4df 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HintListTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/HintListTest.kt @@ -20,10 +20,10 @@ package org.isoron.uhabits.core.ui.screens.habits.list import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.BaseUnitTest -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -37,8 +37,8 @@ class HintListTest : BaseUnitTest() { private lateinit var hints: Array private val prefs: Preferences = mock() - private lateinit var today: Timestamp - private lateinit var yesterday: Timestamp + private lateinit var today: LocalDate + private lateinit var yesterday: LocalDate @Throws(Exception::class) override fun setUp() { @@ -62,9 +62,9 @@ class HintListTest : BaseUnitTest() { @Test @Throws(Exception::class) fun shouldShow() { - whenever(prefs.lastHintTimestamp).thenReturn(today) + whenever(prefs.lastHintDate).thenReturn(today) assertFalse(hintList.shouldShow()) - whenever(prefs.lastHintTimestamp).thenReturn(yesterday) + whenever(prefs.lastHintDate).thenReturn(yesterday) assertTrue(hintList.shouldShow()) } } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt index 950b8f3a..7e904966 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehaviorTest.kt @@ -21,12 +21,11 @@ package org.isoron.uhabits.core.ui.screens.habits.list import org.apache.commons.io.FileUtils import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.preferences.Preferences -import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday -import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.junit.Before import org.junit.Test import org.mockito.kotlin.KArgumentCaptor @@ -78,14 +77,14 @@ class ListHabitsBehaviorTest : BaseUnitTest() { @Test fun testOnEdit() { - behavior.onEdit(habit2, getToday(), 0f, 0f) + val today = getToday() + behavior.onEdit(habit2, today, 0f, 0f) 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)) } @@ -166,7 +165,7 @@ class ListHabitsBehaviorTest : BaseUnitTest() { assertTrue(habit1.isCompletedToday()) behavior.onToggle( habit = habit1, - timestamp = getToday(), + date = getToday(), value = Entry.NO, notes = "", x = 0f, diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt index 37b68401..dd33ef19 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/widgets/WidgetBehaviorTest.kt @@ -18,15 +18,15 @@ */ package org.isoron.uhabits.core.ui.widgets +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.getToday import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.commands.CreateRepetitionCommand import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry.Companion.nextToggleValue import org.isoron.uhabits.core.models.Habit -import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.NotificationTray -import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock @@ -40,7 +40,7 @@ class WidgetBehaviorTest : BaseUnitTest() { private lateinit var preferences: Preferences private lateinit var behavior: WidgetBehavior private lateinit var habit: Habit - private lateinit var today: Timestamp + private lateinit var today: LocalDate @Before @Throws(Exception::class) @@ -51,7 +51,7 @@ class WidgetBehaviorTest : BaseUnitTest() { notificationTray = mock() preferences = mock() behavior = WidgetBehavior(habitList, commandRunner, notificationTray, preferences) - today = getTodayWithOffset() + today = getToday() } @Test diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/DateUtilsTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/DateUtilsTest.kt index e2dec999..0bf38e0b 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/DateUtilsTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/DateUtilsTest.kt @@ -18,677 +18,462 @@ */ package org.isoron.uhabits.core.utils -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.core.IsEqual.equalTo -import org.isoron.uhabits.core.BaseUnitTest -import org.isoron.uhabits.core.models.Timestamp -import org.isoron.uhabits.core.utils.DateUtils.Companion.applyTimezone -import org.isoron.uhabits.core.utils.DateUtils.Companion.formatHeaderDate -import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfDayWithOffset -import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset -import org.isoron.uhabits.core.utils.DateUtils.Companion.millisecondsUntilTomorrowWithOffset -import org.isoron.uhabits.core.utils.DateUtils.Companion.removeTimezone -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocale -import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedTimeZone -import org.isoron.uhabits.core.utils.DateUtils.Companion.setStartDayOffset -import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate +import org.junit.After import org.junit.Before import org.junit.Test import java.util.Calendar import java.util.GregorianCalendar -import java.util.Locale import java.util.TimeZone import kotlin.test.assertEquals -class DateUtilsTest : BaseUnitTest() { - var firstWeekday = Calendar.SUNDAY - - @Before - @Throws(Exception::class) - override fun setUp() { - super.setUp() - setFixedLocale(Locale.US) - } - - @Test - fun testFormatHeaderDate() { - val timestamp = unixTime(2015, Calendar.DECEMBER, 31) - val date = Timestamp(timestamp).toCalendar() - val formatted = formatHeaderDate(date) - assertThat(formatted, equalTo("Thu\n31")) - } - - @Test - fun testGetLocalTime() { - setFixedLocalTime(null) - setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")) - val utcTestTimeInMillis = unixTime(2015, Calendar.JANUARY, 11) - val localTimeInMillis = DateUtils.getLocalTime(utcTestTimeInMillis) - val expectedUnixTimeOffsetForSydney = 11 * 60 * 60 * 1000 - val expectedUnixTimeForSydney = utcTestTimeInMillis + expectedUnixTimeOffsetForSydney - assertThat(expectedUnixTimeForSydney, equalTo(localTimeInMillis)) - } - - @Test - fun testGetWeekdaySequence() { - val weekdaySequence = DateUtils.getWeekdaySequence(3) - assertThat(arrayOf(3, 4, 5, 6, 7, 1, 2), equalTo(weekdaySequence)) - } - - @Test - fun testGetFirstWeekdayNumberAccordingToLocale_germany() { - setFixedLocale(Locale.GERMANY) - val firstWeekdayNumber = DateUtils.getFirstWeekdayNumberAccordingToLocale() - assertThat(2, equalTo(firstWeekdayNumber)) - } - - @Test - fun testGetFirstWeekdayNumberAccordingToLocale_us() { - setFixedLocale(Locale.US) - val firstWeekdayNumber = DateUtils.getFirstWeekdayNumberAccordingToLocale() - assertThat(1, equalTo(firstWeekdayNumber)) - } - - @Test - fun testGetLongWeekdayNames_germany() { - setFixedLocale(Locale.GERMANY) - val longWeekdayNames = DateUtils.getLongWeekdayNames(Calendar.SATURDAY) - assertThat(arrayOf("Samstag", "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"), equalTo(longWeekdayNames)) - } - - @Test - fun testGetLongWeekdayNames_us() { - setFixedLocale(Locale.US) - val longWeekdayNames = DateUtils.getLongWeekdayNames(Calendar.SATURDAY) - assertThat(arrayOf("Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"), equalTo(longWeekdayNames)) - } - - @Test - fun testGetShortWeekdayNames_germany() { - setFixedLocale(Locale.GERMANY) - val longWeekdayNames = DateUtils.getShortWeekdayNames(Calendar.SATURDAY) - assertThat(arrayOf("Sa.", "So.", "Mo.", "Di.", "Mi.", "Do.", "Fr."), equalTo(longWeekdayNames)) - } - - @Test - fun testGetShortWeekdayNames_us() { - setFixedLocale(Locale.US) - val longWeekdayNames = DateUtils.getShortWeekdayNames(Calendar.SATURDAY) - assertThat(arrayOf("Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"), equalTo(longWeekdayNames)) - } - - @Test - fun getWeekdaysInMonth() { - fun getCalendarUTC(year: Int, month: Int, dayOfMonth: Int) = - GregorianCalendar(year, month, dayOfMonth).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - - val february = getCalendarUTC(2018, Calendar.FEBRUARY, 1) - val leapFebruary = getCalendarUTC(2020, Calendar.FEBRUARY, 1) - val month = getCalendarUTC(2020, Calendar.APRIL, 1) - val longMonth = getCalendarUTC(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) - val today = DateUtils.getToday() - assertThat(Timestamp(FIXED_LOCAL_TIME), equalTo(today)) - } - - @Test - fun testGetStartOfDay() { - val expectedStartOfDayUtc = fixedStartOfToday() - val laterInTheDayUtc = fixedStartOfTodayWithOffset(20) - val startOfDay = DateUtils.getStartOfDay(laterInTheDayUtc) - assertThat(expectedStartOfDayUtc, equalTo(startOfDay)) - } - - @Test - fun testGetStartOfToday() { - val expectedStartOfDayUtc = fixedStartOfToday() - val laterInTheDayUtc = fixedStartOfTodayWithOffset(20) - setFixedLocalTime(laterInTheDayUtc) - val startOfToday = DateUtils.getStartOfToday() - assertThat(expectedStartOfDayUtc, equalTo(startOfToday)) - } - - @Test - fun testGetStartOfTomorrowWithOffset_priorToOffset() { - val priorToOffset = HOUR_OFFSET - 1 - testGetStartOfTomorrowWithOffset(priorToOffset) - } - - @Test - fun testGetStartOfTomorrowWithOffset_afterOffset() { - val afterOffset = HOUR_OFFSET + 1 - HOURS_IN_ONE_DAY - testGetStartOfTomorrowWithOffset(afterOffset) - } - - private fun testGetStartOfTomorrowWithOffset(startOfTodayOffset: Int) { - configureOffsetTest(startOfTodayOffset) - assertThat( - fixedStartOfTodayWithOffset(HOUR_OFFSET), - equalTo(DateUtils.getStartOfTomorrowWithOffset()) - ) - } - - @Test - fun testGetStartOfTodayWithOffset_priorToOffset() { - val priorToOffset = HOURS_IN_ONE_DAY + HOUR_OFFSET - 1 - testGetStartOfTodayWithOffset(priorToOffset) - } - - @Test - fun testGetStartOfTodayWithOffset_afterOffset() { - val afterOffset = HOUR_OFFSET + 1 - testGetStartOfTodayWithOffset(afterOffset) - } - - private fun testGetStartOfTodayWithOffset(startOfTodayOffset: Int) { - configureOffsetTest(startOfTodayOffset) - assertThat( - fixedStartOfToday(), - equalTo(DateUtils.getStartOfTodayWithOffset()) - ) - } - - @Test - fun testTruncate_dayOfWeek() { - val field = DateUtils.TruncateField.WEEK_NUMBER - var expected = unixTime(2015, Calendar.JANUARY, 11) - var t0 = unixTime(2015, Calendar.JANUARY, 11) - var t1 = unixTime(2015, Calendar.JANUARY, 16) - var t2 = unixTime(2015, Calendar.JANUARY, 17) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - expected = unixTime(2015, Calendar.JANUARY, 18) - t0 = unixTime(2015, Calendar.JANUARY, 18) - t1 = unixTime(2015, Calendar.JANUARY, 19) - t2 = unixTime(2015, Calendar.JANUARY, 24) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - firstWeekday = Calendar.WEDNESDAY - expected = unixTime(2015, Calendar.JANUARY, 7) - t0 = unixTime(2015, Calendar.JANUARY, 7) - t1 = unixTime(2015, Calendar.JANUARY, 9) - t2 = unixTime(2015, Calendar.JANUARY, 13) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - } - - @Test - fun testTruncate_month() { - var expected = unixTime(2016, Calendar.JUNE, 1) - var t0 = unixTime(2016, Calendar.JUNE, 1) - var t1 = unixTime(2016, Calendar.JUNE, 15) - var t2 = unixTime(2016, Calendar.JUNE, 20) - val field = DateUtils.TruncateField.MONTH - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - expected = unixTime(2016, Calendar.DECEMBER, 1) - t0 = unixTime(2016, Calendar.DECEMBER, 1) - t1 = unixTime(2016, Calendar.DECEMBER, 15) - t2 = unixTime(2016, Calendar.DECEMBER, 31) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - } - - @Test - fun testTruncate_quarter() { - val field = DateUtils.TruncateField.QUARTER - var expected = unixTime(2016, Calendar.JANUARY, 1) - var t0 = unixTime(2016, Calendar.JANUARY, 20) - var t1 = unixTime(2016, Calendar.FEBRUARY, 15) - var t2 = unixTime(2016, Calendar.MARCH, 30) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - expected = unixTime(2016, Calendar.APRIL, 1) - t0 = unixTime(2016, Calendar.APRIL, 1) - t1 = unixTime(2016, Calendar.MAY, 30) - t2 = unixTime(2016, Calendar.JUNE, 20) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - } - - @Test - fun testTruncate_year() { - val field = DateUtils.TruncateField.YEAR - var expected = unixTime(2016, Calendar.JANUARY, 1) - var t0 = unixTime(2016, Calendar.JANUARY, 1) - var t1 = unixTime(2016, Calendar.FEBRUARY, 25) - var t2 = unixTime(2016, Calendar.DECEMBER, 31) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - expected = unixTime(2017, Calendar.JANUARY, 1) - t0 = unixTime(2017, Calendar.JANUARY, 1) - t1 = unixTime(2017, Calendar.MAY, 30) - t2 = unixTime(2017, Calendar.DECEMBER, 31) - assertThat(truncate(field, t0, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t1, firstWeekday), equalTo(expected)) - assertThat(truncate(field, t2, firstWeekday), equalTo(expected)) - } - - @Test - fun testTruncate_timestamp() { - val field = DateUtils.TruncateField.YEAR - val nonTruncatedDate = unixTime(2016, Calendar.MAY, 30) - val expected = Timestamp(unixTime(2016, Calendar.JANUARY, 1)) - assertThat(expected, equalTo(truncate(field, Timestamp(nonTruncatedDate), firstWeekday))) - } - - @Test - fun testGetUpcomingTimeInMillis() { - setFixedLocalTime(FIXED_LOCAL_TIME) - setFixedTimeZone(TimeZone.getTimeZone("GMT")) - val expected = unixTime(2015, Calendar.JANUARY, 25, 10, 1) - val upcomingTimeMillis = DateUtils.getUpcomingTimeInMillis(10, 1) - assertThat(expected, equalTo(upcomingTimeMillis)) - } - - @Test - @Throws(Exception::class) - fun testMillisecondsUntilTomorrow() { - setFixedTimeZone(TimeZone.getTimeZone("GMT")) - setFixedLocalTime(unixTime(2017, Calendar.JANUARY, 1, 23, 59)) - assertThat(millisecondsUntilTomorrowWithOffset(), equalTo(DateUtils.MINUTE_LENGTH)) - setFixedLocalTime(fixedStartOfTodayWithOffset(20)) - assertThat( - millisecondsUntilTomorrowWithOffset(), - equalTo(4 * DateUtils.HOUR_LENGTH) - ) - setStartDayOffset(HOUR_OFFSET, 30) - setFixedLocalTime(unixTime(2017, Calendar.JANUARY, 1, 23, 59)) - assertThat( - millisecondsUntilTomorrowWithOffset(), - equalTo(HOUR_OFFSET * DateUtils.HOUR_LENGTH + 31 * DateUtils.MINUTE_LENGTH) - ) - setFixedLocalTime(unixTime(2017, Calendar.JANUARY, 2, 1, 0)) - assertThat( - millisecondsUntilTomorrowWithOffset(), - equalTo(2 * DateUtils.HOUR_LENGTH + 30 * DateUtils.MINUTE_LENGTH) - ) - } - - @Test - fun testGetStartOfTodayCalendar() { - setFixedLocalTime(FIXED_LOCAL_TIME) - setFixedLocale(Locale.GERMANY) - val expectedStartOfDay = unixTime(2015, Calendar.JANUARY, 25, 0, 0) - val expectedCalendar = GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.GERMANY) - expectedCalendar.timeInMillis = expectedStartOfDay - val startOfTodayCalendar = DateUtils.getStartOfTodayCalendar() - assertThat(expectedCalendar, equalTo(startOfTodayCalendar)) - } - - @Test - fun testGetStartOfTodayCalendarWithOffset_priorToOffset() { - val priorToOffset = HOUR_OFFSET - 1 - testGetStartOfTodayCalendarWithOffset(priorToOffset) - } - - @Test - fun testGetStartOfTodayCalendarWithOffset_afterOffset() { - val afterOffset = HOUR_OFFSET + 1 - testGetStartOfTodayCalendarWithOffset(afterOffset) - } - - private fun testGetStartOfTodayCalendarWithOffset(startOfTodayOffset: Int) { - configureOffsetTest(startOfTodayOffset) - setFixedLocale(Locale.GERMANY) - val expectedCalendar = GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.GERMANY) - expectedCalendar.timeInMillis = fixedStartOfToday() - assertThat( - expectedCalendar, - equalTo(DateUtils.getStartOfTodayCalendar()) - ) - } - - private fun configureOffsetTest(startOfTodayOffset: Int) { - setStartDayOffset(HOUR_OFFSET, 0) - setFixedTimeZone(TimeZone.getTimeZone("GMT")) - setFixedLocalTime(fixedStartOfTodayWithOffset(startOfTodayOffset)) - } - - private fun fixedStartOfToday() = fixedStartOfTodayWithOffset(0) - - private fun fixedStartOfTodayWithOffset(hourOffset: Int): Long { - return unixTime(2017, Calendar.JANUARY, 1, hourOffset, 0) - } - - @Test - @Throws(Exception::class) - fun testGetTodayWithOffset() { - assertThat(getTodayWithOffset(), equalTo(Timestamp(FIXED_LOCAL_TIME))) - setStartDayOffset(9, 0) - assertThat( - getTodayWithOffset(), - equalTo(Timestamp(FIXED_LOCAL_TIME - DateUtils.DAY_LENGTH)) - ) - } - - @Test - @Throws(Exception::class) - fun testGetStartOfDayWithOffset() { - val timestamp = unixTime(2020, Calendar.SEPTEMBER, 3) - assertThat( - getStartOfDayWithOffset(timestamp + DateUtils.HOUR_LENGTH), - equalTo(timestamp) - ) - setStartDayOffset(3, 30) - assertThat( - getStartOfDayWithOffset(timestamp + 3 * DateUtils.HOUR_LENGTH + 29 * DateUtils.MINUTE_LENGTH), - equalTo(timestamp - DateUtils.DAY_LENGTH) - ) - } - - @Test - fun test_applyTimezone() { - setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")) - assertEquals( - applyTimezone(unixTime(2017, Calendar.JULY, 30, 18, 0)), - unixTime(2017, Calendar.JULY, 30, 8, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0)), - unixTime(2017, Calendar.SEPTEMBER, 29, 14, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 10, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 11, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 1, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 2, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 3, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 22, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 23, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 0, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 14, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 1, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 15, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 1, 59)), - unixTime(2017, Calendar.SEPTEMBER, 30, 15, 59) - ) - // DST begins - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 3, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 16, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 4, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 17, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 5, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 18, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 11, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 0, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 12, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 1, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 13, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 2, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 14, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 3, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 15, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 4, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 19, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 8, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.OCTOBER, 2, 19, 0)), - unixTime(2017, Calendar.OCTOBER, 2, 8, 0) - ) - assertEquals( - applyTimezone(unixTime(2017, Calendar.NOVEMBER, 30, 19, 0)), - unixTime(2017, Calendar.NOVEMBER, 30, 8, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.MARCH, 31, 0, 0)), - unixTime(2018, Calendar.MARCH, 30, 13, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.MARCH, 31, 12, 0)), - unixTime(2018, Calendar.MARCH, 31, 1, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.MARCH, 31, 18, 0)), - unixTime(2018, Calendar.MARCH, 31, 7, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 0, 0)), - unixTime(2018, Calendar.MARCH, 31, 13, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 1, 0)), - unixTime(2018, Calendar.MARCH, 31, 14, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 1, 59)), - unixTime(2018, Calendar.MARCH, 31, 14, 59) - ) - // DST ends - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 2, 0)), - unixTime(2018, Calendar.MARCH, 31, 16, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 3, 0)), - unixTime(2018, Calendar.MARCH, 31, 17, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 4, 0)), - unixTime(2018, Calendar.MARCH, 31, 18, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 10, 0)), - unixTime(2018, Calendar.APRIL, 1, 0, 0) - ) - assertEquals( - applyTimezone(unixTime(2018, Calendar.APRIL, 1, 18, 0)), - unixTime(2018, Calendar.APRIL, 1, 8, 0) - ) - } - - @Test - fun test_removeTimezone() { - setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")) - assertEquals( - removeTimezone(unixTime(2017, Calendar.JULY, 30, 8, 0)), - unixTime(2017, Calendar.JULY, 30, 18, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 29, 14, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 10, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 1, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 11, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 2, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 3, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 22, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0)), - unixTime(2017, Calendar.SEPTEMBER, 30, 23, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 14, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 0, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 15, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 1, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 15, 59)), - unixTime(2017, Calendar.OCTOBER, 1, 1, 59) - ) - // DST begins - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 16, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 3, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 17, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 4, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 18, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 5, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 0, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 11, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 1, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 12, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 2, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 13, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 3, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 14, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 4, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 15, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 8, 0)), - unixTime(2017, Calendar.OCTOBER, 1, 19, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.OCTOBER, 2, 8, 0)), - unixTime(2017, Calendar.OCTOBER, 2, 19, 0) - ) - assertEquals( - removeTimezone(unixTime(2017, Calendar.NOVEMBER, 30, 8, 0)), - unixTime(2017, Calendar.NOVEMBER, 30, 19, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 30, 13, 0)), - unixTime(2018, Calendar.MARCH, 31, 0, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 1, 0)), - unixTime(2018, Calendar.MARCH, 31, 12, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 7, 0)), - unixTime(2018, Calendar.MARCH, 31, 18, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 13, 0)), - unixTime(2018, Calendar.APRIL, 1, 0, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 14, 0)), - unixTime(2018, Calendar.APRIL, 1, 1, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 14, 59)), - unixTime(2018, Calendar.APRIL, 1, 1, 59) - ) - // DST ends - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 16, 0)), - unixTime(2018, Calendar.APRIL, 1, 2, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 17, 0)), - unixTime(2018, Calendar.APRIL, 1, 3, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.MARCH, 31, 18, 0)), - unixTime(2018, Calendar.APRIL, 1, 4, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.APRIL, 1, 0, 0)), - unixTime(2018, Calendar.APRIL, 1, 10, 0) - ) - assertEquals( - removeTimezone(unixTime(2018, Calendar.APRIL, 1, 8, 0)), - unixTime(2018, Calendar.APRIL, 1, 18, 0) - ) - } - +class DateUtilsTest { companion object { const val HOUR_OFFSET = 3 const val HOURS_IN_ONE_DAY = 24 } + + @Before + fun setUp() { + DateUtils.setFixedTimeZone(null) + DateUtils.setFixedLocalTime(null) + } + + @After + fun tearDown() { + DateUtils.setFixedLocalTime(null) + DateUtils.setFixedTimeZone(null) + } + + private fun unixTime(year: Int, month: Int, day: Int, hour: Int = 0, minute: Int = 0): Long { + val cal = GregorianCalendar(TimeZone.getTimeZone("GMT")) + cal.set(year, month, day, hour, minute, 0) + cal.set(GregorianCalendar.MILLISECOND, 0) + return cal.timeInMillis + } + + // --------------------------------------------------------------- + // getLocalTime + // --------------------------------------------------------------- + + @Test + fun testGetLocalTime() { + DateUtils.setFixedLocalTime(null) + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")) + val utcTime = unixTime(2015, Calendar.JANUARY, 11) + val localTime = DateUtils.getLocalTime(utcTimeInMillis = utcTime) + val expectedOffset = 11 * 60 * 60 * 1000L + assertEquals(utcTime + expectedOffset, localTime) + } + + // --------------------------------------------------------------- + // getStartOfDay / getStartOfDayWithOffset + // --------------------------------------------------------------- + + @Test + fun testGetStartOfDay() { + val startOfDay = unixTime(2017, Calendar.JANUARY, 1) + val laterInDay = unixTime(2017, Calendar.JANUARY, 1, 20, 0) + assertEquals(startOfDay, DateUtils.getStartOfDay(laterInDay)) + } + + @Test + fun testGetStartOfDayWithOffset_noOffset() { + val timestamp = unixTime(2020, Calendar.SEPTEMBER, 3) + assertEquals( + timestamp, + DateUtils.getStartOfDayWithOffset(timestamp + DateUtils.HOUR_LENGTH, 0, 0) + ) + } + + @Test + fun testGetStartOfDayWithOffset_withOffset() { + val timestamp = unixTime(2020, Calendar.SEPTEMBER, 3) + // 3:29 AM on Sep 3 with a 3:30 offset → still "Sep 2" conceptually + assertEquals( + timestamp - DateUtils.DAY_LENGTH, + DateUtils.getStartOfDayWithOffset( + timestamp + 3 * DateUtils.HOUR_LENGTH + 29 * DateUtils.MINUTE_LENGTH, + 3, + 30 + ) + ) + } + + // --------------------------------------------------------------- + // getStartOfTomorrowWithOffset + // --------------------------------------------------------------- + + @Test + fun testGetStartOfTomorrowWithOffset_priorToOffset() { + configureOffsetTest(HOUR_OFFSET - 1) + assertEquals( + fixedStartOfTodayWithOffset(HOUR_OFFSET), + DateUtils.getStartOfTomorrowWithOffset(HOUR_OFFSET, 0) + ) + } + + @Test + fun testGetStartOfTomorrowWithOffset_afterOffset() { + configureOffsetTest(HOUR_OFFSET + 1 - HOURS_IN_ONE_DAY) + assertEquals( + fixedStartOfTodayWithOffset(HOUR_OFFSET), + DateUtils.getStartOfTomorrowWithOffset(HOUR_OFFSET, 0) + ) + } + + // --------------------------------------------------------------- + // getUpcomingTimeInMillis + // --------------------------------------------------------------- + + @Test + fun testGetUpcomingTimeInMillis() { + val fixedTime = unixTime(2015, Calendar.JANUARY, 25, 8, 0) + DateUtils.setFixedLocalTime(fixedTime) + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("GMT")) + val expected = unixTime(2015, Calendar.JANUARY, 25, 10, 1) + assertEquals(expected, DateUtils.getUpcomingTimeInMillis(10, 1)) + } + + // --------------------------------------------------------------- + // millisecondsUntilTomorrowWithOffset + // --------------------------------------------------------------- + + @Test + fun testMillisecondsUntilTomorrow_justBeforeMidnight() { + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("GMT")) + DateUtils.setFixedLocalTime(unixTime(2017, Calendar.JANUARY, 1, 23, 59)) + assertEquals(DateUtils.MINUTE_LENGTH, DateUtils.millisecondsUntilTomorrowWithOffset(0, 0)) + } + + @Test + fun testMillisecondsUntilTomorrow_earlyEvening() { + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("GMT")) + DateUtils.setFixedLocalTime(unixTime(2017, Calendar.JANUARY, 1, 20, 0)) + assertEquals(4 * DateUtils.HOUR_LENGTH, DateUtils.millisecondsUntilTomorrowWithOffset(0, 0)) + } + + @Test + fun testMillisecondsUntilTomorrow_withHourAndMinuteOffset() { + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("GMT")) + DateUtils.setFixedLocalTime(unixTime(2017, Calendar.JANUARY, 1, 23, 59)) + assertEquals( + HOUR_OFFSET * DateUtils.HOUR_LENGTH + 31 * DateUtils.MINUTE_LENGTH, + DateUtils.millisecondsUntilTomorrowWithOffset(HOUR_OFFSET, 30) + ) + } + + @Test + fun testMillisecondsUntilTomorrow_afterMidnightBeforeOffset() { + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("GMT")) + DateUtils.setFixedLocalTime(unixTime(2017, Calendar.JANUARY, 2, 1, 0)) + assertEquals( + 2 * DateUtils.HOUR_LENGTH + 30 * DateUtils.MINUTE_LENGTH, + DateUtils.millisecondsUntilTomorrowWithOffset(HOUR_OFFSET, 30) + ) + } + + // --------------------------------------------------------------- + // applyTimezone (Australia/Sydney, covering DST transitions) + // --------------------------------------------------------------- + + @Test + fun testApplyTimezone_sydney() { + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")) + // Winter (AEST = UTC+10) + assertEquals( + unixTime(2017, Calendar.JULY, 30, 8, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.JULY, 30, 18, 0)) + ) + // Before DST starts (still UTC+10) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 29, 14, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 10, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 1, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 11, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 2, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 3, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 22, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 23, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 14, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 0, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 15, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 1, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 15, 59), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 1, 59)) + ) + // DST begins (clocks spring forward, AEDT = UTC+11) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 16, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 3, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 17, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 4, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 18, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 5, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 0, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 11, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 1, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 12, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 2, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 13, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 3, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 14, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 4, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 15, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 8, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 1, 19, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 2, 8, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.OCTOBER, 2, 19, 0)) + ) + assertEquals( + unixTime(2017, Calendar.NOVEMBER, 30, 8, 0), + DateUtils.applyTimezone(unixTime(2017, Calendar.NOVEMBER, 30, 19, 0)) + ) + // Before DST ends (still AEDT = UTC+11) + assertEquals( + unixTime(2018, Calendar.MARCH, 30, 13, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.MARCH, 31, 0, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 1, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.MARCH, 31, 12, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 7, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.MARCH, 31, 18, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 13, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 0, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 14, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 1, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 14, 59), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 1, 59)) + ) + // DST ends (clocks fall back, AEST = UTC+10) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 16, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 2, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 17, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 3, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 18, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 4, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 0, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 10, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 8, 0), + DateUtils.applyTimezone(unixTime(2018, Calendar.APRIL, 1, 18, 0)) + ) + } + + // --------------------------------------------------------------- + // removeTimezone (Australia/Sydney, covering DST transitions) + // --------------------------------------------------------------- + + @Test + fun testRemoveTimezone_sydney() { + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("Australia/Sydney")) + // Winter (AEST = UTC+10) + assertEquals( + unixTime(2017, Calendar.JULY, 30, 18, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.JULY, 30, 8, 0)) + ) + // Before DST starts + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 29, 14, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 10, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 0, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 11, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 1, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 2, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 3, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 22, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 12, 0)) + ) + assertEquals( + unixTime(2017, Calendar.SEPTEMBER, 30, 23, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 13, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 0, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 14, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 1, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 15, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 1, 59), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 15, 59)) + ) + // DST begins (AEDT = UTC+11) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 3, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 16, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 4, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 17, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 5, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.SEPTEMBER, 30, 18, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 11, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 0, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 12, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 1, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 13, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 2, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 14, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 3, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 15, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 4, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 1, 19, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.OCTOBER, 1, 8, 0)) + ) + assertEquals( + unixTime(2017, Calendar.OCTOBER, 2, 19, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.OCTOBER, 2, 8, 0)) + ) + assertEquals( + unixTime(2017, Calendar.NOVEMBER, 30, 19, 0), + DateUtils.removeTimezone(unixTime(2017, Calendar.NOVEMBER, 30, 8, 0)) + ) + // Before DST ends (still AEDT = UTC+11) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 0, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 30, 13, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 12, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 1, 0)) + ) + assertEquals( + unixTime(2018, Calendar.MARCH, 31, 18, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 7, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 0, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 13, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 1, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 14, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 1, 59), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 14, 59)) + ) + // DST ends (AEST = UTC+10) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 2, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 16, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 3, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 17, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 4, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.MARCH, 31, 18, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 10, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.APRIL, 1, 0, 0)) + ) + assertEquals( + unixTime(2018, Calendar.APRIL, 1, 18, 0), + DateUtils.removeTimezone(unixTime(2018, Calendar.APRIL, 1, 8, 0)) + ) + } + + // --------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------- + + private fun configureOffsetTest(startOfTodayOffset: Int) { + DateUtils.setFixedTimeZone(TimeZone.getTimeZone("GMT")) + DateUtils.setFixedLocalTime(fixedStartOfTodayWithOffset(startOfTodayOffset)) + } + + private fun fixedStartOfTodayWithOffset(hourOffset: Int): Long { + return unixTime(2017, Calendar.JANUARY, 1, hourOffset, 0) + } } diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt index c0eb3fe2..2a9df17d 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/utils/MidnightTimerTest.kt @@ -5,7 +5,10 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.isoron.uhabits.core.BaseUnitTest import org.isoron.uhabits.core.io.StandardLogging +import org.isoron.uhabits.core.preferences.Preferences +import org.junit.After import org.junit.Test +import org.mockito.Mockito.mock import java.util.Calendar import java.util.TimeZone import java.util.concurrent.Executors @@ -15,6 +18,13 @@ import kotlin.test.assertEquals class MidnightTimerTest : BaseUnitTest() { + @After + override fun tearDown() { + super.tearDown() + DateUtils.setFixedLocalTime(null) + DateUtils.setFixedTimeZone(null) + } + @Test fun testMidnightTimer_notifyListener_atMidnight() = runBlocking { // Given @@ -35,7 +45,7 @@ class MidnightTimerTest : BaseUnitTest() { ) val suspendedListener = suspendCoroutine { continuation -> - MidnightTimer(StandardLogging()).apply { + MidnightTimer(StandardLogging(), mock(Preferences::class.java)).apply { addListener { continuation.resume(true) } // When onResume(1, executor)