View Javadoc
1   /*
2    * Redistribution and use of this software and associated documentation ("Software"), with or
3    * without modification, are permitted provided that the following conditions are met:
4    *
5    * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
6    * must also contain a copy of this document.
7    *
8    * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
9    * conditions and the following disclaimer in the documentation and/or other materials provided with
10   * the distribution.
11   *
12   * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
13   * without prior written permission of Intalio, Inc. For written permission, please contact
14   * info@exolab.org.
15   *
16   * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
17   * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
18   * Intalio, Inc.
19   *
20   * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
21   *
22   * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
23   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
24   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
25   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
29   * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30   *
31   * Copyright 2000-2002 (C) Intalio, Inc. All Rights Reserved.
32   *
33   * $Id$ Date Author Changes 08/30/2001 Arnaud Blandin added to Calendar() (patch from S�bastien
34   * Stormacq [S.Stormacq@aubay-si.lu]) 05/29/2001 Arnaud Blandin Added order methods 05/22/2001
35   * Arnaud Blandin Created
36   */
37  package org.exolab.castor.types;
38  
39  import java.text.DateFormat;
40  import java.text.ParseException;
41  import java.util.Calendar;
42  import java.util.GregorianCalendar;
43  import java.util.Date;
44  import java.util.SimpleTimeZone;
45  import java.util.TimeZone;
46  
47  /**
48   * The base class for date/time XML Schema types.
49   * <p>
50   * The validation of the date/time fields is done in the set methods and follows
51   * <a href="http://www.iso.ch/markete/8601.pdf">the ISO8601 Date and Time Format</a>.
52   * <p>
53   * Note: the Castor date/time types are mutable, unlike the date/time types of the JDK in Java2.
54   * This is needed by the Marshaling framework.
55   *
56   * @version $Revision$
57   * @see DateTime
58   * @see Date
59   * @see Time
60   */
61  public abstract class DateTimeBase implements java.io.Serializable, Cloneable {
62    /** Public constant referring to an indeterminate Date/Time comparison. */
63    public static final int INDETERMINATE = -1;
64  
65    /** Public constant referring to a Date/Time comparison result of "less than". */
66    public static final int LESS_THAN = 0;
67  
68    /** Public constant referring to a Date/Time comparison result of "equals". */
69    public static final int EQUALS = 1;
70  
71    /** Public constant referring to a Date/Time comparison result of "greater than". */
72    public static final int GREATER_THAN = 2;
73  
74    /**
75     * When comparing a date/time with a time zone to one without, the recommendation says that 14
76     * hours is the time zone offset to use for comparison.
77     */
78    protected static final int MAX_TIME_ZONE_COMPARISON_OFFSET = 14;
79  
80    /** Convenience String for complaints. */
81    protected static final String WRONGLY_PLACED = " is wrongly placed.";
82  
83    /** true if this date/time type is negative. */
84    private boolean _isNegative = false;
85  
86    /** The century field. */
87    private short _century = 0;
88  
89    /** The year field. */
90    private short _year = 0;
91  
92    /** The month field. */
93    private short _month = 0;
94  
95    /** The day field. */
96    private short _day = 0;
97  
98    /** the hour field. */
99    private short _hour = 0;
100 
101   /** the minute field. */
102   private short _minute = 0;
103 
104   /** the second field. */
105   private short _second = 0;
106 
107   /** the millsecond field. */
108   private short _millsecond = 0;
109 
110   /** true if the time zone is negative. */
111   private boolean _zoneNegative = false;
112 
113   /** true if this date/time type has a time zone assigned. */
114   private boolean _utc = false;
115 
116   /** the time zone hour field. */
117   private short _zoneHour = 0;
118 
119   /** the time zone minute field. */
120   private short _zoneMinute = 0;
121 
122   ////////////////////////// Abstract methods////////////////////////////////////
123 
124   /**
125    * Returns a java.util.Date that represents the XML Schema Date datatype.
126    * 
127    * @return a java.util.Date that represents the XML Schema Date datatype.
128    */
129   public abstract Date toDate();
130 
131   /**
132    * Sets all the fields by reading the values in an array.
133    * 
134    * @param values an array of shorts with the values.
135    */
136   public abstract void setValues(short[] values);
137 
138   /**
139    * returns an array of short with all the fields that describe a date/time type.
140    * 
141    * @return an array of short with all the fields that describe a date/time type.
142    */
143   public abstract short[] getValues();
144 
145   ////////////////////////////////////////////////////////////////////////////
146 
147   /**
148    * Returns true if the given year represents a leap year. A specific year is a leap year if it is
149    * either evenly divisible by 400 OR evenly divisible by 4 and not evenly divisible by 100.
150    *
151    * @param year the year to test where 0 < year <= 9999
152    * @return true if the given year represents a leap year
153    */
154   public final boolean isLeap(int year) {
155     return ((year % 4) == 0 && (year % 100) != 0) || (year % 400) == 0;
156   }
157 
158   /**
159    * Returns true if the given year represents a leap year. A specific year is a leap year if it is
160    * either evenly divisible by 400 OR evenly divisible by 4 and not evenly divisible by 100.
161    *
162    * @param year the year to test where 0 <= year <= 99
163    * @param century the century to test where 0 <= century <= 99
164    * @return true if the given year represents a leap year
165    */
166   private final boolean isLeap(short century, short year) {
167     return isLeap(century * 100 + year);
168   }
169 
170   ////////////////////////// Setter methods////////////////////////////////////
171 
172   /**
173    * Set the negative field to true.
174    * 
175    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
176    *         "century+year is negative" field is not allowed.
177    */
178   public void setNegative() throws UnsupportedOperationException {
179     _isNegative = true;
180   }
181 
182   /**
183    * Set the century field. Note: year 0000 is not allowed.
184    * 
185    * @param century the value to set
186    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
187    *         century field is not allowed
188    */
189   public void setCentury(short century) throws UnsupportedOperationException {
190     String err = "";
191     if (century < 0) {
192       err = "century " + century + " must not be negative.";
193       throw new IllegalArgumentException(err);
194     } else if (_year == 0 && century == 0 && _century != 0) {
195       err = "century:  0000 is not an allowed year.";
196       throw new IllegalArgumentException(err);
197     }
198 
199     _century = century;
200   }
201 
202   /**
203    * Sets the Year field. Note: year 0000 is not allowed.
204    *
205    * @param year the year to set
206    * @throws UnsupportedOperationException in an overridden method in a derived class if that
207    *         derived class does not support the year element.
208    */
209   public void setYear(short year) throws UnsupportedOperationException {
210     String err = "";
211     if (year < 0) {
212       err = "year " + year + " must not be negative.";
213       throw new IllegalArgumentException(err);
214     } else if (year == -1) {
215       if (_century != -1) {
216         err = "year can not be omitted unless century is also omitted.";
217         throw new IllegalArgumentException(err);
218       }
219     } else if (year == 0 && _century == 0) {
220       err = "year:  0000 is not an allowed year";
221       throw new IllegalArgumentException(err);
222     } else if (year > 99) {
223       err = "year " + year + " is out of range:  0 <= year <= 99.";
224       throw new IllegalArgumentException(err);
225     }
226 
227     _year = year;
228   }
229 
230   /**
231    * Sets the Month Field. Note 1 <= month <= 12.
232    * 
233    * @param month the value to set up
234    * @throws UnsupportedOperationException in an overridden method in a derived class if that
235    *         derived class does not support the month element.
236    */
237   public void setMonth(short month) throws UnsupportedOperationException {
238     String err = "";
239     if (month == -1) {
240       if (_century != -1) {
241         err = "month cannot be omitted unless the previous component is also omitted.\n"
242             + "only higher level components can be omitted.";
243         throw new IllegalArgumentException(err);
244       }
245     } else if (month < 1 || month > 12) {
246       err = "month " + month + " is out of range:  1 <= month <= 12";
247       throw new IllegalArgumentException(err);
248     }
249 
250     _month = month;
251   }
252 
253   /**
254    * Sets the Day Field. Note: This field is validated before the assignment is done.
255    *
256    * @param day the value to set up
257    * @throws UnsupportedOperationException in an overridden method in a derived class if that
258    *         derived class does not support the day element.
259    */
260   public void setDay(short day) throws UnsupportedOperationException {
261     String err = "";
262     if (day == -1) {
263       if (_month != -1) {
264         err = "day cannot be omitted unless the previous component is also omitted.\n"
265             + "only higher level components can be omitted.";
266         throw new IllegalArgumentException(err);
267       }
268     } else if (day < 1) {
269       err = "day " + day + " cannot be negative.";
270       throw new IllegalArgumentException(err);
271     }
272 
273     short maxDay = maxDayInMonthFor(_century, _year, _month);
274     if (day > maxDay) {
275       if (_month != 2) {
276         err =
277             "day " + day + " is out of range for month " + _month + ":  " + "1 <= day <= " + maxDay;
278         throw new IllegalArgumentException(err);
279       } else if (isLeap(_century, _year)) {
280         err = "day " + day + " is out of range for February in a leap year:  " + "1 <= day <= 29";
281         throw new IllegalArgumentException(err);
282       } else {
283         err =
284             "day " + day + " is out of range for February in a non-leap year:  " + "1 <= day <= 28";
285         throw new IllegalArgumentException(err);
286       }
287     }
288 
289     _day = day;
290   }
291 
292   /**
293    * Sets the hour field for this date/time type.
294    *
295    * @param hour the hour to set
296    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
297    *         hour field is not allowed
298    */
299   public void setHour(short hour) throws UnsupportedOperationException {
300     if (hour > 23) {
301       String err = "hour " + hour + " must be strictly less than 24";
302       throw new IllegalArgumentException(err);
303     } else if (hour < 0) {
304       String err = "hour " + hour + " cannot be negative.";
305       throw new IllegalArgumentException(err);
306     }
307 
308     _hour = hour;
309   }
310 
311   /**
312    * set the minute field for this date/time type.
313    *
314    * @param minute the minute to set.
315    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
316    *         minute field is not allowed
317    */
318   public void setMinute(short minute) throws UnsupportedOperationException {
319     if (minute > 59) {
320       String err = "minute " + minute + " must be strictly less than 60.";
321       throw new IllegalArgumentException(err);
322     } else if (minute < 0) {
323       String err = "minute " + minute + " cannot be negative.";
324       throw new IllegalArgumentException(err);
325     }
326 
327     _minute = minute;
328   }
329 
330   /**
331    * Sets the seconds field for this date/time type, including fractional seconds. (In this
332    * implementation, fractional seconds are limited to milliseconds and are truncated at millseconds
333    * if more precision is provided.)
334    *
335    * @param second the second to set
336    * @param millsecond the millisecond to set
337    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
338    *         second field is not allowed
339    */
340   public void setSecond(short second, short millsecond) throws UnsupportedOperationException {
341     setSecond(second);
342     setMilliSecond(millsecond);
343   }
344 
345   /**
346    * Sets the seconds field for this date/time type, not including the fractional seconds. Any
347    * fractional seconds previously set is unmodified.
348    *
349    * @param second the second to set
350    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
351    *         second field is not allowed
352    */
353   public void setSecond(short second) throws UnsupportedOperationException {
354     if (second > 59) {
355       String err = "seconds " + second + " must be less than 60";
356       throw new IllegalArgumentException(err);
357     } else if (second < 0) {
358       String err = "seconds " + second + " cannot be negative.";
359       throw new IllegalArgumentException(err);
360     }
361 
362     _second = second;
363   }
364 
365   /**
366    * Sets the millisecond field for this date/time type.
367    *
368    * @param millisecond the millisecond to set
369    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
370    *         millisecond field is not allowed
371    */
372   public void setMilliSecond(short millisecond) throws UnsupportedOperationException {
373     if (millisecond < 0) {
374       String err = "milliseconds " + millisecond + " cannot be negative.";
375       throw new IllegalArgumentException(err);
376     } else if (millisecond > 999) {
377       String err = "milliseconds " + millisecond + " is out of bounds: 0 <= milliseconds <= 999.";
378       throw new IllegalArgumentException(err);
379     }
380 
381     _millsecond = millisecond;
382   }
383 
384   /**
385    * Sets the UTC field.
386    */
387   public void setUTC() {
388     _utc = true;
389   }
390 
391   /**
392    * Sets the time zone negative field to true.
393    *
394    * @param zoneNegative indicates whether or not the time zone is negative.
395    * @throws UnsupportedOperationException this exception is thrown when changing the time zone
396    *         fields is not allowed
397    */
398   public void setZoneNegative(boolean zoneNegative) {
399     _zoneNegative = zoneNegative;
400   }
401 
402   /**
403    * Sets the time zone fields for this date/time type. A call to this method means that the
404    * date/time type used is UTC.
405    * <p>
406    * For a negative time zone, you first assign the absolute value of the time zone using this
407    * method and then you call {@link #setZoneNegative(boolean)}.
408    *
409    * @param hour The time zone hour to set. Must be positive.
410    * @param minute The time zone minute to set.
411    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
412    *         time zone fields is not allowed
413    */
414   public void setZone(short hour, short minute) {
415     setZoneHour(hour);
416     setZoneMinute(minute);
417   }
418 
419   /**
420    * Sets the time zone hour field for this date/time type. A call to this method means that the
421    * date/time type used is UTC.
422    * <p>
423    * For a negative time zone, you first assign the absolute value of the time zone using this
424    * method and then you call {@link #setZoneNegative(boolean)}.
425    *
426    * @param hour the time zone hour to set. Must be positive.
427    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
428    *         time zone fields is not allowed
429    */
430   public void setZoneHour(short hour) {
431     if (hour > 23) {
432       String err = "time zone hour " + hour + " must be strictly less than 24";
433       throw new IllegalArgumentException(err);
434     } else if (hour < 0) {
435       String err = "time zone hour " + hour + " cannot be negative.";
436       throw new IllegalArgumentException(err);
437     }
438 
439     _zoneHour = hour;
440 
441     // Any call to setZone means that you use the date/time you use is UTC
442     setUTC();
443   }
444 
445   /**
446    * Sets the time zone minute field for this date/time type. A call to this method means that the
447    * date/time type used is UTC.
448    *
449    * @param minute the time zone minute to set
450    * @throws UnsupportedOperationException this exception is thrown when changing the value of the
451    *         time zone fields is not allowed
452    */
453   public void setZoneMinute(short minute) {
454     if (minute > 59) {
455       String err = "time zone minute " + minute + " must be strictly lower than 60";
456       throw new IllegalArgumentException(err);
457     } else if (minute < 0) {
458       String err = "time zone minute " + minute + " cannot be negative.";
459       throw new IllegalArgumentException(err);
460     }
461 
462     _zoneMinute = minute;
463 
464     // Any call to setZone means that you use the date/time you use is UTC
465     setUTC();
466   }
467 
468   //////////////////////// Getter methods//////////////////////////////////////
469 
470   public boolean isNegative() throws UnsupportedOperationException {
471     return _isNegative;
472   }
473 
474   public short getCentury() throws UnsupportedOperationException {
475     return _century;
476   }
477 
478   public short getYear() throws UnsupportedOperationException {
479     return _year;
480   }
481 
482   public short getMonth() throws UnsupportedOperationException {
483     return _month;
484   }
485 
486   public short getDay() throws UnsupportedOperationException {
487     return _day;
488   }
489 
490   public short getHour() throws UnsupportedOperationException {
491     return _hour;
492   }
493 
494   public short getMinute() throws UnsupportedOperationException {
495     return _minute;
496   }
497 
498   public short getSeconds() throws UnsupportedOperationException {
499     return _second;
500   }
501 
502   public short getMilli() throws UnsupportedOperationException {
503     return _millsecond;
504   }
505 
506   /**
507    * Returns true if this date/time type is UTC, that is, has a time zone assigned. A date/time type
508    * is UTC if a 'Z' appears at the end of the lexical representation type or if it contains a time
509    * zone.
510    *
511    * @return true if this type has a time zone assigned, else false.
512    */
513   public boolean isUTC() {
514     return _utc;
515   }
516 
517   public boolean isZoneNegative() {
518     return _zoneNegative;
519   }
520 
521   public short getZoneHour() {
522     return _zoneHour;
523   }
524 
525   public short getZoneMinute() {
526     return _zoneMinute;
527   }
528 
529   //////////////////////// Getter methods//////////////////////////////////////
530 
531   public boolean hasIsNegative() {
532     return true;
533   }
534 
535   public boolean hasCentury() {
536     return true;
537   }
538 
539   public boolean hasYear() {
540     return true;
541   }
542 
543   public boolean hasMonth() {
544     return true;
545   }
546 
547   public boolean hasDay() {
548     return true;
549   }
550 
551   public boolean hasHour() {
552     return true;
553   }
554 
555   public boolean hasMinute() {
556     return true;
557   }
558 
559   public boolean hasSeconds() {
560     return true;
561   }
562 
563   public boolean hasMilli() {
564     return true;
565   }
566 
567   ////////////////////////////////////////////////////////////////////////////////
568 
569   /**
570    * Adds a Duration to this Date/Time type as defined in
571    * <a href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes"> Adding Duration to
572    * dateTimes (W3C XML Schema, part 2 appendix E).</a> This version uses the algorithm defined in
573    * the document from W3C. A later version may optimize it.
574    * <p>
575    * The modified Date/Time instance will keep the same time zone it started with, if any.
576    * <p>
577    * Don't use getter methods but use direct field access for dateTime in order to have the
578    * behaviour defined in the Recommendation document.
579    *
580    * @param duration the duration to add
581    */
582   public void addDuration(Duration duration) {
583     int temp = 0;
584     int carry = 0;
585     int sign = (duration.isNegative()) ? -1 : 1;
586 
587     // First add the month and year.
588 
589     // Months
590     try {
591       temp = _month + sign * duration.getMonth();
592       carry = fQuotient(temp - 1, 12);
593       this.setMonth((short) (modulo(temp - 1, 12) + 1));
594     } catch (UnsupportedOperationException e) {
595       // Ignore
596     }
597 
598     // Years
599     try {
600       temp = _century * 100 + _year + sign * duration.getYear() + carry;
601       this.setCentury((short) (temp / 100));
602       this.setYear((short) (temp % 100));
603     } catch (UnsupportedOperationException e) {
604       // Ignore
605     }
606 
607     // Next, pin the day-of-month so it is not outside the new month.
608 
609     int tempDay = _day;
610     if (tempDay < 1) {
611       tempDay = 1;
612     } else {
613       int maxDay = maxDayInMonthFor(_century, _year, _month);
614       if (_day > maxDay) {
615         tempDay = maxDay;
616       }
617     }
618 
619     // Next, add the time components
620 
621     // Seconds
622     try {
623       temp = _millsecond + sign * (int) duration.getMilli();
624       carry = fQuotient(temp, 1000);
625       this.setMilliSecond((short) modulo(temp, 1000));
626 
627       temp = _second + sign * duration.getSeconds() + carry;
628       carry = fQuotient(temp, 60);
629       this.setSecond((short) modulo(temp, 60));
630     } catch (UnsupportedOperationException e) {
631       // Ignore
632     }
633 
634     // Minutes
635     try {
636       temp = _minute + sign * duration.getMinute() + carry;
637       carry = fQuotient(temp, 60);
638       this.setMinute((short) modulo(temp, 60));
639     } catch (UnsupportedOperationException e) {
640       // Ignore
641     }
642 
643     // Hours
644     try {
645       temp = _hour + sign * duration.getHour() + carry;
646       carry = fQuotient(temp, 24);
647       this.setHour((short) modulo(temp, 24));
648     } catch (UnsupportedOperationException e) {
649       // Ignore
650     }
651 
652     // Finally, set the day-of-month, rolling the month & year as needed
653 
654     // Days
655     try {
656       tempDay += sign * duration.getDay() + carry;
657 
658       // Loop until the day-of-month is within bounds
659       while (true) {
660         short maxDay = maxDayInMonthFor(_century, _year, _month);
661         if (tempDay < 1) {
662           tempDay = (short) (tempDay + maxDayInMonthFor(_century, _year, _month - 1));
663           carry = -1;
664         } else if (tempDay > maxDay) {
665           tempDay = (short) (tempDay - maxDay);
666           carry = 1;
667         } else {
668           break;
669         }
670 
671         try {
672           temp = _month + carry;
673           this.setMonth((short) (modulo(temp - 1, 12) + 1));
674           temp = this.getCentury() * 100 + this.getYear() + fQuotient(temp - 1, 12);
675           this.setCentury((short) (temp / 100));
676           this.setYear((short) (temp % 100));
677         } catch (UnsupportedOperationException e) {
678           // Ignore
679         }
680       }
681 
682       this.setDay((short) tempDay);
683     } catch (UnsupportedOperationException e) {
684       // Ignore
685     }
686   } // addDuration
687 
688   /////////////////////// W3C XML SCHEMA Helpers///////////////////////////////
689 
690   /**
691    * Helper function defined in W3C XML Schema Recommendation part 2.
692    * 
693    * @see <a href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes"> W3C XML Schema
694    *      Recommendation part 2</a>
695    */
696   private int fQuotient(int a, int b) {
697     return (int) Math.floor((float) a / (float) b);
698   }
699 
700   /**
701    * Helper function defined in W3C XML Schema Recommendation part 2.
702    * 
703    * @see <a href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes"> W3C XML Schema
704    *      Recommendation part 2</a>
705    */
706   private int modulo(int a, int b) {
707     return a - fQuotient(a, b) * b;
708   }
709 
710   /**
711    * Returns the maximum day in the given month of the given year.
712    *
713    * @param year
714    * @param month
715    * @return the maximum day in the given month of the given year.
716    */
717   private final short maxDayInMonthFor(short century, short year, int month) {
718     if (month == 4 || month == 6 || month == 9 || month == 11) {
719       return 30;
720     } else if (month == 2) {
721       return (short) ((isLeap(century, year)) ? 29 : 28);
722     } else {
723       return 31;
724     }
725   }
726 
727   ////////////////////////////////////////////////////////////////////////////
728 
729   /**
730    * Normalizes a date/time datatype as defined in W3C XML Schema Recommendation document: if a
731    * timeZone is present but it is not Z then we convert the date/time datatype to Z using the
732    * addition operation defined in
733    * <a href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes"> Adding Duration to
734    * dateTimes (W3C XML Schema, part 2 appendix E).</a>
735    *
736    * @see #addDuration
737    */
738   public void normalize() {
739     if (!isUTC() || (_zoneHour == 0 && _zoneMinute == 0)) {
740       return;
741     }
742 
743     Duration temp = new Duration();
744     temp.setHour(_zoneHour);
745     temp.setMinute(_zoneMinute);
746     if (!isZoneNegative()) {
747       temp.setNegative();
748     }
749 
750     this.addDuration(temp);
751 
752     // reset the zone
753     this.setZone((short) 0, (short) 0);
754     this.setZoneNegative(false);
755   }
756 
757   /**
758    * Compares two date/time data types. The algorithm of comparison is defined in
759    * <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">W3C XML Schema Recommendation (section
760    * 3.2.7.3)</a>
761    * <p>
762    * The returned value will be one of:
763    * <ul>
764    * <li>INDETERMINATE (-1): this ? dateTime</li>
765    * <li>LESS_THAN (0): this < dateTime</li>
766    * <li>EQUALS (1): this == dateTime</li>
767    * <li>GREATER_THAN (2): this > dateTime</li>
768    * </ul>
769    * <p>
770    * FIXME: This code does not compare time zones properly for date/time types that do not contain a
771    * time.
772    *
773    * @param dateTime the dateTime to compare with the current instance.
774    * @return the status of the comparison.
775    */
776   public int compareTo(DateTimeBase dateTime) {
777     if (dateTime == null) {
778       throw new IllegalArgumentException(
779           "a Date/Time datatype cannot be compared with a null value");
780     }
781 
782     // Make copies of the date/times we compare so we DO NOT MODIFY THE CURRENT VALUES!
783     DateTimeBase tempDate1;
784     DateTimeBase tempDate2;
785 
786     try {
787       tempDate1 = clone(this);
788       if (tempDate1.isUTC()) {
789         tempDate1.normalize();
790       }
791 
792       tempDate2 = clone(dateTime);
793       if (tempDate2.isUTC()) {
794         tempDate2.normalize();
795       }
796       // } catch (InstantiationException e) {
797       // // This is a Castor coding error if this occurs -- it should never occur
798       // throw new RuntimeException(e);
799       // } catch (IllegalAccessException e) {
800       // // This is a Castor coding error if this occurs -- it should never occur
801       // throw new RuntimeException(e);
802     } catch (CloneNotSupportedException e) {
803       // This is a Castor coding error if this occurs -- it should never occur
804       throw new RuntimeException("Unexpected 'clone not supported' Exception");
805     }
806 
807     // If both date/time types are in Z-form (or both not), we just compare the fields.
808     if (tempDate1.isUTC() == tempDate2.isUTC()) {
809       return compareFields(tempDate1, tempDate2);
810     }
811 
812     // If datetime1 has a time zone and datetime2 does not
813     if (tempDate1.isUTC()) {
814       tempDate2.setZone((short) MAX_TIME_ZONE_COMPARISON_OFFSET, (short) 0);
815       tempDate2.normalize();
816       int result = compareFields(tempDate1, tempDate2);
817       if (result == LESS_THAN) {
818         return result;
819       }
820 
821       // Restore time from previous offsetting
822       tempDate2.setZone((short) MAX_TIME_ZONE_COMPARISON_OFFSET, (short) 0);
823       tempDate2.setZoneNegative(true);
824       tempDate2.normalize();
825 
826       tempDate2.setZone((short) MAX_TIME_ZONE_COMPARISON_OFFSET, (short) 0);
827       tempDate2.setZoneNegative(true);
828       tempDate2.normalize();
829       result = compareFields(tempDate1, tempDate2);
830       if (result == GREATER_THAN) {
831         return result;
832       }
833       return INDETERMINATE;
834     }
835 
836     // If datetime2 has a time zone and datetime1 does not
837     if (tempDate2.isUTC()) {
838       tempDate1.setZone((short) MAX_TIME_ZONE_COMPARISON_OFFSET, (short) 0);
839       tempDate1.normalize();
840       int result = compareFields(tempDate1, tempDate2);
841       if (result == GREATER_THAN) {
842         return result;
843       }
844 
845       // Restore time from previous offsetting
846       tempDate1.setZone((short) MAX_TIME_ZONE_COMPARISON_OFFSET, (short) 0);
847       tempDate1.setZoneNegative(true);
848       tempDate1.normalize();
849 
850       tempDate1.setZone((short) MAX_TIME_ZONE_COMPARISON_OFFSET, (short) 0);
851       tempDate1.setZoneNegative(true);
852       tempDate1.normalize();
853       result = compareFields(tempDate1, tempDate2);
854       if (result == LESS_THAN) {
855         return result;
856       }
857       return INDETERMINATE;
858     }
859 
860     return INDETERMINATE;
861   }
862 
863   /**
864    * Copies a dateTime instance of some type -- all fields may or may not be present. Always copy
865    * all fields, even if they are 0, and create a full DateTime with all fields. This allows quick
866    * comparisons (no exceptions thrown, which are expensive).
867    */
868   private DateTimeBase copyDateTimeInstance(DateTimeBase dateTime) {
869     DateTimeBase newDateTime = new DateTime();
870     newDateTime._isNegative = dateTime._isNegative;
871     newDateTime._century = dateTime._century;
872     newDateTime._year = dateTime._year;
873     newDateTime._month = dateTime._month;
874     newDateTime._day = dateTime._day;
875     newDateTime._hour = dateTime._hour;
876     newDateTime._minute = dateTime._minute;
877     newDateTime._second = dateTime._second;
878     newDateTime._millsecond = dateTime._millsecond;
879     newDateTime._zoneNegative = dateTime._zoneNegative;
880     newDateTime._utc = dateTime._utc;
881     newDateTime._zoneHour = dateTime._zoneHour;
882     newDateTime._zoneMinute = dateTime._zoneMinute;
883     return newDateTime;
884   }
885 
886   public DateTimeBase clone(DateTimeBase dateTime) throws CloneNotSupportedException {
887     DateTimeBase newDateTime = (DateTimeBase) super.clone();
888     // newDateTime = (DateTimeBase) dateTime.getClass().newInstance();
889     newDateTime.setValues(dateTime.getValues());
890     if (dateTime.hasIsNegative() && dateTime.isNegative()) {
891       newDateTime.setNegative();
892     }
893     if (dateTime.isUTC()) {
894       newDateTime.setUTC();
895       newDateTime.setZone(dateTime.getZoneHour(), dateTime.getZoneMinute());
896       newDateTime.setZoneNegative(dateTime.isZoneNegative());
897     }
898     return newDateTime;
899   }
900 
901   private static int compareFields(DateTimeBase date1, DateTimeBase date2) {
902     short field1;
903     short field2;
904 
905     if (date1.hasCentury() != date2.hasCentury()) {
906       return INDETERMINATE;
907     }
908 
909     if (date1.hasCentury() && date2.hasCentury()) {
910       field1 = date1.getCentury();
911       field2 = date2.getCentury();
912       if (field1 < field2) {
913         return LESS_THAN;
914       } else if (field1 > field2) {
915         return GREATER_THAN;
916       }
917     }
918 
919     if (date1.hasYear() != date2.hasYear()) {
920       return INDETERMINATE;
921     }
922 
923     if (date1.hasYear() && date2.hasYear()) {
924       field1 = date1.getYear();
925       field2 = date2.getYear();
926       if (field1 < field2) {
927         return LESS_THAN;
928       } else if (field1 > field2) {
929         return GREATER_THAN;
930       }
931     }
932 
933     if (date1.hasMonth() != date2.hasMonth()) {
934       return INDETERMINATE;
935     }
936 
937     if (date1.hasMonth() && date2.hasMonth()) {
938       field1 = date1.getMonth();
939       field2 = date2.getMonth();
940       if (field1 < field2) {
941         return LESS_THAN;
942       } else if (field1 > field2) {
943         return GREATER_THAN;
944       }
945     }
946 
947     if (date1.hasDay() != date2.hasDay()) {
948       return INDETERMINATE;
949     }
950 
951     if (date1.hasDay() && date2.hasDay()) {
952       field1 = date1.getDay();
953       field2 = date2.getDay();
954       if (field1 < field2) {
955         return LESS_THAN;
956       } else if (field1 > field2) {
957         return GREATER_THAN;
958       }
959     }
960 
961     if (date1.hasHour() != date2.hasHour()) {
962       return INDETERMINATE;
963     }
964 
965     if (date1.hasHour() && date2.hasHour()) {
966       field1 = date1.getHour();
967       field2 = date2.getHour();
968       if (field1 < field2) {
969         return LESS_THAN;
970       } else if (field1 > field2) {
971         return GREATER_THAN;
972       }
973     }
974 
975     if (date1.hasMinute() != date2.hasMinute()) {
976       return INDETERMINATE;
977     }
978 
979     if (date1.hasMinute() && date2.hasMinute()) {
980       field1 = date1.getMinute();
981       field2 = date2.getMinute();
982       if (field1 < field2) {
983         return LESS_THAN;
984       } else if (field1 > field2) {
985         return GREATER_THAN;
986       }
987     }
988 
989     if (date1.hasSeconds() != date2.hasSeconds()) {
990       return INDETERMINATE;
991     }
992 
993     if (date1.hasSeconds() && date2.hasSeconds()) {
994       field1 = date1.getSeconds();
995       field2 = date2.getSeconds();
996       if (field1 < field2) {
997         return LESS_THAN;
998       } else if (field1 > field2) {
999         return GREATER_THAN;
1000       }
1001     }
1002 
1003     if (date1.hasMilli() != date2.hasMilli()) {
1004       return INDETERMINATE;
1005     }
1006 
1007     if (date1.hasMilli() && date2.hasMilli()) {
1008       field1 = date1.getMilli();
1009       field2 = date2.getMilli();
1010       if (field1 < field2) {
1011         return LESS_THAN;
1012       } else if (field1 > field2) {
1013         return GREATER_THAN;
1014       }
1015     }
1016 
1017     return EQUALS;
1018   }
1019 
1020   /**
1021    * {@inheritDoc} Overrides the java.lang.Object#hashcode method.
1022    */
1023   public int hashCode() {
1024     return _year ^ _month ^ _day ^ _hour ^ _minute ^ _second ^ _millsecond ^ _zoneHour
1025         ^ _zoneMinute;
1026   }
1027 
1028   /**
1029    * {@inheritDoc} Overrides the java.lang.Object#equals method.
1030    * 
1031    * @see #equals(Object)
1032    */
1033   public boolean equals(Object object) {
1034     // No need to check if we are comparing two instances of the same class.
1035     // (if the class is not the same then #equal will return false).
1036     if (object instanceof DateTimeBase) {
1037       return equal((DateTimeBase) object);
1038     }
1039     return false;
1040   }
1041 
1042   /**
1043    * Returns true if the present instance of date/time type is equal to the parameter.
1044    * <p>
1045    * The equals relation is as defined in the W3C XML Schema Recommendation, part2.
1046    *
1047    * @param dateTime the date/time type to compare with the present instance
1048    * @return true if the present instance is equal to the parameter false if not
1049    */
1050   protected boolean equal(DateTimeBase dateTime) {
1051     return EQUALS == this.compareTo(dateTime);
1052   } // equals
1053 
1054   /**
1055    * converts this Date/Time into a local java Calendar.
1056    * 
1057    * @return a local calendar representing this Date or Time
1058    */
1059   public Calendar toCalendar() {
1060     Calendar result = new GregorianCalendar();
1061     result.setTime(toDate());
1062     return result;
1063   } // toCalendar()
1064 
1065   ////////////// COMMON CODE USED BY EXTENDING CLASSES ///////////////////////
1066 
1067   protected static int parseYear(final String str, final DateTimeBase result, final char[] chars,
1068       final int index, final String complaint) throws ParseException {
1069     int idx = index;
1070 
1071     if (chars[idx] == '-') {
1072       idx++;
1073       result.setNegative();
1074     }
1075 
1076     if (str.length() < idx + 4 || !Character.isDigit(chars[idx])
1077         || !Character.isDigit(chars[idx + 1]) || !Character.isDigit(chars[idx + 2])
1078         || !Character.isDigit(chars[idx + 3])) {
1079       throw new ParseException(complaint + str + "\nThe Year must be 4 digits long", idx);
1080     }
1081 
1082     short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx + 1] - '0'));
1083     short value2 = (short) ((chars[idx + 2] - '0') * 10 + (chars[idx + 3] - '0'));
1084 
1085     if (value1 == 0 && value2 == 0) {
1086       throw new ParseException(complaint + str + "\n'0000' is not allowed as a year.", idx);
1087     }
1088 
1089     result.setCentury(value1);
1090     result.setYear(value2);
1091 
1092     idx += 4;
1093 
1094     return idx;
1095   }
1096 
1097   protected static int parseMonth(final String str, final DateTimeBase result, final char[] chars,
1098       final int index, final String complaint) throws ParseException {
1099     int idx = index;
1100 
1101     if (chars[idx] != '-') {
1102       throw new ParseException(complaint + str + "\n '-' " + DateTimeBase.WRONGLY_PLACED, idx);
1103     }
1104 
1105     idx++;
1106 
1107     if (str.length() < idx + 2 || !Character.isDigit(chars[idx])
1108         || !Character.isDigit(chars[idx + 1])) {
1109       throw new ParseException(complaint + str + "\nThe Month must be 2 digits long", idx);
1110     }
1111 
1112     short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx + 1] - '0'));
1113     result.setMonth(value1);
1114 
1115     idx += 2;
1116     return idx;
1117   }
1118 
1119   protected static int parseDay(final String str, final DateTimeBase result, final char[] chars,
1120       final int index, final String complaint) throws ParseException {
1121     int idx = index;
1122 
1123     if (chars[idx] != '-') {
1124       throw new ParseException(complaint + str + "\n '-' " + DateTimeBase.WRONGLY_PLACED, idx);
1125     }
1126 
1127     idx++;
1128 
1129     if (str.length() < idx + 2 || !Character.isDigit(chars[idx])
1130         || !Character.isDigit(chars[idx + 1])) {
1131       throw new ParseException(complaint + str + "\nThe Day must be 2 digits long", idx);
1132     }
1133 
1134     short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx + 1] - '0'));
1135     result.setDay(value1);
1136 
1137     idx += 2;
1138     return idx;
1139   }
1140 
1141   protected static int parseTime(final String str, final DateTimeBase result, final char[] chars,
1142       final int index, final String complaint) throws ParseException {
1143     int idx = index;
1144 
1145     if (str.length() < idx + 8) {
1146       throw new ParseException(
1147           complaint + str + "\nA Time field must be at least 8 characters long", idx);
1148     }
1149 
1150     if (!Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
1151       throw new ParseException(complaint + str + "\nThe Hour must be 2 digits long", idx);
1152     }
1153 
1154     short hour;
1155 
1156     hour = (short) ((chars[idx] - '0') * 10 + (chars[idx + 1] - '0'));
1157 
1158     boolean isHourOfValue24 = false;
1159     if (hour == 24) {
1160       result.setHour((short) 0);
1161       Duration oneDay = new Duration();
1162       oneDay.setDay((short) 1);
1163       result.addDuration(oneDay);
1164       isHourOfValue24 = true;
1165     } else {
1166       result.setHour(hour);
1167     }
1168 
1169     idx += 2;
1170 
1171     // Minutes
1172     if (chars[idx] != ':') {
1173       throw new ParseException(complaint + str + "\n ':#1' " + DateTimeBase.WRONGLY_PLACED, idx);
1174     }
1175 
1176     idx++;
1177 
1178     if (!Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
1179       throw new ParseException(complaint + str + "\nThe Minute must be 2 digits long", idx);
1180     }
1181 
1182     short minutes = (short) ((chars[idx] - '0') * 10 + (chars[idx + 1] - '0'));
1183 
1184     if (isHourOfValue24 && minutes != 0) {
1185       throw new ParseException(
1186           complaint + str + "\nWhen an hour of 24 is used, minutes must be strictly of value 00.",
1187           idx);
1188     }
1189     result.setMinute(minutes);
1190 
1191     idx += 2;
1192 
1193     // Seconds
1194     if (chars[idx] != ':') {
1195       throw new ParseException(complaint + str + "\n ':#2' " + DateTimeBase.WRONGLY_PLACED, idx);
1196     }
1197 
1198     idx++;
1199 
1200     if (!Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
1201       throw new ParseException(complaint + str + "\nThe Second must be 2 digits long", idx);
1202     }
1203 
1204     short seconds = (short) ((chars[idx] - '0') * 10 + (chars[idx + 1] - '0'));
1205     if (isHourOfValue24 && seconds != 0) {
1206       throw new ParseException(
1207           complaint + str + "\nWhen an hour of 24 is used, seconds must be strictly of value 00.",
1208           idx);
1209     }
1210     result.setSecond(seconds);
1211 
1212     idx += 2;
1213 
1214     if (idx < chars.length && chars[idx] == '.') {
1215       idx++;
1216 
1217       long decimalValue = 0;
1218       long powerOfTen = 1;
1219       while (idx < chars.length && Character.isDigit(chars[idx])) {
1220         decimalValue = decimalValue * 10 + (chars[idx] - '0');
1221         powerOfTen *= 10;
1222         idx++;
1223       }
1224 
1225       // Explicitly truncate to milliseconds if more digits were provided
1226       if (powerOfTen > 1000) {
1227         decimalValue /= (powerOfTen / 1000);
1228         powerOfTen = 1000;
1229       } else if (powerOfTen < 1000) {
1230         decimalValue *= (1000 / powerOfTen);
1231         powerOfTen = 1000;
1232       }
1233       result.setMilliSecond((short) decimalValue);
1234     }
1235 
1236     return idx;
1237   }
1238 
1239   protected static int parseTimeZone(final String str, final DateTimeBase result,
1240       final char[] chars, final int index, final String complaint) throws ParseException {
1241     // If we're at the end of the string, there's no time zone to parse
1242     if (index >= chars.length) {
1243       return index;
1244     }
1245 
1246     int idx = index;
1247 
1248     if (chars[idx] == 'Z') {
1249       result.setUTC();
1250       return ++idx;
1251     }
1252 
1253     if (chars[idx] == '+' || chars[idx] == '-') {
1254       if (chars[idx] == '-') {
1255         result.setZoneNegative(true);
1256       }
1257       idx++;
1258       if (idx + 5 > chars.length || chars[idx + 2] != ':' || !Character.isDigit(chars[idx])
1259           || !Character.isDigit(chars[idx + 1]) || !Character.isDigit(chars[idx + 3])
1260           || !Character.isDigit(chars[idx + 4])) {
1261         throw new ParseException(complaint + str + "\nTimeZone must have the format (+/-)hh:mm",
1262             idx);
1263       }
1264       short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx + 1] - '0'));
1265       short value2 = (short) ((chars[idx + 3] - '0') * 10 + (chars[idx + 4] - '0'));
1266       result.setZone(value1, value2);
1267       idx += 5;
1268     }
1269 
1270     return idx;
1271   }
1272 
1273   /**
1274    * Sets the time zone in the provided DateFormat.
1275    * 
1276    * @param df
1277    */
1278   protected void setDateFormatTimeZone(DateFormat df) {
1279     // If no time zone, nothing to do
1280     if (!isUTC()) {
1281       return;
1282     }
1283 
1284     int offset = (this.getZoneMinute() + this.getZoneHour() * 60) * 60 * 1000;
1285     offset = isZoneNegative() ? -offset : offset;
1286 
1287     SimpleTimeZone timeZone = new SimpleTimeZone(0, "UTC");
1288     timeZone.setRawOffset(offset);
1289     timeZone.setID(TimeZone.getAvailableIDs(offset)[0]);
1290     df.setTimeZone(timeZone);
1291   }
1292 
1293   /**
1294    * Sets the time zone in the provided Calendar.
1295    * 
1296    * @param calendar
1297    */
1298   protected void setDateFormatTimeZone(Calendar calendar) {
1299     // If no time zone, nothing to do
1300     if (!isUTC()) {
1301       return;
1302     }
1303 
1304     int offset = (this.getZoneMinute() + this.getZoneHour() * 60) * 60 * 1000;
1305     offset = isZoneNegative() ? -offset : offset;
1306 
1307     SimpleTimeZone timeZone = new SimpleTimeZone(0, "UTC");
1308     timeZone.setRawOffset(offset);
1309     String[] availableIDs = TimeZone.getAvailableIDs(offset);
1310     if (availableIDs != null && availableIDs.length > 0) {
1311       timeZone.setID(availableIDs[0]);
1312     }
1313     calendar.setTimeZone(timeZone);
1314   }
1315 
1316   protected void appendDateString(StringBuffer result) {
1317     if (isNegative()) {
1318       result.append('-');
1319     }
1320 
1321     if ((this.getCentury() / 10) == 0) {
1322       result.append(0);
1323     }
1324     result.append(this.getCentury());
1325 
1326     if ((this.getYear() / 10) == 0) {
1327       result.append(0);
1328     }
1329     result.append(this.getYear());
1330 
1331     result.append('-');
1332     if ((this.getMonth() / 10) == 0) {
1333       result.append(0);
1334     }
1335     result.append(this.getMonth());
1336 
1337     result.append('-');
1338     if ((this.getDay() / 10) == 0) {
1339       result.append(0);
1340     }
1341     result.append(this.getDay());
1342   }
1343 
1344   protected void appendTimeString(StringBuffer result) {
1345     if ((this.getHour() / 10) == 0) {
1346       result.append(0);
1347     }
1348     result.append(this.getHour());
1349 
1350     result.append(':');
1351 
1352     if ((this.getMinute() / 10) == 0) {
1353       result.append(0);
1354     }
1355     result.append(this.getMinute());
1356 
1357     result.append(':');
1358 
1359     if ((this.getSeconds() / 10) == 0) {
1360       result.append(0);
1361     }
1362     result.append(this.getSeconds());
1363 
1364     if (this.getMilli() != 0) {
1365       result.append('.');
1366       if (this.getMilli() < 100) {
1367         result.append('0');
1368         if (this.getMilli() < 10) {
1369           result.append('0');
1370         }
1371       }
1372       result.append(this.getMilli());
1373     }
1374   }
1375 
1376   protected void appendTimeZoneString(StringBuffer result) {
1377     if (!isUTC()) {
1378       return;
1379     }
1380 
1381     // By default we append a 'Z' to indicate UTC
1382     if (this.getZoneHour() == 0 && this.getZoneMinute() == 0) {
1383       result.append('Z');
1384       return;
1385     }
1386 
1387     if (isZoneNegative()) {
1388       result.append('-');
1389     } else {
1390       result.append('+');
1391     }
1392 
1393     if ((this.getZoneHour() / 10) == 0) {
1394       result.append(0);
1395     }
1396     result.append(this.getZoneHour());
1397 
1398     result.append(':');
1399     if ((this.getZoneMinute() / 10) == 0) {
1400       result.append(0);
1401     }
1402     result.append(this.getZoneMinute());
1403   }
1404 
1405 } // -- DateTimeBase