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