jsmtslch
jsmtslch

Reputation: 718

Java Date Time Start and End of day without any TimeZone information

I have two strings as startDate and endDate. These have values like for example:

startDate="2019-09-29T06:00:00.000Z"
endDate="2019-10-06T05:59:59.999Z"

Now let us say that these are in Mountain Time as the start of the day for startDate and End of the day for endDate. But as these are Strings, how can I make the program understand that these are can be converted to Mountain local time like:

startDate="2019-09-29T00:00:00.000Z"
endDate="2019-10-05T11:59:59.999Z"

And the timezone is dynamic. I can get timestamp string from Eastern Time or any other timezone.
UPDATE
I realized I should put in more information.
So the Moutain time I have put in is an example. It can be from Eastern Time or any other timezone. The only fact that this method knows is that startDate is the start of the day in some timezone and endDate is the end of the day in that timezone. I understand that 'Z' is the UTC time but finding out which timezone generated that UTC time at the start of the day (startDate) and end of the day(endDate) and converting back to the corresponding local time, is the challenge that I am facing.

Upvotes: 0

Views: 3177

Answers (2)

Anonymous
Anonymous

Reputation: 86276

I couldn’t keep my fingers off this one, it’s fun. As far as I understood, you don’t know from which time zone the start and end strings come, it could be any time zone. What can we do except try all of them in turn?

    String startDate = "2019-09-29T06:00:00.000Z";
    String endDate = "2019-10-06T05:59:59.999Z";

    LocalTime dayStart = LocalTime.MIN;
    LocalTime dayEndEarliset = LocalTime.of(23, 59, 59);

    Instant startInstant = Instant.parse(startDate);
    Instant endInstant = Instant.parse(endDate);
    // Just out of curiosity find candidate time zones
    Set<ZoneId> candidateZones = ZoneId.getAvailableZoneIds()
            .stream()
            .map(ZoneId::of)
            .filter(zid -> startInstant.atZone(zid).toLocalTime().equals(dayStart)
                    && ! endInstant.atZone(zid).toLocalTime().isBefore(dayEndEarliset))
            .collect(Collectors.toSet());
    System.out.println("Potential time zones: " + candidateZones);
    // Real work: find candidate date(s)
    Set<LocalDate> candidateDates = ZoneId.getAvailableZoneIds()
            .stream()
            .map(ZoneId::of)
            .filter(zid -> startInstant.atZone(zid).toLocalTime().equals(dayStart)
                    && ! endInstant.atZone(zid).toLocalTime().isBefore(dayEndEarliset))
            .map(zid -> startInstant.atZone(zid).toLocalDate())
            .collect(Collectors.toSet());
    if (candidateDates.isEmpty()) {
        System.out.println("Cannot identify a date from the instants");
    } else if (candidateDates.size() > 1) {
        System.out.println("Ambiguous date, candidates are " + candidateDates);
    } else {
        System.out.println("The date is " + candidateDates.iterator().next());
    }

This prints:

Potential time zones: [America/Inuvik, America/Yellowknife, America/Regina, America/Boise, SystemV/MST7MDT, America/El_Salvador, America/Costa_Rica, America/Shiprock, America/Guatemala, America/Denver, America/Belize, America/Swift_Current, America/Managua, Mexico/BajaSur, Canada/Mountain, America/Cambridge_Bay, Navajo, America/Chihuahua, America/Ojinaga, MST7MDT, Pacific/Galapagos, America/Mazatlan, US/Mountain, America/Edmonton, America/Tegucigalpa, Canada/Saskatchewan, Etc/GMT+6, SystemV/CST6]
The date is 2019-09-29

And yes, America/Denver is among the candidate time zones as expected. So far I have found the start date. You can find the pair of start date and end date in a similar manner. Possibly you’ll prefer an old-fashioned loop over the stream operation.

However, the date will not always be unambiguous. Let’s try constructing an interval of one day in Pacific/Pago_Pago time zone. It’s at offset -11:00.

    ZoneId zone = ZoneId.of("Pacific/Pago_Pago");
    LocalDate testDate = LocalDate.of(2019, Month.OCTOBER, 2);
    String startDate = testDate.atStartOfDay(zone).toInstant().toString();
    String endDate = testDate.atTime(LocalTime.of(23, 59, 59, 999_000_000))
            .atZone(zone)
            .toInstant()
            .toString();

Now the output from the program is:

Potential time zones: [Antarctica/McMurdo, Pacific/Niue, Pacific/Samoa, Pacific/Tongatapu, Pacific/Enderbury, Etc/GMT+11, NZ, Antarctica/South_Pole, Etc/GMT-13, Pacific/Pago_Pago, Pacific/Midway, Pacific/Fakaofo, US/Samoa, Pacific/Auckland]
Ambiguous date, candidates are [2019-10-03, 2019-10-02]

This is because there are also time zones with offset +13:00. We can tell that the offset must be either -11:00 or +13:00, but we cannot tell which of the two. So two dates come out.

Use half-open intervals for time

I wasn’t sure whether the end would always be at 23:59:59.999 in the time zone in question, or sometimes perhaps 23:59:59.999999 or something else. Also no matter how many decimals we add, there will still theoretically exist moments that are after that end time but still within the same date. In the code I conservatively accept anything from 23:59:59 and later as end time. The point to avoid the doubt completely is: Use a half-open interval. Represent the start of the interval as 00:00:00 as you already do, and the end as 00:00:00 in the date after the last date. And treat the start date as inclusive and the end as exclusive.

Upvotes: 0

Andreas
Andreas

Reputation: 159096

The Z at the end is time zone information, meaning offset 00:00 from UTC, aka Zero, aka Zulu, so first you parse the string into a type that stores date, time, and the Z timezone.

With Java 8 Time API, that would be Instant, OffsetDateTime, or ZonedDateTime.
If your inputs always ends with a Z, not some other offset, use Instant.

You then convert to the desired time zone, which for US Mountain time is called America/Denver.

Examples

String startDate = "2019-09-29T06:00:00.000Z";
String endDate = "2019-10-06T05:59:59.999Z";
ZoneId zone = ZoneId.of("America/Denver");

System.out.println(Instant.parse(startDate).atZone(zone));
System.out.println(OffsetDateTime.parse(startDate).atZoneSameInstant(zone));
System.out.println(ZonedDateTime.parse(startDate).withZoneSameInstant(zone));

System.out.println(Instant.parse(endDate).atZone(zone));
System.out.println(OffsetDateTime.parse(endDate).atZoneSameInstant(zone));
System.out.println(ZonedDateTime.parse(endDate).withZoneSameInstant(zone));

Output

2019-09-29T00:00-06:00[America/Denver]
2019-09-29T00:00-06:00[America/Denver]
2019-09-29T00:00-06:00[America/Denver]
2019-10-05T23:59:59.999-06:00[America/Denver]
2019-10-05T23:59:59.999-06:00[America/Denver]
2019-10-05T23:59:59.999-06:00[America/Denver]

If you don't want to retain the time zone after conversion, you can remove it by calling toLocalDateTime(), e.g.

System.out.println(Instant.parse(endDate).atZone(zone).toLocalDateTime());

Output

2019-10-05T23:59:59.999

Notice how it doesn't have a Z at the end.

Upvotes: 4

Related Questions