Hello. As I understood, there is a problem with adequate date classes in Java. So, I need to calculate a time offset between two LocalDateTimes. I’m doing it simple:
val dur = Duration.between(dateUtcStart, localDateTime)
But how can I convert this duration to a normalized string like +10:30 or -01:30? There is a KotlinDuration class but I can’t recognize if its methods help.
I also have a dirty manual method:
val tsLocalDateTime = Timestamp.valueOf(localDateTime).time
val dif = tsLocalDateTime - startTs
val tsOffset = LocalTime.ofInstant(Instant.ofEpochMilli(dif), ZoneOffset.systemDefault())
val tzString = if (tsOffset.toSecondOfDay() > 0) "+$tsOffset" else tsOffset.toString()
But it doesn’t work with negative dates (tsOffset.toSecondOfDay() is always > 0).
I think Java time API lacks a duration formatter, but still it is pretty easy to format Duration manually, using methods like toHoursPart(), toMinutesPart(). Did you try this?
This works ok with a positive value but need to add modifications for a negative one. And also I need to add the second digit if the hours or minutes contain only one. So, I’m curious: is there no default method for it in whole Java/Kotlin?
The current code is:
val offset = if (min >= 0) "+${dur.toHoursPart()}:${dur.toMinutesPart()}"
else "${dur.toHoursPart()}:${-dur.toMinutesPart()}"
P.S. This is my original code, so I need to improve it and make work correctly:
fun parseDateTime(startTs: Long, endTs: Long, localizedDateTime: String): Pair<String, String> {
val dateUtcStart = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTs), ZoneOffset.UTC)
val dateUtcEnd = LocalDateTime.ofInstant(Instant.ofEpochMilli(endTs), ZoneOffset.UTC)
val formatter = DateTimeFormatterBuilder()
.parseCaseInsensitive()
.parseDefaulting(ChronoField.YEAR_OF_ERA, dateUtcStart.year.toLong())
.append(DateTimeFormatter.ofPattern("E dd MMM hh:mm a"))
.toFormatter(Locale.ENGLISH)
val localDateTime = LocalDateTime.parse(localizedDateTime, formatter)
val localTs = Timestamp.valueOf(localDateTime).time
val tsOffset = LocalTime.ofInstant(Instant.ofEpochMilli(localTs - startTs), ZoneOffset.systemDefault())
val tzString = if (tsOffset.toSecondOfDay() > 0) "+$tsOffset" else tsOffset.toString()
val startDate = dateUtcStart.toString() + tzString
val endDate = dateUtcEnd.toString() + tzString
return Pair(startDate, endDate)
}
@Test
fun parseDateTime() {
val pair1 = parseDateTime(1626998400000, 1627005600000, "Fri 23 Jul 10:30 am")
val pair2 = parseDateTime(1626998400000, 1627005600000, "Thu 22 Jul 11:30 pm")
// pass
assertEquals("2021-07-23T00:00+10:30", pair1.first)
assertEquals("2021-07-23T02:00+10:30", pair1.second)
// fails
assertEquals("2021-07-23T00:00-00:30", pair2.first)
assertEquals("2021-07-23T02:00-00:30", pair2.second)
}
Where startTs - start of an event timestamp. endTs - end of this event timestamp. localizedDateTime is used to show the start time in the actual time zone (real city) while timestamps show time in UTC. I need to extract this timezone from localizedDateTime and add it to start and end string dateTimes (start = "2021-07-23T00:00+10:30", end = "2021-07-23T02:00+10:30")
If I understand correctly, startTs and localizedDateTime represent exactly the same event timestamp, but in different time zones - respectively UTC and some time zone, that’s local to the event, and your main interest is to retrieve the offset of the event-local time zone, is that right?
If so, it seems that you may be interested in using ZoneOffset and OffsetDateTime
See your example adapted to use them. It seems to follow the formatting you want:
import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import java.util.*
//sampleStart
fun parseDateTime(startTs: Long, endTs: Long, localizedDateTime: String): Pair<String, String> {
val dateUtcStart = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTs), ZoneOffset.UTC)
val dateUtcEnd = LocalDateTime.ofInstant(Instant.ofEpochMilli(endTs), ZoneOffset.UTC)
val formatter = DateTimeFormatterBuilder()
.parseCaseInsensitive()
.parseDefaulting(ChronoField.YEAR_OF_ERA, dateUtcStart.year.toLong())
.append(DateTimeFormatter.ofPattern("E dd MMM hh:mm a"))
.toFormatter(Locale.ENGLISH)
val localDateTime = LocalDateTime.parse(localizedDateTime, formatter)
val duration = Duration.between(dateUtcStart, localDateTime)
val offset = ZoneOffset.ofTotalSeconds(duration.seconds.toInt())
val startWithOffset = dateUtcStart.atOffset(offset)
val endWithOffset = dateUtcEnd.atOffset(offset)
return Pair(startWithOffset.toString(), endWithOffset.toString())
}
fun main() {
val pair1 = parseDateTime(1626998400000, 1627005600000, "Fri 23 Jul 10:30 am")
val pair2 = parseDateTime(1626998400000, 1627005600000, "Thu 22 Jul 11:30 pm")
println(pair1)
println(pair2)
}
//sampleEnd
Java certainly did have problems. Java 1.0’s Date class was basically a toy, and while Java 1.1’s additional Calendar and related classes went some way to provide localisation, time zones, conversions, parsing/formatting, and the like, they’re still very clunky, limited, and fragile compared with a modern date API.
But 7 years ago, Java 8 added the java.time classes (inspired by JodaTime), which are much more flexible, useful, and well-thought-out. (Once you get the hang of them and understand some of the subtle distinctions, anyway. The wide use of immutability makes them harder to optimise for some high-throughput systems — but that’s a niche problem, and adds safety.)
As I understood, there is a problem with adequate date classes in Java.
I meant there is no single class in Java for convenient work with dates and time. There was Date and most of it is deprecated, and now there are multiple classes like LocalDate, LocalDateTime, Calendar, TimeZone, ZoneOffset, Duration, etc. that could be combined better.
So, maybe I need to make a proposal in Kotlin design.
There are multiple classes. because dates are pretty complicated thing. We had simple Date class in Java for many, many years and for most Java developers it was like: “DO NOT USE THIS!” long before it has been deprecated.
There is no single class to work with dates/times, because there is no single type of date/time!
You might have a moment or 24-hour period in some specific place (known or unknown), or a space or moment on some calendar, or a single moment in time, or various other things. They may look very similar, but they all behave differently — so if a date/time API is going to handle them properly, it needs to know exactly what you mean. Otherwise you get into All Sorts Of Trouble the moment summer time starts or ends in your locale, or in some other locale that you deal with, or you talk to some database that’s expecting your UTC-based timestamp to be in the server’s time-zone, or have to look at historical data for a region whose rules about summer time changed recently, or deal with intervals that include leap seconds, or…
There are manyawkwardcornercasesaboutdatesandtimes, and java.time does a far better job than previous APIs of steering you away from them and doing what you expect. You just have to know which of its classes you should be using at each point. Yes, it’s a little complex, but most of that is inherent complexity because dates and times are complex — a simpler API is just hiding that under the carpet, from where it will come back to bite you, with nasty big pointy teeth.