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