001: /**
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */package org.apache.solr.util;
017:
018: import java.util.Date;
019: import java.util.Calendar;
020: import java.util.GregorianCalendar;
021: import java.util.TimeZone;
022: import java.util.Locale;
023: import java.util.Map;
024: import java.util.HashMap;
025: import java.text.ParseException;
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: * @version $Id:$
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 Calendar
091: */
092: public static final Map<String, Integer> CALENDAR_UNITS = makeUnitsMap();
093:
094: /** @see #CALENDAR_UNITS */
095: private static Map<String, Integer> makeUnitsMap() {
096:
097: // NOTE: consciously choosing not to support WEEK at this time,
098: // because of complexity in rounding down to the nearest week
099: // arround a month/year boundry.
100: // (Not to mention: it's not clear what people would *expect*)
101:
102: Map<String, Integer> units = new HashMap<String, Integer>(13);
103: units.put("YEAR", Calendar.YEAR);
104: units.put("YEARS", Calendar.YEAR);
105: units.put("MONTH", Calendar.MONTH);
106: units.put("MONTHS", Calendar.MONTH);
107: units.put("DAY", Calendar.DATE);
108: units.put("DAYS", Calendar.DATE);
109: units.put("DATE", Calendar.DATE);
110: units.put("HOUR", Calendar.HOUR_OF_DAY);
111: units.put("HOURS", Calendar.HOUR_OF_DAY);
112: units.put("MINUTE", Calendar.MINUTE);
113: units.put("MINUTES", Calendar.MINUTE);
114: units.put("SECOND", Calendar.SECOND);
115: units.put("SECONDS", Calendar.SECOND);
116: units.put("MILLI", Calendar.MILLISECOND);
117: units.put("MILLIS", Calendar.MILLISECOND);
118: units.put("MILLISECOND", Calendar.MILLISECOND);
119: units.put("MILLISECONDS", Calendar.MILLISECOND);
120:
121: return units;
122: }
123:
124: /**
125: * Modifies the specified Calendar by "adding" the specified value of units
126: *
127: * @exception IllegalArgumentException if unit isn't recognized.
128: * @see #CALENDAR_UNITS
129: */
130: public static void add(Calendar c, int val, String unit) {
131: Integer uu = CALENDAR_UNITS.get(unit);
132: if (null == uu) {
133: throw new IllegalArgumentException(
134: "Adding Unit not recognized: " + unit);
135: }
136: c.add(uu.intValue(), val);
137: }
138:
139: /**
140: * Modifies the specified Calendar by "rounding" down to the specified unit
141: *
142: * @exception IllegalArgumentException if unit isn't recognized.
143: * @see #CALENDAR_UNITS
144: */
145: public static void round(Calendar c, String unit) {
146: Integer uu = CALENDAR_UNITS.get(unit);
147: if (null == uu) {
148: throw new IllegalArgumentException(
149: "Rounding Unit not recognized: " + unit);
150: }
151: int u = uu.intValue();
152:
153: switch (u) {
154:
155: case Calendar.YEAR:
156: c.clear(Calendar.MONTH);
157: /* fall through */
158: case Calendar.MONTH:
159: c.clear(Calendar.DAY_OF_MONTH);
160: c.clear(Calendar.DAY_OF_WEEK);
161: c.clear(Calendar.DAY_OF_WEEK_IN_MONTH);
162: c.clear(Calendar.DAY_OF_YEAR);
163: c.clear(Calendar.WEEK_OF_MONTH);
164: c.clear(Calendar.WEEK_OF_YEAR);
165: /* fall through */
166: case Calendar.DATE:
167: c.clear(Calendar.HOUR_OF_DAY);
168: c.clear(Calendar.HOUR);
169: c.clear(Calendar.AM_PM);
170: /* fall through */
171: case Calendar.HOUR_OF_DAY:
172: c.clear(Calendar.MINUTE);
173: /* fall through */
174: case Calendar.MINUTE:
175: c.clear(Calendar.SECOND);
176: /* fall through */
177: case Calendar.SECOND:
178: c.clear(Calendar.MILLISECOND);
179: break;
180: default:
181: throw new IllegalStateException(
182: "No logic for rounding value (" + u + ") " + unit);
183: }
184:
185: }
186:
187: private TimeZone zone;
188: private Locale loc;
189: private Date now;
190:
191: /**
192: * @param tz The TimeZone used for rounding (to determine when hours/days begin)
193: * @param l The Locale used for rounding (to determine when weeks begin)
194: * @see Calendar#getInstance(TimeZone,Locale)
195: */
196: public DateMathParser(TimeZone tz, Locale l) {
197: zone = tz;
198: loc = l;
199: setNow(new Date());
200: }
201:
202: /** Redefines this instance's concept of "now" */
203: public void setNow(Date n) {
204: now = n;
205: }
206:
207: /** Returns a cloned of this instance's concept of "now" */
208: public Date getNow() {
209: return (Date) now.clone();
210: }
211:
212: /**
213: * Parses a string of commands relative "now" are returns the resulting Date.
214: *
215: * @exception ParseException positions in ParseExceptions are token positions, not character positions.
216: */
217: public Date parseMath(String math) throws ParseException {
218:
219: Calendar cal = Calendar.getInstance(zone, loc);
220: cal.setTime(getNow());
221:
222: /* check for No-Op */
223: if (0 == math.length()) {
224: return cal.getTime();
225: }
226:
227: String[] ops = splitter.split(math);
228: int pos = 0;
229: while (pos < ops.length) {
230:
231: if (1 != ops[pos].length()) {
232: throw new ParseException(
233: "Multi character command found: \"" + ops[pos]
234: + "\"", pos);
235: }
236: char command = ops[pos++].charAt(0);
237:
238: switch (command) {
239: case '/':
240: if (ops.length < pos + 1) {
241: throw new ParseException(
242: "Need a unit after command: \"" + command
243: + "\"", pos);
244: }
245: try {
246: round(cal, ops[pos++]);
247: } catch (IllegalArgumentException e) {
248: throw new ParseException("Unit not recognized: \""
249: + ops[pos - 1] + "\"", pos - 1);
250: }
251: break;
252: case '+': /* fall through */
253: case '-':
254: if (ops.length < pos + 2) {
255: throw new ParseException(
256: "Need a value and unit for command: \""
257: + command + "\"", pos);
258: }
259: int val = 0;
260: try {
261: val = Integer.valueOf(ops[pos++]);
262: } catch (NumberFormatException e) {
263: throw new ParseException("Not a Number: \""
264: + ops[pos - 1] + "\"", pos - 1);
265: }
266: if ('-' == command) {
267: val = 0 - val;
268: }
269: try {
270: String unit = ops[pos++];
271: add(cal, val, unit);
272: } catch (IllegalArgumentException e) {
273: throw new ParseException("Unit not recognized: \""
274: + ops[pos - 1] + "\"", pos - 1);
275: }
276: break;
277: default:
278: throw new ParseException("Unrecognized command: \""
279: + command + "\"", pos - 1);
280: }
281: }
282:
283: return cal.getTime();
284: }
285:
286: private static Pattern splitter = Pattern
287: .compile("\\b|(?<=\\d)(?=\\D)");
288:
289: }
|