001package org.hl7.fhir.r4.model;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033import static org.apache.commons.lang3.StringUtils.isBlank;
034
035import java.util.Calendar;
036import java.util.Date;
037import java.util.GregorianCalendar;
038import java.util.Map;
039import java.util.TimeZone;
040import java.util.concurrent.ConcurrentHashMap;
041
042import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.commons.lang3.Validate;
045import org.apache.commons.lang3.time.DateUtils;
046
047import ca.uhn.fhir.parser.DataFormatException;
048
049import org.hl7.fhir.utilities.DateTimeUtil;
050import org.hl7.fhir.utilities.Utilities;
051
052import javax.annotation.Nullable;
053
054public abstract class BaseDateTimeType extends PrimitiveType<Date> {
055
056        static final long NANOS_PER_MILLIS = 1000000L;
057
058        static final long NANOS_PER_SECOND = 1000000000L;
059  private static final Map<String, TimeZone> timezoneCache = new ConcurrentHashMap<>();
060        private static final long serialVersionUID = 1L;
061
062        private String myFractionalSeconds;
063        private TemporalPrecisionEnum myPrecision = null;
064        private TimeZone myTimeZone;
065        private boolean myTimeZoneZulu = false;
066
067        /**
068         * Constructor
069         */
070        public BaseDateTimeType() {
071                // nothing
072        }
073
074        /**
075         * Constructor
076         *
077         * @throws IllegalArgumentException
078         *            If the specified precision is not allowed for this type
079         */
080        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision) {
081                setValue(theDate, thePrecision);
082    validatePrecisionAndThrowIllegalArgumentException();
083        }
084
085        /**
086         * Constructor
087         */
088        public BaseDateTimeType(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
089                this(theDate, thePrecision);
090                setTimeZone(theTimeZone);
091    validatePrecisionAndThrowIllegalArgumentException();
092        }
093
094        /**
095         * Constructor
096         *
097         * @throws IllegalArgumentException
098         *            If the specified precision is not allowed for this type
099         */
100        public BaseDateTimeType(String theString) {
101                setValueAsString(theString);
102    validatePrecisionAndThrowIllegalArgumentException();
103        }
104
105  private void validatePrecisionAndThrowIllegalArgumentException() {
106    if (!isPrecisionAllowed(getPrecision())) {
107      throw new IllegalArgumentException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + getValueAsString());
108    }
109  }
110
111  /**
112         * Adds the given amount to the field specified by theField
113         *
114         * @param theField
115         *           The field, uses constants from {@link Calendar} such as {@link Calendar#YEAR}
116         * @param theValue
117         *           The number to add (or subtract for a negative number)
118         */
119        public void add(int theField, int theValue) {
120                switch (theField) {
121                case Calendar.YEAR:
122                        setValue(DateUtils.addYears(getValue(), theValue), getPrecision());
123                        break;
124                case Calendar.MONTH:
125                        setValue(DateUtils.addMonths(getValue(), theValue), getPrecision());
126                        break;
127                case Calendar.DATE:
128                        setValue(DateUtils.addDays(getValue(), theValue), getPrecision());
129                        break;
130                case Calendar.HOUR:
131                        setValue(DateUtils.addHours(getValue(), theValue), getPrecision());
132                        break;
133                case Calendar.MINUTE:
134                        setValue(DateUtils.addMinutes(getValue(), theValue), getPrecision());
135                        break;
136                case Calendar.SECOND:
137                        setValue(DateUtils.addSeconds(getValue(), theValue), getPrecision());
138                        break;
139                case Calendar.MILLISECOND:
140                        setValue(DateUtils.addMilliseconds(getValue(), theValue), getPrecision());
141                        break;
142                default:
143                        throw new DataFormatException("Unknown field constant: " + theField);
144                }
145        }
146
147        /**
148         * Returns <code>true</code> if the given object represents a date/time before <code>this</code> object
149         *
150         * @throws NullPointerException
151         *            If <code>this.getValue()</code> or <code>theDateTimeType.getValue()</code>
152         *            return <code>null</code>
153         */
154        public boolean after(DateTimeType theDateTimeType) {
155                validateBeforeOrAfter(theDateTimeType);
156                return getValue().after(theDateTimeType.getValue());
157        }
158
159        /**
160         * Returns <code>true</code> if the given object represents a date/time before <code>this</code> object
161         *
162         * @throws NullPointerException
163         *            If <code>this.getValue()</code> or <code>theDateTimeType.getValue()</code>
164         *            return <code>null</code>
165         */
166        public boolean before(DateTimeType theDateTimeType) {
167                validateBeforeOrAfter(theDateTimeType);
168                return getValue().before(theDateTimeType.getValue());
169        }
170
171        private void clearTimeZone() {
172                myTimeZone = null;
173                myTimeZoneZulu = false;
174        }
175
176  /**
177   * @param thePrecision
178   * @return the String value of this instance with the specified precision.
179   */
180  public String getValueAsString(TemporalPrecisionEnum thePrecision) {
181    return encode(getValue(), thePrecision);
182  }
183
184        @Override
185        protected String encode(Date theValue) {
186    return encode(theValue, myPrecision);
187  }
188
189  @Nullable
190  private String encode(Date theValue, TemporalPrecisionEnum thePrecision) {
191    if (theValue == null) {
192      return null;
193    } else {
194      GregorianCalendar cal;
195      if (myTimeZoneZulu) {
196        cal = new GregorianCalendar(getTimeZone("GMT"));
197      } else if (myTimeZone != null) {
198        cal = new GregorianCalendar(myTimeZone);
199      } else {
200        cal = new GregorianCalendar();
201      }
202      cal.setTime(theValue);
203
204      StringBuilder b = new StringBuilder();
205      leftPadWithZeros(cal.get(Calendar.YEAR), 4, b);
206
207if (thePrecision.ordinal() > TemporalPrecisionEnum.YEAR.ordinal()) {
208        b.append('-');
209        leftPadWithZeros(cal.get(Calendar.MONTH) + 1, 2, b);
210        if (thePrecision.ordinal() > TemporalPrecisionEnum.MONTH.ordinal()) {
211          b.append('-');
212          leftPadWithZeros(cal.get(Calendar.DATE), 2, b);
213          if (thePrecision.ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
214            b.append('T');
215            leftPadWithZeros(cal.get(Calendar.HOUR_OF_DAY), 2, b);
216            b.append(':');
217            leftPadWithZeros(cal.get(Calendar.MINUTE), 2, b);
218            if (thePrecision.ordinal() > TemporalPrecisionEnum.MINUTE.ordinal()) {
219              b.append(':');
220              leftPadWithZeros(cal.get(Calendar.SECOND), 2, b);
221              if (thePrecision.ordinal() > TemporalPrecisionEnum.SECOND.ordinal()) {
222                b.append('.');
223                b.append(myFractionalSeconds);
224                for (int i = myFractionalSeconds.length(); i < 3; i++) {
225                  b.append('0');
226                }
227              }
228            }
229
230            if (myTimeZoneZulu) {
231              b.append('Z');
232            } else if (myTimeZone != null) {
233              int offset = myTimeZone.getOffset(theValue.getTime());
234              if (offset >= 0) {
235                b.append('+');
236              } else {
237                b.append('-');
238                offset = Math.abs(offset);
239              }
240
241              int hoursOffset = (int) (offset / DateUtils.MILLIS_PER_HOUR);
242              leftPadWithZeros(hoursOffset, 2, b);
243              b.append(':');
244              int minutesOffset = (int) (offset % DateUtils.MILLIS_PER_HOUR);
245              minutesOffset = (int) (minutesOffset / DateUtils.MILLIS_PER_MINUTE);
246              leftPadWithZeros(minutesOffset, 2, b);
247            }
248          }
249        }
250      }
251      return b.toString();
252    }
253  }
254
255  /**
256         * Returns the month with 1-index, e.g. 1=the first day of the month
257         */
258        public Integer getDay() {
259                return getFieldValue(Calendar.DAY_OF_MONTH);
260        }
261
262        /**
263         * Returns the default precision for the given datatype
264         */
265        protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype();
266
267        private Integer getFieldValue(int theField) {
268                if (getValue() == null) {
269                        return null;
270                }
271                Calendar cal = getValueAsCalendar();
272                return cal.get(theField);
273        }
274
275        /**
276         * Returns the hour of the day in a 24h clock, e.g. 13=1pm
277         */
278        public Integer getHour() {
279                return getFieldValue(Calendar.HOUR_OF_DAY);
280        }
281
282        /**
283         * Returns the milliseconds within the current second.
284         * <p>
285         * Note that this method returns the
286         * same value as {@link #getNanos()} but with less precision.
287         * </p>
288         */
289        public Integer getMillis() {
290                return getFieldValue(Calendar.MILLISECOND);
291        }
292
293        /**
294         * Returns the minute of the hour in the range 0-59
295         */
296        public Integer getMinute() {
297                return getFieldValue(Calendar.MINUTE);
298        }
299
300        /**
301         * Returns the month with 0-index, e.g. 0=January
302         */
303        public Integer getMonth() {
304                return getFieldValue(Calendar.MONTH);
305        }
306
307  public float getSecondsMilli() {
308    int sec = getSecond();
309    int milli = getMillis();
310    String s = Integer.toString(sec)+"."+Utilities.padLeft(Integer.toString(milli), '0', 3);
311    return Float.parseFloat(s);
312  }
313        
314        /**
315         * Returns the nanoseconds within the current second
316         * <p>
317         * Note that this method returns the
318         * same value as {@link #getMillis()} but with more precision.
319         * </p>
320         */
321        public Long getNanos() {
322                if (isBlank(myFractionalSeconds)) {
323                        return null;
324                }
325                String retVal = StringUtils.rightPad(myFractionalSeconds, 9, '0');
326                retVal = retVal.substring(0, 9);
327                return Long.parseLong(retVal);
328        }
329
330        private int getOffsetIndex(String theValueString) {
331                int plusIndex = theValueString.indexOf('+', 16);
332                int minusIndex = theValueString.indexOf('-', 16);
333                int zIndex = theValueString.indexOf('Z', 16);
334                int retVal = Math.max(Math.max(plusIndex, minusIndex), zIndex);
335                if (retVal == -1) {
336                        return -1;
337                }
338                if ((retVal - 2) != (plusIndex + minusIndex + zIndex)) {
339                        throwBadDateFormat(theValueString);
340                }
341                return retVal;
342        }
343
344        /**
345         * Gets the precision for this datatype (using the default for the given type if not set)
346         *
347         * @see #setPrecision(TemporalPrecisionEnum)
348         */
349        public TemporalPrecisionEnum getPrecision() {
350                if (myPrecision == null) {
351                        return getDefaultPrecisionForDatatype();
352                }
353                return myPrecision;
354        }
355
356        /**
357         * Returns the second of the minute in the range 0-59
358         */
359        public Integer getSecond() {
360                return getFieldValue(Calendar.SECOND);
361        }
362
363        /**
364         * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was
365         * supplied.
366         */
367        public TimeZone getTimeZone() {
368                if (myTimeZoneZulu) {
369                        return getTimeZone("GMT");
370                }
371                return myTimeZone;
372        }
373
374        /**
375         * Returns the value of this object as a {@link GregorianCalendar}
376         */
377        public GregorianCalendar getValueAsCalendar() {
378                if (getValue() == null) {
379                        return null;
380                }
381                GregorianCalendar cal;
382                if (getTimeZone() != null) {
383                        cal = new GregorianCalendar(getTimeZone());
384                } else {
385                        cal = new GregorianCalendar();
386                }
387                cal.setTime(getValue());
388                return cal;
389        }
390
391        /**
392         * Returns the year, e.g. 2015
393         */
394        public Integer getYear() {
395                return getFieldValue(Calendar.YEAR);
396        }
397
398        /**
399         * To be implemented by subclasses to indicate whether the given precision is allowed by this type
400         */
401        abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
402
403        /**
404         * Returns true if the timezone is set to GMT-0:00 (Z)
405         */
406        public boolean isTimeZoneZulu() {
407                return myTimeZoneZulu;
408        }
409
410        /**
411         * Returns <code>true</code> if this object represents a date that is today's date
412         *
413         * @throws NullPointerException
414         *            if {@link #getValue()} returns <code>null</code>
415         */
416        public boolean isToday() {
417                Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value");
418                return DateUtils.isSameDay(new Date(), getValue());
419        }
420
421        private void leftPadWithZeros(int theInteger, int theLength, StringBuilder theTarget) {
422                String string = Integer.toString(theInteger);
423                for (int i = string.length(); i < theLength; i++) {
424                        theTarget.append('0');
425                }
426                theTarget.append(string);
427        }
428
429        @Override
430        protected Date parse(String theValue) throws DataFormatException {
431                Calendar cal = new GregorianCalendar(0, 0, 0);
432                cal.setTimeZone(TimeZone.getDefault());
433                String value = theValue;
434                boolean fractionalSecondsSet = false;
435
436                if (value.length() > 0 && (value.charAt(0) == ' ' || value.charAt(value.length() - 1) == ' ')) {
437                        value = value.trim();
438                }
439
440                int length = value.length();
441                if (length == 0) {
442                        return null;
443                }
444
445                if (length < 4) {
446                        throwBadDateFormat(value);
447                }
448
449                TemporalPrecisionEnum precision = null;
450                cal.set(Calendar.YEAR, parseInt(value, value.substring(0, 4), 0, 9999));
451                precision = TemporalPrecisionEnum.YEAR;
452                if (length > 4) {
453                        validateCharAtIndexIs(value, 4, '-');
454                        validateLengthIsAtLeast(value, 7);
455                        int monthVal = parseInt(value, value.substring(5, 7), 1, 12) - 1;
456                        cal.set(Calendar.MONTH, monthVal);
457                        precision = TemporalPrecisionEnum.MONTH;
458                        if (length > 7) {
459                                validateCharAtIndexIs(value, 7, '-');
460                                validateLengthIsAtLeast(value, 10);
461                                cal.set(Calendar.DATE, 1); // for some reason getActualMaximum works incorrectly if date isn't set
462                                int actualMaximum = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
463                                cal.set(Calendar.DAY_OF_MONTH, parseInt(value, value.substring(8, 10), 1, actualMaximum));
464                                precision = TemporalPrecisionEnum.DAY;
465                                if (length > 10) {
466                                        validateLengthIsAtLeast(value, 17);
467                                        validateCharAtIndexIs(value, 10, 'T'); // yyyy-mm-ddThh:mm:ss
468                                        int offsetIdx = getOffsetIndex(value);
469                                        String time;
470                                        if (offsetIdx == -1) {
471                                                // throwBadDateFormat(theValue);
472                                                // No offset - should this be an error?
473                                                time = value.substring(11);
474                                        } else {
475                                                time = value.substring(11, offsetIdx);
476                                                String offsetString = value.substring(offsetIdx);
477                                                setTimeZone(value, offsetString);
478                                                cal.setTimeZone(getTimeZone());
479                                        }
480                                        int timeLength = time.length();
481
482                                        validateCharAtIndexIs(value, 13, ':');
483                                        cal.set(Calendar.HOUR_OF_DAY, parseInt(value, value.substring(11, 13), 0, 23));
484                                        cal.set(Calendar.MINUTE, parseInt(value, value.substring(14, 16), 0, 59));
485                                        precision = TemporalPrecisionEnum.MINUTE;
486                                        if (timeLength > 5) {
487                                                validateLengthIsAtLeast(value, 19);
488                                                validateCharAtIndexIs(value, 16, ':'); // yyyy-mm-ddThh:mm:ss
489                                                cal.set(Calendar.SECOND, parseInt(value, value.substring(17, 19), 0, 60)); // note: this allows leap seconds
490                                                precision = TemporalPrecisionEnum.SECOND;
491                                                if (timeLength > 8) {
492                                                        validateCharAtIndexIs(value, 19, '.'); // yyyy-mm-ddThh:mm:ss.SSSS
493                                                        validateLengthIsAtLeast(value, 20);
494                                                        int endIndex = getOffsetIndex(value);
495                                                        if (endIndex == -1) {
496                                                                endIndex = value.length();
497                                                        }
498                                                        int millis;
499                                                        String millisString;
500                                                        if (endIndex > 23) {
501                                                                myFractionalSeconds = value.substring(20, endIndex);
502                                                                fractionalSecondsSet = true;
503                                                                endIndex = 23;
504                                                                millisString = value.substring(20, endIndex);
505                                                                millis = parseInt(value, millisString, 0, 999);
506                                                        } else {
507                                                                millisString = value.substring(20, endIndex);
508                                                                millis = parseInt(value, millisString, 0, 999);
509                                                                myFractionalSeconds = millisString;
510                                                                fractionalSecondsSet = true;
511                                                        }
512                                                        if (millisString.length() == 1) {
513                                                                millis = millis * 100;
514                                                        } else if (millisString.length() == 2) {
515                                                                millis = millis * 10;
516                                                        }
517                                                        cal.set(Calendar.MILLISECOND, millis);
518                                                        precision = TemporalPrecisionEnum.MILLI;
519                                                }
520                                        }
521                                }
522                        } else {
523                                cal.set(Calendar.DATE, 1);
524                        }
525                } else {
526                        cal.set(Calendar.DATE, 1);
527                }
528
529                if (fractionalSecondsSet == false) {
530                        myFractionalSeconds = "";
531                }
532
533                myPrecision = precision;
534                return cal.getTime();
535
536        }
537
538        private int parseInt(String theValue, String theSubstring, int theLowerBound, int theUpperBound) {
539                int retVal = 0;
540                try {
541                        retVal = Integer.parseInt(theSubstring);
542                } catch (NumberFormatException e) {
543                        throwBadDateFormat(theValue);
544                }
545
546                if (retVal < theLowerBound || retVal > theUpperBound) {
547                        throwBadDateFormat(theValue);
548                }
549
550                return retVal;
551        }
552
553        /**
554         * Sets the month with 1-index, e.g. 1=the first day of the month
555         */
556        public BaseDateTimeType setDay(int theDay) {
557                setFieldValue(Calendar.DAY_OF_MONTH, theDay, null, 0, 31);
558                return this;
559        }
560
561        private void setFieldValue(int theField, int theValue, String theFractionalSeconds, int theMinimum, int theMaximum) {
562                validateValueInRange(theValue, theMinimum, theMaximum);
563                Calendar cal;
564                if (getValue() == null) {
565                        cal = new GregorianCalendar();
566                } else {
567                        cal = getValueAsCalendar();
568                }
569                if (theField != -1) {
570                        cal.set(theField, theValue);
571                }
572                if (theFractionalSeconds != null) {
573                        myFractionalSeconds = theFractionalSeconds;
574                } else if (theField == Calendar.MILLISECOND) {
575                        myFractionalSeconds = StringUtils.leftPad(Integer.toString(theValue), 3, '0');
576                }
577                super.setValue(cal.getTime());
578        }
579
580        /**
581         * Sets the hour of the day in a 24h clock, e.g. 13=1pm
582         */
583        public BaseDateTimeType setHour(int theHour) {
584                setFieldValue(Calendar.HOUR_OF_DAY, theHour, null, 0, 23);
585                return this;
586        }
587
588        /**
589         * Sets the milliseconds within the current second.
590         * <p>
591         * Note that this method sets the
592         * same value as {@link #setNanos(long)} but with less precision.
593         * </p>
594         */
595        public BaseDateTimeType setMillis(int theMillis) {
596                setFieldValue(Calendar.MILLISECOND, theMillis, null, 0, 999);
597                return this;
598        }
599
600        /**
601         * Sets the minute of the hour in the range 0-59
602         */
603        public BaseDateTimeType setMinute(int theMinute) {
604                setFieldValue(Calendar.MINUTE, theMinute, null, 0, 59);
605                return this;
606        }
607
608        /**
609         * Sets the month with 0-index, e.g. 0=January
610         */
611        public BaseDateTimeType setMonth(int theMonth) {
612                setFieldValue(Calendar.MONTH, theMonth, null, 0, 11);
613                return this;
614        }
615
616        /**
617         * Sets the nanoseconds within the current second
618         * <p>
619         * Note that this method sets the
620         * same value as {@link #setMillis(int)} but with more precision.
621         * </p>
622         */
623        public BaseDateTimeType setNanos(long theNanos) {
624                validateValueInRange(theNanos, 0, NANOS_PER_SECOND - 1);
625                String fractionalSeconds = StringUtils.leftPad(Long.toString(theNanos), 9, '0');
626
627                // Strip trailing 0s
628                for (int i = fractionalSeconds.length(); i > 0; i--) {
629                        if (fractionalSeconds.charAt(i - 1) != '0') {
630                                fractionalSeconds = fractionalSeconds.substring(0, i);
631                                break;
632                        }
633                }
634                int millis = (int) (theNanos / NANOS_PER_MILLIS);
635                setFieldValue(Calendar.MILLISECOND, millis, fractionalSeconds, 0, 999);
636                return this;
637        }
638
639        /**
640         * Sets the precision for this datatype
641         *
642         * @throws DataFormatException
643         */
644        public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException {
645                if (thePrecision == null) {
646                        throw new NullPointerException("Precision may not be null");
647                }
648                myPrecision = thePrecision;
649                updateStringValue();
650        }
651
652        /**
653         * Sets the second of the minute in the range 0-59
654         */
655        public BaseDateTimeType setSecond(int theSecond) {
656                setFieldValue(Calendar.SECOND, theSecond, null, 0, 59);
657                return this;
658        }
659
660        private BaseDateTimeType setTimeZone(String theWholeValue, String theValue) {
661
662                if (isBlank(theValue)) {
663                        throwBadDateFormat(theWholeValue);
664                } else if (theValue.charAt(0) == 'Z') {
665                        myTimeZone = null;
666                        myTimeZoneZulu = true;
667                } else if (theValue.length() != 6) {
668                        throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
669                } else if (theValue.charAt(3) != ':' || !(theValue.charAt(0) == '+' || theValue.charAt(0) == '-')) {
670                        throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
671                } else {
672                        parseInt(theWholeValue, theValue.substring(1, 3), 0, 23);
673                        parseInt(theWholeValue, theValue.substring(4, 6), 0, 59);
674                        myTimeZoneZulu = false;
675                        myTimeZone = getTimeZone("GMT" + theValue);
676                }
677
678                return this;
679        }
680
681        public BaseDateTimeType setTimeZone(TimeZone theTimeZone) {
682                myTimeZone = theTimeZone;
683                myTimeZoneZulu = false;
684                updateStringValue();
685                return this;
686        }
687
688        public BaseDateTimeType setTimeZoneZulu(boolean theTimeZoneZulu) {
689                myTimeZoneZulu = theTimeZoneZulu;
690                myTimeZone = null;
691                updateStringValue();
692                return this;
693        }
694
695        /**
696         * Sets the value for this type using the given Java Date object as the time, and using the default precision for
697         * this datatype (unless the precision is already set), as well as the local timezone as determined by the local operating
698         * system. Both of these properties may be modified in subsequent calls if neccesary.
699         */
700        @Override
701        public BaseDateTimeType setValue(Date theValue) {
702                setValue(theValue, getPrecision());
703                return this;
704        }
705
706        /**
707         * Sets the value for this type using the given Java Date object as the time, and using the specified precision, as
708         * well as the local timezone as determined by the local operating system. Both of
709         * these properties may be modified in subsequent calls if neccesary.
710         *
711         * @param theValue
712         *           The date value
713         * @param thePrecision
714         *           The precision
715         * @throws DataFormatException
716         */
717        public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException {
718                if (getTimeZone() == null) {
719                        setTimeZone(TimeZone.getDefault());
720                }
721                myPrecision = thePrecision;
722                myFractionalSeconds = "";
723                if (theValue != null) {
724                        long millis = theValue.getTime() % 1000;
725                        if (millis < 0) {
726                                // This is for times before 1970 (see bug #444)
727                                millis = 1000 + millis;
728                        }
729                        String fractionalSeconds = Integer.toString((int) millis);
730                        myFractionalSeconds = StringUtils.leftPad(fractionalSeconds, 3, '0');
731                }
732                super.setValue(theValue);
733        }
734
735        @Override
736        public void setValueAsString(String theString) throws DataFormatException {
737                clearTimeZone();
738                super.setValueAsString(theString);
739        }
740
741        protected void setValueAsV3String(String theV3String) {
742                if (StringUtils.isBlank(theV3String)) {
743                        setValue(null);
744                } else {
745                        StringBuilder b = new StringBuilder();
746                        String timeZone = null;
747                        for (int i = 0; i < theV3String.length(); i++) {
748                                char nextChar = theV3String.charAt(i);
749                                if (nextChar == '+' || nextChar == '-' || nextChar == 'Z') {
750                                        timeZone = (theV3String.substring(i));
751                                        break;
752                                }
753
754                                // assertEquals("2013-02-02T20:13:03-05:00", DateAndTime.parseV3("20130202201303-0500").toString());
755                                if (i == 4 || i == 6) {
756                                        b.append('-');
757                                } else if (i == 8) {
758                                        b.append('T');
759                                } else if (i == 10 || i == 12) {
760                                        b.append(':');
761                                }
762
763                                b.append(nextChar);
764                        }
765
766      if (b.length() == 13)
767        b.append(":00"); // schema rule, must have minutes
768                        if (b.length() == 16)
769                                b.append(":00"); // schema rule, must have seconds
770                        if (timeZone != null && b.length() > 10) {
771                                if (timeZone.length() == 5) {
772                                        b.append(timeZone.substring(0, 3));
773                                        b.append(':');
774                                        b.append(timeZone.substring(3));
775                                } else {
776                                        b.append(timeZone);
777                                }
778                        }
779
780                        setValueAsString(b.toString());
781                }
782        }
783
784        /**
785         * Sets the year, e.g. 2015
786         */
787        public BaseDateTimeType setYear(int theYear) {
788                setFieldValue(Calendar.YEAR, theYear, null, 0, 9999);
789                return this;
790        }
791
792        private void throwBadDateFormat(String theValue) {
793                throw new DataFormatException("Invalid date/time format: \"" + theValue + "\"");
794        }
795
796        private void throwBadDateFormat(String theValue, String theMesssage) {
797                throw new DataFormatException("Invalid date/time format: \"" + theValue + "\": " + theMesssage);
798        }
799
800        /**
801         * Returns a view of this date/time as a Calendar object. Note that the returned
802         * Calendar object is entirely independent from <code>this</code> object. Changes to the
803         * calendar will not affect <code>this</code>.
804         */
805        public Calendar toCalendar() {
806                Calendar retVal = Calendar.getInstance();
807                retVal.setTime(getValue());
808                retVal.setTimeZone(getTimeZone());
809                return retVal;
810        }
811
812  /**
813   * Returns a human readable version of this date/time using the system local format.
814   * <p>
815   * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value.
816   * For example, if this date object contains the value "2012-01-05T12:00:00-08:00",
817   * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a
818   * different time zone. If this behaviour is not what you want, use
819   * {@link #toHumanDisplayLocalTimezone()} instead.
820   * </p>
821   */
822  public String toHumanDisplay() {
823    return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString());
824  }
825
826  /**
827   * Returns a human readable version of this date/time using the system local format, converted to the local timezone
828   * if neccesary.
829   *
830   * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it.
831   */
832  public String toHumanDisplayLocalTimezone() {
833    return DateTimeUtil.toHumanDisplayLocalTimezone(getPrecision(), getValue(), getValueAsString());
834  }
835
836        private void validateBeforeOrAfter(DateTimeType theDateTimeType) {
837                if (getValue() == null) {
838                        throw new NullPointerException("This BaseDateTimeType does not contain a value (getValue() returns null)");
839                }
840                if (theDateTimeType == null) {
841                        throw new NullPointerException("theDateTimeType must not be null");
842                }
843                if (theDateTimeType.getValue() == null) {
844                        throw new NullPointerException("The given BaseDateTimeType does not contain a value (theDateTimeType.getValue() returns null)");
845                }
846        }
847
848        private void validateCharAtIndexIs(String theValue, int theIndex, char theChar) {
849                if (theValue.charAt(theIndex) != theChar) {
850                        throwBadDateFormat(theValue, "Expected character '" + theChar + "' at index " + theIndex + " but found " + theValue.charAt(theIndex));
851                }
852        }
853
854        private void validateLengthIsAtLeast(String theValue, int theLength) {
855                if (theValue.length() < theLength) {
856                        throwBadDateFormat(theValue);
857                }
858        }
859
860        private void validateValueInRange(long theValue, long theMinimum, long theMaximum) {
861                if (theValue < theMinimum || theValue > theMaximum) {
862                        throw new IllegalArgumentException("Value " + theValue + " is not between allowable range: " + theMinimum + " - " + theMaximum);
863                }
864        }
865
866        @Override
867        public boolean isDateTime() {
868          return true;
869        }
870
871  @Override
872  public BaseDateTimeType dateTimeValue() {
873    return this;
874  }
875
876  public boolean hasTime() {
877    return (myPrecision == TemporalPrecisionEnum.MINUTE || myPrecision == TemporalPrecisionEnum.SECOND || myPrecision == TemporalPrecisionEnum.MILLI);
878  }
879
880  /**
881   * This method implements a datetime equality check using the rules as defined by FHIRPath.
882   *
883   * This method returns:
884   * <ul>
885   *     <li>true if the given datetimes represent the exact same instant with the same precision (irrespective of the timezone)</li>
886   *     <li>true if the given datetimes represent the exact same instant but one includes milliseconds of <code>.[0]+</code> while the other includes only SECONDS precision (irrespecitve of the timezone)</li>
887   *     <li>true if the given datetimes represent the exact same year/year-month/year-month-date (if both operands have the same precision)</li>
888   *     <li>false if both datetimes have equal precision of MINUTE or greater, one has no timezone specified but the other does, and could not represent the same instant in any timezone</li>
889   *     <li>null if both datetimes have equal precision of MINUTE or greater, one has no timezone specified but the other does, and could potentially represent the same instant in any timezone</li>
890   *     <li>false if the given datetimes have the same precision but do not represent the same instant (irrespective of timezone)</li>
891   *     <li>null otherwise (since these datetimes are not comparable)</li>
892   * </ul>
893   */
894  public Boolean equalsUsingFhirPathRules(BaseDateTimeType theOther) {
895    if (hasTimezone() != theOther.hasTimezone()) {
896      if (!couldBeTheSameTime(this, theOther)) {
897        return false;
898      } else {
899        return null;
900      }
901    } else {
902      BaseDateTimeType left = (BaseDateTimeType) this.copy();
903      BaseDateTimeType right = (BaseDateTimeType) theOther.copy();
904      if (left.hasTimezone() && left.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
905        left.setTimeZoneZulu(true);
906      }
907      if (right.hasTimezone() && right.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
908        right.setTimeZoneZulu(true);
909      }
910      Integer i = compareTimes(left, right, null);
911      return i == null ? null : i == 0;
912    }    
913  }
914
915  private boolean couldBeTheSameTime(BaseDateTimeType theArg1, BaseDateTimeType theArg2) {
916    long lowLeft = theArg1.getValue().getTime();
917    long highLeft = theArg1.getHighEdge().getValue().getTime();
918    if (!theArg1.hasTimezone()) {
919      lowLeft = lowLeft - (14 * DateUtils.MILLIS_PER_HOUR);
920      highLeft = highLeft + (14 * DateUtils.MILLIS_PER_HOUR);
921    }
922    long lowRight = theArg2.getValue().getTime();
923    long highRight = theArg2.getHighEdge().getValue().getTime();
924    if (!theArg2.hasTimezone()) {
925      lowRight = lowRight - (14 * DateUtils.MILLIS_PER_HOUR);
926      highRight = highRight + (14 * DateUtils.MILLIS_PER_HOUR);
927    }
928    if (highRight < lowLeft) {
929      return false;
930    }
931    if (highLeft < lowRight) {
932      return false;
933    }
934    return true;
935  }
936
937    private BaseDateTimeType getHighEdge() {
938      BaseDateTimeType result = (BaseDateTimeType) copy();
939      switch (getPrecision()) {
940      case DAY:
941        result.add(Calendar.DATE, 1);
942        break;
943      case MILLI:
944        break;
945      case MINUTE:
946        result.add(Calendar.MINUTE, 1);
947        break;
948      case MONTH:
949        result.add(Calendar.MONTH, 1);
950        break;
951      case SECOND:
952        result.add(Calendar.SECOND, 1);
953        break;
954      case YEAR:
955        result.add(Calendar.YEAR, 1);
956        break;
957      default:
958        break;      
959      }
960      return result;
961    }
962
963    boolean hasTimezoneIfRequired() {
964      return getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal() ||
965          getTimeZone() != null;
966    }
967
968
969    boolean hasTimezone() {
970      return getTimeZone() != null;
971    }
972
973    public static Integer compareTimes(BaseDateTimeType left, BaseDateTimeType right, Integer def) {
974      if (left.getYear() < right.getYear()) {
975        return -1;
976      } else if (left.getYear() > right.getYear()) {
977        return 1;
978      } else if (left.getPrecision() == TemporalPrecisionEnum.YEAR && right.getPrecision() == TemporalPrecisionEnum.YEAR) {
979        return 0;
980      } else if (left.getPrecision() == TemporalPrecisionEnum.YEAR || right.getPrecision() == TemporalPrecisionEnum.YEAR) {
981        return def;
982      }
983
984      if (left.getMonth() < right.getMonth()) {
985        return -1;
986      } else if (left.getMonth() > right.getMonth()) {
987        return 1;
988      } else if (left.getPrecision() == TemporalPrecisionEnum.MONTH && right.getPrecision() == TemporalPrecisionEnum.MONTH) {
989        return 0;
990      } else if (left.getPrecision() == TemporalPrecisionEnum.MONTH || right.getPrecision() == TemporalPrecisionEnum.MONTH) {
991        return def;
992      }
993
994      if (left.getDay() < right.getDay()) {
995        return -1;
996      } else if (left.getDay() > right.getDay()) {
997        return 1;
998      } else if (left.getPrecision() == TemporalPrecisionEnum.DAY && right.getPrecision() == TemporalPrecisionEnum.DAY) {
999        return 0;
1000      } else if (left.getPrecision() == TemporalPrecisionEnum.DAY || right.getPrecision() == TemporalPrecisionEnum.DAY) {
1001        return def;
1002      }
1003
1004      if (left.getHour() < right.getHour()) {
1005        return -1;
1006      } else if (left.getHour() > right.getHour()) {
1007        return 1;
1008        // hour is not a valid precision 
1009//      } else if (dateLeft.getPrecision() == TemporalPrecisionEnum.YEAR && dateRight.getPrecision() == TemporalPrecisionEnum.YEAR) {
1010//        return 0;
1011//      } else if (dateLeft.getPrecision() == TemporalPrecisionEnum.HOUR || dateRight.getPrecision() == TemporalPrecisionEnum.HOUR) {
1012//        return null;
1013      }
1014
1015      if (left.getMinute() < right.getMinute()) {
1016        return -1;
1017      } else if (left.getMinute() > right.getMinute()) {
1018        return 1;
1019      } else if (left.getPrecision() == TemporalPrecisionEnum.MINUTE && right.getPrecision() == TemporalPrecisionEnum.MINUTE) {
1020        return 0;
1021      } else if (left.getPrecision() == TemporalPrecisionEnum.MINUTE || right.getPrecision() == TemporalPrecisionEnum.MINUTE) {
1022        return def;
1023      }
1024
1025      if (left.getSecond() < right.getSecond()) {
1026        return -1;
1027      } else if (left.getSecond() > right.getSecond()) {
1028        return 1;
1029      } else if (left.getPrecision() == TemporalPrecisionEnum.SECOND && right.getPrecision() == TemporalPrecisionEnum.SECOND) {
1030        return 0;
1031      }
1032
1033      if (left.getSecondsMilli() < right.getSecondsMilli()) {
1034        return -1;
1035      } else if (left.getSecondsMilli() > right.getSecondsMilli()) {
1036        return 1;
1037      } else {
1038        return 0;
1039      }
1040    }
1041
1042    @Override
1043    public String fpValue() {
1044      return "@"+primitiveValue();
1045    }
1046
1047  private TimeZone getTimeZone(String offset) {
1048    return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone);
1049  }
1050
1051}