1 /* 2 * Copyright (c) 2012, 2018, 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.FilePermission; 63 import java.io.InputStream; 64 import java.io.InvalidObjectException; 65 import java.io.ObjectInputStream; 66 import java.io.Serializable; 67 import java.security.AccessController; 68 import java.security.PrivilegedAction; 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.Properties; 84 85 import sun.util.logging.PlatformLogger; 86 87 /** 88 * The Hijrah calendar is a lunar calendar supporting Islamic calendars. 89 * <p> 90 * The HijrahChronology follows the rules of the Hijrah calendar system. The Hijrah 91 * calendar has several variants based on differences in when the new moon is 92 * determined to have occurred and where the observation is made. 93 * In some variants the length of each month is 94 * computed algorithmically from the astronomical data for the moon and earth and 95 * in others the length of the month is determined by an authorized sighting 96 * of the new moon. For the algorithmically based calendars the calendar 97 * can project into the future. 98 * For sighting based calendars only historical data from past 99 * sightings is available. 100 * <p> 101 * The length of each month is 29 or 30 days. 102 * Ordinary years have 354 days; leap years have 355 days. 103 * 104 * <p> 105 * CLDR and LDML identify variants: 106 * <table class="striped" style="text-align:left"> 107 * <caption style="display:none">Variants of Hijrah Calendars</caption> 108 * <thead> 109 * <tr> 110 * <th scope="col">Chronology ID</th> 111 * <th scope="col">Calendar Type</th> 112 * <th scope="col">Locale extension, see {@link java.util.Locale}</th> 113 * <th scope="col">Description</th> 114 * </tr> 115 * </thead> 116 * <tbody> 117 * <tr> 118 * <th scope="row">Hijrah-umalqura</th> 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 loaded by HijrahChronology as a resource from 147 * hijrah-config-<calendar type>.properties. 148 * <p> 149 * The Hijrah property resource is a set of properties that describe the calendar. 150 * The syntax is defined by {@code java.util.Properties#load(Reader)}. 151 * <table class="striped" style="text-align:left"> 152 * <caption style="display:none">Configuration of Hijrah Calendar</caption> 153 * <thead> 154 * <tr> 155 * <th scope="col">Property Name</th> 156 * <th scope="col">Property value</th> 157 * <th scope="col">Description</th> 158 * </tr> 159 * </thead> 160 * <tbody> 161 * <tr> 162 * <th scope="row">id</th> 163 * <td>Chronology Id, for example, "Hijrah-umalqura"</td> 164 * <td>The Id of the calendar in common usage</td> 165 * </tr> 166 * <tr> 167 * <th scope="row">type</th> 168 * <td>Calendar type, for example, "islamic-umalqura"</td> 169 * <td>LDML defines the calendar types</td> 170 * </tr> 171 * <tr> 172 * <th scope="row">version</th> 173 * <td>Version, for example: "1.8.0_1"</td> 174 * <td>The version of the Hijrah variant data</td> 175 * </tr> 176 * <tr> 177 * <th scope="row">iso-start</th> 178 * <td>ISO start date, formatted as {@code yyyy-MM-dd}, for example: "1900-04-30"</td> 179 * <td>The ISO date of the first day of the minimum Hijrah year.</td> 180 * </tr> 181 * <tr> 182 * <th scope="row">yyyy - a numeric 4 digit year, for example "1434"</th> 183 * <td>The value is a sequence of 12 month lengths, 184 * for example: "29 30 29 30 29 30 30 30 29 30 29 29"</td> 185 * <td>The lengths of the 12 months of the year separated by whitespace. 186 * A numeric year property must be present for every year without any gaps. 187 * The month lengths must be between 29-32 inclusive. 188 * </td> 189 * </tr> 190 * </tbody> 191 * </table> 192 * 193 * @since 1.8 194 */ 195 public final class HijrahChronology extends AbstractChronology implements Serializable { 196 197 /** 198 * The Hijrah Calendar id. 199 */ 200 private final transient String typeId; 201 /** 202 * The Hijrah calendarType. 203 */ 204 private final transient String calendarType; 205 /** 206 * Serialization version. 207 */ 208 @java.io.Serial 209 private static final long serialVersionUID = 3127340209035924785L; 210 /** 211 * Singleton instance of the Islamic Umm Al-Qura calendar of Saudi Arabia. 212 * Other Hijrah chronology variants may be available from 213 * {@link Chronology#getAvailableChronologies}. 214 */ 215 public static final HijrahChronology INSTANCE; 216 /** 217 * Flag to indicate the initialization of configuration data is complete. 218 * @see #checkCalendarInit() 219 */ 220 private transient volatile boolean initComplete; 221 /** 222 * Array of epoch days indexed by Hijrah Epoch month. 223 * Computed by {@link #loadCalendarData}. 224 */ 225 private transient int[] hijrahEpochMonthStartDays; 226 /** 227 * The minimum epoch day of this Hijrah calendar. 228 * Computed by {@link #loadCalendarData}. 229 */ 230 private transient int minEpochDay; 231 /** 232 * The maximum epoch day for which calendar data is available. 233 * Computed by {@link #loadCalendarData}. 234 */ 235 private transient int maxEpochDay; 236 /** 237 * The minimum epoch month. 238 * Computed by {@link #loadCalendarData}. 239 */ 240 private transient int hijrahStartEpochMonth; 241 /** 242 * The minimum length of a month. 243 * Computed by {@link #createEpochMonths}. 244 */ 245 private transient int minMonthLength; 246 /** 247 * The maximum length of a month. 248 * Computed by {@link #createEpochMonths}. 249 */ 250 private transient int maxMonthLength; 251 /** 252 * The minimum length of a year in days. 253 * Computed by {@link #createEpochMonths}. 254 */ 255 private transient int minYearLength; 256 /** 257 * The maximum length of a year in days. 258 * Computed by {@link #createEpochMonths}. 259 */ 260 private transient int maxYearLength; 261 262 /** 263 * Prefix of resource names for Hijrah calendar variants. 264 */ 265 private static final String RESOURCE_PREFIX = "hijrah-config-"; 266 267 /** 268 * Suffix of resource names for Hijrah calendar variants. 269 */ 270 private static final String RESOURCE_SUFFIX = ".properties"; 271 272 /** 273 * Static initialization of the built-in calendars. 274 * The data is not loaded until it is used. 275 */ 276 static { 277 INSTANCE = new HijrahChronology("Hijrah-umalqura", "islamic-umalqura"); 278 // Register it by its aliases 279 AbstractChronology.registerChrono(INSTANCE, "Hijrah"); 280 AbstractChronology.registerChrono(INSTANCE, "islamic"); 281 } 282 283 /** 284 * Create a HijrahChronology for the named variant and type. 285 * 286 * @param id the id of the calendar 287 * @param calType the typeId of the calendar 288 * @throws IllegalArgumentException if the id or typeId is empty 289 */ 290 private HijrahChronology(String id, String calType) { 291 if (id.isEmpty()) { 292 throw new IllegalArgumentException("calendar id is empty"); 293 } 294 if (calType.isEmpty()) { 295 throw new IllegalArgumentException("calendar typeId is empty"); 296 } 297 this.typeId = id; 298 this.calendarType = calType; 299 } 300 301 /** 302 * Check and ensure that the calendar data has been initialized. 303 * The initialization check is performed at the boundary between 304 * public and package methods. If a public calls another public method 305 * a check is not necessary in the caller. 306 * The constructors of HijrahDate call {@link #getEpochDay} or 307 * {@link #getHijrahDateInfo} so every call from HijrahDate to a 308 * HijrahChronology via package private methods has been checked. 309 * 310 * @throws DateTimeException if the calendar data configuration is 311 * malformed or IOExceptions occur loading the data 312 */ 313 private void checkCalendarInit() { 314 // Keep this short so it can be inlined for performance 315 if (initComplete == false) { 316 loadCalendarData(); 317 initComplete = true; 318 } 319 } 320 321 //----------------------------------------------------------------------- 322 /** 323 * Gets the ID of the chronology. 324 * <p> 325 * The ID uniquely identifies the {@code Chronology}. It can be used to 326 * lookup the {@code Chronology} using {@link Chronology#of(String)}. 327 * 328 * @return the chronology ID, non-null 329 * @see #getCalendarType() 330 */ 331 @Override 332 public String getId() { 333 return typeId; 334 } 335 336 /** 337 * Gets the calendar type of the Islamic calendar. 338 * <p> 339 * The calendar type is an identifier defined by the 340 * <em>Unicode Locale Data Markup Language (LDML)</em> specification. 341 * It can be used to lookup the {@code Chronology} using {@link Chronology#of(String)}. 342 * 343 * @return the calendar system type; non-null if the calendar has 344 * a standard type, otherwise null 345 * @see #getId() 346 */ 347 @Override 348 public String getCalendarType() { 349 return calendarType; 350 } 351 352 //----------------------------------------------------------------------- 353 /** 354 * Obtains a local date in Hijrah calendar system from the 355 * era, year-of-era, month-of-year and day-of-month fields. 356 * 357 * @param era the Hijrah era, not null 358 * @param yearOfEra the year-of-era 359 * @param month the month-of-year 360 * @param dayOfMonth the day-of-month 361 * @return the Hijrah local date, not null 362 * @throws DateTimeException if unable to create the date 363 * @throws ClassCastException if the {@code era} is not a {@code HijrahEra} 364 */ 365 @Override 366 public HijrahDate date(Era era, int yearOfEra, int month, int dayOfMonth) { 367 return date(prolepticYear(era, yearOfEra), month, dayOfMonth); 368 } 369 370 /** 371 * Obtains a local date in Hijrah calendar system from the 372 * proleptic-year, month-of-year and day-of-month fields. 373 * 374 * @param prolepticYear the proleptic-year 375 * @param month the month-of-year 376 * @param dayOfMonth the day-of-month 377 * @return the Hijrah local date, not null 378 * @throws DateTimeException if unable to create the date 379 */ 380 @Override 381 public HijrahDate date(int prolepticYear, int month, int dayOfMonth) { 382 return HijrahDate.of(this, prolepticYear, month, dayOfMonth); 383 } 384 385 /** 386 * Obtains a local date in Hijrah calendar system from the 387 * era, year-of-era and day-of-year fields. 388 * 389 * @param era the Hijrah era, not null 390 * @param yearOfEra the year-of-era 391 * @param dayOfYear the day-of-year 392 * @return the Hijrah local date, not null 393 * @throws DateTimeException if unable to create the date 394 * @throws ClassCastException if the {@code era} is not a {@code HijrahEra} 395 */ 396 @Override 397 public HijrahDate dateYearDay(Era era, int yearOfEra, int dayOfYear) { 398 return dateYearDay(prolepticYear(era, yearOfEra), dayOfYear); 399 } 400 401 /** 402 * Obtains a local date in Hijrah calendar system from the 403 * proleptic-year and day-of-year fields. 404 * 405 * @param prolepticYear the proleptic-year 406 * @param dayOfYear the day-of-year 407 * @return the Hijrah local date, not null 408 * @throws DateTimeException if the value of the year is out of range, 409 * or if the day-of-year is invalid for the year 410 */ 411 @Override 412 public HijrahDate dateYearDay(int prolepticYear, int dayOfYear) { 413 HijrahDate date = HijrahDate.of(this, prolepticYear, 1, 1); 414 if (dayOfYear > date.lengthOfYear()) { 415 throw new DateTimeException("Invalid dayOfYear: " + dayOfYear); 416 } 417 return date.plusDays(dayOfYear - 1); 418 } 419 420 /** 421 * Obtains a local date in the Hijrah calendar system from the epoch-day. 422 * 423 * @param epochDay the epoch day 424 * @return the Hijrah local date, not null 425 * @throws DateTimeException if unable to create the date 426 */ 427 @Override // override with covariant return type 428 public HijrahDate dateEpochDay(long epochDay) { 429 return HijrahDate.ofEpochDay(this, epochDay); 430 } 431 432 @Override 433 public HijrahDate dateNow() { 434 return dateNow(Clock.systemDefaultZone()); 435 } 436 437 @Override 438 public HijrahDate dateNow(ZoneId zone) { 439 return dateNow(Clock.system(zone)); 440 } 441 442 @Override 443 public HijrahDate dateNow(Clock clock) { 444 return date(LocalDate.now(clock)); 445 } 446 447 @Override 448 public HijrahDate date(TemporalAccessor temporal) { 449 if (temporal instanceof HijrahDate) { 450 return (HijrahDate) temporal; 451 } 452 return HijrahDate.ofEpochDay(this, temporal.getLong(EPOCH_DAY)); 453 } 454 455 @Override 456 @SuppressWarnings("unchecked") 457 public ChronoLocalDateTime<HijrahDate> localDateTime(TemporalAccessor temporal) { 458 return (ChronoLocalDateTime<HijrahDate>) super.localDateTime(temporal); 459 } 460 461 @Override 462 @SuppressWarnings("unchecked") 463 public ChronoZonedDateTime<HijrahDate> zonedDateTime(TemporalAccessor temporal) { 464 return (ChronoZonedDateTime<HijrahDate>) super.zonedDateTime(temporal); 465 } 466 467 @Override 468 @SuppressWarnings("unchecked") 469 public ChronoZonedDateTime<HijrahDate> zonedDateTime(Instant instant, ZoneId zone) { 470 return (ChronoZonedDateTime<HijrahDate>) super.zonedDateTime(instant, zone); 471 } 472 473 //----------------------------------------------------------------------- 474 @Override 475 public boolean isLeapYear(long prolepticYear) { 476 checkCalendarInit(); 477 if (prolepticYear < getMinimumYear() || prolepticYear > getMaximumYear()) { 478 return false; 479 } 480 int len = getYearLength((int) prolepticYear); 481 return (len > 354); 482 } 483 484 @Override 485 public int prolepticYear(Era era, int yearOfEra) { 486 if (era instanceof HijrahEra == false) { 487 throw new ClassCastException("Era must be HijrahEra"); 488 } 489 return yearOfEra; 490 } 491 492 /** 493 * Creates the HijrahEra object from the numeric value. 494 * The Hijrah calendar system has only one era covering the 495 * proleptic years greater than zero. 496 * This method returns the singleton HijrahEra for the value 1. 497 * 498 * @param eraValue the era value 499 * @return the calendar system era, not null 500 * @throws DateTimeException if unable to create the era 501 */ 502 @Override 503 public HijrahEra eraOf(int eraValue) { 504 switch (eraValue) { 505 case 1: 506 return HijrahEra.AH; 507 default: 508 throw new DateTimeException("invalid Hijrah era"); 509 } 510 } 511 512 @Override 513 public List<Era> eras() { 514 return List.of(HijrahEra.values()); 515 } 516 517 //----------------------------------------------------------------------- 518 @Override 519 public ValueRange range(ChronoField field) { 520 checkCalendarInit(); 521 if (field instanceof ChronoField) { 522 ChronoField f = field; 523 switch (f) { 524 case DAY_OF_MONTH: 525 return ValueRange.of(1, 1, getMinimumMonthLength(), getMaximumMonthLength()); 526 case DAY_OF_YEAR: 527 return ValueRange.of(1, getMaximumDayOfYear()); 528 case ALIGNED_WEEK_OF_MONTH: 529 return ValueRange.of(1, 5); 530 case YEAR: 531 case YEAR_OF_ERA: 532 return ValueRange.of(getMinimumYear(), getMaximumYear()); 533 case ERA: 534 return ValueRange.of(1, 1); 535 default: 536 return field.range(); 537 } 538 } 539 return field.range(); 540 } 541 542 //----------------------------------------------------------------------- 543 @Override // override for return type 544 public HijrahDate resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle) { 545 return (HijrahDate) super.resolveDate(fieldValues, resolverStyle); 546 } 547 548 //----------------------------------------------------------------------- 549 /** 550 * Check the validity of a year. 551 * 552 * @param prolepticYear the year to check 553 */ 554 int checkValidYear(long prolepticYear) { 555 if (prolepticYear < getMinimumYear() || prolepticYear > getMaximumYear()) { 556 throw new DateTimeException("Invalid Hijrah year: " + prolepticYear); 557 } 558 return (int) prolepticYear; 559 } 560 561 void checkValidDayOfYear(int dayOfYear) { 562 if (dayOfYear < 1 || dayOfYear > getMaximumDayOfYear()) { 563 throw new DateTimeException("Invalid Hijrah day of year: " + dayOfYear); 564 } 565 } 566 567 void checkValidMonth(int month) { 568 if (month < 1 || month > 12) { 569 throw new DateTimeException("Invalid Hijrah month: " + month); 570 } 571 } 572 573 //----------------------------------------------------------------------- 574 /** 575 * Returns an array containing the Hijrah year, month and day 576 * computed from the epoch day. 577 * 578 * @param epochDay the EpochDay 579 * @return int[0] = YEAR, int[1] = MONTH, int[2] = DATE 580 */ 581 int[] getHijrahDateInfo(int epochDay) { 582 checkCalendarInit(); // ensure that the chronology is initialized 583 if (epochDay < minEpochDay || epochDay >= maxEpochDay) { 584 throw new DateTimeException("Hijrah date out of range"); 585 } 586 587 int epochMonth = epochDayToEpochMonth(epochDay); 588 int year = epochMonthToYear(epochMonth); 589 int month = epochMonthToMonth(epochMonth); 590 int day1 = epochMonthToEpochDay(epochMonth); 591 int date = epochDay - day1; // epochDay - dayOfEpoch(year, month); 592 593 int dateInfo[] = new int[3]; 594 dateInfo[0] = year; 595 dateInfo[1] = month + 1; // change to 1-based. 596 dateInfo[2] = date + 1; // change to 1-based. 597 return dateInfo; 598 } 599 600 /** 601 * Return the epoch day computed from Hijrah year, month, and day. 602 * 603 * @param prolepticYear the year to represent, 0-origin 604 * @param monthOfYear the month-of-year to represent, 1-origin 605 * @param dayOfMonth the day-of-month to represent, 1-origin 606 * @return the epoch day 607 */ 608 long getEpochDay(int prolepticYear, int monthOfYear, int dayOfMonth) { 609 checkCalendarInit(); // ensure that the chronology is initialized 610 checkValidMonth(monthOfYear); 611 int epochMonth = yearToEpochMonth(prolepticYear) + (monthOfYear - 1); 612 if (epochMonth < 0 || epochMonth >= hijrahEpochMonthStartDays.length) { 613 throw new DateTimeException("Invalid Hijrah date, year: " + 614 prolepticYear + ", month: " + monthOfYear); 615 } 616 if (dayOfMonth < 1 || dayOfMonth > getMonthLength(prolepticYear, monthOfYear)) { 617 throw new DateTimeException("Invalid Hijrah day of month: " + dayOfMonth); 618 } 619 return epochMonthToEpochDay(epochMonth) + (dayOfMonth - 1); 620 } 621 622 /** 623 * Returns day of year for the year and month. 624 * 625 * @param prolepticYear a proleptic year 626 * @param month a month, 1-origin 627 * @return the day of year, 1-origin 628 */ 629 int getDayOfYear(int prolepticYear, int month) { 630 return yearMonthToDayOfYear(prolepticYear, (month - 1)); 631 } 632 633 /** 634 * Returns month length for the year and month. 635 * 636 * @param prolepticYear a proleptic year 637 * @param monthOfYear a month, 1-origin. 638 * @return the length of the month 639 */ 640 int getMonthLength(int prolepticYear, int monthOfYear) { 641 int epochMonth = yearToEpochMonth(prolepticYear) + (monthOfYear - 1); 642 if (epochMonth < 0 || epochMonth >= hijrahEpochMonthStartDays.length) { 643 throw new DateTimeException("Invalid Hijrah date, year: " + 644 prolepticYear + ", month: " + monthOfYear); 645 } 646 return epochMonthLength(epochMonth); 647 } 648 649 /** 650 * Returns year length. 651 * Note: The 12th month must exist in the data. 652 * 653 * @param prolepticYear a proleptic year 654 * @return year length in days 655 */ 656 int getYearLength(int prolepticYear) { 657 return yearMonthToDayOfYear(prolepticYear, 12); 658 } 659 660 /** 661 * Return the minimum supported Hijrah year. 662 * 663 * @return the minimum 664 */ 665 int getMinimumYear() { 666 return epochMonthToYear(0); 667 } 668 669 /** 670 * Return the maximum supported Hijrah year. 671 * 672 * @return the minimum 673 */ 674 int getMaximumYear() { 675 return epochMonthToYear(hijrahEpochMonthStartDays.length - 1) - 1; 676 } 677 678 /** 679 * Returns maximum day-of-month. 680 * 681 * @return maximum day-of-month 682 */ 683 int getMaximumMonthLength() { 684 return maxMonthLength; 685 } 686 687 /** 688 * Returns smallest maximum day-of-month. 689 * 690 * @return smallest maximum day-of-month 691 */ 692 int getMinimumMonthLength() { 693 return minMonthLength; 694 } 695 696 /** 697 * Returns maximum day-of-year. 698 * 699 * @return maximum day-of-year 700 */ 701 int getMaximumDayOfYear() { 702 return maxYearLength; 703 } 704 705 /** 706 * Returns smallest maximum day-of-year. 707 * 708 * @return smallest maximum day-of-year 709 */ 710 int getSmallestMaximumDayOfYear() { 711 return minYearLength; 712 } 713 714 /** 715 * Returns the epochMonth found by locating the epochDay in the table. The 716 * epochMonth is the index in the table 717 * 718 * @param epochDay 719 * @return The index of the element of the start of the month containing the 720 * epochDay. 721 */ 722 private int epochDayToEpochMonth(int epochDay) { 723 // binary search 724 int ndx = Arrays.binarySearch(hijrahEpochMonthStartDays, epochDay); 725 if (ndx < 0) { 726 ndx = -ndx - 2; 727 } 728 return ndx; 729 } 730 731 /** 732 * Returns the year computed from the epochMonth 733 * 734 * @param epochMonth the epochMonth 735 * @return the Hijrah Year 736 */ 737 private int epochMonthToYear(int epochMonth) { 738 return (epochMonth + hijrahStartEpochMonth) / 12; 739 } 740 741 /** 742 * Returns the epochMonth for the Hijrah Year. 743 * 744 * @param year the HijrahYear 745 * @return the epochMonth for the beginning of the year. 746 */ 747 private int yearToEpochMonth(int year) { 748 return (year * 12) - hijrahStartEpochMonth; 749 } 750 751 /** 752 * Returns the Hijrah month from the epochMonth. 753 * 754 * @param epochMonth the epochMonth 755 * @return the month of the Hijrah Year 756 */ 757 private int epochMonthToMonth(int epochMonth) { 758 return (epochMonth + hijrahStartEpochMonth) % 12; 759 } 760 761 /** 762 * Returns the epochDay for the start of the epochMonth. 763 * 764 * @param epochMonth the epochMonth 765 * @return the epochDay for the start of the epochMonth. 766 */ 767 private int epochMonthToEpochDay(int epochMonth) { 768 return hijrahEpochMonthStartDays[epochMonth]; 769 770 } 771 772 /** 773 * Returns the day of year for the requested HijrahYear and month. 774 * 775 * @param prolepticYear the Hijrah year 776 * @param month the Hijrah month 777 * @return the day of year for the start of the month of the year 778 */ 779 private int yearMonthToDayOfYear(int prolepticYear, int month) { 780 int epochMonthFirst = yearToEpochMonth(prolepticYear); 781 return epochMonthToEpochDay(epochMonthFirst + month) 782 - epochMonthToEpochDay(epochMonthFirst); 783 } 784 785 /** 786 * Returns the length of the epochMonth. It is computed from the start of 787 * the following month minus the start of the requested month. 788 * 789 * @param epochMonth the epochMonth; assumed to be within range 790 * @return the length in days of the epochMonth 791 */ 792 private int epochMonthLength(int epochMonth) { 793 // The very last entry in the epochMonth table is not the start of a month 794 return hijrahEpochMonthStartDays[epochMonth + 1] 795 - hijrahEpochMonthStartDays[epochMonth]; 796 } 797 798 //----------------------------------------------------------------------- 799 private static final String KEY_ID = "id"; 800 private static final String KEY_TYPE = "type"; 801 private static final String KEY_VERSION = "version"; 802 private static final String KEY_ISO_START = "iso-start"; 803 804 /** 805 * Return the configuration properties from the resource. 806 * <p> 807 * The location of the variant configuration resource is: 808 * <pre> 809 * "/java/time/chrono/hijrah-config-" + calendarType + ".properties" 810 * </pre> 811 * 812 * @param calendarType the calendarType of the calendar variant 813 * @return a Properties containing the properties read from the resource. 814 * @throws Exception if access to the property resource fails 815 */ 816 private Properties readConfigProperties(final String calendarType) throws Exception { 817 String resourceName = RESOURCE_PREFIX + calendarType + RESOURCE_SUFFIX; 818 PrivilegedAction<InputStream> getResourceAction = () -> HijrahChronology.class.getResourceAsStream(resourceName); 819 FilePermission perm1 = new FilePermission("<<ALL FILES>>", "read"); 820 RuntimePermission perm2 = new RuntimePermission("accessSystemModules"); 821 try (InputStream is = AccessController.doPrivileged(getResourceAction, null, perm1, perm2)) { 822 if (is == null) { 823 throw new RuntimeException("Hijrah calendar resource not found: /java/time/chrono/" + resourceName); 824 } 825 Properties props = new Properties(); 826 props.load(is); 827 return props; 828 } 829 } 830 831 /** 832 * Loads and processes the Hijrah calendar properties file for this calendarType. 833 * The starting Hijrah date and the corresponding ISO date are 834 * extracted and used to calculate the epochDate offset. 835 * The version number is identified and ignored. 836 * Everything else is the data for a year with containing the length of each 837 * of 12 months. 838 * 839 * @throws DateTimeException if initialization of the calendar data from the 840 * resource fails 841 */ 842 private void loadCalendarData() { 843 try { 844 Properties props = readConfigProperties(calendarType); 845 846 Map<Integer, int[]> years = new HashMap<>(); 847 int minYear = Integer.MAX_VALUE; 848 int maxYear = Integer.MIN_VALUE; 849 String id = null; 850 String type = null; 851 String version = null; 852 int isoStart = 0; 853 for (Map.Entry<Object, Object> entry : props.entrySet()) { 854 String key = (String) entry.getKey(); 855 switch (key) { 856 case KEY_ID: 857 id = (String)entry.getValue(); 858 break; 859 case KEY_TYPE: 860 type = (String)entry.getValue(); 861 break; 862 case KEY_VERSION: 863 version = (String)entry.getValue(); 864 break; 865 case KEY_ISO_START: { 866 int[] ymd = parseYMD((String) entry.getValue()); 867 isoStart = (int) LocalDate.of(ymd[0], ymd[1], ymd[2]).toEpochDay(); 868 break; 869 } 870 default: 871 try { 872 // Everything else is either a year or invalid 873 int year = Integer.parseInt(key); 874 int[] months = parseMonths((String) entry.getValue()); 875 years.put(year, months); 876 maxYear = Math.max(maxYear, year); 877 minYear = Math.min(minYear, year); 878 } catch (NumberFormatException nfe) { 879 throw new IllegalArgumentException("bad key: " + key); 880 } 881 } 882 } 883 884 if (!getId().equals(id)) { 885 throw new IllegalArgumentException("Configuration is for a different calendar: " + id); 886 } 887 if (!getCalendarType().equals(type)) { 888 throw new IllegalArgumentException("Configuration is for a different calendar type: " + type); 889 } 890 if (version == null || version.isEmpty()) { 891 throw new IllegalArgumentException("Configuration does not contain a version"); 892 } 893 if (isoStart == 0) { 894 throw new IllegalArgumentException("Configuration does not contain a ISO start date"); 895 } 896 897 // Now create and validate the array of epochDays indexed by epochMonth 898 hijrahStartEpochMonth = minYear * 12; 899 minEpochDay = isoStart; 900 hijrahEpochMonthStartDays = createEpochMonths(minEpochDay, minYear, maxYear, years); 901 maxEpochDay = hijrahEpochMonthStartDays[hijrahEpochMonthStartDays.length - 1]; 902 903 // Compute the min and max year length in days. 904 for (int year = minYear; year < maxYear; year++) { 905 int length = getYearLength(year); 906 minYearLength = Math.min(minYearLength, length); 907 maxYearLength = Math.max(maxYearLength, length); 908 } 909 } catch (Exception ex) { 910 // Log error and throw a DateTimeException 911 PlatformLogger logger = PlatformLogger.getLogger("java.time.chrono"); 912 logger.severe("Unable to initialize Hijrah calendar proxy: " + typeId, ex); 913 throw new DateTimeException("Unable to initialize HijrahCalendar: " + typeId, ex); 914 } 915 } 916 917 /** 918 * Converts the map of year to month lengths ranging from minYear to maxYear 919 * into a linear contiguous array of epochDays. The index is the hijrahMonth 920 * computed from year and month and offset by minYear. The value of each 921 * entry is the epochDay corresponding to the first day of the month. 922 * 923 * @param minYear The minimum year for which data is provided 924 * @param maxYear The maximum year for which data is provided 925 * @param years a Map of year to the array of 12 month lengths 926 * @return array of epochDays for each month from min to max 927 */ 928 private int[] createEpochMonths(int epochDay, int minYear, int maxYear, Map<Integer, int[]> years) { 929 // Compute the size for the array of dates 930 int numMonths = (maxYear - minYear + 1) * 12 + 1; 931 932 // Initialize the running epochDay as the corresponding ISO Epoch day 933 int epochMonth = 0; // index into array of epochMonths 934 int[] epochMonths = new int[numMonths]; 935 minMonthLength = Integer.MAX_VALUE; 936 maxMonthLength = Integer.MIN_VALUE; 937 938 // Only whole years are valid, any zero's in the array are illegal 939 for (int year = minYear; year <= maxYear; year++) { 940 int[] months = years.get(year);// must not be gaps 941 for (int month = 0; month < 12; month++) { 942 int length = months[month]; 943 epochMonths[epochMonth++] = epochDay; 944 945 if (length < 29 || length > 32) { 946 throw new IllegalArgumentException("Invalid month length in year: " + minYear); 947 } 948 epochDay += length; 949 minMonthLength = Math.min(minMonthLength, length); 950 maxMonthLength = Math.max(maxMonthLength, length); 951 } 952 } 953 954 // Insert the final epochDay 955 epochMonths[epochMonth++] = epochDay; 956 957 if (epochMonth != epochMonths.length) { 958 throw new IllegalStateException("Did not fill epochMonths exactly: ndx = " + epochMonth 959 + " should be " + epochMonths.length); 960 } 961 962 return epochMonths; 963 } 964 965 /** 966 * Parses the 12 months lengths from a property value for a specific year. 967 * 968 * @param line the value of a year property 969 * @return an array of int[12] containing the 12 month lengths 970 * @throws IllegalArgumentException if the number of months is not 12 971 * @throws NumberFormatException if the 12 tokens are not numbers 972 */ 973 private int[] parseMonths(String line) { 974 int[] months = new int[12]; 975 String[] numbers = line.split("\\s"); 976 if (numbers.length != 12) { 977 throw new IllegalArgumentException("wrong number of months on line: " + Arrays.toString(numbers) + "; count: " + numbers.length); 978 } 979 for (int i = 0; i < 12; i++) { 980 try { 981 months[i] = Integer.parseInt(numbers[i]); 982 } catch (NumberFormatException nfe) { 983 throw new IllegalArgumentException("bad key: " + numbers[i]); 984 } 985 } 986 return months; 987 } 988 989 /** 990 * Parse yyyy-MM-dd into a 3 element array [yyyy, mm, dd]. 991 * 992 * @param string the input string 993 * @return the 3 element array with year, month, day 994 */ 995 private int[] parseYMD(String string) { 996 // yyyy-MM-dd 997 string = string.trim(); 998 try { 999 if (string.charAt(4) != '-' || string.charAt(7) != '-') { 1000 throw new IllegalArgumentException("date must be yyyy-MM-dd"); 1001 } 1002 int[] ymd = new int[3]; 1003 ymd[0] = Integer.parseInt(string, 0, 4, 10); 1004 ymd[1] = Integer.parseInt(string, 5, 7, 10); 1005 ymd[2] = Integer.parseInt(string, 8, 10, 10); 1006 return ymd; 1007 } catch (NumberFormatException ex) { 1008 throw new IllegalArgumentException("date must be yyyy-MM-dd", ex); 1009 } 1010 } 1011 1012 //----------------------------------------------------------------------- 1013 /** 1014 * Writes the Chronology using a 1015 * <a href="{@docRoot}/serialized-form.html#java.time.chrono.Ser">dedicated serialized form</a>. 1016 * @serialData 1017 * <pre> 1018 * out.writeByte(1); // identifies a Chronology 1019 * out.writeUTF(getId()); 1020 * </pre> 1021 * 1022 * @return the instance of {@code Ser}, not null 1023 */ 1024 @Override 1025 @java.io.Serial 1026 Object writeReplace() { 1027 return super.writeReplace(); 1028 } 1029 1030 /** 1031 * Defend against malicious streams. 1032 * 1033 * @param s the stream to read 1034 * @throws InvalidObjectException always 1035 */ 1036 @java.io.Serial 1037 private void readObject(ObjectInputStream s) throws InvalidObjectException { 1038 throw new InvalidObjectException("Deserialization via serialization delegate"); 1039 } 1040 }