Replace Timestamp with LocalDate

This commit is contained in:
Alinson S. Xavier 2026-04-05 12:52:47 -05:00
parent 55ad585f1e
commit 0544166124
111 changed files with 1723 additions and 2004 deletions

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ node_modules
*.sketch
crowdin.yml
kotlin-js-store
*.md

View File

@ -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() {

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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))))
}
}

View File

@ -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

View File

@ -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))))
}
}

View File

@ -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()
)
)

View File

@ -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()

View File

@ -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()

View File

@ -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)
}

View File

@ -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)

View File

@ -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()

View File

@ -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
)

View File

@ -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
)

View File

@ -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)
}

View File

@ -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() {

View File

@ -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)

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -198,7 +198,7 @@ class CheckmarkButtonView(
canvas = canvas,
color = color,
size = em,
notes = notes,
notes = notes
)
}
}

View File

@ -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) }
}
}
}

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -241,7 +241,7 @@ class NumberButtonView(
canvas = canvas,
color = color,
size = em,
notes = notes,
notes = notes
)
}
}

View File

@ -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) }
}
}
}

View File

@ -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()

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
)
}

View File

@ -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()
}

View File

@ -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}")

View File

@ -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

View File

@ -215,7 +215,7 @@ fun View.drawNotesIndicator(
canvas: Canvas,
color: Int,
size: Float,
notes: String,
notes: String
) {
if (notes.isBlank()) return

View File

@ -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

View File

@ -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

View File

@ -29,7 +29,7 @@ class FrequencyWidgetProvider : BaseWidgetProvider() {
context,
id,
habits[0],
preferences.firstWeekdayInt
preferences.firstWeekday
)
} else {
StackWidget(context, id, StackWidgetType.FREQUENCY, habits)

View File

@ -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()),

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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()
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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 = ""
) {

View File

@ -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
}
}

View File

@ -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(

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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 ?: "")
}
}

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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]
}
}

View File

@ -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) {}
}

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -124,7 +124,7 @@ class ShowHabitPresenter(
),
frequency = FrequencyCardPresenter.buildState(
habit = habit,
firstWeekday = preferences.firstWeekdayInt,
firstWeekday = preferences.firstWeekday,
theme = theme
),
history = HistoryCardPresenter.buildState(

View File

@ -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()

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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)

View File

@ -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)
)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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