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