1 /* 2 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 /* 27 * Copyright (c) 2012, Stephen Colebourne & Michael Nascimento Santos 28 * 29 * All rights reserved. 30 * 31 * Redistribution and use in source and binary forms, with or without 32 * modification, are permitted provided that the following conditions are met: 33 * 34 * * Redistributions of source code must retain the above copyright notice, 35 * this list of conditions and the following disclaimer. 36 * 37 * * Redistributions in binary form must reproduce the above copyright notice, 38 * this list of conditions and the following disclaimer in the documentation 39 * and/or other materials provided with the distribution. 40 * 41 * * Neither the name of JSR-310 nor the names of its contributors 42 * may be used to endorse or promote products derived from this software 43 * without specific prior written permission. 44 * 45 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 46 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 47 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 48 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 49 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 50 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 51 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 52 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 53 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 54 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 55 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 56 */ 57 58 package java.time.chrono; 59 60 import static java.time.temporal.ChronoField.EPOCH_DAY; 61 62 import java.io.File; 63 import java.io.FileInputStream; 64 import java.io.IOException; 65 import java.io.InputStream; 66 import java.io.Serializable; 67 import java.security.AccessController; 68 import java.security.PrivilegedActionException; 69 import java.time.Clock; 70 import java.time.DateTimeException; 71 import java.time.Instant; 72 import java.time.LocalDate; 73 import java.time.ZoneId; 74 import java.time.format.ResolverStyle; 75 import java.time.temporal.ChronoField; 76 import java.time.temporal.TemporalAccessor; 77 import java.time.temporal.TemporalField; 78 import java.time.temporal.ValueRange; 79 import java.util.Arrays; 80 import java.util.HashMap; 81 import java.util.List; 82 import java.util.Map; 83 import java.util.Objects; 84 import java.util.Properties; 85 86 import sun.util.logging.PlatformLogger; 87 88 /** 89 * The Hijrah calendar is a lunar calendar supporting Islamic calendars. 90 * <p> 91 * The HijrahChronology follows the rules of the Hijrah calendar system. The Hijrah 92 * calendar has several variants based on differences in when the new moon is 93 * determined to have occurred and where the observation is made. 94 * In some variants the length of each month is 95 * computed algorithmically from the astronomical data for the moon and earth and 96 * in others the length of the month is determined by an authorized sighting 97 * of the new moon. For the algorithmically based calendars the calendar 98 * can project into the future. 99 * For sighting based calendars only historical data from past 100 * sightings is available. 101 * <p> 102 * The length of each month is 29 or 30 days. 103 * Ordinary years have 354 days; leap years have 355 days. 104 * 105 * <p> 106 * CLDR and LDML identify variants: 107 * <table cellpadding="2" summary="Variants of Hijrah Calendars"> 108 * <thead> 109 * <tr class="tableSubHeadingColor"> 110 * <th class="colFirst" align="left" >Chronology ID</th> 111 * <th class="colFirst" align="left" >Calendar Type</th> 112 * <th class="colFirst" align="left" >Locale extension, see {@link java.util.Locale}</th> 113 * <th class="colLast" align="left" >Description</th> 114 * </tr> 115 * </thead> 116 * <tbody> 117 * <tr class="altColor"> 118 * <td>Hijrah-umalqura</td> 119 * <td>islamic-umalqura</td> 120 * <td>ca-islamic-umalqura</td> 121 * <td>Islamic - Umm Al-Qura calendar of Saudi Arabia</td> 122 * </tr> 123 * </tbody> 124 * </table> 125 * <p>Additional variants may be available through {@link Chronology#getAvailableChronologies()}. 126 * 127 * <p>Example</p> 128 * <p> 129 * Selecting the chronology from the locale uses {@link Chronology#ofLocale} 130 * to find the Chronology based on Locale supported BCP 47 extension mechanism 131 * to request a specific calendar ("ca"). For example, 132 * </p> 133 * <pre> 134 * Locale locale = Locale.forLanguageTag("en-US-u-ca-islamic-umalqura"); 135 * Chronology chrono = Chronology.ofLocale(locale); 136 * </pre> 137 * 138 * @implSpec 139 * This class is immutable and thread-safe. 140 * 141 * @implNote 142 * Each Hijrah variant is configured individually. Each variant is defined by a 143 * property resource that defines the {@code ID}, the {@code calendar type}, 144 * the start of the calendar, the alignment with the 145 * ISO calendar, and the length of each month for a range of years. 146 * The variants are identified in the {@code calendars.properties} file. 147 * The new properties are prefixed with {@code "calendars.hijrah."}: 148 * <table cellpadding="2" border="0" summary="Configuration of Hijrah Calendar Variants"> 149 * <thead> 150 * <tr class="tableSubHeadingColor"> 151 * <th class="colFirst" align="left">Property Name</th> 152 * <th class="colFirst" align="left">Property value</th> 153 * <th class="colLast" align="left">Description </th> 154 * </tr> 155 * </thead> 156 * <tbody> 157 * <tr class="altColor"> 158 * <td>calendars.hijrah.{ID}</td> 159 * <td>The property resource defining the {@code {ID}} variant</td> 160 * <td>The property resource is located with the {@code calendars.properties} file</td> 161 * </tr> 162 * <tr class="rowColor"> 163 * <td>calendars.hijrah.{ID}.type</td> 164 * <td>The calendar type</td> 165 * <td>LDML defines the calendar type names</td> 166 * </tr> 167 * </tbody> 168 * </table> 169 * <p> 170 * The Hijrah property resource is a set of properties that describe the calendar. 171 * The syntax is defined by {@code java.util.Properties#load(Reader)}. 172 * <table cellpadding="2" summary="Configuration of Hijrah Calendar"> 173 * <thead> 174 * <tr class="tableSubHeadingColor"> 175 * <th class="colFirst" align="left" > Property Name</th> 176 * <th class="colFirst" align="left" > Property value</th> 177 * <th class="colLast" align="left" > Description </th> 178 * </tr> 179 * </thead> 180 * <tbody> 181 * <tr class="altColor"> 182 * <td>id</td> 183 * <td>Chronology Id, for example, "Hijrah-umalqura"</td> 184 * <td>The Id of the calendar in common usage</td> 185 * </tr> 186 * <tr class="rowColor"> 187 * <td>type</td> 188 * <td>Calendar type, for example, "islamic-umalqura"</td> 189 * <td>LDML defines the calendar types</td> 190 * </tr> 191 * <tr class="altColor"> 192 * <td>version</td> 193 * <td>Version, for example: "1.8.0_1"</td> 194 * <td>The version of the Hijrah variant data</td> 195 * </tr> 196 * <tr class="rowColor"> 197 * <td>iso-start</td> 198 * <td>ISO start date, formatted as {@code yyyy-MM-dd}, for example: "1900-04-30"</td> 199 * <td>The ISO date of the first day of the minimum Hijrah year.</td> 200 * </tr> 201 * <tr class="altColor"> 202 * <td>yyyy - a numeric 4 digit year, for example "1434"</td> 203 * <td>The value is a sequence of 12 month lengths, 204 * for example: "29 30 29 30 29 30 30 30 29 30 29 29"</td> 205 * <td>The lengths of the 12 months of the year separated by whitespace. 206 * A numeric year property must be present for every year without any gaps. 207 * The month lengths must be between 29-32 inclusive. 208 * </td> 209 * </tr> 210 * </tbody> 211 * </table> 212 * 213 * @since 1.8 214 */ 215 public final class HijrahChronology extends Chronology implements Serializable { 216 217 /** 218 * The Hijrah Calendar id. 219 */ 220 private final String typeId; 221 /** 222 * The Hijrah calendarType. 223 */ 224 private transient final String calendarType; 225 /** 226 * Serialization version. 227 */ 228 private static final long serialVersionUID = 3127340209035924785L; 229 /** 230 * Singleton instance of the Islamic Umm Al-Qura calendar of Saudi Arabia. 231 * Other Hijrah chronology variants may be available from 232 * {@link Chronology#getAvailableChronologies}. 233 */ 234 public static final HijrahChronology INSTANCE; 235 /** 236 * Flag to indicate the initialization of configuration data is complete. 237 * @see #checkCalendarInit() 238 */ 239 private volatile boolean initComplete; 240 /** 241 * Array of epoch days indexed by Hijrah Epoch month. 242 * Computed by {@link #loadCalendarData}. 243 */ 244 private transient int[] hijrahEpochMonthStartDays; 245 /** 246 * The minimum epoch day of this Hijrah calendar. 247 * Computed by {@link #loadCalendarData}. 248 */ 249 private transient int minEpochDay; 250 /** 251 * The maximum epoch day for which calendar data is available. 252 * Computed by {@link #loadCalendarData}. 253 */ 254 private transient int maxEpochDay; 255 /** 256 * The minimum epoch month. 257 * Computed by {@link #loadCalendarData}. 258 */ 259 private transient int hijrahStartEpochMonth; 260 /** 261 * The minimum length of a month. 262 * Computed by {@link #createEpochMonths}. 263 */ 264 private transient int minMonthLength; 265 /** 266 * The maximum length of a month. 267 * Computed by {@link #createEpochMonths}. 268 */ 269 private transient int maxMonthLength; 270 /** 271 * The minimum length of a year in days. 272 * Computed by {@link #createEpochMonths}. 273 */ 274 private transient int minYearLength; 275 /** 276 * The maximum length of a year in days. 277 * Computed by {@link #createEpochMonths}. 278 */ 279 private transient int maxYearLength; 280 /** 281 * A reference to the properties stored in 282 * ${java.home}/lib/calendars.properties 283 */ 284 private transient final static Properties calendarProperties; 285 286 /** 287 * Prefix of property names for Hijrah calendar variants. 288 */ 289 private static final String PROP_PREFIX = "calendar.hijrah."; 290 /** 291 * Suffix of property names containing the calendar type of a variant. 292 */ 293 private static final String PROP_TYPE_SUFFIX = ".type"; 294 295 /** 296 * Static initialization of the predefined calendars found in the 297 * lib/calendars.properties file. 298 */ 299 static { 300 try { 301 calendarProperties = sun.util.calendar.BaseCalendar.getCalendarProperties(); 302 } catch (IOException ioe) { 303 throw new InternalError("Can't initialize lib/calendars.properties", ioe); 304 } 305 306 try { 307 INSTANCE = new HijrahChronology("Hijrah-umalqura"); 308 // Register it by its aliases 309 Chronology.registerChrono(INSTANCE, "Hijrah"); 310 Chronology.registerChrono(INSTANCE, "islamic"); 311 } catch (DateTimeException ex) { 312 // Absence of Hijrah calendar is fatal to initializing this class. 313 PlatformLogger logger = PlatformLogger.getLogger("java.time.chrono"); 314 logger.severe("Unable to initialize Hijrah calendar: Hijrah-umalqura", ex); 315 throw new RuntimeException("Unable to initialize Hijrah-umalqura calendar", ex.getCause()); 316 } 317 registerVariants(); 318 } 319 320 /** 321 * For each Hijrah variant listed, create the HijrahChronology and register it. 322 * Exceptions during initialization are logged but otherwise ignored. 323 */ 324 private static void registerVariants() { 325 for (String name : calendarProperties.stringPropertyNames()) { 326 if (name.startsWith(PROP_PREFIX)) { 327 String id = name.substring(PROP_PREFIX.length()); 328 if (id.indexOf('.') >= 0) { 329 continue; // no name or not a simple name of a calendar 330 } 331 if (id.equals(INSTANCE.getId())) { 332 continue; // do not duplicate the default 333 } 334 try { 335 // Create and register the variant 336 HijrahChronology chrono = new HijrahChronology(id); 337 Chronology.registerChrono(chrono); 338 } catch (DateTimeException ex) { 339 // Log error and continue 340 PlatformLogger logger = PlatformLogger.getLogger("java.time.chrono"); 341 logger.severe("Unable to initialize Hijrah calendar: " + id, ex); 342 } 343 } 344 } 345 } 346 347 /** 348 * Create a HijrahChronology for the named variant. 349 * The resource and calendar type are retrieved from properties 350 * in the {@code calendars.properties}. 351 * The property names are {@code "calendar.hijrah." + id} 352 * and {@code "calendar.hijrah." + id + ".type"} 353 * @param id the id of the calendar 354 * @throws DateTimeException if the calendar type is missing from the properties file. 355 * @throws IllegalArgumentException if the id is empty 356 */ 357 private HijrahChronology(String id) throws DateTimeException { 358 if (id.isEmpty()) { 359 throw new IllegalArgumentException("calendar id is empty"); 360 } 361 String propName = PROP_PREFIX + id + PROP_TYPE_SUFFIX; 362 String calType = calendarProperties.getProperty(propName); 363 if (calType == null || calType.isEmpty()) { 364 throw new DateTimeException("calendarType is missing or empty for: " + propName); 365 } 366 this.typeId = id; 367 this.calendarType = calType; 368 } 369 370 /** 371 * Check and ensure that the calendar data has been initialized. 372 * The initialization check is performed at the boundary between 373 * public and package methods. If a public calls another public method 374 * a check is not necessary in the caller. 375 * The constructors of HijrahDate call {@link #getEpochDay} or 376 * {@link #getHijrahDateInfo} so every call from HijrahDate to a 377 * HijrahChronology via package private methods has been checked. 378 * 379 * @throws DateTimeException if the calendar data configuration is 380 * malformed or IOExceptions occur loading the data 381 */ 382 private void checkCalendarInit() { 383 // Keep this short so it can be inlined for performance 384 if (initComplete == false) { 385 loadCalendarData(); 386 initComplete = true; 387 } 388 } 389 390 //----------------------------------------------------------------------- 391 /** 392 * Gets the ID of the chronology. 393 * <p> 394 * The ID uniquely identifies the {@code Chronology}. It can be used to 395 * lookup the {@code Chronology} using {@link #of(String)}. 396 * 397 * @return the chronology ID, non-null 398 * @see #getCalendarType() 399 */ 400 @Override 401 public String getId() { 402 return typeId; 403 } 404 405 /** 406 * Gets the calendar type of the Islamic calendar. 407 * <p> 408 * The calendar type is an identifier defined by the 409 * <em>Unicode Locale Data Markup Language (LDML)</em> specification. 410 * It can be used to lookup the {@code Chronology} using {@link #of(String)}. 411 * 412 * @return the calendar system type; non-null if the calendar has 413 * a standard type, otherwise null 414 * @see #getId() 415 */ 416 @Override 417 public String getCalendarType() { 418 return calendarType; 419 } 420 421 //----------------------------------------------------------------------- 422 /** 423 * Obtains a local date in Hijrah calendar system from the 424 * era, year-of-era, month-of-year and day-of-month fields. 425 * 426 * @param era the Hijrah era, not null 427 * @param yearOfEra the year-of-era 428 * @param month the month-of-year 429 * @param dayOfMonth the day-of-month 430 * @return the Hijrah local date, not null 431 * @throws DateTimeException if unable to create the date 432 * @throws ClassCastException if the {@code era} is not a {@code HijrahEra} 433 */ 434 @Override 435 public HijrahDate date(Era era, int yearOfEra, int month, int dayOfMonth) { 436 return date(prolepticYear(era, yearOfEra), month, dayOfMonth); 437 } 438 439 /** 440 * Obtains a local date in Hijrah calendar system from the 441 * proleptic-year, month-of-year and day-of-month fields. 442 * 443 * @param prolepticYear the proleptic-year 444 * @param month the month-of-year 445 * @param dayOfMonth the day-of-month 446 * @return the Hijrah local date, not null 447 * @throws DateTimeException if unable to create the date 448 */ 449 @Override 450 public HijrahDate date(int prolepticYear, int month, int dayOfMonth) { 451 return HijrahDate.of(this, prolepticYear, month, dayOfMonth); 452 } 453 454 /** 455 * Obtains a local date in Hijrah calendar system from the 456 * era, year-of-era and day-of-year fields. 457 * 458 * @param era the Hijrah era, not null 459 * @param yearOfEra the year-of-era 460 * @param dayOfYear the day-of-year 461 * @return the Hijrah local date, not null 462 * @throws DateTimeException if unable to create the date 463 * @throws ClassCastException if the {@code era} is not a {@code HijrahEra} 464 */ 465 @Override 466 public HijrahDate dateYearDay(Era era, int yearOfEra, int dayOfYear) { 467 return dateYearDay(prolepticYear(era, yearOfEra), dayOfYear); 468 } 469 470 /** 471 * Obtains a local date in Hijrah calendar system from the 472 * proleptic-year and day-of-year fields. 473 * 474 * @param prolepticYear the proleptic-year 475 * @param dayOfYear the day-of-year 476 * @return the Hijrah local date, not null 477 * @throws DateTimeException if the value of the year is out of range, 478 * or if the day-of-year is invalid for the year 479 */ 480 @Override 481 public HijrahDate dateYearDay(int prolepticYear, int dayOfYear) { 482 HijrahDate date = HijrahDate.of(this, prolepticYear, 1, 1); 483 if (dayOfYear > date.lengthOfYear()) { 484 throw new DateTimeException("Invalid dayOfYear: " + dayOfYear); 485 } 486 return date.plusDays(dayOfYear - 1); 487 } 488 489 /** 490 * Obtains a local date in the Hijrah calendar system from the epoch-day. 491 * 492 * @param epochDay the epoch day 493 * @return the Hijrah local date, not null 494 * @throws DateTimeException if unable to create the date 495 */ 496 @Override // override with covariant return type 497 public HijrahDate dateEpochDay(long epochDay) { 498 return HijrahDate.ofEpochDay(this, epochDay); 499 } 500 501 @Override 502 public HijrahDate dateNow() { 503 return dateNow(Clock.systemDefaultZone()); 504 } 505 506 @Override 507 public HijrahDate dateNow(ZoneId zone) { 508 return dateNow(Clock.system(zone)); 509 } 510 511 @Override 512 public HijrahDate dateNow(Clock clock) { 513 return date(LocalDate.now(clock)); 514 } 515 516 @Override 517 public HijrahDate date(TemporalAccessor temporal) { 518 if (temporal instanceof HijrahDate) { 519 return (HijrahDate) temporal; 520 } 521 return HijrahDate.ofEpochDay(this, temporal.getLong(EPOCH_DAY)); 522 } 523 524 @Override 525 @SuppressWarnings("unchecked") 526 public ChronoLocalDateTime<HijrahDate> localDateTime(TemporalAccessor temporal) { 527 return (ChronoLocalDateTime<HijrahDate>) super.localDateTime(temporal); 528 } 529 530 @Override 531 @SuppressWarnings("unchecked") 532 public ChronoZonedDateTime<HijrahDate> zonedDateTime(TemporalAccessor temporal) { 533 return (ChronoZonedDateTime<HijrahDate>) super.zonedDateTime(temporal); 534 } 535 536 @Override 537 @SuppressWarnings("unchecked") 538 public ChronoZonedDateTime<HijrahDate> zonedDateTime(Instant instant, ZoneId zone) { 539 return (ChronoZonedDateTime<HijrahDate>) super.zonedDateTime(instant, zone); 540 } 541 542 //----------------------------------------------------------------------- 543 @Override 544 public boolean isLeapYear(long prolepticYear) { 545 checkCalendarInit(); 546 int epochMonth = yearToEpochMonth((int) prolepticYear); 547 if (epochMonth < 0 || epochMonth > maxEpochDay) { 548 throw new DateTimeException("Hijrah date out of range"); 549 } 550 int len = getYearLength((int) prolepticYear); 551 return (len > 354); 552 } 553 554 @Override 555 public int prolepticYear(Era era, int yearOfEra) { 556 if (era instanceof HijrahEra == false) { 557 throw new ClassCastException("Era must be HijrahEra"); 558 } 559 return yearOfEra; 560 } 561 562 @Override 563 public HijrahEra eraOf(int eraValue) { 564 switch (eraValue) { 565 case 1: 566 return HijrahEra.AH; 567 default: 568 throw new DateTimeException("invalid Hijrah era"); 569 } 570 } 571 572 @Override 573 public List<Era> eras() { 574 return Arrays.<Era>asList(HijrahEra.values()); 575 } 576 577 //----------------------------------------------------------------------- 578 @Override 579 public ValueRange range(ChronoField field) { 580 checkCalendarInit(); 581 if (field instanceof ChronoField) { 582 ChronoField f = field; 583 switch (f) { 584 case DAY_OF_MONTH: 585 return ValueRange.of(1, 1, getMinimumMonthLength(), getMaximumMonthLength()); 586 case DAY_OF_YEAR: 587 return ValueRange.of(1, getMaximumDayOfYear()); 588 case ALIGNED_WEEK_OF_MONTH: 589 return ValueRange.of(1, 5); 590 case YEAR: 591 case YEAR_OF_ERA: 592 return ValueRange.of(getMinimumYear(), getMaximumYear()); 593 case ERA: 594 return ValueRange.of(1, 1); 595 default: 596 return field.range(); 597 } 598 } 599 return field.range(); 600 } 601 602 //----------------------------------------------------------------------- 603 @Override // override for return type 604 public HijrahDate resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle) { 605 return (HijrahDate) super.resolveDate(fieldValues, resolverStyle); 606 } 607 608 //----------------------------------------------------------------------- 609 /** 610 * Check the validity of a year. 611 * 612 * @param prolepticYear the year to check 613 */ 614 int checkValidYear(long prolepticYear) { 615 if (prolepticYear < getMinimumYear() || prolepticYear > getMaximumYear()) { 616 throw new DateTimeException("Invalid Hijrah year: " + prolepticYear); 617 } 618 return (int) prolepticYear; 619 } 620 621 void checkValidDayOfYear(int dayOfYear) { 622 if (dayOfYear < 1 || dayOfYear > getMaximumDayOfYear()) { 623 throw new DateTimeException("Invalid Hijrah day of year: " + dayOfYear); 624 } 625 } 626 627 void checkValidMonth(int month) { 628 if (month < 1 || month > 12) { 629 throw new DateTimeException("Invalid Hijrah month: " + month); 630 } 631 } 632 633 //----------------------------------------------------------------------- 634 /** 635 * Returns an array containing the Hijrah year, month and day 636 * computed from the epoch day. 637 * 638 * @param epochDay the EpochDay 639 * @return int[0] = YEAR, int[1] = MONTH, int[2] = DATE 640 */ 641 int[] getHijrahDateInfo(int epochDay) { 642 checkCalendarInit(); // ensure that the chronology is initialized 643 if (epochDay < minEpochDay || epochDay >= maxEpochDay) { 644 throw new DateTimeException("Hijrah date out of range"); 645 } 646 647 int epochMonth = epochDayToEpochMonth(epochDay); 648 int year = epochMonthToYear(epochMonth); 649 int month = epochMonthToMonth(epochMonth); 650 int day1 = epochMonthToEpochDay(epochMonth); 651 int date = epochDay - day1; // epochDay - dayOfEpoch(year, month); 652 653 int dateInfo[] = new int[3]; 654 dateInfo[0] = year; 655 dateInfo[1] = month + 1; // change to 1-based. 656 dateInfo[2] = date + 1; // change to 1-based. 657 return dateInfo; 658 } 659 660 /** 661 * Return the epoch day computed from Hijrah year, month, and day. 662 * 663 * @param prolepticYear the year to represent, 0-origin 664 * @param monthOfYear the month-of-year to represent, 1-origin 665 * @param dayOfMonth the day-of-month to represent, 1-origin 666 * @return the epoch day 667 */ 668 long getEpochDay(int prolepticYear, int monthOfYear, int dayOfMonth) { 669 checkCalendarInit(); // ensure that the chronology is initialized 670 checkValidMonth(monthOfYear); 671 int epochMonth = yearToEpochMonth(prolepticYear) + (monthOfYear - 1); 672 if (epochMonth < 0 || epochMonth >= hijrahEpochMonthStartDays.length) { 673 throw new DateTimeException("Invalid Hijrah date, year: " + 674 prolepticYear + ", month: " + monthOfYear); 675 } 676 if (dayOfMonth < 1 || dayOfMonth > getMonthLength(prolepticYear, monthOfYear)) { 677 throw new DateTimeException("Invalid Hijrah day of month: " + dayOfMonth); 678 } 679 return epochMonthToEpochDay(epochMonth) + (dayOfMonth - 1); 680 } 681 682 /** 683 * Returns day of year for the year and month. 684 * 685 * @param prolepticYear a proleptic year 686 * @param month a month, 1-origin 687 * @return the day of year, 1-origin 688 */ 689 int getDayOfYear(int prolepticYear, int month) { 690 return yearMonthToDayOfYear(prolepticYear, (month - 1)); 691 } 692 693 /** 694 * Returns month length for the year and month. 695 * 696 * @param prolepticYear a proleptic year 697 * @param monthOfYear a month, 1-origin. 698 * @return the length of the month 699 */ 700 int getMonthLength(int prolepticYear, int monthOfYear) { 701 int epochMonth = yearToEpochMonth(prolepticYear) + (monthOfYear - 1); 702 if (epochMonth < 0 || epochMonth >= hijrahEpochMonthStartDays.length) { 703 throw new DateTimeException("Invalid Hijrah date, year: " + 704 prolepticYear + ", month: " + monthOfYear); 705 } 706 return epochMonthLength(epochMonth); 707 } 708 709 /** 710 * Returns year length. 711 * Note: The 12th month must exist in the data. 712 * 713 * @param prolepticYear a proleptic year 714 * @return year length in days 715 */ 716 int getYearLength(int prolepticYear) { 717 return yearMonthToDayOfYear(prolepticYear, 12); 718 } 719 720 /** 721 * Return the minimum supported Hijrah year. 722 * 723 * @return the minimum 724 */ 725 int getMinimumYear() { 726 return epochMonthToYear(0); 727 } 728 729 /** 730 * Return the maximum supported Hijrah ear. 731 * 732 * @return the minimum 733 */ 734 int getMaximumYear() { 735 return epochMonthToYear(hijrahEpochMonthStartDays.length - 1) - 1; 736 } 737 738 /** 739 * Returns maximum day-of-month. 740 * 741 * @return maximum day-of-month 742 */ 743 int getMaximumMonthLength() { 744 return maxMonthLength; 745 } 746 747 /** 748 * Returns smallest maximum day-of-month. 749 * 750 * @return smallest maximum day-of-month 751 */ 752 int getMinimumMonthLength() { 753 return minMonthLength; 754 } 755 756 /** 757 * Returns maximum day-of-year. 758 * 759 * @return maximum day-of-year 760 */ 761 int getMaximumDayOfYear() { 762 return maxYearLength; 763 } 764 765 /** 766 * Returns smallest maximum day-of-year. 767 * 768 * @return smallest maximum day-of-year 769 */ 770 int getSmallestMaximumDayOfYear() { 771 return minYearLength; 772 } 773 774 /** 775 * Returns the epochMonth found by locating the epochDay in the table. The 776 * epochMonth is the index in the table 777 * 778 * @param epochDay 779 * @return The index of the element of the start of the month containing the 780 * epochDay. 781 */ 782 private int epochDayToEpochMonth(int epochDay) { 783 // binary search 784 int ndx = Arrays.binarySearch(hijrahEpochMonthStartDays, epochDay); 785 if (ndx < 0) { 786 ndx = -ndx - 2; 787 } 788 return ndx; 789 } 790 791 /** 792 * Returns the year computed from the epochMonth 793 * 794 * @param epochMonth the epochMonth 795 * @return the Hijrah Year 796 */ 797 private int epochMonthToYear(int epochMonth) { 798 return (epochMonth + hijrahStartEpochMonth) / 12; 799 } 800 801 /** 802 * Returns the epochMonth for the Hijrah Year. 803 * 804 * @param year the HijrahYear 805 * @return the epochMonth for the beginning of the year. 806 */ 807 private int yearToEpochMonth(int year) { 808 return (year * 12) - hijrahStartEpochMonth; 809 } 810 811 /** 812 * Returns the Hijrah month from the epochMonth. 813 * 814 * @param epochMonth the epochMonth 815 * @return the month of the Hijrah Year 816 */ 817 private int epochMonthToMonth(int epochMonth) { 818 return (epochMonth + hijrahStartEpochMonth) % 12; 819 } 820 821 /** 822 * Returns the epochDay for the start of the epochMonth. 823 * 824 * @param epochMonth the epochMonth 825 * @return the epochDay for the start of the epochMonth. 826 */ 827 private int epochMonthToEpochDay(int epochMonth) { 828 return hijrahEpochMonthStartDays[epochMonth]; 829 830 } 831 832 /** 833 * Returns the day of year for the requested HijrahYear and month. 834 * 835 * @param prolepticYear the Hijrah year 836 * @param month the Hijrah month 837 * @return the day of year for the start of the month of the year 838 */ 839 private int yearMonthToDayOfYear(int prolepticYear, int month) { 840 int epochMonthFirst = yearToEpochMonth(prolepticYear); 841 return epochMonthToEpochDay(epochMonthFirst + month) 842 - epochMonthToEpochDay(epochMonthFirst); 843 } 844 845 /** 846 * Returns the length of the epochMonth. It is computed from the start of 847 * the following month minus the start of the requested month. 848 * 849 * @param epochMonth the epochMonth; assumed to be within range 850 * @return the length in days of the epochMonth 851 */ 852 private int epochMonthLength(int epochMonth) { 853 // The very last entry in the epochMonth table is not the start of a month 854 return hijrahEpochMonthStartDays[epochMonth + 1] 855 - hijrahEpochMonthStartDays[epochMonth]; 856 } 857 858 //----------------------------------------------------------------------- 859 private static final String KEY_ID = "id"; 860 private static final String KEY_TYPE = "type"; 861 private static final String KEY_VERSION = "version"; 862 private static final String KEY_ISO_START = "iso-start"; 863 864 /** 865 * Return the configuration properties from the resource. 866 * <p> 867 * The default location of the variant configuration resource is: 868 * <pre> 869 * "$java.home/lib/" + resource-name 870 * </pre> 871 * 872 * @param resource the name of the calendar property resource 873 * @return a Properties containing the properties read from the resource. 874 * @throws Exception if access to the property resource fails 875 */ 876 private static Properties readConfigProperties(final String resource) throws Exception { 877 try { 878 return AccessController 879 .doPrivileged((java.security.PrivilegedExceptionAction<Properties>) 880 () -> { 881 String libDir = System.getProperty("java.home") + File.separator + "lib"; 882 File file = new File(libDir, resource); 883 Properties props = new Properties(); 884 try (InputStream is = new FileInputStream(file)) { 885 props.load(is); 886 } 887 return props; 888 }); 889 } catch (PrivilegedActionException pax) { 890 throw pax.getException(); 891 } 892 } 893 894 /** 895 * Loads and processes the Hijrah calendar properties file for this calendarType. 896 * The starting Hijrah date and the corresponding ISO date are 897 * extracted and used to calculate the epochDate offset. 898 * The version number is identified and ignored. 899 * Everything else is the data for a year with containing the length of each 900 * of 12 months. 901 * 902 * @throws DateTimeException if initialization of the calendar data from the 903 * resource fails 904 */ 905 private void loadCalendarData() { 906 try { 907 String resourceName = calendarProperties.getProperty(PROP_PREFIX + typeId); 908 Objects.requireNonNull(resourceName, "Resource missing for calendar: " + PROP_PREFIX + typeId); 909 Properties props = readConfigProperties(resourceName); 910 911 Map<Integer, int[]> years = new HashMap<>(); 912 int minYear = Integer.MAX_VALUE; 913 int maxYear = Integer.MIN_VALUE; 914 String id = null; 915 String type = null; 916 String version = null; 917 int isoStart = 0; 918 for (Map.Entry<Object, Object> entry : props.entrySet()) { 919 String key = (String) entry.getKey(); 920 switch (key) { 921 case KEY_ID: 922 id = (String)entry.getValue(); 923 break; 924 case KEY_TYPE: 925 type = (String)entry.getValue(); 926 break; 927 case KEY_VERSION: 928 version = (String)entry.getValue(); 929 break; 930 case KEY_ISO_START: { 931 int[] ymd = parseYMD((String) entry.getValue()); 932 isoStart = (int) LocalDate.of(ymd[0], ymd[1], ymd[2]).toEpochDay(); 933 break; 934 } 935 default: 936 try { 937 // Everything else is either a year or invalid 938 int year = Integer.valueOf(key); 939 int[] months = parseMonths((String) entry.getValue()); 940 years.put(year, months); 941 maxYear = Math.max(maxYear, year); 942 minYear = Math.min(minYear, year); 943 } catch (NumberFormatException nfe) { 944 throw new IllegalArgumentException("bad key: " + key); 945 } 946 } 947 } 948 949 if (!getId().equals(id)) { 950 throw new IllegalArgumentException("Configuration is for a different calendar: " + id); 951 } 952 if (!getCalendarType().equals(type)) { 953 throw new IllegalArgumentException("Configuration is for a different calendar type: " + type); 954 } 955 if (version == null || version.isEmpty()) { 956 throw new IllegalArgumentException("Configuration does not contain a version"); 957 } 958 if (isoStart == 0) { 959 throw new IllegalArgumentException("Configuration does not contain a ISO start date"); 960 } 961 962 // Now create and validate the array of epochDays indexed by epochMonth 963 hijrahStartEpochMonth = minYear * 12; 964 minEpochDay = isoStart; 965 hijrahEpochMonthStartDays = createEpochMonths(minEpochDay, minYear, maxYear, years); 966 maxEpochDay = hijrahEpochMonthStartDays[hijrahEpochMonthStartDays.length - 1]; 967 968 // Compute the min and max year length in days. 969 for (int year = minYear; year < maxYear; year++) { 970 int length = getYearLength(year); 971 minYearLength = Math.min(minYearLength, length); 972 maxYearLength = Math.max(maxYearLength, length); 973 } 974 } catch (Exception ex) { 975 // Log error and throw a DateTimeException 976 PlatformLogger logger = PlatformLogger.getLogger("java.time.chrono"); 977 logger.severe("Unable to initialize Hijrah calendar proxy: " + typeId, ex); 978 throw new DateTimeException("Unable to initialize HijrahCalendar: " + typeId, ex); 979 } 980 } 981 982 /** 983 * Converts the map of year to month lengths ranging from minYear to maxYear 984 * into a linear contiguous array of epochDays. The index is the hijrahMonth 985 * computed from year and month and offset by minYear. The value of each 986 * entry is the epochDay corresponding to the first day of the month. 987 * 988 * @param minYear The minimum year for which data is provided 989 * @param maxYear The maximum year for which data is provided 990 * @param years a Map of year to the array of 12 month lengths 991 * @return array of epochDays for each month from min to max 992 */ 993 private int[] createEpochMonths(int epochDay, int minYear, int maxYear, Map<Integer, int[]> years) { 994 // Compute the size for the array of dates 995 int numMonths = (maxYear - minYear + 1) * 12 + 1; 996 997 // Initialize the running epochDay as the corresponding ISO Epoch day 998 int epochMonth = 0; // index into array of epochMonths 999 int[] epochMonths = new int[numMonths]; 1000 minMonthLength = Integer.MAX_VALUE; 1001 maxMonthLength = Integer.MIN_VALUE; 1002 1003 // Only whole years are valid, any zero's in the array are illegal 1004 for (int year = minYear; year <= maxYear; year++) { 1005 int[] months = years.get(year);// must not be gaps 1006 for (int month = 0; month < 12; month++) { 1007 int length = months[month]; 1008 epochMonths[epochMonth++] = epochDay; 1009 1010 if (length < 29 || length > 32) { 1011 throw new IllegalArgumentException("Invalid month length in year: " + minYear); 1012 } 1013 epochDay += length; 1014 minMonthLength = Math.min(minMonthLength, length); 1015 maxMonthLength = Math.max(maxMonthLength, length); 1016 } 1017 } 1018 1019 // Insert the final epochDay 1020 epochMonths[epochMonth++] = epochDay; 1021 1022 if (epochMonth != epochMonths.length) { 1023 throw new IllegalStateException("Did not fill epochMonths exactly: ndx = " + epochMonth 1024 + " should be " + epochMonths.length); 1025 } 1026 1027 return epochMonths; 1028 } 1029 1030 /** 1031 * Parses the 12 months lengths from a property value for a specific year. 1032 * 1033 * @param line the value of a year property 1034 * @return an array of int[12] containing the 12 month lengths 1035 * @throws IllegalArgumentException if the number of months is not 12 1036 * @throws NumberFormatException if the 12 tokens are not numbers 1037 */ 1038 private int[] parseMonths(String line) { 1039 int[] months = new int[12]; 1040 String[] numbers = line.split("\\s"); 1041 if (numbers.length != 12) { 1042 throw new IllegalArgumentException("wrong number of months on line: " + Arrays.toString(numbers) + "; count: " + numbers.length); 1043 } 1044 for (int i = 0; i < 12; i++) { 1045 try { 1046 months[i] = Integer.valueOf(numbers[i]); 1047 } catch (NumberFormatException nfe) { 1048 throw new IllegalArgumentException("bad key: " + numbers[i]); 1049 } 1050 } 1051 return months; 1052 } 1053 1054 /** 1055 * Parse yyyy-MM-dd into a 3 element array [yyyy, mm, dd]. 1056 * 1057 * @param string the input string 1058 * @return the 3 element array with year, month, day 1059 */ 1060 private int[] parseYMD(String string) { 1061 // yyyy-MM-dd 1062 string = string.trim(); 1063 try { 1064 if (string.charAt(4) != '-' || string.charAt(7) != '-') { 1065 throw new IllegalArgumentException("date must be yyyy-MM-dd"); 1066 } 1067 int[] ymd = new int[3]; 1068 ymd[0] = Integer.valueOf(string.substring(0, 4)); 1069 ymd[1] = Integer.valueOf(string.substring(5, 7)); 1070 ymd[2] = Integer.valueOf(string.substring(8, 10)); 1071 return ymd; 1072 } catch (NumberFormatException ex) { 1073 throw new IllegalArgumentException("date must be yyyy-MM-dd", ex); 1074 } 1075 } 1076 }