How to map offset integer to IANA timezone

Viewed 120

Using a system that stores timezone information for historical object as follows:

o1:    dt= 2022-07-15 13:32:00  offset=5   dst=false    
o2:    dt= 2022-01-15 13:32:00  offset=5   dst=true     
o3:    dt= 2022-07-15 13:32:00  offset=7   dst=false    

Life would be much easier if it stored IANA timezones like "America/New York" but this is what I have to deal with. My end goal is to compare these historical dates to the current time and get a time difference between them.

Creating current time is easy. I can get it in UTC

   ZonedDateTime.now(ZoneId.of("UTC"))
   2022-07-15T15:33:04.177Z[UTC]

Or my local timezone

ZonedDateTime.now()
2022-07-15T11:33:28.648-04:00[America/New_York]

The question is, how can I read the table data into a ZonedDateTime for an apples to apples comparison. For example some magical method like

ZonedDateTime.fromOffset(5).fromDst(false).asTz('UTC')

3 Answers

You can use ZoneOffset with the specified offset, the DST true/false doesn't seem relevant here. Assuming the timestamps are shown are local (and not UTC), you can do something like:

public static void main(String[] args) {
    record TimestampAndOffset(String timestamp, int offsetHours) { }
    
    var timeData = List.of(
            new TimestampAndOffset("2022-07-15T13:32:00", 5),
            new TimestampAndOffset("2022-01-15T13:32:00", 5),
            new TimestampAndOffset("2022-07-15T13:32:00", 7));

    timeData.forEach(to -> {
        LocalDateTime localTimestamp = LocalDateTime.parse(to.timestamp());
        var zoneOffset = ZoneOffset.ofHours(to.offsetHours());
        var zonedValue = ZonedDateTime.of(localTimestamp, zoneOffset);
        System.out.println(zonedValue);
        System.out.println(zonedValue.withZoneSameInstant(ZoneOffset.UTC));
    });
}

To simplify parsing for this example, I use ISO-8601 format with T as the separator.

Output:

2022-07-15T13:32+05:00
2022-07-15T08:32Z
2022-01-15T13:32+05:00
2022-01-15T08:32Z
2022-07-15T13:32+07:00
2022-07-15T06:32Z

Important caveat as pointed out by user16320675 in the comments, double check the meaning of your offsets, as based on the original version of your question, your data might be using an inverse offset (e.g. 5 instead of -5), in which case you need to use ZoneOffset.ofHours(-to.offsetHours()) instead.

As mentioned in the comments by Basil Bourque, consider if using OffsetDateTime makes more sense for your use case.

@Mark Rotteveel answered this question. I had to adapt his answer a bit to fit my exact needs and the constraint of being on Java 8 so am posting my exact solution in case useful.

LocalDateTime localDt = LocalDateTime.parse("2022-07-15T13:32:00");
// IF DST MAKE THIS 4
ZoneOffset offset = ZoneOffset.ofHours(-5);
OffsetDateTime offsetDt = localDt.atOffset(offset);

// Not sure the canonical way to do a duration check but this seems to work
// Duration d = Duration.between( offsetDt , Instant.now());
Instant now = Instant.now();
boolean isFutureDate = offsetDt.toInstant().getEpochSecond() > now.getEpochSecond();
log.info("Original: {}, OffsetDt:{}, Now: {}, isFutureDate: {}" , localDt, offsetDt, now, isFutureDate);

Which outputs

Original: 2022-07-15T13:32, OffsetDt:2022-07-15T13:32-05:00, Now: 2022-07-18T16:28:48.384Z, isFutureDate: false

tl;dr

You said:

My end goal is to compare these historical dates to the current time and get a time difference between them.

record History( String dt , int offset , boolean dst ) { }
Instant now = Instant.now();
List.of(
                new History( "2022-07-15 13:32:00" , 5 , false ) ,
                new History( "2022-01-15 13:32:00" , 5 , true ) ,
                new History( "2022-07-15 13:32:00" , 7 , false )
        )
        .forEach(
                history -> System.out.println(
                        Duration.between(
                                LocalDateTime
                                        .parse( 
                                                history.dt().replace( " " , "T" ) 
                                        )
                                        .atOffset(
                                                ZoneOffset.ofHours( history.offset() )
                                        )
                                        .toInstant() ,
                                now
                        )
                )
        );

PT17H4M40.423919S

PT4361H4M40.423919S

PT19H4M40.423919S

OffsetDateTime

The other two Answers by Mark Rotteveel and by Adam Hughes are both basically correct. But they, like the Question, use ZonedDateTime rather than the more appropriate class, OffsetDateTime. And they do not address the specific parsing needs asked in the Question. And apparently the input data is already in objects, so I will represent that as records.

public record History( String dt , int offset , boolean dst ) { }

Populate some data.

List < History > historyList =
        List.of(
                new History( "2022-07-15 13:32:00" , 5 , false ) ,
                new History( "2022-01-15 13:32:00" , 5 , true ) ,
                new History( "2022-07-15 13:32:00" , 7 , false )
        );

We need to produce a java.time.OffsetDateTime object for each of those inputs. We can add a method to our record to perform that work.

To parse the given inputs, we can either define a DateTimeFormatter with a formatting pattern to match, or we can do string manipulation to bring the inputs into compliance with the ISO 8601 standard format used by default in OffsetDateTime.parse. I choose the latter. We simply need to replace the SPACE in the middle with a T.

As Rotteveel mentioned in his Answer, the boolean for whether Daylight Saving Time (DST) is in effect or not is irrelevant. We ignore that member field in our History record.

public record History( String dt , int offset , boolean dst )
{
    OffsetDateTime toOffsetDateTime ( )
    {
        LocalDateTime ldt = LocalDateTime.parse( this.dt.replace( " " , "T" ) );
        ZoneOffset o = ZoneOffset.ofHours( this.offset );
        OffsetDateTime odt = ldt.atOffset( o );
        return odt;
    }
}

Capture the current moment.

Instant now = Instant.now() ;

Your goal is to determine elapsed time. Use Duration for that. We can add another method to History record for that purpose.

package work.basil.example.history;

import java.time.*;

public record History( String dt , int offset , boolean dst )
{
    OffsetDateTime toOffsetDateTime ( )
    {
        LocalDateTime ldt = LocalDateTime.parse( this.dt.replace( " " , "T" ) );
        ZoneOffset o = ZoneOffset.ofHours( this.offset );
        OffsetDateTime odt = ldt.atOffset( o );
        return odt;
    }

    Duration elapsed ( final Instant instant )
    {
        Duration d = Duration.between( this.toOffsetDateTime().toInstant() , instant );
        return d;
    }
}

Loop the inputs to report their elapsed time.

Instant now = Instant.now();
for ( History history : historyList )
{
    String message = history.elapsed( now ).toString() + " from " + history.toString() + " to " + now.toString();
    System.out.println( message );
}

When run.

PT16H27M40.119075S from History[dt=2022-07-15 13:32:00, offset=5, dst=false] to 2022-07-16T00:59:40.119075Z
PT4360H27M40.119075S from History[dt=2022-01-15 13:32:00, offset=5, dst=true] to 2022-07-16T00:59:40.119075Z
PT18H27M40.119075S from History[dt=2022-07-15 13:32:00, offset=7, dst=false] to 2022-07-16T00:59:40.119075Z
Related