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