Christopher Schultz
Christopher Schultz

Reputation: 20837

Am I using java.time correctly, here? It seems like the old API would be easier

I haven't used the java.time API very much because mostly my work involves capturing, storing, and displaying dates and times without manipulating them in any way. But occasionally, I have to do something like:

I've also decided that, based upon other unrelated experiences, humans tend to consider "30 days in the past" to actually mean "the start of the day 30 days in the past" not just "now minus 30*24*60*60*1000 milliseconds in the past" which is super-easy to do with just System.currentTimeMillis and arithmetic.

So my requirement is:

If I were to do this with the old API, I would do this:

TimeZone zone = ...; // However I get my time zone
Locale locale = ...; // However I get my locale
Calendar now = Calendar.getInstance(zone, locale);
// Reset to midnight
now.set(Calendar.HOUR, 0);
now.set(Calendar.MINUTE, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MILLISECOND, 0);
// Now go back 30 days
now.add(Calendar.DAY, -30);
// done

Aside from the repeated calls to blank-out the time part of the Calendar, it's pretty straightforward. It uses only 3 classes, and two of them are the TimeZone and Locale classes, only one of which is really necessary (TimeZone).

This time around, I decided to use java.time because, well, it's "better." I really don't know my way around it, so I did a lot of internet searching to figure out how to do various things, and I finally came up with the code below. Isn't this supposed to be "easier?"

TimeZone zone = ...; // However I get my time zone
ZoneId zid = zone.toZoneId(); // Convert to new kind of time zone
ZonedDateTime date = ZonedDateTime.now(zid);
date = date.minus(Period.ofDays(30)); // Rewind 30 days
date = date.truncatedTo(ChronoUnit.DAYS);
Instant instant = date.toInstant(); // Convert to Instant for comparisons

Now I'm using 6 different classes. Okay, 2 of them are constant-type things so we can ignore them. I have to use Instant because all the dates I have to use for comparison are java.util.Date, and Instant is the class that bridges all the gaps.

Is the problem that this is a "simple problem" and java.time is intended to solve problems that are far more complicated?

Upvotes: 1

Views: 710

Answers (3)

Basil Bourque
Basil Bourque

Reputation: 338266

tl;dr

ZoneId zTokyo = ZoneId.of( "Asia/Tokyo" ) ;        // Date varies around the globe by time zone.
LocalDate todayTokyo = LocalDate.now( zTokyo ) ;   // Capture the current date as seen in a particular time zone.
ZonedDateTime firstMomentOfDateInTokyo = todayTokyo.atStartOfDay( zTokyo ) ;  // Determine the first moment of that date in that zone.
Instant firstMomentOfDateInTokyoAsSeenInUtc = firstMomentOfDateInTokyo.toInstant() ;

See this code run live at Ideone.com.

2022-05-24

2022-05-24T00:00+09:00[Asia/Tokyo]

2022-05-23T15:00:00Z

Details

Date only

As shown in correct Answer by Thilo, if you want the current date without a time-of-day and without a time zone or offset-from-UTC, use LocalDate.

LocalDate today = LocalDate.now() ;

The now method implicitly uses your JVM's current default time zone to determine the date. Be aware that for any given moment, the date varies around the globe by time zone. Right now it is "tomorrow" in Tokyo Japan while simultaneously "yesterday" in Toledo Ohio US.

Best to specify your desired/expected time zone.

ZoneId zTokyo = ZoneId.of( "Asia/Tokyo" ) ;
LocalDate todayTokyo = LocalDate.now( zTokyo ) ;

Date with time-of-day & zone

If you want the first moment of the day on that date as seen in that time zone, use ZonedDateTime class. Do not assume the day starts at 00:00. Some dates in some time zones may start at another time such as 01:00. Let java.time determine the first moment.

ZonedDateTime firstMomentOfDateInTokyo = todayTokyo.atStartOfDay( zTokyo ) ;

If you want to see what that moment simultaneously looks like with an offset of zero hours-minutes-seconds from UTC, extract an Instant.

Instant firstMomentOfDateInTokyoAsSeenInUtc = firstMomentOfDateInTokyo.toInstant() ;

Avoid legacy classes

You said:

Instant is the class that bridges all the gaps.

You should not be mixing the legacy date-time classes with the modern java.time classes. The old classes are terrible, bloody awful, a masterclass in how not to do object-oriented programming.

If you must interoperate with old code not yet updated for java.time, convert to-and-fro using new to…/from…/valueOf conversion methods added to the old classes. But do not mix legacy & modern within your logic.

Is the problem that this is a "simple problem" and java.time is intended to solve problems that are far more complicated?

No, java.time is appropriate to all business and personal oriented date-time work (perhaps not all scientific or academic work). The legacy classes are a disaster that should be avoided entirely.

The java.time classes are actually utterly simple in the entities they represent. You just need to get real clear on what your needs are, what goals are you trying to accomplish.

  • Represent a moment as seen with an offset of zero hours-minutes-seconds: Instant
  • Represent a date, just a date, no time, no zone, and therefore inherently ambiguous: LocalDate
  • Represent a moment as seen in a particular time zone: ZonedDateTime.
  • Represent a moment as seen in a particular offset from UTC: OffsetDateTime. This class maps to TIMESTAMP WITH TIME ZONE in standard SQL.
  • Represent a date with time of day but lacking the context of a time zone or offset and therefore ambiguous within a range of about 26-27 hours: LocalDateTime. This class maps to TIMESTAMP WITHOUT TIME ZONE in standard SQL.

The legacy classes have equivalents for only two of those: (a) java.util.Date is replaced by Instant, and (b) java.util.Calendar (well, actually java.util.GregorianCalendar) is replaced by ZonedDateTime. The legacy classes neglected to represent a date-only, a date with offset, and a date and time without zone/offset. The java.sql.Date class pretends to represent a date-only, but does not. And java.sql.Timestamp, well, I never could figure out what that beast was trying to do.

FYI, an offset is merely a number of hours-minutes-seconds ahead or behind UTC. A time zone is much more. A time zone is a named history of the past, present, and future changes to the offset used by the people of a particular region, as decided by their politicians.

Upvotes: 2

MikeFHay
MikeFHay

Reputation: 8983

The purpose of the multiple different classes in java.time is exactly to model many of the things you mentioned in your question.

humans tend to consider "30 days in the past" to actually mean "the start of the day 30 days in the past"

Right, you don't care about the time, so you don't need a DateTime, just a LocalDate.

LocalDate prev = LocalDate.now().minusDays(30);

If you have to convert to an Instant, then you need to specify a timezone.

Instant asInstant = prev.atStartOfDay​(zone).toInstant();

However if you don't care about the time then you probably shouldn't be converting to Instant, just stick with LocalDate.

I have to use Instant because all the dates I have to use for comparison are java.util.Date

Date includes a time part. Are you truncating those to the start of day as well? One way to do that and get back a LocalDate is:

date.toInstant().atZone(zone).toLocalDate();

Upvotes: 6

Thilo
Thilo

Reputation: 262474

Get the date 30 days in the past

sounds like

java.time.LocalDate.now().minusDays(30)

Upvotes: 8

Related Questions