Kotlin / kotlinx-datetime

KotlinX multiplatform date/time library
Apache License 2.0
2.44k stars 101 forks source link

Previous/next date and/or time with the given form #325

Open dkhalanskyjb opened 12 months ago

dkhalanskyjb commented 12 months ago

"I have a date or a time, and I want to round/adjust it."

Examples:

volkert-fastned commented 9 months ago

One practical use case for such functionality: the OCPP specification. It mandates that ISO date/time strings don't have more than 3 decimal points:

number of decimal places SHALL NOT exceed the maximum of 3.

In other words: no more than millisecond precision.

While working with kotlinx-datetime, we actually ran into compatibility problems with vendors that will flat out reject the nanosecond-precise ISO date/time strings that kotlinx.datetime.Instant gets serialized to by kotlinx.serialization.

We could use JSR-310 (java.time) instead, so we could solve it with truncateTo, or we can write a helper function as suggested on StackOverflow, but I was kind of hoping that there would be an equally convenient built-in multiplatform solution for this.

volkert-fastned commented 9 months ago

Update:

I found a single-line workaround for forcibly reducing the precision to millisecond-level in serialized ISO date/time strings, at least for timestamps originating from the system clock:

import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

// Will have 6 digits behind the decimal point on Kotlin/JVM, but 3 digits on Kotlin/JS and Kotlin/Native
println(Clock.System.now())

// Will have 3 digits behind the decimal point on Kotlin/JVM, Kotlin/JS and Kotlin/Native
println(Instant.fromEpochMilliseconds(Clock.System.now().toEpochMilliseconds()))

So Kotlin/JVM appears to appears to be the only Kotlin platform that creates Instants from the system clock at nanosecond precision, although I haven't tried it with Kotlin for Android yet.

dkhalanskyjb commented 9 months ago

@volkert-fastned, since your problem is with system interoperability, the upcoming API for parsing and formatting may solve it: https://github.com/Kotlin/kotlinx-datetime/pull/343

alghe-global commented 6 days ago

I've the use case where I want to check whether a day (LocalDate) is in current week or current month. I don't think there's support for this yet, and I think this issue would help - correct me if I'm wrong. Is there any chance this will be implemented soon?

LE: here's the equivalent code using Java API (in Kotlin) of what I'm trying to do

fun isCurrentDay(timestamp: Long): Boolean {
    val instant = Instant.ofEpochMilli(timestamp)
    val currentLocalDate = LocalDate.now(ZoneId.systemDefault())
    val timestampLocalDate = instant.atZone(ZoneId.systemDefault()).toLocalDate()
    return currentLocalDate == timestampLocalDate
}

fun isCurrentWeek(timestamp: Long): Boolean {
    val instant = Instant.ofEpochMilli(timestamp)
    val currentLocalDate = LocalDate.now(ZoneId.systemDefault())
    val timestampLocalDate = instant.atZone(ZoneId.systemDefault()).toLocalDate()

    val currentWeekStart = currentLocalDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
    val currentWeekEnd = currentWeekStart.plusDays(6)

    return timestampLocalDate in currentWeekStart..currentWeekEnd
}

fun isCurrentMonth(timestamp: Long): Boolean {
    val instant = Instant.ofEpochMilli(timestamp)
    val currentLocalDate = LocalDate.now(ZoneId.systemDefault())
    val timestampLocalDate = instant.atZone(ZoneId.systemDefault()).toLocalDate()

    val currentMonthStart = currentLocalDate.with(TemporalAdjusters.firstDayOfMonth())
    val currentMonthEnd = currentMonthStart.plusDays(
        (currentLocalDate.lengthOfMonth() - 1).toLong()
    )

    return timestampLocalDate in currentMonthStart..currentMonthEnd
}
dkhalanskyjb commented 4 days ago

@alghe-global, here are some kotlinx-datetime implementations:

import kotlinx.datetime.*

fun LocalDate.isSameDay(timestamp: Long): Boolean =
    this == timestampToDateInSystemTimeZone(timestamp)

fun LocalDate.isSameWeek(timestamp: Long): Boolean =
    previousOrSame(DayOfWeek.MONDAY) ==
        timestampToDateInSystemTimeZone(timestamp).previousOrSame(DayOfWeek.MONDAY)

fun LocalDate.isSameMonth(timestamp: Long): Boolean {
    val timestampLocalDate = timestampToDateInSystemTimeZone(timestamp)
    return year == timestampLocalDate.year && month == timestampLocalDate.month
}
/*
// After https://github.com/Kotlin/kotlinx-datetime/pull/457:
fun LocalDate.isSameMonth(timestamp: Long): Boolean =
    yearMonth == timestampToDateInSystemTimeZone(timestamp).yearMonth
*/

private fun timestampToDateInSystemTimeZone(timestampMillis: Long) =
    Instant.fromEpochMilliseconds(timestampMillis).toLocalDateTime(TimeZone.currentSystemDefault()).date

// https://github.com/Kotlin/kotlinx-datetime/issues/129#issuecomment-1152301045
private fun LocalDate.previousOrSame(requiredDayOfWeek: DayOfWeek): LocalDate =
    minus((dayOfWeek.isoDayNumber - requiredDayOfWeek.isoDayNumber).mod(7), DateTimeUnit.DAY)

The "current week" use case would indeed benefit from temporal-adjuster-style functionality, but as a workaround, you could copy this implementation of previousOrSame to your code.