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