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...

Lovegiver
  • 413
  • 6
  • 22
  • 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. – x80486 Jul 15 '19 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. – Dawood ibn Kareem Jul 15 '19 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. – Matt Johnson-Pint Jul 15 '19 at 23:51
  • Yes that's true. It's the reason to use TimeZone – Lovegiver Jul 16 '19 at 00:08

1 Answers1

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 ) ;
}
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Thanks @basile. I will test that in exactly 1h, just enough time to be back at work. I come back to you. – Lovegiver Jul 16 '19 at 04:30
  • 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 Jul 16 '19 at 08:01
  • @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. – Basil Bourque Jul 16 '19 at 15:21
  • 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 Jul 16 '19 at 18:56
  • @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. – Basil Bourque Jul 16 '19 at 21:09
  • @BasilBourque your like the timezone guru, everytime I have to look up something related to Java 8 Time classes, your answers pop up. Great work man, they are really helpful! – Rvb84 Dec 31 '22 at 10:45