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