1 /* Copyright 2004 The Apache Software Foundation 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package org.apache.xmlbeans; 17 18 import java.math.BigDecimal; 19 import java.math.BigInteger; 20 import java.util.Calendar; 21 import java.util.Date; 22 import java.util.GregorianCalendar; 23 import java.util.TimeZone; 24 25 /** 26 * Represents an XML Schema-compatible Gregorian date. 27 * <p> 28 * There are many date types in XML Schema, and this type 29 * represents the natural union of all those types. A GDate 30 * can hold any subset of date fields (Year, Month, Day, Time, 31 * Timezone, or some combination). Wherever the specification 32 * provides guidance, the guidelines in the 33 * <a target="_blank" href="http://www.w3.org/TR/xmlschema-2/">XML Schema 1.0 specification</a> 34 * (plus <a target="_blank" href="http://www.w3.org/2001/05/xmlschema-errata">published errata</a>) are followed. 35 * <p> 36 * Instances may separately have values or no values for 37 * the year, month, day-of-month, and time-of-day. Not all 38 * operations are meaningful on all combinations. 39 */ 40 public final class GDate implements GDateSpecification, java.io.Serializable 41 { 42 private static final long serialVersionUID = 1L; 43 44 // XMLSchema spec requires support only for years 1 to 9999, but XMLBeans covers more up to the following limitations 45 // to avoid losing precision when transforming to a java.util.Date 46 static final int MAX_YEAR = 292277265; // is Long.MAX_VALUE ms in years - 1 (for the 11month, 31days, 23h, 59m, 59sec case). 47 static final int MIN_YEAR = -292275295; // is Long.MIN_VALUE ms in years + 1970 + 1 48 49 // for fast equality comparison, hashing, and serialization 50 private transient String _canonicalString; 51 private transient String _string; 52 53 private int _bits; 54 private int _CY; 55 private int _M; 56 private int _D; 57 private int _h; 58 private int _m; 59 private int _s; 60 private BigDecimal _fs; 61 private int _tzsign; 62 private int _tzh; 63 private int _tzm; 64 65 66 /* package */ static final BigDecimal _zero = BigDecimal.valueOf(0); 67 /* package */ static final BigDecimal _one = BigDecimal.valueOf(1); 68 69 /** 70 * Constructs a GDate based on a lexical representation. 71 */ 72 public GDate(CharSequence string) 73 { 74 // first trim XML whitespace 75 int len = string.length(); 76 int start = 0; 77 while (len > 0 && isSpace(string.charAt(len - 1))) 78 len -= 1; 79 while (start < len && isSpace(string.charAt(start))) 80 start += 1; 81 82 // pick optional timezone off the end 83 if (len - start >= 1 && string.charAt(len - 1) == 'Z') 84 { 85 _bits |= HAS_TIMEZONE; 86 len -= 1; 87 } 88 else if (len - start >= 6) 89 timezone: { 90 int tzsign; 91 int tzhour; 92 int tzminute; 93 94 if (string.charAt(len - 3) != ':') 95 break timezone; 96 97 switch (string.charAt(len - 6)) 98 { 99 case '-': 100 tzsign = -1; break; 101 case '+': 102 tzsign = 1; break; 103 default: 104 break timezone; 105 } 106 107 tzhour = twoDigit(string, len - 5); 108 tzminute = twoDigit(string, len - 2); 109 if (tzhour > 14) 110 throw new IllegalArgumentException("time zone hour must be two digits between -14 and +14"); 111 if (tzminute > 59) 112 throw new IllegalArgumentException("time zone minute must be two digits between 00 and 59"); 113 _bits |= HAS_TIMEZONE; 114 _tzsign = tzsign; 115 _tzh = tzhour; 116 _tzm = tzminute; 117 len -= 6; 118 } 119 120 // pick date fields off the beginning if it doesn't look like a time 121 if (start < len && (start + 2 >= len || string.charAt(start + 2) != ':')) 122 scandate: 123 { 124 // parse year sign 125 boolean negyear = false; 126 if (start < len && string.charAt(start) == '-') 127 { 128 negyear = true; 129 start += 1; 130 } 131 132 // scan year digits 133 int value = 0; 134 int digits = -start; 135 char ch; 136 boolean startsWithZero = start < len && digitVal(string.charAt(start))==0; 137 138 for (;;) 139 { 140 ch = start < len ? string.charAt(start) : '\0'; 141 if (!isDigit(ch)) 142 break; 143 144 if ( startsWithZero && start+digits>=4 ) 145 throw new IllegalArgumentException("year value starting with zero must be 4 or less digits: " + string); 146 147 value = value * 10 + digitVal(ch); 148 start += 1; 149 } 150 digits += start; 151 if (digits > 9) 152 throw new IllegalArgumentException("year too long (up to 9 digits)"); 153 else if (digits >= 4) 154 { 155 _bits |= HAS_YEAR; 156 _CY = negyear ? -value : value; 157 if (_CY == 0) throw new IllegalArgumentException("year must not be zero"); 158 } 159 else if (digits > 0) 160 throw new IllegalArgumentException("year must be four digits (may pad with zeroes, e.g., 0560)"); 161 162 if ( _CY > MAX_YEAR ) 163 throw new IllegalArgumentException("year value not supported: too big, must be less than " + MAX_YEAR); 164 165 if ( _CY < MIN_YEAR ) 166 throw new IllegalArgumentException("year values not supported: too small, must be bigger than " + MIN_YEAR); 167 168 // hyphen introduces a month 169 if (ch != '-') 170 { 171 if (negyear && !hasYear()) 172 throw new IllegalArgumentException(); // a single minus 173 else 174 break scandate; 175 } 176 start += 1; 177 178 // two-digit month 179 if (len - start >= 2) 180 { 181 value = twoDigit(string, start); 182 if (value >= 1 && value <= 12) 183 { 184 _bits |= HAS_MONTH; 185 _M = value; 186 start += 2; 187 } 188 } 189 190 // hyphen introduces a day 191 ch = start < len ? string.charAt(start) : '\0'; 192 if (ch != '-') 193 { 194 if (!hasMonth()) 195 throw new IllegalArgumentException(); // minus after a year 196 else 197 break scandate; 198 } 199 start += 1; 200 201 // two-digit day 202 if (len - start >= 2) 203 { 204 value = twoDigit(string, start); 205 if (value >= 1 && value <= 31) 206 { 207 _bits |= HAS_DAY; 208 _D = value; 209 start += 2; 210 } 211 } 212 213 if (!hasDay()) 214 { 215 // error in the original schema spec permits an extra '-' here 216 if (hasMonth() && !hasYear()) 217 { 218 ch = start < len ? string.charAt(start) : '\0'; 219 if (ch == '-') 220 { 221 start += 1; 222 break scandate; 223 } 224 } 225 throw new IllegalArgumentException(); // minus after a month 226 } 227 } 228 229 // time 230 if (start < len) 231 { 232 if (hasYear() || hasMonth() || hasDay()) 233 { 234 if (string.charAt(start) != 'T') 235 throw new IllegalArgumentException("date and time must be separated by 'T'"); 236 start += 1; 237 } 238 239 if (len < start + 8 || string.charAt(start + 2) != ':' || string.charAt(start + 5) != ':') 240 throw new IllegalArgumentException(); 241 242 int h = twoDigit(string, start); 243 if (h > 24) 244 throw new IllegalArgumentException("hour must be between 00 and 23"); 245 int m = twoDigit(string, start + 3); 246 if (m >= 60) 247 throw new IllegalArgumentException("minute must be between 00 and 59"); 248 int s = twoDigit(string, start + 6); 249 if (s >= 60) 250 throw new IllegalArgumentException("second must be between 00 and 59"); 251 252 start += 8; 253 254 BigDecimal fs = _zero; 255 if (start < len) 256 { 257 if (string.charAt(start) != '.') 258 throw new IllegalArgumentException(); 259 if (start + 1 < len) 260 { 261 for (int i = start + 1; i < len; i++) 262 { 263 if (!isDigit(string.charAt(i))) 264 throw new IllegalArgumentException(); 265 } 266 try 267 { 268 fs = new BigDecimal(string.subSequence(start, len).toString()); 269 } 270 catch (Throwable e) 271 { 272 throw new IllegalArgumentException(); 273 } 274 } 275 } 276 277 _bits |= HAS_TIME; 278 _h = h; 279 _m = m; 280 _s = s; 281 _fs = fs; 282 } 283 284 if ( hasTime() && _h == 24 ) 285 { 286 if ( _m != 0 || _s != 0 || _fs.compareTo(_zero) != 0 ) 287 throw new IllegalArgumentException("if hour is 24, minutes, seconds and fraction must be 0"); 288 else 289 { // normalize to next day if it has date or at least has day 290 if ( hasDate() ) 291 { 292 GDateBuilder gdb = new GDateBuilder(_CY, _M, _D, _h, _m, _s, _fs, _tzsign, _tzh, _tzm); 293 gdb.normalize24h(); 294 295 _D = gdb.getDay(); 296 _M = gdb.getMonth(); 297 _CY = gdb.getYear(); 298 _h = 0; 299 } 300 else if ( hasDay() ) // if no date only days increment 301 { 302 _D++; 303 _h = 0; 304 } 305 } 306 } 307 308 if (!isValid()) 309 throw new IllegalArgumentException("invalid date"); 310 } 311 312 /** 313 * Constructs a GDate with the specified year, month, day, 314 * hours, minutes, seconds, and optional fractional seconds, in 315 * an unspecified timezone. 316 * <p> 317 * Note that by not specifying the timezone the GDate 318 * becomes partially unordered with respect to times that 319 * do have a specified timezone. 320 */ 321 public GDate( 322 int year, 323 int month, 324 int day, 325 int hour, 326 int minute, 327 int second, 328 BigDecimal fraction) 329 { 330 _bits = HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME; 331 332 _CY = year; 333 _M = month; 334 _D = day; 335 _h = hour; 336 _m = minute; 337 _s = second; 338 _fs = fraction == null ? _zero : fraction; 339 340 if (!isValid()) 341 throw new IllegalArgumentException(); 342 } 343 344 /** 345 * Constructs an absolute GDate with the specified year, 346 * month, day, hours, minutes, seconds, and optional fractional 347 * seconds, and in the timezone specified. 348 * <p> 349 * If you wish to have a time or date that isn't in a specified timezone, 350 * then use the constructor that does not include the timezone arguments. 351 */ 352 public GDate( 353 int year, 354 int month, 355 int day, 356 int hour, 357 int minute, 358 int second, 359 BigDecimal fraction, 360 int tzSign, 361 int tzHour, 362 int tzMinute) 363 { 364 _bits = HAS_TIMEZONE | HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME; 365 366 _CY = year; 367 _M = month; 368 _D = day; 369 _h = hour; 370 _m = minute; 371 _s = second; 372 _fs = fraction == null ? _zero : fraction; 373 _tzsign = tzSign; 374 _tzh = tzHour; 375 _tzm = tzMinute; 376 377 if (!isValid()) 378 throw new IllegalArgumentException(); 379 } 380 381 /** 382 * Constructs a GDate based on a java.util.Date. 383 * <p> 384 * The current offset of the default timezone is used as the timezone. 385 * <p> 386 * For example, if eastern daylight time is in effect at the given 387 * date, the timezone on the east coast of the united states 388 * translates to GMT-05:00 (EST) + 1:00 (DT offset) == GMT-04:00. 389 */ 390 public GDate(Date date) 391 { 392 // requires some date math, so ctor lives on GDateBuilder 393 this(new GDateBuilder(date)); 394 } 395 396 /** 397 * Constructs a GDate based on a java.util.Calendar. 398 * <p> 399 * If the calendar does not have some fields set, the same absence 400 * of information is reflected in the GDate. Note that 401 * java.util.GregorianCalendar fills in all fields as soon as any 402 * are fetched, so constructing a GDate with the same calendar object 403 * twice may result in a different GDate because of a changed calendar. 404 * Note that org.apache.xmlbeans.XmlCalendar is stable if you re-get a set field, 405 * so it does not have the same problem. 406 */ 407 public GDate(Calendar calendar) 408 { 409 // we must scrape the "isSet" information out before accessing anything 410 boolean isSetYear = calendar.isSet(Calendar.YEAR); 411 boolean isSetEra = calendar.isSet(Calendar.ERA); 412 boolean isSetMonth = calendar.isSet(Calendar.MONTH); 413 boolean isSetDay = calendar.isSet(Calendar.DAY_OF_MONTH); 414 boolean isSetHourOfDay = calendar.isSet(Calendar.HOUR_OF_DAY); 415 boolean isSetHour = calendar.isSet(Calendar.HOUR); 416 boolean isSetAmPm = calendar.isSet(Calendar.AM_PM); 417 boolean isSetMinute = calendar.isSet(Calendar.MINUTE); 418 boolean isSetSecond = calendar.isSet(Calendar.SECOND); 419 boolean isSetMillis = calendar.isSet(Calendar.MILLISECOND); 420 boolean isSetZone = calendar.isSet(Calendar.ZONE_OFFSET); 421 boolean isSetDst = calendar.isSet(Calendar.DST_OFFSET); 422 423 if (isSetYear) 424 { 425 int y = calendar.get(Calendar.YEAR); 426 if (isSetEra && calendar instanceof GregorianCalendar) 427 if (calendar.get(Calendar.ERA) == GregorianCalendar.BC) 428 y = -y; //1 - y; 429 _bits |= HAS_YEAR; 430 _CY = y; 431 } 432 if (isSetMonth) 433 { 434 _bits |= HAS_MONTH; 435 _M = calendar.get(Calendar.MONTH) + 1; // !!note 436 } 437 if (isSetDay) 438 { 439 _bits |= HAS_DAY; 440 _D = calendar.get(Calendar.DAY_OF_MONTH); 441 } 442 boolean gotTime = false; 443 444 int h = 0; 445 int m = 0; 446 int s = 0; 447 BigDecimal fs = _zero; 448 449 if (isSetHourOfDay) 450 { 451 h = calendar.get(Calendar.HOUR_OF_DAY); 452 gotTime = true; 453 } 454 else if (isSetHour && isSetAmPm) 455 { 456 h = calendar.get(Calendar.HOUR) + calendar.get(Calendar.AM_PM) * 12; 457 gotTime = true; 458 } 459 460 if (isSetMinute) 461 { 462 m = calendar.get(Calendar.MINUTE); 463 gotTime = true; 464 } 465 466 if (isSetSecond) 467 { 468 s = calendar.get(Calendar.SECOND); 469 gotTime = true; 470 } 471 472 if (isSetMillis) 473 { 474 fs = BigDecimal.valueOf(calendar.get(Calendar.MILLISECOND), 3); 475 gotTime = true; 476 } 477 478 if (gotTime) 479 { 480 _bits |= HAS_TIME; 481 _h = h; 482 _m = m; 483 _s = s; 484 _fs = fs; 485 } 486 487 if (isSetZone) 488 { 489 int zoneOffsetInMilliseconds = calendar.get(Calendar.ZONE_OFFSET); 490 if (isSetDst) 491 zoneOffsetInMilliseconds += calendar.get(Calendar.DST_OFFSET); 492 493 _bits |= HAS_TIMEZONE; 494 if (zoneOffsetInMilliseconds == 0) 495 { 496 _tzsign = 0; 497 _tzh = 0; 498 _tzm = 0; 499 TimeZone zone = calendar.getTimeZone(); 500 String id = zone.getID(); 501 if (id != null && id.length() > 3) switch (id.charAt(3)) 502 { 503 case '+': _tzsign = 1; break; // GMT+00:00 504 case '-': _tzsign = -1; break; // GMT-00:00 505 } 506 } 507 else 508 { 509 _tzsign = (zoneOffsetInMilliseconds < 0 ? -1 : +1); 510 zoneOffsetInMilliseconds = zoneOffsetInMilliseconds * _tzsign; 511 _tzh = zoneOffsetInMilliseconds / 3600000; 512 _tzm = (zoneOffsetInMilliseconds - _tzh * 3600000) / 60000; 513 } 514 } 515 } 516 517 /** 518 * Constructs a GDate based on another GDateSpecification. 519 */ 520 public GDate(GDateSpecification gdate) 521 { 522 if (gdate.hasTimeZone()) 523 { 524 _bits |= HAS_TIMEZONE; 525 _tzsign = gdate.getTimeZoneSign(); 526 _tzh = gdate.getTimeZoneHour(); 527 _tzm = gdate.getTimeZoneMinute(); 528 } 529 530 if (gdate.hasTime()) 531 { 532 _bits |= HAS_TIME; 533 _h = gdate.getHour(); 534 _m = gdate.getMinute(); 535 _s = gdate.getSecond(); 536 _fs = gdate.getFraction(); 537 } 538 539 if (gdate.hasDay()) 540 { 541 _bits |= HAS_DAY; 542 _D = gdate.getDay(); 543 } 544 545 if (gdate.hasMonth()) 546 { 547 _bits |= HAS_MONTH; 548 _M = gdate.getMonth(); 549 } 550 551 if (gdate.hasYear()) 552 { 553 _bits |= HAS_YEAR; 554 _CY = gdate.getYear(); 555 } 556 } 557 558 /* package */ static final boolean isDigit(char ch) 559 { 560 return ((char)(ch - '0') <= '9' - '0'); // char is unsigned 561 } 562 563 /* package */ static final boolean isSpace(char ch) 564 { 565 switch (ch) 566 { 567 case ' ': 568 case '\t': 569 case '\r': 570 case '\n': 571 return true; 572 default: 573 return false; 574 } 575 } 576 577 /* package */ static final int digitVal(char ch) 578 { 579 return (ch - '0'); 580 } 581 582 private static final int twoDigit(CharSequence str, int index) 583 { 584 char ch1 = str.charAt(index); 585 char ch2 = str.charAt(index + 1); 586 if (!isDigit(ch1) || !isDigit(ch2)) 587 return 100; // not two digits 588 return digitVal(ch1) * 10 + digitVal(ch2); 589 } 590 591 /** 592 * Returns true: all GDate instances are immutable. 593 */ 594 public final boolean isImmutable() 595 { 596 return true; 597 } 598 599 /** 600 * Returns a combination of flags indicating the information 601 * contained by this GDate. The five flags are 602 * HAS_TIMEZONE, HAS_YEAR, HAS_MONTH, HAS_DAY, and HAS_TIME. 603 */ 604 public int getFlags() 605 { 606 return _bits; 607 } 608 609 /** 610 * True if this date/time specification specifies a timezone. 611 */ 612 public final boolean hasTimeZone() 613 { return ((_bits & HAS_TIMEZONE) != 0); } 614 615 /** 616 * True if this date/time specification specifies a year. 617 */ 618 public final boolean hasYear() 619 { return ((_bits & HAS_YEAR) != 0); } 620 621 /** 622 * True if this date/time specification specifies a month-of-year. 623 */ 624 public final boolean hasMonth() 625 { return ((_bits & HAS_MONTH) != 0); } 626 627 /** 628 * True if this date/time specification specifies a day-of-month. 629 */ 630 public final boolean hasDay() 631 { return ((_bits & HAS_DAY) != 0); } 632 633 /** 634 * True if this date/time specification specifies a time-of-day. 635 */ 636 public final boolean hasTime() 637 { return ((_bits & HAS_TIME) != 0); } 638 639 /** 640 * True if this date/time specification specifies a full date (year, month, day) 641 */ 642 public final boolean hasDate() 643 { return ((_bits & (HAS_DAY | HAS_MONTH | HAS_YEAR)) == (HAS_DAY | HAS_MONTH | HAS_YEAR)); } 644 645 /** 646 * Gets the year. Should be a four-digit year specification. 647 */ 648 public final int getYear() 649 { return _CY; } 650 651 /** 652 * Gets the month-of-year. January is 1. 653 */ 654 public final int getMonth() 655 { return _M; } 656 657 /** 658 * Gets the day-of-month. The first day of each month is 1. 659 */ 660 public final int getDay() 661 { return _D; } 662 663 /** 664 * Gets the hour-of-day. Midnight is 0, and 11PM is 23. 665 */ 666 public final int getHour() 667 { return _h; } 668 669 /** 670 * Gets the minute-of-hour. Range from 0 to 59. 671 */ 672 public final int getMinute() 673 { return _m; } 674 675 /** 676 * Gets the second-of-minute. Range from 0 to 59. 677 */ 678 public final int getSecond() 679 { return _s; } 680 681 /** 682 * Gets the fraction-of-second. Range from 0 (inclusive) to 1 (exclusive). 683 */ 684 public final BigDecimal getFraction() 685 { return _fs; } 686 687 /** 688 * Gets the time zone sign. For time zones east of GMT, 689 * this is positive; for time zones west, this is negative. 690 */ 691 public final int getTimeZoneSign() 692 { return _tzsign; } 693 694 /** 695 * Gets the time zone hour. 696 * 697 * This is always positive: for the sign, look at 698 * getTimeZoneSign(). 699 */ 700 public final int getTimeZoneHour() 701 { return _tzh; } 702 703 /** 704 * Gets the time zone minutes. 705 * 706 * This is always positive: for the sign, look at 707 * getTimeZoneSign(). 708 */ 709 public final int getTimeZoneMinute() 710 { return _tzm; } 711 712 /** 713 * Gets the rounded millisecond value. Range from 0 to 999 714 */ 715 public int getMillisecond() 716 { 717 if (_fs == null) 718 return 0; 719 return _fs.setScale(3, BigDecimal.ROUND_DOWN).unscaledValue().intValue(); 720 } 721 722 /** 723 * The canonical string representation. Specific moments or 724 * times-of-day in a specified timezone are normalized to 725 * UTC time to produce a canonical string form for them. 726 * Other recurring time specifications keep their timezone 727 * information. 728 */ 729 public String canonicalString() 730 { 731 ensureCanonicalString(); 732 return _canonicalString; 733 } 734 735 /** 736 * True if this GDate corresponds to a valid gregorian date value 737 * in XML schema. 738 */ 739 public boolean isValid() 740 { 741 return GDateBuilder.isValidGDate(this); 742 } 743 744 /** 745 * Returns the Julian date corresponding to this Gregorian date. 746 * The Julian date (JD) is a continuous count of days from 747 * 1 January 4713 BC. 748 */ 749 public int getJulianDate() 750 { 751 return GDateBuilder.julianDateForGDate(this); 752 } 753 754 /** 755 * Retrieves the value of the current time as an {@link XmlCalendar}. 756 * <p> 757 * {@link XmlCalendar} is a subclass of {@link java.util.GregorianCalendar} 758 * which is slightly customized to match XML schema date rules. 759 * <p> 760 * The returned {@link XmlCalendar} has only those time and date fields 761 * set that are reflected in the GDate object. Because of the way the 762 * {@link java.util.Calendar} contract works, any information in the isSet() vanishes 763 * as soon as you view any unset field using get() methods. 764 * This means that if it is important to understand which date fields 765 * are set, you must call isSet() first before get(). 766 */ 767 public XmlCalendar getCalendar() 768 { 769 return new XmlCalendar(this); 770 } 771 772 773 /** 774 * Retrieves the value of the current time as a java.util.Date 775 * instance. 776 */ 777 public Date getDate() 778 { 779 return GDateBuilder.dateForGDate(this); 780 } 781 782 /** 783 * Comparison to another GDate. 784 * <ul> 785 * <li>Returns -1 if this < date. (less-than) 786 * <li>Returns 0 if this == date. (equal) 787 * <li>Returns 1 if this > date. (greater-than) 788 * <li>Returns 2 if this <> date. (incomparable) 789 * </ul> 790 * Two instances are incomparable if they have different amounts 791 * of information. 792 */ 793 public int compareToGDate(GDateSpecification datespec) 794 { 795 return GDateBuilder.compareGDate(this, datespec); 796 } 797 798 /** 799 * Returns the builtin type code for the shape of the information 800 * contained in this instance, or 0 if the 801 * instance doesn't contain information corresponding to a 802 * Schema type. 803 * <p> 804 * Value will be equal to 805 * {@link SchemaType#BTC_NOT_BUILTIN}, 806 * {@link SchemaType#BTC_G_YEAR}, 807 * {@link SchemaType#BTC_G_YEAR_MONTH}, 808 * {@link SchemaType#BTC_G_MONTH}, 809 * {@link SchemaType#BTC_G_MONTH_DAY}, 810 * {@link SchemaType#BTC_G_DAY}, 811 * {@link SchemaType#BTC_DATE}, 812 * {@link SchemaType#BTC_DATE_TIME}, or 813 * {@link SchemaType#BTC_TIME}. 814 */ 815 public int getBuiltinTypeCode() 816 { 817 return GDateBuilder.btcForFlags(_bits); 818 } 819 820 /** 821 * Adds a duration to this GDate, and returns a new GDate. 822 */ 823 public GDate add(GDurationSpecification duration) 824 { 825 GDateBuilder builder = new GDateBuilder(this); 826 builder.addGDuration(duration); 827 return builder.toGDate(); 828 } 829 830 /** 831 * Adds a duration to this GDate, and returns a new GDate. 832 */ 833 public GDate subtract(GDurationSpecification duration) 834 { 835 GDateBuilder builder = new GDateBuilder(this); 836 builder.subtractGDuration(duration); 837 return builder.toGDate(); 838 } 839 840 /** 841 * GDate is an immutable class, and equality is computed based 842 * on its canonical value. 843 */ 844 public boolean equals(Object obj) 845 { 846 if (obj == this) 847 return true; 848 if (!(obj instanceof GDate)) 849 return false; 850 851 ensureCanonicalString(); 852 return _canonicalString.equals(((GDate)obj).canonicalString()); 853 } 854 855 /** 856 * Returns a hash code for this GDate. 857 */ 858 public int hashCode() 859 { 860 ensureCanonicalString(); 861 return _canonicalString.hashCode(); 862 } 863 864 /** 865 * The canonical string representation. Specific moments or 866 * times-of-day in a specified timezone are normalized to 867 * UTC time to produce a canonical string form for them. 868 * Other recurring time specifications keep their timezone 869 * information. 870 */ 871 private void ensureCanonicalString() 872 { 873 if (_canonicalString != null) 874 return; 875 876 boolean needNormalize = 877 (hasTimeZone() && getTimeZoneSign() != 0 && hasTime() && 878 ((hasDay() == hasMonth() && hasDay() == hasYear()))); 879 880 if (!needNormalize && getFraction() != null && getFraction().scale() > 0) 881 { 882 BigInteger bi = getFraction().unscaledValue(); 883 needNormalize = (bi.mod(GDateBuilder.TEN).signum() == 0); 884 } 885 886 if (!needNormalize) 887 _canonicalString = toString(); 888 else 889 { 890 GDateBuilder gdb = new GDateBuilder(this); 891 gdb.normalize(); 892 _canonicalString = gdb.toString(); 893 } 894 } 895 896 /** 897 * The natural string representation. This represents the information 898 * that is available, including timezone. For types that correspond 899 * to defined schema types (schemaBuiltinTypeCode() > 0), 900 * this provides the natural lexical representation. 901 * <p> 902 * When both time and timezone are specified, this string is not 903 * the canonical representation unless the timezone is UTC (Z) 904 * (since the same moment in time can be expressed in different 905 * timezones). To get a canonical string, use the canonicalString() 906 * method. 907 */ 908 public String toString() 909 { 910 if (_string == null) 911 _string = formatGDate(this); 912 return _string; 913 } 914 915 private final static char[] _tensDigit = 916 { 917 '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', 918 '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', 919 '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', 920 '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', 921 '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', 922 '5', '5', '5', '5', '5', '5', '5', '5', '5', '5', 923 '6', '6', '6', '6', '6', '6', '6', '6', '6', '6', 924 '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', 925 '8', '8', '8', '8', '8', '8', '8', '8', '8', '8', 926 '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', 927 }; 928 private final static char[] _onesDigit = 929 { 930 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 931 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 932 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 933 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 934 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 935 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 936 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 937 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 938 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 939 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 940 }; 941 942 private static final int _padTwoAppend(char[] b, int i, int n) 943 { 944 assert(n >= 0 && n < 100); 945 b[i] = _tensDigit[n]; 946 b[i + 1] = _onesDigit[n]; 947 return i + 2; 948 } 949 950 private static final int _padFourAppend(char[] b, int i, int n) 951 { 952 if (n < 0) 953 { 954 b[i++] = '-'; 955 n = -n; 956 } 957 if (n >= 10000) 958 { 959 String s = Integer.toString(n); 960 s.getChars(0, s.length(), b, i); 961 return i + s.length(); 962 } 963 int q = n / 100; 964 int r = n - q * 100; 965 b[i] = _tensDigit[q]; 966 b[i + 1] = _onesDigit[q]; 967 b[i + 2] = _tensDigit[r]; 968 b[i + 3] = _onesDigit[r]; 969 return i + 4; 970 } 971 972 private static final TimeZone GMTZONE = TimeZone.getTimeZone("GMT"); 973 private static final TimeZone[] MINUSZONE = 974 { 975 TimeZone.getTimeZone("GMT-00:00"), 976 TimeZone.getTimeZone("GMT-01:00"), 977 TimeZone.getTimeZone("GMT-02:00"), 978 TimeZone.getTimeZone("GMT-03:00"), 979 TimeZone.getTimeZone("GMT-04:00"), 980 TimeZone.getTimeZone("GMT-05:00"), 981 TimeZone.getTimeZone("GMT-06:00"), 982 TimeZone.getTimeZone("GMT-07:00"), 983 TimeZone.getTimeZone("GMT-08:00"), 984 TimeZone.getTimeZone("GMT-09:00"), 985 TimeZone.getTimeZone("GMT-10:00"), 986 TimeZone.getTimeZone("GMT-11:00"), 987 TimeZone.getTimeZone("GMT-12:00"), 988 TimeZone.getTimeZone("GMT-13:00"), 989 TimeZone.getTimeZone("GMT-14:00"), 990 }; 991 private static final TimeZone[] PLUSZONE = 992 { 993 TimeZone.getTimeZone("GMT+00:00"), 994 TimeZone.getTimeZone("GMT+01:00"), 995 TimeZone.getTimeZone("GMT+02:00"), 996 TimeZone.getTimeZone("GMT+03:00"), 997 TimeZone.getTimeZone("GMT+04:00"), 998 TimeZone.getTimeZone("GMT+05:00"), 999 TimeZone.getTimeZone("GMT+06:00"), 1000 TimeZone.getTimeZone("GMT+07:00"), 1001 TimeZone.getTimeZone("GMT+08:00"), 1002 TimeZone.getTimeZone("GMT+09:00"), 1003 TimeZone.getTimeZone("GMT+10:00"), 1004 TimeZone.getTimeZone("GMT+11:00"), 1005 TimeZone.getTimeZone("GMT+12:00"), 1006 TimeZone.getTimeZone("GMT+13:00"), 1007 TimeZone.getTimeZone("GMT+14:00"), 1008 }; 1009 1010 /* package */ static final TimeZone timeZoneForGDate(GDateSpecification date) 1011 { 1012 // use a cached timezone if integral; otherwise make a new one. 1013 if (!date.hasTimeZone()) 1014 return TimeZone.getDefault(); 1015 if (date.getTimeZoneSign() == 0) 1016 return GMTZONE; 1017 if (date.getTimeZoneMinute() == 0 && date.getTimeZoneHour() <= 14 && date.getTimeZoneHour() >= 0) 1018 return date.getTimeZoneSign() < 0 ? MINUSZONE[date.getTimeZoneHour()] : PLUSZONE[date.getTimeZoneHour()]; 1019 1020 char[] zb = new char[9]; 1021 zb[0] = 'G'; 1022 zb[1] = 'M'; 1023 zb[2] = 'T'; 1024 zb[3] = (date.getTimeZoneSign() < 0) ? '-' : '+'; 1025 GDate._padTwoAppend(zb, 4, date.getTimeZoneHour()); 1026 zb[6] = ':'; 1027 GDate._padTwoAppend(zb, 7, date.getTimeZoneMinute()); 1028 return TimeZone.getTimeZone(new String(zb)); 1029 } 1030 1031 /* package */ static String formatGDate(GDateSpecification spec) 1032 { 1033 // We've used a char[] rather than a StringBuffer for a 4x speedup 1034 // -YY(10)YY-MM-DDTHH:MM:SS.FFFFFF+ZH:ZM 1035 // 1 + 10 + 3+ 3+ 3+ 3+ 3+1 + s + 3+ 3 = 33 + s 1036 BigDecimal fs = spec.getFraction(); 1037 char[] message = new char[33 + (fs == null ? 0 : fs.scale())]; 1038 int i = 0; 1039 1040 if (spec.hasYear() || spec.hasMonth() || spec.hasDay()) 1041 { 1042 dmy: { 1043 if (spec.hasYear()) 1044 i = _padFourAppend(message, 0, spec.getYear()); 1045 else 1046 message[i++] = '-'; 1047 1048 if (!(spec.hasMonth() || spec.hasDay())) 1049 break dmy; 1050 1051 message[i++] = '-'; 1052 if (spec.hasMonth()) 1053 i = _padTwoAppend(message, i, spec.getMonth()); 1054 1055 if (!spec.hasDay()) 1056 break dmy; 1057 1058 message[i++] = '-'; 1059 i = _padTwoAppend(message, i, spec.getDay()); 1060 break dmy; 1061 } 1062 if (spec.hasTime()) 1063 message[i++] = 'T'; 1064 } 1065 1066 if (spec.hasTime()) 1067 { 1068 i = _padTwoAppend(message, i, spec.getHour()); 1069 message[i++] = ':'; 1070 i = _padTwoAppend(message, i, spec.getMinute()); 1071 message[i++] = ':'; 1072 i = _padTwoAppend(message, i, spec.getSecond()); 1073 if (fs != _zero) // (optimization ~3%) 1074 { 1075 String frac = fs.toString(); 1076 int point = frac.indexOf('.'); 1077 if (point >= 0) 1078 { 1079 frac.getChars(point, frac.length(), message, i); 1080 i += frac.length() - point; 1081 } 1082 } 1083 } 1084 1085 if (spec.hasTimeZone()) 1086 { 1087 if (spec.getTimeZoneSign() == 0) 1088 { 1089 message[i++] = 'Z'; 1090 } 1091 else 1092 { 1093 message[i++] = spec.getTimeZoneSign() > 0 ? '+' : '-'; 1094 i = _padTwoAppend(message, i, spec.getTimeZoneHour()); 1095 message[i++] = ':'; 1096 i = _padTwoAppend(message, i, spec.getTimeZoneMinute()); 1097 } 1098 } 1099 1100 // it would be nice to use (0, i, message) ctor instead 1101 return new String(message, 0, i); 1102 } 1103 1104 } 1105