1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.text.format; 18 19 import android.util.TimeFormatException; 20 21 import com.android.i18n.timezone.WallTime; 22 import com.android.i18n.timezone.ZoneInfoData; 23 import com.android.i18n.timezone.ZoneInfoDb; 24 25 import java.util.Locale; 26 import java.util.TimeZone; 27 28 /** 29 * An alternative to the {@link java.util.Calendar} and 30 * {@link java.util.GregorianCalendar} classes. An instance of the Time class represents 31 * a moment in time, specified with second precision. It is modelled after 32 * struct tm. This class is not thread-safe and does not consider leap seconds. 33 * 34 * <p>This class has a number of issues and it is recommended that 35 * {@link java.util.GregorianCalendar} is used instead. 36 * 37 * <p>Known issues: 38 * <ul> 39 * <li>For historical reasons when performing time calculations all arithmetic currently takes 40 * place using 32-bit integers. This limits the reliable time range representable from 1902 41 * until 2037.See the wikipedia article on the 42 * <a href="http://en.wikipedia.org/wiki/Year_2038_problem">Year 2038 problem</a> for details. 43 * Do not rely on this behavior; it may change in the future. 44 * </li> 45 * <li>Calling {@link #switchTimezone(String)} on a date that cannot exist, such as a wall time 46 * that was skipped due to a DST transition, will result in a date in 1969 (i.e. -1, or 1 second 47 * before 1st Jan 1970 UTC).</li> 48 * <li>Much of the formatting / parsing assumes ASCII text and is therefore not suitable for 49 * use with non-ASCII scripts.</li> 50 * <li>No support for pseudo-zones like "GMT-07:00".</li> 51 * </ul> 52 * 53 * @deprecated Use {@link java.util.GregorianCalendar} instead. 54 */ 55 @Deprecated 56 public class Time { 57 private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000"; 58 private static final String Y_M_D_T_H_M_S_000_Z = "%Y-%m-%dT%H:%M:%S.000Z"; 59 private static final String Y_M_D = "%Y-%m-%d"; 60 61 public static final String TIMEZONE_UTC = "UTC"; 62 63 /** 64 * The Julian day of the epoch, that is, January 1, 1970 on the Gregorian 65 * calendar. 66 */ 67 public static final int EPOCH_JULIAN_DAY = 2440588; 68 69 /** 70 * The Julian day of the Monday in the week of the epoch, December 29, 1969 71 * on the Gregorian calendar. 72 */ 73 public static final int MONDAY_BEFORE_JULIAN_EPOCH = EPOCH_JULIAN_DAY - 3; 74 75 /** 76 * True if this is an allDay event. The hour, minute, second fields are 77 * all zero, and the date is displayed the same in all time zones. 78 */ 79 public boolean allDay; 80 81 /** 82 * Seconds [0-61] (2 leap seconds allowed) 83 */ 84 public int second; 85 86 /** 87 * Minute [0-59] 88 */ 89 public int minute; 90 91 /** 92 * Hour of day [0-23] 93 */ 94 public int hour; 95 96 /** 97 * Day of month [1-31] 98 */ 99 public int monthDay; 100 101 /** 102 * Month [0-11] 103 */ 104 public int month; 105 106 /** 107 * Year. For example, 1970. 108 */ 109 public int year; 110 111 /** 112 * Day of week [0-6] 113 */ 114 public int weekDay; 115 116 /** 117 * Day of year [0-365] 118 */ 119 public int yearDay; 120 121 /** 122 * This time is in daylight savings time. One of: 123 * <ul> 124 * <li><b>positive</b> - in dst</li> 125 * <li><b>0</b> - not in dst</li> 126 * <li><b>negative</b> - unknown</li> 127 * </ul> 128 */ 129 public int isDst; 130 131 /** 132 * Offset in seconds from UTC including any DST offset. 133 */ 134 public long gmtoff; 135 136 /** 137 * The timezone for this Time. Should not be null. 138 */ 139 public String timezone; 140 141 /* 142 * Define symbolic constants for accessing the fields in this class. Used in 143 * getActualMaximum(). 144 */ 145 public static final int SECOND = 1; 146 public static final int MINUTE = 2; 147 public static final int HOUR = 3; 148 public static final int MONTH_DAY = 4; 149 public static final int MONTH = 5; 150 public static final int YEAR = 6; 151 public static final int WEEK_DAY = 7; 152 public static final int YEAR_DAY = 8; 153 public static final int WEEK_NUM = 9; 154 155 public static final int SUNDAY = 0; 156 public static final int MONDAY = 1; 157 public static final int TUESDAY = 2; 158 public static final int WEDNESDAY = 3; 159 public static final int THURSDAY = 4; 160 public static final int FRIDAY = 5; 161 public static final int SATURDAY = 6; 162 163 // An object that is reused for date calculations. 164 private TimeCalculator calculator; 165 166 /** 167 * Construct a Time object in the timezone named by the string 168 * argument "timezone". The time is initialized to Jan 1, 1970. 169 * @param timezoneId string containing the timezone to use. 170 * @see TimeZone 171 */ Time(String timezoneId)172 public Time(String timezoneId) { 173 if (timezoneId == null) { 174 throw new NullPointerException("timezoneId is null!"); 175 } 176 initialize(timezoneId); 177 } 178 179 /** 180 * Construct a Time object in the default timezone. The time is initialized to 181 * Jan 1, 1970. 182 */ Time()183 public Time() { 184 initialize(TimeZone.getDefault().getID()); 185 } 186 187 /** 188 * A copy constructor. Construct a Time object by copying the given 189 * Time object. No normalization occurs. 190 * 191 * @param other 192 */ Time(Time other)193 public Time(Time other) { 194 initialize(other.timezone); 195 set(other); 196 } 197 198 /** Initialize the Time to 00:00:00 1/1/1970 in the specified timezone. */ initialize(String timezoneId)199 private void initialize(String timezoneId) { 200 this.timezone = timezoneId; 201 this.year = 1970; 202 this.monthDay = 1; 203 // Set the daylight-saving indicator to the unknown value -1 so that 204 // it will be recomputed. 205 this.isDst = -1; 206 207 // A reusable object that performs the date/time calculations. 208 calculator = new TimeCalculator(timezoneId); 209 } 210 211 /** 212 * Ensures the values in each field are in range. For example if the 213 * current value of this calendar is March 32, normalize() will convert it 214 * to April 1. It also fills in weekDay, yearDay, isDst and gmtoff. 215 * 216 * <p> 217 * If "ignoreDst" is true, then this method sets the "isDst" field to -1 218 * (the "unknown" value) before normalizing. It then computes the 219 * time in milliseconds and sets the correct value for "isDst" if the 220 * fields resolve to a valid date / time. 221 * 222 * <p> 223 * See {@link #toMillis(boolean)} for more information about when to 224 * use <tt>true</tt> or <tt>false</tt> for "ignoreDst" and when {@code -1} 225 * might be returned. 226 * 227 * @return the UTC milliseconds since the epoch, or {@code -1} 228 */ normalize(boolean ignoreDst)229 public long normalize(boolean ignoreDst) { 230 calculator.copyFieldsFromTime(this); 231 long timeInMillis = calculator.toMillis(ignoreDst); 232 calculator.copyFieldsToTime(this); 233 return timeInMillis; 234 } 235 236 /** 237 * Convert this time object so the time represented remains the same, but is 238 * instead located in a different timezone. This method automatically calls 239 * normalize() in some cases. 240 * 241 * <p>This method can return incorrect results if the date / time cannot be normalized. 242 */ switchTimezone(String timezone)243 public void switchTimezone(String timezone) { 244 calculator.copyFieldsFromTime(this); 245 calculator.switchTimeZone(timezone); 246 calculator.copyFieldsToTime(this); 247 this.timezone = timezone; 248 } 249 250 private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31, 251 31, 30, 31, 30, 31 }; 252 253 /** 254 * Return the maximum possible value for the given field given the value of 255 * the other fields. Requires that it be normalized for MONTH_DAY and 256 * YEAR_DAY. 257 * @param field one of the constants for HOUR, MINUTE, SECOND, etc. 258 * @return the maximum value for the field. 259 */ getActualMaximum(int field)260 public int getActualMaximum(int field) { 261 switch (field) { 262 case SECOND: 263 return 59; // leap seconds, bah humbug 264 case MINUTE: 265 return 59; 266 case HOUR: 267 return 23; 268 case MONTH_DAY: { 269 int n = DAYS_PER_MONTH[this.month]; 270 if (n != 28) { 271 return n; 272 } else { 273 int y = this.year; 274 return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 29 : 28; 275 } 276 } 277 case MONTH: 278 return 11; 279 case YEAR: 280 return 2037; 281 case WEEK_DAY: 282 return 6; 283 case YEAR_DAY: { 284 int y = this.year; 285 // Year days are numbered from 0, so the last one is usually 364. 286 return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 365 : 364; 287 } 288 case WEEK_NUM: 289 throw new RuntimeException("WEEK_NUM not implemented"); 290 default: 291 throw new RuntimeException("bad field=" + field); 292 } 293 } 294 295 /** 296 * Clears all values, setting the timezone to the given timezone. Sets isDst 297 * to a negative value to mean "unknown". 298 * @param timezoneId the timezone to use. 299 */ clear(String timezoneId)300 public void clear(String timezoneId) { 301 if (timezoneId == null) { 302 throw new NullPointerException("timezone is null!"); 303 } 304 this.timezone = timezoneId; 305 this.allDay = false; 306 this.second = 0; 307 this.minute = 0; 308 this.hour = 0; 309 this.monthDay = 0; 310 this.month = 0; 311 this.year = 0; 312 this.weekDay = 0; 313 this.yearDay = 0; 314 this.gmtoff = 0; 315 this.isDst = -1; 316 } 317 318 /** 319 * Compare two {@code Time} objects and return a negative number if {@code 320 * a} is less than {@code b}, a positive number if {@code a} is greater than 321 * {@code b}, or 0 if they are equal. 322 * 323 * <p> 324 * This method can return an incorrect answer when the date / time fields of 325 * either {@code Time} have been set to a local time that contradicts the 326 * available timezone information. 327 * 328 * @param a first {@code Time} instance to compare 329 * @param b second {@code Time} instance to compare 330 * @throws NullPointerException if either argument is {@code null} 331 * @throws IllegalArgumentException if {@link #allDay} is true but {@code 332 * hour}, {@code minute}, and {@code second} are not 0. 333 * @return a negative result if {@code a} is earlier, a positive result if 334 * {@code b} is earlier, or 0 if they are equal. 335 */ compare(Time a, Time b)336 public static int compare(Time a, Time b) { 337 if (a == null) { 338 throw new NullPointerException("a == null"); 339 } else if (b == null) { 340 throw new NullPointerException("b == null"); 341 } 342 a.calculator.copyFieldsFromTime(a); 343 b.calculator.copyFieldsFromTime(b); 344 345 return TimeCalculator.compare(a.calculator, b.calculator); 346 } 347 348 /** 349 * Print the current value given the format string provided. See 350 * strftime(3) manual page for what means what. The final string must be 351 * less than 256 characters. 352 * @param format a string containing the desired format. 353 * @return a String containing the current time expressed in the current locale. 354 */ format(String format)355 public String format(String format) { 356 calculator.copyFieldsFromTime(this); 357 return calculator.format(format); 358 } 359 360 /** 361 * Return the current time in YYYYMMDDTHHMMSS<tz> format 362 */ 363 @Override toString()364 public String toString() { 365 // toString() uses its own TimeCalculator rather than the shared one. Otherwise weird stuff 366 // happens during debugging when the debugger calls toString(). 367 TimeCalculator calculator = new TimeCalculator(this.timezone); 368 calculator.copyFieldsFromTime(this); 369 return calculator.toStringInternal(); 370 } 371 372 /** 373 * Parses a date-time string in either the RFC 2445 format or an abbreviated 374 * format that does not include the "time" field. For example, all of the 375 * following strings are valid: 376 * 377 * <ul> 378 * <li>"20081013T160000Z"</li> 379 * <li>"20081013T160000"</li> 380 * <li>"20081013"</li> 381 * </ul> 382 * 383 * Returns whether or not the time is in UTC (ends with Z). If the string 384 * ends with "Z" then the timezone is set to UTC. If the date-time string 385 * included only a date and no time field, then the <code>allDay</code> 386 * field of this Time class is set to true and the <code>hour</code>, 387 * <code>minute</code>, and <code>second</code> fields are set to zero; 388 * otherwise (a time field was included in the date-time string) 389 * <code>allDay</code> is set to false. The fields <code>weekDay</code>, 390 * <code>yearDay</code>, and <code>gmtoff</code> are always set to zero, 391 * and the field <code>isDst</code> is set to -1 (unknown). To set those 392 * fields, call {@link #normalize(boolean)} after parsing. 393 * 394 * To parse a date-time string and convert it to UTC milliseconds, do 395 * something like this: 396 * 397 * <pre> 398 * Time time = new Time(); 399 * String date = "20081013T160000Z"; 400 * time.parse(date); 401 * long millis = time.normalize(false); 402 * </pre> 403 * 404 * @param s the string to parse 405 * @return true if the resulting time value is in UTC time 406 * @throws android.util.TimeFormatException if s cannot be parsed. 407 */ parse(String s)408 public boolean parse(String s) { 409 if (s == null) { 410 throw new NullPointerException("time string is null"); 411 } 412 if (parseInternal(s)) { 413 timezone = TIMEZONE_UTC; 414 return true; 415 } 416 return false; 417 } 418 419 /** 420 * Parse a time in the current zone in YYYYMMDDTHHMMSS format. 421 */ parseInternal(String s)422 private boolean parseInternal(String s) { 423 int len = s.length(); 424 if (len < 8) { 425 throw new TimeFormatException("String is too short: \"" + s + 426 "\" Expected at least 8 characters."); 427 } 428 429 boolean inUtc = false; 430 431 // year 432 int n = getChar(s, 0, 1000); 433 n += getChar(s, 1, 100); 434 n += getChar(s, 2, 10); 435 n += getChar(s, 3, 1); 436 year = n; 437 438 // month 439 n = getChar(s, 4, 10); 440 n += getChar(s, 5, 1); 441 n--; 442 month = n; 443 444 // day of month 445 n = getChar(s, 6, 10); 446 n += getChar(s, 7, 1); 447 monthDay = n; 448 449 if (len > 8) { 450 if (len < 15) { 451 throw new TimeFormatException( 452 "String is too short: \"" + s 453 + "\" If there are more than 8 characters there must be at least" 454 + " 15."); 455 } 456 checkChar(s, 8, 'T'); 457 allDay = false; 458 459 // hour 460 n = getChar(s, 9, 10); 461 n += getChar(s, 10, 1); 462 hour = n; 463 464 // min 465 n = getChar(s, 11, 10); 466 n += getChar(s, 12, 1); 467 minute = n; 468 469 // sec 470 n = getChar(s, 13, 10); 471 n += getChar(s, 14, 1); 472 second = n; 473 474 if (len > 15) { 475 // Z 476 checkChar(s, 15, 'Z'); 477 inUtc = true; 478 } 479 } else { 480 allDay = true; 481 hour = 0; 482 minute = 0; 483 second = 0; 484 } 485 486 weekDay = 0; 487 yearDay = 0; 488 isDst = -1; 489 gmtoff = 0; 490 return inUtc; 491 } 492 checkChar(String s, int spos, char expected)493 private void checkChar(String s, int spos, char expected) { 494 char c = s.charAt(spos); 495 if (c != expected) { 496 throw new TimeFormatException(String.format( 497 "Unexpected character 0x%02d at pos=%d. Expected 0x%02d (\'%c\').", 498 (int) c, spos, (int) expected, expected)); 499 } 500 } 501 getChar(String s, int spos, int mul)502 private static int getChar(String s, int spos, int mul) { 503 char c = s.charAt(spos); 504 if (Character.isDigit(c)) { 505 return Character.getNumericValue(c) * mul; 506 } else { 507 throw new TimeFormatException("Parse error at pos=" + spos); 508 } 509 } 510 511 /** 512 * Parse a time in RFC 3339 format. This method also parses simple dates 513 * (that is, strings that contain no time or time offset). For example, 514 * all of the following strings are valid: 515 * 516 * <ul> 517 * <li>"2008-10-13T16:00:00.000Z"</li> 518 * <li>"2008-10-13T16:00:00.000+07:00"</li> 519 * <li>"2008-10-13T16:00:00.000-07:00"</li> 520 * <li>"2008-10-13"</li> 521 * </ul> 522 * 523 * <p> 524 * If the string contains a time and time offset, then the time offset will 525 * be used to convert the time value to UTC. 526 * </p> 527 * 528 * <p> 529 * If the given string contains just a date (with no time field), then 530 * the {@link #allDay} field is set to true and the {@link #hour}, 531 * {@link #minute}, and {@link #second} fields are set to zero. 532 * </p> 533 * 534 * <p> 535 * Returns true if the resulting time value is in UTC time. 536 * </p> 537 * 538 * @param s the string to parse 539 * @return true if the resulting time value is in UTC time 540 * @throws android.util.TimeFormatException if s cannot be parsed. 541 */ parse3339(String s)542 public boolean parse3339(String s) { 543 if (s == null) { 544 throw new NullPointerException("time string is null"); 545 } 546 if (parse3339Internal(s)) { 547 timezone = TIMEZONE_UTC; 548 return true; 549 } 550 return false; 551 } 552 parse3339Internal(String s)553 private boolean parse3339Internal(String s) { 554 int len = s.length(); 555 if (len < 10) { 556 throw new TimeFormatException("String too short --- expected at least 10 characters."); 557 } 558 boolean inUtc = false; 559 560 // year 561 int n = getChar(s, 0, 1000); 562 n += getChar(s, 1, 100); 563 n += getChar(s, 2, 10); 564 n += getChar(s, 3, 1); 565 year = n; 566 567 checkChar(s, 4, '-'); 568 569 // month 570 n = getChar(s, 5, 10); 571 n += getChar(s, 6, 1); 572 --n; 573 month = n; 574 575 checkChar(s, 7, '-'); 576 577 // day 578 n = getChar(s, 8, 10); 579 n += getChar(s, 9, 1); 580 monthDay = n; 581 582 if (len >= 19) { 583 // T 584 checkChar(s, 10, 'T'); 585 allDay = false; 586 587 // hour 588 n = getChar(s, 11, 10); 589 n += getChar(s, 12, 1); 590 591 // Note that this.hour is not set here. It is set later. 592 int hour = n; 593 594 checkChar(s, 13, ':'); 595 596 // minute 597 n = getChar(s, 14, 10); 598 n += getChar(s, 15, 1); 599 // Note that this.minute is not set here. It is set later. 600 int minute = n; 601 602 checkChar(s, 16, ':'); 603 604 // second 605 n = getChar(s, 17, 10); 606 n += getChar(s, 18, 1); 607 second = n; 608 609 // skip the '.XYZ' -- we don't care about subsecond precision. 610 611 int tzIndex = 19; 612 if (tzIndex < len && s.charAt(tzIndex) == '.') { 613 do { 614 tzIndex++; 615 } while (tzIndex < len && Character.isDigit(s.charAt(tzIndex))); 616 } 617 618 int offset = 0; 619 if (len > tzIndex) { 620 char c = s.charAt(tzIndex); 621 // NOTE: the offset is meant to be subtracted to get from local time 622 // to UTC. we therefore use 1 for '-' and -1 for '+'. 623 switch (c) { 624 case 'Z': 625 // Zulu time -- UTC 626 offset = 0; 627 break; 628 case '-': 629 offset = 1; 630 break; 631 case '+': 632 offset = -1; 633 break; 634 default: 635 throw new TimeFormatException(String.format( 636 "Unexpected character 0x%02d at position %d. Expected + or -", 637 (int) c, tzIndex)); 638 } 639 inUtc = true; 640 641 if (offset != 0) { 642 if (len < tzIndex + 6) { 643 throw new TimeFormatException( 644 String.format("Unexpected length; should be %d characters", 645 tzIndex + 6)); 646 } 647 648 // hour 649 n = getChar(s, tzIndex + 1, 10); 650 n += getChar(s, tzIndex + 2, 1); 651 n *= offset; 652 hour += n; 653 654 // minute 655 n = getChar(s, tzIndex + 4, 10); 656 n += getChar(s, tzIndex + 5, 1); 657 n *= offset; 658 minute += n; 659 } 660 } 661 this.hour = hour; 662 this.minute = minute; 663 664 if (offset != 0) { 665 normalize(false); 666 } 667 } else { 668 allDay = true; 669 this.hour = 0; 670 this.minute = 0; 671 this.second = 0; 672 } 673 674 this.weekDay = 0; 675 this.yearDay = 0; 676 this.isDst = -1; 677 this.gmtoff = 0; 678 return inUtc; 679 } 680 681 /** 682 * Returns the timezone string that is currently set for the device. 683 */ getCurrentTimezone()684 public static String getCurrentTimezone() { 685 return TimeZone.getDefault().getID(); 686 } 687 688 /** 689 * Sets the time of the given Time object to the current time. 690 */ setToNow()691 public void setToNow() { 692 set(System.currentTimeMillis()); 693 } 694 695 /** 696 * Converts this time to milliseconds. Suitable for interacting with the 697 * standard java libraries. The time is in UTC milliseconds since the epoch. 698 * This does an implicit normalization to compute the milliseconds but does 699 * <em>not</em> change any of the fields in this Time object. If you want 700 * to normalize the fields in this Time object and also get the milliseconds 701 * then use {@link #normalize(boolean)}. 702 * 703 * <p> 704 * If "ignoreDst" is false, then this method uses the current setting of the 705 * "isDst" field and will adjust the returned time if the "isDst" field is 706 * wrong for the given time. See the sample code below for an example of 707 * this. 708 * 709 * <p> 710 * If "ignoreDst" is true, then this method ignores the current setting of 711 * the "isDst" field in this Time object and will instead figure out the 712 * correct value of "isDst" (as best it can) from the fields in this 713 * Time object. The only case where this method cannot figure out the 714 * correct value of the "isDst" field is when the time is inherently 715 * ambiguous because it falls in the hour that is repeated when switching 716 * from Daylight-Saving Time to Standard Time. 717 * 718 * <p> 719 * Here is an example where <tt>toMillis(true)</tt> adjusts the time, 720 * assuming that DST changes at 2am on Sunday, Nov 4, 2007. 721 * 722 * <pre> 723 * Time time = new Time(); 724 * time.set(4, 10, 2007); // set the date to Nov 4, 2007, 12am 725 * time.normalize(false); // this sets isDst = 1 726 * time.monthDay += 1; // changes the date to Nov 5, 2007, 12am 727 * millis = time.toMillis(false); // millis is Nov 4, 2007, 11pm 728 * millis = time.toMillis(true); // millis is Nov 5, 2007, 12am 729 * </pre> 730 * 731 * <p> 732 * To avoid this problem, use <tt>toMillis(true)</tt> 733 * after adding or subtracting days or explicitly setting the "monthDay" 734 * field. On the other hand, if you are adding 735 * or subtracting hours or minutes, then you should use 736 * <tt>toMillis(false)</tt>. 737 * 738 * <p> 739 * You should also use <tt>toMillis(false)</tt> if you want 740 * to read back the same milliseconds that you set with {@link #set(long)} 741 * or {@link #set(Time)} or after parsing a date string. 742 * 743 * <p> 744 * This method can return {@code -1} when the date / time fields have been 745 * set to a local time that conflicts with available timezone information. 746 * For example, when daylight savings transitions cause an hour to be 747 * skipped: times within that hour will return {@code -1} if isDst = 748 * {@code -1}. 749 */ toMillis(boolean ignoreDst)750 public long toMillis(boolean ignoreDst) { 751 calculator.copyFieldsFromTime(this); 752 return calculator.toMillis(ignoreDst); 753 } 754 755 /** 756 * Sets the fields in this Time object given the UTC milliseconds. After 757 * this method returns, all the fields are normalized. 758 * This also sets the "isDst" field to the correct value. 759 * 760 * @param millis the time in UTC milliseconds since the epoch. 761 */ set(long millis)762 public void set(long millis) { 763 allDay = false; 764 calculator.timezone = timezone; 765 calculator.setTimeInMillis(millis); 766 calculator.copyFieldsToTime(this); 767 } 768 769 /** 770 * Format according to RFC 2445 DATE-TIME type. 771 * 772 * <p>The same as format("%Y%m%dT%H%M%S"), or format("%Y%m%dT%H%M%SZ") for a Time with a 773 * timezone set to "UTC". 774 */ format2445()775 public String format2445() { 776 calculator.copyFieldsFromTime(this); 777 return calculator.format2445(!allDay); 778 } 779 780 /** 781 * Copy the value of that to this Time object. No normalization happens. 782 */ set(Time that)783 public void set(Time that) { 784 this.timezone = that.timezone; 785 this.allDay = that.allDay; 786 this.second = that.second; 787 this.minute = that.minute; 788 this.hour = that.hour; 789 this.monthDay = that.monthDay; 790 this.month = that.month; 791 this.year = that.year; 792 this.weekDay = that.weekDay; 793 this.yearDay = that.yearDay; 794 this.isDst = that.isDst; 795 this.gmtoff = that.gmtoff; 796 } 797 798 /** 799 * Sets the fields. Sets weekDay, yearDay and gmtoff to 0, and isDst to -1. 800 * Call {@link #normalize(boolean)} if you need those. 801 */ set(int second, int minute, int hour, int monthDay, int month, int year)802 public void set(int second, int minute, int hour, int monthDay, int month, int year) { 803 this.allDay = false; 804 this.second = second; 805 this.minute = minute; 806 this.hour = hour; 807 this.monthDay = monthDay; 808 this.month = month; 809 this.year = year; 810 this.weekDay = 0; 811 this.yearDay = 0; 812 this.isDst = -1; 813 this.gmtoff = 0; 814 } 815 816 /** 817 * Sets the date from the given fields. Also sets allDay to true. 818 * Sets weekDay, yearDay and gmtoff to 0, and isDst to -1. 819 * Call {@link #normalize(boolean)} if you need those. 820 * 821 * @param monthDay the day of the month (in the range [1,31]) 822 * @param month the zero-based month number (in the range [0,11]) 823 * @param year the year 824 */ set(int monthDay, int month, int year)825 public void set(int monthDay, int month, int year) { 826 this.allDay = true; 827 this.second = 0; 828 this.minute = 0; 829 this.hour = 0; 830 this.monthDay = monthDay; 831 this.month = month; 832 this.year = year; 833 this.weekDay = 0; 834 this.yearDay = 0; 835 this.isDst = -1; 836 this.gmtoff = 0; 837 } 838 839 /** 840 * Returns true if the time represented by this Time object occurs before 841 * the given time. 842 * 843 * <p> 844 * Equivalent to {@code Time.compare(this, that) < 0}. See 845 * {@link #compare(Time, Time)} for details. 846 * 847 * @param that a given Time object to compare against 848 * @return true if this time is less than the given time 849 */ before(Time that)850 public boolean before(Time that) { 851 return Time.compare(this, that) < 0; 852 } 853 854 855 /** 856 * Returns true if the time represented by this Time object occurs after 857 * the given time. 858 * 859 * <p> 860 * Equivalent to {@code Time.compare(this, that) > 0}. See 861 * {@link #compare(Time, Time)} for details. 862 * 863 * @param that a given Time object to compare against 864 * @return true if this time is greater than the given time 865 */ after(Time that)866 public boolean after(Time that) { 867 return Time.compare(this, that) > 0; 868 } 869 870 /** 871 * This array is indexed by the weekDay field (SUNDAY=0, MONDAY=1, etc.) 872 * and gives a number that can be added to the yearDay to give the 873 * closest Thursday yearDay. 874 */ 875 private static final int[] sThursdayOffset = { -3, 3, 2, 1, 0, -1, -2 }; 876 877 /** 878 * Computes the week number according to ISO 8601. The current Time 879 * object must already be normalized because this method uses the 880 * yearDay and weekDay fields. 881 * 882 * <p> 883 * In IS0 8601, weeks start on Monday. 884 * The first week of the year (week 1) is defined by ISO 8601 as the 885 * first week with four or more of its days in the starting year. 886 * Or equivalently, the week containing January 4. Or equivalently, 887 * the week with the year's first Thursday in it. 888 * </p> 889 * 890 * <p> 891 * The week number can be calculated by counting Thursdays. Week N 892 * contains the Nth Thursday of the year. 893 * </p> 894 * 895 * @return the ISO week number. 896 */ getWeekNumber()897 public int getWeekNumber() { 898 // Get the year day for the closest Thursday 899 int closestThursday = yearDay + sThursdayOffset[weekDay]; 900 901 // Year days start at 0 902 if (closestThursday >= 0 && closestThursday <= 364) { 903 return closestThursday / 7 + 1; 904 } 905 906 // The week crosses a year boundary. 907 Time temp = new Time(this); 908 temp.monthDay += sThursdayOffset[weekDay]; 909 temp.normalize(true /* ignore isDst */); 910 return temp.yearDay / 7 + 1; 911 } 912 913 /** 914 * Return a string in the RFC 3339 format. 915 * <p> 916 * If allDay is true, expresses the time as Y-M-D</p> 917 * <p> 918 * Otherwise, if the timezone is UTC, expresses the time as Y-M-D-T-H-M-S UTC</p> 919 * <p> 920 * Otherwise the time is expressed the time as Y-M-D-T-H-M-S +- GMT</p> 921 * @return string in the RFC 3339 format. 922 */ format3339(boolean allDay)923 public String format3339(boolean allDay) { 924 if (allDay) { 925 return format(Y_M_D); 926 } else if (TIMEZONE_UTC.equals(timezone)) { 927 return format(Y_M_D_T_H_M_S_000_Z); 928 } else { 929 String base = format(Y_M_D_T_H_M_S_000); 930 String sign = (gmtoff < 0) ? "-" : "+"; 931 int offset = (int) Math.abs(gmtoff); 932 int minutes = (offset % 3600) / 60; 933 int hours = offset / 3600; 934 935 return String.format(Locale.US, "%s%s%02d:%02d", base, sign, hours, minutes); 936 } 937 } 938 939 /** 940 * Returns true if the instant of the supplied time would be for the 941 * Gregorian calendar date January 1, 1970 <em>for a user observing UTC 942 * </em>, i.e. the timezone of the time object is ignored. 943 * <p> 944 * See {@link #getJulianDay(long, long)} for how to determine the Julian day 945 * for the timezone of the time object. 946 * <p> 947 * This method can return an incorrect answer when the date / time fields have 948 * been set to a local time that contradicts the available timezone information. 949 * 950 * @param time the time to test 951 * @return true if epoch. 952 */ isEpoch(Time time)953 public static boolean isEpoch(Time time) { 954 long millis = time.toMillis(true); 955 return getJulianDay(millis, 0 /* UTC offset */) == EPOCH_JULIAN_DAY; 956 } 957 958 /** 959 * Computes the Julian day number for a point in time in a particular 960 * timezone. The Julian day for a given calendar date is the same for 961 * every timezone. For example, the Julian day for July 1, 2008 is 962 * 2454649. 963 * 964 * <p>Callers must pass the time in UTC millisecond (as can be returned 965 * by {@link #toMillis(boolean)} or {@link #normalize(boolean)}) 966 * and the offset from UTC of the timezone in seconds at that time (as 967 * might be in {@link #gmtoff}). 968 * 969 * <p>The Julian day is useful for testing if two events occur on the 970 * same calendar date and for determining the relative time of an event 971 * from the present ("yesterday", "3 days ago", etc.). 972 * 973 * @param millis the time in UTC milliseconds 974 * @param gmtoffSeconds the offset from UTC in seconds 975 * @return the Julian day 976 * @deprecated Use {@link java.time.temporal.JulianFields#JULIAN_DAY} instead. 977 */ 978 @Deprecated getJulianDay(long millis, long gmtoffSeconds)979 public static int getJulianDay(long millis, long gmtoffSeconds) { 980 long offsetMillis = gmtoffSeconds * 1000; 981 long adjustedMillis = millis + offsetMillis; 982 long julianDay = adjustedMillis / DateUtils.DAY_IN_MILLIS; 983 // Negative adjustedMillis values must round towards Integer.MIN_VALUE. 984 if (adjustedMillis < 0 && adjustedMillis % DateUtils.DAY_IN_MILLIS != 0) { 985 julianDay--; 986 } 987 return (int) (julianDay + EPOCH_JULIAN_DAY); 988 } 989 990 /** 991 * <p>Sets the time from the given Julian day number, which must be based on 992 * the same timezone that is set in this Time object. The "gmtoff" field 993 * need not be initialized because the given Julian day may have a different 994 * GMT offset than whatever is currently stored in this Time object anyway. 995 * After this method returns all the fields will be normalized and the time 996 * will be set to 12am at the beginning of the given Julian day. 997 * </p> 998 * 999 * <p> 1000 * The only exception to this is if 12am does not exist for that day because 1001 * of daylight saving time. For example, Cairo, Eqypt moves time ahead one 1002 * hour at 12am on April 25, 2008 and there are a few other places that 1003 * also change daylight saving time at 12am. In those cases, the time 1004 * will be set to 1am. 1005 * </p> 1006 * 1007 * @param julianDay the Julian day in the timezone for this Time object 1008 * @return the UTC milliseconds for the beginning of the Julian day 1009 */ setJulianDay(int julianDay)1010 public long setJulianDay(int julianDay) { 1011 // Don't bother with the GMT offset since we don't know the correct 1012 // value for the given Julian day. Just get close and then adjust 1013 // the day. 1014 long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS; 1015 set(millis); 1016 1017 // Figure out how close we are to the requested Julian day. 1018 // We can't be off by more than a day. 1019 int approximateDay = getJulianDay(millis, gmtoff); 1020 int diff = julianDay - approximateDay; 1021 monthDay += diff; 1022 1023 // Set the time to 12am and re-normalize. 1024 hour = 0; 1025 minute = 0; 1026 second = 0; 1027 millis = normalize(true); 1028 return millis; 1029 } 1030 1031 /** 1032 * Returns the week since {@link #EPOCH_JULIAN_DAY} (Jan 1, 1970) adjusted 1033 * for first day of week. This takes a julian day and the week start day and 1034 * calculates which week since {@link #EPOCH_JULIAN_DAY} that day occurs in, 1035 * starting at 0. *Do not* use this to compute the ISO week number for the 1036 * year. 1037 * 1038 * @param julianDay The julian day to calculate the week number for 1039 * @param firstDayOfWeek Which week day is the first day of the week, see 1040 * {@link #SUNDAY} 1041 * @return Weeks since the epoch 1042 */ getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek)1043 public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) { 1044 int diff = THURSDAY - firstDayOfWeek; 1045 if (diff < 0) { 1046 diff += 7; 1047 } 1048 int refDay = EPOCH_JULIAN_DAY - diff; 1049 return (julianDay - refDay) / 7; 1050 } 1051 1052 /** 1053 * Takes a number of weeks since the epoch and calculates the Julian day of 1054 * the Monday for that week. This assumes that the week containing the 1055 * {@link #EPOCH_JULIAN_DAY} is considered week 0. It returns the Julian day 1056 * for the Monday week weeks after the Monday of the week containing the 1057 * epoch. 1058 * 1059 * @param week Number of weeks since the epoch 1060 * @return The julian day for the Monday of the given week since the epoch 1061 */ getJulianMondayFromWeeksSinceEpoch(int week)1062 public static int getJulianMondayFromWeeksSinceEpoch(int week) { 1063 return MONDAY_BEFORE_JULIAN_EPOCH + week * 7; 1064 } 1065 1066 /** 1067 * A class that handles date/time calculations. 1068 * 1069 * This class originated as a port of a native C++ class ("android.Time") to pure Java. It is 1070 * separate from the enclosing class because some methods copy the result of calculations back 1071 * to the enclosing object, but others do not: thus separate state is retained. 1072 */ 1073 private static class TimeCalculator { 1074 public final WallTime wallTime; 1075 public String timezone; 1076 1077 // Information about the current timezone. 1078 private ZoneInfoData mZoneInfoData; 1079 TimeCalculator(String timezoneId)1080 public TimeCalculator(String timezoneId) { 1081 this.mZoneInfoData = lookupZoneInfoData(timezoneId); 1082 this.wallTime = new WallTime(); 1083 } 1084 toMillis(boolean ignoreDst)1085 public long toMillis(boolean ignoreDst) { 1086 if (ignoreDst) { 1087 wallTime.setIsDst(-1); 1088 } 1089 1090 int r = wallTime.mktime(mZoneInfoData); 1091 if (r == -1) { 1092 return -1; 1093 } 1094 return r * 1000L; 1095 } 1096 setTimeInMillis(long millis)1097 public void setTimeInMillis(long millis) { 1098 // Preserve old 32-bit Android behavior. 1099 int intSeconds = (int) (millis / 1000); 1100 1101 updateZoneInfoFromTimeZone(); 1102 wallTime.localtime(intSeconds, mZoneInfoData); 1103 } 1104 format(String format)1105 public String format(String format) { 1106 if (format == null) { 1107 format = "%c"; 1108 } 1109 TimeFormatter formatter = new TimeFormatter(); 1110 return formatter.format(format, wallTime, mZoneInfoData); 1111 } 1112 updateZoneInfoFromTimeZone()1113 private void updateZoneInfoFromTimeZone() { 1114 if (!mZoneInfoData.getID().equals(timezone)) { 1115 this.mZoneInfoData = lookupZoneInfoData(timezone); 1116 } 1117 } 1118 lookupZoneInfoData(String timezoneId)1119 private static ZoneInfoData lookupZoneInfoData(String timezoneId) { 1120 ZoneInfoData zoneInfoData = ZoneInfoDb.getInstance().makeZoneInfoData(timezoneId); 1121 if (zoneInfoData == null) { 1122 zoneInfoData = ZoneInfoDb.getInstance().makeZoneInfoData("GMT"); 1123 } 1124 if (zoneInfoData == null) { 1125 throw new AssertionError("GMT not found: \"" + timezoneId + "\""); 1126 } 1127 return zoneInfoData; 1128 } 1129 switchTimeZone(String timezone)1130 public void switchTimeZone(String timezone) { 1131 int seconds = wallTime.mktime(mZoneInfoData); 1132 this.timezone = timezone; 1133 updateZoneInfoFromTimeZone(); 1134 wallTime.localtime(seconds, mZoneInfoData); 1135 } 1136 format2445(boolean hasTime)1137 public String format2445(boolean hasTime) { 1138 char[] buf = new char[hasTime ? 16 : 8]; 1139 int n = wallTime.getYear(); 1140 1141 buf[0] = toChar(n / 1000); 1142 n %= 1000; 1143 buf[1] = toChar(n / 100); 1144 n %= 100; 1145 buf[2] = toChar(n / 10); 1146 n %= 10; 1147 buf[3] = toChar(n); 1148 1149 n = wallTime.getMonth() + 1; 1150 buf[4] = toChar(n / 10); 1151 buf[5] = toChar(n % 10); 1152 1153 n = wallTime.getMonthDay(); 1154 buf[6] = toChar(n / 10); 1155 buf[7] = toChar(n % 10); 1156 1157 if (!hasTime) { 1158 return new String(buf, 0, 8); 1159 } 1160 1161 buf[8] = 'T'; 1162 1163 n = wallTime.getHour(); 1164 buf[9] = toChar(n / 10); 1165 buf[10] = toChar(n % 10); 1166 1167 n = wallTime.getMinute(); 1168 buf[11] = toChar(n / 10); 1169 buf[12] = toChar(n % 10); 1170 1171 n = wallTime.getSecond(); 1172 buf[13] = toChar(n / 10); 1173 buf[14] = toChar(n % 10); 1174 1175 if (TIMEZONE_UTC.equals(timezone)) { 1176 // The letter 'Z' is appended to the end. 1177 buf[15] = 'Z'; 1178 return new String(buf, 0, 16); 1179 } else { 1180 return new String(buf, 0, 15); 1181 } 1182 } 1183 toChar(int n)1184 private char toChar(int n) { 1185 return (n >= 0 && n <= 9) ? (char) (n + '0') : ' '; 1186 } 1187 1188 /** 1189 * A method that will return the state of this object in string form. Note: it has side 1190 * effects and so has deliberately not been made the default {@link #toString()}. 1191 */ toStringInternal()1192 public String toStringInternal() { 1193 // This implementation possibly displays the un-normalized fields because that is 1194 // what it has always done. 1195 return String.format("%04d%02d%02dT%02d%02d%02d%s(%d,%d,%d,%d,%d)", 1196 wallTime.getYear(), 1197 wallTime.getMonth() + 1, 1198 wallTime.getMonthDay(), 1199 wallTime.getHour(), 1200 wallTime.getMinute(), 1201 wallTime.getSecond(), 1202 timezone, 1203 wallTime.getWeekDay(), 1204 wallTime.getYearDay(), 1205 wallTime.getGmtOffset(), 1206 wallTime.getIsDst(), 1207 toMillis(false /* use isDst */) / 1000 1208 ); 1209 1210 } 1211 compare(TimeCalculator aObject, TimeCalculator bObject)1212 public static int compare(TimeCalculator aObject, TimeCalculator bObject) { 1213 if (aObject.timezone.equals(bObject.timezone)) { 1214 // If the timezones are the same, we can easily compare the two times. 1215 int diff = aObject.wallTime.getYear() - bObject.wallTime.getYear(); 1216 if (diff != 0) { 1217 return diff; 1218 } 1219 1220 diff = aObject.wallTime.getMonth() - bObject.wallTime.getMonth(); 1221 if (diff != 0) { 1222 return diff; 1223 } 1224 1225 diff = aObject.wallTime.getMonthDay() - bObject.wallTime.getMonthDay(); 1226 if (diff != 0) { 1227 return diff; 1228 } 1229 1230 diff = aObject.wallTime.getHour() - bObject.wallTime.getHour(); 1231 if (diff != 0) { 1232 return diff; 1233 } 1234 1235 diff = aObject.wallTime.getMinute() - bObject.wallTime.getMinute(); 1236 if (diff != 0) { 1237 return diff; 1238 } 1239 1240 diff = aObject.wallTime.getSecond() - bObject.wallTime.getSecond(); 1241 if (diff != 0) { 1242 return diff; 1243 } 1244 1245 return 0; 1246 } else { 1247 // Otherwise, convert to milliseconds and compare that. This requires that object be 1248 // normalized. Note: For dates that do not exist: toMillis() can return -1, which 1249 // can be confused with a valid time. 1250 long am = aObject.toMillis(false /* use isDst */); 1251 long bm = bObject.toMillis(false /* use isDst */); 1252 long diff = am - bm; 1253 return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0); 1254 } 1255 1256 } 1257 copyFieldsToTime(Time time)1258 public void copyFieldsToTime(Time time) { 1259 time.second = wallTime.getSecond(); 1260 time.minute = wallTime.getMinute(); 1261 time.hour = wallTime.getHour(); 1262 time.monthDay = wallTime.getMonthDay(); 1263 time.month = wallTime.getMonth(); 1264 time.year = wallTime.getYear(); 1265 1266 // Read-only fields that are derived from other information above. 1267 time.weekDay = wallTime.getWeekDay(); 1268 time.yearDay = wallTime.getYearDay(); 1269 1270 // < 0: DST status unknown, 0: is not in DST, 1: is in DST 1271 time.isDst = wallTime.getIsDst(); 1272 // This is in seconds and includes any DST offset too. 1273 time.gmtoff = wallTime.getGmtOffset(); 1274 } 1275 copyFieldsFromTime(Time time)1276 public void copyFieldsFromTime(Time time) { 1277 wallTime.setSecond(time.second); 1278 wallTime.setMinute(time.minute); 1279 wallTime.setHour(time.hour); 1280 wallTime.setMonthDay(time.monthDay); 1281 wallTime.setMonth(time.month); 1282 wallTime.setYear(time.year); 1283 wallTime.setWeekDay(time.weekDay); 1284 wallTime.setYearDay(time.yearDay); 1285 wallTime.setIsDst(time.isDst); 1286 wallTime.setGmtOffset((int) time.gmtoff); 1287 1288 if (time.allDay && (time.second != 0 || time.minute != 0 || time.hour != 0)) { 1289 throw new IllegalArgumentException("allDay is true but sec, min, hour are not 0."); 1290 } 1291 1292 timezone = time.timezone; 1293 updateZoneInfoFromTimeZone(); 1294 } 1295 } 1296 } 1297