001: /*
002: * Copyright 2004-2006 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.compass.core.converter.basic;
018:
019: import java.text.ParseException;
020: import java.util.Calendar;
021: import java.util.Date;
022: import java.util.HashMap;
023: import java.util.Locale;
024: import java.util.Map;
025: import java.util.TimeZone;
026: import java.util.regex.Pattern;
027:
028: /**
029: * A Simple Utility class for parsing "math" like strings relating to Dates.
030: *
031: * <p>
032: * The basic syntax support addition, subtraction and rounding at various
033: * levels of granularity (or "units"). Commands can be chained together
034: * and are parsed from left to right. '+' and '-' denote addition and
035: * subtraction, while '/' denotes "round". Round requires only a unit, while
036: * addition/subtraction require an integer value and a unit.
037: * Command strings must not include white space, but the "No-Op" command
038: * (empty string) is allowed....
039: * </p>
040: *
041: * <pre>
042: * /HOUR
043: * ... Round to the start of the current hour
044: * /DAY
045: * ... Round to the start of the current day
046: * +2YEARS
047: * ... Exactly two years in the future from now
048: * -1DAY
049: * ... Exactly 1 day prior to now
050: * /DAY+6MONTHS+3DAYS
051: * ... 6 months and 3 days in the future from the start of
052: * the current day
053: * +6MONTHS+3DAYS/DAY
054: * ... 6 months and 3 days in the future from now, rounded
055: * down to nearest day
056: * </pre>
057: *
058: * <p>
059: * All commands are relative to a "now" which is fixed in an instance of
060: * DateMathParser such that
061: * <code>p.parseMath("+0MILLISECOND").equals(p.parseMath("+0MILLISECOND"))</code>
062: * no matter how many wall clock milliseconds elapse between the two
063: * distinct calls to parse (Assuming no other thread calls
064: * "<code>setNow</code>" in the interim)
065: * </p>
066: *
067: * <p>
068: * Multiple aliases exist for the various units of time (ie:
069: * <code>MINUTE</code> and <code>MINUTES</code>; <code>MILLI</code>,
070: * <code>MILLIS</code>, <code>MILLISECOND</code>, and
071: * <code>MILLISECONDS</code>.) The complete list can be found by
072: * inspecting the keySet of <code>CALENDAR_UNITS</code>.
073: * </p>
074: *
075: * @author taken from Solr
076: */
077: public class DateMathParser {
078:
079: /**
080: * A mapping from (uppercased) String labels idenyifying time units,
081: * to the corresponding Calendar constant used to set/add/roll that unit
082: * of measurement.
083: *
084: * <p>
085: * A single logical unit of time might be represented by multiple labels
086: * for convenience (ie: <code>DATE==DAY</code>,
087: * <code>MILLI==MILLISECOND</code>)
088: * </p>
089: *
090: * @see java.util.Calendar
091: */
092: public static final Map CALENDAR_UNITS = makeUnitsMap();
093:
094: /**
095: * @see #CALENDAR_UNITS
096: */
097: private static Map makeUnitsMap() {
098:
099: // NOTE: consciously choosing not to support WEEK at this time,
100: // because of complexity in rounding down to the nearest week
101: // arround a month/year boundry.
102: // (Not to mention: it's not clear what people would *expect*)
103:
104: Map units = new HashMap(13);
105: units.put("YEAR", new Integer(Calendar.YEAR));
106: units.put("YEARS", new Integer(Calendar.YEAR));
107: units.put("MONTH", new Integer(Calendar.MONTH));
108: units.put("MONTHS", new Integer(Calendar.MONTH));
109: units.put("DAY", new Integer(Calendar.DATE));
110: units.put("DAYS", new Integer(Calendar.DATE));
111: units.put("DATE", new Integer(Calendar.DATE));
112: units.put("HOUR", new Integer(Calendar.HOUR_OF_DAY));
113: units.put("HOURS", new Integer(Calendar.HOUR_OF_DAY));
114: units.put("MINUTE", new Integer(Calendar.MINUTE));
115: units.put("MINUTES", new Integer(Calendar.MINUTE));
116: units.put("SECOND", new Integer(Calendar.SECOND));
117: units.put("SECONDS", new Integer(Calendar.SECOND));
118: units.put("MILLI", new Integer(Calendar.MILLISECOND));
119: units.put("MILLIS", new Integer(Calendar.MILLISECOND));
120: units.put("MILLISECOND", new Integer(Calendar.MILLISECOND));
121: units.put("MILLISECONDS", new Integer(Calendar.MILLISECOND));
122:
123: return units;
124: }
125:
126: /**
127: * Modifies the specified Calendar by "adding" the specified value of units
128: *
129: * @throws IllegalArgumentException if unit isn't recognized.
130: * @see #CALENDAR_UNITS
131: */
132: public static void add(Calendar c, int val, String unit) {
133: Integer uu = (Integer) CALENDAR_UNITS.get(unit.toUpperCase());
134: if (null == uu) {
135: throw new IllegalArgumentException(
136: "Adding Unit not recognized: " + unit);
137: }
138: c.add(uu.intValue(), val);
139: }
140:
141: /**
142: * Modifies the specified Calendar by "rounding" down to the specified unit
143: *
144: * @throws IllegalArgumentException if unit isn't recognized.
145: * @see #CALENDAR_UNITS
146: */
147: public static void round(Calendar c, String unit) {
148: Integer uu = (Integer) CALENDAR_UNITS.get(unit.toUpperCase());
149: if (null == uu) {
150: throw new IllegalArgumentException(
151: "Rounding Unit not recognized: " + unit);
152: }
153: int u = uu.intValue();
154:
155: switch (u) {
156:
157: case Calendar.YEAR:
158: c.clear(Calendar.MONTH);
159: /* fall through */
160: case Calendar.MONTH:
161: c.clear(Calendar.DAY_OF_MONTH);
162: c.clear(Calendar.DAY_OF_WEEK);
163: c.clear(Calendar.DAY_OF_WEEK_IN_MONTH);
164: c.clear(Calendar.DAY_OF_YEAR);
165: c.clear(Calendar.WEEK_OF_MONTH);
166: c.clear(Calendar.WEEK_OF_YEAR);
167: /* fall through */
168: case Calendar.DATE:
169: c.clear(Calendar.HOUR_OF_DAY);
170: c.clear(Calendar.HOUR);
171: c.clear(Calendar.AM_PM);
172: /* fall through */
173: case Calendar.HOUR_OF_DAY:
174: c.clear(Calendar.MINUTE);
175: /* fall through */
176: case Calendar.MINUTE:
177: c.clear(Calendar.SECOND);
178: /* fall through */
179: case Calendar.SECOND:
180: c.clear(Calendar.MILLISECOND);
181: break;
182: default:
183: throw new IllegalStateException(
184: "No logic for rounding value (" + u + ") " + unit);
185: }
186:
187: }
188:
189: private TimeZone zone;
190: private Locale loc;
191: private Date now;
192:
193: /**
194: * @param tz The TimeZone used for rounding (to determine when hours/days begin)
195: * @param l The Locale used for rounding (to determine when weeks begin)
196: * @see Calendar#getInstance(TimeZone,Locale)
197: */
198: public DateMathParser(TimeZone tz, Locale l) {
199: zone = tz;
200: loc = l;
201: setNow(new Date());
202: }
203:
204: /**
205: * Redefines this instance's concept of "now"
206: */
207: public void setNow(Date n) {
208: now = n;
209: }
210:
211: /**
212: * Returns a cloned of this instance's concept of "now"
213: */
214: public Date getNow() {
215: return (Date) now.clone();
216: }
217:
218: /**
219: * Parses a string of commands relative "now" are returns the resulting Date.
220: *
221: * @throws java.text.ParseException positions in ParseExceptions are token positions, not character positions.
222: */
223: public Date parseMath(String math) throws ParseException {
224:
225: Calendar cal = Calendar.getInstance(zone, loc);
226: cal.setTime(getNow());
227:
228: /* check for No-Op */
229: if (0 == math.length()) {
230: return cal.getTime();
231: }
232:
233: String[] ops = splitter.split(math);
234: int pos = 0;
235: while (pos < ops.length) {
236:
237: if (1 != ops[pos].length()) {
238: throw new ParseException(
239: "Multi character command found: \"" + ops[pos]
240: + "\"", pos);
241: }
242: char command = ops[pos++].charAt(0);
243:
244: switch (command) {
245: case '/':
246: if (ops.length < pos + 1) {
247: throw new ParseException(
248: "Need a unit after command: \"" + command
249: + "\"", pos);
250: }
251: try {
252: round(cal, ops[pos++]);
253: } catch (IllegalArgumentException e) {
254: throw new ParseException("Unit not recognized: \""
255: + ops[pos - 1] + "\"", pos - 1);
256: }
257: break;
258: case '+': /* fall through */
259: case '-':
260: if (ops.length < pos + 2) {
261: throw new ParseException(
262: "Need a value and unit for command: \""
263: + command + "\"", pos);
264: }
265: int val;
266: try {
267: val = Integer.valueOf(ops[pos++]).intValue();
268: } catch (NumberFormatException e) {
269: throw new ParseException("Not a Number: \""
270: + ops[pos - 1] + "\"", pos - 1);
271: }
272: if ('-' == command) {
273: val = 0 - val;
274: }
275: try {
276: String unit = ops[pos++];
277: add(cal, val, unit);
278: } catch (IllegalArgumentException e) {
279: throw new ParseException("Unit not recognized: \""
280: + ops[pos - 1] + "\"", pos - 1);
281: }
282: break;
283: default:
284: throw new ParseException("Unrecognized command: \""
285: + command + "\"", pos - 1);
286: }
287: }
288:
289: return cal.getTime();
290: }
291:
292: private static Pattern splitter = Pattern
293: .compile("\\b|(?<=\\d)(?=\\D)");
294: }
|