I am trying to write a DateTimeFormatter that will allow me to take in multiple different String formats, and then convert the String formats to a specific type. Due to the scope of the project and the code that already exists, I cannot use a different type of formatter.
E.g., I want to accept MM/dd/yyyy as well as yyyy-MM-dd'T'HH:mm:ss but then when I print I only want to print to MM/dd/yyyy format and have it in the format when I call LocalDate.format(formatter);
Could someone suggest ideas on how to do this with the java.time.format.*;
Here is how I could do it in org.joda:
// MM/dd/yyyy format
DateTimeFormatter monthDayYear = DateTimeFormat.forPattern("MM/dd/yyyy");
// array of parsers, with all possible input patterns
DateTimeParser[] parsers = {
// parser for MM/dd/yyyy format
monthDayYear.getParser(),
// parser for yyyy-MM-dd'T'HH:mm:ss format
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss").getParser()
};
DateTimeFormatter parser = new DateTimeFormatterBuilder()
// use the monthDayYear formatter for output (monthDayYear.getPrinter())
// and parsers array for input (parsers)
.append(monthDayYear.getPrinter(), parsers)
// create formatter (using UTC to avoid DST problems)
.toFormatter()
.withZone(DateTimeZone.UTC);
I have not found a good/working example of this online.
I've tested this with JDK 1.8.0_131 for Mac OS X and JDK 1.8.0111 for Windows (both worked).
I've created a DateTimeFormatter with optional sections (delimited by []), to parse both cases (MM/dd/yyyy and yyyy-MM-dd'T'HH:mm:ss).
The same formatter worked for your case (LocalDate), but there are some considerations below.
// parse both formats (use optional section, delimited by [])
DateTimeFormatter parser = DateTimeFormatter.ofPattern("[MM/dd/yyyy][yyyy-MM-dd'T'HH:mm:ss]");
// parse MM/dd/yyyy
LocalDate d1 = LocalDate.parse("10/16/2016", parser);
// parse yyyy-MM-dd'T'HH:mm:ss
LocalDate d2 = LocalDate.parse("2016-10-16T10:20:30", parser);
// parser.format(d1) is the same as d1.format(parser)
System.out.println(parser.format(d1));
System.out.println(parser.format(d2));
The output is:
10/16/2016
10/16/2016
PS: this works only with LocalDate. If I try to format an object with time fields (like LocalDateTime), both formats are used:
System.out.println(parser.format(LocalDateTime.now()));
This prints:
06/18/20172017-06-18T07:40:55
Note that it formatted with both patterns. My guess is that the formatter checks if the object has the fields in each optional section. As the LocalDate has no time fields (hour/minute/second), the second pattern fails and it prints only the first one (MM/dd/yyyy). But the LocalDateTime has all the time fields, and both patterns are valid, so both are used to format.
My conclusion is: this isn't a general solution (like the Joda-Time's version), it's more like a "lucky" case where the patterns involved created the desired situation. But I wouldn't rely on that for all cases.
Anyway, if you are only using LocalDate, you can try to use this code. But if you're working with another types, then you'll probably have to use another formatter for the output, like this:
// parser/formatter for month/day/year
DateTimeFormatter mdy = DateTimeFormatter.ofPattern("MM/dd/yyyy");
// parser for both patterns
DateTimeFormatter parser = new DateTimeFormatterBuilder()
// optional MM/dd/yyyy
.appendOptional(mdy)
// optional yyyy-MM-dd'T'HH:mm:ss (use built-in formatter)
.appendOptional(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// create formatter
.toFormatter();
// parse MM/dd/yyyy
LocalDate d1 = LocalDate.parse("10/16/2016", parser);
// parse yyyy-MM-dd'T'HH:mm:ss
LocalDate d2 = LocalDate.parse("2016-10-16T10:20:30", parser);
// use mdy to format
System.out.println(mdy.format(d1));
System.out.println(mdy.format(d2));
// format object with time fields: using mdy formatter to avoid multiple pattern problem
System.out.println(mdy.format(LocalDateTime.now()));
The output is:
10/16/2016
10/16/2016
06/18/2017
The parsing part can be written, and has been added in the ThreeTen-Extra library. The relevant code is here and is included below for clarity. The key trick is using parseUnresolved() to find out which format is correct:
public static <T> T parseFirstMatching(CharSequence text, TemporalQuery<T> query, DateTimeFormatter... formatters) {
Objects.requireNonNull(text, "text");
Objects.requireNonNull(query, "query");
Objects.requireNonNull(formatters, "formatters");
if (formatters.length == 0) {
throw new DateTimeParseException("No formatters specified", text, 0);
}
if (formatters.length == 1) {
return formatters[0].parse(text, query);
}
for (DateTimeFormatter formatter : formatters) {
try {
ParsePosition pp = new ParsePosition(0);
formatter.parseUnresolved(text, pp);
int len = text.length();
if (pp.getErrorIndex() == -1 && pp.getIndex() == len) {
return formatter.parse(text, query);
}
} catch (RuntimeException ex) {
// should not happen, but ignore if it does
}
}
throw new DateTimeParseException("Text '" + text + "' could not be parsed", text, 0);
}
Unfortunately, there is no way to write a single DateTimeFormatter that supports flexible parsing and prints using a specific output format as per Joda-Time.
What you're asking is not possible.
DateTimeFormatter is a final class, so you cannot subclass it to implement your own behavior.
The constructor is package-private, so you can't call it yourself. The only way to create a DateTimeFormatter is by using a DateTimeFormatterBuilder. Note that the static helper methods for creating a DateTimeFormatter are internally using DateTimeFormatterBuilder, e.g.
public static DateTimeFormatter ofPattern(String pattern) {
return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter();
}
DateTimeFormatterBuilder is also a final class, and cannot be subclassed, and it doesn't provide any methods for supplying multiple alternate formats to use, like you want.
In short, DateTimeFormatter is closed and cannot be extended. If your code can only use a DateTimeFormatter, then you are out of luck.
The Answer by Andreas is correct and should be accepted.
Check length of string
As an alternative, you can simply test the length of your string and apply one of two formatters.
DateTimeFormatter fDateOnly = DateTimeFormatter.ofPattern( "MM/dd/uuuu" ) ;
DateTimeFormatter fDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME ;
LocalDate ld = null ;
if( input.length() == 10 ) {
try {
ld = LocalDate.parse( input , fDateOnly ) ;
} catch (DateTimeParseException e ) {
…
}
} else if ( input.length() == 19 ) {
try {
LocalDateTime ldt = LocalDateTime.parse( input , fDateTime ) ;
ld = ldt.toLocalDate() ;
} catch (DateTimeParseException e ) {
…
}
} else {
// Received unexpected input.
…
}
String output = ld.format( fDateOnly ) ;
Be aware that you can let java.time automatically localize when generating a string representing the value of your date-time rather than hard-code a specific format. See DateTimeFormatter.ofLocalizedDate.
Related
I'm given some datetime format string that user entered, and need to check into what java.time temporals I can parse data in that format (within reason, I indend to support only the simpler cases for now).
Something along the lines of this table:
Input Format
Expected Answer
yyyy-MM
java.time.YearMonth
MM/dd/yyyy
java.time.LocalDate
yyyy:DD
java.time.LocalDate (because of day-of-year data)
HH:mm:ss
java.time.LocalTime
dd MM yyyy hh:mm:ssV
java.time.ZonedDateTime
Keeping in mind, that both the date format and the date are entered by the user, so these input formats are just examples, and they obviously can contain literals (but not optional parts, that's a relief).
So far I've only been able to come up with this tornado of ifs as a solution:
private static final ZonedDateTime ZDT = ZonedDateTime.of(1990, 10, 26, 14, 40, 59, 123456, ZoneId.of("Europe/Oslo"));
...
String formatted = externalFormatter.format(ZDT);
Class<?> type;
TemporalAccessor parsed = externalFormatter.parse(formatted);
if (parsed.isSupported(YEAR)) {
if (parsed.isSupported(MONTH_OF_YEAR)) {
if (parsed.query(TemporalQueries.localDate()) != null) {
if (parsed.query(TemporalQueries.localTime()) != null) {
if (parsed.query(TemporalQueries.zone()) != null) {
type = ZonedDateTime.class;
}
else if (parsed.query(TemporalQueries.offset()) != null) {
type = OffsetDateTime.class;
}
else {
type = LocalDateTime.class;
}
}
else {
type = LocalDate.class;
}
}
else {
type = YearMonth.class;
}
}
else {
type = Year.class;
}
}
else if (parsed.query(TemporalQueries.localTime()) != null) {
if (parsed.query(TemporalQueries.offset()) != null) {
type = OffsetTime.class;
}
else {
type = LocalTime.class;
}
}
Surely, there must be some better way, at least marginally? I will not limit myself to just using java.time, I also have the Joda-Time library available to me (although it's technically on legacy status), and I will not turn down a simpler code that uses the SimpleDateFormat if there is such an option.
DateTimeFormatter::parseBest()
I am taking your word for it:
Keeping in mind, that both the date format and the date are entered by
the user, …
So I am assuming that I may use both the format pattern and the date string entered for seeing what I can make of it. DateTimeFormatter::parseBest() is the method we need for that.
public static TemporalAccessor parse(String formatPattern, String toBeParsed) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern, Locale.ROOT);
return formatter.parseBest(toBeParsed, ZonedDateTime::from,
LocalDate::from, LocalTime::from, YearMonth::from);
}
For demonstrating the power of the above method I am using the following auxiliary method:
public static void demo(String formatPattern, String toBeParsed) {
TemporalAccessor result = parse(formatPattern, toBeParsed);
System.out.format("%-21s %s%n", formatPattern, result.getClass().getName());
}
Let’s call it using your examples:
demo("yyyy-MM", "2017-11");
demo("MM/dd/yyyy", "10/21/2023");
demo("yyyy:DD", "2021:303");
demo("HH:mm:ss", "23:34:45");
demo("dd MM yyyy HH:mm:ssVV", "05 09 2023 14:01:55Europe/Oslo");
I have corrected a couple of errors in the last format pattern string. Output is:
yyyy-MM java.time.YearMonth
MM/dd/yyyy java.time.LocalDate
yyyy:DD java.time.LocalDate
HH:mm:ss java.time.LocalTime
dd MM yyyy HH:mm:ssVV java.time.ZonedDateTime
In the list of TemporalQuery arguments to parseBest() it’s essential to put the classes that hold the most information first. Or which classes you prefer, but I assumed you wanted as much information as you can have. parseBest() uses the first query that works. So put ZonedDateTime first and Year, Month and DayOfWeek last if you intend to support any of the last three.
Remember to decide which locale you want.
With only the format pattern string
If you are supposed to do the trick without the string to be parsed, it’s not badly more complicated. Start by formatting some ZonedDateTime using the formatter that you have constructed from the format pattern string. Then use parseBest() to parse the resulting string back. You must use ZonedDateTime because it’s the only class that holds about every field that could thinkably be in the format pattern string, so with some other class, formatting could break. On the other hand the information coming into the formatted string is limited by the pattern, so in most cases you won’t be able to parse a ZonedDateTime back. Which is exactly what we want: parseBest() will tell us which class we can parse into.
public static Class<? extends TemporalAccessor> getParseableType(String formatPattern) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern, Locale.ROOT);
String example = ZonedDateTime.now(ZoneId.systemDefault()).format(formatter);
TemporalAccessor parseableTemporalAccessor = formatter.parseBest(example, ZonedDateTime::from,
LocalDate::from, LocalTime::from, YearMonth::from);
return parseableTemporalAccessor.getClass();
}
Documentation link
parseBest(CharSequence text, TemporalQuery<?>... queries)
I am currently getting two version of timestamp format eg '2017-04-17 20:33:45.223+05:30' and '2017-04-17 20:33:45+05:30'.My parsing is failing due to dynamic timestamp .Is it possible to handle both of these time stamp with one DateTimeFormatter Pattern .Below is the example code what i tried
val myDate=LocalDateTime.parse("2017-04-17 20:33:45.223+05:30", DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSZ")).toDateTime(DateTimeZone.UTC)//this will fail if time stamp comes with '2017-04-17 20:33:45+05:30
I had seen one way to achieve the same using optional part however I canot make it work
val pattern = "MM/dd/yyyy HH:mm:ss[.SSS]Z"
val fmt = DateTimeFormatter.ofPattern(pattern)
val temporalAccessor = fmt.parse("2017-04-17 20:33:45.223+05:30")
Ant help on this or any suggestion how to handle such cases will be helpful .Thanks in advance .
uuuu-MM-dd
Edit: This fixes it. I am using java.time, the modern Java date and time API, and Java syntax.
private static final DateTimeFormatter FORMATTER
= DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss[.SSS]xxx", Locale.ROOT);
Trying it out:
String[] variants = {
"2017-04-17 20:33:45.223+05:30",
"2017-04-17 20:33:45+05:30",
// Variants we don’t want to accept
"2017-04-17 20:00+05:30",
"2017-04-17 20:00:00.000000+05:30" };
for (String inputString : variants) {
try {
OffsetDateTime dateTime = OffsetDateTime.parse(inputString, FORMATTER);
System.out.println("Parsed: " + dateTime);
} catch (DateTimeParseException dtpe) {
System.out.println("Invalid: " + inputString);
}
}
Output:
Parsed: 2017-04-17T20:33:45.223+05:30
Parsed: 2017-04-17T20:33:45+05:30
Invalid: 2017-04-17 20:00+05:30
Invalid: 2017-04-17 20:00:00.000000+05:30
What went wrong in your code?
You had the right idea for your purpose.
You attempted using the outmoded Joda-Time library. Joda-Time can support optional parts when parsing, but not through the square bracket syntax. Instead its DateTimeFormatterBuilder has got an appendOptional method.
In your java.time code this part of your format pattern string doesn’t match any of your inputs: MM/dd/yyyy. Java parsed 20 as a 2 digit month number (postponing validation of the number) and threw the exception because no slash was found after 20.
Edit 2: why xxx works but Z doesn't:
With Joda-Time’s DateTimeFormat one Z is for offset without colon, for example +0530. ZZ should have worked for +05:30 with colon.
With java.time both x and Z (and also upper case X) are for zone offset. Here too Z is for offset without colon. Either xxx or ZZZZZ works for +05:30.
Use the built-in formatters
Original answer, likely useful for others: This one does it (using Java syntax):
private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral(' ')
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.appendOffsetId()
.toFormatter();
Let’s try it out:
String[] variants = {
"2017-04-17 20:33:45.223+05:30",
"2017-04-17 20:33:45+05:30",
"2017-04-17 20:00+05:30",
"2017-04-17 20:00:00.000000+05:30" };
for (String inputString : variants) {
OffsetDateTime dateTime = OffsetDateTime.parse(inputString, FORMATTER);
System.out.println(dateTime);
}
Output:
2017-04-17T20:33:45.223+05:30
2017-04-17T20:33:45+05:30
2017-04-17T20:00+05:30
2017-04-17T20:00+05:30
I am exploiting the fact that the built-in DateTimeFormatter.ISO_LOCAL_TIME accepts a time both with and without decimals on the seconds. We can reuse existing formatters in our own formatter through a DateTimeFormatterBuilder.
parseBest looks a good fit for this
public TemporalAccessor parseBest(CharSequence text,
TemporalQuery<?>... queries)
Fully parses the text producing an object of one of the specified
types.
This parse method is convenient for use when the parser can handle optional elements. For example, a pattern of 'uuuu-MM-dd HH.mm[ VV]'
can be fully parsed to a ZonedDateTime, or partially parsed to a
LocalDateTime. The queries must be specified in order, starting from
the best matching full-parse option and ending with the worst matching
minimal parse option. The query is typically a method reference to a
from(TemporalAccessor) method.
The result is associated with the first type that successfully parses
I got multiple string date to convert to OffsetDateTime and I did that with multiple try and catch, I think I will not have other DateTimeFormatter to write. So, how to make that more beautiful ?
code:
public static OffsetDateTime convertStringDateToOffsetDate(String dateStr){
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withLocale( Locale.US );
DateTimeFormatter f2 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss").withZone(ZoneId.of("Europe/Paris"));
DateTimeFormatter f3 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX").withZone(ZoneId.of("Europe/Paris"));
DateTimeFormatter f4 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS").withZone(ZoneId.of("Europe/Paris"));
DateTimeFormatter f5 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").withZone(ZoneId.of("Europe/Paris"));
DateTimeFormatter f6 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX").withZone(ZoneId.of("Europe/Paris"));
OffsetDateTime myDate = null;
try{
myDate = ZonedDateTime.parse(dateStr, f).toOffsetDateTime();
} catch(DateTimeParseException e){
try{
myDate = ZonedDateTime.parse(dateStr, f2).toOffsetDateTime();
} catch (DateTimeParseException ex) {
try{
myDate = ZonedDateTime.parse(dateStr, f3).toOffsetDateTime();
} catch (DateTimeParseException exc) {
try{
myDate = ZonedDateTime.parse(dateStr, f4).toOffsetDateTime();
} catch (DateTimeParseException exce) {
try{
myDate = ZonedDateTime.parse(dateStr, f5).toOffsetDateTime();
} catch(DateTimeParseException excep){
myDate = ZonedDateTime.parse(dateStr, f6).toOffsetDateTime();
}
}
}
}
}
return myDate;
}
public static OffsetDateTime convertStringDateToOffsetDate(String dateStr){
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd['T'][ ][HH:mm:ss][.][SSSSSS][SSSSS][SSSS][SSS][XXX][XX][X]").withZone(ZoneId.of("Europe/Paris"));
return ZonedDateTime.parse(dateStr, f).toOffsetDateTime();
}
This should handle all your patterns. No multiple formatters or regex is needed.
You can declare parts of the format string to be optional, using the [] syntax. This may simply get you to a single pattern that takes care of it all. However, this setup where one pattern has US locale but the others don't, that part is not going to fit in a single format string. So, you can reduce the # of format strings you have, but probably not to a single one.
Then, use a list, and a helper method, to achieve clean code:
private static final List<DateTimeFormatter> FORMATS = List.of(
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withLocale( Locale.US ),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.][SSSSSS][XXX]").withZone(ZoneId.of("Europe/Paris")));
public static OffsetDateTime parse(String dateStr) throws DateTimeParseException {
DateTimeParseException ex = null;
for (var format : FORMATS) try {
return ZonedDateTime.parse(dateStr, format).toOffsetDateTime();
} catch (DateTimeParseException e) {
ex = e;
}
throw ex;
}
Here’s my stab at it.
private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.optionalStart()
.appendOffsetId()
.optionalEnd()
.toFormatter(Locale.ROOT);
private static final ZoneId DEFAULT_ZONE = ZoneId.of("Europe/Paris");
public static OffsetDateTime convertStringDateToOffsetDate(String dateStr) {
TemporalAccessor parsed
= PARSER.parseBest(dateStr, OffsetDateTime::from, LocalDateTime::from);
if (parsed instanceof OffsetDateTime) {
return (OffsetDateTime) parsed;
} else {
return ((LocalDateTime) parsed).atZone(DEFAULT_ZONE).toOffsetDateTime();
}
}
To try it out:
String[] testStrings = {
"2021-01-01T12:34:56.789-07:00",
"2021-02-01T12:34:56",
"2021-03-01T12:34:56-06:00",
"2021-04-01T12:34:56.987654",
"2021-05-01T12:34:56.789",
"2021-06-01T12:34:56.987654-05:00"
};
for (String testString : testStrings) {
System.out.format("%-32s -> %s%n", testString, convertStringDateToOffsetDate(testString));
}
Output:
2021-01-01T12:34:56.789-07:00 -> 2021-01-01T12:34:56.789-07:00
2021-02-01T12:34:56 -> 2021-02-01T12:34:56+01:00
2021-03-01T12:34:56-06:00 -> 2021-03-01T12:34:56-06:00
2021-04-01T12:34:56.987654 -> 2021-04-01T12:34:56.987654+02:00
2021-05-01T12:34:56.789 -> 2021-05-01T12:34:56.789+02:00
2021-06-01T12:34:56.987654-05:00 -> 2021-06-01T12:34:56.987654-05:00
You notice:
It handles all 6 formats from your question.
For the strings that have a UTC offset in them, the offset is retained. For strings that haven’t got one, the correct offset for Paris is assumed (+01:00 in February and +02:00 in April and May).
I believe that it has the following advantages:
I need only one formatter.
I have written no format pattern string at all, just assembled my formatter from built-in parts.
The DateTimeFormatter.parseBest method that I use to parse will try first to create an OffsetDateTime and if unsuccessful, it will resort to creating and returning a LocalDateTime. In the latter case I will need to convert it. The downside of my solution is that I need to go through a TemporalAccessor, which is an interface that I consider low-level and that we usually should not use in application code.
The built-in DateTimeFOrmatter.ISO_LOCAL_DATE_TIME already handles the presence and absence of up to 9 decimals on the seconds. So by reusing this in my formatter I already handle the cases of no decimals and of 3 and 6 decimals.
One challenge of your requirement to use Europe/Paris time zone for the strings that haven’t got an offset in them is, while a DateTimeFormatter can have many default values, it cannot have a default time zone. The withZone method gives us a formatter with an override zone, but this is something else. That formatter will enforce the override zone on the result of either formatting or parsing. While it wasn’t clear from your question I was assuming that you didn’t want this.
Edit: does the formatter need a locale? I used .toFormatter(Locale.ROOT) for building the formatter from the builder. Technically the locale isn’t necessary in this case since my formatter doesn’t include any parts that depend on locale, and in the first version of this answer I had left the locale out (calling the no-arg toFormatter method instead). However I tend to agree with Arvind Kumar Avinash in the comment:
Just small nitpicking: Please always use Locale with a date-time
parsing/formatting type … because it is a Locale-sensitive type. It
may not be relevant for the date-time strings dealt with in this
solution but we should stick to it as if it were a rule.
It was probably just me being arrogant and assuming that the reader was able to determine that there were no locale-sensitive parts in the formatter. Supplying a locale is the better habit (otherwise at least stick in a comment why there isn’t one).
You can use a single try-catch inside a loop, where the exception gets ignored.
List<DateTimeFormatter> list = Arrays.toList<>(f, f1, f2, f3, f4, f5, f6);
for(DateTimeFormatter formatter : list)
{
try
{
myDate = ZonedDateTime.parse(dateStr, formatter).toOffsetDateTime();
break;
}
catch(Exception e)
{
}
}
But keep in mind that exceptions have a bad performance (writing stacktrace to variable takes time), so maybe the comment from M. Dudek to use regex could be the better answer.
I am working on a REST API which supports Date as a query param. Since it is Query param it will be String. Now the Date can be sent in the following formats in the QueryParams:
yyyy-mm-dd[(T| )HH:MM:SS[.fff]][(+|-)NNNN]
It means following are valid dates:
2017-05-05 00:00:00.000+0000
2017-05-05 00:00:00.000
2017-05-05T00:00:00
2017-05-05+0000
2017-05-05
Now to parse all these different date-times i am using Java8 datetime api. The code is as shown below:
DateTimeFormatter formatter = new DateTimeFormatterBuilder().parseCaseInsensitive()
.append(DateTimeFormatter.ofPattern("yyyy-MM-dd[[ ][['T'][ ]HH:mm:ss[.SSS]][Z]"))
.toFormatter();
LocalDateTime localDateTime = null;
LocalDate localDate = null;
ZoneId zoneId = ZoneId.of(ZoneOffset.UTC.getId());
Date date = null;
try {
localDateTime = LocalDateTime.parse(datetime, formatter);
date = Date.from(localDateTime.atZone(zoneId).toInstant());
} catch (Exception exception) {
System.out.println("Inside Excpetion");
localDate = LocalDate.parse(datetime, formatter);
date = Date.from(localDate.atStartOfDay(zoneId).toInstant());
}
As can be seens from the code I am using DateTimeFormatter and appending a pattern. Now I am first trying to parse date as LocalDateTime in the try-block and if it throws an exception for cases like 2017-05-05 as no time is passed, I am using a LocalDate in the catch block.
The above approach is giving me the solution I am looking for but my questions are that is this the standard way to deal with date sent as String and is my approach is in line with those standards?
Also, If possible what is the other way I can parse the different kinds of date (shown as the Valid dates above) except some other straightforward solutions like using an Array list and putting all the possible formats and then using for-loop trying to parse the date?
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
// time is optional
.optionalStart()
.parseCaseInsensitive()
.appendPattern("[ ]['T']")
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.optionalEnd()
// offset is optional
.appendPattern("[xx]")
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
.toFormatter();
for (String queryParam : new String[] {
"2017-05-05 00:00:00.000+0000",
"2017-05-05 00:00:00.000",
"2017-05-05T00:00:00",
"2017-05-05+0000",
"2017-05-05",
"2017-05-05T11:20:30.643+0000",
"2017-05-05 16:25:09.897+0000",
"2017-05-05 22:13:55.996",
"2017-05-05t02:24:01"
}) {
Instant inst = OffsetDateTime.parse(queryParam, formatter).toInstant();
System.out.println(inst);
}
The output from this snippet is:
2017-05-05T00:00:00Z
2017-05-05T00:00:00Z
2017-05-05T00:00:00Z
2017-05-05T00:00:00Z
2017-05-05T00:00:00Z
2017-05-05T11:20:30.643Z
2017-05-05T16:25:09.897Z
2017-05-05T22:13:55.996Z
2017-05-05T02:24:01Z
The tricks I am using include:
Optional parts may be included in either optionalStart/optionalEnd or in [] in a pattern. I use both, each where I find it easier to read, and you may prefer differently.
There are already predefined formatters for date and time of day, so I reuse those. In particular I take advantage of the fact that DateTimeFormatter.ISO_LOCAL_TIME already handles optional seconds and fraction of second.
For parsing into an OffsetDateTime to work we need to supply default values for the parts that may be missing in the query parameter. parseDefaulting does this.
In your code you are converting to a Date. The java.util.Date class is long outdated and has a number of design problems, so avoid it if you can. Instant will do fine. If you do need a Date for a legacy API that you cannot change or don’t want to change just now, convert in the same way as you do in the question.
EDIT: Now defaulting HOUR_OF_DAY, not MILLI_OF_DAY. The latter caused a conflict when only the millis were missing, but it seems the formatter is happy with just default hour of day when the time is missing.
I usually use the DateUtils.parseDate which belongs to commons-lang.
This method looks like this:
public static Date parseDate(String str,
String... parsePatterns)
throws ParseException
Here is the description:
Parses a string representing a date by trying a variety of different parsers.
The parse will try each parse pattern in turn. A parse is only deemed successful if it parses the whole of the input string. If no parse patterns match, a ParseException is thrown.
The parser will be lenient toward the parsed date.
#Configuration
public class DateTimeConfig extends WebMvcConfigurationSupport {
/**
* https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#format-configuring-formatting-globaldatetimeformat
* #return
*/
#Bean
#Override
public FormattingConversionService mvcConversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// Register JSR-310 date conversion with a specific global format
DateTimeFormatterRegistrar dateTimeRegistrar = new DateTimeFormatterRegistrar();
dateTimeRegistrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
dateTimeRegistrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
dateTimeRegistrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
dateTimeRegistrar.registerFormatters(conversionService);
// Register date conversion with a specific global format
DateFormatterRegistrar dateRegistrar = new DateFormatterRegistrar();
dateRegistrar.setFormatter(new DateFormatter("yyyy-MM-dd"));
dateRegistrar.setFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
dateRegistrar.setFormatter(new DateFormatter("yyyy-MM-dd'T'HH:mm:ss'Z'"));
dateRegistrar.registerFormatters(conversionService);
return conversionService;
}
}
My requirement is to validate that a date String is in the correct format based on a set of valid formats specified.
Valid formats:
MM/dd/yy
MM/dd/yyyy
I created a simple test method that uses the Java 8 DateTimeFormatterBuilder to create a flexible formatter that supports multiple optional formats. Here is the code:
public static void test() {
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy"))
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
.toFormatter();
String dateString = "10/30/2017";
try {
LocalDate.parse(dateString, formatter);
System.out.println(dateString + " has a valid date format");
} catch (Exception e) {
System.out.println(dateString + " has an invalid date format");
}
}
When I run this, here is the output
10/30/2017 has an invalid date format
As you see in the code, the valid date formats are MM/dd/yy and MM/dd/yyyy.
My expectation was that the date 10/30/2017 should be valid as it matches MM/dd/yyyy. However, 10/30/2017 is being reported as invalid.
What is going wrong ? Why is this not working ?
I also tried
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy[yy]"))
in place of
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy"))
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
but still had the same issue.
This code runs as expected if I use:
String dateString = "10/30/17";
in place of
String dateString = "10/30/2017";
I have 2 questions
What is going wrong here ? Why is it not working for "10/30/2017" ?
Using Java 8, how to correctly create a flexible Date formatter (a formatter that supports multiple optional formats) ? I know the use of [] to create optional sections in the pattern string itself. I'm looking for something more similar to what I am trying (avoiding [] inside the pattern string and using separate optional clauses for each separate format string)
The formatter does not work the way you expect, the optional part means
if there is nothing extra attached to the first pattern (e.g., "MM/dd/yy"), that is fine,
if there is something extra, it needs to match the second pattern (e.g, "MM/dd/yyyy")
To make it a bit clearer, try to run the sample code below to understand it better:
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy"))
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
.toFormatter();
String[] dateStrings = {
"10/30/17", // valid
"10/30/2017", // invalid
"10/30/1710/30/2017", // valid
"10/30/201710/30/17" // invalid
};
for (String dateString : dateStrings) {
try {
LocalDate.parse(dateString, formatter);
System.out.println(dateString + " has a valid date format");
} catch (Exception e) {
System.err.println(dateString + " has an invalid date format");
}
}
==
10/30/17 has a valid date format
10/30/1710/30/2017 has a valid date format
10/30/2017 has an invalid date format
10/30/201710/30/17 has an invalid date format
==
This is only a simple solution, if performance is of your concern, the validation by catching the parsing exception should be the last resort
you may check the string by length or regex first before doing the date string parsing
you may also replace the stream with a method containing a simple for loop, etc.
String[] patterns = { "MM/dd/yy", "MM/dd/yyyy" };
Map<String, DateTimeFormatter> formatters = Stream.of(patterns).collect(Collectors.toMap(
pattern -> pattern,
pattern -> new DateTimeFormatterBuilder().appendOptional(DateTimeFormatter.ofPattern(pattern)).toFormatter()
));
String dateString = "10/30/17";
boolean valid = formatters.entrySet().stream().anyMatch(entry -> {
// relying on catching parsing exception will have serious expense on performance
// a simple check will already improve a lot
if (dateString.length() == entry.getKey().length()) {
try {
LocalDate.parse(dateString, entry.getValue());
return true;
}
catch (DateTimeParseException e) {
// ignore or log it
}
}
return false;
});
The builder's appendValueReduced() method was designed to handle this case.
When parsing a complete value for a field, the formatter will treat it as an absolute value.
When parsing an partial value for a field, the formatter will interpret it relative to a base that you specify. For example, if you want two-digit years to be interpreted as being between 1970 and 2069, you can specify 1970 as your base. Here's an illustration:
LocalDate century = LocalDate.ofEpochDay(0); /* Beginning Jan. 1, 1970 */
DateTimeFormatter f = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ofPattern("MM/dd/"))
.appendValueReduced(ChronoField.YEAR, 2, 4, century)
.toFormatter();
System.out.println(LocalDate.parse("10/30/2017", f)); /* 2017-10-30 */
System.out.println(LocalDate.parse("10/30/17", f)); /* 2017-10-30 */
System.out.println(LocalDate.parse("12/28/1969", f)); /* 1969-12-28 */
System.out.println(LocalDate.parse("12/28/69", f)); /* 2069-12-28 */