001/* ====================================================================
002 * /util/DateConverter.java
003 * 
004 * (c) by Dirk Lehmann
005 * ====================================================================
006 */
007package de.lehmannet.om.util;
008import java.util.Calendar;
009import java.util.GregorianCalendar;
010import java.util.SimpleTimeZone;
011import java.util.StringTokenizer;
012import java.util.TimeZone;
013/**
014 * 
015 * The DateConverter is a helper class that provides methods for
016 * 
017 * handling the all kind of date formats.<br>
018 * 
019 * E.g. the ISO8601 date format (A short summary about the ISO8601 date
020 * 
021 * format can be accessed at the
022 * 
023 * <a href="http://www.w3.org/TR/NOTE-datetime">W3C</a>.), or the Julian
024 * 
025 * date.<br>
026 * 
027 * 
028 * 
029 * @author doergn@users.sourceforge.net
030 * 
031 * @since 1.0
032 */
033public class DateConverter {
034  // ---------
035  // Constants ---------------------------------------------------------
036  // ---------
037  /* Delimiter for ISO8601 date entries. Example: year-month-day */
038  private static final String DATE_DELIMITER = "-";
039  /* Delimiter for ISO8601 time entries. Example: hour-minute-second */
040  private static final String TIME_DELIMITER = ":";
041  /* Delimiter for ISO8601 date and time section. Example: dateTtime */
042  private static final String DATETIME_DELIMITER = "T";
043  /* Timezone symbol for UTC (zulu) time. Used when time is set in UTC. */
044  private static final String UTC_TIMEZONE_OFFSET = "Z";
045  // --------------
046  // Public methods ----------------------------------------------------
047  // --------------
048  // -------------------------------------------------------------------
049  /**
050   * 
051   * Converts a gregorian date into a julian date.
052   * 
053   * 
054   * 
055   * @param gregorianDate
056   *          The gregorianDate date
057   * 
058   * @return A julian date with seconds accuracy
059   */
060  public static double toJulianDate(Calendar gregorianCalendar) {
061    if (gregorianCalendar == null) {
062      throw new IllegalArgumentException("Gregorian date has illegal value. (NULL)");
063    }
064    int month = gregorianCalendar.get(Calendar.MONTH) + 1; // Java Month starts with 0!
065    int year = gregorianCalendar.get(Calendar.YEAR);
066    int day = gregorianCalendar.get(Calendar.DAY_OF_MONTH);
067    int hour = gregorianCalendar.get(Calendar.HOUR_OF_DAY);
068    int minute = gregorianCalendar.get(Calendar.MINUTE); 
069    int seconds = gregorianCalendar.get(Calendar.SECOND);
070    // Timezone offset (including Daylight Saving Time) in hours
071    int tzOffset = (gregorianCalendar.get(Calendar.DST_OFFSET) + gregorianCalendar.get(Calendar.ZONE_OFFSET))
072        / (60 * 60 * 1000);
073    if (month < 3) {
074      year--;
075      month = month + 12;
076    }
077
078    // Calculation of leap year
079    double leapYear = Double.NaN;
080    Calendar gregStartDate = Calendar.getInstance();
081    gregStartDate.set(1583, 9, 16); // 16.10.1583 day of introduction of greg. Calendar. Before that there're no leap
082                                    // years
083    if (gregorianCalendar.after(gregStartDate)) {
084      leapYear = 2 - (Math.round(year / 100)) + Math.round((Math.round(year / 100)) / 4);
085    }
086    Calendar gregBeginDate = Calendar.getInstance();
087    gregBeginDate.set(1583, 9, 4); // 04.10.1583 last day before gregorian calendar reformation
088    if ((gregorianCalendar.before(gregBeginDate))
089    || (gregorianCalendar.equals(gregBeginDate))
090    ) {
091      leapYear = 0;
092    }
093    if (Double.isNaN(leapYear)) {
094      throw new IllegalArgumentException(
095          "Date is not valid. Due to gregorian calendar reformation after the 04.10.1583 follows the 16.10.1583.");
096    }
097
098
099    //GMB APR12,2012 - fixing bug #3514617 ---------------------------------------------------------------------------- 
100    double fracSecs = (double)(((hour - tzOffset) * 3600) + (minute * 60) + seconds) / 86400;
101    
102    long c = (long)(365.25 * (year + 4716));
103    long d = (long)(30.6001 * (month + 1));
104
105    double julianDate = day + c + d + fracSecs + leapYear - 1524.5;
106
107    /*
108     * double julianDate = c + d + hourAndMinutes + leapYear - 1524.5;
109
110
111    if( (month == 5) || (month == 7) || (month == 10) || (month == 12) ) { // Those month have 31 days
112      julianDate = julianDate-1;
113    }
114    */
115
116
117
118    //System.out.println("@@ day= " + Double.toString(day) + " month= " + Long.toString(month) +  " year= " + Long.toString(year) + " hour= " + Long.toString(hour) + " mimute= " + Long.toString(minute) + " seconds= " + Long.toString(seconds) + " tzOffset= " + Long.toString(tzOffset));
119    //System.out.println("@@ JULIAN DATE: " + Double.toString(julianDate) + " ==> c=" + Long.toString(c) + " d= " + Long.toString(d) + " fracSecs= " + Double.toString(fracSecs) + " leapYear= " + Double.toString(leapYear));
120
121    
122    //GMB APR12,2012 - end fix #3514617 ------------------------------------------------------------------------------- 
123
124    return  julianDate;
125  }
126
127
128
129  // -------------------------------------------------------------------
130  /**
131   * 
132   * Converts a julian date into a gregorian date.<br>
133   * 
134   * 
135   * 
136   * @param julianDate
137   *          The julian date
138   * 
139   * @return A gregorian date with seconds accuracy (Timezone = GMT)
140   */
141  public static Calendar toGregorianDate(double julianDate) {
142    return DateConverter.toGregorianDate(julianDate, TimeZone.getTimeZone("GMT"));
143  }
144  // -------------------------------------------------------------------
145  /**
146   * 
147   * Converts a julian date into a gregorian date.<br>
148   * 
149   * 
150   * 
151   * @param julianDate
152   *          The julian date
153   * 
154   * @param zone
155   *          The timzone for the returned gregorian date (if <code>NULL</code> is
156   * 
157   *          passed GMT will be taken)
158   * 
159   * @return A gregorian date
160   */
161  public static Calendar toGregorianDate(double julianDate, TimeZone zone) {
162    if ((julianDate == Double.NaN)
163    || (julianDate == Double.NEGATIVE_INFINITY)
164    || (julianDate == Double.POSITIVE_INFINITY)) {
165      throw new IllegalArgumentException("Julian Date has illegal value. (Value=" + julianDate + ")");
166    }
167    if (zone == null) {
168      zone = TimeZone.getTimeZone("GMT");
169    }
170    julianDate = julianDate + 0.5;
171    int onlyDays = (int) Math.round(julianDate);
172    double onlyMinutes = julianDate - onlyDays;
173    double hours = 24 * onlyMinutes;
174    int hour = (int) (Math.round(hours));
175    int minute = (int) ((hours - hour) * 60);
176    // int sec = (int)Math.round((hours * 3600) - ((minute * 60) + (hour * 3600)));
177    int sec = (int) ((((hours - hour) * 60) - minute) * 60);
178    double leapYear100 = (int) ((onlyDays - 1867216.25) / 36524.25);
179    double daysLeapYear = onlyDays + 1 + leapYear100 - (int) (leapYear100 / 4);
180    if (onlyDays < 2299161) {
181      daysLeapYear = onlyDays;
182    }
183    double completeLeapDays = daysLeapYear + 1524;
184    double completeYear = (int) ((completeLeapDays - 122.1) / 365.25);
185    double completeDays = (int) (365.25 * completeYear);
186    double completeMonths = (int) ((completeLeapDays - completeDays) / 30.6001);
187    int day = (int) (completeLeapDays - completeDays - (int) (30.6001 * completeMonths) + onlyMinutes);
188    int month = 0;
189    if (completeMonths < 14) {
190      month = (int) completeMonths - 1;
191    } else {
192      month = (int) completeMonths - 13;
193    }
194    int year = 0;
195    if (month > 2) {
196      year = (int) completeYear - 4716; // only AD years
197    } else {
198      year = (int) completeYear - 4715; // only AD years
199    }
200    Calendar gregorianDate = Calendar.getInstance(zone);
201    gregorianDate.set(year, month - 1, day + 1);
202    // DST offset and timezone offset calculation
203    int offset = (gregorianDate.get(Calendar.ZONE_OFFSET) + gregorianDate.get(Calendar.DST_OFFSET)) / (3600 * 1000);
204    // Month-1 as January is 0 in JAVA dates/calendars
205    gregorianDate.set(year, month - 1, day + 1, hour + offset, minute, sec);
206    return gregorianDate;
207  }
208  // -------------------------------------------------------------------
209  /**
210   * 
211   * Converts a Date object into a String object that represents a
212   * 
213   * ISO8601 conform string.
214   * 
215   * 
216   * 
217   * @param calendar
218   *          A java.util.Date object that has to be converted
219   * 
220   * @return A ISO8601 conform String, or <code>null</code> if the
221   * 
222   *         given date was <code>null</code>
223   */
224  public static String toISO8601(Calendar calendar) {
225    if (calendar == null) {
226      return null;
227    }
228    StringBuffer iso8601 = new StringBuffer();
229    iso8601.append(calendar.get(Calendar.YEAR));
230    iso8601.append(DATE_DELIMITER);
231    iso8601.append(setLeadingZero(calendar.get(Calendar.MONTH) + 1));
232    iso8601.append(DATE_DELIMITER);
233    iso8601.append(setLeadingZero(calendar.get(Calendar.DAY_OF_MONTH)));
234    iso8601.append(DATETIME_DELIMITER);
235    iso8601.append(setLeadingZero(calendar.get(Calendar.HOUR_OF_DAY)));
236    iso8601.append(TIME_DELIMITER);
237    iso8601.append(setLeadingZero(calendar.get(Calendar.MINUTE)));
238    iso8601.append(TIME_DELIMITER);
239    iso8601.append(setLeadingZero(calendar.get(Calendar.SECOND)));
240    // Get Offset in minutes
241    int offset = ((calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / 1000) / 60;
242    iso8601.append(formatTimezone(offset));
243    return iso8601.toString();
244  }
245  // -------------------------------------------------------------------
246  /**
247   * 
248   * Converts a String object that contains a ISO8601 conform value to an
249   * 
250   * java.util.Calendar object.
251   * 
252   * 
253   * 
254   * @param iso8601
255   *          A String with a ISO8601 conform value
256   * 
257   * @return The parameters date as java.util.Calendar, or <code>null</code>
258   * 
259   *         if the given string was <code>null</code> or empty.
260   * 
261   * @throws NumberFormatException
262   *           if given ISO8601 is malformed.
263   */
264  public static Calendar toDate(String iso8601) throws NumberFormatException {
265    if (iso8601 == null
266    || "".equals(iso8601)
267    ) {
268      return null;
269    }
270    StringTokenizer tokenizer = new StringTokenizer(iso8601);
271    String year = tokenizer.nextToken(DATE_DELIMITER);
272    String month = tokenizer.nextToken(DATE_DELIMITER);
273    String day = tokenizer.nextToken(DATETIME_DELIMITER);
274    day = day.substring(1, day.length()); // cutoff '-'
275    String hour = tokenizer.nextToken(TIME_DELIMITER);
276    hour = hour.substring(1, hour.length()); // cutoff 'T'
277    hour = cutLeadingZeroAndPlus(hour);
278    String minute = tokenizer.nextToken(TIME_DELIMITER);
279    minute = cutLeadingZeroAndPlus(minute);
280    String secAndTZ = iso8601.substring(iso8601.indexOf(TIME_DELIMITER) + 4, iso8601.length());
281    String second = secAndTZ.substring(0, 2);
282    second = cutLeadingZeroAndPlus(second);
283    String timeZone = secAndTZ.substring(2, secAndTZ.length());
284    int i_year = 0;
285    int i_month = 0;
286    int i_day = 0;
287    int i_hour = 0;
288    int i_minute = 0;
289    int i_second = 0;
290    String step = "";
291    try {
292      step = "year";
293      i_year = Integer.parseInt(year);
294      step = "month";
295      i_month = Integer.parseInt(month) - 1;
296      step = "day";
297      i_day = Integer.parseInt(day);
298      step = "hour";
299      i_hour = Integer.parseInt(hour);
300      step = "minute";
301      i_minute = Integer.parseInt(minute);
302      step = "second";
303      i_second = Integer.parseInt(second);
304    } catch (NumberFormatException nfe) {
305      throw new NumberFormatException("Cannot generate ISO8601 date because " + step + " is malformed. ");
306    }
307    Calendar calendar = null;
308    if ((UTC_TIMEZONE_OFFSET.equals(timeZone))
309    || ("".equals(timeZone.trim()))
310    ) {
311      calendar = new GregorianCalendar(new SimpleTimeZone(0, "GMT"));
312    } else {
313      calendar = new GregorianCalendar(createTimezone(timeZone));
314    }
315    calendar.set(Calendar.YEAR, i_year);
316    calendar.set(Calendar.MONTH, i_month);
317    calendar.set(Calendar.DAY_OF_MONTH, i_day);
318    calendar.set(Calendar.HOUR_OF_DAY, i_hour);
319    calendar.set(Calendar.MINUTE, i_minute);
320    calendar.set(Calendar.SECOND, i_second);
321    calendar.set(Calendar.MILLISECOND, 0); // make sure they have no meanings if we compare dates
322    return calendar;
323  }
324  // ---------------
325  // Private methods ---------------------------------------------------
326  // ---------------
327  // -------------------------------------------------------------------
328  /*
329   * 
330   * Creates a TimeZone object from the last part of a ISO8601 date
331   * 
332   * representing String. (e.g. +1:00)
333   */
334  private static TimeZone createTimezone(String timeZone) throws NumberFormatException {
335    String h = timeZone.substring(0, timeZone.indexOf(TIME_DELIMITER));
336    if (h.startsWith("+")) {
337      h = cutLeadingZeroAndPlus(h);
338    }
339    String m = timeZone.substring(timeZone.indexOf(TIME_DELIMITER) + 1, timeZone.length());
340    int hour = 0;
341    int minute = 0;
342    try {
343      hour = Integer.parseInt(h);
344      minute = Integer.parseInt(m);
345    } catch (NumberFormatException nfe) {
346      new NumberFormatException("Cannot generate ISO8601 date because timezones hour or minute value is malformed. ");
347    }
348    // Calculate Timezone Offset:
349    // Hour: hour * secOfHour(3600) * mSecOfSecond(1000)
350    // Minute: minute * mSecOfSecond(1000) * secOfMinute(60)
351    // Depending on negative or positiv offset the minutes have to be added or subtracted from hour offset.
352    // Therefore add always and set minutes positiv or negativ (depending on hour value (if it is set)).
353    SimpleTimeZone tz = null;
354    int offset = 0;
355    if (hour == 0) {
356      offset = (hour * 3600 * 1000) + ((minute * 1000 * 60));
357      tz = new SimpleTimeZone(offset, "");
358    } else {
359      offset = (hour * 3600 * 1000) + ((minute * 1000 * 60) * (hour / Math.abs(hour)));
360      tz = new SimpleTimeZone(offset, "");
361    }
362    tz.setDSTSavings(1); // We request user to not enter DST
363    return tz;
364  }
365  // -------------------------------------------------------------------
366  /*
367   * 
368   * Sets a leading 0 to a given value, if the value has only one digit.
369   */
370  public static String setLeadingZero(int value) {
371    if ((value <= 9)
372    && (value >= -9)) {
373      if (value < 0) {
374        return "-0" + Math.abs(value);
375      }
376      return "0" + Math.abs(value);
377    }
378    return "" + value;
379  }
380  
381  // -------------------------------------------------------------------
382  /*
383   * 
384   * Sets a leading 0 to a given value, if the value has only one digit.
385   */
386  public static String setLeadingZero(double value) {
387    if ((value <= 9)
388    && (value >= -9)) {
389      if (value < 0) {
390        return "-0" + Math.abs(value);
391      }
392      return "0" + Math.abs(value);
393    }
394    return "" + value;
395  }
396  
397  // -------------------------------------------------------------------
398  /*
399   * 
400   * Cuts off leadings zeros (and the + sign, if given) from a string
401   */
402  private static String cutLeadingZeroAndPlus(String value) {
403    if (value.startsWith("+0")) {
404      return value.substring(2, value.length());
405    } else if (value.startsWith("-0")) {
406      return "-" + value.substring(2, value.length());
407    }
408    if (value.startsWith("+")) {
409      value = value.substring(1, value.length());
410    }
411    return value;
412  }
413  // -------------------------------------------------------------------
414  /*
415   * 
416   * Takes a value in minutes and formats it an ISO8601 timezone String.
417   * 
418   * If the timezone is UTC, then the returned String will only contain
419   * 
420   * a 'Z' (not 0:00).
421   */
422  private static String formatTimezone(int min) {
423    // Get complete hours
424    int hour = (int) min / 60;
425    // Get minutes from not complete hour
426    int minutes = (hour * 60) - min;
427    // If hour and minutes equal 0 (UTC) the offset is given as Z
428    if ((hour == 0)
429    && (minutes == 0)) {
430      return UTC_TIMEZONE_OFFSET;
431    }
432    // Calculate the hour offset
433    String hourOffset = setLeadingZero(hour);
434    if (hour > -1) {
435      hourOffset = "+" + hourOffset;
436    }
437    // Calculate the minute offset
438    String minOffset = setLeadingZero(Math.abs(minutes));
439    return hourOffset + TIME_DELIMITER + minOffset;
440  }
441}