简体   繁体   中英

Java Using TimeZone

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 ZonedDateTime s can produce same Instant s and Date s...

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 ) ;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM