1

In my application users can create repeating events, such as "Lunch every saturday 12.00".

Now I've come across a problem that I'm not sure how to be approached correctly. If the chain of events is large and with different time zones (winter and summer time), listing all of those events shows different times.

Dates are for example stored server side as:

2017-10-28T12:00:00.000+02:00

So when listing events client side it can look like this:

2017-10-21 12:00:00.000 (parsed from: 2017-10-21T12:00:00.000+02:00)
2017-10-28 12:00:00.000 (parsed from: 2017-10-28T12:00:00.000+02:00)
2017-11-04 11:00:00.000 (parsed from: 2017-11-04T12:00:00.000+02:00)
2017-11-11 11:00:00.000 (parsed from: 2017-11-11T12:00:00.000+02:00)

Between the 2nd and 3rd occasion, client changes to winter time and +01:00. Time is adjusted accordingly and users might believe that the event occasion suddenly starts and hour earlier even though it starts the same time.

I would like it that it always shows the event time (12:00) when parsed on client side, regardless of timezone. Another solution is to state that is shown with summer time / winter time if that information can be extracted with Joda time.

Lucas Arrefelt
  • 3,879
  • 5
  • 41
  • 71
  • you might just want to store the timestamp on the server and send that exactly to the client and then according to timezone decide what is displayed on the client – Lino Oct 27 '17 at 10:58
  • 1
    One alternative is to store the values as `LocalDateTime`, and then use `toDateTime(DateTimeZone)` when displaying to the user. –  Oct 27 '17 at 11:02
  • Why do you store +02:00 for all dates if the timezone is not +02:00, but something like Europe/Paris, which has rules for DST? – JB Nizet Oct 27 '17 at 11:04
  • @JBNizet All events (since repeating) were created when timezone was +02:00. Then it changed to +01:00 in the middle of the event chain – Lucas Arrefelt Oct 27 '17 at 11:07
  • That's my point. You using an offset (+02:00, which never changes), instead of use a timezone (Europe/Paris, which deals with DSTs correctly, and thus is equivalent to +02:00 in the summer, and +01:00 in the winter) – JB Nizet Oct 27 '17 at 11:09
  • @JBNizet Alright, then I understand your point. However, such information cant be stored in the same column as the date on SQL server, or am I mistaken? – Lucas Arrefelt Oct 27 '17 at 11:11
  • 1
    I don't know SQL server. It would be useful to mention how you're storing the dates and where (database, column definition), and of course, to post your code. We're forced to guess what you're doing, instead of knowing. – JB Nizet Oct 27 '17 at 11:17
  • @JBNizet Thanks for your help, I've got what I needed from the comment section of the answer below. – Lucas Arrefelt Oct 27 '17 at 11:24

2 Answers2

3

java.time

The Answer by Buurman is almost correct, but not quite.

The Joda-Time project is indeed in maintenance-mode, with its creators advising migration to the java.time classes built into Java 8 and later. Both Joda-Time and the java.time JSR 310 are led by the same man, Stephen Colebourne. So migration is fairly easy, as many of the core ideas are the same.

If you are trying to represent "lunch every Saturday at noon", you cannot reliably store exact moments going out into the future. Politicians around the world have shown a predilection for redefining their time zone(s). They do so surprisingly often, and often do so with very little warning. So you cannot know when "noon" is going to be, in terms of a time-of-day on the clock. A changing definition of the clock means you are chasing a moving target.

So, "lunch every Saturday at noon" requires tracking a day-of-week and tracking a time-of-day. Java has classes for both.

DayOfWeek dayOfWeek = DayOfWeek.SATURDAY ;  // Enum with seven predefined objects, Monday-Saturday.

LocalTime localTime = LocalTime.of( 12 , 0 ) ;  // Noon.

You also need to store the time zone intended when you said "noon". Every day, noon happens much earlier in India than it does in France, and happens even later in Québec. So a time-of-day only has meaning when in the context of a particular time zone.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId zoneId = ZoneId.of( "America/Montreal" ) ;

You can project out a series of these lunches, determining the exact moment (point on the timeline) for each. But you can only do so provisionally. Given that governments have proven they are willing to redefine the offset-from-UTC of their time zone with a forewarning of only months, weeks, days, or even hours, you cannot store these projections. Calculate them dynamically at runtime.

Determine today. This requires a time zone of course. For any given moment, the date varies around the globe by zone.

LocalDate today = LocalDate.now( zoneId ) ;

Get the next Saturday, or stay with today if already a Saturday.

LocalDate firstSaturday = today.with( TemporalAdjusters.nextOrSame( dayOfWeek ) ) ;

Determine the point on the timeline for noon in that zone on that date.

ZonedDateTime firstLunch = ZonedDateTime.of( firstSaturday , localTime , zoneId ) ;

You can view that same moment as UTC by extracting an Instant.

Instant firstLunchInstant = firstLunch.toInstant() ;

You can continue out this projection by adding a week to the LocalDate, and repeating the steps above to ask for noon on that next date. If that time-of-day happens to not be valid on that date in that zone (such as a Daylight Saving Time "Spring-ahead" cutover), the ZonedDateTime class adjusts accordingly. Be sure to read the doc to understand its adjustment algorithm.

int weeksToProject = 10 ;  // Run out 10 weeks.
List< ZonedDateTime > lunches = new ArrayList<>( weeksToProject ) ;
LocalDate localDate = firstSaturday ;
for( int i = 0 ; i < 10 ; i ++ ) {
    localDate = localDate.plusWeeks( i ) ;
    ZonedDateTime zdt = ZonedDateTime.of( localDate , localTime , zoneId ) ;  
    lunches.add( zdt ) ;
}

Database

As for storing "lunch every Saturday at noon" in a database:

  • The time-of-day can be stored in the SQL-standard type TIME WITHOUT TIME ZONE type.
    myPreparedStatement.setObject( … , localTime ) ;
    LocalTime localTime = myResultSet.getObject( … , LocalTime.class ) ;
  • The day-of-week could be stored as text in a VARCHAR using the English-hardcoded DayOfWeek enum value names such as MONDAY. Or you could store the day-of-week as in integer number. For a number, I strongly suggest using the ISO 8601 standard 1-7 for Monday-Sunday. Be sure to clearly document the meaning of your number for posterity.
  • The time zone can be stored as text in a VARCHAR in standard IANA Continent/Region format.

The ZonedDateTime objects should only be transient, created dynamically as needed in the moment. So you will not be storing them.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
2

I'd use the java8 LocalDateTime class server side and ignore timezones if that is what you want.

LocalDateTime is made exactly for handling cases where you want to state dates (and times) without timezone information.

If your database doesn't support storing timezone-less datetimes you could always convert it to and from a timestamp on the server using the same timezone everytime (for example, UTC). Just make sure you send it to the client, and receive it from the client, as a LocalDateTime.

EDIT: In response to comments below, if you want to show the date to the user according to their local timezone, it might be better to store a ZonedDateTime in a standard timezone (for example, UTC) and store the Locale of the users. You can then convert the dates to user-specific date formats using the Locale, which will keep DST and other timezone changes in mind.

EDIT2: For converting the datetimes you could use a regional ZoneId. The Locale could still be useful for formatting.

Buurman
  • 1,914
  • 17
  • 26
  • Since I have users from multiple countries I thought I would benefit from saving timezone information in database. I will give it a try and convert dates to LocalDateTime before responding to clients – Lucas Arrefelt Oct 27 '17 at 11:08
  • 1
    @LucasA You're saving the [UTC offset](https://en.wikipedia.org/wiki/UTC_offset), which is [**not** the same thing as a timezone](https://stackoverflow.com/a/36636543). `+02:00` just means "2 hours ahead UTC", but it's used by lots of countries in different times of the year and it doesn't handle DST changes. To really consider DST effects, you should store [IANA timezones names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (always in the format `Region/City`, like `America/New_York` or `Europe/Berlin`) –  Oct 27 '17 at 11:11
  • That depends on what those users want to see. Say user A lives in UTC, and B lives in UTC+5. If B creates a repeating event at 13:30 every Saturday, does A want to see it as 08:30 or as 13:30? – Buurman Oct 27 '17 at 11:13
  • @Hugo Thanks for the clearification. In terms of database structure, would client send their respective IANA zone in requests and that be stored in a separate nvarchar column? – Lucas Arrefelt Oct 27 '17 at 11:14
  • And, as Hugo adds, you might want to store the Local (https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html) of the user, not the timezone. You can use the Locale to convert a date while taking changing timezones (such as DST) in account. – Buurman Oct 27 '17 at 11:14
  • @Buurman In that scenario, I think A should see it with B's time (13:30) and maybe with a side note that it is not showing with A's local time – Lucas Arrefelt Oct 27 '17 at 11:15
  • @LucasA Based on what I understood from the question (perhaps you could edit it with more details, like the A-B scenario that Buurman asked about), I'd store a column with `LocalDateTime` (date and time without zone/offset) and another column with the timezone name. When displaying the date, you just join the pieces and it'll use the correct offset. –  Oct 27 '17 at 11:19
  • Thanks for the comments, I know what changes needs to be done now to achieve a proper structure of the dates so that they can be shown correctly on client side :) – Lucas Arrefelt Oct 27 '17 at 11:23