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