1

I am working on a system where each user can specify a setting of their adjustment from UTC in hours.

So when a date/time is input from this user, the value is adjusted by their setting and saved as UTC. Similarly, on the way out, the date/time is retrieved from the database, adjusted per their setting and displayed.

I might be thinking about this too much, but does this mean to show the correct date/time for each person, I have to effectively adjust the hours and tell the instance of my SimpleDateFormat that this is "UTC"? Right now I am in the UK where the current time zone is UTC+1 and if I don't specify to print in UTC then the time is off by one hour!

DateTime dateWithOffset = statusDate.plusMinutes(currentTimezoneOffsetInMinutes);

SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy HH:mm");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
return sdf.format(dateTime.toDate());

Am I thinking about this correctly? Or is there an easier way to print a date in a format given that I just want an hours adjustment from UTC?

David Spence
  • 7,999
  • 3
  • 39
  • 63
  • Would the `setRawOffset` method of the `TimeZone` class work for you? – Dawood ibn Kareem Jun 17 '16 at 11:15
  • Minor inconsistency; Your first sentence says hours but your code says minutes. – Basil Bourque Jun 17 '16 at 21:25
  • @BasilBourque Thanks! I tried to simplify the question as much as possible! We save the offset in minutes so it would be 60, 120, etc. Thank you for your detailed answer, I will read and understand it properly on Monday, right now it's the weekend! – David Spence Jun 17 '16 at 21:34
  • @DavidSpence What did you mean by `locale` in the title? No mention in the rest of the Question. – Basil Bourque Jun 18 '16 at 02:20
  • Probably not the correct term. I meant that the user does not choose their country etc. I'm not sure how common the approach is, choosing the number of hours offset from UTC as a user setting! – David Spence Jun 18 '16 at 06:26

1 Answers1

3

You are working too hard. Never do manual adjustments for offsets and time zones, never be adding or subtracting minutes to a date-time value for that purpose. Let a decent date-time library do that work.

java.time

The Joda-Time team advises us to migrate to the java.time framework built into Java 8 and later.

The ZoneOffset class represents an offset-from-UTC. Keep in mind that in some areas an offset may involve not only a number of hours but also minutes and even seconds.

The OffsetDateTime class represents a a moment in the timeline with an assigned offset.

int hours = 3;  // input by user

ZoneOffset offset = ZoneOffset.ofHours( hours );
OffsetDateTime odt = OffsetDateTime.now( offset );

The standard ISO 8601 formats are used by the toString methods in java.time.

String output = odt.toString();

Generally the best practice is to do your business logic and data storage in UTC. Convert to/from an offset or zoned value only for interaction with user.

In java.time a moment on the timeline in UTC is represented by the Instant class. You can extract an Instant object from the OffsetDateTime.

Instant instant = odt.toString();

Both this Instant and this OffsetDateTime represent the same simultaneous moment on the timeline. They present different wall-clock times.

It may be more clear to skip the use of the OffsetDateTime.now convenience method and start with Instant.

Instant instant = Instant.now();  // Always in UTC, by definition.
ZoneOffset offset = ZoneOffset.ofHours( hours );
OffsetDateTime odt = OffsetDateTime.ofInstant( instant , offset );  // Same moment but presenting alternate wall-clock time.

Handling input

If the user is inputting date-time values as strings, we need to parse. In java.time that means the DateTimeFormatter class. The formatting codes are similar to the outmoded java.text.SimpleDateFormat but not exactly identical, so be sure to study the doc.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern( "dd MMM uuuu HH:mm";

Since the offset-from-UTC is given separately, we parse this input string as a LocalDateTime devoid of time zone information.

LocalDateTime ldt = LocalDateTime.parse( inputString , formatter );

To view, create a String object formatted in ISO 8601 format by calling ldt.toString().

2016-01-02T12:34:45

Apply the pre-determined ZoneOffset object to yield a OffsetDateTime object.

OffsetDateTime odt = ldt.atOffset( offset );

2016-01-12T12:34:45+03:00

Think in UTC

Handling date-time values is a headache. UTC is your aspirin.

When a programmer arrives at the office, she should take off her “UK citizen / London resident” hat, and put on her “UTC” hat. Forget all about your own local time zone. Learn to think in UTC (and 24-hour clock). Add another clock to your desk or computer, set to UTC (or Reykjavík Iceland), or at least bookmark a page like time.is/UTC. Do all your logging, business logic, data serialization, data-exchange, and debugging in UTC.

Make Instant your first-thought, your go-to class. It's value is always in UTC by definition.

Instant instant = Instant.now();

Look at the Instant extracted from the OffsetDateTime value we saw above whose String representation was 2016-01-12T12:34:45+03:00. Being in UTC means 9 AM rather than noon, same moment but three hours difference in wall-clock time. The Z is short for Zulu and means UTC.

String output = odt.toInstant().toString();

2016-01-12T09:34:45Z

Adjust into an offset or time zone only as needed, when expected by a user or data sink. FYI, a time zone is an offset-from-UTC plus a set of rules for handling anomalies such as Daylight Saving Time (DST). Use a time zone in preference to a mere offset wherever possible.

The Europe/London time zone is the same as UTC in the summer, but in winter uses Daylight Saving Time nonsense, and is one hour ahead of UTC. So using the same Instant seen just above, the London wall-clock time is 10 AM rather than 9 AM in UTC, and different from the noon we saw with an offset of +03:00.

ZoneId zoneId = ZoneId.of( "Europe/London" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

2016-01-12T10:34:45+01:00[Europe/London]

Always specify the desired/required offset or time zone; never rely on the implicit current default by omitting this optional argument. (Ditto for Locale by the way.) Note how in all the code of this answer the fact that your JVM has a current default time zone (ZoneId.systemDefault) of Europe/London and the fact that my JVM has a current default time zone of America/Los_Angeles is completely irrelevant. The code runs the same, gets the same results, regardless of whatever machine you use to develop, test, and deploy.

Locale

Specify a Locale object when generating a textual representation of a date-time value that involves a name of month or day, commas or periods and so on. The Locale determines (a) the human language to use when translating such names, and (b) the cultural norms to follow in deciding issues such as punctuation marks.

The Locale has nothing to do with time zones and offset-from-UTC. For example, you could use Locale.CANADA_FRENCH with a date-time zoned for Asia/Kolkata if you had a Québécois user in India.

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