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