Move DateUtilities to kmp

pull/2998/head
Alex Baker 4 months ago
parent fcc4b2ef2a
commit 865cec7220

@ -5,452 +5,372 @@
*/ */
package com.todoroo.andlib.utility package com.todoroo.andlib.utility
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.tasks.Freeze.Companion.freezeAt import org.tasks.SuspendFreeze
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.withLocale
import org.tasks.date.DateTimeUtils import org.tasks.date.DateTimeUtils
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.Context.is24HourOverride
import org.tasks.kmp.formatDayOfWeek
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.TextStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DateUtilitiesTest { class DateUtilitiesTest {
@After @After
fun after() { fun after() {
DateUtilities.is24HourOverride = null is24HourOverride = null
} }
@Test @Test
fun testGet24HourTime() { fun testGet24HourTime() {
DateUtilities.is24HourOverride = true is24HourOverride = true
assertEquals("09:05", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 9, 5, 36))) assertEquals("09:05", getTimeString(DateTime(2014, 1, 4, 9, 5, 36).millis, is24HourFormat))
assertEquals("13:00", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 13, 0, 1))) assertEquals("13:00", getTimeString(DateTime(2014, 1, 4, 13, 0, 1).millis, is24HourFormat))
} }
@Test @Test
fun testGetTime() { fun testGetTime() {
DateUtilities.is24HourOverride = false is24HourOverride = false
assertEquals("9:05 AM", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 9, 5, 36))) assertEquals("9:05 AM", getTimeString(DateTime(2014, 1, 4, 9, 5, 36).millis, is24HourFormat))
assertEquals("1:05 PM", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 13, 5, 36))) assertEquals("1:05 PM", getTimeString(DateTime(2014, 1, 4, 13, 5, 36).millis, is24HourFormat))
} }
@Test @Test
fun testGetTimeWithNoMinutes() { fun testGetTimeWithNoMinutes() {
DateUtilities.is24HourOverride = false is24HourOverride = false
assertEquals("1 PM", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 13, 0, 59))) // derp? assertEquals("1 PM", getTimeString(DateTime(2014, 1, 4, 13, 0, 59).millis, is24HourFormat)) // derp?
} }
@Test @Test
fun testGetDateStringWithYear() { fun testGetDateStringWithYear() = runBlocking {
assertEquals("Jan 4, 2014", DateUtilities.getDateString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 0, 0, 0))) assertEquals("Jan 4, 2014", getRelativeDay(DateTime(2014, 1, 4, 0, 0, 0).millis))
} }
@Test @Test
fun testGetDateStringHidingYear() { fun testGetDateStringHidingYear() = runBlocking {
freezeAt(DateTimeUtils.newDate(2014, 2, 1)) { freezeAt(DateTimeUtils.newDate(2014, 2, 1)) {
assertEquals("Jan 1", DateUtilities.getDateString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 1))) assertEquals("Jan 1", getRelativeDay(DateTime(2014, 1, 1).millis))
} }
} }
@Test @Test
fun testGetDateStringWithDifferentYear() { fun testGetDateStringWithDifferentYear() = runBlocking {
freezeAt(DateTimeUtils.newDate(2013, 12, 1)) { freezeAt(DateTimeUtils.newDate(2013, 12, 1)) {
assertEquals("Jan 1, 2014", DateUtilities.getDateString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 1, 0, 0, 0))) assertEquals("Jan 1, 2014", getRelativeDay(DateTime(2014, 1, 1, 0, 0, 0).millis))
} }
} }
@Test @Test
fun testGetWeekdayLongString() { fun testGetWeekdayLongString() = withLocale(Locale.US) {
assertEquals("Sunday", DateUtilities.getWeekday(DateTimeUtils.newDate(2013, 12, 29), Locale.US)) assertEquals("Sunday", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 29).millis, TextStyle.FULL))
assertEquals("Monday", DateUtilities.getWeekday(DateTimeUtils.newDate(2013, 12, 30), Locale.US)) assertEquals("Monday", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 30).millis, TextStyle.FULL))
assertEquals("Tuesday", DateUtilities.getWeekday(DateTimeUtils.newDate(2013, 12, 31), Locale.US)) assertEquals("Tuesday", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 31).millis, TextStyle.FULL))
assertEquals("Wednesday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 1), Locale.US)) assertEquals("Wednesday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 1).millis, TextStyle.FULL))
assertEquals("Thursday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 2), Locale.US)) assertEquals("Thursday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 2).millis, TextStyle.FULL))
assertEquals("Friday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 3), Locale.US)) assertEquals("Friday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 3).millis, TextStyle.FULL))
assertEquals("Saturday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 4), Locale.US)) assertEquals("Saturday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 4).millis, TextStyle.FULL))
} }
@Test @Test
fun testGetWeekdayShortString() { fun testGetWeekdayShortString() = withLocale(Locale.US) {
assertEquals("Sun", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2013, 12, 29), Locale.US)) assertEquals("Sun", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 29).millis, TextStyle.SHORT))
assertEquals("Mon", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2013, 12, 30), Locale.US)) assertEquals("Mon", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 30).millis, TextStyle.SHORT))
assertEquals("Tue", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2013, 12, 31), Locale.US)) assertEquals("Tue", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 31).millis, TextStyle.SHORT))
assertEquals("Wed", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 1), Locale.US)) assertEquals("Wed", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 1).millis, TextStyle.SHORT))
assertEquals("Thu", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 2), Locale.US)) assertEquals("Thu", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 2).millis, TextStyle.SHORT))
assertEquals("Fri", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 3), Locale.US)) assertEquals("Fri", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 3).millis, TextStyle.SHORT))
assertEquals("Sat", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 4), Locale.US)) assertEquals("Sat", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 4).millis, TextStyle.SHORT))
} }
@Test @Test
fun getRelativeFullDate() { fun getRelativeFullDate() = withLocale(Locale.US) {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"Sunday, January 14", "Sunday, January 14",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
fun getRelativeFullDateWithYear() { fun getRelativeFullDateWithYear() = withLocale(Locale.US) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"Sunday, January 14, 2018", "Sunday, January 14, 2018",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
fun getRelativeFullDateTime() { fun getRelativeFullDateTime() = withLocale(Locale.US) {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertMatches( assertMatches(
"Sunday, January 14( at)? 1:43 PM", "Sunday, January 14( at)? 1:43 PM",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 43, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 43, 1).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
@Ignore("Fails on CI - need to investigate") @Ignore("Fails on CI - need to investigate")
fun getRelativeDateTimeWithAlwaysDisplayFullDateOption() { fun getRelativeDateTimeWithAlwaysDisplayFullDateOption() = withLocale(Locale.US) {
freezeAt(DateTime(2020, 1, 1)) { freezeAt(DateTime(2020, 1, 1)) {
assertMatches( assertMatches(
"Thursday, January 2 at 11:50 AM", "Thursday, January 2 at 11:50 AM",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2020, 1, 2, 11, 50, 1).millis, is24HourFormat, DateStyle.FULL, true, false)
ApplicationProvider.getApplicationContext(), )
DateTime(2020, 1, 2, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL,
true,
false
))
} }
} }
@Test @Test
fun getRelativeFullDateTimeWithYear() { fun getRelativeFullDateTimeWithYear() = withLocale(Locale.US) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"Sunday, January 14, 2018( at)? 11:50 AM", "Sunday, January 14, 2018( at)? 11:50 AM",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 11, 50, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
fun getRelativeDayWithAlwaysDisplayFullDateOption() { fun getRelativeDayWithAlwaysDisplayFullDateOption() = withLocale(Locale.US) {
freezeAt(DateTime(2020, 1, 1)) { freezeAt(DateTime(2020, 1, 1)) {
assertEquals( assertEquals(
"Thursday, January 2", "Thursday, January 2",
DateUtilities.getRelativeDay( getRelativeDay(DateTime(2020, 1, 2, 11, 50, 1).millis, DateStyle.FULL, alwaysDisplayFullDate = true, lowercase = true)
ApplicationProvider.getApplicationContext(),
DateTime(2020, 1, 2, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL,
true,
true
)
) )
} }
} }
@Test @Test
fun getRelativeDayWithoutAlwaysDisplayFullDateOption() { fun getRelativeDayWithoutAlwaysDisplayFullDateOption() = withLocale(Locale.US) {
freezeAt(DateTime(2020, 1, 1)) { freezeAt(DateTime(2020, 1, 1)) {
assertEquals( assertEquals(
"tomorrow", "tomorrow",
DateUtilities.getRelativeDay( getRelativeDay(DateTime(2020, 1, 2, 11, 50, 1).millis, DateStyle.FULL, lowercase = true)
ApplicationProvider.getApplicationContext(),
DateTime(2020, 1, 2, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL,
false,
true
)
) )
} }
} }
@Test @Test
fun germanDateNoYear() { fun germanDateNoYear() = withLocale(Locale.GERMAN) {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"Sonntag, 14. Januar", "Sonntag, 14. Januar",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.GERMAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun germanDateWithYear() { fun germanDateWithYear() = withLocale(Locale.GERMAN) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"Sonntag, 14. Januar 2018", "Sonntag, 14. Januar 2018",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.GERMAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun koreanDateNoYear() { fun koreanDateNoYear() = withLocale(Locale.KOREAN) {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1월 14일 일요일", "1월 14일 일요일",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.KOREAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun koreanDateWithYear() { fun koreanDateWithYear() = withLocale(Locale.KOREAN) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018년 1월 14일 일요일", "2018년 1월 14일 일요일",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.KOREAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun japaneseDateNoYear() { fun japaneseDateNoYear() = withLocale(Locale.JAPANESE) {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1月14日日曜日", "1月14日日曜日",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.JAPANESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun japaneseDateWithYear() { fun japaneseDateWithYear() = withLocale(Locale.JAPANESE) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018年1月14日日曜日", "2018年1月14日日曜日",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.JAPANESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateNoYear() { fun chineseDateNoYear() = withLocale(Locale.CHINESE) {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1月14日星期日", "1月14日星期日",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateWithYear() { fun chineseDateWithYear() = withLocale(Locale.CHINESE) {
freezeAt(DateTime(2017, 12, 12)) { SuspendFreeze.freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018年1月14日星期日", "2018年1月14日星期日",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateTimeNoYear() { fun chineseDateTimeNoYear() = withLocale(Locale.CHINESE) {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1月14日星期日 上午11:53", "1月14日星期日 上午11:53",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 11, 53, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 11, 53, 1).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateTimeWithYear() { fun chineseDateTimeWithYear() = withLocale(Locale.CHINESE) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018年1月14日星期日 下午1:45", "2018年1月14日星期日 下午1:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun frenchDateTimeWithYear() { fun frenchDateTimeWithYear() = withLocale(Locale.FRENCH) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"dimanche 14 janvier 2018( à)? 13:45", "dimanche 14 janvier 2018( à)? 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.FRENCH,
FormatStyle.FULL))
} }
} }
@Test @Test
fun indiaDateTimeWithYear() { fun indiaDateTimeWithYear() = withLocale(Locale.forLanguageTag("hi-IN")) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"रविवार, 14 जनवरी 2018( को)? 1:45 pm", "रविवार, 14 जनवरी 2018( को)? 1:45 pm",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("hi-IN"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun russiaDateTimeNoYear() { fun russiaDateTimeNoYear() = withLocale(Locale.forLanguageTag("ru")) {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertMatches( assertMatches(
"воскресенье, 14 января,? 13:45", "воскресенье, 14 января,? 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("ru"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun russiaDateTimeWithYear() { fun russiaDateTimeWithYear() = withLocale(Locale.forLanguageTag("ru")) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"воскресенье, 14 января 2018 г.,? 13:45", "воскресенье, 14 января 2018 г.,? 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("ru"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun brazilDateTimeNoYear() { fun brazilDateTimeNoYear() = withLocale(Locale.forLanguageTag("pt-br")) {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertEquals( assertEquals(
"domingo, 14 de janeiro 13:45", "domingo, 14 de janeiro 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("pt-br"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun brazilDateTimeWithYear() { fun brazilDateTimeWithYear() = withLocale(Locale.forLanguageTag("pt-br")) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"domingo, 14 de janeiro de 2018 13:45", "domingo, 14 de janeiro de 2018 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("pt-br"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun spainDateTimeNoYear() { fun spainDateTimeNoYear() = withLocale(Locale.forLanguageTag("es")) {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertMatches( assertMatches(
"domingo, 14 de enero,? 13:45", "domingo, 14 de enero,? 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("es"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun spainDateTimeWithYear() { fun spainDateTimeWithYear() = withLocale(Locale.forLanguageTag("es")) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"domingo, 14 de enero de 2018,? 13:45", "domingo, 14 de enero de 2018,? 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("es"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun hebrewDateTimeNoYear() { fun hebrewDateTimeNoYear() = withLocale(Locale.forLanguageTag("iw")) {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertMatches( assertMatches(
"יום ראשון, 14 בינואר( בשעה)? 13:45", "יום ראשון, 14 בינואר( בשעה)? 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("iw"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun hebrewDateTimeWithYear() { fun hebrewDateTimeWithYear() = withLocale(Locale.forLanguageTag("iw")) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"יום ראשון, 14 בינואר 2018( בשעה)? 13:45", "יום ראשון, 14 בינואר 2018( בשעה)? 13:45",
DateUtilities.getRelativeDateTime( getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL)
ApplicationProvider.getApplicationContext(), )
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("iw"),
FormatStyle.FULL))
} }
} }
private fun assertMatches(regex: String, actual: String) = private fun assertMatches(regex: String, actual: String) =
assertTrue("expected=$regex\nactual=$actual", actual.matches(Regex(regex))) assertTrue("expected=$regex\nactual=$actual", actual.matches(Regex(regex)))
private val is24HourFormat: Boolean
get() = InstrumentationRegistry.getInstrumentation().targetContext.is24HourFormat
} }

@ -1,16 +1,17 @@
package com.todoroo.andlib.utility package com.todoroo.andlib.utility
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.tasks.Freeze import org.tasks.Freeze
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.time.format.FormatStyle import java.util.Locale
import java.util.*
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RelativeDayTest { class RelativeDayTest {
@ -67,12 +68,12 @@ class RelativeDayTest {
checkRelativeDay(DateTime().plusDays(7), "January 7, 2014", "Jan 7, 2014") checkRelativeDay(DateTime().plusDays(7), "January 7, 2014", "Jan 7, 2014")
} }
private fun checkRelativeDay(now: DateTime, full: String, abbreviated: String) { private fun checkRelativeDay(now: DateTime, full: String, abbreviated: String) = runBlocking {
assertEquals( assertEquals(
full, full,
DateUtilities.getRelativeDay(ApplicationProvider.getApplicationContext(), now.millis, Locale.US, FormatStyle.LONG)) getRelativeDay(now.millis, DateStyle.LONG))
assertEquals( assertEquals(
abbreviated, abbreviated,
DateUtilities.getRelativeDay(ApplicationProvider.getApplicationContext(), now.millis, Locale.US, FormatStyle.MEDIUM)) getRelativeDay(now.millis))
} }
} }

@ -1,205 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.andlib.utility
import android.content.Context
import android.text.format.DateFormat
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.data.entity.Task.Companion.hasDueTime
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.time.DateTime
import org.tasks.time.ONE_DAY
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.format.TextStyle
import java.util.Locale
import kotlin.math.abs
object DateUtilities {
var is24HourOverride: Boolean? = null
/* ======================================================================
* =========================================================== formatters
* ====================================================================== */
private fun is24HourFormat(context: Context): Boolean {
return if (BuildConfig.DEBUG && is24HourOverride != null
) is24HourOverride!!
else DateFormat.is24HourFormat(context)
}
@JvmStatic
fun getTimeString(context: Context, date: DateTime): String {
val value = if (is24HourFormat(context)) {
"HH:mm"
} else if (date.minuteOfHour == 0) {
"h a"
} else {
"h:mm a"
}
return date.toString(value)
}
fun getLongDateString(date: DateTime, locale: Locale): String {
return getFullDate(date, locale, FormatStyle.LONG)
}
/**
* @param date date to format
* @return date, with month, day, and year
*/
fun getDateString(context: Context, date: DateTime): String {
return getRelativeDay(
context, date.millis, Locale.getDefault(), FormatStyle.MEDIUM
)
}
fun getWeekday(date: DateTime, locale: Locale?): String {
return date.toLocalDate()!!
.dayOfWeek.getDisplayName(TextStyle.FULL, locale)
}
/** @return weekday
*/
fun getWeekdayShort(date: DateTime, locale: Locale?): String {
return date.toLocalDate()!!
.dayOfWeek.getDisplayName(TextStyle.SHORT, locale)
}
fun getLongDateStringWithTime(timestamp: Long, locale: Locale): String {
return getFullDateTime(newDateTime(timestamp), locale, FormatStyle.LONG)
}
fun getRelativeDateTime(
context: Context, date: Long, locale: Locale, style: FormatStyle
): String {
return getRelativeDateTime(context, date, locale, style, false, false)
}
fun getRelativeDateTime(
context: Context, date: Long, locale: Locale, style: FormatStyle, lowercase: Boolean
): String {
return getRelativeDateTime(context, date, locale, style, false, lowercase)
}
fun getRelativeDateTime(
context: Context,
date: Long,
locale: Locale,
style: FormatStyle,
alwaysDisplayFullDate: Boolean,
lowercase: Boolean
): String {
if (alwaysDisplayFullDate || !isWithinSixDays(date)) {
return if (hasDueTime(date)
) getFullDateTime(newDateTime(date), locale, style)
else getFullDate(newDateTime(date), locale, style)
}
val day = getRelativeDay(context, date, locale, isAbbreviated(style), lowercase)
if (hasDueTime(date)) {
val time = getTimeString(context, newDateTime(date))
return if (newDateTime().startOfDay()
.equals(newDateTime(date).startOfDay())
) time else String.format("%s %s", day, time)
} else {
return day
}
}
private fun isAbbreviated(style: FormatStyle): Boolean {
return style == FormatStyle.SHORT || style == FormatStyle.MEDIUM
}
fun getRelativeDay(
context: Context,
date: Long,
locale: Locale,
style: FormatStyle
): String {
return getRelativeDay(context, date, locale, style, false, false)
}
fun getRelativeDay(
context: Context,
date: Long,
locale: Locale,
style: FormatStyle,
alwaysDisplayFullDate: Boolean,
lowercase: Boolean
): String {
if (alwaysDisplayFullDate) {
return getFullDate(newDateTime(date), locale, style)
}
return if (isWithinSixDays(date)
) getRelativeDay(context, date, locale, isAbbreviated(style), lowercase)
else getFullDate(newDateTime(date), locale, style)
}
private fun getFullDate(date: DateTime, locale: Locale, style: FormatStyle): String {
return stripYear(
DateTimeFormatter.ofLocalizedDate(style)
.withLocale(locale)
.format(date.toLocalDate()),
newDateTime().year
)
}
private fun getFullDateTime(date: DateTime, locale: Locale, style: FormatStyle): String {
return stripYear(
DateTimeFormatter.ofLocalizedDateTime(style, FormatStyle.SHORT)
.withLocale(locale)
.format(date.toLocalDateTime()),
newDateTime().year
)
}
private fun stripYear(date: String, year: Int): String {
return date.replace("(?: de |, |/| )?$year(?:年|년 | г\\.)?".toRegex(), "")
}
private fun getRelativeDay(
context: Context,
date: Long,
locale: Locale,
abbreviated: Boolean,
lowercase: Boolean
): String {
val startOfToday = newDateTime().startOfDay()
val startOfDate = newDateTime(date).startOfDay()
if (startOfToday.equals(startOfDate)) {
return context.getString(if (lowercase) R.string.today_lowercase else R.string.today)
}
if (startOfToday.plusDays(1).equals(startOfDate)) {
return context.getString(
if (abbreviated
) if (lowercase) R.string.tomorrow_abbrev_lowercase else R.string.tmrw
else if (lowercase) R.string.tomorrow_lowercase else R.string.tomorrow
)
}
if (startOfDate.plusDays(1).equals(startOfToday)) {
return context.getString(
if (abbreviated
) if (lowercase) R.string.yesterday_abbrev_lowercase else R.string.yest
else if (lowercase) R.string.yesterday_lowercase else R.string.yesterday
)
}
val dateTime = newDateTime(date)
return if (abbreviated
) getWeekdayShort(dateTime, locale)
else getWeekday(dateTime, locale)
}
private fun isWithinSixDays(date: Long): Boolean {
val startOfToday = newDateTime().startOfDay()
val startOfDate = newDateTime(date).startOfDay()
return abs((startOfToday.millis - startOfDate.millis).toDouble()) <= ONE_DAY * 6
}
}

@ -44,7 +44,6 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreoMR1 import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreoMR1
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.files.FilesControlSet import com.todoroo.astrid.files.FilesControlSet
import com.todoroo.astrid.repeats.RepeatControlSet import com.todoroo.astrid.repeats.RepeatControlSet
@ -57,6 +56,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
@ -89,6 +89,7 @@ import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.Linkify import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.hideKeyboard import org.tasks.extensions.hideKeyboard
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.filters.Filter import org.tasks.filters.Filter
@ -98,6 +99,8 @@ import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DESCR
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DUE_DATE import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DUE_DATE
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_LIST import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_LIST
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_PRIORITY import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_PRIORITY
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.markdown.MarkdownProvider import org.tasks.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.play.PlayServices import org.tasks.play.PlayServices
@ -111,7 +114,6 @@ import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskEditViewModel import org.tasks.ui.TaskEditViewModel
import org.tasks.ui.TaskEditViewModel.Companion.stripCarriageReturns import org.tasks.ui.TaskEditViewModel.Companion.stripCarriageReturns
import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
@ -436,18 +438,19 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable @Composable
private fun DueDateRow() { private fun DueDateRow() {
val dueDate = editViewModel.dueDate.collectAsStateWithLifecycle().value val dueDate = editViewModel.dueDate.collectAsStateWithLifecycle().value
val context = LocalContext.current
DueDateRow( DueDateRow(
dueDate = if (dueDate == 0L) { dueDate = if (dueDate == 0L) {
null null
} else { } else {
DateUtilities.getRelativeDateTime( runBlocking {
LocalContext.current, getRelativeDateTime(
dueDate, dueDate,
locale, context.is24HourFormat,
FormatStyle.FULL, DateStyle.FULL,
preferences.alwaysDisplayFullDate, alwaysDisplayFullDate = preferences.alwaysDisplayFullDate
false
) )
}
}, },
overdue = dueDate.isOverdue, overdue = dueDate.isOverdue,
onClick = { onClick = {

@ -56,12 +56,10 @@ import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.adapter.TaskAdapter import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider import com.todoroo.astrid.adapter.TaskAdapterProvider
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID
import org.tasks.filters.CustomFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.repeats.RepeatTaskHelper import com.todoroo.astrid.repeats.RepeatTaskHelper
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
@ -111,6 +109,7 @@ import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.PriorityPicker.Companion.newPriorityPicker import org.tasks.dialogs.PriorityPicker.Companion.newPriorityPicker
import org.tasks.dialogs.SortSettingsActivity import org.tasks.dialogs.SortSettingsActivity
import org.tasks.extensions.Context.canScheduleExactAlarms import org.tasks.extensions.Context.canScheduleExactAlarms
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.Context.openAppNotificationSettings import org.tasks.extensions.Context.openAppNotificationSettings
import org.tasks.extensions.Context.openReminderSettings import org.tasks.extensions.Context.openReminderSettings
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
@ -120,12 +119,15 @@ import org.tasks.extensions.hideKeyboard
import org.tasks.extensions.setOnQueryTextListener import org.tasks.extensions.setOnQueryTextListener
import org.tasks.filters.AstridOrderingFilter import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.CaldavFilter import org.tasks.filters.CaldavFilter
import org.tasks.filters.CustomFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.filters.FilterImpl import org.tasks.filters.FilterImpl
import org.tasks.filters.GtasksFilter import org.tasks.filters.GtasksFilter
import org.tasks.filters.MyTasksFilter import org.tasks.filters.MyTasksFilter
import org.tasks.filters.PlaceFilter import org.tasks.filters.PlaceFilter
import org.tasks.filters.TagFilter import org.tasks.filters.TagFilter
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.markdown.MarkdownProvider import org.tasks.markdown.MarkdownProvider
import org.tasks.preferences.Device import org.tasks.preferences.Device
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
@ -147,7 +149,6 @@ import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus import org.tasks.ui.TaskListEventBus
import org.tasks.ui.TaskListViewModel import org.tasks.ui.TaskListViewModel
import org.tasks.ui.TaskListViewModel.Companion.createSearchQuery import org.tasks.ui.TaskListViewModel.Companion.createSearchQuery
import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
@ -176,7 +177,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var shortcutManager: ShortcutManager @Inject lateinit var shortcutManager: ShortcutManager
@Inject lateinit var taskCompleter: TaskCompleter @Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var locale: Locale
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var repeatTaskHelper: RepeatTaskHelper @Inject lateinit var repeatTaskHelper: RepeatTaskHelper
@Inject lateinit var taskListEventBus: TaskListEventBus @Inject lateinit var taskListEventBus: TaskListEventBus
@ -1045,8 +1045,11 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val text = getString( val text = getString(
R.string.repeat_snackbar, R.string.repeat_snackbar,
title, title,
DateUtilities.getRelativeDateTime( getRelativeDateTime(
context, task.dueDate, locale, FormatStyle.LONG, true task.dueDate,
context.is24HourFormat,
DateStyle.LONG,
lowercase = true
) )
) )
makeSnackbar(text)?.setAction(R.string.DLG_undo, undoCompletion)?.show() makeSnackbar(text)?.setAction(R.string.DLG_undo, undoCompletion)?.show()

@ -14,17 +14,18 @@ import androidx.appcompat.app.AlertDialog
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.ui.TimeDurationControlSet import com.todoroo.astrid.ui.TimeDurationControlSet
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.compose.edit.TimerRow import org.tasks.compose.edit.TimerRow
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.TaskEditControlFragment import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject import javax.inject.Inject
@ -112,9 +113,10 @@ class TimerControlSet : TaskEditControlFragment() {
viewModel.addComment(String.format( viewModel.addComment(String.format(
"%s %s\n%s %s", // $NON-NLS-1$ "%s %s\n%s %s", // $NON-NLS-1$
getString(R.string.TEA_timer_comment_stopped), getString(R.string.TEA_timer_comment_stopped),
DateUtilities.getTimeString(requireContext(), DateTimeUtils.newDateTime()), getTimeString(currentTimeMillis(), requireContext().is24HourFormat),
getString(R.string.TEA_timer_comment_spent), getString(R.string.TEA_timer_comment_spent),
elapsedTime), elapsedTime
),
null) null)
return model return model
} }
@ -125,7 +127,8 @@ class TimerControlSet : TaskEditControlFragment() {
viewModel.addComment(String.format( viewModel.addComment(String.format(
"%s %s", "%s %s",
getString(R.string.TEA_timer_comment_started), getString(R.string.TEA_timer_comment_started),
DateUtilities.getTimeString(requireContext(), DateTimeUtils.newDateTime())), getTimeString(currentTimeMillis(), requireContext().is24HourFormat)
),
null) null)
return model return model
} }

@ -33,14 +33,12 @@ import org.tasks.extensions.Context.openReminderSettings
import org.tasks.scheduling.NotificationSchedulerIntentService import org.tasks.scheduling.NotificationSchedulerIntentService
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.ui.TaskEditControlFragment import org.tasks.ui.TaskEditControlFragment
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ReminderControlSet : TaskEditControlFragment() { class ReminderControlSet : TaskEditControlFragment() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var locale: Locale
private val ringMode = mutableStateOf(0) private val ringMode = mutableStateOf(0)
@ -104,7 +102,6 @@ class ReminderControlSet : TaskEditControlFragment() {
viewModel.addAlarm(Alarm(time = timestamp, type = TYPE_DATE_TIME)) viewModel.addAlarm(Alarm(time = timestamp, type = TYPE_DATE_TIME))
} }
AlarmRow( AlarmRow(
locale = locale,
alarms = viewModel.selectedAlarms.collectAsStateWithLifecycle().value, alarms = viewModel.selectedAlarms.collectAsStateWithLifecycle().value,
hasNotificationPermissions = hasReminderPermissions && hasNotificationPermissions = hasReminderPermissions &&
(notificationPermissions == null || notificationPermissions.status == PermissionStatus.Granted), (notificationPermissions == null || notificationPermissions.status == PermissionStatus.Granted),

@ -9,28 +9,29 @@ import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.getTimeString
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.compose.edit.StartDateRow import org.tasks.compose.edit.StartDateRow
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.StartDatePicker import org.tasks.dialogs.StartDatePicker
import org.tasks.dialogs.StartDatePicker.Companion.EXTRA_DAY import org.tasks.dialogs.StartDatePicker.Companion.EXTRA_DAY
import org.tasks.dialogs.StartDatePicker.Companion.EXTRA_TIME import org.tasks.dialogs.StartDatePicker.Companion.EXTRA_TIME
import org.tasks.dialogs.StartDatePicker.Companion.NO_DAY import org.tasks.dialogs.StartDatePicker.Companion.NO_DAY
import org.tasks.dialogs.StartDatePicker.Companion.NO_TIME import org.tasks.dialogs.StartDatePicker.Companion.NO_TIME
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.withMillisOfDay
import org.tasks.ui.TaskEditControlFragment import org.tasks.ui.TaskEditControlFragment
import java.time.format.FormatStyle
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class StartDateControlSet : TaskEditControlFragment() { class StartDateControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var locale: Locale
private val vm: StartDateViewModel by viewModels() private val vm: StartDateViewModel by viewModels()
@ -57,14 +58,14 @@ class StartDateControlSet : TaskEditControlFragment() {
selectedTime = selectedTime, selectedTime = selectedTime,
hasDueDate = viewModel.dueDate.collectAsStateWithLifecycle().value > 0, hasDueDate = viewModel.dueDate.collectAsStateWithLifecycle().value > 0,
printDate = { printDate = {
DateUtilities.getRelativeDateTime( runBlocking {
requireContext(), getRelativeDateTime(
selectedDay + selectedTime, selectedDay + selectedTime,
locale, requireContext().is24HourFormat,
FormatStyle.FULL, DateStyle.FULL,
preferences.alwaysDisplayFullDate, alwaysDisplayFullDate = preferences.alwaysDisplayFullDate
false
) )
}
}, },
onClick = { onClick = {
val fragmentManager = parentFragmentManager val fragmentManager = parentFragmentManager
@ -117,7 +118,7 @@ class StartDateControlSet : TaskEditControlFragment() {
if (time == NO_TIME) { if (time == NO_TIME) {
getString(resId) getString(resId)
} else { } else {
"${getString(resId)} ${getTimeString(this, newDateTime().withMillisOfDay(time))}" "${getString(resId)} ${getTimeString(currentTimeMillis().withMillisOfDay(time), this.is24HourFormat)}"
} }
} }
} }

@ -5,12 +5,15 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.todoroo.andlib.utility.DateUtilities import kotlinx.coroutines.runBlocking
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.themes.TasksIcons import org.tasks.themes.TasksIcons
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay import org.tasks.time.startOfDay
import java.time.format.FormatStyle
@Composable @Composable
fun StartDateChip( fun StartDateChip(
@ -29,19 +32,18 @@ fun StartDateChip(
) { ) {
startDate startDate
.takeIf { Task.hasDueTime(it) } .takeIf { Task.hasDueTime(it) }
?.let { DateUtilities.getTimeString(context, it.toDateTime()) } ?.let { getTimeString(currentTimeMillis(), context.is24HourFormat) }
} else { } else {
DateUtilities.getRelativeDateTime( runBlocking {
context, getRelativeDateTime(
startDate, startDate,
context.resources.configuration.locales[0], context.is24HourFormat,
if (compact) FormatStyle.SHORT else FormatStyle.MEDIUM, if (compact) DateStyle.SHORT else DateStyle.MEDIUM
false,
false
) )
} }
} }
} }
}
if (text != null) { if (text != null) {
Chip( Chip(
icon = TasksIcons.PENDING_ACTIONS, icon = TasksIcons.PENDING_ACTIONS,

@ -21,7 +21,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.todoroo.astrid.ui.ReminderControlSetViewModel import com.todoroo.astrid.ui.ReminderControlSetViewModel
import org.tasks.R import org.tasks.R
import org.tasks.compose.AddAlarmDialog import org.tasks.compose.AddAlarmDialog
@ -32,7 +31,6 @@ import org.tasks.compose.TaskEditRow
import org.tasks.data.entity.Alarm import org.tasks.data.entity.Alarm
import org.tasks.reminders.AlarmToString import org.tasks.reminders.AlarmToString
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import java.util.Locale
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
@ -42,7 +40,6 @@ fun AlarmRow(
fixNotificationPermissions: () -> Unit, fixNotificationPermissions: () -> Unit,
alarms: List<Alarm>, alarms: List<Alarm>,
ringMode: Int, ringMode: Int,
locale: Locale,
addAlarm: (Alarm) -> Unit, addAlarm: (Alarm) -> Unit,
deleteAlarm: (Alarm) -> Unit, deleteAlarm: (Alarm) -> Unit,
openRingType: () -> Unit, openRingType: () -> Unit,
@ -56,7 +53,6 @@ fun AlarmRow(
Alarms( Alarms(
alarms = alarms, alarms = alarms,
ringMode = ringMode, ringMode = ringMode,
locale = locale,
replaceAlarm = { replaceAlarm = {
vm.setReplace(it) vm.setReplace(it)
vm.showAddAlarm(visible = true) vm.showAddAlarm(visible = true)
@ -125,7 +121,6 @@ fun AlarmRow(
fun Alarms( fun Alarms(
alarms: List<Alarm>, alarms: List<Alarm>,
ringMode: Int, ringMode: Int,
locale: Locale,
replaceAlarm: (Alarm) -> Unit, replaceAlarm: (Alarm) -> Unit,
addAlarm: () -> Unit, addAlarm: () -> Unit,
deleteAlarm: (Alarm) -> Unit, deleteAlarm: (Alarm) -> Unit,
@ -135,7 +130,7 @@ fun Alarms(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
alarms.forEach { alarm -> alarms.forEach { alarm ->
AlarmRow( AlarmRow(
text = AlarmToString(LocalContext.current, locale).toString(alarm), text = AlarmToString(LocalContext.current).toString(alarm),
onClick = { replaceAlarm(alarm) }, onClick = { replaceAlarm(alarm) },
remove = { deleteAlarm(alarm) } remove = { deleteAlarm(alarm) }
) )
@ -193,7 +188,6 @@ private fun AlarmRow(
} }
} }
@OptIn(ExperimentalPermissionsApi::class)
@Preview(showBackground = true, widthDp = 320) @Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable @Composable
@ -202,7 +196,6 @@ fun NoAlarms() {
AlarmRow( AlarmRow(
alarms = emptyList(), alarms = emptyList(),
ringMode = 0, ringMode = 0,
locale = Locale.getDefault(),
addAlarm = {}, addAlarm = {},
deleteAlarm = {}, deleteAlarm = {},
openRingType = {}, openRingType = {},
@ -213,7 +206,6 @@ fun NoAlarms() {
} }
} }
@OptIn(ExperimentalPermissionsApi::class)
@Preview(showBackground = true, widthDp = 320) @Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable @Composable
@ -222,7 +214,6 @@ fun PermissionDenied() {
AlarmRow( AlarmRow(
alarms = emptyList(), alarms = emptyList(),
ringMode = 0, ringMode = 0,
locale = Locale.getDefault(),
addAlarm = {}, addAlarm = {},
deleteAlarm = {}, deleteAlarm = {},
openRingType = {}, openRingType = {},

@ -14,13 +14,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R import org.tasks.R
import org.tasks.compose.DeleteButton import org.tasks.compose.DeleteButton
import org.tasks.compose.TaskEditRow import org.tasks.compose.TaskEditRow
import org.tasks.data.entity.UserActivity import org.tasks.data.entity.UserActivity
import org.tasks.data.pictureUri import org.tasks.data.pictureUri
import java.util.Locale import org.tasks.kmp.org.tasks.time.getFullDateTime
@Composable @Composable
fun CommentsRow( fun CommentsRow(
@ -75,7 +74,7 @@ fun Comment(
) )
} }
Text( Text(
text = DateUtilities.getLongDateStringWithTime(comment.created!!, Locale.getDefault()), text = getFullDateTime(comment.created!!),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )

@ -1,7 +1,6 @@
package org.tasks.compose.pickers package org.tasks.compose.pickers
import android.content.res.Configuration import android.content.res.Configuration
import android.os.LocaleList
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -49,7 +48,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import com.todoroo.andlib.utility.DateUtilities import kotlinx.coroutines.runBlocking
import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay import net.fortuna.ical4j.model.WeekDay
import org.tasks.R import org.tasks.R
@ -57,10 +56,10 @@ import org.tasks.compose.OutlinedBox
import org.tasks.compose.OutlinedNumberInput import org.tasks.compose.OutlinedNumberInput
import org.tasks.compose.OutlinedSpinner import org.tasks.compose.OutlinedSpinner
import org.tasks.compose.border import org.tasks.compose.border
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.repeats.CustomRecurrenceViewModel import org.tasks.repeats.CustomRecurrenceViewModel
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.format.FormatStyle
import java.time.format.TextStyle import java.time.format.TextStyle
import java.util.Locale import java.util.Locale
@ -342,10 +341,11 @@ private fun EndsPicker(
Text(text = stringResource(id = R.string.repeats_on)) Text(text = stringResource(id = R.string.repeats_on))
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
val context = LocalContext.current val context = LocalContext.current
val locale = remember { LocaleList.getDefault()[0] }
val endDateString by remember(context, endDate) { val endDateString by remember(context, endDate) {
derivedStateOf { derivedStateOf {
DateUtilities.getRelativeDay(context, endDate, locale, FormatStyle.MEDIUM) runBlocking {
getRelativeDay(endDate)
}
} }
} }
var showDatePicker by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) }

@ -14,11 +14,13 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R import org.tasks.R
import org.tasks.date.DateTimeUtils import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.withMillisOfDay
import javax.inject.Inject import javax.inject.Inject
abstract class BaseDateTimePicker : BottomSheetDialogFragment() { abstract class BaseDateTimePicker : BottomSheetDialogFragment() {
@ -113,10 +115,12 @@ abstract class BaseDateTimePicker : BottomSheetDialogFragment() {
afternoon = preferences.dateShortcutAfternoon + 1000 afternoon = preferences.dateShortcutAfternoon + 1000
evening = preferences.dateShortcutEvening + 1000 evening = preferences.dateShortcutEvening + 1000
night = preferences.dateShortcutNight + 1000 night = preferences.dateShortcutNight + 1000
morningButton.text = DateUtilities.getTimeString(requireContext(), DateTimeUtils.newDateTime().withMillisOfDay(morning)) val is24HourFormat = requireContext().is24HourFormat
afternoonButton.text = DateUtilities.getTimeString(requireContext(), DateTimeUtils.newDateTime().withMillisOfDay(afternoon)) val now = currentTimeMillis()
eveningButton.text = DateUtilities.getTimeString(requireContext(), DateTimeUtils.newDateTime().withMillisOfDay(evening)) morningButton.text = getTimeString(now.withMillisOfDay(morning), is24HourFormat)
nightButton.text = DateUtilities.getTimeString(requireContext(), DateTimeUtils.newDateTime().withMillisOfDay(night)) afternoonButton.text = getTimeString(now.withMillisOfDay(afternoon), is24HourFormat)
eveningButton.text = getTimeString(now.withMillisOfDay(evening), is24HourFormat)
nightButton.text = getTimeString(now.withMillisOfDay(night), is24HourFormat)
val firstDayOfWeek = preferences.firstDayOfWeek val firstDayOfWeek = preferences.firstDayOfWeek
if (firstDayOfWeek in 1..7) { if (firstDayOfWeek in 1..7) {
calendarView.firstDayOfWeek = firstDayOfWeek calendarView.firstDayOfWeek = firstDayOfWeek

@ -10,11 +10,11 @@ import android.view.ViewGroup
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.data.createDueDate import org.tasks.data.createDueDate
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
@ -22,11 +22,14 @@ import org.tasks.databinding.DialogDateTimePickerBinding
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.millisOfDay import org.tasks.time.millisOfDay
import org.tasks.time.startOfDay import org.tasks.time.startOfDay
import java.time.format.FormatStyle import org.tasks.time.withMillisOfDay
import java.util.Calendar.FRIDAY import java.util.Calendar.FRIDAY
import java.util.Calendar.MONDAY import java.util.Calendar.MONDAY
import java.util.Calendar.SATURDAY import java.util.Calendar.SATURDAY
@ -34,14 +37,12 @@ import java.util.Calendar.SUNDAY
import java.util.Calendar.THURSDAY import java.util.Calendar.THURSDAY
import java.util.Calendar.TUESDAY import java.util.Calendar.TUESDAY
import java.util.Calendar.WEDNESDAY import java.util.Calendar.WEDNESDAY
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class DateTimePicker : BaseDateTimePicker() { class DateTimePicker : BaseDateTimePicker() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var locale: Locale
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var notificationManager: NotificationManager @Inject lateinit var notificationManager: NotificationManager
@ -170,7 +171,9 @@ class DateTimePicker : BaseDateTimePicker() {
binding.shortcuts.currentDateSelection.text = if (customDate == MULTIPLE_DAYS) { binding.shortcuts.currentDateSelection.text = if (customDate == MULTIPLE_DAYS) {
requireContext().getString(R.string.date_picker_multiple) requireContext().getString(R.string.date_picker_multiple)
} else { } else {
DateUtilities.getRelativeDay(requireContext(), selectedDay, locale, FormatStyle.MEDIUM) runBlocking {
getRelativeDay(selectedDay)
}
} }
} }
} }
@ -184,10 +187,14 @@ class DateTimePicker : BaseDateTimePicker() {
customTime = selectedTime customTime = selectedTime
binding.shortcuts.timeGroup.check(R.id.current_time_selection) binding.shortcuts.timeGroup.check(R.id.current_time_selection)
binding.shortcuts.currentTimeSelection.visibility = View.VISIBLE binding.shortcuts.currentTimeSelection.visibility = View.VISIBLE
binding.shortcuts.currentTimeSelection.text = if (customTime == MULTIPLE_TIMES) { binding.shortcuts.currentTimeSelection.text =
if (customTime == MULTIPLE_TIMES) {
requireContext().getString(R.string.date_picker_multiple) requireContext().getString(R.string.date_picker_multiple)
} else { } else {
DateUtilities.getTimeString(requireContext(), today.withMillisOfDay(selectedTime)) getTimeString(
today.millis.withMillisOfDay(selectedTime),
requireContext().is24HourFormat
)
} }
} }
} }

@ -8,25 +8,26 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.databinding.DialogStartDatePickerBinding import org.tasks.databinding.DialogStartDatePickerBinding
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.time.format.FormatStyle import org.tasks.time.withMillisOfDay
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class StartDatePicker : BaseDateTimePicker() { class StartDatePicker : BaseDateTimePicker() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var locale: Locale
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var notificationManager: NotificationManager @Inject lateinit var notificationManager: NotificationManager
@ -112,7 +113,9 @@ class StartDatePicker : BaseDateTimePicker() {
binding.shortcuts.dateGroup.check(R.id.current_date_selection) binding.shortcuts.dateGroup.check(R.id.current_date_selection)
binding.shortcuts.currentDateSelection.visibility = View.VISIBLE binding.shortcuts.currentDateSelection.visibility = View.VISIBLE
binding.shortcuts.currentDateSelection.text = binding.shortcuts.currentDateSelection.text =
DateUtilities.getRelativeDay(requireContext(), selectedDay, locale, FormatStyle.MEDIUM) runBlocking {
getRelativeDay(selectedDay)
}
} }
} }
if (Task.hasDueTime(selectedTime.toLong())) { if (Task.hasDueTime(selectedTime.toLong())) {
@ -125,7 +128,10 @@ class StartDatePicker : BaseDateTimePicker() {
customTime = selectedTime customTime = selectedTime
binding.shortcuts.timeGroup.check(R.id.current_time_selection) binding.shortcuts.timeGroup.check(R.id.current_time_selection)
binding.shortcuts.currentTimeSelection.visibility = View.VISIBLE binding.shortcuts.currentTimeSelection.visibility = View.VISIBLE
binding.shortcuts.currentTimeSelection.text = DateUtilities.getTimeString(requireContext(), today.withMillisOfDay(selectedTime)) binding.shortcuts.currentTimeSelection.text = getTimeString(
today.millis.withMillisOfDay(selectedTime),
requireContext().is24HourFormat
)
} }
} }
if (selectedDay == DUE_TIME) { if (selectedDay == DUE_TIME) {

@ -15,6 +15,7 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.text.format.DateFormat
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
@ -22,6 +23,7 @@ import androidx.annotation.AnyRes
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import com.todoroo.andlib.utility.AndroidUtilities.atLeastS import com.todoroo.andlib.utility.AndroidUtilities.atLeastS
import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.notifications.NotificationManager.Companion.NOTIFICATION_CHANNEL_DEFAULT import org.tasks.notifications.NotificationManager.Companion.NOTIFICATION_CHANNEL_DEFAULT
@ -29,6 +31,14 @@ object Context {
private const val HTTP = "http" private const val HTTP = "http"
private const val HTTPS = "https" private const val HTTPS = "https"
var is24HourOverride: Boolean? = null
val Context.is24HourFormat: Boolean
get() = if (BuildConfig.DEBUG && is24HourOverride != null)
is24HourOverride!!
else
DateFormat.is24HourFormat(this)
fun Context.safeStartActivity(intent: Intent) { fun Context.safeStartActivity(intent: Intent) {
try { try {
startActivity(intent) startActivity(intent)

@ -6,7 +6,6 @@ import android.os.Bundle
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.PermissionUtil import org.tasks.PermissionUtil
import org.tasks.R import org.tasks.R
@ -16,11 +15,11 @@ import org.tasks.drive.DriveLoginActivity
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.injection.InjectingPreferenceFragment import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.kmp.org.tasks.time.getFullDateTime
import org.tasks.preferences.FragmentPermissionRequestor import org.tasks.preferences.FragmentPermissionRequestor
import org.tasks.preferences.PermissionRequestor import org.tasks.preferences.PermissionRequestor
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.preferences.PreferencesViewModel import org.tasks.preferences.PreferencesViewModel
import java.util.*
import javax.inject.Inject import javax.inject.Inject
private const val REQUEST_CODE_BACKUP_DIR = 10001 private const val REQUEST_CODE_BACKUP_DIR = 10001
@ -35,7 +34,6 @@ class Backups : InjectingPreferenceFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor @Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var locale: Locale
private val viewModel: PreferencesViewModel by activityViewModels() private val viewModel: PreferencesViewModel by activityViewModels()
@ -101,7 +99,7 @@ class Backups : InjectingPreferenceFragment() {
R.string.last_backup, R.string.last_backup,
timestamp timestamp
?.takeIf { it >= 0 } ?.takeIf { it >= 0 }
?.let { DateUtilities.getLongDateStringWithTime(it, locale) } ?.let { getFullDateTime(it) }
?: getString(R.string.last_backup_never) ?: getString(R.string.last_backup_never)
) )
} }
@ -119,7 +117,7 @@ class Backups : InjectingPreferenceFragment() {
R.string.last_backup, R.string.last_backup,
timestamp timestamp
?.takeIf { it >= 0 } ?.takeIf { it >= 0 }
?.let { DateUtilities.getLongDateStringWithTime(it, locale) } ?.let { getFullDateTime(it) }
?: getString(R.string.last_backup_never) ?: getString(R.string.last_backup_never)
) )
} }
@ -137,7 +135,7 @@ class Backups : InjectingPreferenceFragment() {
R.string.last_backup, R.string.last_backup,
timestamp timestamp
?.takeIf { it >= 0 } ?.takeIf { it >= 0 }
?.let { DateUtilities.getLongDateStringWithTime(it, locale) } ?.let { getFullDateTime(it) }
?: getString(R.string.last_backup_never) ?: getString(R.string.last_backup_never)
) )
} }

@ -16,9 +16,9 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
@ -32,10 +32,10 @@ import org.tasks.data.entity.CaldavAccount.Companion.isPaymentRequired
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.preferences.IconPreference import org.tasks.preferences.IconPreference
import org.tasks.preferences.fragments.MainSettingsFragment.Companion.REQUEST_TASKS_ORG import org.tasks.preferences.fragments.MainSettingsFragment.Companion.REQUEST_TASKS_ORG
import java.time.format.FormatStyle
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -44,7 +44,6 @@ class TasksAccount : BaseAccountPreference() {
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var workManager: WorkManager @Inject lateinit var workManager: WorkManager
@Inject lateinit var locale: Locale
private val viewModel: TasksAccountViewModel by viewModels() private val viewModel: TasksAccountViewModel by viewModels()
@ -98,7 +97,11 @@ class TasksAccount : BaseAccountPreference() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.appPasswords.observe(this) { passwords -> viewModel.appPasswords.observe(this) { passwords ->
passwords?.let { refreshPasswords(passwords) } passwords?.let {
runBlocking {
refreshPasswords(passwords)
}
}
} }
viewModel.newPassword.observe(this) { viewModel.newPassword.observe(this) {
it?.let { it?.let {
@ -212,7 +215,7 @@ class TasksAccount : BaseAccountPreference() {
} }
} }
private fun refreshPasswords(passwords: List<TasksAccountViewModel.AppPassword>) { private suspend fun refreshPasswords(passwords: List<TasksAccountViewModel.AppPassword>) {
findPreference(R.string.app_passwords_more_info).isVisible = passwords.isEmpty() findPreference(R.string.app_passwords_more_info).isVisible = passwords.isEmpty()
val category = findPreference(R.string.app_passwords) as PreferenceCategory val category = findPreference(R.string.app_passwords) as PreferenceCategory
category.removeAll() category.removeAll()
@ -243,14 +246,11 @@ class TasksAccount : BaseAccountPreference() {
} }
} }
private fun formatString(date: Long?): String? = date?.let { private suspend fun formatString(date: Long?): String? = date?.let {
DateUtilities.getRelativeDay( getRelativeDay(
requireContext(),
date, date,
locale, DateStyle.FULL,
FormatStyle.FULL, lowercase = true
false,
true
) )
} }

@ -2,14 +2,13 @@ package org.tasks.reminders
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R import org.tasks.R
import org.tasks.data.entity.Alarm import org.tasks.data.entity.Alarm
import java.util.Locale import org.tasks.kmp.org.tasks.time.getFullDateTime
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class AlarmToString(context: Context, var locale: Locale) { class AlarmToString(context: Context) {
private val resources = context.resources private val resources = context.resources
fun toString(alarm: Alarm): String { fun toString(alarm: Alarm): String {
@ -41,10 +40,10 @@ class AlarmToString(context: Context, var locale: Locale) {
Alarm.TYPE_SNOOZE -> Alarm.TYPE_SNOOZE ->
resources.getString( resources.getString(
R.string.snoozed_until, R.string.snoozed_until,
DateUtilities.getLongDateStringWithTime(alarm.time, locale) getFullDateTime(alarm.time)
) )
else -> else ->
DateUtilities.getLongDateStringWithTime(alarm.time, locale) getFullDateTime(alarm.time)
} }
return if (alarm.repeat > 0) { return if (alarm.repeat > 0) {
reminder + "\n" + resources.getRepeatString(alarm.repeat, alarm.interval) reminder + "\n" + resources.getRepeatString(alarm.repeat, alarm.interval)

@ -1,23 +1,27 @@
package org.tasks.reminders; package org.tasks.reminders;
import static com.todoroo.andlib.utility.DateUtilities.getTimeString;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import dagger.hilt.android.AndroidEntryPoint;
import dagger.hilt.android.qualifiers.ApplicationContext;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.kmp.org.tasks.time.DateUtilitiesKt;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import dagger.hilt.android.qualifiers.ApplicationContext;
@AndroidEntryPoint @AndroidEntryPoint
public class SnoozeDialog extends DialogFragment { public class SnoozeDialog extends DialogFragment {
@ -92,7 +96,11 @@ public class SnoozeDialog extends DialogFragment {
String.format( String.format(
"%s (%s)", "%s (%s)",
getString(snoozeOption.getResId()), getString(snoozeOption.getResId()),
getTimeString(context, snoozeOption.getDateTime()))); DateUtilitiesKt.getTimeString(
snoozeOption.getDateTime().getMillis(),
org.tasks.extensions.Context.INSTANCE.is24HourFormat(context))
)
);
} }
items.add(getString(R.string.pick_a_date_and_time)); items.add(getString(R.string.pick_a_date_and_time));

@ -1,19 +1,31 @@
package org.tasks.repeats package org.tasks.repeats
import android.content.Context import android.content.Context
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.Recur.Frequency import net.fortuna.ical4j.model.Recur.Frequency
import net.fortuna.ical4j.model.Recur.Frequency.* import net.fortuna.ical4j.model.Recur.Frequency.DAILY
import net.fortuna.ical4j.model.Recur.Frequency.HOURLY
import net.fortuna.ical4j.model.Recur.Frequency.MINUTELY
import net.fortuna.ical4j.model.Recur.Frequency.MONTHLY
import net.fortuna.ical4j.model.Recur.Frequency.WEEKLY
import net.fortuna.ical4j.model.Recur.Frequency.YEARLY
import net.fortuna.ical4j.model.WeekDay.Day import net.fortuna.ical4j.model.WeekDay.Day
import net.fortuna.ical4j.model.WeekDay.Day.* import net.fortuna.ical4j.model.WeekDay.Day.FR
import net.fortuna.ical4j.model.WeekDay.Day.MO
import net.fortuna.ical4j.model.WeekDay.Day.SA
import net.fortuna.ical4j.model.WeekDay.Day.SU
import net.fortuna.ical4j.model.WeekDay.Day.TH
import net.fortuna.ical4j.model.WeekDay.Day.TU
import net.fortuna.ical4j.model.WeekDay.Day.WE
import org.tasks.R import org.tasks.R
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.kmp.org.tasks.time.getFullDate
import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.util.* import java.util.Calendar
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class RepeatRuleToString @Inject constructor( class RepeatRuleToString @Inject constructor(
@ -50,7 +62,7 @@ class RepeatRuleToString @Inject constructor(
R.string.repeats_single_on_until, R.string.repeats_single_on_until,
frequencyString, frequencyString,
dayString, dayString,
DateUtilities.getLongDateString(repeatUntil, locale) getFullDate(repeatUntil.millis)
) )
} }
} else if (count > 0) { } else if (count > 0) {
@ -62,7 +74,8 @@ class RepeatRuleToString @Inject constructor(
context.getString( context.getString(
R.string.repeats_single_until, R.string.repeats_single_until,
frequencyString, frequencyString,
DateUtilities.getLongDateString(repeatUntil, locale)) getFullDate(repeatUntil.millis)
)
} }
} else { } else {
val plural = getFrequencyPlural(frequency) val plural = getFrequencyPlural(frequency)
@ -83,7 +96,7 @@ class RepeatRuleToString @Inject constructor(
R.string.repeats_plural_on_until, R.string.repeats_plural_on_until,
frequencyPlural, frequencyPlural,
dayString, dayString,
DateUtilities.getLongDateString(repeatUntil, locale) getFullDate(repeatUntil.millis)
) )
} }
} else if (count > 0) { } else if (count > 0) {
@ -95,7 +108,8 @@ class RepeatRuleToString @Inject constructor(
context.getString( context.getString(
R.string.repeats_plural_until, R.string.repeats_plural_until,
frequencyPlural, frequencyPlural,
DateUtilities.getLongDateString(repeatUntil, locale)) getFullDate(repeatUntil.millis)
)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

@ -2,21 +2,19 @@ package org.tasks.tasklist
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.core.SortHelper import com.todoroo.astrid.core.SortHelper
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import java.time.format.FormatStyle
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class HeaderFormatter @Inject constructor( class HeaderFormatter @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val preferences: Preferences, private val preferences: Preferences,
private val locale: Locale,
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
) { ) {
private val listCache = HashMap<Long, String?>() private val listCache = HashMap<Long, String?>()
@ -25,7 +23,7 @@ class HeaderFormatter @Inject constructor(
value: Long, value: Long,
groupMode: Int = preferences.groupMode, groupMode: Int = preferences.groupMode,
alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate, alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate,
style: FormatStyle = FormatStyle.FULL, style: DateStyle = DateStyle.FULL,
compact: Boolean = false, compact: Boolean = false,
) = runBlocking { ) = runBlocking {
headerString(value, groupMode, alwaysDisplayFullDate, style, compact) headerString(value, groupMode, alwaysDisplayFullDate, style, compact)
@ -35,7 +33,7 @@ class HeaderFormatter @Inject constructor(
value: Long, value: Long,
groupMode: Int = preferences.groupMode, groupMode: Int = preferences.groupMode,
alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate, alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate,
style: FormatStyle = FormatStyle.FULL, style: DateStyle = DateStyle.FULL,
compact: Boolean = false compact: Boolean = false
): String = ): String =
when { when {
@ -50,8 +48,11 @@ class HeaderFormatter @Inject constructor(
else -> R.string.no_date else -> R.string.no_date
}) })
else -> { else -> {
val dateString = DateUtilities.getRelativeDay( val dateString = getRelativeDay(
context, value, locale, style, alwaysDisplayFullDate, !compact value,
style,
alwaysDisplayFullDate = alwaysDisplayFullDate,
lowercase = !compact
) )
when { when {
compact -> dateString compact -> dateString

@ -13,11 +13,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.core.SortHelper.SORT_DUE import com.todoroo.astrid.core.SortHelper.SORT_DUE
import com.todoroo.astrid.core.SortHelper.SORT_LIST import com.todoroo.astrid.core.SortHelper.SORT_LIST
import com.todoroo.astrid.core.SortHelper.SORT_START import com.todoroo.astrid.core.SortHelper.SORT_START
import com.todoroo.astrid.ui.CheckableImageView import com.todoroo.astrid.ui.CheckableImageView
import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.compose.ChipGroup import org.tasks.compose.ChipGroup
import org.tasks.compose.FilterChip import org.tasks.compose.FilterChip
@ -28,13 +28,15 @@ import org.tasks.data.hasNotes
import org.tasks.data.isHidden import org.tasks.data.isHidden
import org.tasks.data.isOverdue import org.tasks.data.isOverdue
import org.tasks.databinding.TaskAdapterRowBinding import org.tasks.databinding.TaskAdapterRowBinding
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.Linkify import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.filters.CaldavFilter import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.filters.GtasksFilter import org.tasks.filters.GtasksFilter
import org.tasks.filters.PlaceFilter import org.tasks.filters.PlaceFilter
import org.tasks.filters.TagFilter import org.tasks.filters.TagFilter
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.markdown.Markdown import org.tasks.markdown.Markdown
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.TasksIcons import org.tasks.themes.TasksIcons
@ -43,8 +45,6 @@ import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay import org.tasks.time.startOfDay
import org.tasks.ui.CheckBoxProvider import org.tasks.ui.CheckBoxProvider
import org.tasks.ui.ChipProvider import org.tasks.ui.ChipProvider
import java.time.format.FormatStyle
import java.util.Locale
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -64,7 +64,6 @@ class TaskViewHolder internal constructor(
private val rowPaddingDp: Int, private val rowPaddingDp: Int,
private val rowPaddingPx: Int, private val rowPaddingPx: Int,
private val linkify: Linkify, private val linkify: Linkify,
private val locale: Locale,
private val markdown: Markdown private val markdown: Markdown
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
private val row: ViewGroup = binding.row private val row: ViewGroup = binding.row
@ -220,10 +219,16 @@ class TaskViewHolder internal constructor(
&& (task.sortGroup ?: 0) >= currentTimeMillis().startOfDay() && (task.sortGroup ?: 0) >= currentTimeMillis().startOfDay()
) { ) {
task.takeIf { it.hasDueTime() }?.let { task.takeIf { it.hasDueTime() }?.let {
DateUtilities.getTimeString(context, newDateTime(task.dueDate)) getTimeString(task.dueDate, context.is24HourFormat)
} }
} else { } else {
DateUtilities.getRelativeDateTime(context, task.dueDate, locale, FormatStyle.MEDIUM, alwaysDisplayFullDate, false) runBlocking {
getRelativeDateTime(
task.dueDate,
context.is24HourFormat,
alwaysDisplayFullDate = alwaysDisplayFullDate
)
}
} }
dueDate.text = dateValue dueDate.text = dateValue
dueDate.visibility = View.VISIBLE dueDate.visibility = View.VISIBLE

@ -16,7 +16,6 @@ import org.tasks.preferences.ResourceResolver
import org.tasks.tasklist.TaskViewHolder.ViewHolderCallbacks import org.tasks.tasklist.TaskViewHolder.ViewHolderCallbacks
import org.tasks.ui.CheckBoxProvider import org.tasks.ui.CheckBoxProvider
import org.tasks.ui.ChipProvider import org.tasks.ui.ChipProvider
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class ViewHolderFactory @Inject constructor( class ViewHolderFactory @Inject constructor(
@ -25,7 +24,6 @@ class ViewHolderFactory @Inject constructor(
private val chipProvider: ChipProvider, private val chipProvider: ChipProvider,
private val checkBoxProvider: CheckBoxProvider, private val checkBoxProvider: CheckBoxProvider,
private val linkify: Linkify, private val linkify: Linkify,
private val locale: Locale,
private val headerFormatter: HeaderFormatter, private val headerFormatter: HeaderFormatter,
) { ) {
private val textColorSecondary: Int = ResourceResolver.getData(context, android.R.attr.textColorSecondary) private val textColorSecondary: Int = ResourceResolver.getData(context, android.R.attr.textColorSecondary)
@ -64,7 +62,6 @@ class ViewHolderFactory @Inject constructor(
rowPaddingDp, rowPaddingDp,
rowPaddingPx, rowPaddingPx,
linkify, linkify,
locale,
markdown markdown
) )
} }

@ -142,7 +142,7 @@ class DateTime {
} }
fun minusSeconds(seconds: Int): DateTime { fun minusSeconds(seconds: Int): DateTime {
return subtract(Calendar.SECOND, seconds) return add(Calendar.SECOND, -seconds)
} }
fun minusDays(days: Int): DateTime = DateTime(millis.minusDays(days)) fun minusDays(days: Int): DateTime = DateTime(millis.minusDays(days))
@ -199,10 +199,6 @@ class DateTime {
return DateTime(calendar) return DateTime(calendar)
} }
private fun subtract(field: Int, value: Int): DateTime {
return add(field, -value)
}
private fun add(field: Int, value: Int): DateTime { private fun add(field: Int, value: Int): DateTime {
val calendar = calendar val calendar = calendar
calendar.add(field, value) calendar.add(field, value)
@ -262,15 +258,14 @@ class DateTime {
throw RuntimeException() throw RuntimeException()
} }
override fun equals(o: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === o) { if (this === other) {
return true return true
} }
if (o !is DateTime) { if (other !is DateTime) {
return false return false
} }
val dateTime = o return millis == other.millis && timeZone == other.timeZone
return millis == dateTime.millis && timeZone == dateTime.timeZone
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -303,11 +298,10 @@ class DateTime {
fun from(date: Date?): DateTime { fun from(date: Date?): DateTime {
if (date is net.fortuna.ical4j.model.DateTime) { if (date is net.fortuna.ical4j.model.DateTime) {
val dt = date val tz: TimeZone? = date.timeZone
val tz: TimeZone? = dt.timeZone
return DateTime( return DateTime(
dt.time, date.time,
tz ?: if (dt.isUtc) UTC else TimeZone.getDefault() tz ?: if (date.isUtc) UTC else TimeZone.getDefault()
) )
} else { } else {
return from(date as java.util.Date?) return from(date as java.util.Date?)

@ -1,14 +1,19 @@
package org.tasks.ui; package org.tasks.ui;
import static org.tasks.time.DateTimeUtils2.currentTimeMillis;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.util.AttributeSet; import android.util.AttributeSet;
import androidx.preference.Preference; import androidx.preference.Preference;
import com.todoroo.andlib.utility.DateUtilities;
import org.tasks.R; import org.tasks.R;
import org.tasks.dialogs.MyTimePickerDialog; import org.tasks.dialogs.MyTimePickerDialog;
import org.tasks.kmp.org.tasks.time.DateUtilitiesKt;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import org.tasks.time.LongExtensionsKt;
public class TimePreference extends Preference { public class TimePreference extends Preference {
@ -47,7 +52,10 @@ public class TimePreference extends Preference {
private void setMillisOfDay(int millisOfDay) { private void setMillisOfDay(int millisOfDay) {
this.millisOfDay = millisOfDay; this.millisOfDay = millisOfDay;
String setting = String setting =
DateUtilities.getTimeString(getContext(), new DateTime().withMillisOfDay(millisOfDay)); DateUtilitiesKt.getTimeString(
LongExtensionsKt.withMillisOfDay(currentTimeMillis(), millisOfDay),
org.tasks.extensions.Context.INSTANCE.is24HourFormat(getContext())
);
setSummary(summary == null ? setting : String.format(summary, setting)); setSummary(summary == null ? setting : String.format(summary, setting));
} }

@ -13,7 +13,6 @@ import org.tasks.markdown.MarkdownProvider
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter import org.tasks.tasklist.HeaderFormatter
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -23,7 +22,6 @@ class TasksWidgetAdapter : RemoteViewsService() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var subtasksHelper: SubtasksHelper @Inject lateinit var subtasksHelper: SubtasksHelper
@Inject lateinit var locale: Locale
@Inject lateinit var chipProvider: WidgetChipProvider @Inject lateinit var chipProvider: WidgetChipProvider
@Inject lateinit var markdownProvider: MarkdownProvider @Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var headerFormatter: HeaderFormatter @Inject lateinit var headerFormatter: HeaderFormatter
@ -41,7 +39,6 @@ class TasksWidgetAdapter : RemoteViewsService() {
applicationContext, applicationContext,
widgetId, widgetId,
taskDao, taskDao,
locale,
chipProvider, chipProvider,
markdownProvider.markdown(false), markdownProvider.markdown(false),
headerFormatter, headerFormatter,

@ -5,7 +5,6 @@ import android.content.Intent
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory import android.widget.RemoteViewsService.RemoteViewsFactory
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.core.SortHelper import com.todoroo.astrid.core.SortHelper
import com.todoroo.astrid.subtasks.SubtasksHelper import com.todoroo.astrid.subtasks.SubtasksHelper
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -17,7 +16,7 @@ import org.tasks.data.dao.TaskDao
import org.tasks.data.hasNotes import org.tasks.data.hasNotes
import org.tasks.data.isHidden import org.tasks.data.isHidden
import org.tasks.data.isOverdue import org.tasks.data.isOverdue
import org.tasks.date.DateTimeUtils import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.setBackgroundResource import org.tasks.extensions.setBackgroundResource
import org.tasks.extensions.setColorFilter import org.tasks.extensions.setColorFilter
import org.tasks.extensions.setMaxLines import org.tasks.extensions.setMaxLines
@ -26,6 +25,9 @@ import org.tasks.extensions.strikethrough
import org.tasks.filters.AstridOrderingFilter import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.kmp.org.tasks.themes.ColorProvider.priorityColor import org.tasks.kmp.org.tasks.themes.ColorProvider.priorityColor
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.markdown.Markdown import org.tasks.markdown.Markdown
import org.tasks.tasklist.HeaderFormatter import org.tasks.tasklist.HeaderFormatter
import org.tasks.tasklist.SectionedDataSource import org.tasks.tasklist.SectionedDataSource
@ -34,8 +36,6 @@ import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay import org.tasks.time.startOfDay
import org.tasks.ui.CheckBoxProvider.Companion.getCheckboxRes import org.tasks.ui.CheckBoxProvider.Companion.getCheckboxRes
import timber.log.Timber import timber.log.Timber
import java.time.format.FormatStyle
import java.util.Locale
import kotlin.math.max import kotlin.math.max
internal class TasksWidgetViewFactory( internal class TasksWidgetViewFactory(
@ -45,7 +45,6 @@ internal class TasksWidgetViewFactory(
private val context: Context, private val context: Context,
private val widgetId: Int, private val widgetId: Int,
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val locale: Locale,
private val chipProvider: WidgetChipProvider, private val chipProvider: WidgetChipProvider,
private val markdown: Markdown, private val markdown: Markdown,
private val headerFormatter: HeaderFormatter, private val headerFormatter: HeaderFormatter,
@ -111,7 +110,7 @@ internal class TasksWidgetViewFactory(
value = section.value, value = section.value,
groupMode = settings.groupMode, groupMode = settings.groupMode,
alwaysDisplayFullDate = settings.showFullDate, alwaysDisplayFullDate = settings.showFullDate,
style = FormatStyle.MEDIUM, style = DateStyle.MEDIUM,
compact = settings.compact, compact = settings.compact,
) )
} else { } else {
@ -290,13 +289,17 @@ internal class TasksWidgetViewFactory(
!disableGroups !disableGroups
) { ) {
task.takeIf { it.hasDueTime() }?.let { task.takeIf { it.hasDueTime() }?.let {
DateUtilities.getTimeString(context, DateTimeUtils.newDateTime(task.dueDate)) getTimeString(task.dueDate, context.is24HourFormat)
} }
} else { } else {
DateUtilities.getRelativeDateTime( runBlocking {
context, task.dueDate, locale, FormatStyle.MEDIUM, settings.showFullDate, false getRelativeDateTime(
task.dueDate,
context.is24HourFormat,
alwaysDisplayFullDate = settings.showFullDate
) )
} }
}
setTextViewText(dueDateRes, text) setTextViewText(dueDateRes, text)
setTextColor( setTextColor(
dueDateRes, dueDateRes,

@ -4,15 +4,15 @@ import android.content.Context
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.data.isHidden import org.tasks.data.isHidden
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.setColorFilter import org.tasks.extensions.setColorFilter
import org.tasks.filters.CaldavFilter import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
@ -21,16 +21,15 @@ import org.tasks.filters.PlaceFilter
import org.tasks.filters.TagFilter import org.tasks.filters.TagFilter
import org.tasks.filters.getIcon import org.tasks.filters.getIcon
import org.tasks.icons.OutlinedGoogleMaterial import org.tasks.icons.OutlinedGoogleMaterial
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.time.startOfDay import org.tasks.time.startOfDay
import org.tasks.ui.ChipListCache import org.tasks.ui.ChipListCache
import java.time.format.FormatStyle
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class WidgetChipProvider @Inject constructor( class WidgetChipProvider @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val chipListCache: ChipListCache, private val chipListCache: ChipListCache,
private val locale: Locale,
private val inventory: Inventory, private val inventory: Inventory,
) { ) {
var isDark = false var isDark = false
@ -59,18 +58,17 @@ class WidgetChipProvider @Inject constructor(
val time = if (sortByStartDate && task.sortGroup?.startOfDay() == task.task.hideUntil.startOfDay()) { val time = if (sortByStartDate && task.sortGroup?.startOfDay() == task.task.hideUntil.startOfDay()) {
task.task.hideUntil task.task.hideUntil
.takeIf { Task.hasDueTime(it) } .takeIf { Task.hasDueTime(it) }
?.let { DateUtilities.getTimeString(context, it.toDateTime()) } ?.let { getTimeString(it, context.is24HourFormat) }
?: return null ?: return null
} else { } else {
DateUtilities.getRelativeDateTime( runBlocking {
context, getRelativeDateTime(
task.task.hideUntil, task.task.hideUntil,
locale, context.is24HourFormat,
FormatStyle.MEDIUM, alwaysDisplayFullDate = showFullDate
showFullDate,
false
) )
} }
}
newChip().apply { newChip().apply {
setTextViewText(R.id.chip_text, time) setTextViewText(R.id.chip_text, time)
setImageViewResource(R.id.chip_icon, R.drawable.ic_pending_actions_24px) setImageViewResource(R.id.chip_icon, R.drawable.ic_pending_actions_24px)

@ -3,12 +3,12 @@ package org.tasks
import android.content.Context import android.content.Context
import at.bitfire.ical4android.Task.Companion.tasksFromReader import at.bitfire.ical4android.Task.Companion.tasksFromReader
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import org.tasks.data.entity.Task
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.caldav.applyRemote import org.tasks.caldav.applyRemote
import org.tasks.caldav.iCalendar.Companion.reminders import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.data.entity.Alarm import org.tasks.data.entity.Alarm
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Task
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.sync.microsoft.MicrosoftConverter.applyRemote import org.tasks.sync.microsoft.MicrosoftConverter.applyRemote
import org.tasks.sync.microsoft.Tasks import org.tasks.sync.microsoft.Tasks
@ -16,6 +16,7 @@ import org.tasks.time.DateTime
import java.io.StringReader import java.io.StringReader
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
object TestUtilities { object TestUtilities {
@ -34,6 +35,18 @@ object TestUtilities {
} }
} }
fun withLocale(locale: Locale, runnable: suspend () -> Unit) {
val def = Locale.getDefault()
try {
Locale.setDefault(locale)
runBlocking {
runnable()
}
} finally {
Locale.setDefault(def)
}
}
fun assertEquals(expected: Long, actual: DateTime) = fun assertEquals(expected: Long, actual: DateTime) =
org.junit.Assert.assertEquals(expected, actual.millis) org.junit.Assert.assertEquals(expected, actual.millis)

@ -58,6 +58,7 @@ android {
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
@ -67,6 +68,7 @@ android {
} }
dependencies { dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
debugImplementation(compose.uiTooling) debugImplementation(compose.uiTooling)
} }
} }

@ -5,6 +5,12 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import org.tasks.data.BuildConfig import org.tasks.data.BuildConfig
import org.tasks.extensions.formatNumber import org.tasks.extensions.formatNumber
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.TextStyle
import org.tasks.kmp.org.tasks.time.toFormatStyle
import org.tasks.kmp.org.tasks.time.toLocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
actual fun formatNumber(number: Int) = Locale.getDefault().formatNumber(number) actual fun formatNumber(number: Int) = Locale.getDefault().formatNumber(number)
@ -14,3 +20,31 @@ fun createDataStore(context: Context): DataStore<Preferences> = createDataStore(
) )
actual val IS_DEBUG = BuildConfig.DEBUG actual val IS_DEBUG = BuildConfig.DEBUG
actual fun formatDate(timestamp: Long, style: DateStyle): String =
DateTimeFormatter
.ofLocalizedDate(style.toFormatStyle())
.withLocale(Locale.getDefault())
.format(timestamp.toLocalDateTime().toLocalDate())
actual fun formatDateTime(timestamp: Long, style: DateStyle): String =
DateTimeFormatter
.ofLocalizedDateTime(style.toFormatStyle(), FormatStyle.SHORT)
.withLocale(Locale.getDefault())
.format(timestamp.toLocalDateTime())
actual fun formatDayOfWeek(timestamp: Long, style: TextStyle): String =
timestamp
.toLocalDateTime()
.dayOfWeek
.getDisplayName(
when (style) {
TextStyle.FULL -> java.time.format.TextStyle.FULL
TextStyle.SHORT -> java.time.format.TextStyle.SHORT
TextStyle.NARROW -> java.time.format.TextStyle.NARROW
},
Locale.getDefault()
)
actual fun formatDateTime(timestamp: Long, format: String): String =
timestamp.toLocalDateTime().format(DateTimeFormatter.ofPattern(format))

@ -0,0 +1,16 @@
package org.tasks.kmp.org.tasks.time
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.FormatStyle
fun Long.toLocalDateTime(): LocalDateTime =
LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
fun DateStyle.toFormatStyle() = when (this) {
DateStyle.FULL -> FormatStyle.FULL
DateStyle.LONG -> FormatStyle.LONG
DateStyle.MEDIUM -> FormatStyle.MEDIUM
DateStyle.SHORT -> FormatStyle.SHORT
}

@ -18,4 +18,14 @@
<string name="drawer_tags">Tags</string> <string name="drawer_tags">Tags</string>
<string name="drawer_places">Places</string> <string name="drawer_places">Places</string>
<string name="drawer_local_lists">Local lists</string> <string name="drawer_local_lists">Local lists</string>
<string name="tmrw">Tmrw</string>
<string name="tomorrow_abbrev_lowercase">tmrw</string>
<string name="tomorrow">Tomorrow</string>
<string name="tomorrow_lowercase">tomorrow</string>
<string name="yest">Yest</string>
<string name="yesterday_abbrev_lowercase">yest</string>
<string name="yesterday">Yesterday</string>
<string name="yesterday_lowercase">yesterday</string>
<string name="today">Today</string>
<string name="today_lowercase">today</string>
</resources> </resources>

@ -4,6 +4,8 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.TextStyle
expect fun formatNumber(number: Int): String expect fun formatNumber(number: Int): String
@ -15,3 +17,11 @@ fun createDataStore(producePath: () -> String): DataStore<Preferences> =
) )
const val dataStoreFileName = "tasks.preferences_pb" const val dataStoreFileName = "tasks.preferences_pb"
expect fun formatDate(timestamp: Long, style: DateStyle): String
expect fun formatDateTime(timestamp: Long, style: DateStyle): String
expect fun formatDayOfWeek(timestamp: Long, style: TextStyle): String
expect fun formatDateTime(timestamp: Long, format: String): String

@ -0,0 +1,5 @@
package org.tasks.kmp.org.tasks.time
enum class DateStyle {
FULL, LONG, MEDIUM, SHORT
}

@ -0,0 +1,138 @@
package org.tasks.kmp.org.tasks.time
import org.jetbrains.compose.resources.getString
import org.tasks.data.entity.Task.Companion.hasDueTime
import org.tasks.kmp.formatDate
import org.tasks.kmp.formatDateTime
import org.tasks.kmp.formatDayOfWeek
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.ONE_DAY
import org.tasks.time.minuteOfHour
import org.tasks.time.plusDays
import org.tasks.time.startOfDay
import org.tasks.time.year
import tasks.kmp.generated.resources.Res
import tasks.kmp.generated.resources.tmrw
import tasks.kmp.generated.resources.today
import tasks.kmp.generated.resources.today_lowercase
import tasks.kmp.generated.resources.tomorrow
import tasks.kmp.generated.resources.tomorrow_abbrev_lowercase
import tasks.kmp.generated.resources.tomorrow_lowercase
import tasks.kmp.generated.resources.yest
import tasks.kmp.generated.resources.yesterday
import tasks.kmp.generated.resources.yesterday_abbrev_lowercase
import tasks.kmp.generated.resources.yesterday_lowercase
import kotlin.math.abs
fun getTimeString(date: Long, is24HourFormat: Boolean): String =
formatDateTime(
timestamp = date,
format = when {
is24HourFormat -> "HH:mm"
date.minuteOfHour == 0 -> "h a"
else -> "h:mm a"
}
)
suspend fun getRelativeDateTime(
date: Long,
is24HourFormat: Boolean,
style: DateStyle = DateStyle.MEDIUM,
alwaysDisplayFullDate: Boolean = false,
lowercase: Boolean = false
): String {
if (alwaysDisplayFullDate || !isWithinSixDays(date)) {
return if (hasDueTime(date))
getFullDateTime(date, style)
else
getFullDate(date, style)
}
val day = getRelativeDay(date, isAbbreviated(style), lowercase)
return if (hasDueTime(date)) {
val time = getTimeString(date, is24HourFormat)
if (currentTimeMillis().startOfDay() == date.startOfDay())
time
else
String.format("%s %s", day, time)
} else {
day
}
}
suspend fun getRelativeDay(
date: Long,
style: DateStyle = DateStyle.MEDIUM,
alwaysDisplayFullDate: Boolean = false,
lowercase: Boolean = false,
): String =
if (alwaysDisplayFullDate || !isWithinSixDays(date)) {
getFullDate(date, style)
} else {
getRelativeDay(date, isAbbreviated(style), lowercase)
}
fun getFullDate(
date: Long,
style: DateStyle = DateStyle.LONG,
): String = stripYear(formatDate(date, style), currentTimeMillis().year)
fun getFullDateTime(
date: Long,
style: DateStyle = DateStyle.LONG,
): String = stripYear(formatDateTime(date, style), currentTimeMillis().year)
private fun isAbbreviated(style: DateStyle): Boolean =
style == DateStyle.SHORT || style == DateStyle.MEDIUM
private fun stripYear(date: String, year: Int): String =
date.replace("(?: de |, |/| )?$year(?:年|년 | г\\.)?".toRegex(), "")
private suspend fun getRelativeDay(
date: Long,
abbreviated: Boolean,
lowercase: Boolean
): String {
val startOfToday = currentTimeMillis().startOfDay()
val startOfDate = date.startOfDay()
if (startOfToday == startOfDate) {
return getString(if (lowercase) Res.string.today_lowercase else Res.string.today)
}
if (startOfToday.plusDays(1) == startOfDate) {
return getString(
if (abbreviated) {
if (lowercase) Res.string.tomorrow_abbrev_lowercase else Res.string.tmrw
} else {
if (lowercase) Res.string.tomorrow_lowercase else Res.string.tomorrow
}
)
}
if (startOfDate.plusDays(1) == startOfToday) {
return getString(
when {
abbreviated ->
if (lowercase) Res.string.yesterday_abbrev_lowercase else Res.string.yest
lowercase ->
Res.string.yesterday_lowercase
else ->
Res.string.yesterday
}
)
}
return formatDayOfWeek(
timestamp = date,
style = if (abbreviated) TextStyle.SHORT else TextStyle.FULL
)
}
private fun isWithinSixDays(date: Long): Boolean {
val startOfToday = currentTimeMillis().startOfDay()
val startOfDate = date.startOfDay()
return abs((startOfToday - startOfDate).toDouble()) <= ONE_DAY * 6
}

@ -7,6 +7,7 @@ import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.atTime import kotlinx.datetime.atTime
import kotlinx.datetime.minus import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
@ -95,6 +96,18 @@ fun Long.withMillisOfDay(millisOfDay: Int): Long =
0 0
} }
fun Long.plusDays(days: Int): Long =
if (this > 0) {
with (toLocalDateTime()) {
date
.plus(days, DateTimeUnit.DAY)
.atTime(time)
.toEpochMilliseconds()
}
} else {
0
}
fun Long.minusDays(days: Int): Long = fun Long.minusDays(days: Int): Long =
if (this > 0) { if (this > 0) {
with (toLocalDateTime()) { with (toLocalDateTime()) {
@ -124,6 +137,12 @@ private fun Long.minus(value: Int, units: DateTimeUnit.TimeBased): Long =
val Long.millisOfDay: Int val Long.millisOfDay: Int
get() = if (this > 0) toLocalDateTime().time.toMillisecondOfDay() else 0 get() = if (this > 0) toLocalDateTime().time.toMillisecondOfDay() else 0
val Long.minuteOfHour: Int
get() = if (this > 0) toLocalDateTime().minute else 0
val Long.year: Int
get() = if (this > 0) toLocalDateTime().year else 0
private fun Long.toLocalDateTime(): LocalDateTime = private fun Long.toLocalDateTime(): LocalDateTime =
Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault()) Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.currentSystemDefault())

@ -0,0 +1,5 @@
package org.tasks.kmp.org.tasks.time
enum class TextStyle {
FULL, SHORT, NARROW
}

@ -1,5 +1,41 @@
package org.tasks.kmp package org.tasks.kmp
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.TextStyle
import org.tasks.kmp.org.tasks.time.toFormatStyle
import org.tasks.kmp.org.tasks.time.toLocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
actual fun formatNumber(number: Int) = number.toString() actual fun formatNumber(number: Int) = number.toString()
actual val IS_DEBUG = true actual val IS_DEBUG = true
actual fun formatDate(timestamp: Long, style: DateStyle): String =
DateTimeFormatter
.ofLocalizedDate(style.toFormatStyle())
.withLocale(Locale.getDefault())
.format(timestamp.toLocalDateTime().toLocalDate())
actual fun formatDateTime(timestamp: Long, style: DateStyle): String =
DateTimeFormatter
.ofLocalizedDateTime(style.toFormatStyle(), FormatStyle.SHORT)
.withLocale(Locale.getDefault())
.format(timestamp.toLocalDateTime())
actual fun formatDayOfWeek(timestamp: Long, style: TextStyle): String =
timestamp
.toLocalDateTime()
.dayOfWeek
.getDisplayName(
when (style) {
TextStyle.FULL -> java.time.format.TextStyle.FULL
TextStyle.SHORT -> java.time.format.TextStyle.SHORT
TextStyle.NARROW -> java.time.format.TextStyle.NARROW
},
Locale.getDefault()
)
actual fun formatDateTime(timestamp: Long, format: String): String =
timestamp.toLocalDateTime().format(DateTimeFormatter.ofPattern(format))

@ -0,0 +1,16 @@
package org.tasks.kmp.org.tasks.time
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.FormatStyle
fun Long.toLocalDateTime(): LocalDateTime =
LocalDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
fun DateStyle.toFormatStyle() = when (this) {
DateStyle.FULL -> FormatStyle.FULL
DateStyle.LONG -> FormatStyle.LONG
DateStyle.MEDIUM -> FormatStyle.MEDIUM
DateStyle.SHORT -> FormatStyle.SHORT
}
Loading…
Cancel
Save