001: /*
002: * This file is part of the WfMOpen project.
003: * Copyright (C) 2001-2003 Danet GmbH (www.danet.de), GS-AN.
004: * All rights reserved.
005: *
006: * This program is free software; you can redistribute it and/or modify
007: * it under the terms of the GNU General Public License as published by
008: * the Free Software Foundation; either version 2 of the License, or
009: * (at your option) any later version.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: * GNU General Public License for more details.
015: *
016: * You should have received a copy of the GNU General Public License
017: * along with this program; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: *
020: * $Id: Duration.java,v 1.3 2007/05/03 21:58:21 mlipp Exp $
021: *
022: * $Log: Duration.java,v $
023: * Revision 1.3 2007/05/03 21:58:21 mlipp
024: * Internal refactoring for making better use of local EJBs.
025: *
026: * Revision 1.2 2006/09/29 12:32:08 drmlipp
027: * Consistently using WfMOpen as projct name now.
028: *
029: * Revision 1.1.1.1 2003/12/19 13:01:36 drmlipp
030: * Updated to 1.1rc1
031: *
032: * Revision 1.8 2003/09/25 11:00:38 lipp
033: * Added support for remote resources in evaluation.
034: *
035: * Revision 1.7 2003/09/20 19:26:06 lipp
036: * Modified complex time value specification handling.
037: *
038: * Revision 1.6 2003/09/09 13:38:16 lipp
039: * Added ability to handle post parsing fixes in bulk.
040: *
041: * Revision 1.5 2003/09/07 19:40:53 lipp
042: * Fixed duration arithmetic.
043: *
044: * Revision 1.4 2003/09/06 20:36:59 lipp
045: * Some fixes.
046: *
047: * Revision 1.3 2003/09/05 15:09:00 lipp
048: * Fixed sign handling, untested arithmetic added.
049: *
050: * Revision 1.2 2003/09/05 11:16:12 lipp
051: * Optimized.
052: *
053: * Revision 1.1 2003/09/04 15:55:46 lipp
054: * Initial version.
055: *
056: */
057: package de.danet.an.util;
058:
059: import java.util.Calendar;
060: import java.util.Date;
061: import java.util.TimeZone;
062: import java.util.regex.Matcher;
063: import java.util.regex.Pattern;
064:
065: import java.rmi.RemoteException;
066: import java.text.ParseException;
067:
068: /**
069: * This class provides a representation of durations and a parser for
070: * strings that represent durations.<P>
071: *
072: * Strings that can be parsed consist of value/unit pairs. Values can
073: * be arbitrary text, valid unit names are "years", "yrs", "months",
074: * "mts", "hours", "hrs", "min" and "sec". There may be only one pair
075: * for each unit i.e. "5 years 3 yrs" is illegal.<P>
076: *
077: * Value/unit pairs must be separated from each other with at least
078: * one whitespace character. Units may follow values immediately if
079: * the value is a number, else they must be separated by at least one
080: * whitespace.<P>
081: *
082: * Any duration unit may be omitted, but the descending order of units
083: * must be preserved.<P>
084: *
085: * Parsing is done in two phases. First, during <code>parse</code> the
086: * value/unit pairs are identified and the values are stored as
087: * strings. This allows the class even to be used when the values are
088: * expressions that must be evaluated. If this feature is used, the
089: * evaluation must be performed by retrieving the string values (using
090: * "<code>...AsParsed</code>"), evaluating the expression and setting
091: * the result.<P>
092: *
093: * <code>Duration</code> will try to convert the parsed string to a
094: * number only if a unit is accessed and has not been set
095: * explicitly. Thus, all accessor methods may throw a
096: * <code>NumberFormatException</code> even if <code>parse</code> has
097: * not thrown a <code>ParseException</code>.
098: *
099: * @author <a href="mailto:lipp@danet.de">Michael Lipp</a>
100: * @version $Revision: 1.3 $
101: */
102:
103: public class Duration {
104:
105: private static final org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory
106: .getLog(Duration.class);
107:
108: /**
109: * This interface must be implemented by a class that wants to
110: * evaluate the value expression during duration parsing.
111: */
112: public static interface ValueEvaluator {
113:
114: /**
115: * Evaluate the given string.
116: * @param s the string to be parsed
117: * @return the evaluated value
118: * @throws ParseException if a value cannot be derived
119: */
120: float evaluate(String s) throws ParseException;
121: }
122:
123: private static Pattern parsePat = Pattern
124: .compile("^\\s*((.*?(\\s+|[0-9]))(years|yrs)\\s+)?"
125: + "((.*?(\\s+|[0-9]))(months|mth)\\s+)?"
126: + "((.*?(\\s+|[0-9]))(days|dys)\\s+)?"
127: + "((.*?(\\s+|[0-9]))(hours|hrs)\\s+)?"
128: + "((.*?(\\s+|[0-9]))(min)\\s+)?"
129: + "((.*?(\\s+|[0-9]))(sec)\\s+)?\\s*?$");
130:
131: private int years = 0;
132: private int months = 0;
133: private int days = 0;
134: private int hours = 0;
135: private int minutes = 0;
136: private float seconds = 0;
137:
138: /**
139: * Creates an instance of <code>Duration</code> with a time span of 0.
140: */
141: public Duration() {
142: }
143:
144: /**
145: * Get the value of years.
146: * @return value of years.
147: * @see #setYears
148: */
149: public int getYears() {
150: return years;
151: }
152:
153: /**
154: * Set the value of years.
155: * @param v value to assign to years.
156: * @see #getYears
157: */
158: public void setYears(int v) {
159: years = v;
160: }
161:
162: /**
163: * Get the value of months.
164: * @return value of months.
165: * @see #setMonths
166: */
167: public int getMonths() {
168: return months;
169: }
170:
171: /**
172: * Set the value of months.
173: * @param v value to assign to months.
174: * @see #getMonths
175: */
176: public void setMonths(int v) {
177: months = v;
178: }
179:
180: /**
181: * Get the value of days.
182: * @return value of days.
183: * @see #setDays
184: */
185: public int getDays() {
186: return days;
187: }
188:
189: /**
190: * Set the value of days.
191: * @param v value to assign to days.
192: * @see #getDays
193: */
194: public void setDays(int v) {
195: days = v;
196: }
197:
198: /**
199: * Get the value of hours.
200: * @return value of hours.
201: * @see #setHours
202: */
203: public int getHours() {
204: return hours;
205: }
206:
207: /**
208: * Set the value of hours.
209: * @param v value to assign to hours.
210: * @see #getHours
211: */
212: public void setHours(int v) {
213: hours = v;
214: }
215:
216: /**
217: * Get the value of minutes.
218: * @return value of minutes.
219: * @see #setMinutes
220: */
221: public int getMinutes() {
222: return minutes;
223: }
224:
225: /**
226: * Set the value of minutes.
227: * @param v value to assign to minutes.
228: * @see #getMinutes
229: */
230: public void setMinutes(int v) {
231: minutes = v;
232: }
233:
234: /**
235: * Get the value of seconds.
236: * @return value of seconds.
237: * @see #setSeconds
238: */
239: public float getSeconds() {
240: return seconds;
241: }
242:
243: /**
244: * Set the value of seconds.
245: * @param v value to assign to seconds.
246: * @see #getSeconds
247: */
248: public void setSeconds(float v) {
249: seconds = v;
250: }
251:
252: /**
253: * Parse the given string.
254: * @param s the text to parse
255: * @return the duration
256: * @throws ParseException if the string cannot be parsed
257: */
258: public static Duration parse(String s) throws ParseException {
259: return parse(s, null);
260: }
261:
262: /**
263: * Parse the given string.
264: * @param s the text to parse
265: * @param e the evaluator, if <code>null</code> use simple float
266: * parser
267: * @return the duration
268: * @throws ParseException if the string cannot be parsed
269: */
270: public static Duration parse(String s, ValueEvaluator e)
271: throws ParseException {
272: Duration res = new Duration();
273: Matcher m = parsePat.matcher(s + " ");
274: if (!m.matches()) {
275: throw new ParseException(
276: "Invalid duration specification: \"" + s + "\".", 0);
277: }
278: if (e == null) {
279: e = new ValueEvaluator() {
280: public float evaluate(String s) throws ParseException {
281: try {
282: return Float.parseFloat(s);
283: } catch (NumberFormatException e) {
284: throw new ParseException("Not a number: " + s
285: + ": " + e.getMessage(), 0);
286: }
287: }
288: };
289: }
290: String g = m.group(2);
291: if (g != null) {
292: res.years = (int) e.evaluate(g.trim());
293: }
294: g = m.group(6);
295: if (g != null) {
296: res.months = (int) e.evaluate(g.trim());
297: }
298: g = m.group(10);
299: if (g != null) {
300: res.days = (int) e.evaluate(g.trim());
301: }
302: g = m.group(14);
303: if (g != null) {
304: res.hours = (int) e.evaluate(g.trim());
305: }
306: g = m.group(18);
307: if (g != null) {
308: res.minutes = (int) e.evaluate(g.trim());
309: }
310: g = m.group(22);
311: if (g != null) {
312: res.seconds = e.evaluate(g.trim());
313: }
314: return res;
315: }
316:
317: /**
318: * Adds this duration to the given date and returns the
319: * result. This uses the <a
320: * href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes">
321: * algorithm</a> described in the XSD datatypes specification.
322: *
323: * @param dateTime the date/time to add to
324: * @return the date/time offset by this duration
325: */
326: public Date addTo(Date dateTime) {
327: Calendar cal = Calendar
328: .getInstance(TimeZone.getTimeZone("GMT"));
329: cal.setTime(dateTime);
330: // month (step 1)
331: int month = cal.get(Calendar.MONTH) + getMonths();
332: int carry = fQuotient(month, 12);
333: month -= (carry * 12 - 1);
334: // year (step 1)
335: int year = cal.get(Calendar.YEAR) + getYears() + carry;
336: // seconds
337: float seconds = cal.get(Calendar.SECOND)
338: + (float) cal.get(Calendar.MILLISECOND) / 1000
339: + getSeconds();
340: carry = (int) Math.floor(seconds / 60);
341: seconds -= carry * 60;
342: // minutes
343: int minutes = cal.get(Calendar.MINUTE) + getMinutes() + carry;
344: carry = fQuotient(minutes, 60);
345: minutes -= carry * 60;
346: // hours
347: int hours = cal.get(Calendar.HOUR_OF_DAY) + getHours() + carry;
348: carry = fQuotient(hours, 24);
349: hours -= carry * 24;
350: // day
351: int day = cal.get(Calendar.DAY_OF_MONTH);
352: int maxInRes = maxDayInMonth(year, month);
353: if (day > maxInRes) {
354: day = maxInRes;
355: }
356: day += getDays() + carry;
357: while (true) {
358: if (day < 1) {
359: month -= 1;
360: carry = fQuotient(month - 1, 12);
361: month -= carry * 12;
362: maxInRes = maxDayInMonth(year, month);
363: day += maxInRes;
364: } else if (day > maxInRes) {
365: day -= maxInRes;
366: month += 1;
367: carry = fQuotient(month - 1, 12);
368: month -= carry * 12;
369: maxInRes = maxDayInMonth(year, month);
370: } else {
371: break;
372: }
373: year += carry;
374: }
375: cal.set(Calendar.YEAR, year);
376: cal.set(Calendar.MONTH, month - 1);
377: cal.set(Calendar.DAY_OF_MONTH, day);
378: cal.set(Calendar.HOUR_OF_DAY, hours);
379: cal.set(Calendar.MINUTE, minutes);
380: cal.set(Calendar.SECOND, (int) seconds);
381: cal.set(Calendar.MILLISECOND, Math.round((seconds % 1) * 1000));
382:
383: return cal.getTime();
384: }
385:
386: private final int fQuotient(int a, int b) {
387: return (int) Math.floor((float) a / b);
388: }
389:
390: private static int[] daysPerMonth = new int[] { 31, // Jan
391: 28, // Feb
392: 31, // Mar
393: 30, // Apr
394: 31, // Mai
395: 30, // Jun
396: 31, // Jul
397: 31, // Aug
398: 30, // Sep
399: 31, // Oct
400: 30, // Nov
401: 31 // Dec
402: };
403:
404: private int maxDayInMonth(int year, int month) {
405: year = year + (month - 1) / 12;
406: month = (month - 1) % 12 + 1;
407: if (month != 2) {
408: return daysPerMonth[month - 1];
409: }
410: if (year % 400 == 0 || year % 100 != 0 && year % 4 == 0) {
411: return 29;
412: }
413: return 28;
414: }
415:
416: /**
417: * Return a string representation for debugging purposes.
418: * @return string representation.
419: */
420: public String toString() {
421: return "Duration[" + "yrs=" + years + ",mts=" + months
422: + ",dys=" + days + ",hrs=" + hours + ",min=" + minutes
423: + ",sec=" + seconds + "]";
424: }
425: }
|