DateUtils#truncate() to work in UTC timezone

3.1k views Asked by At

I have a java.util.Date on which i am using org.apache.commons.lang.time#truncate(javaUtilDateObj, Calendar.DATE) to convert the date to midnight.

The problem is the UTC date gets represented in local timezone and the truncate method just converts the Hour,minute and seconds to 00:00:00 without considering the timezone.

Example :- Say UTC time is 30 seconds from epoch. Wed Dec 31 19:00:30 EST 1969 is the Date object representation I am seeing. On calling DateUtils#truncate() method on the above date, the output is Wed Dec 31 00:00:00 EST 1969.

What i am expecting is if UTC time is 30 seconds from epoch. And if that can be represented/converted to Thu Jan 1 00:00:30 EST 1970, i can call DateUtils#truncate() method on that and expect Thu Jan 1 00:00:00 EST 1970.

Note: I am not in a position to use joda-time API and hence I am stuck with what i have.

4

There are 4 answers

1
djmj On

Following function is usefull if your data layer saves UTC times but your presentation layer has different timeZone and you need to truncate a Date for filtering.

/**
 * Truncates the given UTC date for the given TimeZone
 *
 * @param date UTC date
 * @param timeZone Target timezone
 * @param field Calendar field
 *
 * @return
 */
public static Date truncate(Date date, final TimeZone timeZone, final int field)
{
    int timeZoneOffset = timeZone.getOffset(date.getTime());

    // convert UTC date to target timeZone
    date = DateUtils.addToDate(date, Calendar.MILLISECOND, timeZoneOffset);

    // truncate in target TimeZone
    date = org.apache.commons.lang3.time.DateUtils.truncate(date, field);

    // convert back to UTC
    date = DateUtils.addToDate(date, Calendar.MILLISECOND, -timeZoneOffset);

    return date;
}

/**
 * Adds the given amount to the given Calendar field to the given date.
 *
 * @see Calendar
 *
 * @param date the date
 * @param field the calendar field to add to
 * @param amount the amount to add, may be negative
 *
 * @return
 */
public static Date addToDate(final Date date, final int field, final int amount)
{
    Objects.requireNonNull(date, "date must not be null");

    final Calendar c = Calendar.getInstance();
    c.setTime(date);
    c.add(field, amount);
    return c.getTime();
}
0
koral On
Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
utc.setTime(date);
Calendar midnight = DateUtils.truncate(utc, Calendar.HOUR_OF_DAY);
Date midnightDate = midnight.getTime();
1
Jean Logeart On

Use the Calendar version of truncate:

Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")).setTime(date);
Calendar midnight = DateUtils.truncate(utc, Calendar.HOUR_OF_DAY);
Date midnightDate = midnight.getTime();
0
Basil Bourque On

tl;dr

  • Use modern java.time classes. Built into Java.
  • The day may not start at 00:00:00 on some dates in some places.
    • Let java.time determine first moment of the day.
  • Avoid the ambiguous term “midnight”.
    • Think of “first moment of the day”.
    • Search Stack Overflow to learn about Half-Open spans of time.

First moment of the day:

ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;    // Specify the region whose wall-clock time you want to perceive “today”. Specify time zone with `Continent/Region`, never the 3-4 letter pseudo-zones such as `EST` or `IST`. 
LocalDate today = LocalDate.now( z ) ;          // Capture the current date as seen in a specific time zone.
ZonedDateTime zdt = today.atStartOfDay( z ) ;   // Let java.time determine first moment of the day.
Instant instant = zdt.toInstant() ;             // Adjust from time zone to UTC. Same moment, same point on the timeline, different wall-clock time.

Details

Many problems at play here.

Adjusting zones

if UTC time is 30 seconds from epoch. And if that can be represented/converted to Thu Jan 1 00:00:30 EST 1970,

No, wrong. You cannot convert. Those two date-time values do not represent the same moment. If by EST you meant a time zone such as America/New_York or America/Montreal, then those two values are several hours apart.

Instant thirtySecondsFromEpoch = Instant.EPOCH.plusSeconds( 30 ) ;

thirtySecondsFromEpoch.toString(): 1970-01-01T00:00:30Z

See that same moment in much of the east coast of North America.

ZoneId z = ZoneId.of( "America/New_York" ) ;
ZonedDateTime zdtAdjustedFromThirtySecondsFromEpoch = thirtySecondsFromEpoch.atZone( z ) ;

zdtAdjustedFromThirtySecondsFromEpoch.toString(): 1969-12-31T19:00:30-05:00[America/New_York]

Because most of the people on the east coast of North America use a wall-clock time that is five hours behind UTC, that same moment of 30 seconds past epoch is seen as half a minute past 7 PM the day before, the last day of 1969.

Try the same time-of-day, 30 seconds into the first day of 1970, but as seen in America/New_York time zone rather than in UTC. Now we are talking about a different moment entirely. Half a minute into a new day happens five hours later than a half a minute into new day in UTC.

ZonedDateTime zdt = ZonedDateTime.of( 1970 , 1 , 1 , 0 , 0 , 30 , 0 , z ) ;

zdt.toString(): 1970-01-01T00:00:30-05:00[America/New_York]

zdt.toInstant().toString(): 1970-01-01T05:00:30Z

Time zone

You ignore the crucial issue of time zone.

A time zone is crucial in determining a date. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

java.util.Date::toString

The toString method on Date has a well-intentioned but troublesome behavior of implicitly applying the JVM’s current default time zone while generating a string representing the Date object’s UTC value. Creates the false impression that this zone is present in the Date.

This is one of many reasons to avoid this awful class. Use java.time classes instead.

java.time

The modern approach uses the java.time classes that supplant the troublesome old legacy date-time classes such as Date.

If you have a Date in hand, convert to/from java.time.Instant via new methods added to the old class.

Instant instant = myJavaUtilDate.toInstant() ;

Avoid talking about “midnight” as that is an amorphous topic. Instead, focus on the first moment of the day, the start of the day.

Do not assume a day starts at 00:00:00. Anomalies such as Daylight Saving Time (DST) mean the day may start at another time-of-day such as 01:00:00. Let java.time determine the start of the day.

Determining the start of the day means determining the date. And determining the date requires a time zone, as mentioned above. So we need to adjust from your Instant in UTC to a ZonedDateTime in a specific time zone (ZoneId) .

ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;

Extract from that ZonedDateTime just the date-only portion.

LocalDate ld = zdt.toLocalDate() ;

Now ask for the first moment of the day in a specified time zone.

ZonedDateTime zdtStartOfDay = ld.atStartOfDay( z ) ;

Truncating

You can truncate various java.time objects. Look for a truncatedTo method where you pass a TemporalUnit object (likely a ChronoUnit) to specify the granularity you want in your result. No need for the Apache DateUtils for this purpose.

If truncating Instant you are always using UTC for the time zone logic.

Instant instantTrucToDay = Instant.now().truncatedTo( ChronoUnit.DAYS ) ;

More likely, you will want to truncate within the context a some time zone other than UTC as discussed above.

ZonedDateTime zdtTruncToDay = zdt.truncatedTo( ChronoUnit.DAYS ) ;

By the way, if your ultimate goal is to work with an entire date, a date-only without any time-of-day or zone, just stick to using LocalDate.

if UTC time is 30 seconds from epoch. And if that can be represented/converted to Thu Jan 1 00:00:30 EST 1970

Not sure what you meant here. But keep in mind that given 30 seconds after the epoch of 1970-01-01T00:00:00Z (1970-01-01T00:00:30Z), the same moment in the east coast of North America is several hours earlier. That means around 7 PM the previous day.

Instant instantThirtySecsAfterEpoch = Instant.EPOCH.plusSeconds( 30 ) ;

instantThirtySecsAfterEpoch.toString(): 1970-01-01T00:00:30Z

ZoneId z = ZoneId.of( "America/New_York" ) ;
ZonedDateTime zdt = instantThirtySecsAfterEpoch.atZone( z ) ;

zdt.toString(): 1969-12-31T19:00:30-05:00[America/New_York]


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.

With a JDBC driver complying with JDBC 4.2 or later, you may exchange java.time objects directly with your database. No need for strings or java.sql.* classes.

Where to obtain the java.time classes?

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.