1

I'm suffering using TimeZone and need help.

Context : I have 2 offices, 1 in Paris, 1 in New York. I need to execute some tasks for both. My only server is located in Paris. Tasks have to be executed at 9:00 AM local times.

So, at 9:00 Paris Time, my process will run and do all the stuff for Paris office.

At 15:00 Paris Time (or 3:00 PM), process will run again to achieve New York tasks, because when it's 3:00 PM at Paris, it's 9:00 AM in New York.

I tried things like the following, but with mitigated success :

Entity e1 = Entity.getEntityInstance("Paris", TimeZone.getTimeZone("Europe/Paris"));
Entity e2 = Entity.getEntityInstance("NewYork", TimeZone.getTimeZone("America/New_York"));

    Date now = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat("ddMMyyyy");
    sdf.setTimeZone(e1.getTimeZone());
    sdf.format(now);

    System.out.printf("Date à %s = %s", e1.getName(), sdf.getCalendar());

    sdf.setTimeZone(e2.getTimeZone());
    sdf.format(now);

    System.out.printf("Date à %s = %s", e2.getName(), sdf.getCalendar());

The result I have is something like this :

Date à Paris = java.util.GregorianCalendar[time=1563224081926,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Paris",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=184,lastRule=java.util.SimpleTimeZone[id=Europe/Paris,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2019,MONTH=6,WEEK_OF_YEAR=29,WEEK_OF_MONTH=3,DAY_OF_MONTH=15,DAY_OF_YEAR=196,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=3,AM_PM=1,HOUR=10,**HOUR_OF_DAY=22,MINUTE=54,SECOND=41**,MILLISECOND=926,ZONE_OFFSET=3600000,DST_OFFSET=3600000]
Date à NewYork = java.util.GregorianCalendar[time=1563224081926,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/New_York",offset=-18000000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/New_York,offset=-18000000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2019,MONTH=6,WEEK_OF_YEAR=29,WEEK_OF_MONTH=3,DAY_OF_MONTH=15,DAY_OF_YEAR=196,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=3,AM_PM=1,HOUR=4,**HOUR_OF_DAY=16,MINUTE=54,SECOND=41**,MILLISECOND=926,ZONE_OFFSET=-18000000,DST_OFFSET=3600000]

I effectively can see differences in HOUR_OF_DAY, but what should I do to have the same result with a non-deprecated object like DateTime (Joda time) or anything else ?


EDIT

I tried what I found here :

ZonedDateTime zdt = ZonedDateTime.now( ZoneId.of( "America/Montreal" ) );
…
Instant instant = zdt.toInstant();

I thought it could be the solution to my problem, because in fact, what I want is to obtain 2 Date objects representing the same Instant but in 2 different places.

What I did is :

ZonedDateTime zdt1 = ZonedDateTime.now( ZoneId.of(e1.getTimeZone().getID()));
ZonedDateTime zdt2 = ZonedDateTime.now( ZoneId.of(e2.getTimeZone().getID()));
System.out.println("ZDT : " + zdt1);
System.out.println("ZDT : " + zdt2);

The result was engaging :

ZDT : 2019-07-16T01:23:29.344+02:00[Europe/Paris]
ZDT : 2019-07-15T19:23:29.346-04:00[America/New_York]

But when I tried to convert them in Instant then in Date, the result was just the same :

Instant i1 = zdt1.toInstant();
Instant i2 = zdt2.toInstant();
System.out.println(i1);
System.out.println(i2);
Date dd1 = Date.from(i1);
Date dd2 = Date.from(i2);
System.out.println(dd1);
System.out.println(dd2);

In the console :

Instant 1 = 2019-07-15T23:23:29.344Z
Instant 2 = 2019-07-15T23:23:29.346Z
Date 1 = Tue Jul 16 01:23:29 CEST 2019
Date 2 = Tue Jul 16 01:23:29 CEST 2019

I really don't understand how 2 different ZonedDateTimes can produce same Instants and Dates...

4
  • 5
    Have a look at the ZonedDateTime type (introduced in Java 1.8). By the way, you should consider using UTC all the time; it will let you handle these sort of things seamlessly and in a consistent way. Commented Jul 15, 2019 at 21:22
  • 2
    The new Java 8 date/time classes help here. Just use Instant instead of Date everywhere, and DateTimeFormatter instead of SimpleDateFormat. Otherwise, you're doing the right thing. Commented Jul 15, 2019 at 22:21
  • Regarding, "...when it's 3:00 PM at Paris, it's 9:00 AM in New York". Nope - not always. Sometimes it's 10:00 AM in New York. The US and the EU do not follow the same DST schedules. Commented Jul 15, 2019 at 23:51
  • Yes that's true. It's the reason to use TimeZone Commented Jul 16, 2019 at 0:08

1 Answer 1

3

Avoid legacy classes

You are using terrible date-time classes that were supplanted years ago by the modern java.time classes defined in JSR 310.

Keep servers in UTC

The default time zone (and locale) of your server should not impact your code. The settings there are out of your control as a programmer. So rather than rely on those defaults, always specify the desired/expected time zone (and locale) by passing optional argument to various methods.

Tip: Best practice is to generally keep your servers in UTC as their default time zone. Indeed, most of your business logic, logging, data exchange, and data storage should be done in UTC.

java.time

LocalTime

Each location has a target time-of-day.

LocalTime targetTimeOfDayParis = LocalTime.of( 9 , 0 ) ;  // 09:00.
LocalTime targetTimeOfDayNewYork = LocalTime.of( 9 , 0 ) ;  // 09:00.

Instant.now

Get the current moment in UTC.

Instant now = Instant.now() ;  // No need for time zone here. `Instant` is always in UTC, by definition.

ZoneId

When is that moment today? First define our time zones.

ZoneId zParis = ZoneId.of( "Europe/Paris" ) ;
ZoneId zNewYork = ZoneId.of( "America/New_York" ) ;

Instant::atZone

Then adjust from UTC in the Instant to the very same moment as seen through the wall-clock time employed by the people of a particular region (a time zone). That adjusted moment is represented by the ZonedDateTime class.

Note that this is not changing the moment. We have the same point on the timeline on both. Only the wall-clock time is different. Like two people on long-distance phone call can simultaneously look up at the clock on their respective wall and see a different time-of-day (and date!) at the very same moment.

ZonedDateTime zdtParis = now.atZone( zParis ) ;
ZonedDateTime zdtNewYork = now.atZone( zNewYork ) ;

ZonedDateTime::with

Change the time-of-day to our target. The ZonedDateTime::with method will generate a new ZonedDateTime object with values based on the original excepting for the values you pass. Here we pass a LocalTime to change from the hour & minute & second & fractional-second of the original.

The is the key part where we diverge for Paris versus New York. Nine in the morning in Paris is a very different moment than 9 AM in New York, several hours apart.

ZonedDateTime zdtParisTarget = zdtParis.with( targetTimeOfDayParis ) ;
ZonedDateTime zdtNewYorkTarget = zdtNewYork.with( targetTimeOfDayNewYork ) ;

Compare with isBefore

Did we miss 9 AM in either zone yet?

boolean pastParis = zdtParisTarget.isBefore( zdtParis ) ;
boolean pastNewYork = zdtNewYorkTarget.isBefore( zdtNewYork ) ;

If past, then add a day.

if( pastParis ) {
    zdtParisTarget = zdtParisTarget.plusDays( 1 ) ;
}

if( pastNewYork ) {
    zdtNewYorkTarget = zdtNewYorkTarget.plusDays( 1 ) ;
}

Duration

Calculate elapsed time using Duration class.

Duration durationParis = Duration.between( now , zdtParisTarget.toInstant() ) ;
Duration durationNewYork = Duration.between( now , zdtNewYorkTarget.toInstant() ) ;

Sanity-check that the durations are in the future, not the past.

if( durationParis.isNegative() ) { … handle error … } 
if( durationNewYork.isNegative() ) { … handle error … } 

Ditto for zero.

if( durationParis.isZero() ) { … handle error … } 
if( durationNewYork.isZero() ) { … handle error … } 

In real work, I would check to see if the duration is extremely tiny. If so brief that the code below may take longer than span of time, add some arbitrary amount of time. I'll leave that as an exercise for the reader.

Schedule work to be done

Prepare the work to be done.

Runnable runnableParis = () -> taskParis.work() ;
Runnable runnableNewYork = () -> taskNewYork.work() ;

Or you could write a utility that takes an ZoneId argument, looks up the work to be done, and returns a Runnable.

Schedule the work to be done. See the Oracle Tutorial on the Executor framework. This framework greatly simplifies scheduling work to be done on a background thread.

We schedule a task to be run after a certain number of minutes, seconds, or any such granularity has elapsed. We specify a number and a granularity with the TimeUnit enum. We can extract the number from our Duration calculated above.

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool( 1 );

ScheduledFuture<?> futureParis = scheduler.schedule( runnableParis , durationParis.toSeconds() , TimeUnit.SECONDS ) ;
ScheduledFuture<?> futureNewYork = scheduler.schedule( runnableNewYork , durationNewYork.toSeconds() , TimeUnit.SECONDS ) ;

The future object lets you check on progress status. You may not need it at all.

IMPORTANT: Be sure to gracefully shutdown your ScheduledExecutorService when no longer needed, such as when your app is ending.

Caveat: I have not tried any of this code. But it should get you headed into the right direction.

Note the repetition. There is no need to hard-code the city name in all those variables. You could write this logic once in a method that takes the ZoneId as an argument, and the time-of-day too if there is any chance it would vary by city. Then call that method for any number of zones.

List< ZoneId > zones = 
    List.of( 
            ZoneId.of( "Europe/Paris" ) , 
            ZoneId.of( "America/New_York" ) , 
            ZoneId.of( "Asia/Kolkata" ) 
    ) 
;
for( ZoneId z : zones ) {
    workManager.scheduleForZone( z ) ;
}
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks @basile. I will test that in exactly 1h, just enough time to be back at work. I come back to you.
It's perfect @basile-bourque. I'm absolutely discovering this Date/Time API and your explanations are fine. Another bonus question : Instant is perfect to set a timeline usefull to compare 'now' with any targeted Time or Date. Is there a way to use a Date as a timeline instead of Instant ? This point is quiet important because in my database is stored (will be stored) the 'last execution date" of my process which is running many times a day. In consequence, I will have to detect if the saved date and time are related to the today's last execution or day before today's one.
@Lovegiver The Instant class replaces the java.util.Date class. The old class has new conversion methods if you must interface with old code not yet updated you java.time. For database column of the data type akin to standard-SQL TIMESTAMP WITH TIME ZONE you should be using Java class OffsetDateTime with JDBC 4.2 or later. All this has been covered many times already on Stack Overflow. So search to learn more.
Your precious advices help much. After a moment for understanding I did the job. For anyone who may read this thread, notice that persistence layer have some troubles as SQL Datetime doesn't accept Java LocaleDateTime. Workarounds based on Converter can be found on the Web but it's easier with JPA 2.1. Again great thanx for help.
@Lovegiver Caution: Never use LocalDateTime when trying to represent a moment, a specific point on the timeline. A moment requires the context of a time zone or offset-from-UTC, but LocalDateTime purposely lacks both. A LocalDateTime is nothing but a date and a time-of-day, such as noon on the 23rd of January next year — but we don't know if that means noon in Tokyo, noon in Paris, or noon in Montréal, all very different moments several hours apart.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.