I'm reading in date strings that could be with or without a time zone adjustment: yyyyMMddHHmmssz
or yyyyMMddHHmmss
. When a string is missing a zone, I'll treat it as GMT. I'm not seeing any way to create optional sections in a SimpleDateFormat
, but maybe I'm missing something. Is there a way to do this with a SimpleDateFormat
, or should I just write a new concrete DateFormat
to handle this?

- 28,416
- 10
- 82
- 109

- 1,247
- 1
- 9
- 13
-
Should have mentioned- it is a requirement that there be only one DateFormat object involved here, because I'm passing it into a library which will use it for formatting. – traffichazard May 06 '11 at 02:49
-
Similar: http://stackoverflow.com/questions/4515023/how-do-i-create-a-dateformat-with-an-optional-time-argument – Vadzim Jun 08 '15 at 19:18
-
FYI, the troublesome old date-time classes such as [`java.util.Date`](https://docs.oracle.com/javase/9/docs/api/java/util/Date.html), [`java.util.Calendar`](https://docs.oracle.com/javase/9/docs/api/java/util/Calendar.html), and `java.text.SimpleDateFormat` are now [legacy](https://en.wikipedia.org/wiki/Legacy_system), supplanted by the [*java.time*](https://docs.oracle.com/javase/9/docs/api/java/time/package-summary.html) classes built into Java 8 & Java 9. See [*Tutorial* by Oracle](https://docs.oracle.com/javase/tutorial/datetime/TOC.html). – Basil Bourque Mar 07 '18 at 23:08
7 Answers
JSR-310 has been delivered with Java 8 which provides enhanced support for parsing temporal values where components may now be optional. Not only can you make the zone optional, but you can also make the time component optional and return the correct temporal unit for the given string.
Consider the following test cases.
public class DateFormatTest {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
"yyyy-MM-dd[[ ]['T']HH:mm[:ss][XXX]]");
private TemporalAccessor parse(String v) {
return formatter.parseBest(v,
ZonedDateTime::from,
LocalDateTime::from,
LocalDate::from);
}
@Test public void testDateTime1() {
assertEquals(LocalDateTime.of(2014, 9, 23, 14, 20, 59),
parse("2014-09-23T14:20:59"));
}
@Test public void testDateTime2() {
assertEquals(LocalDateTime.of(2014, 9, 23, 14, 20),
parse("2014-09-23 14:20"));
}
@Test public void testDateOnly() {
assertEquals(LocalDate.of(2014, 9, 23), parse("2014-09-23"));
}
@Test public void testZonedDateTime() {
assertEquals(ZonedDateTime.of(2014, 9, 23, 14, 20, 59, 0,
ZoneOffset.ofHoursMinutes(10, 30)),
parse("2014-09-23T14:20:59+10:30"));
}
}
Here the DateTimeFormatter pattern of "yyyy-MM-dd[[ ]['T']HH:mm[:ss][XXX]]"
allows optionals within the square parentheses which can also be nested. Patterns can also be constructed from a DateTimeFormatterBuilder, which the above pattern is demonstrated here:
private final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.optionalStart()
.optionalStart()
.appendLiteral(' ')
.optionalEnd()
.optionalStart()
.appendLiteral('T')
.optionalEnd()
.appendOptional(DateTimeFormatter.ISO_TIME)
.toFormatter();
This would translate to an expression which looks like the following:
yyyy-MM-dd[[' ']['T']HH:mm[':'ss[.SSS]]].
Optional values can be nested and are also auto closed at the end if still open. Note however that there is no way to provide an exclusive OR on optional parts, thus the above format would actually parse the following value quite fine:
2018-03-08 T11:12
Note the really neat capability that we can reuse existing formatter's as parts of our current format.

- 26,937
- 30
- 128
- 163
-
-
2No, the multiple optional's are correct, it would be equivalent something like `yyyy-MM-dd[[' ']['T']HH:mm[':'ss[.SSS]]]`. Optional values can be nested, they are also auto closed at the end if still open. – Brett Ryan Mar 07 '18 at 23:19
-
1Thanks for the info! I suggest you add that explanation to your Answer. – Basil Bourque Mar 07 '18 at 23:40
-
1No problem, have added. Note the caveat that you can't provide exclusive optional values, though I think that's a small problem :) – Brett Ryan Mar 08 '18 at 00:16
-
-
I know this is an old post but just for the record...
Apache DateUtils class can help you with that.
String[] acceptedFormats = {"dd/MM/yyyy","dd/MM/yyyy HH:mm","dd/MM/yyyy HH:mm:ss"};
Date date1 = DateUtils.parseDate("12/07/2012", acceptedFormats);
Date date2 = DateUtils.parseDate("12/07/2012 23:59:59", acceptedFormats);
Link to the Apache Commons Lang library in Maven repository, you might want to check what's the latest version:
http://mvnrepository.com/artifact/org.apache.commons/commons-lang3
Maven v3.4
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
Gradle v3.4
'org.apache.commons:commons-lang3:3.4'

- 2,636
- 31
- 26
-
Just a warning: I had strange results with `DateUtils.parseDate` method regarding timezones. It kept adjusting the time it parsed as if I read the date in UTC time, when in fact I read it in local time. – Diederik Feb 26 '13 at 13:31
-
2@Diederik, could you elaborate on that? Did you have timezones in your format? Was the behavior documented? Was it a bug? If so, did you file a bug report? – aioobe Sep 18 '13 at 16:10
-
@aioobe if I recall correctly, the date I parsed had the timezone set to GMT+2. The parser however kept subtracting 2 hours from the time. This was not the expected behaviour. I didn't want the UTC time, I wanted the full time, with timezone info all parsed correctly. E.g I parsed the time string `08:43:53.594+0200` but when I queried the parsed date object, it had `6` for the hour field. – Diederik Sep 19 '13 at 06:49
I would create two SimpleDateFormat, one with a time zone and one without. You can look at the length of the String to determine which one to use.
Sounds like you need a DateFormat which delegates to two different SDF.
DateFormat df = new DateFormat() {
static final String FORMAT1 = "yyyyMMddHHmmss";
static final String FORMAT2 = "yyyyMMddHHmmssz";
final SimpleDateFormat sdf1 = new SimpleDateFormat(FORMAT1);
final SimpleDateFormat sdf2 = new SimpleDateFormat(FORMAT2);
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
throw new UnsupportedOperationException();
}
@Override
public Date parse(String source, ParsePosition pos) {
if (source.length() - pos.getIndex() == FORMAT1.length())
return sdf1.parse(source, pos);
return sdf2.parse(source, pos);
}
};
System.out.println(df.parse("20110102030405"));
System.out.println(df.parse("20110102030405PST"));

- 525,659
- 79
- 751
- 1,130
-
a library i'm using requires me to pass it one dateformat object, so unfortunately this won't work. – traffichazard May 06 '11 at 02:47
-
1cool, that's what i was hoping I didn't have to do, but it's really helpful that you've confirmed that it's the way to go. – traffichazard May 06 '11 at 11:08
-
Great approach - facading formatters behind a superformatter, capable to handle when to use what.. – Illarion Kovalchuk Oct 27 '12 at 14:08
-
1Good but still there is the possibility that completely different date formats can be equal on length. – Timuçin Feb 02 '14 at 19:20
If you can use Joda Datetime it supports optional parts in formatter, for example, "yyyy-MM-dd [hh:mm:ss]"
private DateTimeFormatter fmt = new DateTimeFormatterBuilder()
.append(DateTimeFormat.forPattern("yyyy-MM-dd"))
.appendOptional(
new DateTimeFormatterBuilder()
.appendLiteral(' ')
.append(DateTimeFormat.forPattern("HH:mm:ss"))
.toParser()
.toFormatter();

- 831
- 6
- 3
I would loop over the list of potential DateFormat
objects using a try-catch
operation to break the loop on the first successful parse.

- 47,929
- 21
- 130
- 148
-
You can match the String length to the SDF which will accept them. This is faster and creates less Exceptions. – Peter Lawrey May 05 '11 at 12:02
-
1@Peter, In this particular situation, yes. I guess it depends on how generic he wants the code to be. – Johan Sjöberg May 05 '11 at 12:04
-
This is an old post, but I would avoid this approach, since try-catch is not intended to control the logic or to make decisions about what to do next. – Steve Storck Mar 02 '21 at 12:25
I have solved a similar problem some time ago by extending SimpleDateFormat. Below an crude implementation to show the idea of my solution. It may not be fully complete/optimised.
public class MySimpleDateFormat extends SimpleDateFormat {
private static final long serialVersionUID = 1L;
private static String FORMAT = "
private static int FORMAT_LEN = "yyyyMMddHHmmss".length();
private static String TZ_ID = "GMT";
public MySimpleDateFormat() {
this(TimeZone.getTimeZone(TZ_ID));
}
public MySimpleDateFormat(TimeZone tz) {
super(FORMAT);
setTimeZone(tz);
}
@Override
public Date parse(String source, ParsePosition pos) {
// TODO: args validation
int offset = pos.getIndex() + FORMAT_LEN;
Date result;
if (offset < source.length()) {
// there maybe is a timezone
result = super.parse(source, pos);
if (result != null) {
return result;
}
if (pos.getErrorIndex() >= offset) {
// there isn't a TZ after all
String part0 = source.substring(0, offset);
String part1 = source.substring(offset);
ParsePosition anotherPos = new ParsePosition(pos.getIndex());
result = super.parse(part0 + TZ_ID + part1, anotherPos);
if(result == null) {
pos.setErrorIndex(anotherPos.getErrorIndex());
} else {
// check SimpleDateFormat#parse javadoc to implement correctly the pos updates
pos.setErrorIndex(-1);
pos.setIndex(offset);
}
return result;
}
// there's something wrong with the first FORMAT_LEN chars
return null;
}
result = super.parse(source + TZ_ID, pos);
if(result != null) {
pos.setIndex(pos.getIndex() - TZ_ID.length());
}
return result;
}
public static void main(String [] args) {
ParsePosition pos = new ParsePosition(0);
MySimpleDateFormat mySdf = new MySimpleDateFormat();
System.out.println(mySdf.parse("20120622131415", pos) + " -- " + pos);
pos = new ParsePosition(0);
System.out.println(mySdf.parse("20120622131415GMT", pos) + " -- " + pos);
pos = new ParsePosition(0);
System.out.println(mySdf.parse("20120622131415xxx", pos) + " -- " + pos);
pos = new ParsePosition(0);
System.out.println(mySdf.parse("20120x22131415xxx", pos) + " -- " + pos);
}
}
The gist is that you need to check the input string and "guess" somehow that the TZ field is missing, add it if so and then let the SimpleDateFormat#parse(String, ParsePosition)
do the rest. The implementation above isn't updating ParsePosition according to the contract in javadoc in SimpleDateFormat#parse(String, ParsePosition)
The class has a single default ctor as there's only one format allowed.
The method MySimpleDateFormat#parse(String, ParsePosition)
is invoked by SimpleDateFormat#parse(String)
so it's sufficient to cover both cases.
Running the main() this is the output (as expected)
Fri Jun 22 14:14:15 BST 2012 -- java.text.ParsePosition[index=14,errorIndex=-1]
Fri Jun 22 14:14:15 BST 2012 -- java.text.ParsePosition[index=17,errorIndex=-1]
Fri Jun 22 14:14:15 BST 2012 -- java.text.ParsePosition[index=14,errorIndex=-1]
null -- java.text.ParsePosition[index=0,errorIndex=5]

- 296
- 1
- 6
You can create two different SimpleDateFormats
like
public PWMDateTimeFormatter(String aPatternStr)
{
_formatter = DateTimeFormat.forPattern(aPatternStr);
}
public PWMDateTimeFormatter(String aPatternStr, TimeZone aZone)
{
_formatter = DateTimeFormat.forPattern(aPatternStr).withZone(XXDateTime._getTimeZone(aZone));
}

- 3,026
- 3
- 41
- 50