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