Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,126 @@ package at.techbee.jtx.util

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import at.techbee.jtx.util.DateTimeUtils.isLocalizedWeekstartMonday
import at.techbee.jtx.util.DateTimeUtils.getLocaleForLocalizedFirstDayOfWeek
import at.techbee.jtx.util.DateTimeUtils.getLocalizedDaysOfWeek
import at.techbee.jtx.util.DateTimeUtils.getLocalizedFirstDayOfWeek
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import java.time.DayOfWeek
import java.time.temporal.WeekFields
import java.util.Locale


@RunWith(AndroidJUnit4::class)
@SmallTest
class DateTimeUtilsAndroidTest {

private val defaultLocale: Locale = Locale.getDefault()

@After
fun tearDown() {
Locale.setDefault(defaultLocale)
}

@Test
fun isLocalizedWeekstartMonday_GERMAN() {
fun getLocalizedFirstDayOfWeek_GERMAN() {
Locale.setDefault(Locale.GERMAN)
assertEquals(true, isLocalizedWeekstartMonday())
assertEquals(DayOfWeek.MONDAY, getLocalizedFirstDayOfWeek())
}

@Test
fun isLocalizedWeekstartMonday_US() {
fun getLocalizedFirstDayOfWeek_US() {
Locale.setDefault(Locale.US)
assertEquals(false, isLocalizedWeekstartMonday())
assertEquals(DayOfWeek.SUNDAY, getLocalizedFirstDayOfWeek())
}

@Test
fun getLocalizedFirstDayOfWeek_US_withFirstDayOfWeekOverrideMonday() {
// Emulates the "Regional preferences -> First day of week = Monday" system setting,
// which is exposed as the "-u-fw-mon" unicode extension on the default locale.
Locale.setDefault(Locale.forLanguageTag("en-US-u-fw-mon"))
assertEquals(DayOfWeek.MONDAY, getLocalizedFirstDayOfWeek())
}

@Test
fun getLocalizedFirstDayOfWeek_GERMAN_withFirstDayOfWeekOverrideSunday() {
Locale.setDefault(Locale.forLanguageTag("de-DE-u-fw-sun"))
assertEquals(DayOfWeek.SUNDAY, getLocalizedFirstDayOfWeek())
}

@Test
fun getLocalizedDaysOfWeek_GERMAN_startsWithMonday() {
Locale.setDefault(Locale.GERMAN)
assertEquals(
listOf(
DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY,
DayOfWeek.FRIDAY, DayOfWeek.SATURDAY, DayOfWeek.SUNDAY
),
getLocalizedDaysOfWeek()
)
}

@Test
fun getLocalizedDaysOfWeek_US_startsWithSunday() {
Locale.setDefault(Locale.US)
assertEquals(
listOf(
DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY,
DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY
),
getLocalizedDaysOfWeek()
)
}

@Test
fun getLocalizedDaysOfWeek_US_withFirstDayOfWeekOverrideSaturday() {
Locale.setDefault(Locale.forLanguageTag("en-US-u-fw-sat"))
assertEquals(
listOf(
DayOfWeek.SATURDAY, DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY
),
getLocalizedDaysOfWeek()
)
}

// The DatePicker workaround has to produce a locale whose WeekFields.of(...) resolves to the
// device's first day of the week, because that is how Material3 derives it.

@Test
fun getLocaleForLocalizedFirstDayOfWeek_US_weekFieldsStartSunday() {
Locale.setDefault(Locale.US)
val locale = getLocaleForLocalizedFirstDayOfWeek()
assertEquals(DayOfWeek.SUNDAY, WeekFields.of(locale).firstDayOfWeek)
}

@Test
fun getLocaleForLocalizedFirstDayOfWeek_GERMAN_weekFieldsStartMonday() {
Locale.setDefault(Locale.GERMAN)
val locale = getLocaleForLocalizedFirstDayOfWeek()
assertEquals(DayOfWeek.MONDAY, WeekFields.of(locale).firstDayOfWeek)
}

@Test
fun getLocaleForLocalizedFirstDayOfWeek_US_withOverrideMonday_weekFieldsStartMonday() {
Locale.setDefault(Locale.forLanguageTag("en-US-u-fw-mon"))
val locale = getLocaleForLocalizedFirstDayOfWeek()
assertEquals(DayOfWeek.MONDAY, WeekFields.of(locale).firstDayOfWeek)
}

@Test
fun getLocaleForLocalizedFirstDayOfWeek_GERMAN_withOverrideSunday_weekFieldsStartSunday() {
Locale.setDefault(Locale.forLanguageTag("de-DE-u-fw-sun"))
val locale = getLocaleForLocalizedFirstDayOfWeek()
assertEquals(DayOfWeek.SUNDAY, WeekFields.of(locale).firstDayOfWeek)
}

@Test
fun getLocaleForLocalizedFirstDayOfWeek_keepsLanguage() {
Locale.setDefault(Locale.forLanguageTag("de-DE-u-fw-sun"))
assertEquals("de", getLocaleForLocalizedFirstDayOfWeek().language)
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import at.techbee.jtx.util.DateTimeUtils
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.TextStyle
import java.time.temporal.WeekFields
import java.util.Locale


Expand Down Expand Up @@ -194,7 +193,7 @@ data class ICal4ListRel(
val date = ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), DateTimeUtils.requireTzId(ical4ListRel.iCal4List.dtstartTimezone)).toLocalDate()
context.getString(
R.string.week_number_year,
date[WeekFields.of(Locale.getDefault()).weekOfWeekBasedYear()],
date[DateTimeUtils.getLocalizedWeekFields().weekOfWeekBasedYear()],
date.year
)
}
Expand Down Expand Up @@ -229,7 +228,7 @@ data class ICal4ListRel(
val date = ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), DateTimeUtils.requireTzId(ical4ListRel.iCal4List.dueTimezone)).toLocalDate()
context.getString(
R.string.week_number_year,
date[WeekFields.of(Locale.getDefault()).weekOfWeekBasedYear()],
date[DateTimeUtils.getLocalizedWeekFields().weekOfWeekBasedYear()],
date.year
)
}
Expand Down
19 changes: 15 additions & 4 deletions app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ import java.util.Locale
import kotlin.math.absoluteValue


/**
* @return the ical4j [WeekDay] that corresponds to this [DayOfWeek]
*/
private fun DayOfWeek.toICal4jWeekDay(): WeekDay = when (this) {
DayOfWeek.MONDAY -> WeekDay.MO
DayOfWeek.TUESDAY -> WeekDay.TU
DayOfWeek.WEDNESDAY -> WeekDay.WE
DayOfWeek.THURSDAY -> WeekDay.TH
DayOfWeek.FRIDAY -> WeekDay.FR
DayOfWeek.SATURDAY -> WeekDay.SA
DayOfWeek.SUNDAY -> WeekDay.SU
}


@SuppressLint("LocalContextGetResourceValueCall")
@OptIn(ExperimentalLayoutApi::class)
@Composable
Expand Down Expand Up @@ -128,10 +142,7 @@ fun DetailsCardRecur(
var showDetachSingleFromSeriesDialog by rememberSaveable { mutableStateOf(false) }
var showDetachAllFromSeriesDialog by rememberSaveable { mutableStateOf(false) }

val weekdays = if (DateTimeUtils.isLocalizedWeekstartMonday())
listOf(WeekDay.MO, WeekDay.TU, WeekDay.WE, WeekDay.TH, WeekDay.FR, WeekDay.SA, WeekDay.SU)
else
listOf(WeekDay.SU, WeekDay.MO, WeekDay.TU, WeekDay.WE, WeekDay.TH, WeekDay.FR, WeekDay.SA)
val weekdays = DateTimeUtils.getLocalizedDaysOfWeek().map { it.toICal4jWeekDay() }


fun buildRRule(): Recur<Temporal>? {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ fun ListScreenWeek(
val currentMonth = remember(currentDate) { currentDate.yearMonth }
val startMonth = remember(currentDate) { currentMonth.minusMonths(500) }
val endMonth = remember(currentDate) { currentMonth.plusMonths(500) }
val daysOfWeek = remember { daysOfWeek() }
val daysOfWeek = remember { daysOfWeek(firstDayOfWeek = DateTimeUtils.getLocalizedFirstDayOfWeek()) }

val scrollId by scrollOnceId.observeAsState(null)
val weekState = rememberWeekCalendarState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.material.icons.outlined.TravelExplore
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerState
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
Expand All @@ -38,7 +39,6 @@ import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand All @@ -50,6 +50,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
Expand Down Expand Up @@ -104,19 +105,26 @@ fun DatePickerDialog(
?.let { ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), DateTimeUtils.requireTzId(timezone)) }
?: minDate

val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = initialZonedDateTime?.toInstant()?.toEpochMilli()?.plus(initialZonedDateTime.offset.totalSeconds*1000),
selectableDates = object: SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
return if (allowedDates.isNotEmpty())
allowedDates.any {
utcTimeMillis == it.toLocalDate().atStartOfDay().atZone(ZoneId.of("UTC")).toInstant().toEpochMilli()
}
else
true
// Material3's DatePicker derives the first day of the week from WeekFields.of(locale), which
// ignores the system "first day of week" setting. Pass a locale that reflects that setting
// (see DateTimeUtils.getLocaleForLocalizedFirstDayOfWeek) so the calendar starts on the right day.
val configuration = LocalConfiguration.current
val datePickerState = remember(configuration) {
DatePickerState(
locale = DateTimeUtils.getLocaleForLocalizedFirstDayOfWeek(configuration.locales[0]),
initialSelectedDateMillis = initialZonedDateTime?.toInstant()?.toEpochMilli()?.plus(initialZonedDateTime.offset.totalSeconds*1000),
selectableDates = object: SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
return if (allowedDates.isNotEmpty())
allowedDates.any {
utcTimeMillis == it.toLocalDate().atStartOfDay().atZone(ZoneId.of("UTC")).toInstant().toEpochMilli()
}
else
true
}
}
}
)
)
}
val timePickerState = rememberTimePickerState(initialZonedDateTime?.hour?:0, initialZonedDateTime?.minute?:0)
val showTabs = !dateOnly || allowNull
val pagerState = rememberPagerState(initialPage = 0, pageCount = { if(showTabs) 3 else 1 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DateRangePicker
import androidx.compose.material3.DateRangePickerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDateRangePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import at.techbee.jtx.R
import at.techbee.jtx.util.DateTimeUtils
import kotlin.time.Duration.Companion.days


Expand All @@ -39,10 +42,18 @@ fun DateRangePickerDialog(
onDismiss: () -> Unit
) {

val dateRangePickerState = rememberDateRangePickerState(
initialSelectedStartDateMillis = dateRangeStart,
initialSelectedEndDateMillis = dateRangeEnd
)
// Material3's DateRangePicker derives the first day of the week from WeekFields.of(locale),
// which ignores the system "first day of week" setting. Pass a locale that reflects that
// setting (see DateTimeUtils.getLocaleForLocalizedFirstDayOfWeek) so the calendar starts on
// the right day.
val configuration = LocalConfiguration.current
val dateRangePickerState = remember(configuration) {
DateRangePickerState(
locale = DateTimeUtils.getLocaleForLocalizedFirstDayOfWeek(configuration.locales[0]),
initialSelectedStartDateMillis = dateRangeStart,
initialSelectedEndDateMillis = dateRangeEnd
)
}

AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false), // Workaround due to Google Issue: https://issuetracker.google.com/issues/194911971?pli=1
Expand Down
Loading