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) 2011-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 package java.time.temporal;
  58 
  59 import static java.time.DayOfWeek.THURSDAY;
  60 import static java.time.DayOfWeek.WEDNESDAY;
  61 import static java.time.temporal.ChronoField.DAY_OF_WEEK;
  62 import static java.time.temporal.ChronoField.DAY_OF_YEAR;
  63 import static java.time.temporal.ChronoField.EPOCH_DAY;
  64 import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
  65 import static java.time.temporal.ChronoField.YEAR;
  66 import static java.time.temporal.ChronoUnit.DAYS;
  67 import static java.time.temporal.ChronoUnit.FOREVER;
  68 import static java.time.temporal.ChronoUnit.MONTHS;
  69 import static java.time.temporal.ChronoUnit.WEEKS;
  70 import static java.time.temporal.ChronoUnit.YEARS;
  71 
  72 import java.time.Duration;
  73 import java.time.LocalDate;
  74 import java.time.chrono.Chronology;
  75 import java.time.chrono.IsoChronology;
  76 import java.util.HashMap;
  77 import java.util.Locale;
  78 import java.util.Map;
  79 import java.util.Objects;
  80 import java.util.ResourceBundle;
  81 import sun.util.locale.provider.LocaleProviderAdapter;
  82 import sun.util.locale.provider.LocaleResources;
  83 
  84 /**
  85  * Fields and units specific to the ISO-8601 calendar system,
  86  * including quarter-of-year and week-based-year.
  87  * <p>
  88  * This class defines fields and units that are specific to the ISO calendar system.
  89  *
  90  * <h3>Quarter of year</h3>
  91  * The ISO-8601 standard is based on the standard civic 12 month year.
  92  * This is commonly divided into four quarters, often abbreviated as Q1, Q2, Q3 and Q4.
  93  * <p>
  94  * January, February and March are in Q1.
  95  * April, May and June are in Q2.
  96  * July, August and September are in Q3.
  97  * October, November and December are in Q4.
  98  * <p>
  99  * The complete date is expressed using three fields:
 100  * <p><ul>
 101  * <li>{@link #DAY_OF_QUARTER DAY_OF_QUARTER} - the day within the quarter, from 1 to 90, 91 or 92
 102  * <li>{@link #QUARTER_OF_YEAR QUARTER_OF_YEAR} - the week within the week-based-year
 103  * <li>{@link ChronoField#YEAR YEAR} - the standard ISO year
 104  * </ul><p>
 105  *
 106  * <h3>Week based years</h3>
 107  * The ISO-8601 standard was originally intended as a data interchange format,
 108  * defining a string format for dates and times. However, it also defines an
 109  * alternate way of expressing the date, based on the concept of week-based-year.
 110  * <p>
 111  * The date is expressed using three fields:
 112  * <p><ul>
 113  * <li>{@link ChronoField#DAY_OF_WEEK DAY_OF_WEEK} - the standard field defining the
 114  *  day-of-week from Monday (1) to Sunday (7)
 115  * <li>{@link #WEEK_OF_WEEK_BASED_YEAR} - the week within the week-based-year
 116  * <li>{@link #WEEK_BASED_YEAR WEEK_BASED_YEAR} - the week-based-year
 117  * </ul><p>
 118  * The week-based-year itself is defined relative to the standard ISO proleptic year.
 119  * It differs from the standard year in that it always starts on a Monday.
 120  * <p>
 121  * The first week of a week-based-year is the first Monday-based week of the standard
 122  * ISO year that has at least 4 days in the new year.
 123  * <p><ul>
 124  * <li>If January 1st is Monday then week 1 starts on January 1st
 125  * <li>If January 1st is Tuesday then week 1 starts on December 31st of the previous standard year
 126  * <li>If January 1st is Wednesday then week 1 starts on December 30th of the previous standard year
 127  * <li>If January 1st is Thursday then week 1 starts on December 29th of the previous standard year
 128  * <li>If January 1st is Friday then week 1 starts on January 4th
 129  * <li>If January 1st is Saturday then week 1 starts on January 3rd
 130  * <li>If January 1st is Sunday then week 1 starts on January 2nd
 131  * </ul><p>
 132  * There are 52 weeks in most week-based years, however on occasion there are 53 weeks.
 133  * <p>
 134  * For example:
 135  * <p>
 136  * <table cellpadding="0" cellspacing="3" border="0" style="text-align: left; width: 50%;">
 137  * <caption>Examples of Week based Years</caption>
 138  * <tr><th>Date</th><th>Day-of-week</th><th>Field values</th></tr>
 139  * <tr><th>2008-12-28</th><td>Sunday</td><td>Week 52 of week-based-year 2008</td></tr>
 140  * <tr><th>2008-12-29</th><td>Monday</td><td>Week 1 of week-based-year 2009</td></tr>
 141  * <tr><th>2008-12-31</th><td>Wednesday</td><td>Week 1 of week-based-year 2009</td></tr>
 142  * <tr><th>2009-01-01</th><td>Thursday</td><td>Week 1 of week-based-year 2009</td></tr>
 143  * <tr><th>2009-01-04</th><td>Sunday</td><td>Week 1 of week-based-year 2009</td></tr>
 144  * <tr><th>2009-01-05</th><td>Monday</td><td>Week 2 of week-based-year 2009</td></tr>
 145  * </table>
 146  *
 147  * <h3>Specification for implementors</h3>
 148  * <p>
 149  * This class is immutable and thread-safe.
 150  *
 151  * @since 1.8
 152  */
 153 public final class IsoFields {
 154 
 155     /**
 156      * The field that represents the day-of-quarter.
 157      * <p>
 158      * This field allows the day-of-quarter value to be queried and set.
 159      * The day-of-quarter has values from 1 to 90 in Q1 of a standard year, from 1 to 91
 160      * in Q1 of a leap year, from 1 to 91 in Q2 and from 1 to 92 in Q3 and Q4.
 161      * <p>
 162      * The day-of-quarter can only be calculated if the day-of-year, month-of-year and year
 163      * are available.
 164      * <p>
 165      * When setting this field, the value is allowed to be partially lenient, taking any
 166      * value from 1 to 92. If the quarter has less than 92 days, then day 92, and
 167      * potentially day 91, is in the following quarter.
 168      * <p>
 169      * This unit is an immutable and thread-safe singleton.
 170      */
 171     public static final TemporalField DAY_OF_QUARTER = Field.DAY_OF_QUARTER;
 172     /**
 173      * The field that represents the quarter-of-year.
 174      * <p>
 175      * This field allows the quarter-of-year value to be queried and set.
 176      * The quarter-of-year has values from 1 to 4.
 177      * <p>
 178      * The day-of-quarter can only be calculated if the month-of-year is available.
 179      * <p>
 180      * This unit is an immutable and thread-safe singleton.
 181      */
 182     public static final TemporalField QUARTER_OF_YEAR = Field.QUARTER_OF_YEAR;
 183     /**
 184      * The field that represents the week-of-week-based-year.
 185      * <p>
 186      * This field allows the week of the week-based-year value to be queried and set.
 187      * <p>
 188      * This unit is an immutable and thread-safe singleton.
 189      */
 190     public static final TemporalField WEEK_OF_WEEK_BASED_YEAR = Field.WEEK_OF_WEEK_BASED_YEAR;
 191     /**
 192      * The field that represents the week-based-year.
 193      * <p>
 194      * This field allows the week-based-year value to be queried and set.
 195      * <p>
 196      * This unit is an immutable and thread-safe singleton.
 197      */
 198     public static final TemporalField WEEK_BASED_YEAR = Field.WEEK_BASED_YEAR;
 199     /**
 200      * The unit that represents week-based-years for the purpose of addition and subtraction.
 201      * <p>
 202      * This allows a number of week-based-years to be added to, or subtracted from, a date.
 203      * The unit is equal to either 52 or 53 weeks.
 204      * The estimated duration of a week-based-year is the same as that of a standard ISO
 205      * year at {@code 365.2425 Days}.
 206      * <p>
 207      * The rules for addition add the number of week-based-years to the existing value
 208      * for the week-based-year field. If the resulting week-based-year only has 52 weeks,
 209      * then the date will be in week 1 of the following week-based-year.
 210      * <p>
 211      * This unit is an immutable and thread-safe singleton.
 212      */
 213     public static final TemporalUnit WEEK_BASED_YEARS = Unit.WEEK_BASED_YEARS;
 214     /**
 215      * Unit that represents the concept of a quarter-year.
 216      * For the ISO calendar system, it is equal to 3 months.
 217      * The estimated duration of a quarter-year is one quarter of {@code 365.2425 Days}.
 218      * <p>
 219      * This unit is an immutable and thread-safe singleton.
 220      */
 221     public static final TemporalUnit QUARTER_YEARS = Unit.QUARTER_YEARS;
 222 
 223     /**
 224      * Restricted constructor.
 225      */
 226     private IsoFields() {
 227         throw new AssertionError("Not instantiable");
 228     }
 229 
 230     //-----------------------------------------------------------------------
 231     /**
 232      * Implementation of the field.
 233      */
 234     private static enum Field implements TemporalField {
 235         DAY_OF_QUARTER {
 236             @Override
 237             public String getName() {
 238                 return "DayOfQuarter";
 239             }
 240             @Override
 241             public TemporalUnit getBaseUnit() {
 242                 return DAYS;
 243             }
 244             @Override
 245             public TemporalUnit getRangeUnit() {
 246                 return QUARTER_YEARS;
 247             }
 248             @Override
 249             public ValueRange range() {
 250                 return ValueRange.of(1, 90, 92);
 251             }
 252             @Override
 253             public boolean isSupportedBy(TemporalAccessor temporal) {
 254                 return temporal.isSupported(DAY_OF_YEAR) && temporal.isSupported(MONTH_OF_YEAR) &&
 255                         temporal.isSupported(YEAR) && isIso(temporal);
 256             }
 257             @Override
 258             public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
 259                 if (isSupportedBy(temporal) == false) {
 260                     throw new UnsupportedTemporalTypeException("Unsupported field: DayOfQuarter");
 261                 }
 262                 long qoy = temporal.getLong(QUARTER_OF_YEAR);
 263                 if (qoy == 1) {
 264                     long year = temporal.getLong(YEAR);
 265                     return (IsoChronology.INSTANCE.isLeapYear(year) ? ValueRange.of(1, 91) : ValueRange.of(1, 90));
 266                 } else if (qoy == 2) {
 267                     return ValueRange.of(1, 91);
 268                 } else if (qoy == 3 || qoy == 4) {
 269                     return ValueRange.of(1, 92);
 270                 } // else value not from 1 to 4, so drop through
 271                 return range();
 272             }
 273             @Override
 274             public long getFrom(TemporalAccessor temporal) {
 275                 if (isSupportedBy(temporal) == false) {
 276                     throw new UnsupportedTemporalTypeException("Unsupported field: DayOfQuarter");
 277                 }
 278                 int doy = temporal.get(DAY_OF_YEAR);
 279                 int moy = temporal.get(MONTH_OF_YEAR);
 280                 long year = temporal.getLong(YEAR);
 281                 return doy - QUARTER_DAYS[((moy - 1) / 3) + (IsoChronology.INSTANCE.isLeapYear(year) ? 4 : 0)];
 282             }
 283             @SuppressWarnings("unchecked")
 284             @Override
 285             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
 286                 // calls getFrom() to check if supported
 287                 long curValue = getFrom(temporal);
 288                 range().checkValidValue(newValue, this);  // leniently check from 1 to 92 TODO: check
 289                 return (R) temporal.with(DAY_OF_YEAR, temporal.getLong(DAY_OF_YEAR) + (newValue - curValue));
 290             }
 291             @Override
 292             public Map<TemporalField, Long> resolve(TemporalAccessor temporal, long value) {
 293                 if ((temporal.isSupported(YEAR) && temporal.isSupported(QUARTER_OF_YEAR)) == false) {
 294                     return null;
 295                 }
 296                 int y = temporal.get(YEAR);
 297                 int qoy = temporal.get(QUARTER_OF_YEAR);
 298                 range().checkValidValue(value, this);  // leniently check from 1 to 92 TODO: check
 299                 LocalDate date = LocalDate.of(y, ((qoy - 1) * 3) + 1, 1).plusDays(value - 1);
 300                 Map<TemporalField, Long> result = new HashMap<>(4, 1.0f);
 301                 result.put(EPOCH_DAY, date.toEpochDay());
 302                 result.put(YEAR, null);
 303                 result.put(QUARTER_OF_YEAR, null);
 304                 return result;
 305             }
 306         },
 307         QUARTER_OF_YEAR {
 308             @Override
 309             public String getName() {
 310                 return "QuarterOfYear";
 311             }
 312             @Override
 313             public TemporalUnit getBaseUnit() {
 314                 return QUARTER_YEARS;
 315             }
 316             @Override
 317             public TemporalUnit getRangeUnit() {
 318                 return YEARS;
 319             }
 320             @Override
 321             public ValueRange range() {
 322                 return ValueRange.of(1, 4);
 323             }
 324             @Override
 325             public boolean isSupportedBy(TemporalAccessor temporal) {
 326                 return temporal.isSupported(MONTH_OF_YEAR) && isIso(temporal);
 327             }
 328             @Override
 329             public long getFrom(TemporalAccessor temporal) {
 330                 if (isSupportedBy(temporal) == false) {
 331                     throw new UnsupportedTemporalTypeException("Unsupported field: QuarterOfYear");
 332                 }
 333                 long moy = temporal.getLong(MONTH_OF_YEAR);
 334                 return ((moy + 2) / 3);
 335             }
 336             @SuppressWarnings("unchecked")
 337             @Override
 338             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
 339                 // calls getFrom() to check if supported
 340                 long curValue = getFrom(temporal);
 341                 range().checkValidValue(newValue, this);  // strictly check from 1 to 4
 342                 return (R) temporal.with(MONTH_OF_YEAR, temporal.getLong(MONTH_OF_YEAR) + (newValue - curValue) * 3);
 343             }
 344         },
 345         WEEK_OF_WEEK_BASED_YEAR {
 346             @Override
 347             public String getName() {
 348                 return "WeekOfWeekBasedYear";
 349             }
 350 
 351             @Override
 352             public String getDisplayName(Locale locale) {
 353                 Objects.requireNonNull(locale, "locale");
 354                 LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased()
 355                                             .getLocaleResources(locale);
 356                 ResourceBundle rb = lr.getFormatData();
 357                 return rb.containsKey("field.week") ? rb.getString("field.week") : getName();
 358             }
 359 
 360             @Override
 361             public TemporalUnit getBaseUnit() {
 362                 return WEEKS;
 363             }
 364             @Override
 365             public TemporalUnit getRangeUnit() {
 366                 return WEEK_BASED_YEARS;
 367             }
 368             @Override
 369             public ValueRange range() {
 370                 return ValueRange.of(1, 52, 53);
 371             }
 372             @Override
 373             public boolean isSupportedBy(TemporalAccessor temporal) {
 374                 return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
 375             }
 376             @Override
 377             public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
 378                 if (isSupportedBy(temporal) == false) {
 379                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekOfWeekBasedYear");
 380                 }
 381                 return getWeekRange(LocalDate.from(temporal));
 382             }
 383             @Override
 384             public long getFrom(TemporalAccessor temporal) {
 385                 if (isSupportedBy(temporal) == false) {
 386                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekOfWeekBasedYear");
 387                 }
 388                 return getWeek(LocalDate.from(temporal));
 389             }
 390             @SuppressWarnings("unchecked")
 391             @Override
 392             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
 393                 // calls getFrom() to check if supported
 394                 range().checkValidValue(newValue, this);  // lenient range
 395                 return (R) temporal.plus(Math.subtractExact(newValue, getFrom(temporal)), WEEKS);
 396             }
 397             @Override
 398             public Map<TemporalField, Long> resolve(TemporalAccessor temporal, long value) {
 399                 if ((temporal.isSupported(WEEK_BASED_YEAR) && temporal.isSupported(DAY_OF_WEEK)) == false) {
 400                     return null;
 401                 }
 402                 int wby = temporal.get(WEEK_BASED_YEAR);
 403                 int dow = temporal.get(DAY_OF_WEEK);
 404                 range().checkValidValue(value, this);  // lenient range
 405                 LocalDate date = LocalDate.of(wby, 1, 4).plusWeeks(value - 1).with(DAY_OF_WEEK, dow);
 406                 Map<TemporalField, Long> result = new HashMap<>(2, 1.0f);
 407                 result.put(EPOCH_DAY, date.toEpochDay());
 408                 result.put(WEEK_BASED_YEAR, null);
 409                 result.put(DAY_OF_WEEK, null);
 410                 return result;
 411             }
 412         },
 413         WEEK_BASED_YEAR {
 414             @Override
 415             public String getName() {
 416                 return "WeekBasedYear";
 417             }
 418             @Override
 419             public TemporalUnit getBaseUnit() {
 420                 return WEEK_BASED_YEARS;
 421             }
 422             @Override
 423             public TemporalUnit getRangeUnit() {
 424                 return FOREVER;
 425             }
 426             @Override
 427             public ValueRange range() {
 428                 return YEAR.range();
 429             }
 430             @Override
 431             public boolean isSupportedBy(TemporalAccessor temporal) {
 432                 return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
 433             }
 434             @Override
 435             public long getFrom(TemporalAccessor temporal) {
 436                 if (isSupportedBy(temporal) == false) {
 437                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
 438                 }
 439                 return getWeekBasedYear(LocalDate.from(temporal));
 440             }
 441             @SuppressWarnings("unchecked")
 442             @Override
 443             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
 444                 if (isSupportedBy(temporal) == false) {
 445                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
 446                 }
 447                 int newVal = range().checkValidIntValue(newValue, WEEK_BASED_YEAR);  // strict check
 448                 LocalDate date = LocalDate.from(temporal);
 449                 int week = getWeek(date);
 450                 date = date.withDayOfYear(180).withYear(newVal).with(WEEK_OF_WEEK_BASED_YEAR, week);
 451                 return (R) date.with(date);
 452             }
 453         };
 454 
 455         @Override
 456         public boolean isDateBased() {
 457             return true;
 458         }
 459 
 460         @Override
 461         public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
 462             return range();
 463         }
 464 
 465         @Override
 466         public String toString() {
 467             return getName();
 468         }
 469 
 470         //-------------------------------------------------------------------------
 471         private static final int[] QUARTER_DAYS = {0, 90, 181, 273, 0, 91, 182, 274};
 472 
 473         private static boolean isIso(TemporalAccessor temporal) {
 474             return Chronology.from(temporal).equals(IsoChronology.INSTANCE);
 475         }
 476 
 477         private static ValueRange getWeekRange(LocalDate date) {
 478             int wby = getWeekBasedYear(date);
 479             date = date.withDayOfYear(1).withYear(wby);
 480             // 53 weeks if standard year starts on Thursday, or Wed in a leap year
 481             if (date.getDayOfWeek() == THURSDAY || (date.getDayOfWeek() == WEDNESDAY && date.isLeapYear())) {
 482                 return ValueRange.of(1, 53);
 483             }
 484             return ValueRange.of(1, 52);
 485         }
 486 
 487         private static int getWeek(LocalDate date) {
 488             int dow0 = date.getDayOfWeek().ordinal();
 489             int doy0 = date.getDayOfYear() - 1;
 490             int doyThu0 = doy0 + (3 - dow0);  // adjust to mid-week Thursday (which is 3 indexed from zero)
 491             int alignedWeek = doyThu0 / 7;
 492             int firstThuDoy0 = doyThu0 - (alignedWeek * 7);
 493             int firstMonDoy0 = firstThuDoy0 - 3;
 494             if (firstMonDoy0 < -3) {
 495                 firstMonDoy0 += 7;
 496             }
 497             if (doy0 < firstMonDoy0) {
 498                 return (int) getWeekRange(date.withDayOfYear(180).minusYears(1)).getMaximum();
 499             }
 500             int week = ((doy0 - firstMonDoy0) / 7) + 1;
 501             if (week == 53) {
 502                 if ((firstMonDoy0 == -3 || (firstMonDoy0 == -2 && date.isLeapYear())) == false) {
 503                     week = 1;
 504                 }
 505             }
 506             return week;
 507         }
 508 
 509         private static int getWeekBasedYear(LocalDate date) {
 510             int year = date.getYear();
 511             int doy = date.getDayOfYear();
 512             if (doy <= 3) {
 513                 int dow = date.getDayOfWeek().ordinal();
 514                 if (doy - dow < -2) {
 515                     year--;
 516                 }
 517             } else if (doy >= 363) {
 518                 int dow = date.getDayOfWeek().ordinal();
 519                 doy = doy - 363 - (date.isLeapYear() ? 1 : 0);
 520                 if (doy - dow >= 0) {
 521                     year++;
 522                 }
 523             }
 524             return year;
 525         }
 526     }
 527 
 528     //-----------------------------------------------------------------------
 529     /**
 530      * Implementation of the period unit.
 531      */
 532     private static enum Unit implements TemporalUnit {
 533 
 534         /**
 535          * Unit that represents the concept of a week-based-year.
 536          */
 537         WEEK_BASED_YEARS("WeekBasedYears", Duration.ofSeconds(31556952L)),
 538         /**
 539          * Unit that represents the concept of a quarter-year.
 540          */
 541         QUARTER_YEARS("QuarterYears", Duration.ofSeconds(31556952L / 4));
 542 
 543         private final String name;
 544         private final Duration duration;
 545 
 546         private Unit(String name, Duration estimatedDuration) {
 547             this.name = name;
 548             this.duration = estimatedDuration;
 549         }
 550 
 551         @Override
 552         public String getName() {
 553             return name;
 554         }
 555 
 556         @Override
 557         public Duration getDuration() {
 558             return duration;
 559         }
 560 
 561         @Override
 562         public boolean isDurationEstimated() {
 563             return true;
 564         }
 565 
 566         @Override
 567         public boolean isSupportedBy(Temporal temporal) {
 568             return temporal.isSupported(EPOCH_DAY);
 569         }
 570 
 571         @SuppressWarnings("unchecked")
 572         @Override
 573         public <R extends Temporal> R addTo(R temporal, long amount) {
 574             switch(this) {
 575                 case WEEK_BASED_YEARS:
 576                     return (R) temporal.with(WEEK_BASED_YEAR,
 577                             Math.addExact(temporal.get(WEEK_BASED_YEAR), amount));
 578                 case QUARTER_YEARS:
 579                     // no overflow (256 is multiple of 4)
 580                     return (R) temporal.plus(amount / 256, YEARS)
 581                             .plus((amount % 256) * 3, MONTHS);
 582                 default:
 583                     throw new IllegalStateException("Unreachable");
 584             }
 585         }
 586 
 587         @Override
 588         public long between(Temporal temporal1, Temporal temporal2) {
 589             switch(this) {
 590                 case WEEK_BASED_YEARS:
 591                     return Math.subtractExact(temporal2.getLong(WEEK_BASED_YEAR),
 592                             temporal1.getLong(WEEK_BASED_YEAR));
 593                 case QUARTER_YEARS:
 594                     return temporal1.periodUntil(temporal2, MONTHS) / 3;
 595                 default:
 596                     throw new IllegalStateException("Unreachable");
 597             }
 598         }
 599 
 600         @Override
 601         public String toString() {
 602             return getName();
 603 
 604         }
 605     }
 606 }