Portable way to parse local date/time (with implicit time zone) - java

I need to parse strings containing local date and time. The time zone is not explicitly specified, but the timestamps are in local time for a particular place.
Challenges are:
I need to be compatible with Java 7, which rules out a bunch of nifty features introduces with Java 8.
Java 7 compatibility is mostly needed for older Android versions, but the code will run on both JRE and Android.
Times are either in standard time or DST, depending on the time of the year. Hence a simple UTC offset will not work, as it jumps back and forward by an hour per year.
(This also means that timestamps during the DST to standard time switchover are ambiguous, but I can live with an hour of inaccuracy for that particular corner case.)
Here’s how far I got:
/*
* Let timestampString be the date/time string, examples:
* "2023-01-28 20:36"
* "2022-07-07 13:37"
*/
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm zzzz");
Date date = dateFormat.parse(timestampString + " CET");
System.out.println("Timestamp: " + dateFormat.format(date));
Problem: this interprets everything as Central European Standard time, all year round – during the summer season, times are off by one hour.
Using a time zone name such as Europe/Brussels instead of CET (which works with DateTimeFormatter and ZonedDateTime on Java 8) does not work, as the string is not recognized.
Does Java 7 offer anything comparable? (Preferably without having to resort to external libraries.)

Two general options:
Option 1: Use Java 8+ classes, with API desugaring on Android
Pros: Future proof; does not suffer from the bugs in Java’s Date and related classes; gives you a number of Java 8 APIs
Cons: Will work on Android prior to API 26 (Android 8) but not on JRE7; may require toolchain upgrades (which you will eventually need to do anyway)
This is the preferred way at least if you don’t have to support old desktop Java versions. Java 8 support on Android (at least in part) is done through a process called desugaring.
First, make sure you are at least on Gradle 6.1.1 and version 4.0.0 of the Android Gradle plugin.
Your build.gradle should not specify a buildToolsVersion unless you need a specific one (which then needs to be 29.0.2 or higher).
Then add the following to your build.gradle:
android {
defaultConfig {
multiDexEnabled true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8'
}
Version 1.1.8 is equivalent to 1.2.2 (which is the version Google recommends as of January 2023) but works on older versions of the R8/D8 compiler (I got an error with 1.2.2 but 1.1.8 worked).
All the changes so far only apply to the Android build. For other platforms you may be running the code on, nothing changes.
This allows you to then run the following Java code:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
.withZone(ZoneId.of("Europe/Brussels"));
try {
ZonedDateTime zonedDate = ZonedDateTime.parse(rawDate, formatter);
System.out.println("Timestamp: " + zonedDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm zzzz")))
} catch (DateTimeParseException e) {
// do whatever is necessary here
}
If you need to pass this to something that expects a Date, you can convert that using Date.from(zonedDate.toInstant()).
Option 2: Use Java 7-compatible classes
Pros: Works on any Java 7 API, whether desktop or Android
Cons: Not future proof (as you are using deprecated classes); still suffers from all the bugs in Java’s Date and related classes; will solve this particular API incompatibility but not others
DateFormat has a setTimeZone() method, which accepts a TimeZone argument (which, in turn, can be generated from a string such as Europe/Brussels). Setting it will cause date/time strings to be parsed as local time in that time zone, with automatic adjustment for DST. No need to append anything to the string being parsed.
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
dateFormat.setTimeZone(TimeZone.getTimeZone("Europe/Brussels"));
Date date = dateFormat.parse(timestampString);
/*
* dateFormat no longer contains a time zone, so use a separate format
* for output (with the time zone)
*/
SimpleDateFormat dateFormatOut = new SimpleDateFormat("yyyy-MM-dd HH:mm zzzz");
dateFormatOut.setTimeZone(TimeZone.getTimeZone("Europe/Brussels"));
System.out.println("Timestamp: " + dateFormatOut.format(date));

Related

Getting the UTC timestamp in Java

An old Stack Overflow posting suggests that the way to get the UTC timestamp in Java is the following:
Instant.now() // Capture the current moment in UTC.
Unfortunately this does not work for me. I have a very simple program (reproduced below) which demonstrates different behavior.
On Windows: the time is the local time and it is labeled with the offset with GMT
On Linux: the time is again the local time, and it is labeled correctly for the local timezone
Question: How do we display the UTC timestamp in a Java program?
My sample source code is as follows:
import java.time.Instant;
import java.util.Date;
public class UTCTimeDisplayer {
public static void main(String[] args) {
System.out.println(System.getProperty("os.name"));
Date currentUtcTime = Date.from(Instant.now());
System.out.println("Current UTC time is " + currentUtcTime);
}
}
Windows Output:
C:\tmp>java UTCTimeDisplayer
Windows 10
Current UTC time is Fri Jan 22 14:28:59 GMT-06:00 2021
Linux Output:
/tmp> java UTCTimeDisplayer
Linux
Current UTC time is Fri Jan 22 14:31:10 MST 2021
Your code:
Date.from(Instant.now())
You are mixing the terrible legacy classes with their replacement, the modern java.time classes.
Don’t.
Never use Date. Certainly no need to mix with java.time.Instant.
To explain your particular example, understand that among the Date class’ many poor design choices is the anti-feature of its Date#toString method implicitly applying the JVM’s current default time zone while generating its text.
You ran your code on two different JVMs that had different current default time zones. So you got different outputs.
Sun, Oracle, and the JCP gave up on the legacy date-time classes. So should we all. I recommend you not spend time trying understand Date, Calendar, SimpleDateFormat, and such.
You asked:
Question: How do we display the UTC timestamp in a Java program?
Instant.now().toString()
See that code run live at IdeOne.com.
2021-01-22T21:50:18.887335Z
You said:
On Windows: …
On Linux: …
You’ll get the same consistent results from Instant.now().toString() across Windows, Linux, BSD, macOS, iOS, Android, AIX, and so on.
Here is a table I made to guide you in transitioning from the legacy classes.
The java.util.Date object is not a real date-time object like the modern date-time types; rather, it represents the number of milliseconds since the standard base time known as "the epoch", namely January 1, 1970, 00:00:00 GMT (or UTC). When you print an object of java.util.Date, its toString method returns the date-time in the JVM's timezone, calculated from this milliseconds value. If you need to print the date-time in a different timezone, you will need to set the timezone to SimpleDateFormat and obtain the formatted string from it.
I would suggest you simply use Instant.now() which you can convert to other java.time type.
The date-time API of java.util and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern date-time API.
For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7.
If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.
However, if you still want to use java.util.Date, use SimpleDateFormat as mentioned above.
Demo:
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.TimeZone;
public class Main {
public static void main(String[] args) {
Date currentUtcTime = Date.from(Instant.now());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
sdf.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
System.out.println("Current UTC time is " + sdf.format(currentUtcTime));
}
}
Output:
Current UTC time is 2021-01-22 21:53:07 UTC
suggests that the way to get the UTC timestamp in Java is the following:
Instant.now() // Capture the current moment in UTC.
This, and most answers in this thread, are misleading.
Instant represents an instant in time. It's 'solarflares' time: Absolutely not one iota about it represents anything that is invented by human brains, and UTC is a timezone: A human invention. The cosmos, the sun, astronomy - they have no idea what UTC is, and don't care - and that's what Instant is all about. Instants are devoid of such human concepts as 'hours' or 'days' or 'timezones'. It makes no sense to ask an instant what day it happened on. It cannot tell you; some event occurred: If I ask a russian from the 19th century when that happened, they'll likely give a completely different answer vs. if I ask someone living a mere 100 miles west, for example. Instant doesn't know which localization to apply and thus doesn't let you ask it this question - that's a good thing, objects should not expose methods to which any answer it gives is either gobbledygook or at least requires knowing about all sorts of surprising caveats.
Crucially, if you tell me '... in UTC', you surely can tell with exacting detail which month, which day, etcetera. And Instant does not do this, which is why it is misleading to say that a java.time.Instant represents a moment of time in UTC. It doesn't. It represents a moment in time (not in any particular timezone).
Yeah, internally Instant, just like Date, is just a light wrapper around what System.currentTimeMillis() returns: "millis since epoch", but the crucial thing to understand about it, is that 'UTC' is not part of what it means, and therefore, when you give an Instant instance to some other method (such as System.out.println, to a database via JDBC, etc), that method is under absolutely no obligation to assume that UTC is semantically relevant.
When you want to mix human notions of time keeping (years, days, months, hours, minutes, milliseconds, and, yeah, timezones) with the notion of a more or less absolute* time, the right answer is java.time.ZonedDateTime. Note that any representation of time in something that isn't java.time.* based is by definition broken, as it is in most programming languages - turns out time is a lot more complex than most takes on a library to represent it realize. The fact that java.time is in effect the 4th attempt at writing a time library should be ample indication that it's hard to get it right.
ZonedDateTime zdt = ZonedDateTime.now(ZoneOffset.UTC);
THAT is what you want - that isn't just implementation-detail-wise what you want, but it is code that exactly describes what you mean: Right now, at the UTC time zone, stored in an object that semantically doesn't just store the right time but also stores, and tightly entangles into its very identity, that it is specifically in UTC and is not to be re-interpreted, moved to the local zone, or any other such shenanigans - at least, not unless you explicitly ask it to do so.
Date currentUtcTime = Date.from(Instant.now());
Note that Date is the old API and therefore necessarily broken. In this case, Date is a lying liar who lies - it doesn't represent a date, it represents an instant; it is badly named. (The second API is Calendar, also broken. For example, that is also a lying liar who lies: It doesn't represent a Calendar whatsoever. It represents some bizarre amalgamation of a zoned datetime and an instant and is fit to represent neither as a consequence). Any time you go to the Date API weirdness ensues, and something as simple as 'I just want the concept of the time, at some specific moment, in UTC' isn't possible in these APIs. You are now dependent on barely defined behaviour of all the various libraries up and down the chain - you're effectively stuck praying that they do the right thing, or delving into exotic settings to try to cajole these libraries into doing what you want.
TL;DR: Use java.time.
*) Note that ZonedDateTime is not absolute. For example, if you have the time January 20th, 2023, 8 in the morning, at Europe/Amsterdam, in the form of a ZonedDateTime object, then the amount of seconds that will pass between now and that moment sure seems like it does not change and will not change when e.g. amsterdam goes through an hour change due daylight savings. However, if the dutch government decrees that henceforth The Netherlands will no longer move the clocks at all and will stay in summer time forever (which is likely - EU directive is already in place, it's now just a matter of when), then at the moment the gavel lands, your appointment shifts by 1 hour exactly.
That hopefully provides crucial insight in the difference: Instant, representing events (hence why I like to call it 'solarflares time', to disentangle it from human time keeping concepts as much as possible), doesn't even understand the very concept of such a decision having an effect on things. ZonedDateTime on the other hand is inherently bound up in it - hence the Zone in ZonedDateTime.
If you want to store barber appointments and use Instant to do it, you WILL be an hour late or early sooner rather than later.
An Instant object and also a Date object by themselves
only contain a point in time, but no timezone information.
Furthermore, the toString() method of the Date class
implicitly chooses the timezone provided by the system environment,
which is not what you want.
Therefore you need to chose the timezone (in your case UTC) explicitly.
For example like this:
Instant instant = Instant.now();
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.UTC);
System.out.println("Current UTC time is " + offsetDateTime);
This will (independently from the operation system) print
Current UTC time is 2021-01-22T22:37:21.950354100Z
where the trailing Z denotes the zero timezone offset (i.e. UTC).
A simple method that could work!
My requirement was date time with milliseconds
2021-11-25 19:55:00.743
private String getUTCTimestamp() {
ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC);
return utc.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
}
Instant.now() is essentially the period of time since the epoch, (midnight 1 January 1970 in UTC), but you are using a Date to present that instant. Date is a reflection of the instant with millisecond precision, but as explained in the documentation at https://docs.oracle.com/javase/8/docs/api/java/util/Date.html, presenting a date should be done using a Calendar, as the presentation of a Date depends on the host. Essentially Date wraps the instant but is displayed according to other factors.
The simplest approach now if you want to output the instant is to use OffsetDateTime so that you can elect to present the instant in your desired timezone - UTC in your case. Use either OffsetDateTime.now() or OffsetDateTime.ofInstant() but if you are using the instant within your application logic then just stick with Instant.
Sometimes your program has to work with older java versions, so here is an example for 1.5:
java.text.SimpleDateFormat tfGMT = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
java.util.Calendar cUTC = java.util.Calendar.getInstance (java.util.TimeZone.getTimeZone ("GMT+0"));
tfGMT.setCalendar (cUTC);
java.util.Date d= new java.util.Date ();
String s= tfGMT.format (d);
System.out.printf ("now=%s [unix ts=%d.%03d]\n", s, d.getTime()/1000, d.getTime()%1000);
Mind you, the first three lines don't have to be repeat at every call, but keep in mind that SimpleDateFormat is not thread-safe. (Simple solution: create one for each thread.)
Example usage (it shows that setting TZ doesn't affect UTC-timestamp):
$ TZ=GMT+3 java5 now_utc; TZ=GMT-3 java5 now_utc
now=2021-01-24 12:56:14 [unix ts=1611492974.264]
now=2021-01-24 12:56:14 [unix ts=1611492974.726]

Getting specific date with timezone in Java

I am getting a little crazy with date and time libraries in Java. Basically what I need is to get two String dates, the first one corresponds to the previous day in Australia time (taking into account daylight time) with a specific hour (will come as a parameter) and with the following date format: "yyyy-MM-dd'T'HH:mm:ss.SSS zzz".
Consider I will receive the time as a string like the following: "180000". So if today is Nov-17 in Australia, I will need to get the following String:
"2017-11-16T18:00:00.000 AEDT"
And the second string date I need is the same day as today in au time:
"2017-11-17T18:00:00.000 AEDT"
I am not able to use JDK 8, I must use JDK 7.
I tried different ways but I am not getting what I need. Any suggestion will be great.
I saw your comment that you cannot use Java 8, you must use JDK 7. At the same time, especially for not quite trivial run-of-the-mill operations like yours, java.time, the modern Java date and time API that came out with Java 8 in 2014 is so much nicer to work with. Fortunately the modern API has been backported to Java 6 and 7 too, in the ThreeTen Backport (that’s ThreeTen for JSR-310, where the API was first described). So I encourage you to get the backport and start coding:
ZoneId australianTime = ZoneId.of("Australia/Sydney");
DateTimeFormatter receivedTimeFormat = DateTimeFormatter.ofPattern("HHmmss");
DateTimeFormatter neededDateTimeFormatter
= DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS zzz",
Locale.forLanguageTag("en-AU"));
LocalDate today = LocalDate.now(australianTime);
LocalDate yesterday = today.minusDays(1);
String receivedTimeString = "180000";
LocalTime time = LocalTime.parse(receivedTimeString, receivedTimeFormat);
String previousDayAtTime = yesterday.atTime(time)
.atZone(australianTime)
.format(neededDateTimeFormatter);
System.out.println(previousDayAtTime);
When I ran this snippet today, it printed the requested output for yesterday:
2017-11-16T18:00:00.000 AEDT
I trust you to do similarly for today.
Since you had AEDT in your requested result, I picked Australia/Sydney as time zone over Australia/Darwin or other Australian options.
Using the backport is also the futureproof solution: you will need the backport only until one day you upgrade to Java 8 or later (Java 9 is out already). When upgrading, simply change your import statement from org.threeten.bp to java.time.

How to translate between windows and IANA timezones in java

I need to translate between IANA timezone & windows timezone & vice-versa. There is another question reported: How to translate between Windows and IANA time zones?
It specifies that Noda time library can be used in .Net
Do we have any library to be used in Java? Or any other utility to be used in java?
This may be what you need, but I don't know if it will work for all your use cases:
for (String tzId : TimeZone.getAvailableIDs()) {
TimeZone tz = TimeZone.getTimeZone(tzId);
if (tz.getDisplayName(Locale.ENGLISH).equals("Eastern Standard Time")) {
System.out.println("tz = " + tzId);
}
}
I have implemented support for Windows zones in my Java-library Time4J. The last version v4.2 is also interoperable with Java-8 so it is easy to convert all basic Time4J-types to java.time-equivalents. For example recognizing Windows zones as strings is possible in constructing as well as during parsing:
// conversion Windows to IANA
WindowsZone wzn = WindowsZone.of("Eastern Standard Time");
TZID winzone = wzn.resolveSmart(Locale.US);
System.out.println(winzone.canonical()); // WINDOWS~America/New_York
// usage in timezone calculation
Timezone tz = Timezone.of(winzone);
System.out.println(Moment.UNIX_EPOCH.toZonalTimestamp(winzone)); // 1969-12-31T19
// usage in parsing and formatting
ChronoFormatter<Moment> f =
ChronoFormatter.ofMomentPattern(
"MM/dd/uuuu hh:mm a zzzz", PatternType.CLDR, Locale.US, winzone);
Moment pacificTime = f.parse("07/17/2015 02:45 PM Pacific Standard Time");
System.out.println(f.format(pacificTime)); // 07/17/2015 05:45 PM Eastern Standard Time
As you can see, a locale Information is necessary to map a Windows zone like "Eastern Standard Time" to an Olson/IANA-identifier like "America/New_York". The underlying data and mapping informations are taken from CLDR.
The reverse way from IANA to Windows might be done this simple way:
String iana = "America/New_York";
String winzone = "WINDOWS~" + iana;
NameStyle dummy = NameStyle.LONG_STANDARD_TIME; // does not really matter
String name = Timezone.of(winzone).getDisplayName(dummy, Locale.US);
System.out.println(name); // Eastern Standard Time
However, this reverse conversion might not work for all iana-identifiers because Windows only supports a very simplified subset of timezones compared with IANA-TZDB. I also think that the reverse way is hardly used in practice. Users should rather work with IANA-timezones by default and only use Windows timezones if that is the (unavoidable) input to handle (see first part of my answer).
I finally had to make my own implementation. The windowsZones.xml needs to be updated for plenty of missing timezone entries. I'm not publishing the updated file as there are many timezones where there is no perfect match between Windows offset & IANA offset.
Also, as for one Windows timezone there could be multiple IANA timezone. So, i had to make implementation to choose best suitable according to other information available like geography of user(address) etc.
With this, I'm just using the windowsZones.xml to get IANA timezone from Windows timezone & vice-versa.

How to remove the SECONDS field from a DateFormat

I want to print a time without seconds in the default format for the locale.
So I get the formatter with getTimeInstance() or getTimeInstance(int style). But even when I use a SHORT style it will contain the seconds part of the time as well.
Is there any way (apart from creating my own format, which then would not be the locale default and manually maintained) I can grab the default and split off the seconds ?
Thanks
Roman
DateFormat.getTimeInstance(DateFormat.SHORT) works perfectly fine here: from 20:00:00 to 20:00 and from 8:00:00 PM to 8:00 PM.
EDIT: This is insufficient (as stated by the first comment below). I'm keeping this here for the sake of history and to keep others from responding in a similar fashion :)
Have you considered saving the current format as a string and manually removing the seconds using String's substring method?
In case someone is reading this and either uses Java 8 or later or is fine with a (good and futureproof) external library:
DateTimeFormatter noSeconds = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.ITALY);
LocalTime time = LocalTime.now(ZoneId.systemDefault());
System.out.println(time.format(noSeconds));
This just printed:
15.26
Please substitute your desired locale instead of Locale.ITALY. Use Locale.getDefault() for your JVM’s locale setting. I believe it prints without seconds in all locales.
In the code I have used a LocalTime object, but the same code works for many other date and time classes including LocalDateTime, OffsetDateTime, OffsetTime and ZonedDateTime.
To use DateTimeFormatter and any of the other classes mentioned on Android you need the ThreeTenABP. More details on how to in this question: How to use ThreeTenABP in Android Project. For any non-Android Java 6 or 7, use ThreeTen Backport.

Joda time week calculation reasoning

The code below demonstrates the problematic joda-time implementation of week calculation. This behavior is not a bug but a design decision Joda-Time uses the ISO standard Monday to Sunday week. (perhaps it should be a bug?)
Given a date I need to calculate the week number, this calculation must be i18n in nature. Meaning I must take into consideration the correct week numbering based on the regional settings of the user.
The demo code below shows wrong calculation by Joda-Time and correct calculation by the JDK, in the application we try to stick with Joda-Time being a superior solution for date manipulations. So, should I be mixing the two Time calculation libraries? I would obviously prefer not to, is this even a safe thing to do or would I come into corner cases (having experience with Date, Calendar I know for a fact that this is a painful issue for Java).
Bottom line: What is the recommended best-practice for the described requirement?
Problem demonstration code
Please see this online calendar displaying week numbers for correct week calculation example.
public class JodaTest {
static DateTimeFormatter formatter = DateTimeFormat.forPattern("ww yyyy");
static SimpleDateFormat jdkFormatter = new SimpleDateFormat("ww yyyy");
public static void main(String[] args) {
DateTime time = new DateTime(/*year*/2009, /*monthOfYear*/12, /*dayOfMonth*/6, /*hourOfDay*/23, /*minuteOfHour*/0, /*secondOfMinute*/0, /*millisOfSecond*/0);
StringBuilder buffer = new StringBuilder()
.append("Testing date ").append(time.toString()).append("\n")
.append("Joda-Time timezone is ").append(DateTimeZone.getDefault()).append(" yet joda wrongly thinks week is ").append(formatter.print(time)).append("\n")
.append("JDK timezone is ").append(TimeZone.getDefault().getID()).append(" yet jdk rightfully thinks week is ").append(jdkFormatter.format(time.toDate())).append(" (jdk got it right ?!?!)");
System.out.println(buffer.toString());
}
}
Output:
Testing date 2009-12-06T23:00:00.000+02:00
Joda-Time timezone is Asia/Jerusalem yet joda wrongly thinks week is 49 2009
JDK time zone is Asia/Jerusalem yet jdk rightfully thinks week is 50 2009 (jdk got it right ?!?!)
The best available solution is to write an implementation of DateTimeField that wraps up the logic to extract the value you need based on a locale. Internally, you'll probably still rely on the JDK data. The aim is to wrap all the JDK code in a single reusable class. You then use it like this:
int value = dateTime.get(new LocaleAwareWeekField("en_GB"));

Categories