Optional parts in SimpleDateFormat

JavaDatetimeLocalizationSimpledateformat

Java Problem Overview


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?

Java Solutions


Solution 1 - Java

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.

Solution 2 - Java

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'

Solution 3 - Java

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"));

Solution 4 - Java

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();

Solution 5 - Java

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

Solution 6 - Java

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));
    }

Solution 7 - Java

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]

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiontraffichazardView Question on Stackoverflow
Solution 1 - JavaBrett RyanView Answer on Stackoverflow
Solution 2 - JavatbraunView Answer on Stackoverflow
Solution 3 - JavaPeter LawreyView Answer on Stackoverflow
Solution 4 - JavahghView Answer on Stackoverflow
Solution 5 - JavaJohan SjöbergView Answer on Stackoverflow
Solution 6 - JavaGustyWindView Answer on Stackoverflow
Solution 7 - JavasmartricsView Answer on Stackoverflow