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