JSR310 when to use which classes? - java-time

I would like to verify if my understanding of JSR310 classes use-cases are correct, below a diagram with classes, and how i see them fit:
Database (UTC)
||
\/
OffsetDateTime (used in persistence or transmission, databases and XML)
||
\/
Instant (used in business logic, for date time calculations)
||
\/
ZonedDateTime <-- ZoneId (used in presentation layer, requiring the client zoneId)
||
\/
LocalDateTime (used in presentation layer, obtained from ZonedDateTime)
||
\/
Front-end
My uncertainity its related with ZonedDateTime and LocalDateTime, since as far as i understand are both suitable for presentation layer, altough LocalDatetime does not have time-zone or offset.
Its the LocalDateTime what i should send to front-end, after ZonedDateTime has handled all DST conversions and anomalies?
Do i not send a ZonedDateTime to front-end since it has extra information like time-zone?
thanks in advance

JSR310 is the Java specification for the java.time package, which provides classes for date and time manipulation in Java. The main classes in the package are:
LocalDate: Represents a date without a time (year, month, and day)
LocalTime: Represents a time without a date (hours, minutes, seconds, and nanoseconds)
LocalDateTime: Represents a date and time without a time zone (combines LocalDate and LocalTime)
ZonedDateTime: Represents a date and time with a time zone
Instant: Represents a point in time (similar to java.util.Date)
You should use LocalDate when you only need to represent a date, LocalTime when you only need to represent a time, LocalDateTime when you need to represent both date and time but without timezone, ZonedDateTime when you need to represent both date and time with timezone and Instant when you need to represent a point in time.

Related

PostgreSQL TIMESTAMPTZ is not working with SpringBoot Java Query

The SpringBoot Query returns null while using TIMESTAMPTZ as the Datatype, but the Query works for other Datatypes like TIMESTAMP etc. My Date formats are like, "2022-07-24 10:11:29.452+00".
The DB screenshot is added below.
Also the date type is defined as follows
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "datem")
private Date datem;
The API calls the below code
Date start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").parse("2022-07-24 10:11:29.452+00");
Date end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").parse("2022-07-24 10:11:49.452+00");
List<MqttMessageParsed> sensor_data = messageParsedRepository.findByCreatedAtBetween(start, end);
The Query function is as follows
#Query("SELECT t FROM MqttMessageParsed t WHERE t.datem BETWEEN :startDate AND :endDate") List<MqttMessageParsed> findByCreatedAtBetween(#Param("startDate")Date start, #Param("endDate")Date end);
The API shoud return the data between the above start and end dates, but it is returning null now. Am i missing something?
Thanks
Avoid legacy classes
You are using terrible date-time classes that were years ago supplanted by the modern java.time classes. Avoid Date, SimpleDateFormat, and Timestamp.
java.time
For a column of a type akin to the SQL standard type TIMESTAMP WITH TIME ZONE, use the class OffsetDateTime in JDBC 4.2 and later.
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
Writing:
myPreparedStatement.setObject( … , odt);
Hibernate was years ago updated to support java.time. Ditto for Jakarta Persistence, formerly Java Persistence API (JPA).
ISO 8601
I suggest you educate the publisher of your inputs about the value of strict compliance with the ISO 8601 standard for date-time formats.
Replace SPACE in the middle with a T.
Use full offset with both hours and minutes, separated by a COLON character.
So this:
"2022-07-24 10:11:29.452+00"
… should be:
"2022-07-24T10:11:29.452+00:00"
… or alternatively use a Z as the suffix to indicate an offset of zero:
"2022-07-24T10:11:29.452Z"
If you cannot effect that change, then define a custom formatting pattern to parse that non-standard format. Use DateTimeFormatter class, as has been covered many times already on Stack Overflow.
By the way, know that the other data type, TIMESTAMP WITHOUT TIME ZONE cannot be used to record a moment, a specific point on the timeline. This type stores only a date and a time-of-day without the context of a time zone or offset-from-UTC. So, for example, given the date of last January 23rd at 12:00, we cannot know if that meant noon in Tokyo Japan, noon in Toulouse France, or noon in Toledo Ohio US — three different moments several hours apart.
If you need to track when something happened, always use TIMESTAMP WITH TIME ZONE. In the case of Postgres, any time zone or offset info supplied with an input is used to adjust to UTC (an offset of zero) and then discarded. The moment is always stored in UTC, in Postgres for this type. If you care about the original time zone, store that in a second column.

Is it possible to specify an ISO Date with a timezone (i.e. GMT offset) but without a time?

I'm trying to see if you can specify a date with a timezone in ISO but without also specifying a time.
This may seem odd to ask about having a timezone without actually having a time, but technically a date represents a range between two times... the 24-hour period spanning from midnight to midnight, and that 'midnight' has to be in a timezone.
In our case, we have an API that wants to say 'Filter things on-or-before date X and on-or-after date Y' and we want the user to specify 'April 9th' (in their time zone) for both to get all things that happen on that day.
Of course we solve this by adding a day to the first date, then changing it to a pure 'before', but the front-end is required to do that math. We can't do it on the backend because having to send a date with a time means we would be sending April 9th at midnight, then on the backend adding a day to that, but what if someone passed in 4pm?
We could fail the date if it has a non-midnight time, but then we're back to why pass it in the first place.
So again, can you have a date with a timezone but not have a time component?
To decode ISO8601 dates only with year-month-day and time zone set the appropriate formatOptions of the ISO8601DateFormatter
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withFullDate, .withTimeZone]
If by time zone you mean a UTC offset (as used with ISO 8601 dates with times), this is no problem. If by time zone you mean a true time zone with historic, present and known future offsets from UTC, including for example summer time/DST, like America/New_York or North American Eastern Time, then ISO 8601 does not support that, neither for dates with nor without time of day.
2020-04-25-04:00
This is perfectly valid ISO 8601 for April 25 this year at offset -04:00. So you may use it for representing the interval from 2020-04-25T00:00-04:00 (inclusive) to 2020-04-26T00:00-04:00 (exclusive). Which would then be equivalent to 2020-04-25T04:00Z to 2020-04-26T04:00Z (Z meaning UTC).
Java example code
I don’t know any Swift, so cannot tell you how to format or parse such a string in Swift. In Java formatting it is not bad. Example:
LocalDate date = LocalDate.of(2020, Month.APRIL, 25);
String isoOffsetDateString = date
.atStartOfDay(ZoneId.of("America/New_York"))
.format(DateTimeFormatter.ISO_OFFSET_DATE);
System.out.println(isoOffsetDateString);
Output:
2020-04-25-04:00
I am using Java’s built-in ISO_OFFSET_DATE formatter. The documentation informs us that this formnatter is:
The ISO date formatter that formats or parses a date with an offset,
such as '2011-12-03+01:00'.
Parsing the string and producing the start and end of the day takes a little more:
TemporalAccessor parsed
= DateTimeFormatter.ISO_OFFSET_DATE.parse(isoOffsetDateString);
Instant start = LocalDate.from(parsed)
.atStartOfDay(ZoneOffset.from(parsed))
.toInstant();
Instant end = start.plus(1, ChronoUnit.DAYS);
System.out.println("From " + start + " inclusive to " + end + " exclusive");
From 2020-04-25T04:00:00Z inclusive to 2020-04-26T04:00:00Z exclusive
I have opted to convert to Instant, the class for a moment in time independent of offset or time zone. Instants print in UTC, as the trailing Z on each says. In your Java code you may prefer not to do this conversion or to do a different conversion, all depending on circumstances.
Link
Documentation of DateTimeFormatter.ISO_OFFSET_DATE

How can I get the start and end time of a period?

I have an enum TimeFrame that has values like: Yesterday, LastWeek, NextMonth etc.
I'm trying to write a method that takes in a TimeFrame and returns the start and end dates of that period of time.
I looked into the new Java 8 Period class which can take a start time and end time but it doesn't seem there's any clean way to retrieve those values afterwards.
How could I return the start and end date at once cleanly without using a List (seems like the wrong data structure) or some ugly datetime arithmetic?
No, a Period doesn't record a start/end - it just represents the amount of time between them, in a calendrical sense (rather than the "precise amount of time" represented by a Duration).
It sounds like basically you should create your own class that has a start date and an end date. You can still use Period to implement that, but you'd have getStartDate() and getEndDate() returning LocalDate values. You shouldn't need to do any heavy date/time arithmetic yourself - just work out the start date and add the appropriate period, or the end date and subtract the appropriate period, depending on the time frame.
Time marches on
Be careful about passing around enum values for “yesterday”, “tomorrow”, and so on. That raises a couple of issues, time zone and midnight.
Dates and day-of-week only have meaning in the context of a time zone. For any given moment, the date varies around the globe. For example, a few minutes after midnight in Paris is still “yesterday” in Montréal. So when ever you intend “yesterday” and such, always specify the desired/expected time zone as well.
Each non-atomic command and line of code executes separately from the previous. Each execution takes a moment of time, however brief. At any of those moments midnight in the specified time zone may be rolling over into a new day. At that stroke of midnight, your relative-time flag such as “yesterday” suddenly takes on a whole new meaning. That meaning is likely different that was intended by the earlier code given that conditions (the date) were different when that code began.
So it makes more sense to me to be passing around Instant objects, or perhaps OffsetDateTime or ZonedDateTime objects. These date-time values are frozen, representing a specific moment on the timeline. Your earlier original code can verify the meaning of that value, check that the date is indeed a Friday or some such. After such verification, the value can be passed on to other code. Now you need not worry about strange occasional bugs occurring at midnight.
java.time
You don't really need to build a class or enum to express your intention of relative time such as “yesterday”. The java.time classes built into Java 8 and later have plain-reading methods for adding and subtracting days, weeks, months. These are basically one-liners, so just call these plus… and minus… methods directly.
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime yesterday = now.minusDays( 1 );
ZonedDateTime weekLater = now.plusWeeks( 1 );
That code is implicitly applying the JVM’s current default time zone. Better to specify.
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime now = ZonedDateTime.now( zoneId );
You may want the date-only without time-of-day and without time zone. Use LocalDate.
LocalDate weekPrior = now.toLocalDate().minusWeeks( 1 );
You may want to get first moment of the day. Not always the time of 00:00:00.0.
ZonedDateTime yesterdayStart = now.minusDays( 1 ).toLocalDate().atStartOfDay( zoneId );
If you want to represent the span of time defined by a pair of date-time values, look at the Interval class found in the ThreeTen-Extra project that extends the java.time framework. This class tracks a pair of Instant objects which are moments on the timeline in UTC. You can extract an Instant from your ZonedDateTime by calling toInstant.
Interval intervalYesterday = Interval.of( yesterdayStart.toInstant() , yesterdayStart.plusDays( 1 ).toInstant() );
To get from an Instant back to a zoned date-time, apply a ZoneId.
ZonedDateTime zdt = ZonedDateTime( instant , zoneId );
For a date-only interval, you'll need to build your own class that stores a pair of LocalDate objects. I would call it something like DateRange.
Tip: Search "Half-Open" to learn about the practice of tracking spans of time where beginning in inclusive while the ending is exclusive.

Why is org.joda.time.LocalDate a date without a timezone?

I think the name org.joda.time.LocalDate is kind of confusing. The documentation says:
LocalDate is an immutable datetime class representing a date without a time zone.
In comparison org.joda.time.DateTime says:
A DateTime calculates its fields with respect to a time zone.
I am always confusing those two so I hope somebody can tell me why those names are supposed to make sense.
Here is my intuition: A local date or time object would represent a point in time but with regards to a location. Hence it should contain the time zone information since the time zone also gives you some sort of location information. In any case you know a little more about somebodies location than without that time zone.
A date-time, at least as it sounds like, should only represent a date and a time. In this sense it is just the long value since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970.
So why is it actually the other way around? This naming bugs me every time I have something to do with time stamps. Can somebody explain?
Update:
Interesting link posted by a user: https://sourceforge.net/p/threeten/mailman/message/29962542/
Terminology
Your intuition is the opposite of the terminology used by both Joda-Time and java.time.
Both frameworks have a class named LocalDate to represent a date-only value without time-of-day and without time zone. The ”local“ means ”could be any locality, not any particular locality“.
The ”Local…“ classes are just a rough idea of a time. They are not on the timeline. They have no real meaning until you apply a time zone or offset-from-UTC to get actual moments on the timeline. When you see ”Local“ think: ”this value does not really make sense until we apply a time zone“.
For example, we say that Christmas is on 2016-12-25 this year. But the date is not the same around the world at any given moment. A new day dawns earlier in Paris than in Montréal for example. So to get the moment when Christmas starts in Paris, you must apply the Europe/Paris time zone and get the first moment of that day. That first moment will be represented by DateTime in Joda-Time and by ZonedDateTime (or OffsetDateTime) in java.time.
For example, in java.time:
LocalDate xmas2016 = LocalDate.of( 2016 , 12 , 25 );
ZonedDateTime xmas2016FirstMomentMontreal = xmas2016.atStartOfDay( ZoneId.of( "America/Montreal" ) );
java.time
In the java.time framework, a moment on the timeline in UTC is represented by the Instant class.
Instant now = Instant.now();
Apply a time zone, ZoneId, to get a ZonedDateTime.
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );
If all you have is an offset-from-UTC rather than a full time zone, than use OffsetDateTime rather than ZonedDateTime.
You can think of this way:
Instant + ZoneId -> ZonedDateTime
Instant + ZoneOffset ->OffsetDateTime
This information has been covered many hundreds of times already on StackOverflow. Please search to learn more and see many pieces of example code.

DST problems with storing Joda LocalDateTime in a PostgreSQL 'timestamp' column

In our app we're storing datetimes that belong to many different timezones.
We decided to use the Joda LocalDateTime type - so that the user always gets literally whatever they entered in the first place. This is exactly what we need.
Internally we know which timezone the user belongs to - so when they enter a datetime we do a check like this:
dateTimeZone.isLocalDateTimeGap(localDateTime)
If that datetime does not exist in their timezone (it's in the daylight-savings gap) we display an error message that the date is not correct, thus preventing incorrect datetimes from being stored in the DB.
For storing we're using a timestamp column. Problems start when the user-entered datetime exists in their timezone but does not exist in the database timezone (Europe/Berlin). E.g. when I store LocalDateTime 2015-03-29 02:30:00 from the Europe/London timezone (this is valid - in London the gap is between 01:00 and 02:00), PostgreSQL shifts the hour by 1 and saves it as 2015-03-29 03:30:00.
What to do? Is there a way to tell PostgreSQL not do anything regarding timezones and just store datetimes literally as Joda represents them? (other than storing them as strings ;))
In PostgreSQL 7.3 and higher, timestamp is equivalent to timestamp without time zone. That data type is not time zone aware. It stores only a date and time. If you are finding it shifted, then it might be related to the code or tools you are using to store or retrieve the data.
Note that before version 7.3, timestamp was equivalent to timestamp with timezone. This is mentioned in the first note-box in the documentation here.
Postgres offers two date-time types per the SQL standard. The standard barely touches on the topic unfortunately, so the behavior described here is specific to Postgres. Other databases may behave differently.
TIMESTAMP WITHOUT TIME ZONEStores just a date and a time-of-day. Any time zone or offset-from-UTC passed is ignored.
TIMESTAMP WITH TIME ZONEFirst adjusts the passed date+time using its passed zone/offset to get a value in UTC. The passed zone/offset is then discarded after the adjustment is made; if needed, you must store that original zone/offset information in a separate column yourself.
Be aware that TIMESTAMP WITHOUT TIME ZONE does not represent an actual moment, does not store a point on the timeline. Without the context of a zone or offset, it has no real meaning. It represents a range of possible moments over a span of about 26-27 hours. Good for problems such as storing a appointment far enough out in the future that the time zone rules may be changed before its arrival. Also good for problems such as “Christmas starts after midnight on December 25 this year”, where you mean a different moment in time in each zone with each zone westward arriving later and later in succession.
When recording actual moments, specific points on the timeline, use TIMESTAMP WITH TIME ZONE.
The modern approach in Java uses the java.time classes rather than either the Joda-Time library or the troublesome old legacy date-time classes bundled with the earliest versions of Java.
TIMESTAMP WITHOUT TIME ZONE
For TIMESTAMP WITHOUT TIME ZONE, the equivalent class in java.time is LocalDateTime for a date and time-of-day without any offset or zone.
As others pointed out, some tools may dynamically apply a time zone to the retrieved value in a misguided and confusing albeit well-intentioned anti-feature. The following Java code will retrieve your true date-time value sans zone/offset.
Requires a JDBC driver compliant with JDBC 4.2 or later to directly work with java.time types.
LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ; // Retrieving a `TIMESTAMP WITHOUT TIME ZONE` value.
To insert/update database:
myPreparedStatement.setObject( … , ldt ) ; // Inserting/updating a `TIMESTAMP WITHOUT TIME ZONE` column.
TIMESTAMP WITH TIME ZONE
Your discussion of time zones suggests you are concerned with actual moments on the timeline. So you should absolutely be using TIMESTAMP WITH TIME ZONE instead of TIMESTAMP WITHOUT TIME ZONE. You should not be messing about with Daylight Saving Time (DST) gaps and such. Let java.time and Postgres do that work for you, with much better code already written and tested.
To retrieve:
Instant instant = myResultSet.getObject( … , Instant.class ) ; // Retrieving a `TIMESTAMP WITH TIME ZONE` value in UTC.
ZonedDateTime zdt = instant.atZone( ZoneId.of( "Africa/Tunis" ) ) ; // Adjusting from a UTC value to a specific time zone.
To insert/update database:
myPreparedStatement.setObject( … , zdt ) ; // Inserting/updating a `TIMESTAMP WITH TIME ZONE` column.
To retrieve from database:
Instant instant = myResultSet.getObject( … , Instant.class ) ;
E.g. when I store LocalDateTime 2015-03-29 02:30:00 from the Europe/London timezone
No, no, no. Do not work this way. You are misusing the types of both Java and Postgres.
If the user entered 2015-03-29 02:30:00 intended to represent a moment in Europe/London time zone, then parse as a LocalDateTime and immediately apply a ZoneId to get a ZonedDateTime.
To parse, replace the SPACE in the middle with a T to comply with ISO 8601 standard formatting used by default in the java.time classes.
String input = "2015-03-29 02:30:00".replace( " " , "T" ) ;
LocalDateTime ldt = LocalDateTime.parse( input ) ;
ZoneId z = ZoneId.of( "Europe/London" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ;
To see that same moment in UTC, extract a Instant. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).
Instant instant = zdt.toInstant() ;
Pass the instant via JDBC for storage in the database in a TIMESTAMP WITH TIME ZONE.
myPreparedStatement.setObject( … , instant ) ;
Use objects, not strings
Note that all my code here is using java.time objects to exchange data with the database. Always use these objects rather than mere strings for exchanging date-time values.
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, and later
Built-in.
Part of the standard Java API with a bundled implementation.
Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
Much of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
The ThreeTenABP project adapts ThreeTen-Backport (mentioned above) for Android specifically.
See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.