Replace Timestamp with LocalDate
This commit is contained in:
parent
55ad585f1e
commit
0544166124
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,3 +18,4 @@ node_modules
|
||||
*.sketch
|
||||
crowdin.yml
|
||||
kotlin-js-store
|
||||
*.md
|
||||
|
||||
61
build.sh
61
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() {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Timestamp>()
|
||||
view.onToggle = { t, _, _ -> timestamps.add(t) }
|
||||
val dates = mutableListOf<LocalDate>()
|
||||
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<Timestamp>()
|
||||
val dates = mutableListOf<LocalDate>()
|
||||
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))))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Timestamp>()
|
||||
view.onEdit = { t -> timestamps.plusAssign(t) }
|
||||
val dates = mutableListOf<LocalDate>()
|
||||
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<Timestamp>()
|
||||
val dates = mutableListOf<LocalDate>()
|
||||
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))))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
)
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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<Timestamp, Array<Int>>
|
||||
private lateinit var frequency: HashMap<LocalDate, Array<Int>>
|
||||
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<Timestamp, Array<Int>>) {
|
||||
fun setFrequency(frequency: java.util.HashMap<LocalDate, Array<Int>>) {
|
||||
this.frequency = frequency
|
||||
maxFreq = getMaxFreq(frequency)
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
fun setFirstWeekday(firstWeekday: Int) {
|
||||
fun setFirstWeekday(firstWeekday: DayOfWeek) {
|
||||
this.firstWeekday = firstWeekday
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
private fun getMaxFreq(frequency: HashMap<Timestamp, Array<Int>>): Int {
|
||||
private fun getMaxFreq(frequency: HashMap<LocalDate, Array<Int>>): 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<Int> = 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)
|
||||
}
|
||||
|
||||
@ -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<Score>()
|
||||
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() {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<Streak>? = 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<Streak> = 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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -198,7 +198,7 @@ class CheckmarkButtonView(
|
||||
canvas = canvas,
|
||||
color = color,
|
||||
size = em,
|
||||
notes = notes,
|
||||
notes = notes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,7 +241,7 @@ class NumberButtonView(
|
||||
canvas = canvas,
|
||||
color = color,
|
||||
size = em,
|
||||
notes = notes,
|
||||
notes = notes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -215,7 +215,7 @@ fun View.drawNotesIndicator(
|
||||
canvas: Canvas,
|
||||
color: Int,
|
||||
size: Float,
|
||||
notes: String,
|
||||
notes: String
|
||||
) {
|
||||
if (notes.isBlank()) return
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -29,7 +29,7 @@ class FrequencyWidgetProvider : BaseWidgetProvider() {
|
||||
context,
|
||||
id,
|
||||
habits[0],
|
||||
preferences.firstWeekdayInt
|
||||
preferences.firstWeekday
|
||||
)
|
||||
} else {
|
||||
StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<Habit>,
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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<LocalDate> {
|
||||
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<DayOfWeek> {
|
||||
val allDays = DayOfWeek.entries
|
||||
return (0 until 7).map { offset ->
|
||||
allDays[(firstWeekday.daysSinceSunday + offset) % 7]
|
||||
}
|
||||
}
|
||||
|
||||
fun countWeekdayOccurrencesInMonth(startOfMonth: LocalDate): Array<Int> {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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<String> {
|
||||
return getWeekdaySequence(firstWeekday).map { shortWeekdayName(it) }.toTypedArray()
|
||||
}
|
||||
|
||||
fun longWeekdayNames(firstWeekday: DayOfWeek): Array<String> {
|
||||
return getWeekdaySequence(firstWeekday).map { longWeekdayName(it) }.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<Entry>> = ArrayList()
|
||||
val scores: MutableList<ArrayList<Score>> = 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<Timestamp> {
|
||||
var oldest = Timestamp.ZERO.plus(1000000)
|
||||
var newest = Timestamp.ZERO
|
||||
private fun getTimeframe(): Array<LocalDate> {
|
||||
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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 = ""
|
||||
) {
|
||||
|
||||
@ -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<Timestamp, Entry> = HashMap()
|
||||
private val entriesByDate: HashMap<LocalDate, Entry> = 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<Entry> {
|
||||
open fun getByInterval(from: LocalDate, to: LocalDate): List<Entry> {
|
||||
val result = mutableListOf<Entry>()
|
||||
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<Entry> {
|
||||
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<Timestamp, Array<Int>> {
|
||||
fun computeWeekdayFrequency(isNumerical: Boolean): HashMap<LocalDate, Array<Int>> {
|
||||
val entries = getKnown()
|
||||
val map = hashMapOf<Timestamp, Array<Int>>()
|
||||
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<LocalDate, Array<Int>>()
|
||||
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<Entry>()
|
||||
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<Interval>) {
|
||||
@ -253,15 +251,14 @@ open class EntryList {
|
||||
val den = freq.denominator
|
||||
val intervals = arrayListOf<Interval>()
|
||||
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<Entry>.groupedSum(
|
||||
truncateField: DateUtils.TruncateField,
|
||||
truncateField: TruncateField,
|
||||
firstWeekday: Int = Calendar.SATURDAY,
|
||||
isNumerical: Boolean
|
||||
): List<Entry> {
|
||||
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<Entry>.countSkippedDays(
|
||||
truncateField: DateUtils.TruncateField,
|
||||
truncateField: TruncateField,
|
||||
firstWeekday: Int = Calendar.SATURDAY
|
||||
): List<Entry> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Timestamp, Score>()
|
||||
private val map = HashMap<LocalDate, Score>()
|
||||
|
||||
/**
|
||||
* 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<Score> {
|
||||
val result: MutableList<Score> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.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<Timestamp> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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<Habit> { 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)
|
||||
|
||||
@ -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<Entry> {
|
||||
override fun getByInterval(from: LocalDate, to: LocalDate): List<Entry> {
|
||||
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
|
||||
|
||||
@ -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 ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<String>) {
|
||||
open class HintList(
|
||||
private val prefs: Preferences,
|
||||
private val hints: Array<String>
|
||||
) {
|
||||
/**
|
||||
* 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<Str
|
||||
*/
|
||||
open fun shouldShow(): Boolean {
|
||||
val today = getToday()
|
||||
val lastHintTimestamp = prefs.lastHintTimestamp
|
||||
return lastHintTimestamp?.isOlderThan(today) == true
|
||||
val lastHintDate = prefs.lastHintDate
|
||||
return lastHintDate != null && lastHintDate < today
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.isoron.uhabits.core.ui.screens.habits.list
|
||||
|
||||
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.Companion.YES_MANUAL
|
||||
@ -27,11 +29,9 @@ import org.isoron.uhabits.core.models.HabitType
|
||||
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.tasks.ExportCSVTask
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.LinkedList
|
||||
@ -51,8 +51,8 @@ open class ListHabitsBehavior @Inject constructor(
|
||||
screen.showHabitScreen(h)
|
||||
}
|
||||
|
||||
fun onEdit(habit: Habit, timestamp: Timestamp?, x: Float, y: Float) {
|
||||
val entry = habit.computedEntries.get(timestamp!!)
|
||||
fun onEdit(habit: Habit, date: LocalDate, x: Float, y: Float) {
|
||||
val entry = habit.computedEntries.get(date)
|
||||
if (habit.type == HabitType.NUMERICAL) {
|
||||
val oldValue = entry.value.toDouble() / 1000
|
||||
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ class ShowHabitPresenter(
|
||||
),
|
||||
frequency = FrequencyCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
firstWeekday = preferences.firstWeekday,
|
||||
theme = theme
|
||||
),
|
||||
history = HistoryCardPresenter.buildState(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<Timestamp, Array<Int>>,
|
||||
val firstWeekday: DayOfWeek,
|
||||
val frequency: HashMap<LocalDate, Array<Int>>,
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Score>,
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> {
|
||||
val calendar = GregorianCalendar(getLocale())
|
||||
calendar.set(DAY_OF_WEEK, firstWeekDay)
|
||||
|
||||
val daysNullable = ArrayList<String>()
|
||||
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<Int> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<Int> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<MidnightListener> = 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()
|
||||
}
|
||||
|
||||
@ -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<IllegalStateException> { 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.platform.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user