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.