/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright 2005 - 2008 Pentaho Corporation. All rights reserved. * * @created Nov 3, 2005 */ package org.pentaho.platform.util; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Locale; import java.util.StringTokenizer; import org.pentaho.platform.util.messages.LocaleHelper; /** * Provides a utility for calculating relative dates. The class calculates a * date based upon an expression. The syntax of the expression is given below. * <p> * <b>Date Expression</b><br> * * <pre> * <expression> := <expression>+ ( ';' DATESPEC )? * <expression> := OPERATION? OPERAND ':' <unit> <position>? * <unit> := 'Y' | 'M' | 'D' | 'W' | 'h' | 'm' | 's' * <position> := 'S' | 'E' * OPERATION := '+' | '-' * OPERAND := [0..9]+ * DATESPEC := <i>any {@link java.text.SimpleDateFormat} format pattern</i> * </pre> * * The <tt>OPERAND</tt> specifies the positive or negative offset to the date. * The <tt>unit</tt> inidcates the <i>unit</i> of the date to manipulate. The * optional position indicates the relative position for the specified unit: * <i>S</i> for start and <i>E</i> for end. The following are the valid unit * values. * * <pre> * Y Year * M Month * W Week * D Day * h hour * m minute * s second * </pre> * * <p> * <b>Examples</b>: * * <pre> * 0:ME -1:DS 00:00:00.000 of the day before the last day of the current month * 0:MS 0:WE 23:59:59.999 the last day of the first week of the month * 0:ME 23:59:59.999 of the last day of the current month * 5:Y the current month, day and time 5 years in the future * 5:YS 00:00:00.000 of the first day of the years 5 years in the future * </pre> */ public class DateMath { private static final char POSITION_END = 'E'; private static final char POSITION_START = 'S'; private static final char UNIT_YEAR = 'Y'; private static final char UNIT_MONTH = 'M'; private static final char UNIT_WEEK = 'W'; private static final char UNIT_DAY = 'D'; private static final char UNIT_HOUR = 'h'; private static final char UNIT_MINUTE = 'm'; private static final char UNIT_SECOND = 's'; /** * Calculates a date, returning the formatted string version of the * calculated date. The method is a short cut for * {@link #calculateDate(Calendar,String,Locale) calculateDate(null,expressionWithFormat,null)}. * If the date format is omitted, the short format for the * {@link PentahoSystem#getLocale()} is used. * * @param expressionWithFormat * the relative date expression with optional format * specification. * @return The calculated date as a string. * @throws IllegalArgumentException * if <tt>expressionWithFormat</tt> is invalid. */ public static String claculateDateString(final String expressionWithFormat) { return DateMath.calculateDateString(null, expressionWithFormat, null); } /** * Calculates a date, returning the formatted string version of the * calculated date. The method is a short cut for * {@link #calculateDate(Calendar,String,Locale) calculateDate(date,expressionWithFormat,null)}. * * @param date * the target date against the expression will be applied. * @param expressionWithFormat * the relative date expression with optional format * specification. * @return The calculated date as a string. * @throws IllegalArgumentException * if <tt>expressionWithFormat</tt> is invalid. */ public static String calculateDateString(final Calendar date, final String expressionWithFormat) { return DateMath.calculateDateString(date, expressionWithFormat, null); } /** * Calculates a date, returning the formatted string version of the * calculated date. * * @param date * the target date against the expression will be applied. If * <tt>null</tt>, the current date is used. * @param expressionWithFormat * the relative date expression with optional format * specification. * @param locale * the desired locale for the formatted string. * @return The calculated date as a string. * @throws IllegalArgumentException * if <tt>expressionWithFormat</tt> is invalid. */ public static String calculateDateString(final Calendar date, final String expressionWithFormat, final Locale locale) { int index = expressionWithFormat.indexOf(';'); String expression; String pattern = null; Calendar target = (date == null) ? Calendar.getInstance() : date; DateFormat format; Locale myLocale; if (index >= 0) { pattern = expressionWithFormat.substring(index + 1); expression = expressionWithFormat.substring(0, index); } else { expression = expressionWithFormat; } target = DateMath.calculateDate(date, expression); myLocale = (locale == null) ? LocaleHelper.getLocale() : locale; if (myLocale == null) { myLocale = LocaleHelper.getDefaultLocale(); } if (pattern != null) { format = new SimpleDateFormat(pattern, myLocale); } else { format = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, myLocale); } return format.format(target.getTime()); } /** * Calculates the date specified by the expression, relative to the current * date/time. The method is a short cut for * {@link #calculate(Calendar, String) calculate(null,expression)}. * * @param expression * the date expression as described above. * @return The calculated date. * @throws IllegalArgumentException * if <tt>expression</tt> is invalid. */ public static Calendar calculateDate(final String expression) { return DateMath.calculateDate(null, expression); } /** * Calculates the date specified by the expression, relative to the * indicated date/time. * * @param date * the target date against the expression is evaluated. If * <tt>null</tt>, the current date/time is used. If not * <tt>null</tt>, the object is manipulated by the expression. * @param expression * the date expression as described above. * @return The calculated date. This will be <tt>date</tt> if * <tt>date</tt> is not <tt>null</tt>. */ public static Calendar calculateDate(final Calendar date, final String expression) { StringTokenizer tok; String myExpression; Calendar target = date; int index = expression.indexOf(';'); if (index >= 0) { myExpression = expression.substring(index + 1); } else { myExpression = expression; } tok = new StringTokenizer(myExpression, " \t;"); //$NON-NLS-1$ while (tok.hasMoreElements()) { target = DateMath.parseAndCalculateDate(target, (String) tok.nextElement()); } return target; } /** * Parses and executes a single expression, one without subexpressions. * * @param date * the target date against the expression is evaluated. If * <tt>null</tt>, the current date/time is used. If not * <tt>null</tt>, the object is manipulated by the expression. * @param expression * the date expression as described above. * @return The calculated date. This will be <tt>date</tt> if * <tt>date</tt> is not <tt>null</tt>. */ private static Calendar parseAndCalculateDate(final Calendar date, final String expression) { int index = expression.indexOf(':'); // $NON-NLS-1$ char operation = '+'; // $NON-NLS-1$ char unit = ' '; // $NON-NLS-1$ char position = ' '; // $NON-NLS-1$ int operand = 0; Calendar result; if (index >= 0) { try { String number = expression.substring(0, index); operation = number.charAt(0); if ((operation == '+') || (operation == '-')) { // $NON-NLS-1$ // Integer.praseInt doesn't handle '+' for positive numbers // number = number.substring(1); } else { operation = '+'; } operand = Integer.parseInt(number); index++; unit = expression.charAt(index); index++; if (index < expression.length()) { position = expression.charAt(index); } result = DateMath.calculateDate(date, operation, operand, unit, position); } catch (Exception ex) { IllegalArgumentException err = new IllegalArgumentException(expression); err.initCause(ex); throw err; } } else { throw new IllegalArgumentException(expression); } return result; } /** * Calculates the relative date based upon the values of the BNF * non-terminals above. * * @param date * the target date against the expression is evaluated. If * <tt>null</tt>, the current date/time is used. If not * <tt>null</tt>, the object is manipulated by the expression. * @param operation * the value of the operation. Currently, this is the sign on the * operand. However, in the future, it could be some value to * indicate a relative or specific value. * @param operand * the value of the NUM token. * @param unit * the value of the <unit> non-terminal * @param position * the value of teh <position> non-terminal * @return The calculated date. This will be <tt>date</tt> if * <tt>date</tt> is not <tt>null</tt>. */ private static Calendar calculateDate(final Calendar date, final char operation, int operand, final char unit, final char position) { Calendar target = (date == null) ? Calendar.getInstance() : date; int calendarField = -1; switch (unit) { case UNIT_YEAR: calendarField = Calendar.YEAR; break; case UNIT_MONTH: calendarField = Calendar.MONTH; break; case UNIT_WEEK: calendarField = Calendar.DAY_OF_YEAR; operand = operand * 7; break; case UNIT_DAY: calendarField = Calendar.DAY_OF_YEAR; break; case UNIT_HOUR: calendarField = Calendar.HOUR_OF_DAY; break; case UNIT_MINUTE: calendarField = Calendar.MINUTE; break; case UNIT_SECOND: calendarField = Calendar.SECOND; break; default: throw new IllegalArgumentException(); } if (operation == ' ') { target.set(calendarField, operand); } else if (operation == '+') { target.add(calendarField, operand); } else if (operation == '-') { target.add(calendarField, -Math.abs(operand)); } if (unit == DateMath.UNIT_YEAR) { if (position == DateMath.POSITION_START) { target.set(Calendar.DAY_OF_YEAR, 1); DateMath.setTimeToStart(target); } else if (position == DateMath.POSITION_END) { target.set(Calendar.DAY_OF_YEAR, target.getActualMaximum(Calendar.DAY_OF_YEAR)); DateMath.setTimeToEnd(target); } } else if (unit == DateMath.UNIT_MONTH) { if (position == DateMath.POSITION_START) { target.set(Calendar.DAY_OF_MONTH, 1); DateMath.setTimeToStart(target); } else if (position == DateMath.POSITION_END) { target.set(Calendar.DAY_OF_MONTH, target.getActualMaximum(Calendar.DAY_OF_MONTH)); DateMath.setTimeToEnd(target); } } else if (unit == DateMath.UNIT_WEEK) { int firstDOW = target.getFirstDayOfWeek(); int dayOfWeek = target.get(Calendar.DAY_OF_WEEK); // force // calculation int dayOffset = 0; if (position == DateMath.POSITION_START) { if (dayOfWeek > firstDOW) { // Past first day of week; go backwards to first day // dayOffset = firstDOW - dayOfWeek; } else if (dayOfWeek < firstDOW) { // Before the first day; go back a week and move forward to // first day // Should only happen if first day is not Sunday. // dayOffset = -7 + (firstDOW - dayOfWeek); } DateMath.setTimeToStart(target); } else if (position == DateMath.POSITION_END) { int lastDOW; if (firstDOW == Calendar.SUNDAY) { lastDOW = Calendar.SATURDAY; } else { lastDOW = firstDOW - 1; } if (dayOfWeek < lastDOW) { // Before the last day of week; move forward to last day // dayOffset = lastDOW - dayOfWeek; } else if (dayOfWeek > lastDOW) { // Should only happen if last day is anything but Saturday; // Move to next week; roll back to last day. dayOffset = 7 - (dayOfWeek - lastDOW); } DateMath.setTimeToEnd(target); } if (dayOffset != 0) { target.add(Calendar.DAY_OF_YEAR, dayOffset); } } else if (unit == DateMath.UNIT_DAY) { if (position == DateMath.POSITION_START) { DateMath.setTimeToStart(target); } else if (position == DateMath.POSITION_END) { DateMath.setTimeToEnd(target); } } else if (unit == DateMath.UNIT_HOUR) { if (position == DateMath.POSITION_START) { target.set(Calendar.MINUTE, 0); target.set(Calendar.SECOND, 0); target.set(Calendar.MILLISECOND, 0); } else if (position == DateMath.POSITION_END) { target.set(Calendar.MINUTE, 59); target.set(Calendar.SECOND, 59); target.set(Calendar.MILLISECOND, 999); } } else if (unit == DateMath.UNIT_MINUTE) { if (position == DateMath.POSITION_START) { target.set(Calendar.SECOND, 0); target.set(Calendar.MILLISECOND, 0); } else if (position == DateMath.POSITION_END) { target.set(Calendar.SECOND, 59); target.set(Calendar.MILLISECOND, 999); } } else if (unit == DateMath.UNIT_SECOND) { if (position == DateMath.POSITION_START) { target.set(Calendar.MILLISECOND, 0); } else if (position == DateMath.POSITION_END) { target.set(Calendar.MILLISECOND, 999); } } target.getTimeInMillis(); // force calculations return target; } /** * Sets the time to the start of the day (00:00:00.000). * * @param target * the target calendar for which the time will be set. */ private static void setTimeToStart(final Calendar target) { target.set(Calendar.MILLISECOND, 0); target.set(Calendar.SECOND, 0); target.set(Calendar.MINUTE, 0); target.set(Calendar.HOUR_OF_DAY, 0); } /** * Sets the time to the endof the day (23:59:59.999). * * @param target * the target calendar for which the time will be set. */ private static void setTimeToEnd(final Calendar target) { target.set(Calendar.MILLISECOND, 999); target.set(Calendar.SECOND, 59); target.set(Calendar.MINUTE, 59); target.set(Calendar.HOUR_OF_DAY, 23); } }