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