Streamline the java StringToTime code

package com.clutch.dates;

import java.beans.PropertyEditorSupport;
import java.util.Date;

import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;

public class DateEditor extends PropertyEditorSupport implements PropertyEditorRegistrar {

	@Override
	public void setAsText(String text) throws IllegalArgumentException {
		Object date = StringToTime.date(text);
		if (!Boolean.FALSE.equals(date))
			setValue((Date) date);
		else
			throw new IllegalArgumentException(String.format("[%s] is not a date.", text));
	}

//	@Override
	public void registerCustomEditors(PropertyEditorRegistry registry) {
		registry.registerCustomEditor(Date.class, new DateEditor());
		registry.registerCustomEditor(Date.class, new DateEditor());
	}

}

package com.clutch.dates;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A Java implementation of the PHP function strtotime(String, int): accepts various expressions of time
 * in String format, and instantiates a {@link java.util.Date} object.
 *
 *

 * There are two ways to use StringToTime to parse dates:
 *

 *
static methods ({@link #time(Object)}, {@link #date(Object)}, {@link #cal(Object)})

 *
creating an instance of StringToTime with {@link #StringToTime(Object)}

 *

 *

 *
 *
The static methods provide a UNIX-style timestamp, a {@link java.util.Date} instance, or a
 * {@link java.util.Calendar} instance.  In the event the time expression provided is invalid,
 * these methods return Boolean.FALSE.

 *
 *
Instances of StringToTime inherit from {@link java.util.Date}; so, when instantiated
 * with an expression that the algorithm recognizes, the resulting instance of StringToTime
 * can be passed to any method or caller requiring a {@link java.util.Date} object. Unlike the static methods,
 * attempting to create a StringToTime instance with an invalid expression of time
 * results in a {@link StringToTimeException}.

 *
 *
Valid expressions of time

 *
 *
All expressions are case-insensitive.

 *
 *

 *
now (equal to new Date())

 *
today (equal to StringToTime("00:00:00.000"))

 *
midnight (equal to StringToTime("00:00:00.000 +24 hours"))

 *
morning or this morning (by default, equal toStringToTime("07:00:00.000"))

 *
noon (by default, equal to StringToTime("12:00:00.000")

 *
afternoon or this afternoon (by default, equal to StringToTime("13:00:00.000")

 *
evening or this evening (by default, equal to StringToTime("17:00:00.000")

 *
tonight (by default, equal to StringToTime("20:00:00.000")

 *
tomorrow (by default, equal to StringToTime("now +24 hours"))

 *
tomorrow morning (by default, equal to StringToTime("morning +24 hours"))

 *
noon tomorrow or tomorrow noon (by default, equal to StringToTime("noon +24 hours"))

 *
tomorrow afternoon (by default, equal to StringToTime("afternoon +24 hours"))

 *
yesterday (by default, equal to StringToTime("now -24 hours"))

 *
all the permutations of yesterday and morning, noon, afternoon, and evening

 *
October 26, 1981 or Oct 26, 1981

 *
October 26 or Oct 26

 *
26 October 1981

 *
26 Oct 1981

 *
26 Oct 81

 *
10/26/1981 or 10-26-1981

 *
10/26/81 or 10-26-81

 *
1981/10/26 or 1981-10-26

 *
10/26 or 10-26

 *

 *
 * @author Aaron Collegeman acollegeman@clutch-inc.com
 * @since JRE 1.5.0
 * @see http://us3.php.net/manual/en/function.strtotime.php
 */
public class StringToTime extends Date {

	private static final long serialVersionUID = 7889493424407815134L;

	private static final Log log = LogFactory.getLog(StringToTime.class);

	// default SimpleDateFormat string is the standard MySQL date format
	private static final String defaultSimpleDateFormat = "yyyy-MM-dd HH:mm:ss.SSS";

	// An expression of time (hour)(:(minute))?((:(second))(.(millisecond))?)?( *(am?|pm?))?(RFC 822 time zone|general time zone)?
	private static final String timeExpr = "(\\d{1,2})(:(\\d{1,2}))?(:(\\d{1,2})(\\.(\\d{1,3}))?)?( *(am?|pm?))?( *\\-\\d{4}|[a-z]{3}|[a-z ]+)?";

	/** Patterns and formats recognized by the algorithm; first match wins, so insert most specific patterns first. */
	private static final PatternAndFormat[] known = {

		// TODO: ISO 8601 and derivatives

		// just the year
		new PatternAndFormat(
			Pattern.compile("\\d{4}"),
			new Format(FormatType.YEAR)
		),

		// decrement, e.g., -1 day
		new PatternAndFormat(
			Pattern.compile("\\-( *\\d{1,} +[^ ]+){1,}", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.DECREMENT)
		),

		// increment, e.g., +1 day
		new PatternAndFormat(
			Pattern.compile("\\+?( *\\d{1,} +[^ ]+){1,}", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.INCREMENT)
		),

		// e.g., October 26 and Oct 26
		new PatternAndFormat(
			Pattern.compile("([a-z]+) +(\\d{1,2})", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.MONTH_AND_DATE)
		),

		// e.g., 26 October 1981, or 26 Oct 1981, or 26 Oct 81
		new PatternAndFormat(
			Pattern.compile("\\d{1,2} +[a-z]+ +(\\d{2}|\\d{4})", Pattern.CASE_INSENSITIVE),
			new Format("d MMM y")
		),

		// now or today
		new PatternAndFormat(
			Pattern.compile("(midnight|now|today|(this +)?(morning|afternoon|evening)|tonight|noon( +tomorrow)?|tomorrow|tomorrow +(morning|afternoon|evening|night|noon)?|yesterday|yesterday +(morning|afternoon|evening|night)?)", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.WORD)
		),

		// time, 24-hour and 12-hour
		new PatternAndFormat(
			Pattern.compile(timeExpr, Pattern.CASE_INSENSITIVE),
			new Format(FormatType.TIME)
		),

		// e.g., October 26, 1981 or Oct 26, 1981
		new PatternAndFormat(
			Pattern.compile("[a-z]+ +\\d{1,2} *, *(\\d{2}|\\d{4})", Pattern.CASE_INSENSITIVE),
			new Format("MMM d, y")
		),

		// e.g., 10/26/1981 or 10/26/81
		new PatternAndFormat(
			Pattern.compile("\\d{1,2}/\\d{1,2}/\\d{2,4}"),
			new Format("M/d/y")
		),

		// e.g., 10-26-1981 or 10-26-81
		new PatternAndFormat(
			Pattern.compile("\\d{1,2}\\-\\d{1,2}\\-\\d{2,4}"),
			new Format("M-d-y")
		),

		// e.g., 10/26 or 10-26
		new PatternAndFormat(
			Pattern.compile("(\\d{1,2})(/|\\-)(\\d{1,2})"),
			new Format(FormatType.MONTH_AND_DATE_WITH_SLASHES)
		),

		// e.g., 1981/10/26
		new PatternAndFormat(
			Pattern.compile("\\d{4}/\\d{1,2}/\\d{1,2}"),
			new Format("y/M/d")
		),

		// e.g., 1981-10-26
		new PatternAndFormat(
			Pattern.compile("\\d{4}\\-\\d{1,2}\\-\\d{1,2}"),
			new Format("y-M-d")
		),

		// e.g., October or Oct
		new PatternAndFormat(
			Pattern.compile("(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?)", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.MONTH)
		),

		// e.g., Tuesday or Tue
		new PatternAndFormat(
			Pattern.compile("(Sun(day)?|Mon(day)?|Tue(sday)?|Wed(nesday)?|Thu(rsday)?|Fri(day)?|Sat(urday)?)", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.DAY_OF_WEEK)
		),

		// next, e.g., next Tuesday
		new PatternAndFormat(
			Pattern.compile("next +(.*)", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.NEXT)
		),

		// last, e.g., last Tuesday
		new PatternAndFormat(
			Pattern.compile("last +(.*)", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.LAST)
		),

		// compound statement
		new PatternAndFormat(
			Pattern.compile("(.*) +(((\\+|\\-){1}.*)|"+timeExpr+")$", Pattern.CASE_INSENSITIVE),
			new Format(FormatType.COMPOUND)
		)

	};

	/** Date/Time string parsed */
	private Object dateTimeString;

	/** The format to use in {@link #toString()) */
	private String simpleDateFormat;

	/** The {@link java.util.Date} interpreted from {@link #dateTimeString}, or {@link java.lang.Boolean} false */
	private Object date;

	public StringToTime() {
		super();
		this.date = new Date(this.getTime());
	}

	public StringToTime(Date date) {
		super(date.getTime());
		this.date = new Date(this.getTime());
	}

	public StringToTime(Object dateTimeString) {
		this(dateTimeString, new Date(), defaultSimpleDateFormat);
	}

	public StringToTime(Object dateTimeString, String simpleDateFormat) {
		this(dateTimeString, new Date(), simpleDateFormat);
	}

	public StringToTime(Object dateTimeString, Date now) {
		this(dateTimeString, now, defaultSimpleDateFormat);
	}

	public StringToTime(Object dateTimeString, Long now) {
		this(dateTimeString, new Date(now), defaultSimpleDateFormat);
	}

	public StringToTime(Object dateTimeString, Integer now) {
		this(dateTimeString, new Date(new Long(now)), defaultSimpleDateFormat);
	}

	public StringToTime(Object dateTimeString, Date now, String simpleDateFormat) {
		super(0);
		assert dateTimeString != null;
        assert now != null;
        assert simpleDateFormat != null;

		this.dateTimeString = dateTimeString;
		this.simpleDateFormat = simpleDateFormat;

		date = StringToTime.date(dateTimeString, now);
		if (!Boolean.FALSE.equals(date))
			setTime(((Date) date).getTime());
		else
			throw new StringToTimeException(dateTimeString);
	}

	/**
	 * @return {@link java.util.Date#getTime()}
	 */
	public long getTime() {
		return super.getTime();
	}

	/**
	 * @return Calendar set to timestamp {@link java.util.Date#getTime()}
	 */
	public Calendar getCal() {
		Calendar cal = Calendar.getInstance();
		cal.setTimeInMillis(super.getTime());
		return cal;
	}

	/**
	 * @param simpleDateFormat
	 * @see {@link SimpleDateFormat}
	 * @return Date formatted according to simpleDateFormat
	 */
	public String format(String simpleDateFormat) {
		return new SimpleDateFormat(simpleDateFormat).format(this);
	}

	/**
	 * @return If {@link #simpleDateFormat} provided in constructor, then attempts to format date
	 * accordingly; otherwise, returns the String value of {@link java.util.Date#getTime()}.
	 */
	public String toString() {
		if (simpleDateFormat != null)
			return new SimpleDateFormat(simpleDateFormat).format(this);
		else
			return new SimpleDateFormat("yyyy/dd/MM").format(this); //String.valueOf(super.getTime());
	}

	/**
	 * A single parameter version of {@link #time(String, Date)}, passing a new instance of {@link java.util.Date} as the
	 * second parameter.
	 * @param dateTimeString
	 * @return A {@link java.lang.Long} timestamp representative of dateTimeString, or {@link java.lang.Boolean} false.
	 * @see #time(String, Date)
	 */
	public static Object time(Object dateTimeString) {
		return time(dateTimeString, new Date());
	}

	/**
	 * Parse dateTimeString and produce a timestamp.
	 * @param dateTimeString
	 * @param now
	 * @return

	 *
If equal to "now", return the number of milliseconds since January 1, 1970 or the value of now.

	 *
If an incremental or decremental statement, e.g., +1 hour or -1 week, or a composite thereof, e.g., +1 hour 1 minute 1 second,
	 * returns a date equal to the increment/decrement plus the value of now.
	 *

	 */
	public static Object time(Object dateTimeString, Date now) {
		try {
			if (dateTimeString == null)
				return Boolean.FALSE;
			else {
				String trimmed = String.valueOf(dateTimeString).trim();
				for(PatternAndFormat paf : known) {
					Matcher m = paf.matches(trimmed);
					if (m.matches()) {
						Long time = paf.parse(trimmed, now, m);
						//System.out.println(String.format("[%s] triggered format [%s]: %s", dateTimeString, paf.f, new Date(time)));
						if (log.isDebugEnabled())
							log.debug(String.format("[%s] triggered format [%s]: %s", dateTimeString, paf.f, new Date(time)));
						return time;
					}
				}

				// no match
				if (log.isDebugEnabled())
					log.debug(String.format("Unrecognized date/time string [%s]", dateTimeString));
				return Boolean.FALSE;
			}
		} catch (Exception e) { // thrown by various features of the parser
			if (!Boolean.parseBoolean(System.getProperty(StringToTime.class+".EXCEPTION_ON_PARSE_FAILURE", "false"))) {
				if (log.isDebugEnabled())
					log.debug(String.format("Failed to parse [%s] into a java.util.Date instance", dateTimeString));
				return Boolean.FALSE;
			}
			else
				throw new StringToTimeException(dateTimeString, e);
		}
	}

	private static ParserResult getParserResult(String trimmedDateTimeString, Date now) throws ParseException {
		for(PatternAndFormat paf : known) {
			Matcher m = paf.matches(trimmedDateTimeString);
			if (m.matches()) {
				log.debug(String.format("Date/time string [%s] triggered format [%s]", trimmedDateTimeString, paf.f));
				return new ParserResult(paf.parse(trimmedDateTimeString, now, m), paf.f.type);
			}
		}

		return null;
	}

	public static Object date(Object dateTimeString) {
		return date(dateTimeString, new Date());
	}

	public static Object date(Object dateTimeString, Date now) {
		Object time = time(dateTimeString, now);
		return (Boolean.FALSE.equals(time)) ? Boolean.FALSE : new Date((Long) time);
	}

	public static Object cal(Object dateTimeString) {
		return cal(dateTimeString, new Date());
	}

	public static Object cal(Object dateTimeString, Date now) {
		Object date = date(dateTimeString, now);
		if (Boolean.FALSE.equals(date))
			return Boolean.FALSE;
		else {
			Calendar cal = Calendar.getInstance();
			cal.setTime((Date) date);
			return cal;
		}
	}

	private static class PatternAndFormat {
		public Pattern p;
		public Format f;

		public PatternAndFormat(Pattern p, Format f) {
			this.p = p;
			this.f = f;
		}

		public Matcher matches(String dateTimeString) {
			return p.matcher(dateTimeString);
		}

		public Long parse(String dateTimeString, Date now, Matcher m) throws ParseException {
			return f.parse(dateTimeString, now, m).getTime();
		}
	}

	private static class ParserResult {
		public FormatType type;
		public Long timestamp;

		public ParserResult(Long timestamp, FormatType type) {
			this.timestamp = timestamp;
			this.type = type;
		}
	}

	private static class Format {

		private static Pattern unit = Pattern.compile("(\\d{1,}) +(s(ec(ond)?)?|mo(n(th)?)?|(hour|hr?)|d(ay)?|(w(eek)?|wk)|m(in(ute)?)?|(y(ear)?|yr))s?");

		private static Pattern removeExtraSpaces = Pattern.compile(" +");

		private static Map<String, Integer> translateDayOfWeek = new HashMap<String, Integer>();

		static {
			translateDayOfWeek.put("sunday", 1);
			translateDayOfWeek.put("sun", 1);
			translateDayOfWeek.put("monday", 2);
			translateDayOfWeek.put("mon", 2);
			translateDayOfWeek.put("tuesday", 3);
			translateDayOfWeek.put("tue", 3);
			translateDayOfWeek.put("wednesday", 4);
			translateDayOfWeek.put("wed", 4);
			translateDayOfWeek.put("thursday", 5);
			translateDayOfWeek.put("thu", 5);
			translateDayOfWeek.put("friday", 6);
			translateDayOfWeek.put("fri", 6);
			translateDayOfWeek.put("saturday", 7);
			translateDayOfWeek.put("sat", 7);
		}

		private String sdf;

		private FormatType type;

		public Format(FormatType type) {
			this.type = type;
		}

		public Format(String sdf) {
			this.sdf = sdf;
		}

		public String toString() {
			if (sdf != null)
				return sdf;
			else
				return type.toString();
		}

		public Date parse(String dateTimeString, Date now, Matcher m) throws ParseException {
			if (sdf != null)
				return new SimpleDateFormat(sdf).parse(dateTimeString);
			else {
				dateTimeString = removeExtraSpaces.matcher(dateTimeString).replaceAll(" ").toLowerCase();

				try {
					Calendar cal = Calendar.getInstance();
					cal.setTime(now);

					// word expressions, e.g., "now" and "today" and "tonight"
					if (type == FormatType.WORD) {
						if ("now".equals(dateTimeString))
							return (now != null ? now : new Date());

						else if ("today".equals(dateTimeString)) {
							cal.set(Calendar.HOUR_OF_DAY, 0);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if ("morning".equals(dateTimeString) || "this morning".equals(dateTimeString)) {
							// by default, this morning begins at 07:00:00.000
							int thisMorningBeginsAt = Integer.parseInt(System.getProperty(StringToTime.class+".THIS_MORNING_BEGINS_AT", "7"));
							cal.set(Calendar.HOUR_OF_DAY, thisMorningBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if ("noon".equals(dateTimeString)) {
							// noon is 12:00:00.000
							cal.set(Calendar.HOUR_OF_DAY, 12);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if ("afternoon".equals(dateTimeString) || "this afternoon".equals(dateTimeString)) {
							// by default, this afternoon begins at 13:00:00.000
							int thisAfternoonBeginsAt = Integer.parseInt(System.getProperty(StringToTime.class+".THIS_AFTERNOON_BEGINS_AT", "13"));
							cal.set(Calendar.HOUR_OF_DAY, thisAfternoonBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if ("evening".equals(dateTimeString) || "this evening".equals(dateTimeString)) {
							// by default, this evening begins at 17:00:00.000
							int thisEveningBeginsAt = Integer.parseInt(System.getProperty(StringToTime.class+".THIS_EVENING_BEGINS_AT", "17"));
							cal.set(Calendar.HOUR_OF_DAY, thisEveningBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if ("tonight".equals(dateTimeString)) {
							// by default, tonight begins at 20:00:00.000
							int tonightBeginsAt = Integer.parseInt(System.getProperty(StringToTime.class+".TONIGHT_BEGINS_AT", "20"));
							cal.set(Calendar.HOUR_OF_DAY, tonightBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if ("midnight".equals(dateTimeString)) {
							return new StringToTime("00:00:00 +24 hours", now);
						}

						else if ("tomorrow".equals(dateTimeString)) {
							return new StringToTime("now +24 hours", now);
						}

						else if ("tomorrow morning".equals(dateTimeString)) {
							return new StringToTime("morning +24 hours", now);
						}

						else if ("tomorrow noon".equals(dateTimeString) || "noon tomorrow".equals(dateTimeString)) {
							return new StringToTime("noon +24 hours", now);
						}

						else if ("tomorrow afternoon".equals(dateTimeString)) {
							return new StringToTime("afternoon +24 hours", now);
						}

						else if ("tomorrow evening".equals(dateTimeString)) {
							return new StringToTime("evening +24 hours", now);
						}

						else if ("tomorrow night".equals(dateTimeString)) {
							return new StringToTime("tonight +24 hours", now);
						}

						else if ("yesterday".equals(dateTimeString)) {
							return new StringToTime("now -24 hours", now);
						}

						else if ("yesterday morning".equals(dateTimeString)) {
							return new StringToTime("morning -24 hours", now);
						}

						else if ("yesterday noon".equals(dateTimeString) || "noon yesterday".equals(dateTimeString)) {
							return new StringToTime("noon -24 hours", now);
						}

						else if ("yesterday afternoon".equals(dateTimeString)) {
							return new StringToTime("afternoon -24 hours", now);
						}

						else if ("yesterday evening".equals(dateTimeString)) {
							return new StringToTime("evening -24 hours", now);
						}

						else if ("yesterday night".equals(dateTimeString)) {
							return new StringToTime("tonight -24 hours", now);
						}

						else
							throw new ParseException(String.format("Unrecognized date word: %s", dateTimeString), 0);
					}

					// time expressions, 24-hour and 12-hour
					else if (type == FormatType.TIME) {
						// An expression of time (hour)(:(minute))?((:(second))(.(millisecond))?)?( *(am?|pm?))?(RFC 822 time zone|general time zone)?
						String hour = m.group(1);
						String min = m.group(3);
						String sec = m.group(5);
						String ms = m.group(7);
						String amOrPm = m.group(8);

						if (hour != null) {
							if (amOrPm != null)
								cal.set(Calendar.HOUR, new Integer(hour));
							else
								cal.set(Calendar.HOUR_OF_DAY, new Integer(hour));
						}
						else
							cal.set(Calendar.HOUR, 0);

						cal.set(Calendar.MINUTE, (min != null ? new Integer(min) : 0));
						cal.set(Calendar.SECOND, (sec != null ? new Integer(sec) : 0));
						cal.set(Calendar.MILLISECOND, (ms != null ? new Integer(ms) : 0));

						if (amOrPm != null)
							cal.set(Calendar.AM_PM, (amOrPm.equals("a") || amOrPm.equals("am") ? Calendar.AM : Calendar.PM));

						return new Date(cal.getTimeInMillis());
					}

					// increments
					else if (type == FormatType.INCREMENT || type == FormatType.DECREMENT) {
						Matcher units = unit.matcher(dateTimeString);
						while (units.find()) {
							Integer val = new Integer(units.group(1)) * (type == FormatType.DECREMENT ? -1 : 1);
							String u = units.group(2);

							// second
							if ("s".equals(u) || "sec".equals(u) || "second".equals(u))
								cal.set(Calendar.SECOND, cal.get(Calendar.SECOND)+val);

							// minute
							else if ("m".equals(u) || "min".equals(u) || "minute".equals(u))
								cal.set(Calendar.MINUTE, cal.get(Calendar.MINUTE)+val);

							// hour
							else if ("h".equals(u) || "hr".equals(u) || "hour".equals(u))
								cal.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY)+val);

							// day
							else if ("d".equals(u) || "day".equals(u))
								cal.set(Calendar.DATE, cal.get(Calendar.DATE)+val);

							// week
							else if ("w".equals(u) || "wk".equals(u) || "week".equals(u))
								cal.set(Calendar.WEEK_OF_YEAR, cal.get(Calendar.WEEK_OF_YEAR)+val);

							// month
							else if ("mo".equals(u) || "mon".equals(u) || "month".equals(u))
								cal.set(Calendar.MONTH, cal.get(Calendar.MONTH)+val);

							// year
							else if ("y".equals(u) || "yr".equals(u) || "year".equals(u))
								cal.set(Calendar.YEAR, cal.get(Calendar.YEAR)+val);

							else
								throw new IllegalArgumentException(String.format("Unrecognized %s unit: [%s]", type, u));
						}

						return new Date(cal.getTimeInMillis());
					}

					// compound expressions
					else if (type == FormatType.COMPOUND) {
						Object date = StringToTime.date(m.group(1), now);
						if (!Boolean.FALSE.equals(date))
							return (Date) StringToTime.date(m.group(2), (Date) date);
						else
							throw new IllegalArgumentException(String.format("Couldn't parse %s, so couldn't compound with %s", m.group(1), m.group(2)));
					}

					// month of the year
					else if (type == FormatType.MONTH) {
						Calendar ref = Calendar.getInstance();
						ref.setTime(new SimpleDateFormat("MMM d, y").parse(String.format("%s 1, 1970", m.group(1))));
						cal.set(Calendar.MONTH, ref.get(Calendar.MONTH));

						return new Date(cal.getTimeInMillis());
					}

					// day of week
					else if (type == FormatType.DAY_OF_WEEK) {
						Integer ref = translateDayOfWeek.get(dateTimeString);
						if (cal.get(Calendar.DAY_OF_WEEK) >= ref)
							cal.set(Calendar.WEEK_OF_YEAR, cal.get(Calendar.WEEK_OF_YEAR)+1);
						cal.set(Calendar.DAY_OF_WEEK, ref);

						return new Date(cal.getTimeInMillis());
					}

					// month and day with slashes
					else if (type == FormatType.MONTH_AND_DATE_WITH_SLASHES) {
						Calendar ref = Calendar.getInstance();
						ref.setTime(new SimpleDateFormat("M/d/y").parse(String.format("%s/%s/1970", m.group(1), m.group(3))));
						cal.set(Calendar.MONTH, ref.get(Calendar.MONTH));
						cal.set(Calendar.DATE, ref.get(Calendar.DATE));

						return new Date(cal.getTimeInMillis());
					}

					// month and day long-hand
					else if (type == FormatType.MONTH_AND_DATE) {
						Calendar ref = Calendar.getInstance();
						ref.setTime(new SimpleDateFormat("MMM d, y").parse(String.format("%s %s, 1970", m.group(1), m.group(2))));
						cal.set(Calendar.MONTH, ref.get(Calendar.MONTH));
						cal.set(Calendar.DATE, ref.get(Calendar.DATE));

						return new Date(cal.getTimeInMillis());
					}

					// next X
					else if (type == FormatType.NEXT) {
						// Format types MONTH and DAY_OF_WEEK both return future dates, so no additional processing is needed
						String expr = m.group(1);
						ParserResult parsed = StringToTime.getParserResult(expr, now);

						if (parsed != null && ( FormatType.MONTH.equals(parsed.type) || FormatType.DAY_OF_WEEK.equals(parsed.type) || FormatType.MONTH_AND_DATE.equals(parsed.type)) )
							return new Date(parsed.timestamp);
						else {
							if ("week".equals(expr))
								cal.set(Calendar.WEEK_OF_YEAR, cal.get(Calendar.WEEK_OF_YEAR)+1);
							else if ("month".equals(expr))
								cal.set(Calendar.MONTH, cal.get(Calendar.MONTH)+1);
							else if ("year".equals(expr))
								cal.set(Calendar.YEAR, cal.get(Calendar.YEAR)+1);
							else
								throw new IllegalArgumentException(String.format("Invalid expression of time: %s", dateTimeString));

							return new Date(cal.getTimeInMillis());
						}
					}

					// last X
					else if (type == FormatType.LAST) {
						String expr = m.group(1);
						ParserResult parsed = StringToTime.getParserResult(expr, now);

						if (parsed != null && (FormatType.MONTH.equals(parsed.type) || FormatType.MONTH_AND_DATE.equals(parsed.type))) {
							return new StringToTime("-1 year", new Date(parsed.timestamp));
						}

						else if (parsed != null && FormatType.DAY_OF_WEEK.equals(parsed.type)) {
							return new StringToTime("-1 week", new Date(parsed.timestamp));
						}

						else {
							if ("week".equals(expr))
								cal.set(Calendar.WEEK_OF_YEAR, cal.get(Calendar.WEEK_OF_YEAR)-1);
							else if ("month".equals(expr))
								cal.set(Calendar.MONTH, cal.get(Calendar.MONTH)-1);
							else if ("year".equals(expr))
								cal.set(Calendar.YEAR, cal.get(Calendar.YEAR)-1);
							else
								throw new IllegalArgumentException(String.format("Invalid expression of time: %s", dateTimeString));

							return new Date(cal.getTimeInMillis());
						}
					}

					// year
					else if (type == FormatType.YEAR) {
						cal.set(Calendar.YEAR, new Integer(m.group(0)));
						return new Date(cal.getTimeInMillis());
					}

					// unimplemented format type
					else
						throw new IllegalStateException(String.format("Unimplemented FormatType: %s", type));
				} catch (ParseException e) {
					throw e;
				} catch (IllegalStateException e) {
					throw e;
				} catch (IllegalArgumentException e) {
					throw e;
				} catch (Exception e) {
					throw new RuntimeException(String.format("Unknown failure in string-to-time conversion: %s", e.getMessage()), e);
				}
			}
		}
	}

	private enum FormatType {
		COMPOUND,
		MONTH_AND_DATE_WITH_SLASHES,
		MONTH_AND_DATE,
		MONTH,
		DAY_OF_WEEK,
		NEXT,
		LAST,
		INCREMENT,
		DECREMENT,
		WORD,
		TIME,
		YEAR
	}

}

package com.clutch.dates;

public class StringToTimeException extends RuntimeException {

	private static final long serialVersionUID = -3777846121104246071L;

	public StringToTimeException(Object dateTimeString) {
		super(String.format("Failed to parse [%s] into a java.util.Date", dateTimeString));
	}

	public StringToTimeException(Object dateTimeString, Throwable cause) {
		super(String.format("Failed to parse [%s] into a java.util.Date", dateTimeString), cause);
	}

}

package com.clutch.dates;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import junit.framework.TestCase;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

public class StringToTimeTest extends TestCase {

	public void testMySqlDateFormat() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		cal.set(Calendar.YEAR, 1981);
		cal.set(Calendar.HOUR_OF_DAY, 15);
		cal.set(Calendar.MINUTE, 26);
		cal.set(Calendar.SECOND, 3);
		cal.set(Calendar.MILLISECOND, 435);

		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("1981-10-26 15:26:03.435", now));
	}

	/* FIXME
	public void testISO8601() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		cal.set(Calendar.YEAR, 1981);
		cal.set(Calendar.HOUR_OF_DAY, 15);
		cal.set(Calendar.MINUTE, 25);
		cal.set(Calendar.SECOND, 2);
		cal.set(Calendar.MILLISECOND, 435);

		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("1981-10-26T15:26:03.435ZEST", now));
	}
	*/

	public void test1200Seconds() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		cal.set(Calendar.SECOND, cal.get(Calendar.SECOND)+1200);
		assertTrue(new Date(cal.getTimeInMillis()).equals(new StringToTime("+1200 s", now)));
		assertFalse(new Date(cal.getTimeInMillis()).equals(new StringToTime("+1 s", now)));
	}

	public void testVariousExpressionsOfTimeOfDay() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		cal.set(Calendar.HOUR_OF_DAY, 23);
		cal.set(Calendar.MINUTE, 59);
		cal.set(Calendar.SECOND, 59);
		cal.set(Calendar.MILLISECOND, 0);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("11:59:59 PM", now));
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("23:59:59", now));

		cal.set(Calendar.SECOND, 0);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("23:59", now));
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("11:59 PM", now));

		cal.set(Calendar.MILLISECOND, 123);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("23:59:00.123"));

		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		cal.set(Calendar.YEAR, 1981);
		cal.set(Calendar.HOUR_OF_DAY, 15);
		cal.set(Calendar.MINUTE, 27);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("October 26, 1981 3:27:00 PM", now));

		cal.set(Calendar.HOUR, 5);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.AM_PM, Calendar.PM);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("10/26/81 5PM", now));

		cal.setTime(now);
		cal.set(Calendar.DATE, cal.get(Calendar.DATE)+1);
		cal.set(Calendar.HOUR, 5);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);
		cal.set(Calendar.AM_PM, Calendar.PM);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("tomorrow 5PM", now));

		cal.set(Calendar.DATE, cal.get(Calendar.DATE)-2);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("yesterday 5PM", now));
		assertEquals(new StringToTime("yesterday evening", now), new StringToTime("yesterday 5PM", now));
	}

	public void testStaticMethods() {
		Date now = new Date();

		// timestamp
		Long time = (Long) StringToTime.time("now", now);
		assertEquals(new Date(now.getTime()), new Date(time));

		// calendar
		Calendar cal = (Calendar) StringToTime.cal("now", now);
		assertEquals(new Date(now.getTime()), new Date(cal.getTimeInMillis()));

		// date
		Date date = (Date) StringToTime.date("now", now);
		assertEquals(new Date(now.getTime()), date);
	}

	public void testInstancePattern() {
		StringToTime date = new StringToTime("26 October 1981");
		BeanWrapper bean = new BeanWrapperImpl(date);
		Calendar cal = new GregorianCalendar(1981, Calendar.OCTOBER, 26);
		Long myBirthday = cal.getTimeInMillis();

		// string value of the StringToTime object is the timestamp
		assertEquals(myBirthday, new Long(date.getTime()));

		// formatting controlled by constructor
		date = new StringToTime("26 October 1981", "d MMM yyyy");
		assertEquals("26 Oct 1981", date.toString());
		date = new StringToTime("26 October 1981", "M/d/yy");
		assertEquals("10/26/81", date.toString());

		// time property
		assertEquals(myBirthday, bean.getPropertyValue("time"));

		// date property
		Date now = new Date(myBirthday);
		assertEquals(now, date);

		// calendar property
		assertEquals(cal, bean.getPropertyValue("cal"));

		// format on demand
		assertEquals("October 26, 1981", date.format("MMMM d, yyyy"));
	}

	public void testNow() {
		Date now = new Date();
		assertEquals(new Date(now.getTime()), new StringToTime("now", now));
	}

	public void testToday() {
		Date now = new Date();
		assertEquals(new StringToTime("00:00:00.000", now), new StringToTime("today", now));
	}

	public void testThisMorning() {
		Date now = new Date();
		assertEquals(new StringToTime("07:00:00.000", now), new StringToTime("this morning", now));
		assertEquals(new StringToTime("morning", now), new StringToTime("this morning", now));
	}

	public void testNoon() {
		Date now = new Date();
		assertEquals(new StringToTime("12:00:00.000", now), new StringToTime("noon", now));
	}

	public void testThisAfternoon() {
		Date now = new Date();
		assertEquals(new StringToTime("13:00:00.000", now), new StringToTime("this afternoon", now));
		assertEquals(new StringToTime("afternoon", now), new StringToTime("this afternoon", now));
	}

	public void testThisEvening() {
		Date now = new Date();
		assertEquals(new StringToTime("17:00:00.000", now), new StringToTime("this evening", now));
		assertEquals(new StringToTime("evening", now), new StringToTime("this evening", now));
	}

	public void testTonight() {
		Date now = new Date();
		assertEquals(StringToTime.time("20:00:00.000", now), StringToTime.time("tonight", now));
	}

	public void testIncrements() {
		Date now = new Date();

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);
		cal.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY)+1);
		assertEquals(cal.getTimeInMillis(), StringToTime.time("+1 hour", now));

		cal.setTime(now);
		cal.set(Calendar.WEEK_OF_YEAR, cal.get(Calendar.WEEK_OF_YEAR)+52);
		assertEquals(cal.getTimeInMillis(), StringToTime.time("+52 weeks", now));

		assertEquals(new StringToTime("1 year", now), new StringToTime("+1 year", now));

		assertEquals(new StringToTime("+1 year", now), new StringToTime("+12 months", now));

		assertEquals(new StringToTime("+1 year 6 months", now), new StringToTime("+18 months", now));

		assertEquals(new StringToTime("12 months 1 day 60 seconds", now), new StringToTime("1 year 24 hours 1 minute", now));
	}

	public void testDecrements() {
		Date now = new Date();

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);
		cal.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY)-1);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("-1 hour", now));
	}

	public void testTomorrow() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);
		cal.set(Calendar.DATE, cal.get(Calendar.DATE)+1);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("tomorrow", now));
		assertEquals(new StringToTime("now +24 hours", now), new StringToTime("tomorrow", now));
	}

	public void testTomorrowMorning() {
		Date now = new Date();
		assertEquals(new StringToTime("this morning +24 hours", now), new StringToTime("tomorrow morning", now));
	}

	public void testTomorrowNoon() {
		Date now = new Date();
		assertEquals(new StringToTime("noon +24 hours", now), new StringToTime("tomorrow noon", now));
		assertEquals(new StringToTime("noon +24 hours", now), new StringToTime("noon tomorrow", now));
	}

	public void testTomorrowAfternoon() {
		Date now = new Date();
		assertEquals(new StringToTime("this afternoon +24 hours", now), new StringToTime("tomorrow afternoon", now));
	}

	public void testTomorrowEvening() {
		Date now = new Date();
		assertEquals(new StringToTime("this evening +24 hours", now), new StringToTime("tomorrow evening", now));
	}

	public void testTomorrowNight() {
		Date now = new Date();
		assertEquals(new StringToTime("tonight +24 hours", now), new StringToTime("tomorrow night", now));
	}

	// e.g., October 26, 1981, or Oct 26, 1981, or 26 October 1981, or 26 Oct 1981, or 26 Oct 81
	public void testLongHand() throws Exception {
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("October 26, 1981"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("Oct 26, 1981"));

		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("26 October 1981"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("26 Oct 1981"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("26 Oct 81"));

		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("26 october 1981"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("26 oct 1981"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("26 oct 81"));

		assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"), new StringToTime("1 Jan 2000"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"), new StringToTime("1 Jan 00"));

		assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"), new StringToTime("1 jan 2000"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"), new StringToTime("1 jan 00"));
	}

	// e.g., 10/26/1981 or 10/26/81
	public void testWithSlahesMonthFirst() throws Exception {
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("10/26/1981"));
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("10/26/81"));
	}

	// e.g., 1981/10/26
	public void testWithSlashesYearFirst() throws Exception {
		assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"), new StringToTime("1981/10/26"));
	}

	// e.g., October 26 and Oct 26
	public void testMonthAndDate() throws Exception {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("October 26", now));
		assertEquals(new StringToTime("Oct 26", now), new StringToTime("October 26", now));
	}

	// e.g., 10/26
	public void testWithSlahesMonthAndDate() throws Exception {
		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		assertEquals(new Date(cal.getTimeInMillis()), new StringToTime("10/26"));
	}

	// e.g., October or Oct
	public void testMonth() throws Exception {
		Date now = new Date();

		assertEquals(new StringToTime("October", now), new StringToTime("Oct", now));

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		// it should be this year
		assertEquals(cal.get(Calendar.YEAR), new StringToTime("January", now).getCal().get(Calendar.YEAR));
        assertEquals(cal.get(Calendar.YEAR), new StringToTime("December", now).getCal().get(Calendar.YEAR));
	}

	public void testDayOfWeek() throws Exception {
		Date now = new Date();
		assertEquals(StringToTime.date("Friday", now), StringToTime.date("Fri", now));

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		// if today's day of the week is greater than or equal to our test day of the week (Wednesday)
		if (cal.get(Calendar.DAY_OF_WEEK) >= 3) // then the day of the week on the date returned should be next week
			assertEquals(cal.get(Calendar.WEEK_OF_YEAR)+1, new StringToTime("Wednesday", now).getCal().get(Calendar.WEEK_OF_YEAR));
		else // otherwise, it should be this year
			assertEquals(cal.get(Calendar.WEEK_OF_YEAR), new StringToTime("Wednesday", now).getCal().get(Calendar.WEEK_OF_YEAR));
	}

	public void testNext() {
		Date now = new Date();
		assertEquals(new StringToTime("next January 15", now), new StringToTime("Jan 15", now));
		assertEquals(new StringToTime("next Dec", now), new StringToTime("December", now));
		assertEquals(new StringToTime("next Sunday", now), new StringToTime("Sun", now));
		assertEquals(new StringToTime("next Sat", now), new StringToTime("Saturday", now));
	}

	public void testLast() {
		Date now = new Date();
		assertEquals(new StringToTime("last January 15", now), new StringToTime("Jan 15 -1 year", now));
		assertEquals(new StringToTime("last Dec", now), new StringToTime("December -1 year", now));
		assertEquals(new StringToTime("last Sunday", now), new StringToTime("Sun -1 week", now));
		assertEquals(new StringToTime("last Sat", now), new StringToTime("Saturday -1 week", now));
	}

}

package com.clutch.dates;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Level;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;

/**
 * A Java implementation of the PHP function strtotime(String, int): accepts
 * various expressions of time
 * in String format, and returns a timestamp or {@link DateTime}
 * object.
 *
 *
Valid expressions of time

 *
 *

 * All expressions are case-insensitive.
 *

 *
 *

 *
now (equal to new Date())

 *
today (equal to StringToTime("00:00:00.000"))

 *
midnight (equal to
 * StringToTime("00:00:00.000 +24 hours"))

 *
morning or this morning (by default, equal to
 * StringToTime("07:00:00.000"))

 *
noon (by default, equal to
 * StringToTime("12:00:00.000")

 *
afternoon or this afternoon (by default, equal
 * to StringToTime("13:00:00.000")

 *
evening or this evening (by default, equal to
 * StringToTime("17:00:00.000")

 *
tonight (by default, equal to
 * StringToTime("20:00:00.000")

 *
tomorrow (by default, equal to
 * StringToTime("now +24 hours"))

 *
tomorrow morning (by default, equal to
 * StringToTime("morning +24 hours"))

 *
noon tomorrow or tomorrow noon (by default,
 * equal to StringToTime("noon +24 hours"))

 *
tomorrow afternoon (by default, equal to
 * StringToTime("afternoon +24 hours"))

 *
yesterday (by default, equal to
 * StringToTime("now -24 hours"))

 *
all the permutations of yesterday and morning,
 * noon, afternoon, and evening

 *
October 26, 1981 or Oct 26, 1981

 *
October 26 or Oct 26

 *
26 October 1981

 *
26 Oct 1981

 *
26 Oct 81

 *
10/26/1981 or 10-26-1981

 *
10/26/81 or 10-26-81

 *
1981/10/26 or 1981-10-26

 *
10/26 or 10-26

 *

 *
 * @author Aaron Collegeman acollegeman@clutch-inc.com
 * @since JRE 1.5.0
 * @see http://us3.php.net/manual/en/function.strtotime.php
 */
public class StringToTime {

	/**
	 * Parse {@code string} and return a {@link DateTime} object.
	 *
	 * @param string
	 * @return the corresponding DateTime
	 */
	public static DateTime parseDateTime(String string) {
		return new DateTime(parseLong(string));
	}

	/**
	 * Parse {@code string} and return a timestamp with millisecond precision.
	 *
	 * @param string
	 * @return the corresponding timestamp
	 */
	public static long parseLong(String string) {
		return parseLong(string, new Date());
	}

	protected static DateTime parseDateTime(String string, Date now){
		return new DateTime(parseLong(string, now));
	}

	/**
	 * Parse {@code string} relative to {@code now} and return a timestamp with
	 * millisecond precision.
	 *
	 * @param string
	 * @param now
	 * @return the corresponding timestamp
	 */
	protected static long parseLong(String string, Date now) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(string));
		try {
			for (PatternAndFormat paf : known) {
				Matcher m = paf.matches(string);
				if(m.matches()) {
					Long time = paf.parse(string, now, m);
					log.debug(String.format("{} triggered format {}: {}",
							string, paf.f, new Date(time)));
					return time;
				}
			}
			log.debug(String.format("Unrecognized date/time string {}", string));
			throw new ParseException("Unrecognized date/time string '" + string
					+ "'", 0);
		}
		catch (Exception e) { // thrown by various features of the parser
			throw Throwables.propagate(e);
		}
	}

	/**
	 * Return the parse result.
	 *
	 * @param trimmedDateTimeString
	 * @param now
	 * @return
	 * @throws ParseException
	 */
	private static ParserResult getParserResult(String trimmedDateTimeString,
			Date now) throws ParseException {
		for (PatternAndFormat paf : known) {
			Matcher m = paf.matches(trimmedDateTimeString);
			if(m.matches()) {
				log.debug(String.format(
						"Date/time string [%s] triggered format [%s]",
						trimmedDateTimeString, paf.f));
				return new ParserResult(
						paf.parse(trimmedDateTimeString, now, m), paf.f.type);
			}
		}

		return null;
	}

	private static final Logger log = LoggerFactory
			.getLogger(StringToTime.class);
	static {
		// Set to Level.DEBUG to see information about string parsing logic
		((ch.qos.logback.classic.Logger) log).setLevel(Level.INFO);
	}

	// An expression of time (hour)(:(minute))?((:(second))(.(millisecond))?)?(
	// *(am?|pm?))?(RFC 822 time zone|general time zone)?
	private static final String timeExpr = "(\\d{1,2})(:(\\d{1,2}))?(:(\\d{1,2})(\\.(\\d{1,3}))?)?( *(am?|pm?))?( *\\-\\d{4}|[a-z]{3}|[a-z ]+)?";

	/**
	 * Patterns and formats recognized by the algorithm; first match wins, so
	 * insert most specific patterns first.
	 */
	private static final PatternAndFormat[] known = {

			// TODO: ISO 8601 and derivatives

			// just the year
			new PatternAndFormat(Pattern.compile("\\d{4}"), new Format(
					FormatType.YEAR)),

			// decrement, e.g., -1 day
			new PatternAndFormat(Pattern.compile("\\-( *\\d{1,} +[^ ]+){1,}",
					Pattern.CASE_INSENSITIVE), new Format(FormatType.DECREMENT)),

			// increment, e.g., +1 day
			new PatternAndFormat(Pattern.compile("\\+?( *\\d{1,} +[^ ]+){1,}",
					Pattern.CASE_INSENSITIVE), new Format(FormatType.INCREMENT)),

			// e.g., October 26 and Oct 26
			new PatternAndFormat(Pattern.compile("([a-z]+) +(\\d{1,2})",
					Pattern.CASE_INSENSITIVE), new Format(
					FormatType.MONTH_AND_DATE)),

			// e.g., 26 October 1981, or 26 Oct 1981, or 26 Oct 81
			new PatternAndFormat(Pattern.compile(
					"\\d{1,2} +[a-z]+ +(\\d{2}|\\d{4})",
					Pattern.CASE_INSENSITIVE), new Format("d MMM y")),

			// now or today
			new PatternAndFormat(
					Pattern.compile(
							"(midnight|now|today|(this +)?(morning|afternoon|evening)|tonight|noon( +tomorrow)?|tomorrow|tomorrow +(morning|afternoon|evening|night|noon)?|yesterday|yesterday +(morning|afternoon|evening|night)?)",
							Pattern.CASE_INSENSITIVE), new Format(
							FormatType.WORD)),

			// time, 24-hour and 12-hour
			new PatternAndFormat(Pattern.compile(timeExpr,
					Pattern.CASE_INSENSITIVE), new Format(FormatType.TIME)),

			// e.g., October 26, 1981 or Oct 26, 1981
			new PatternAndFormat(Pattern.compile(
					"[a-z]+ +\\d{1,2} *, *(\\d{2}|\\d{4})",
					Pattern.CASE_INSENSITIVE), new Format("MMM d, y")),

			// e.g., 10/26/1981 or 10/26/81
			new PatternAndFormat(Pattern.compile("\\d{1,2}/\\d{1,2}/\\d{2,4}"),
					new Format("M/d/y")),

			// e.g., 10-26-1981 or 10-26-81
			new PatternAndFormat(
					Pattern.compile("\\d{1,2}\\-\\d{1,2}\\-\\d{2,4}"),
					new Format("M-d-y")),

			// e.g., 10/26 or 10-26
			new PatternAndFormat(
					Pattern.compile("(\\d{1,2})(/|\\-)(\\d{1,2})"), new Format(
							FormatType.MONTH_AND_DATE_WITH_SLASHES)),

			// e.g., 1981/10/26
			new PatternAndFormat(Pattern.compile("\\d{4}/\\d{1,2}/\\d{1,2}"),
					new Format("y/M/d")),

			// e.g., 1981-10-26
			new PatternAndFormat(
					Pattern.compile("\\d{4}\\-\\d{1,2}\\-\\d{1,2}"),
					new Format("y-M-d")),

			// e.g., October or Oct
			new PatternAndFormat(
					Pattern.compile(
							"(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sep(tember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?)",
							Pattern.CASE_INSENSITIVE), new Format(
							FormatType.MONTH)),

			// e.g., Tuesday or Tue
			new PatternAndFormat(
					Pattern.compile(
							"(Sun(day)?|Mon(day)?|Tue(sday)?|Wed(nesday)?|Thu(rsday)?|Fri(day)?|Sat(urday)?)",
							Pattern.CASE_INSENSITIVE), new Format(
							FormatType.DAY_OF_WEEK)),

			// next, e.g., next Tuesday
			new PatternAndFormat(Pattern.compile("next +(.*)",
					Pattern.CASE_INSENSITIVE), new Format(FormatType.NEXT)),

			// last, e.g., last Tuesday
			new PatternAndFormat(Pattern.compile("last +(.*)",
					Pattern.CASE_INSENSITIVE), new Format(FormatType.LAST)),

			// compound statement
			new PatternAndFormat(Pattern.compile("(.*) +(((\\+|\\-){1}.*)|"
					+ timeExpr + ")$", Pattern.CASE_INSENSITIVE), new Format(
					FormatType.COMPOUND))

	};

	private static class Format {

		private static Pattern unit = Pattern
				.compile("(\\d{1,}) +(s(ec(ond)?)?|mo(n(th)?)?|(hour|hr?)|d(ay)?|(w(eek)?|wk)|m(in(ute)?)?|(y(ear)?|yr))s?");

		private static Pattern removeExtraSpaces = Pattern.compile(" +");

		private static Map<String, Integer> translateDayOfWeek = new HashMap<String, Integer>();

		static {
			translateDayOfWeek.put("sunday", 1);
			translateDayOfWeek.put("sun", 1);
			translateDayOfWeek.put("monday", 2);
			translateDayOfWeek.put("mon", 2);
			translateDayOfWeek.put("tuesday", 3);
			translateDayOfWeek.put("tue", 3);
			translateDayOfWeek.put("wednesday", 4);
			translateDayOfWeek.put("wed", 4);
			translateDayOfWeek.put("thursday", 5);
			translateDayOfWeek.put("thu", 5);
			translateDayOfWeek.put("friday", 6);
			translateDayOfWeek.put("fri", 6);
			translateDayOfWeek.put("saturday", 7);
			translateDayOfWeek.put("sat", 7);
		}

		private String sdf;

		private FormatType type;

		public Format(FormatType type) {
			this.type = type;
		}

		public Format(String sdf) {
			this.sdf = sdf;
		}

		public Date parse(String dateTimeString, Date now, Matcher m)
				throws ParseException {
			if(sdf != null)
				return new SimpleDateFormat(sdf).parse(dateTimeString);
			else {
				dateTimeString = removeExtraSpaces.matcher(dateTimeString)
						.replaceAll(" ").toLowerCase();

				try {
					Calendar cal = Calendar.getInstance();
					cal.setTime(now);

					// word expressions, e.g., "now" and "today" and "tonight"
					if(type == FormatType.WORD) {
						if("now".equalsIgnoreCase(dateTimeString))
							return (now != null ? now : new Date());

						else if("today".equalsIgnoreCase(dateTimeString)) {
							cal.set(Calendar.HOUR_OF_DAY, 0);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if("morning".equalsIgnoreCase(dateTimeString)
								|| "this morning"
										.equalsIgnoreCase(dateTimeString)) {
							// by default, this morning begins at 07:00:00.000
							int thisMorningBeginsAt = Integer.parseInt(System
									.getProperty(StringToTime.class
											+ ".THIS_MORNING_BEGINS_AT", "7"));
							cal.set(Calendar.HOUR_OF_DAY, thisMorningBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if("noon".equalsIgnoreCase(dateTimeString)) {
							// noon is 12:00:00.000
							cal.set(Calendar.HOUR_OF_DAY, 12);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if("afternoon".equalsIgnoreCase(dateTimeString)
								|| "this afternoon"
										.equalsIgnoreCase(dateTimeString)) {
							// by default, this afternoon begins at 13:00:00.000
							int thisAfternoonBeginsAt = Integer
									.parseInt(System
											.getProperty(
													StringToTime.class
															+ ".THIS_AFTERNOON_BEGINS_AT",
													"13"));
							cal.set(Calendar.HOUR_OF_DAY, thisAfternoonBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if("evening".equalsIgnoreCase(dateTimeString)
								|| "this evening"
										.equalsIgnoreCase(dateTimeString)) {
							// by default, this evening begins at 17:00:00.000
							int thisEveningBeginsAt = Integer.parseInt(System
									.getProperty(StringToTime.class
											+ ".THIS_EVENING_BEGINS_AT", "17"));
							cal.set(Calendar.HOUR_OF_DAY, thisEveningBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if("tonight".equalsIgnoreCase(dateTimeString)) {
							// by default, tonight begins at 20:00:00.000
							int tonightBeginsAt = Integer.parseInt(System
									.getProperty(StringToTime.class
											+ ".TONIGHT_BEGINS_AT", "20"));
							cal.set(Calendar.HOUR_OF_DAY, tonightBeginsAt);
							cal.set(Calendar.MINUTE, 0);
							cal.set(Calendar.SECOND, 0);
							cal.set(Calendar.MILLISECOND, 0);
							return new Date(cal.getTimeInMillis());
						}

						else if("midnight".equalsIgnoreCase(dateTimeString)) {
							return new Date(
									parseLong("00:00:00 +24 hours", now));
						}

						else if("tomorrow".equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("now +24 hours", now));
						}

						else if("tomorrow morning"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("morning +24 hours", now));
						}

						else if("tomorrow noon"
								.equalsIgnoreCase(dateTimeString)
								|| "noon tomorrow"
										.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("noon +24 hours", now));
						}

						else if("tomorrow afternoon"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("afternoon +24 hours",
									now));
						}

						else if("tomorrow evening"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("evening +24 hours", now));
						}

						else if("tomorrow night"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("tonight +24 hours", now));
						}

						else if("yesterday".equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("now -24 hours", now));
						}

						else if("yesterday morning"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("morning -24 hours", now));
						}

						else if("yesterday noon"
								.equalsIgnoreCase(dateTimeString)
								|| "noon yesterday"
										.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("noon -24 hours", now));
						}

						else if("yesterday afternoon"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("afternoon -24 hours",
									now));
						}

						else if("yesterday evening"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("evening -24 hours", now));
						}

						else if("yesterday night"
								.equalsIgnoreCase(dateTimeString)) {
							return new Date(parseLong("tonight -24 hours", now));
						}

						else
							throw new ParseException(String.format(
									"Unrecognized date word: %s",
									dateTimeString), 0);
					}

					// time expressions, 24-hour and 12-hour
					else if(type == FormatType.TIME) {
						// An expression of time
						// (hour)(:(minute))?((:(second))(.(millisecond))?)?(
						// *(am?|pm?))?(RFC 822 time zone|general time zone)?
						String hour = m.group(1);
						String min = m.group(3);
						String sec = m.group(5);
						String ms = m.group(7);
						String amOrPm = m.group(8);

						if(hour != null) {
							if(amOrPm != null)
								cal.set(Calendar.HOUR, new Integer(hour));
							else
								cal.set(Calendar.HOUR_OF_DAY, new Integer(hour));
						}
						else
							cal.set(Calendar.HOUR, 0);

						cal.set(Calendar.MINUTE,
								(min != null ? new Integer(min) : 0));
						cal.set(Calendar.SECOND,
								(sec != null ? new Integer(sec) : 0));
						cal.set(Calendar.MILLISECOND,
								(ms != null ? new Integer(ms) : 0));

						if(amOrPm != null)
							cal.set(Calendar.AM_PM,
									(amOrPm.equalsIgnoreCase("a")
											|| amOrPm.equalsIgnoreCase("am") ? Calendar.AM
											: Calendar.PM));

						return new Date(cal.getTimeInMillis());
					}

					// increments
					else if(type == FormatType.INCREMENT
							|| type == FormatType.DECREMENT) {
						Matcher units = unit.matcher(dateTimeString);
						while (units.find()) {
							Integer val = new Integer(units.group(1))
									* (type == FormatType.DECREMENT ? -1 : 1);
							String u = units.group(2);

							// second
							if("s".equalsIgnoreCase(u)
									|| "sec".equalsIgnoreCase(u)
									|| "second".equalsIgnoreCase(u))
								cal.set(Calendar.SECOND,
										cal.get(Calendar.SECOND) + val);

							// minute
							else if("m".equalsIgnoreCase(u)
									|| "min".equalsIgnoreCase(u)
									|| "minute".equalsIgnoreCase(u))
								cal.set(Calendar.MINUTE,
										cal.get(Calendar.MINUTE) + val);

							// hour
							else if("h".equalsIgnoreCase(u)
									|| "hr".equalsIgnoreCase(u)
									|| "hour".equalsIgnoreCase(u))
								cal.set(Calendar.HOUR_OF_DAY,
										cal.get(Calendar.HOUR_OF_DAY) + val);

							// day
							else if("d".equalsIgnoreCase(u)
									|| "day".equalsIgnoreCase(u))
								cal.set(Calendar.DATE, cal.get(Calendar.DATE)
										+ val);

							// week
							else if("w".equalsIgnoreCase(u)
									|| "wk".equalsIgnoreCase(u)
									|| "week".equalsIgnoreCase(u))
								cal.set(Calendar.WEEK_OF_YEAR,
										cal.get(Calendar.WEEK_OF_YEAR) + val);

							// month
							else if("mo".equalsIgnoreCase(u)
									|| "mon".equalsIgnoreCase(u)
									|| "month".equalsIgnoreCase(u))
								cal.set(Calendar.MONTH, cal.get(Calendar.MONTH)
										+ val);

							// year
							else if("y".equalsIgnoreCase(u)
									|| "yr".equalsIgnoreCase(u)
									|| "year".equalsIgnoreCase(u))
								cal.set(Calendar.YEAR, cal.get(Calendar.YEAR)
										+ val);

							else
								throw new IllegalArgumentException(
										String.format(
												"Unrecognized %s unit: [%s]",
												type, u));
						}

						return new Date(cal.getTimeInMillis());
					}

					// compound expressions
					else if(type == FormatType.COMPOUND) {
						return new Date(parseLong(m.group(2), new Date(
								parseLong(m.group(1), now))));
					}

					// month of the year
					else if(type == FormatType.MONTH) {
						Calendar ref = Calendar.getInstance();
						ref.setTime(new SimpleDateFormat("MMM d, y")
								.parse(String.format("%s 1, 1970", m.group(1))));
						cal.set(Calendar.MONTH, ref.get(Calendar.MONTH));

						return new Date(cal.getTimeInMillis());
					}

					// day of week
					else if(type == FormatType.DAY_OF_WEEK) {
						Integer ref = translateDayOfWeek.get(dateTimeString);
						if(cal.get(Calendar.DAY_OF_WEEK) >= ref)
							cal.set(Calendar.WEEK_OF_YEAR,
									cal.get(Calendar.WEEK_OF_YEAR) + 1);
						cal.set(Calendar.DAY_OF_WEEK, ref);

						return new Date(cal.getTimeInMillis());
					}

					// month and day with slashes
					else if(type == FormatType.MONTH_AND_DATE_WITH_SLASHES) {
						Calendar ref = Calendar.getInstance();
						ref.setTime(new SimpleDateFormat("M/d/y").parse(String
								.format("%s/%s/1970", m.group(1), m.group(3))));
						cal.set(Calendar.MONTH, ref.get(Calendar.MONTH));
						cal.set(Calendar.DATE, ref.get(Calendar.DATE));

						return new Date(cal.getTimeInMillis());
					}

					// month and day long-hand
					else if(type == FormatType.MONTH_AND_DATE) {
						Calendar ref = Calendar.getInstance();
						ref.setTime(new SimpleDateFormat("MMM d, y")
								.parse(String.format("%s %s, 1970", m.group(1),
										m.group(2))));
						cal.set(Calendar.MONTH, ref.get(Calendar.MONTH));
						cal.set(Calendar.DATE, ref.get(Calendar.DATE));

						return new Date(cal.getTimeInMillis());
					}

					// next X
					else if(type == FormatType.NEXT) {
						// Format types MONTH and DAY_OF_WEEK both return future
						// dates, so no additional processing is needed
						String expr = m.group(1);
						ParserResult parsed = StringToTime.getParserResult(
								expr, now);

						if(parsed != null
								&& (FormatType.MONTH.equals(parsed.type)
										|| FormatType.DAY_OF_WEEK
												.equals(parsed.type) || FormatType.MONTH_AND_DATE
											.equals(parsed.type)))
							return new Date(parsed.timestamp);
						else {
							if("week".equalsIgnoreCase(expr))
								cal.set(Calendar.WEEK_OF_YEAR,
										cal.get(Calendar.WEEK_OF_YEAR) + 1);
							else if("month".equalsIgnoreCase(expr))
								cal.set(Calendar.MONTH,
										cal.get(Calendar.MONTH) + 1);
							else if("year".equalsIgnoreCase(expr))
								cal.set(Calendar.YEAR,
										cal.get(Calendar.YEAR) + 1);
							else
								throw new IllegalArgumentException(
										String.format(
												"Invalid expression of time: %s",
												dateTimeString));

							return new Date(cal.getTimeInMillis());
						}
					}

					// last X
					else if(type == FormatType.LAST) {
						String expr = m.group(1);
						ParserResult parsed = StringToTime.getParserResult(
								expr, now);

						if(parsed != null
								&& (FormatType.MONTH.equals(parsed.type) || FormatType.MONTH_AND_DATE
										.equals(parsed.type))) {
							return new Date(parseLong("-1 year", new Date(
									parsed.timestamp)));
						}

						else if(parsed != null
								&& FormatType.DAY_OF_WEEK.equals(parsed.type)) {
							return new Date(parseLong("-1 week", new Date(
									parsed.timestamp)));
						}

						else {
							if("week".equalsIgnoreCase(expr))
								cal.set(Calendar.WEEK_OF_YEAR,
										cal.get(Calendar.WEEK_OF_YEAR) - 1);
							else if("month".equalsIgnoreCase(expr))
								cal.set(Calendar.MONTH,
										cal.get(Calendar.MONTH) - 1);
							else if("year".equalsIgnoreCase(expr))
								cal.set(Calendar.YEAR,
										cal.get(Calendar.YEAR) - 1);
							else
								throw new IllegalArgumentException(
										String.format(
												"Invalid expression of time: %s",
												dateTimeString));

							return new Date(cal.getTimeInMillis());
						}
					}

					// year
					else if(type == FormatType.YEAR) {
						cal.set(Calendar.YEAR, new Integer(m.group(0)));
						return new Date(cal.getTimeInMillis());
					}

					// unimplemented format type
					else
						throw new IllegalStateException(String.format(
								"Unimplemented FormatType: %s", type));
				}
				catch (ParseException e) {
					throw e;
				}
				catch (IllegalStateException e) {
					throw e;
				}
				catch (IllegalArgumentException e) {
					throw e;
				}
				catch (Exception e) {
					throw new RuntimeException(String.format(
							"Unknown failure in string-to-time conversion: %s",
							e.getMessage()), e);
				}
			}
		}

		@Override
		public String toString() {
			if(sdf != null)
				return sdf;
			else
				return type.toString();
		}
	}

	private enum FormatType {
		COMPOUND,
		MONTH_AND_DATE_WITH_SLASHES,
		MONTH_AND_DATE,
		MONTH,
		DAY_OF_WEEK,
		NEXT,
		LAST,
		INCREMENT,
		DECREMENT,
		WORD,
		TIME,
		YEAR
	}

	private static class ParserResult {
		public FormatType type;
		public Long timestamp;

		public ParserResult(Long timestamp, FormatType type) {
			this.timestamp = timestamp;
			this.type = type;
		}
	}

	private static class PatternAndFormat {
		public Pattern p;
		public Format f;

		public PatternAndFormat(Pattern p, Format f) {
			this.p = p;
			this.f = f;
		}

		public Matcher matches(String dateTimeString) {
			return p.matcher(dateTimeString);
		}

		public Long parse(String dateTimeString, Date now, Matcher m)
				throws ParseException {
			return f.parse(dateTimeString, now, m).getTime();
		}
	}

}

package com.clutch.dates;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import org.junit.Assert;
import org.junit.Test;

public class StringToTimeTest {

	@Test
	public void testMySqlDateFormat() {
		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		cal.set(Calendar.YEAR, 1981);
		cal.set(Calendar.HOUR_OF_DAY, 15);
		cal.set(Calendar.MINUTE, 26);
		cal.set(Calendar.SECOND, 3);
		cal.set(Calendar.MILLISECOND, 435);

		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("1981-10-26 15:26:03.435").toDate());
	}

	/*
	 * FIXME
	 *
	 * @Test public void testISO8601() {
	 * Date now = new Date();
	 * Calendar cal = Calendar.getInstance();
	 * cal.setTime(now);
	 *
	 * cal.set(Calendar.MONTH, Calendar.OCTOBER);
	 * cal.set(Calendar.DATE, 26);
	 * cal.set(Calendar.YEAR, 1981);
	 * cal.set(Calendar.HOUR_OF_DAY, 15);
	 * cal.set(Calendar.MINUTE, 25);
	 * cal.set(Calendar.SECOND, 2);
	 * cal.set(Calendar.MILLISECOND, 435);
	 *
	 * Assert.assertEquals(new Date(cal.getTimeInMillis()), new
	 * StringToTime("1981-10-26T15:26:03.435ZEST", now).toDate());
	 * }
	 */

	@Test
	public void test1200Seconds() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		cal.set(Calendar.SECOND, cal.get(Calendar.SECOND) + 1200);
		Assert.assertTrue(new Date(cal.getTimeInMillis()).equals(StringToTime
				.parseDateTime("+1200 s", now).toDate()));
		Assert.assertFalse(new Date(cal.getTimeInMillis()).equals(StringToTime
				.parseDateTime("+1 s", now).toDate()));
	}

	@Test
	public void testVariousExpressionsOfTimeOfDay() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		cal.set(Calendar.HOUR_OF_DAY, 23);
		cal.set(Calendar.MINUTE, 59);
		cal.set(Calendar.SECOND, 59);
		cal.set(Calendar.MILLISECOND, 0);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("11:59:59 PM", now).toDate());
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("23:59:59", now).toDate());

		cal.set(Calendar.SECOND, 0);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("23:59", now).toDate());
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("11:59 PM", now).toDate());

		cal.set(Calendar.MILLISECOND, 123);
		Assert.assertEquals(new Date(cal.getTimeInMillis()),
				StringToTime.parseDateTime("23:59:00.123"));

		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		cal.set(Calendar.YEAR, 1981);
		cal.set(Calendar.HOUR_OF_DAY, 15);
		cal.set(Calendar.MINUTE, 27);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("October 26, 1981 3:27:00 PM", now).toDate());

		cal.set(Calendar.HOUR, 5);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.AM_PM, Calendar.PM);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("10/26/81 5PM", now).toDate());

		cal.setTime(now);
		cal.set(Calendar.DATE, cal.get(Calendar.DATE) + 1);
		cal.set(Calendar.HOUR, 5);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);
		cal.set(Calendar.AM_PM, Calendar.PM);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("tomorrow 5PM", now).toDate());

		cal.set(Calendar.DATE, cal.get(Calendar.DATE) - 2);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("yesterday 5PM", now).toDate());
		Assert.assertEquals(StringToTime
				.parseDateTime("yesterday evening", now).toDate(), StringToTime
				.parseDateTime("yesterday 5PM", now).toDate());
	}

	@Test
	public void testNow() {
		Date now = new Date();
		Assert.assertEquals(new Date(now.getTime()), StringToTime
				.parseDateTime("now", now).toDate());
	}

	@Test
	public void testToday() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("00:00:00.000", now)
				.toDate(), StringToTime.parseDateTime("today", now).toDate());
	}

	@Test
	public void testThisMorning() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("07:00:00.000", now)
				.toDate(), StringToTime.parseDateTime("this morning", now)
				.toDate());
		Assert.assertEquals(
				StringToTime.parseDateTime("morning", now).toDate(),
				StringToTime.parseDateTime("this morning", now).toDate());
	}

	@Test
	public void testNoon() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("12:00:00.000", now)
				.toDate(), StringToTime.parseDateTime("noon", now).toDate());
	}

	@Test
	public void testThisAfternoon() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("13:00:00.000", now)
				.toDate(), StringToTime.parseDateTime("this afternoon", now)
				.toDate());
		Assert.assertEquals(StringToTime.parseDateTime("afternoon", now)
				.toDate(), StringToTime.parseDateTime("this afternoon", now)
				.toDate());
	}

	@Test
	public void testThisEvening() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("17:00:00.000", now)
				.toDate(), StringToTime.parseDateTime("this evening", now)
				.toDate());
		Assert.assertEquals(
				StringToTime.parseDateTime("evening", now).toDate(),
				StringToTime.parseDateTime("this evening", now).toDate());
	}

	@Test
	public void testTonight() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("20:00:00.000", now)
				.toDate(), StringToTime.parseDateTime("tonight", now).toDate());
	}

	@Test
	public void testIncrements() {
		Date now = new Date();

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);
		cal.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY) + 1);
		Assert.assertEquals(cal.getTimeInMillis(),
				StringToTime.parseDateTime("+1 hour", now).toDate());

		cal.setTime(now);
		cal.set(Calendar.WEEK_OF_YEAR, cal.get(Calendar.WEEK_OF_YEAR) + 52);
		Assert.assertEquals(cal.getTimeInMillis(),
				StringToTime.parseDateTime("+52 weeks", now).toDate());

		Assert.assertEquals(StringToTime.parseDateTime("1 year", now).toDate(),
				StringToTime.parseDateTime("+1 year", now).toDate());

		Assert.assertEquals(
				StringToTime.parseDateTime("+1 year", now).toDate(),
				StringToTime.parseDateTime("+12 months", now).toDate());

		Assert.assertEquals(StringToTime.parseDateTime("+1 year 6 months", now)
				.toDate(), StringToTime.parseDateTime("+18 months", now)
				.toDate());

		Assert.assertEquals(
				StringToTime.parseDateTime("12 months 1 day 60 seconds", now)
						.toDate(),
				StringToTime.parseDateTime("1 year 24 hours 1 minute", now)
						.toDate());
	}

	@Test
	public void testDecrements() {
		Date now = new Date();

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);
		cal.set(Calendar.HOUR_OF_DAY, cal.get(Calendar.HOUR_OF_DAY) - 1);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("-1 hour", now).toDate());
	}

	@Test
	public void testTomorrow() {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.setTime(now);
		cal.set(Calendar.DATE, cal.get(Calendar.DATE) + 1);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("tomorrow", now).toDate());
		Assert.assertEquals(StringToTime.parseDateTime("now +24 hours", now)
				.toDate(), StringToTime.parseDateTime("tomorrow", now).toDate());
	}

	@Test
	public void testTomorrowMorning() {
		Date now = new Date();
		Assert.assertEquals(
				StringToTime.parseDateTime("this morning +24 hours", now)
						.toDate(),
				StringToTime.parseDateTime("tomorrow morning", now).toDate());
	}

	@Test
	public void testTomorrowNoon() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("noon +24 hours", now)
				.toDate(), StringToTime.parseDateTime("tomorrow noon", now)
				.toDate());
		Assert.assertEquals(StringToTime.parseDateTime("noon +24 hours", now)
				.toDate(), StringToTime.parseDateTime("noon tomorrow", now)
				.toDate());
	}

	@Test
	public void testTomorrowAfternoon() {
		Date now = new Date();
		Assert.assertEquals(
				StringToTime.parseDateTime("this afternoon +24 hours", now)
						.toDate(),
				StringToTime.parseDateTime("tomorrow afternoon", now).toDate());
	}

	@Test
	public void testTomorrowEvening() {
		Date now = new Date();
		Assert.assertEquals(
				StringToTime.parseDateTime("this evening +24 hours", now)
						.toDate(),
				StringToTime.parseDateTime("tomorrow evening", now).toDate());
	}

	@Test
	public void testTomorrowNight() {
		Date now = new Date();
		Assert.assertEquals(StringToTime
				.parseDateTime("tonight +24 hours", now).toDate(), StringToTime
				.parseDateTime("tomorrow night", now).toDate());
	}

	// e.g., October 26, 1981, or Oct 26, 1981, or 26 October 1981, or 26 Oct
	// 1981, or 26 Oct 81
	@Test
	public void testLongHand() throws Exception {
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("October 26, 1981"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("Oct 26, 1981"));

		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("26 October 1981"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("26 Oct 1981"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("26 Oct 81"));

		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("26 october 1981"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("26 oct 1981"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("26 oct 81"));

		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"),
				StringToTime.parseDateTime("1 Jan 2000"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"),
				StringToTime.parseDateTime("1 Jan 00"));

		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"),
				StringToTime.parseDateTime("1 jan 2000"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("1/1/2000"),
				StringToTime.parseDateTime("1 jan 00"));
	}

	// e.g., 10/26/1981 or 10/26/81
	@Test
	public void testWithSlahesMonthFirst() throws Exception {
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("10/26/1981"));
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("10/26/81"));
	}

	// e.g., 1981/10/26
	@Test
	public void testWithSlashesYearFirst() throws Exception {
		Assert.assertEquals(new SimpleDateFormat("M/d/y").parse("10/26/1981"),
				StringToTime.parseDateTime("1981/10/26"));
	}

	// e.g., October 26 and Oct 26
	@Test
	public void testMonthAndDate() throws Exception {
		Date now = new Date();
		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("October 26", now).toDate());
		Assert.assertEquals(StringToTime.parseDateTime("Oct 26", now).toDate(),
				StringToTime.parseDateTime("October 26", now).toDate());
	}

	// e.g., 10/26
	@Test
	public void testWithSlahesMonthAndDate() throws Exception {
		Calendar cal = Calendar.getInstance();
		cal.set(Calendar.MONTH, Calendar.OCTOBER);
		cal.set(Calendar.DATE, 26);
		Assert.assertEquals(new Date(cal.getTimeInMillis()), StringToTime
				.parseDateTime("10/26").toDate());
	}

	// e.g., October or Oct
	@Test
	public void testMonth() throws Exception {
		Date now = new Date();

		Assert.assertEquals(
				StringToTime.parseDateTime("October", now).toDate(),
				StringToTime.parseDateTime("Oct", now));

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		Calendar cal2 = Calendar.getInstance();

		// it should be this year
		cal2.setTime(StringToTime.parseDateTime("January", now).toDate());
		Assert.assertEquals(cal.get(Calendar.YEAR), cal2.get(Calendar.YEAR));
		cal2.setTime(StringToTime.parseDateTime("December", now).toDate());
		Assert.assertEquals(cal.get(Calendar.YEAR), cal2.get(Calendar.YEAR));
	}

	@Test
	public void testDayOfWeek() throws Exception {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("Friday", now).toDate(),
				StringToTime.parseDateTime("Fri", now).toDate());

		Calendar cal = Calendar.getInstance();
		cal.setTime(now);

		// if today's day of the week is greater than or equal to our test day
		// of the week (Wednesday)
		Calendar cal2 = Calendar.getInstance();
		if(cal.get(Calendar.DAY_OF_WEEK) >= 3) {// then the day of the week on
												// the date returned should be
												// next week
			cal2.setTime(StringToTime.parseDateTime("Wednesday", now).toDate());
			Assert.assertEquals(cal.get(Calendar.WEEK_OF_YEAR) + 1,
					cal2.get(Calendar.WEEK_OF_YEAR));
		}
		else {
			// otherwise, it should be this year
			cal2.setTime(StringToTime.parseDateTime("Wednesday", now).toDate());
			Assert.assertEquals(cal.get(Calendar.WEEK_OF_YEAR),
					cal2.get(Calendar.WEEK_OF_YEAR));
		}
	}

	@Test
	public void testNext() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("next January 15", now)
				.toDate(), StringToTime.parseDateTime("Jan 15", now).toDate());
		Assert.assertEquals(StringToTime.parseDateTime("next Dec", now)
				.toDate(), StringToTime.parseDateTime("December", now).toDate());
		Assert.assertEquals(StringToTime.parseDateTime("next Sunday", now)
				.toDate(), StringToTime.parseDateTime("Sun", now).toDate());
		Assert.assertEquals(StringToTime.parseDateTime("next Sat", now)
				.toDate(), StringToTime.parseDateTime("Saturday", now).toDate());
	}

	@Test
	public void testLast() {
		Date now = new Date();
		Assert.assertEquals(StringToTime.parseDateTime("last January 15", now)
				.toDate(), StringToTime.parseDateTime("Jan 15 -1 year", now)
				.toDate());
		Assert.assertEquals(StringToTime.parseDateTime("last Dec", now)
				.toDate(), StringToTime.parseDateTime("December -1 year", now)
				.toDate());
		Assert.assertEquals(StringToTime.parseDateTime("last Sunday", now)
				.toDate(), StringToTime.parseDateTime("Sun -1 week", now)
				.toDate());
		Assert.assertEquals(StringToTime.parseDateTime("last Sat", now)
				.toDate(), StringToTime.parseDateTime("Saturday -1 week", now)
				.toDate());
	}

}