Single class to parse any Date Format in Java - java

I have been parsing dates in the below formats. I maintain an array of these formats and parse every date string in all these formats.
The code I used was -
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
simpleDateFormat.setTimeZone(timeZone); //timeZone is a java.util.TimeZone object
Date date = simpleDateFormat.parse(dateString);
Now I want to parse yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX format as well but using SimpleDateFormat the 6 digit microseconds are not considered. So I looked into java.time package.
To parse yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX formats I will be needing OffsetDateTime class and for other formats, I need ZonedDateTime class. The format will be set in DateTimeFormatter class.
Is there a way to use a single class like SimpleDateFormat to pass all the formats?

Since your Java 8 doesn’t behave as would be reasonably expected, I suggest that a workaround is trying to parse without zone first. If a zone or an offset is parsed from the string, this will be used. If the parsing without zone fails, try with a zone. The following method does that:
private static void parseAndPrint(String formatPattern, String dateTimeString) {
// Try parsing without zone first
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern);
Instant parsedInstant;
try {
parsedInstant = formatter.parse(dateTimeString, Instant::from);
} catch (DateTimeParseException dtpe) {
// Try parsing with zone
ZoneId defaultZone = ZoneId.of("Asia/Calcutta");
formatter = formatter.withZone(defaultZone);
parsedInstant = formatter.parse(dateTimeString, Instant::from);
}
System.out.println("Parsed instant: " + parsedInstant);
}
Let’s try it:
parseAndPrint("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX", "2018-10-22T02:17:58.717853Z");
parseAndPrint("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "2018-10-22T02:17:58.717853");
parseAndPrint("EEE MMM d HH:mm:ss zzz yyyy", "Mon Oct 22 02:17:58 CEST 2018");
Output on Java 8 is:
Parsed instant: 2018-10-22T02:17:58.717853Z
Parsed instant: 2018-10-21T20:47:58.717853Z
Parsed instant: 2018-10-22T00:17:58Z
The first example has an offset in the string and the last a time zone abbreviation in the string, and in both cases are these respected: the instant printed has adjusted the time into UTC (since an Instant always prints in UTC, its toString method makes sure). The middle example has got neither offset nor time zone in the string, so uses the default time zone of Asia/Calcutta specified in the method.
That said, parsing a three or four letter time zone abbreviation like CEST is a dangerous and discouraged practice since the abbreviations are often ambiguous. I included the example for demonstration only.
Is there a way to use a single class…?
I have used Instant for all cases, so yes there is a way to use just one class. The limitation is that you do not know afterward whether any time zone or offset was in the string nor what it was. You didn’t know when you were using SimpleDateFormat and Date either, so I figured it was OK?
A bug in Java 8?
The results from your demonstration on REX tester are disappointing and wrong and do not agree with the results I got on Java 11. It seems to me that you have been hit by a bug in Java 8, possibly this one: Parsing with DateTimeFormatter.withZone does not behave as described in javadocs.

Related

Convert or format string to date

I am struggling with this ..
I have an input string - like this: 2021-10-13 11:33:16.000-04
Using Java.
I need to get a Date object from it.
which formatting pattern can I use ?
I try with these
SimpleDateFormat inFormatter = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss.SSS'-'ZZ");
and
SimpleDateFormat inFormatter = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss.SSSZZ");
and I keep getting
java.text.ParseException: Unparseable date: "2021-10-13 11:33:16.000-04"
at java.base/java.text.DateFormat.parse(DateFormat.java:396)
at com.dima.tests.DatesConversions.main(DatesConversions.java:24)
Please, help !!
Don't use Date as it is outdated. Use the classes in the java.time
OffsetDateTime odt = OffsetDateTime.parse(str,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSX"));
System.out.println(odt);
Prints
2021-10-13T11:33:16-04:00
java.time
Even though you need to give an old-fashionede Date object to a legacy API beyond your control, I still recommend that you use java.time, the modern Java date and time API, in your own code. The final conversion to Date is pretty straight-forward.
I’d use this formatter for maximum reuse of existing formatters:
private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral(' ')
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.appendOffset("+HHmm", "+00")
.toFormatter(Locale.ROOT);
Then we parse and convert like this:
String input = "2021-10-13 11:33:16.000-04";
OffsetDateTime dateTime = OffsetDateTime.parse(input, PARSER);
System.out.println(dateTime);
Instant i = dateTime.toInstant();
Date oldfashionedDate = Date.from(i);
System.out.println(oldfashionedDate);
Output in my time zone, Europe/Copenhagen:
2021-10-13T11:33:16-04:00
Wed Oct 13 17:33:16 CEST 2021
Denmark is at offset +02:00 at this time of year, so 6 hours ahead of the UTC offset -04 from your string. Therefore Date.toString() confusingly prints a clock hour that is 6 hours ahead of the original time of day.
Note: if your forward service accepts anything else than an old-fashioned Date, you should not be using that class. For example, if a String is required, the OffsetDateTime that we got can be formatted into a new string using a second DateTimeFormatter (or in lucky cases, its toString method).
What went wrong in your code?
First, a UTC offset can have positive or negative sign. Instead of -04 you could have had for example +09. Formatters are designed for to take the sign, + or -, as part of the offset. Therefore hardcoding the minus sign as a literal, as in your first attempt, is bound to fail. In your second attempt, yyyy-MM-dd HH:mm:ss.SSSZZ, you are already closer. However, ZZ is for an offset with sign and four digits (like +0530 or -0400; hour and minute), so does not work for a two-digit offset like -04. Your SimpleDateFormat expected more digits where your string ended and therefore threw the exception that you saw.
Link
Oracle tutorial: Date Time explaining how to use java.time.
Since you are using ISO 8601 time zone timezone, you have the use the below pattern.
SimpleDateFormat inFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSX");
And then, to get the date:
Date date = inFormatter.parse("2021-10-13 11:33:16.000-04");
Always check the documentation.

How to convert String to Date with TimeZone?

I'm trying to convert my String in Date + Timezone.
I get my String from a DateTime Variable (here: xyz).
My code:
String abc = xyz.toString("yyyy-MM-ddZZ");
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-ddXXX");
java.util.Date date = sdf.parse(abc);
System.out.println("Date: " + sdf.format(date));
Error:
Invalid format: "2017-01-03+01:00" is malformed at "+01:00"
If I try SimpleDateFormat("yyyy-MM-dd"); it works but without the Timezone ("+01:00")
The input has a date - year, month, day - and an offset - the difference from UTC - but to build a java.util.Date, you also need the time: hour, minutes, seconds, fraction of seconds.
SimpleDateFormat is terrible because it does some "magic", setting the missing fields to default values. Another problem is that the X pattern doesn't work for all Java versions, and the documentation sucks.
You can use the new Java 8 classes, as explained. With them, you can parse the input, choose the default values to be used for the time fields and convert to java.util.Date, if that's what you need:
DateTimeFormatter fmt = new DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_OFFSET_DATE)
// set hour to midnight
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0).toFormatter();
OffsetDateTime odt = OffsetDateTime.parse("2017-01-03+01:00", fmt); // 2017-01-03T00:00+01:00
The OffsetDateTime will have the time set to midnight, but you can change it to whatever values you need, while with SimpleDateFormat it's not possible, because it uses internal default values and you can't control it.
And the date and offset were correctly set to the values in the input string. You can then convert to java.util.Date if you want:
Date date = Date.from(odt.toInstant());
You can also get the individual "pieces" of the date if you want:
// get just the date
LocalDate localDate = odt.toLocalDate(); // 2017-01-03
// get just the offset
ZoneOffset offset = odt.getOffset(); // +01:00
PS: the offset +01:00 is not the same thing as a timezone. See the difference here
String abc = "2017-01-03+01:00";
TemporalAccessor parsed = DateTimeFormatter.ISO_OFFSET_DATE.parse(abc);
LocalDate date = LocalDate.from(parsed);
ZoneOffset offset = ZoneOffset.from(parsed);
System.out.println("Date: " + date + "; offset: " + offset + '.');
This prints:
Date: 2017-01-03; offset: +01:00.
I am using java.time, the modern Java date and time API, and recommend you do the same. The Date class is long outdated (sorry, no pun intended) and SimpleDateFormat in particular notoriously troublesome. Don’t use them. The modern API is so much nicer to work with. Only if you need a java.util.Date and/or a java.util.TimeZone for a legacy API that you cannot change, convert like this:
Date oldfashionedDate = Date.from(date.atStartOfDay(offset).toInstant());
TimeZone oldfashionedTimeZone = TimeZone.getTimeZone(offset);
System.out.println("Old-fashioned date: " + oldfashionedDate
+ "; old-fashioned time-zone: " + oldfashionedTimeZone.getDisplayName() + '.');
On my computer this prints:
Old-fashioned date: Tue Jan 03 00:00:00 CET 2017; old-fashioned time-zone: GMT+01:00.
I happen to be in a time zone that agrees with your offset from UTC, so it’s fairly obvious that the conversion has given the correct result. In other time zones the output will be more confusing because Date.toString() uses the JVM’s time zone setting for generating the string, but the Date will still be correct.
A date with a time zone? Neither a LocalDate nor a Date can hold a time zone in them, so you need to have the offset information separately. Interestingly your string seems to follow a “ISO-8601-like” format for an offset date that is even represented by a built-in formatter that has ISO in its name. If Java had contained an OffsetDate or a ZonedDate class, I would have expected such a class to parse your string into just one object and even without an explicit formatter. Unfortunately no such class exists, not even in the ThreeTen-Extra project, as far as I can tell at a glance.
Links
Oracle tutorial: Date Time, explaining how to use java.time.
ThreeTen Extra, more classes developed along with java.time.
EDIT: See my updated code run live on ideone.
"2017-01-03+01:00"
I thought it a similar ISO 8601 format date string, but actually not ISO 8601. Thanks #Meno Hochschild and #Basil Bourque's indication.
It is so luck that this method works for such format's string: javax.xml.bind.DatatypeConverter.parseDateTime, it will return a Calendar:
System.out.println(DatatypeConverter.parseDate("2017-01-03+01:00").getTime());
Output:
Tue Jan 03 07:00:00 CST 2017
From the method javadoc:
public static Calendar parseDate(String lexicalXSDDate)
Converts the string argument into a Calendar value.
Parameters: lexicalXSDDate - A string containing lexical
representation of xsd:Date.
Returns: A Calendar value represented by
the string argument.
Throws: IllegalArgumentException - if string
parameter does not conform to lexical value space defined in XML
Schema Part 2: Datatypes for xsd:Date.

How do I get Date only from epoch time having time as 23:59:59

I have date 2015-12-25 23:59:59 in the form of epoch milliseconds 1451087999000, And I want the date part only i.e. 2015/12/25, how do I do that efficiently might be with the JODA time library which is nowdays standard for dealing with Date time in java.
I have this code which works in most the case but when time is like 23:59:59 it gives me the next date (as in my case it gives 2015-12-26 with input of 2015-12-25 23:59:59)-
String dateInMilliSeconds = "1451087999000";
String dateInYYYYMMDDFormat = DateHelper.convertDateFormat(new Date(Long.valueOf(dateInMilliSeconds)),DateHelper.yyyy_MM_dd);
DateHelper.convertDateFormat() -
public static final String yyyy_MM_dd = "yyyy-MM-dd";
public static String convertDateFormat( Date date, String outputFormat )
{
String returnDate = "";
if( null != date )
{
SimpleDateFormat formatter = new SimpleDateFormat(outputFormat);
returnDate = formatter.format(date);
}
return returnDate;
}
You can use localDate from java 8
LocalDate date = Instant.ofEpochMilli(dateInMilliSeconds).atZone(ZoneId.of(timeZone)).toLocalDate();
I should like to make two points:
Time zone is crucial.
Skip the outdated classes Date and SimpleDateFormat.
My suggestion is:
String dateInMilliSeconds = "1451087999000";
LocalDate date = Instant.ofEpochMilli(Long.parseLong(dateInMilliSeconds))
.atOffset(ZoneOffset.UTC)
.toLocalDate();
System.out.println(date);
This prints
2015-12-25
Please note that you get your desired output format for free: LocalDate.toString() produces it. If you want to be able to produce different output formats, use a DateTimeFormatter.
Time zone
Your millisecond value isn’t just equal to 2015-12-25 23:59:59. It is equal to this date and time in UTC, so you need to make sure that your conversion uses this time zone offset. When I run your code from the question on my computer, I incorrectly get 2015-12-26 because my computer is in the Europe/Copenhagen time zone.
JSR-310 AKA java.time
Joda-Time was the widely acknowledged better alternative to the original date and time API from Java 1 that many considered poor and troublesome. The Joda-Time project is now finished because the modern Java date and time API known as JSR-310 or java.time came out three and a half years ago, so they recommend we use this instead. So my code does.
The timestamp 1451087999000 is 2015-12-25 23:59:59 in UTC. In your code, you're not specifying the timezone when you format it with a SimpleDateFormat, so it's formatted in your local timezone.
With Joda Time:
String dateInMilliSeconds = "1451087999000";
LocalDate date = new LocalDate(Long.parseLong(dateInMilliSeconds), DateTimeZone.UTC);
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd");
String result = formatter.print(date);

Trying to parse a datetime in PDT to a ZonedDateTime representation

How should I parse this datetime value that is in the PDT timezone?
06/24/2017 07:00 AM (PDT)
I want to maintain the timezone so that I can then represent the time in other timezones depending on the website visitors preferences.
I tried using ZonedDateTime but I get a parse error:
java.time.ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)")
The error is:
java.time.format.DateTimeParseException: Text '06/24/2017 07:00 AM (PDT)' could not be parsed at index 0
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
at java.time.ZonedDateTime.parse(ZonedDateTime.java:597)
at java.time.ZonedDateTime.parse(ZonedDateTime.java:582) ... 29 elided
Also, do you agree that I should be using a ZonedDateTime?
Since your format is non-standard, you need to specify it to the parser:
ZonedDateTime.parse(
"06/24/2017 07:00 AM (PDT)",
DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm a (zzz)")
);
The parse method expects a String in a specific format, like 2007-12-03T10:15:30+01:00[Europe/Paris]. As your input is in a different format, you need a DateTimeFormatter.
One detail to notice is that the API uses IANA timezones names (always in the format Continent/City, like America/Sao_Paulo or Europe/Berlin).
Avoid using the 3-letter abbreviations (like CST or PST) because they are ambiguous and not standard.
The API makes some exceptions with specific IDs and provides some defaults for them. For PDT, it defaults to America/Los_Angeles.
Another detail is that in the example below I used lowercase hh in the pattern: the format has AM/PM indication, so I think that hh is the correct pattern, as its value is from 1 to 12 (the common values when there's the AM/PM indicator).
If you use uppercase HH, it allows values from 0 to 23 (and it's not common to use this with AM/PM), and it will throw an exception if the input contains an hour like 07:00 PM.
So the code will be like:
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm a (zzz)");
ZonedDateTime z = ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)", fmt);
System.out.println(z);
The output is:
2017-06-24T07:00-07:00[America/Los_Angeles]
But not all the 3-letter timezone names will be recognized by the API and will throw an exception.
Anyway, there are other timezones that also are in PDT (like America/Vancouver) - you can get a list of all by calling ZoneId.getAvailableZoneIds(). If you want to use a different timezone as the default, you can create a set of preferred zones and build a formatter with this set:
Set<ZoneId> preferredZones = new HashSet<>();
// set America/Vancouver as preferred zone
preferredZones.add(ZoneId.of("America/Vancouver"));
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
// pattern
.appendPattern("MM/dd/yyyy hh:mm a (")
// append timezone with set of prefered zones
.appendZoneText(TextStyle.SHORT, preferredZones)
// finish the pattern
.appendPattern(")")
// create formatter
.toFormatter();
System.out.println(ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)", fmt));
The API will use the preferred zones set (in this case, America/Vancouver) instead of the default (America/Los_Angeles). The output will be:
2017-06-24T07:00-07:00[America/Vancouver]
It's not clear where the input String's come from. If you can't control their format, then you have no choice: they need to be parsed this way. Then you can convert it to another timezone using the withZoneSameInstant method:
// parse the input string
ZonedDateTime z = ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)", fmt);
// convert to another timezone
ZonedDateTime other = z.withZoneSameInstant(ZoneId.of("America/Sao_Paulo")); // 2017-06-24T11:00-03:00[America/Sao_Paulo]
The value of other will be 2017-06-24T11:00-03:00[America/Sao_Paulo].
But if you can control the output, it's always better (IMO) to internally work with UTC (java.time.Instant), and convert to some timezone only when displaying to users:
// convert ZonedDateTime to instant
ZonedDateTime z = // parse input
// convert to UTC (Instant is always in UTC)
Instant instant = z.toInstant();
// internally work with instant (as it's always in UTC)
// convert instant to some timezone only when necessary (like displaying to users)
ZonedDateTime converted = instant.atZone(ZoneId.of("Europe/London"));
The error you get is well covered in the other answers already.
Also, do you agree that I should be using a ZonedDateTime?
Yes and no. Your string should definitely be parsed into a ZonedDateTime. I recommend you convert it to an Instant and store this. Then when you need to present it to a user according to his/her time zone preference, you may either convert the Instant to a ZonedDateTime again or just format it using a DateTimeFormatter with the desired default time zone.
Why do it this way? First, common practice is to store Instants. Some prefer to store just milliseconds since the epoch, I think this some (often misunderstood) performance measure. Certainly such milliseconds I quite unreadable while Instants can be deciphered on eye-sight, at least roughly. The only other alternative I respect is when you know for certain that your application will never need to be concerned with a time zone (does this ever happen?), then sometimes LocalDateTime is used for storage.
If I understand your situation correctly, you need to store the point in time for display into multiple time zones. You don’t need to store the time zone in which the time was originally entered (like PDT, except PDT is not really a full time zone). Instant is time zone neutral, which is one reason I prefer it over storing the time in some time zone, as ZonedDateTime would. Also an Instant is simpler conceptually, and my guess is that it is also simpler implementation-wise.
There are a couple of much better answers here: Best practices with saving datetime & timezone info in database when data is dependant on datetime.

How can I convert Date.toString back to Date?

I have a string obtained by calling the toString method of an instance of the class Date. How can I get a Date object from this string?
Date d = new Date();
String s = d.toString;
Date theSameDate = ...
UPDATE
I've tried to use SimpleDateFormat, but I get java.text.ParseException: Unparseable date
What is the date format produced by Date.toString ()?
If your real goal is to serialize a Date object for some kind of custom made persistence or data transfer, a simple solution would be:
Date d = new Date();
long l = d.getTime();
Date theSameDate = new Date(l);
You could do it like this
Date d = new Date();
String s = d.toString;
Date theSameDate = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy").parse(s);
If your real goal is to serialize and deserialize a date and time (for data transfer or for persistence, for example), serialize to ISO 8601, the standard format for date and time data.
Skip the long outdated Date class. The modern Java date and time API known as java.time is so much nicer to work with. The class you need from it is probably Instant (this depends on your more exact requirements).
The two points go nicely hand in hand:
Instant i = Instant.now();
String s = i.toString();
Instant theSameInstant = Instant.parse(s);
The modern classes’ toString methods produce ISO 8601 format (e.g., 2018-01-11T10:59:45.036Z), and their parse methods read the same format back. So this snippet is all you need, and you get an instant equal to the first, with nanosecond precision.
If you cannot control the string you get, and you get the result from Date.toString(), the format pattern string in Sedalb’s answer works with java.time too:
DateTimeFormatter dtf
= DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ROOT);
Date d = new Date();
String s = d.toString();
Instant nearlyTheSameInstant = ZonedDateTime.parse(s, dtf).toInstant();
It’s essential to provide a locale. Otherwise the JVM’s default locale will be used, and if it’s not English, parsing will fail. In the worst case you will see your code running fine for many years and suddenly it will break when one day someone runs it on a computer or device with a different locale setting.
The point from jambjo’s answer still applies: The three and four letter time zone abbreviations used in Date.toString() are very often ambiguous, so there is no guarantee that the time zone is interpreted correctly, and again, it will be interpreted differently on different JVMs.
Finally, Date.toString() does not render the milliseconds that the Date holds, which leads to an inaccuracy of up to 999 milliseconds. If using the string from Date.toString(), there is nothing we can do about it (which was why I named the variable nearlyTheSameInstant).
Take a look at SimpleDateFormat#parse(). It should provide the functionality you're looking for.
Date theSameDate = new Date(Date.parse(s));
For some not so obvious reasons, this is not a particularly good idea. You can find details on that in the API documentation for the parse method. One problem is e.g. that the time zone abbreviations are ambiguous, so that the parser may fail in interpreting the correct time zone.

Categories