Teaser Image

qnoid

Markos Charatzas - Edinburgh, UK




/**
     * Don't use *Util classes as they are deemed to have throw away code
     */
    public class DateUtil
    {
        private static final DateFormat ddMMyyyyDashed = new SimpleDateFormat("dd-MM-yyyy");
        private static final DateFormat yyyyMMddDashed = new SimpleDateFormat("yyyy-MM-dd");
        private static final DateFormat ddMMyyyySlashed = new SimpleDateFormat("dd/MM/yyyy");
        private static final DateFormat yyyyMMddSlashed = new SimpleDateFormat("yyyy/MM/dd");
         
        public static Date textToDate(String text) 
        {
            Date date = null;
     
            try {
                date = ddMMyyyyDashed.parse(text);
            } catch (java.text.ParseException e) {
            }
     
            try {
                date = yyyyMMddDashed.parse(text);
            } catch (java.text.ParseException e) {
            }
            try {
                date = ddMMyyyySlashed.parse(text);
            } catch (java.text.ParseException e) {
            }
            try {
                date = yyyyMMddSlashed.parse(text);
            } catch (java.text.ParseException e) {
            }
     
            System.out.println("Date is: " + date);
        return date;            
        }
    }

    /**
     *
     * A date format that can be used to parse a date using a number of 
     * {@link DateFormat}s
     */
    public class CompositeDateFormat
    {
        private static final DateFormat ddMMyyyyDashed = new SimpleDateFormat("dd-MM-yyyy"){
            {
                this.setLenient(false);
            }
        };
        private static final DateFormat yyyyMMddDashed = new SimpleDateFormat("yyyy-MM-dd"){
            {
                this.setLenient(false);
            }
        };
        private static final DateFormat ddMMyyyySlashed = new SimpleDateFormat("dd/MM/yyyy"){
            {
                this.setLenient(false);
            }
        };
        private static final DateFormat yyyyMMddSlashed = new SimpleDateFormat("yyyy/MM/dd"){
            {
                this.setLenient(false);
            }
        };

        /**
         * @return a new {@link CompositeDateFormat} which will parse any date in
         * <ul>
         *  <li>dd-MM-yyyy</li>
         *  <li>yyyy-MM-dd</li>
         *  <li>dd/MM/yyyy</li>
         *  <li>yyyy/MM/dd</li>
         * </ul>
         * 
         * format
         * 
         * @see #parse(String)
         */
        public static CompositeDateFormat newDateFormat(){
        return newDateFormat(ddMMyyyyDashed, yyyyMMddDashed, ddMMyyyySlashed, yyyyMMddSlashed);
        }
        
        /**
         * Create a new CompositeDateFormat which will parse the dates in the same order as the DateFormat(s) are supplied
         * @param dateformats the date formats to include when parsing a date using
         * this format
         * @return a new {@link CompositeDateFormat} with the specified formats
         */
        public static CompositeDateFormat newDateFormat(DateFormat... dateformats){
        return new CompositeDateFormat(dateformats);
        }     

        private final DateFormat[] dateformats;
        
        /**
         * @param dateformats the date formats to use when parsing a date
         */
        private CompositeDateFormat(DateFormat... dateformats)
        {
            this.dateformats = dateformats;
        }

        /**
         * Will try and parse the source using any of the date formatters.
         * On the first one that succeeds will skip the remaining. 
         * @see DateFormat#parse(String)
         */
        public Date parse(String source) throws ParseException 
        {
            for (DateFormat df : dateformats)
            {
                try {
                    return df.parse(source);
                }
                catch (ParseException e) {
                    //better log this
                }
            }
            
        throw new ParseException("Please make up your mind already ", 0);
        }
    }

    /**
     * A test to come with it
     */
    @RunWith(Theories.class)
    public class CompositeDateFormatTest
    {
        @DataPoints
        public static final String[] DATES = {"05-10-2010", "2010-10-05", "05/10/2010", "2010/10/05"};
        
        @Theory
        public void parse(String source) throws Exception
        {
            CompositeDateFormat newDateFormat = CompositeDateFormat.newDateFormat();
            Date date = newDateFormat.parse(source);
            
            Assert.assertNotNull(date);
        }
        
        @Test(expected=ParseException.class)
        public void parse() throws Exception
        {
            CompositeDateFormat newDateFormat = CompositeDateFormat.newDateFormat();
            newDateFormat.parse("Can't touch this!");
        }
    }

A friend of mine came across this somehow difficult situation where he had to parse a date only that he has no way of knowing the format.

In cases like these we usually apply "brute force" to overcome them as they look rather trivial and can't be bothered.

However, it's good if we hold our train of thoughts as this will lead to a code that is easier to extend, reuse, maintain and even understand in days to come.

So what's wrong with the DateUtils class? Well, a couple of things.

Not extensible

Not reusable

  • You can't reuse instances of the DateUtil class since the method is static.
  • It doesn't extend DateFormat so you have to add new code to accommodate for your DateUtil which is merely a different implementation for parsing dates. ([Disclaimer])

Validates the contract of Format#parse(String)

  • The contract of the method #parse(String):Date defines a throw ParseException in case an error occurs during parsing. Your client code using any date format to parse the dates might look like it's behaving correctly since it's not catching an expected Exception.

  • You are forced to check for null every time you use that way of parsing dates.

As you can see following principles isn't all that subjective as you might think. So the next time you decide to violate any at least do it for the right reasons. If there are any that is.

Software development shouldn't be a throwaway process.

Disclaimer Your custom class should extend the DateFormat class. However, DateFormat as an abstract class forces you to also override both

  • #format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition)
  • #parse(String source, ParsePosition pos)

which on its own is a chapter on API design. Ideally an interface should have been present which holds the single method #parse(String):Date which you should implement.

Kudoz Heinz Kabutz who wrote an article on the cost of causing Exceptions. Not performance wise, my personal opinion is that the DateFormat should have a method #isParsable(String) which can be used instead.

Update While writing this post it came to my attention that all of the date formats should have lenient to false as you risk interpreting a date with a wrong format. Another reason why you should test even trivial code!

By default, parsing is lenient: If the input is not in the form used by this object's format method but can still be parsed as a date, then the parse succeeds. Clients may insist on strict adherence to the format by calling setLenient(false).*

Also the order of the DateFormat(s) can play an important role as you may find yourself parsing a date wrong for cases where the day is <=12 and a SimpleDateFormat("MM-dd-yyyy") comes before a new SimpleDateFormat("dd-MM-yyyy"). Though in some cases a different ordering isn't sufficient.