4
\$\begingroup\$

I want to parse a date string with the following general format: Weekday, DD-Mon-YYYY HH:MM:SS. The calendar should also accept:

  1. a date without the weekday
  2. spaces instead of dashes
  3. case-insensitive month (e.g., allow "Jan", "JAN" and "jAn")
  4. two-digit year
  5. a missing timezone
  6. allow multiple spaces wherever a single space is allowed.

Return null when:

Input isn't a valid date string, meaning only checks that don't require connecting different. Example: the weekday string "XXX" is invalid but "Fri, 09-Jun-2015" is considered valid even though it was a Tuesday

My code:

public static Calendar parseDate(String input) {
    List<String> months = Arrays.asList("jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec");
    Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    Pattern pattern = Pattern.compile ("(?:[A-Z][a-z][a-z],\\s+)?([0-2][0-9]|[3][0-1])(?:\\s+|-)([a-zA-z]{3})(?:\\s+|-)([0-9]{2,4})(?:\\s+)([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-9]{2})(?:\\s+GMT|$)");
    Matcher matcher = pattern.matcher (input);
    Calendar calendar = Calendar.getInstance (TimeZone.getTimeZone ("GTM"));
    if ( !matcher.find () ){
        return null;
    }
    int dayOfMonth = Integer.parseInt (matcher.group (1)); //since the first group is the time zone
    int month = months.indexOf (months.indexOf(matcher.group(2).toLowerCase()));
    int year = Integer.parseInt (matcher.group (3));
    if (year >= 0 && year <= 69){
        year += 2000;
    }
    if ( year >= 70 && year <= 99 ) {
        year += 1900;
    }
    int hours = Integer.parseInt(matcher.group(4));
    int minutes = Integer.parseInt(matcher.group(5));
    int seconds = Integer.parseInt(matcher.group(6));
    cal.set(year, month, dayOfMonth, hours, minutes, seconds);
    return calendar;
}

My question is how to improve "(?:[A-Z][a-z][a-z],\\s+)?([0-2][0-9]|[3][0-1])(?:\\s+|-)([a-zA-z]{3})(?:\\s+|-)([0-9]{2,4})(?:\\s+)([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-9]{2})(?:\\s+GMT|$)"?
EDIT: I can only import regex matcher and pattern and ArrayList, and the only additional methods allowed are: months.indexOf, Calendar.set, Integer.parseInt and String.toLowerCase
Tester:

public void testParseDate()  {
        DateFormat df = DateFormat.getDateTimeInstance();
        df.setTimeZone(TimeZone.getTimeZone("GMT"));

        ArrayList<String> inputs = new ArrayList<String>();
        ArrayList<Long> expect = new ArrayList<Long>();

        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        cal.set(Calendar.MILLISECOND, 0);

        cal.set(2012,4,23,1,23,31);
        long time1 = cal.getTimeInMillis();

        inputs.add("Wed, 23-May-12 01:23:31 GMT"); expect.add(time1);   
        inputs.add("23-May-12 01:23:31 GMT"); expect.add(time1);    
        inputs.add("23-May-12 01:23:31"); expect.add(time1);    
        inputs.add("23 May 2012 01:23:31 GMT"); expect.add(time1);  
        inputs.add("23 May  2012   01:23:31     GMT"); expect.add(time1);
        inputs.add("Wed,   23 May  2012   01:23:31     GMT"); expect.add(time1);
        inputs.add("23 mAy 2012 01:23:31 GMT"); expect.add(time1);
        inputs.add("23 maY 12 01:23:31"); expect.add(time1);
        inputs.add("23 jan 12 01:23:31"); cal.set(2012,0,23,1,23,31); expect.add(cal.getTimeInMillis());
        inputs.add("01 AUG 12 15:23:31 GMT"); cal.set(2012,7,1,15,23,31); expect.add(cal.getTimeInMillis());
        inputs.add("01 bla 12 15:23:31 GMT"); expect.add(null);
        inputs.add("1 May 2012 15:23:31 GMT"); expect.add(null);
        inputs.add("01 May 2012 15:23:31 BLA"); expect.add(null);
        inputs.add("01 May 2012-15:23:31"); expect.add(null);
        inputs.add("01 May 2012-15/23:31"); expect.add(null);
        inputs.add("01 May 2012-15/23:31"); expect.add(null);

        for (int i = 0; i < inputs.size(); ++i) {
            Long expectTime = expect.get(i);
            Calendar output = RegexpPractice.parseDate(inputs.get(i));
            if (expectTime == null) {
                assertNull(String.format("Test %d failed: Parsing <<%s>> (should be null)", i, inputs.get(i)), output);
                continue;
            } else {
                assertNotNull(String.format("Test %d failed: Parsing <<%s>> (unexpected null)", i, inputs.get(i)), output);
            }
            output.set(Calendar.MILLISECOND, 0);

            long outTime = output.getTimeInMillis();

            assertEquals(String.format("Test %d failed: Parsing <<%s>> (was %s not %s)", 
                            i, inputs.get(i), df.format(outTime), df.format(expectTime)), 
                    (long) expectTime, outTime);
        }
    }
```
\$\endgroup\$
10
  • \$\begingroup\$ I'd prefer to use the regex to match the particular weekday amd month names. That makes it a bit longer,, but better readable (semantically), and maybe even increases the performance of that function. Also leave case insensivity to the regex engine and setting it into case insensitive mode. \$\endgroup\$ Dec 29, 2019 at 18:29
  • \$\begingroup\$ do you think there is no way to shorten it? It's hardly readable for me this way, but I do want it to be efficient \$\endgroup\$
    – Rika
    Dec 29, 2019 at 18:32
  • 3
    \$\begingroup\$ WHY would you want to create a custom RegEx?! You can use a library like JodaTime to build the parser and have it spit out the RegEx. If you need to remove optional extra spaces a single `"\s+" => " '" replacement will fix those prior to parsing. programcreek.com/java-api-examples/… \$\endgroup\$ Dec 29, 2019 at 18:54
  • 2
    \$\begingroup\$ TimeZone.getTimeZone ("GTM")? Surely, you mean "GMT". \$\endgroup\$
    – AJNeufeld
    Dec 30, 2019 at 0:43
  • 1
    \$\begingroup\$ added the tester, it's clear there what is the expected \$\endgroup\$
    – Rika
    Dec 30, 2019 at 19:49

1 Answer 1

2
\$\begingroup\$

You could utilize Named Capturing Groups that would clarify the meaning of capturing group in the regex, as well as the method calls in the matcher. furthermore, with Named Capturing Groups, you don't care about the order of the groups and don't need to "skip" captured groups that you do not want to read (like the time zone)

you could also break the regex into separate Strings for the capturing groups to further enhance readability:

String timeZoneRegex = "(?<timeZone>[A-Z][a-z][a-z],\\s+)";
String dayOfMonthRegex = "(?<dayOfMonth>[0-2][0-9]|[3][0-1])";
...
Pattern pattern = Pattern.compile(timeZoneRegex + "?" +  dayOfMonthRegex + ...

instead of matcher.group(1) --> matcher.group("dayOfMonth");

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.