001: /*
002: * Copyright 2006 Pentaho Corporation. All rights reserved.
003: * This software was developed by Pentaho Corporation and is provided under the terms
004: * of the Mozilla Public License, Version 1.1, or any later version. You may not use
005: * this file except in compliance with the license. If you need a copy of the license,
006: * please go to http://www.mozilla.org/MPL/MPL-1.1.txt. The Original Code is the Pentaho
007: * BI Platform. The Initial Developer is Pentaho Corporation.
008: *
009: * Software distributed under the Mozilla Public License is distributed on an "AS IS"
010: * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. Please refer to
011: * the license for the specific language governing your rights and limitations.
012: *
013: * @created Nov 3, 2005
014: */
015: package org.pentaho.util;
016:
017: import java.util.Calendar;
018: import java.util.Locale;
019: import java.util.StringTokenizer;
020: import java.text.DateFormat;
021: import java.text.SimpleDateFormat;
022:
023: import org.pentaho.messages.util.LocaleHelper;
024:
025: /**
026: * Provides a utility for calculating relative dates. The class calculates a
027: * date based upon an expression. The syntax of the expression is given below.
028: * <p>
029: * <b>Date Expression</b><br>
030: *
031: * <pre>
032: * <expression> := <expression>+ ( ';' DATESPEC )?
033: * <expression> := OPERATION? OPERAND ':' <unit> <position>?
034: * <unit> := 'Y' | 'M' | 'D' | 'W' | 'h' | 'm' | 's'
035: * <position> := 'S' | 'E'
036: * OPERATION := '+' | '-'
037: * OPERAND := [0..9]+
038: * DATESPEC := <i>any {@link java.text.SimpleDateFormat} format pattern</i>
039: * </pre>
040: *
041: * The <tt>OPERAND</tt> specifies the positive or negative offset to the date.
042: * The <tt>unit</tt> inidcates the <i>unit</i> of the date to manipulate. The
043: * optional position indicates the relative position for the specified unit:
044: * <i>S</i> for start and <i>E</i> for end. The following are the valid unit
045: * values.
046: *
047: * <pre>
048: * Y Year
049: * M Month
050: * W Week
051: * D Day
052: * h hour
053: * m minute
054: * s second
055: * </pre>
056: *
057: * <p>
058: * <b>Examples</b>:
059: *
060: * <pre>
061: * 0:ME -1:DS 00:00:00.000 of the day before the last day of the current month
062: * 0:MS 0:WE 23:59:59.999 the last day of the first week of the month
063: * 0:ME 23:59:59.999 of the last day of teh current month
064: * 5:Y the current monty, day and time 5 years in the future
065: * 5:YS 00:00:00.000 of the first day of the years 5 years in the future
066: * </pre>
067: */
068: public class DateMath {
069:
070: private static final char POSITION_END = 'E';
071:
072: private static final char POSITION_START = 'S';
073:
074: private static final char UNIT_YEAR = 'Y';
075:
076: private static final char UNIT_MONTH = 'M';
077:
078: private static final char UNIT_WEEK = 'W';
079:
080: private static final char UNIT_DAY = 'D';
081:
082: private static final char UNIT_HOUR = 'h';
083:
084: private static final char UNIT_MINUTE = 'm';
085:
086: private static final char UNIT_SECOND = 's';
087:
088: /**
089: * Calculates a date, returning the formatted string version of the
090: * calculated date. The method is a short cut for
091: * {@link #calculateDate(Calendar,String,Locale) calculateDate(null,expressionWithFormat,null)}.
092: * If the date format is omitted, the short format for the
093: * {@link PentahoSystem#getLocale()} is used.
094: *
095: * @param expressionWithFormat
096: * the relative date expression with optional format
097: * specification.
098: * @return The calculated date as a string.
099: * @throws IllegalArgumentException
100: * if <tt>expressionWithFormat</tt> is invalid.
101: */
102: public static String claculateDateString(String expressionWithFormat) {
103: return calculateDateString(null, expressionWithFormat, null);
104: }
105:
106: /**
107: * Calculates a date, returning the formatted string version of the
108: * calculated date. The method is a short cut for
109: * {@link #calculateDate(Calendar,String,Locale) calculateDate(date,expressionWithFormat,null)}.
110: *
111: * @param date
112: * the target date against the expression will be applied.
113: * @param expressionWithFormat
114: * the relative date expression with optional format
115: * specification.
116: * @return The calculated date as a string.
117: * @throws IllegalArgumentException
118: * if <tt>expressionWithFormat</tt> is invalid.
119: */
120: public static String calculateDateString(Calendar date,
121: String expressionWithFormat) {
122: return calculateDateString(date, expressionWithFormat, null);
123: }
124:
125: /**
126: * Calculates a date, returning the formatted string version of the
127: * calculated date.
128: *
129: * @param date
130: * the target date against the expression will be applied. If
131: * <tt>null</tt>, the current date is used.
132: * @param expressionWithFormat
133: * the relative date expression with optional format
134: * specification.
135: * @param locale
136: * the desired locale for the formatted string.
137: * @return The calculated date as a string.
138: * @throws IllegalArgumentException
139: * if <tt>expressionWithFormat</tt> is invalid.
140: */
141: public static String calculateDateString(Calendar date,
142: String expressionWithFormat, Locale locale) {
143: int index = expressionWithFormat.indexOf(';');
144: String expression;
145: String pattern = null;
146: Calendar target = (date == null) ? Calendar.getInstance()
147: : date;
148: DateFormat format;
149: Locale myLocale;
150:
151: if (index >= 0) {
152: pattern = expressionWithFormat.substring(index + 1);
153: expression = expressionWithFormat.substring(0, index);
154: } else {
155: expression = expressionWithFormat;
156: }
157:
158: target = calculateDate(date, expression);
159:
160: myLocale = (locale == null) ? LocaleHelper.getLocale() : locale;
161: if (myLocale == null) {
162: myLocale = LocaleHelper.getDefaultLocale();
163: }
164:
165: if (pattern != null) {
166: format = new SimpleDateFormat(pattern, myLocale);
167: } else {
168: format = DateFormat.getDateTimeInstance(DateFormat.SHORT,
169: DateFormat.SHORT, myLocale);
170: }
171:
172: return format.format(target.getTime());
173: }
174:
175: /**
176: * Calculates the date specified by the expression, relative to the current
177: * date/time. The method is a short cut for
178: * {@link #calculate(Calendar, String) calculate(null,expression)}.
179: *
180: * @param expression
181: * the date expression as described above.
182: * @return The calculated date.
183: * @throws IllegalArgumentException
184: * if <tt>expression</tt> is invalid.
185: */
186: public static Calendar calculateDate(String expression) {
187: return calculateDate(null, expression);
188: }
189:
190: /**
191: * Calculates the date specified by the expression, relative to the
192: * indicated date/time.
193: *
194: * @param date
195: * the target date against the expression is evaluated. If
196: * <tt>null</tt>, the current date/time is used. If not
197: * <tt>null</tt>, the object is manipulated by the expression.
198: * @param expression
199: * the date expression as described above.
200: * @return The calculated date. This will be <tt>date</tt> if
201: * <tt>date</tt> is not <tt>null</tt>.
202: */
203: public static Calendar calculateDate(Calendar date,
204: String expression) {
205: StringTokenizer tok;
206: String myExpression;
207: Calendar target = date;
208: int index = expression.indexOf(';');
209:
210: if (index >= 0) {
211: myExpression = expression.substring(index + 1);
212: } else {
213: myExpression = expression;
214: }
215:
216: tok = new StringTokenizer(myExpression, " \t;"); //$NON-NLS-1$
217: while (tok.hasMoreElements()) {
218: target = parseAndCalculateDate(target, (String) tok
219: .nextElement());
220: }
221:
222: return target;
223: }
224:
225: /**
226: * Parses and executes a single expression, one without subexpressions.
227: *
228: * @param date
229: * the target date against the expression is evaluated. If
230: * <tt>null</tt>, the current date/time is used. If not
231: * <tt>null</tt>, the object is manipulated by the expression.
232: * @param expression
233: * the date expression as described above.
234: * @return The calculated date. This will be <tt>date</tt> if
235: * <tt>date</tt> is not <tt>null</tt>.
236: */
237: private static Calendar parseAndCalculateDate(Calendar date,
238: String expression) {
239: int index = expression.indexOf(':'); // $NON-NLS-1$
240: char operation = '+'; // $NON-NLS-1$
241: char unit = ' '; // $NON-NLS-1$
242: char position = ' '; // $NON-NLS-1$
243: int operand = 0;
244: Calendar result;
245:
246: if (index >= 0) {
247: try {
248: String number = expression.substring(0, index);
249:
250: operation = number.charAt(0);
251: if ((operation == '+') || (operation == '-')) { // $NON-NLS-1$
252: // Integer.praseInt doesn't handle '+' for positive numbers
253: //
254: number = number.substring(1);
255: } else {
256: operation = '+';
257: }
258:
259: operand = Integer.parseInt(number);
260:
261: index++;
262: unit = expression.charAt(index);
263:
264: index++;
265: if (index < expression.length()) {
266: position = expression.charAt(index);
267: }
268:
269: result = calculateDate(date, operation, operand, unit,
270: position);
271: } catch (Exception ex) {
272: IllegalArgumentException err = new IllegalArgumentException(
273: expression);
274:
275: err.initCause(ex);
276: throw err;
277: }
278: } else {
279: throw new IllegalArgumentException(expression);
280: }
281:
282: return result;
283: }
284:
285: /**
286: * Calculates the relative date based upon the values of the BNF
287: * non-terminals above.
288: *
289: * @param date
290: * the target date against the expression is evaluated. If
291: * <tt>null</tt>, the current date/time is used. If not
292: * <tt>null</tt>, the object is manipulated by the expression.
293: * @param operation
294: * the value of the operation. Currently, this is the sign on the
295: * operand. However, in the future, it could be some value to
296: * indicate a relative or specific value.
297: * @param operand
298: * the value of the NUM token.
299: * @param unit
300: * the value of the <unit> non-terminal
301: * @param position
302: * the value of teh <position> non-terminal
303: * @return The calculated date. This will be <tt>date</tt> if
304: * <tt>date</tt> is not <tt>null</tt>.
305: */
306: private static Calendar calculateDate(Calendar date,
307: char operation, int operand, char unit, char position) {
308: Calendar target = (date == null) ? Calendar.getInstance()
309: : date;
310: int calendarField = -1;
311:
312: switch (unit) {
313: case UNIT_YEAR:
314: calendarField = Calendar.YEAR;
315: break;
316: case UNIT_MONTH:
317: calendarField = Calendar.MONTH;
318: break;
319: case UNIT_WEEK:
320: calendarField = Calendar.DAY_OF_YEAR;
321: operand = operand * 7;
322: break;
323: case UNIT_DAY:
324: calendarField = Calendar.DAY_OF_YEAR;
325: break;
326: case UNIT_HOUR:
327: calendarField = Calendar.HOUR_OF_DAY;
328: break;
329: case UNIT_MINUTE:
330: calendarField = Calendar.MINUTE;
331: break;
332: case UNIT_SECOND:
333: calendarField = Calendar.SECOND;
334: break;
335:
336: default:
337: throw new IllegalArgumentException();
338: }
339:
340: if (operation == ' ') {
341: target.set(calendarField, operand);
342: } else if (operation == '+') {
343: target.add(calendarField, operand);
344: } else if (operation == '-') {
345: target.add(calendarField, -Math.abs(operand));
346: }
347:
348: if (unit == UNIT_YEAR) {
349: if (position == POSITION_START) {
350: target.set(Calendar.DAY_OF_YEAR, 1);
351: setTimeToStart(target);
352: } else if (position == POSITION_END) {
353: target.set(Calendar.DAY_OF_YEAR, target
354: .getActualMaximum(Calendar.DAY_OF_YEAR));
355: setTimeToEnd(target);
356: }
357: } else if (unit == UNIT_MONTH) {
358: if (position == POSITION_START) {
359: target.set(Calendar.DAY_OF_MONTH, 1);
360: setTimeToStart(target);
361: } else if (position == POSITION_END) {
362: target.set(Calendar.DAY_OF_MONTH, target
363: .getActualMaximum(Calendar.DAY_OF_MONTH));
364: setTimeToEnd(target);
365: }
366: } else if (unit == UNIT_WEEK) {
367: int firstDOW = target.getFirstDayOfWeek();
368: int dayOfWeek = target.get(Calendar.DAY_OF_WEEK); // force
369: // calculation
370: int dayOffset = 0;
371:
372: if (position == POSITION_START) {
373: if (dayOfWeek > firstDOW) {
374:
375: // Past first day of week; go backwards to first day
376: //
377: dayOffset = firstDOW - dayOfWeek;
378: } else if (dayOfWeek < firstDOW) {
379:
380: // Before the first day; go back a week and move forward to
381: // first day
382: // Should only happen if first day is not Sunday.
383: //
384: dayOffset = -7 + (firstDOW - dayOfWeek);
385: }
386:
387: setTimeToStart(target);
388: } else if (position == POSITION_END) {
389: int lastDOW;
390:
391: if (firstDOW == Calendar.SUNDAY) {
392: lastDOW = Calendar.SATURDAY;
393: } else {
394: lastDOW = firstDOW - 1;
395: }
396:
397: if (dayOfWeek < lastDOW) {
398:
399: // Before the last day of week; move forward to last day
400: //
401: dayOffset = lastDOW - dayOfWeek;
402: } else if (dayOfWeek > lastDOW) {
403:
404: // Should only happen if last day is anything but Saturday;
405: // Move to next week; roll back to last day.
406: dayOffset = 7 - (dayOfWeek - lastDOW);
407: }
408:
409: setTimeToEnd(target);
410: }
411:
412: if (dayOffset != 0) {
413: target.add(Calendar.DAY_OF_YEAR, dayOffset);
414: }
415: } else if (unit == UNIT_DAY) {
416: if (position == POSITION_START) {
417: setTimeToStart(target);
418: } else if (position == POSITION_END) {
419: setTimeToEnd(target);
420: }
421: } else if (unit == UNIT_HOUR) {
422: if (position == POSITION_START) {
423: target.set(Calendar.MINUTE, 0);
424: target.set(Calendar.SECOND, 0);
425: target.set(Calendar.MILLISECOND, 0);
426: } else if (position == POSITION_END) {
427: target.set(Calendar.MINUTE, 59);
428: target.set(Calendar.SECOND, 59);
429: target.set(Calendar.MILLISECOND, 999);
430: }
431: } else if (unit == UNIT_MINUTE) {
432: if (position == POSITION_START) {
433: target.set(Calendar.SECOND, 0);
434: target.set(Calendar.MILLISECOND, 0);
435: } else if (position == POSITION_END) {
436: target.set(Calendar.SECOND, 59);
437: target.set(Calendar.MILLISECOND, 999);
438: }
439: } else if (unit == UNIT_SECOND) {
440: if (position == POSITION_START) {
441: target.set(Calendar.MILLISECOND, 0);
442: } else if (position == POSITION_END) {
443: target.set(Calendar.MILLISECOND, 999);
444: }
445: }
446:
447: target.getTimeInMillis(); // force calculations
448:
449: return target;
450: }
451:
452: /**
453: * Sets the time to the start of the day (00:00:00.000).
454: *
455: * @param target
456: * the target calendar for which the time will be set.
457: */
458: private static void setTimeToStart(Calendar target) {
459: target.set(Calendar.MILLISECOND, 0);
460: target.set(Calendar.SECOND, 0);
461: target.set(Calendar.MINUTE, 0);
462: target.set(Calendar.HOUR_OF_DAY, 0);
463: }
464:
465: /**
466: * Sets the time to the endof the day (23:59:59.999).
467: *
468: * @param target
469: * the target calendar for which the time will be set.
470: */
471: private static void setTimeToEnd(Calendar target) {
472: target.set(Calendar.MILLISECOND, 999);
473: target.set(Calendar.SECOND, 59);
474: target.set(Calendar.MINUTE, 59);
475: target.set(Calendar.HOUR_OF_DAY, 23);
476: }
477: }
|