001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.model.primitive;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.model.api.BasePrimitive;
024import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
025import ca.uhn.fhir.parser.DataFormatException;
026import org.apache.commons.lang3.StringUtils;
027import org.apache.commons.lang3.Validate;
028import org.apache.commons.lang3.time.DateUtils;
029import org.apache.commons.lang3.time.FastDateFormat;
030
031import java.util.Calendar;
032import java.util.Date;
033import java.util.GregorianCalendar;
034import java.util.Map;
035import java.util.TimeZone;
036import java.util.concurrent.ConcurrentHashMap;
037
038import static org.apache.commons.lang3.StringUtils.isBlank;
039
040public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
041        static final long NANOS_PER_MILLIS = 1000000L;
042        static final long NANOS_PER_SECOND = 1000000000L;
043
044        private static final Map<String, TimeZone> timezoneCache = new ConcurrentHashMap<>();
045
046        private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
047        private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
048        public static final String NOW_DATE_CONSTANT = "%now";
049        public static final String TODAY_DATE_CONSTANT = "%today";
050        private String myFractionalSeconds;
051        private TemporalPrecisionEnum myPrecision = null;
052        private TimeZone myTimeZone;
053        private boolean myTimeZoneZulu = false;
054
055        /**
056         * Constructor
057         */
058        public BaseDateTimeDt() {
059                // nothing
060        }
061
062        /**
063         * Constructor
064         * 
065         * @throws DataFormatException
066         *            If the specified precision is not allowed for this type
067         */
068        public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision) {
069                setValue(theDate, thePrecision);
070                if (isPrecisionAllowed(thePrecision) == false) {
071                        throw new DataFormatException(Msg.code(1880) + "Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate);
072                }
073        }
074
075        /**
076         * Constructor
077         */
078        public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
079                this(theDate, thePrecision);
080                setTimeZone(theTimeZone);
081        }
082
083        /**
084         * Constructor
085         * 
086         * @throws DataFormatException
087         *            If the specified precision is not allowed for this type
088         */
089        public BaseDateTimeDt(String theString) {
090                setValueAsString(theString);
091                validatePrecisionAndThrowDataFormatException(theString, getPrecision());
092        }
093
094        private void clearTimeZone() {
095                myTimeZone = null;
096                myTimeZoneZulu = false;
097        }
098
099        @Override
100        protected String encode(Date theValue) {
101                if (theValue == null) {
102                        return null;
103                }
104                GregorianCalendar cal;
105                if (myTimeZoneZulu) {
106                        cal = new GregorianCalendar(getTimeZone("GMT"));
107                } else if (myTimeZone != null) {
108                        cal = new GregorianCalendar(myTimeZone);
109                } else {
110                        cal = new GregorianCalendar();
111                }
112                cal.setTime(theValue);
113
114                StringBuilder b = new StringBuilder();
115                leftPadWithZeros(cal.get(Calendar.YEAR), 4, b);
116                if (myPrecision.ordinal() > TemporalPrecisionEnum.YEAR.ordinal()) {
117                        b.append('-');
118                        leftPadWithZeros(cal.get(Calendar.MONTH) + 1, 2, b);
119                        if (myPrecision.ordinal() > TemporalPrecisionEnum.MONTH.ordinal()) {
120                                b.append('-');
121                                leftPadWithZeros(cal.get(Calendar.DATE), 2, b);
122                                if (myPrecision.ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
123                                        b.append('T');
124                                        leftPadWithZeros(cal.get(Calendar.HOUR_OF_DAY), 2, b);
125                                        b.append(':');
126                                        leftPadWithZeros(cal.get(Calendar.MINUTE), 2, b);
127                                        if (myPrecision.ordinal() > TemporalPrecisionEnum.MINUTE.ordinal()) {
128                                                b.append(':');
129                                                leftPadWithZeros(cal.get(Calendar.SECOND), 2, b);
130                                                if (myPrecision.ordinal() > TemporalPrecisionEnum.SECOND.ordinal()) {
131                                                        b.append('.');
132                                                        b.append(myFractionalSeconds);
133                                                        for (int i = myFractionalSeconds.length(); i < 3; i++) {
134                                                                b.append('0');
135                                                        }
136                                                }
137                                        }
138
139                                        if (myTimeZoneZulu) {
140                                                b.append('Z');
141                                        } else if (myTimeZone != null) {
142                                                int offset = myTimeZone.getOffset(theValue.getTime());
143                                                if (offset >= 0) {
144                                                        b.append('+');
145                                                } else {
146                                                        b.append('-');
147                                                        offset = Math.abs(offset);
148                                                }
149
150                                                int hoursOffset = (int) (offset / DateUtils.MILLIS_PER_HOUR);
151                                                leftPadWithZeros(hoursOffset, 2, b);
152                                                b.append(':');
153                                                int minutesOffset = (int) (offset % DateUtils.MILLIS_PER_HOUR);
154                                                minutesOffset = (int) (minutesOffset / DateUtils.MILLIS_PER_MINUTE);
155                                                leftPadWithZeros(minutesOffset, 2, b);
156                                        }
157                                }
158                        }
159                }
160                return b.toString();
161        }
162
163        /**
164         * Returns the month with 1-index, e.g. 1=the first day of the month
165         */
166        public Integer getDay() {
167                return getFieldValue(Calendar.DAY_OF_MONTH);
168        }
169
170        /**
171         * Returns the default precision for the given datatype
172         */
173        protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype();
174
175        private Integer getFieldValue(int theField) {
176                if (getValue() == null) {
177                        return null;
178                }
179                Calendar cal = getValueAsCalendar();
180                return cal.get(theField);
181        }
182
183        /**
184         * Returns the hour of the day in a 24h clock, e.g. 13=1pm
185         */
186        public Integer getHour() {
187                return getFieldValue(Calendar.HOUR_OF_DAY);
188        }
189
190        /**
191         * Returns the milliseconds within the current second.
192         * <p>
193         * Note that this method returns the
194         * same value as {@link #getNanos()} but with less precision.
195         * </p>
196         */
197        public Integer getMillis() {
198                return getFieldValue(Calendar.MILLISECOND);
199        }
200
201        /**
202         * Returns the minute of the hour in the range 0-59
203         */
204        public Integer getMinute() {
205                return getFieldValue(Calendar.MINUTE);
206        }
207
208        /**
209         * Returns the month with 0-index, e.g. 0=January
210         */
211        public Integer getMonth() {
212                return getFieldValue(Calendar.MONTH);
213        }
214
215        /**
216         * Returns the nanoseconds within the current second
217         * <p>
218         * Note that this method returns the
219         * same value as {@link #getMillis()} but with more precision.
220         * </p>
221         */
222        public Long getNanos() {
223                if (isBlank(myFractionalSeconds)) {
224                        return null;
225                }
226                String retVal = StringUtils.rightPad(myFractionalSeconds, 9, '0');
227                retVal = retVal.substring(0, 9);
228                return Long.parseLong(retVal);
229        }
230
231        private int getOffsetIndex(String theValueString) {
232                int plusIndex = theValueString.indexOf('+', 16);
233                int minusIndex = theValueString.indexOf('-', 16);
234                int zIndex = theValueString.indexOf('Z', 16);
235                int retVal = Math.max(Math.max(plusIndex, minusIndex), zIndex);
236                if (retVal == -1) {
237                        return -1;
238                }
239                if ((retVal - 2) != (plusIndex + minusIndex + zIndex)) {
240                        throwBadDateFormat(theValueString);
241                }
242                return retVal;
243        }
244
245        /**
246         * Gets the precision for this datatype (using the default for the given type if not set)
247         * 
248         * @see #setPrecision(TemporalPrecisionEnum)
249         */
250        public TemporalPrecisionEnum getPrecision() {
251                if (myPrecision == null) {
252                        return getDefaultPrecisionForDatatype();
253                }
254                return myPrecision;
255        }
256
257        /**
258         * Returns the second of the minute in the range 0-59
259         */
260        public Integer getSecond() {
261                return getFieldValue(Calendar.SECOND);
262        }
263
264        /**
265         * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was
266         * supplied.
267         */
268        public TimeZone getTimeZone() {
269                if (myTimeZoneZulu) {
270                        return getTimeZone("GMT");
271                }
272                return myTimeZone;
273        }
274
275        /**
276         * Returns the value of this object as a {@link GregorianCalendar}
277         */
278        public GregorianCalendar getValueAsCalendar() {
279                if (getValue() == null) {
280                        return null;
281                }
282                GregorianCalendar cal;
283                if (getTimeZone() != null) {
284                        cal = new GregorianCalendar(getTimeZone());
285                } else {
286                        cal = new GregorianCalendar();
287                }
288                cal.setTime(getValue());
289                return cal;
290        }
291
292        /**
293         * Returns the year, e.g. 2015
294         */
295        public Integer getYear() {
296                return getFieldValue(Calendar.YEAR);
297        }
298
299        /**
300         * To be implemented by subclasses to indicate whether the given precision is allowed by this type
301         */
302        protected abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
303
304        /**
305         * Returns true if the timezone is set to GMT-0:00 (Z)
306         */
307        public boolean isTimeZoneZulu() {
308                return myTimeZoneZulu;
309        }
310
311        /**
312         * Returns <code>true</code> if this object represents a date that is today's date
313         * 
314         * @throws NullPointerException
315         *            if {@link #getValue()} returns <code>null</code>
316         */
317        public boolean isToday() {
318                Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value");
319                return DateUtils.isSameDay(new Date(), getValue());
320        }
321
322        private void leftPadWithZeros(int theInteger, int theLength, StringBuilder theTarget) {
323                String string = Integer.toString(theInteger);
324                for (int i = string.length(); i < theLength; i++) {
325                        theTarget.append('0');
326                }
327                theTarget.append(string);
328        }
329
330        @Override
331        protected Date parse(String theValue) throws DataFormatException {
332                Calendar cal = new GregorianCalendar(0, 0, 0);
333                cal.setTimeZone(TimeZone.getDefault());
334                String value = theValue;
335                boolean fractionalSecondsSet = false;
336
337                if (value.length() > 0 && (value.charAt(0) == ' ' || value.charAt(value.length() - 1) == ' ')) {
338                        value = value.trim();
339                }
340
341                int length = value.length();
342                if (length == 0) {
343                        return null;
344                }
345
346                if (length < 4) {
347                        throwBadDateFormat(value);
348                }
349
350                TemporalPrecisionEnum precision = null;
351                cal.set(Calendar.YEAR, parseInt(value, value.substring(0, 4), 0, 9999));
352                precision = TemporalPrecisionEnum.YEAR;
353                if (length > 4) {
354                        validateCharAtIndexIs(value, 4, '-');
355                        validateLengthIsAtLeast(value, 7);
356                        int monthVal = parseInt(value, value.substring(5, 7), 1, 12) - 1;
357                        cal.set(Calendar.MONTH, monthVal);
358                        precision = TemporalPrecisionEnum.MONTH;
359                        if (length > 7) {
360                                validateCharAtIndexIs(value, 7, '-');
361                                validateLengthIsAtLeast(value, 10);
362                                cal.set(Calendar.DATE, 1); // for some reason getActualMaximum works incorrectly if date isn't set
363                                int actualMaximum = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
364                                cal.set(Calendar.DAY_OF_MONTH, parseInt(value, value.substring(8, 10), 1, actualMaximum));
365                                precision = TemporalPrecisionEnum.DAY;
366                                if (length > 10) {
367                                        validateLengthIsAtLeast(value, 16);
368                                        validateCharAtIndexIs(value, 10, 'T'); // yyyy-mm-ddThh:mm:ss
369                                        int offsetIdx = getOffsetIndex(value);
370                                        String time;
371                                        if (offsetIdx == -1) {
372                                                //throwBadDateFormat(theValue);
373                                                // No offset - should this be an error?
374                                                time = value.substring(11);
375                                        } else {
376                                                time = value.substring(11, offsetIdx);
377                                                String offsetString = value.substring(offsetIdx);
378                                                setTimeZone(value, offsetString);
379                                                cal.setTimeZone(getTimeZone());
380                                        }
381                                        int timeLength = time.length();
382
383                                        validateCharAtIndexIs(value, 13, ':');
384                                        cal.set(Calendar.HOUR_OF_DAY, parseInt(value, value.substring(11, 13), 0, 23));
385                                        cal.set(Calendar.MINUTE, parseInt(value, value.substring(14, 16), 0, 59));
386                                        precision = TemporalPrecisionEnum.MINUTE;
387                                        if (timeLength > 5) {
388                                                validateLengthIsAtLeast(value, 19);
389                                                validateCharAtIndexIs(value, 16, ':'); // yyyy-mm-ddThh:mm:ss
390                                                cal.set(Calendar.SECOND, parseInt(value, value.substring(17, 19), 0, 59));
391                                                precision = TemporalPrecisionEnum.SECOND;
392                                                if (timeLength > 8) {
393                                                        validateCharAtIndexIs(value, 19, '.'); // yyyy-mm-ddThh:mm:ss.SSSS
394                                                        validateLengthIsAtLeast(value, 20);
395                                                        int endIndex = getOffsetIndex(value);
396                                                        if (endIndex == -1) {
397                                                                endIndex = value.length();
398                                                        }
399                                                        int millis;
400                                                        String millisString;
401                                                        if (endIndex > 23) {
402                                                                myFractionalSeconds = value.substring(20, endIndex);
403                                                                fractionalSecondsSet = true;
404                                                                endIndex = 23;
405                                                                millisString = value.substring(20, endIndex);
406                                                                millis = parseInt(value, millisString, 0, 999);
407                                                        } else {
408                                                                millisString = value.substring(20, endIndex);
409                                                                millis = parseInt(value, millisString, 0, 999);
410                                                                myFractionalSeconds = millisString;
411                                                                fractionalSecondsSet = true;
412                                                        }
413                                                        if (millisString.length() == 1) {
414                                                                millis = millis * 100;
415                                                        } else if (millisString.length() == 2) {
416                                                                millis = millis * 10;
417                                                        }
418                                                        cal.set(Calendar.MILLISECOND, millis);
419                                                        precision = TemporalPrecisionEnum.MILLI;
420                                                }
421                                        }
422                                }
423                        } else {
424                                cal.set(Calendar.DATE, 1);
425                        }
426                } else {
427                        cal.set(Calendar.DATE, 1);
428                }
429
430                if (fractionalSecondsSet == false) {
431                        myFractionalSeconds = "";
432                }
433
434                if (precision == TemporalPrecisionEnum.MINUTE) {
435                        validatePrecisionAndThrowDataFormatException(value, precision);
436                }
437                
438                setPrecision(precision);
439                return cal.getTime();
440
441        }
442
443        private int parseInt(String theValue, String theSubstring, int theLowerBound, int theUpperBound) {
444                int retVal = 0;
445                try {
446                        retVal = Integer.parseInt(theSubstring);
447                } catch (NumberFormatException e) {
448                        throwBadDateFormat(theValue);
449                }
450
451                if (retVal < theLowerBound || retVal > theUpperBound) {
452                        throwBadDateFormat(theValue);
453                }
454
455                return retVal;
456        }
457
458        /**
459         * Sets the month with 1-index, e.g. 1=the first day of the month
460         */
461        public BaseDateTimeDt setDay(int theDay) {
462                setFieldValue(Calendar.DAY_OF_MONTH, theDay, null, 0, 31);
463                return this;
464        }
465
466        private void setFieldValue(int theField, int theValue, String theFractionalSeconds, int theMinimum, int theMaximum) {
467                validateValueInRange(theValue, theMinimum, theMaximum);
468                Calendar cal;
469                if (getValue() == null) {
470                        cal = new GregorianCalendar(0, 0, 0);
471                } else {
472                        cal = getValueAsCalendar();
473                }
474                if (theField != -1) {
475                        cal.set(theField, theValue);
476                }
477                if (theFractionalSeconds != null) {
478                        myFractionalSeconds = theFractionalSeconds;
479                } else if (theField == Calendar.MILLISECOND) {
480                        myFractionalSeconds = StringUtils.leftPad(Integer.toString(theValue), 3, '0');
481                }
482                super.setValue(cal.getTime());
483        }
484
485        /**
486         * Sets the hour of the day in a 24h clock, e.g. 13=1pm
487         */
488        public BaseDateTimeDt setHour(int theHour) {
489                setFieldValue(Calendar.HOUR_OF_DAY, theHour, null, 0, 23);
490                return this;
491        }
492
493        /**
494         * Sets the milliseconds within the current second.
495         * <p>
496         * Note that this method sets the
497         * same value as {@link #setNanos(long)} but with less precision.
498         * </p>
499         */
500        public BaseDateTimeDt setMillis(int theMillis) {
501                setFieldValue(Calendar.MILLISECOND, theMillis, null, 0, 999);
502                return this;
503        }
504
505        /**
506         * Sets the minute of the hour in the range 0-59
507         */
508        public BaseDateTimeDt setMinute(int theMinute) {
509                setFieldValue(Calendar.MINUTE, theMinute, null, 0, 59);
510                return this;
511        }
512
513        /**
514         * Sets the month with 0-index, e.g. 0=January
515         */
516        public BaseDateTimeDt setMonth(int theMonth) {
517                setFieldValue(Calendar.MONTH, theMonth, null, 0, 11);
518                return this;
519        }
520
521        /**
522         * Sets the nanoseconds within the current second
523         * <p>
524         * Note that this method sets the
525         * same value as {@link #setMillis(int)} but with more precision.
526         * </p>
527         */
528        public BaseDateTimeDt setNanos(long theNanos) {
529                validateValueInRange(theNanos, 0, NANOS_PER_SECOND-1);
530                String fractionalSeconds = StringUtils.leftPad(Long.toString(theNanos), 9, '0');
531
532                // Strip trailing 0s
533                for (int i = fractionalSeconds.length(); i > 0; i--) {
534                        if (fractionalSeconds.charAt(i-1) != '0') {
535                                fractionalSeconds = fractionalSeconds.substring(0, i);
536                                break;
537                        }
538                }
539                int millis = (int)(theNanos / NANOS_PER_MILLIS);
540                setFieldValue(Calendar.MILLISECOND, millis, fractionalSeconds, 0, 999);
541                return this;
542        }
543
544        /**
545         * Sets the precision for this datatype
546         * 
547         * @throws DataFormatException
548         */
549        public BaseDateTimeDt setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException {
550                if (thePrecision == null) {
551                        throw new NullPointerException(Msg.code(1881) + "Precision may not be null");
552                }
553                myPrecision = thePrecision;
554                updateStringValue();
555                return this;
556        }
557
558        /**
559         * Sets the second of the minute in the range 0-59
560         */
561        public BaseDateTimeDt setSecond(int theSecond) {
562                setFieldValue(Calendar.SECOND, theSecond, null, 0, 59);
563                return this;
564        }
565
566        private BaseDateTimeDt setTimeZone(String theWholeValue, String theValue) {
567
568                if (isBlank(theValue)) {
569                        throwBadDateFormat(theWholeValue);
570                } else if (theValue.charAt(0) == 'Z') {
571                        clearTimeZone();
572                        setTimeZoneZulu(true);
573                } else if (theValue.length() != 6) {
574                        throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
575                } else if (theValue.charAt(3) != ':' || !(theValue.charAt(0) == '+' || theValue.charAt(0) == '-')) {
576                        throwBadDateFormat(theWholeValue, "Timezone offset must be in the form \"Z\", \"-HH:mm\", or \"+HH:mm\"");
577                } else {
578                        parseInt(theWholeValue, theValue.substring(1, 3), 0, 23);
579                        parseInt(theWholeValue, theValue.substring(4, 6), 0, 59);
580                        clearTimeZone();
581                        setTimeZone(getTimeZone("GMT" + theValue));
582                }
583
584                return this;
585        }
586
587        private TimeZone getTimeZone(String offset) {
588                return timezoneCache.computeIfAbsent(offset, TimeZone::getTimeZone);
589        }
590
591        public BaseDateTimeDt setTimeZone(TimeZone theTimeZone) {
592                myTimeZone = theTimeZone;
593                updateStringValue();
594                return this;
595        }
596
597        public BaseDateTimeDt setTimeZoneZulu(boolean theTimeZoneZulu) {
598                myTimeZoneZulu = theTimeZoneZulu;
599                updateStringValue();
600                return this;
601        }
602
603        /**
604         * Sets the value for this type using the given Java Date object as the time, and using the default precision for
605         * this datatype (unless the precision is already set), as well as the local timezone as determined by the local operating
606         * system. Both of these properties may be modified in subsequent calls if neccesary.
607         */
608        @Override
609        public BaseDateTimeDt setValue(Date theValue) {
610                setValue(theValue, getPrecision());
611                return this;
612        }
613
614        /**
615         * Sets the value for this type using the given Java Date object as the time, and using the specified precision, as
616         * well as the local timezone as determined by the local operating system. Both of
617         * these properties may be modified in subsequent calls if neccesary.
618         * 
619         * @param theValue
620         *           The date value
621         * @param thePrecision
622         *           The precision
623         * @throws DataFormatException
624         */
625        public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException {
626                if (getTimeZone() == null) {
627                        setTimeZone(TimeZone.getDefault());
628                }
629                myPrecision = thePrecision;
630                myFractionalSeconds = "";
631                if (theValue != null) {
632                        long millis = theValue.getTime() % 1000;
633                        if (millis < 0) {
634                                // This is for times before 1970 (see bug #444)
635                                millis = 1000 + millis;
636                        }
637                        String fractionalSeconds = Integer.toString((int) millis);
638                        myFractionalSeconds = StringUtils.leftPad(fractionalSeconds, 3, '0');
639                }
640                super.setValue(theValue);
641        }
642
643        @Override
644        public void setValueAsString(String theValue) throws DataFormatException {
645                clearTimeZone();
646
647                if (NOW_DATE_CONSTANT.equalsIgnoreCase(theValue)) {
648                        setValue(new Date());
649                } else if (TODAY_DATE_CONSTANT.equalsIgnoreCase(theValue)) {
650                        setValue(new Date(), TemporalPrecisionEnum.DAY);
651                } else {
652                        super.setValueAsString(theValue);
653                }
654        }
655
656        /**
657         * Sets the year, e.g. 2015
658         */
659        public BaseDateTimeDt setYear(int theYear) {
660                setFieldValue(Calendar.YEAR, theYear, null, 0, 9999);
661                return this;
662        }
663
664        private void throwBadDateFormat(String theValue) {
665                throw new DataFormatException(Msg.code(1882) + "Invalid date/time format: \"" + theValue + "\"");
666        }
667
668        private void throwBadDateFormat(String theValue, String theMesssage) {
669                throw new DataFormatException(Msg.code(1883) + "Invalid date/time format: \"" + theValue + "\": " + theMesssage);
670        }
671
672        /**
673         * Returns a human readable version of this date/time using the system local format.
674         * <p>
675         * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value.
676         * For example, if this date object contains the value "2012-01-05T12:00:00-08:00",
677         * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a
678         * different time zone. If this behaviour is not what you want, use
679         * {@link #toHumanDisplayLocalTimezone()} instead.
680         * </p>
681         */
682        public String toHumanDisplay() {
683                TimeZone tz = getTimeZone();
684                Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance();
685                value.setTime(getValue());
686
687                switch (getPrecision()) {
688                case YEAR:
689                case MONTH:
690                case DAY:
691                        return ourHumanDateFormat.format(value);
692                case MILLI:
693                case SECOND:
694                default:
695                        return ourHumanDateTimeFormat.format(value);
696                }
697        }
698
699        /**
700         * Returns a human readable version of this date/time using the system local format, converted to the local timezone
701         * if neccesary.
702         * 
703         * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it.
704         */
705        public String toHumanDisplayLocalTimezone() {
706                switch (getPrecision()) {
707                case YEAR:
708                case MONTH:
709                case DAY:
710                        return ourHumanDateFormat.format(getValue());
711                case MILLI:
712                case SECOND:
713                default:
714                        return ourHumanDateTimeFormat.format(getValue());
715                }
716        }
717
718        private void validateCharAtIndexIs(String theValue, int theIndex, char theChar) {
719                if (theValue.charAt(theIndex) != theChar) {
720                        throwBadDateFormat(theValue, "Expected character '" + theChar + "' at index " + theIndex + " but found " + theValue.charAt(theIndex));
721                }
722        }
723
724        private void validateLengthIsAtLeast(String theValue, int theLength) {
725                if (theValue.length() < theLength) {
726                        throwBadDateFormat(theValue);
727                }
728        }
729
730        private void validateValueInRange(long theValue, long theMinimum, long theMaximum) {
731                if (theValue < theMinimum || theValue > theMaximum) {
732                        throw new IllegalArgumentException(Msg.code(1884) + "Value " + theValue + " is not between allowable range: " + theMinimum + " - " + theMaximum);
733                }
734        }
735
736        private void validatePrecisionAndThrowDataFormatException(String theValue, TemporalPrecisionEnum thePrecision) {
737                if (isPrecisionAllowed(thePrecision) == false) {
738                        throw new DataFormatException(Msg.code(1885) + "Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theValue);
739                }
740        }
741
742}