Question

Consistent time zone for date comparison

I am looking to pass in a date String and check if this date is before the current MST time.

I want to be able to reliably test this locally and when I deploy it.

Locally I am based in the UK. And when I deploy it, server is in Phoenix.

The time now in UK is 2024-06-28 15.45.00 and this logic produces true for isValidDate at present when I pass in 2024-06-28 15.00.00.

But I am setting the zone to MST. I was expecting this to be false.
MST time is like 8am now. So it's not before. It seems to continue to work against UK time.

How can I update this so that when I deploy it, it will check the date string against MST time. And locally continue to run for MST too? Essentially if I end up in another server in Australia, logic should continue to work against MST time.

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss");
private static final ZoneId MST_TIMEZONE = ZoneId.of("America/Phoenix");

// Spring bean set to Clock.systemDefaultZone(); Can't change this.
// using a clock for unit testing purposes.
private final Clock clock;

private ZonedDateTime parseDate(String dateStr) {
    try {
        return LocalDateTime
                .parse(dateStr, DATE_FORMATTER)
                .atZone(MST_TIMEZONE);
    } catch (DateTimeParseException e) {
        return null;
    }
}

private boolean isValidDate(String startDateTime) {
    ZonedDateTime start = parseDate(startDateTime);        
    return start != null
            && start.isBefore(LocalDateTime.now(clock).atZone(MST_TIMEZONE));
}
 5  115  5
1 Jan 1970

Solution

 3

I think the problem you've got here is with this:

LocalDateTime.now(clock).atZone(MST_TIMEZONE)

This will do different things depending on the time zone of the JVM you are running it in.

LocalDateTime.now(clock) will give you the local time in the JVM's timezone - since we're both in London, let's say that 2024-06-28 16:46:23. Invoking atZone(MST) on that gives you a ZonedDateTime which is 2024-06-28 16:46:23 -08:00.

If you had run that on a server in Phoenix, LocalDateTime.now(clock) would have got 2024-06-28 08:46:23; invoking atZone(MST) on that gives you 2024-06-28 08:46:23 -08:00.

If your intention is to get the current time in MST_TIMEZONE, change it to:

clock.now().atZone(MST_TIMEZONE)

clock.now() gives you an Instant, which is time zone-agnostic type. The Instant corresponding to the time I wrote above is Instant.ofSeconds(1719593183L). Converting that to a ZonedDateTime gives the LocalDateTime in that zone, plus the zone.

2024-06-28
Andy Turner

Solution

 1

tl;dr

LocalDateTime                                              // Represent a date with time-of-day but lacking the context of an offset-from-UTC or a time zone. Does *not* represent a moment, is *not* a point on the timeline.
.parse(                                                    
    "2024-06-28 15.00.00" ,                                // Best to *not* invent custom formats for such textual inputs. Better to exchange date-time values textually using only ISO 8601 standard formats. 
    DateTimeFormatter.ofPattern( "uuuu-MM-dd HH.mm.ss" )
)                                                          // Returns a `LocalDateTime` object.
.atZone( ZoneId.of( "America/Phoenix" ) )                  // Returns a `ZonedDateTime` object. Determines a moment, a point on the timeline, by applying the context of a time zone. 
.toInstant()                                               // Returns a `Instant` object. Same point on the timeline, but seen through an offset-from-UTC of zero hours-minutes-seconds.
.isBefore( Instant.now() )                                 // Compares to the current moment as seen in UTC (an offset of zero). 

UTC is your friend

Programmers and sysadmins should learn to think and work in UTC, date-time moments as seen with an offset of zero hours-minutes-seconds from the temporal meridian of UTC. Regard UTC as the One True Time. All other time zones are but mere variations.

So we redefine your problem as two phases:

  • We receive a text input of a date with time-of-day. Unfortunately,this input has two flaws. (A) The text is not in standard ISO 8601 format. (B) The text represents a moment as seen in the wall-clock/calendar of a particular time zone (America/Phoenix) but that crucial fact is not included within the input text.
  • We must compare the moment represented by this text input to the current moment. We want to know if the intended moment is past or future.

ISO 8601

The ideal solution to the flawed input would educating the publisher of that data about the ISO 8601 standard. The input 2024-06-28 15.00.00 shared instead be 2024-06-28T15:00:00-07:00.

OffsetDateTime

The java.time classes use ISO 8601 formats by default. So such a standard string can be directly parsed without needing to define any formatting pattern. We get an OffsetDateTime object.

OffsetDateTime odt = OffsetDateTime.parse( "2024-06-28T15:00:00-07:00" ) ;

Correcting the data published to use ISO 8601 standard formats really is the ideal solution. ISO 8601 was invented expressly for exchanging date-time values textually in way that works well for machines, and works well for humans across cultures.

LocalDateTime

If fixing the inputs is not possible, then we must parse the input using a class that represents a date with time-of-day but lacks the context of an offset or time zone. That class would be LocalDateTime.

DateTimeFormatter f = DateTimeFormatter.ofPattern( "uuuu-MM-dd HH.mm.ss" ) ;
LocalDateTime ldt = LocalDateTime.parse( "2024-06-28 15.00.00" , f ) ;

Be aware that `LocalDateTime does not represent a moment, is not a point on the timeline.

ZonedDateTime

We have been assured that our text input for date and time is meant to be seen though the time zone of America/Phoenix. So assign that zone to create a ZonedDateTime object.

ZoneId z = ZoneId.of( "America/Phoenix" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ;

Instant

Now we have a moment, a point on the timeline. Adjust from that zone to UTC, our One True Time.

The easiest way to adjust to UTC is to extract an Instant object. The Instant class is the building block of the java.time framework. It represents a moment as seen in UTC, always in UTC.

Instant instant = zdt.toInstant() ;

Capture the current moment.

Instant now = Instant.now() ; 

Compare.

Boolean isPast = instant.isBefore( now ) ;

Focus on UTC

check if this date is before the current MST time.

No. Change your thinking to focus on UTC.

So, we need to check if the passed moment is before the current moment.

Pseudo time zones

MST

“MST” is not a real time zone. That text is a pseudo zone which merely hints at a real zone, and indicates whether Daylight Saving Time (DST) is observed on that date and time. Such pseudo zones are not standardized; they are not even unique!

Use only real time zones in your discussion and your logic. The pseudo zones should only be used in generated localized text for display to the user.

Real time zones have names in the format of Continent/Region.

Never call LocalDateTime.now

LocalDateTime.now(

I cannot imagine a scenario where calling LocalDateTime.now is the correct or optimal thing to do. "now" means you intend a specific point on the timeline. The LocalDateTime class cannot represent a specific point on the timeline.

Set servers to UTC

Essentially if I end up in another server in Australia

Servers should have their default time zone set the UTC.

Also, keep mind that a server can have multiple default time zones. These include a default in the OS, a default in any database server, and a default within the JVM.

You should always write your code in such a way as to avoid depending on external factors such as the host OS’ current default time zone. You do not want your code to break just because of some sys-admin changing a time zone setting. The key is to always specify explicitly a desired/expected time zone; always pass an optional time zone or offset argument rather than rely implicitly on default.

Notice how the example code in this Answer is impervious to changes in the OS’ default zone.

2024-06-28
Basil Bourque

Solution

 0

You need to ensure that all time-related operations are done using the MST timezone. This can be achieved by setting the appropriate ZoneId to your Clock object and performing all time comparisons in that zone.

Here's an updated version of your code that makes sure the date string is parsed and compared correctly against MST time, regardless of where it is run:

 private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss");
 private static final ZoneId MST_TIMEZONE = ZoneId.of("America/Phoenix");

 private final Clock clock;

 public DateValidator(Clock clock) {
     this.clock = clock;
 }

 private ZonedDateTime parseDate(String dateStr) {
     try {
         return LocalDateTime
                 .parse(dateStr, DATE_FORMATTER)
                 .atZone(MST_TIMEZONE);
     } catch (DateTimeParseException e) {
         return null;
     }
 }

 public boolean isValidDate(String startDateTime) {
     ZonedDateTime start = parseDate(startDateTime);
     if (start == null) {
         return false;
     }

     // Get the current time in MST
     ZonedDateTime nowInMST = ZonedDateTime.now(clock.withZone(MST_TIMEZONE));
     return start.isBefore(nowInMST);
 }

 public static void main(String[] args) {
     Clock clock = Clock.systemDefaultZone();
     DateValidator validator = new DateValidator(clock);
      // Testing with a date string
      String dateStr = "2024-06-28 15.00.00";
      boolean isValid = validator.isValidDate(dateStr);
      System.out.println("Is valid date: " + isValid);
  }

hope it helps

2024-06-28
Matt

Solution

 0

I recommend a more general solution for a SpringBoot application

  1. Create a Bean that is configured from a parameter in your application.yaml that defines ZonedId. The bean calls something like ZoneId.of(zoneId)
  2. Create a second Clock Bean that is configured from that ZoneId bean

Then in any Service class you need a clock you inject clock and you can be sure it has the right Zone. You can then use it like this: ZonedDateTime.now(clock)

And when you want to write a Unit Test that depends on the Clock, you can used a "Fixed Clock" e.g. Clock.fixed(Instant.parse("2021-01-01T08:20:50Z"), ZoneOffset.UTC) which gives you predictable and testable clock/zone values in your tests.

Here is how to do (1) & (2) in Kotlin; yes I know your question is Java, but I am sure you can figure out the idea from this:

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
import java.time.ZoneId

@Configuration
class ClockConfig {
    @Bean
    fun zoneId(@Value("\${zone-id}") zoneId: String): ZoneId {
        return ZoneId.of(zoneId)
    }

    @Bean
    fun clock(zoneId: ZoneId): Clock {
        return Clock.system(zoneId)
    }
}
2024-06-28
AndrewL