View Javadoc
1   package ca.uhn.fhir.model.primitive;
2   
3   /*
4    * #%L
5    * HAPI FHIR - Core Library
6    * %%
7    * Copyright (C) 2014 - 2018 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import static org.apache.commons.lang3.StringUtils.isBlank;
24  
25  import java.util.Calendar;
26  import java.util.Date;
27  import java.util.GregorianCalendar;
28  import java.util.TimeZone;
29  
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.commons.lang3.Validate;
32  import org.apache.commons.lang3.time.DateUtils;
33  import org.apache.commons.lang3.time.FastDateFormat;
34  
35  import ca.uhn.fhir.model.api.BasePrimitive;
36  import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
37  import ca.uhn.fhir.parser.DataFormatException;
38  
39  public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
40  	static final long NANOS_PER_MILLIS = 1000000L;
41  	static final long NANOS_PER_SECOND = 1000000000L;
42  
43  	private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM);
44  	private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM);
45  
46  	private String myFractionalSeconds;
47  	private TemporalPrecisionEnum myPrecision = null;
48  	private TimeZone myTimeZone;
49  	private boolean myTimeZoneZulu = false;
50  
51  	/**
52  	 * Constructor
53  	 */
54  	public BaseDateTimeDt() {
55  		// nothing
56  	}
57  
58  	/**
59  	 * Constructor
60  	 * 
61  	 * @throws DataFormatException
62  	 *            If the specified precision is not allowed for this type
63  	 */
64  	public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision) {
65  		setValue(theDate, thePrecision);
66  		if (isPrecisionAllowed(thePrecision) == false) {
67  			throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate);
68  		}
69  	}
70  
71  	/**
72  	 * Constructor
73  	 */
74  	public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) {
75  		this(theDate, thePrecision);
76  		setTimeZone(theTimeZone);
77  	}
78  
79  	/**
80  	 * Constructor
81  	 * 
82  	 * @throws DataFormatException
83  	 *            If the specified precision is not allowed for this type
84  	 */
85  	public BaseDateTimeDt(String theString) {
86  		setValueAsString(theString);
87  		validatePrecisionAndThrowDataFormatException(theString, getPrecision());
88  	}
89  
90  	private void clearTimeZone() {
91  		myTimeZone = null;
92  		myTimeZoneZulu = false;
93  	}
94  
95  	@Override
96  	protected String encode(Date theValue) {
97  		if (theValue == null) {
98  			return null;
99  		}
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 }