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