1/*
2 * Copyright (c) 2012, 2017, 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 */
57package java.time.temporal;
58
59import static java.time.DayOfWeek.THURSDAY;
60import static java.time.DayOfWeek.WEDNESDAY;
61import static java.time.temporal.ChronoField.DAY_OF_WEEK;
62import static java.time.temporal.ChronoField.DAY_OF_YEAR;
63import static java.time.temporal.ChronoField.EPOCH_DAY;
64import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
65import static java.time.temporal.ChronoField.YEAR;
66import static java.time.temporal.ChronoUnit.DAYS;
67import static java.time.temporal.ChronoUnit.FOREVER;
68import static java.time.temporal.ChronoUnit.MONTHS;
69import static java.time.temporal.ChronoUnit.WEEKS;
70import static java.time.temporal.ChronoUnit.YEARS;
71
72import java.time.DateTimeException;
73import java.time.Duration;
74import java.time.LocalDate;
75import java.time.chrono.ChronoLocalDate;
76import java.time.chrono.Chronology;
77import java.time.chrono.IsoChronology;
78import java.time.format.ResolverStyle;
79import java.util.Locale;
80import java.util.Map;
81import java.util.Objects;
82import java.util.ResourceBundle;
83
84import sun.util.locale.provider.LocaleProviderAdapter;
85import sun.util.locale.provider.LocaleResources;
86
87/**
88 * Fields and units specific to the ISO-8601 calendar system,
89 * including quarter-of-year and week-based-year.
90 * <p>
91 * This class defines fields and units that are specific to the ISO calendar system.
92 *
93 * <h3>Quarter of year</h3>
94 * The ISO-8601 standard is based on the standard civic 12 month year.
95 * This is commonly divided into four quarters, often abbreviated as Q1, Q2, Q3 and Q4.
96 * <p>
97 * January, February and March are in Q1.
98 * April, May and June are in Q2.
99 * July, August and September are in Q3.
100 * October, November and December are in Q4.
101 * <p>
102 * The complete date is expressed using three fields:
103 * <ul>
104 * <li>{@link #DAY_OF_QUARTER DAY_OF_QUARTER} - the day within the quarter, from 1 to 90, 91 or 92
105 * <li>{@link #QUARTER_OF_YEAR QUARTER_OF_YEAR} - the quarter within the year, from 1 to 4
106 * <li>{@link ChronoField#YEAR YEAR} - the standard ISO year
107 * </ul>
108 *
109 * <h3>Week based years</h3>
110 * The ISO-8601 standard was originally intended as a data interchange format,
111 * defining a string format for dates and times. However, it also defines an
112 * alternate way of expressing the date, based on the concept of week-based-year.
113 * <p>
114 * The date is expressed using three fields:
115 * <ul>
116 * <li>{@link ChronoField#DAY_OF_WEEK DAY_OF_WEEK} - the standard field defining the
117 *  day-of-week from Monday (1) to Sunday (7)
118 * <li>{@link #WEEK_OF_WEEK_BASED_YEAR} - the week within the week-based-year
119 * <li>{@link #WEEK_BASED_YEAR WEEK_BASED_YEAR} - the week-based-year
120 * </ul>
121 * The week-based-year itself is defined relative to the standard ISO proleptic year.
122 * It differs from the standard year in that it always starts on a Monday.
123 * <p>
124 * The first week of a week-based-year is the first Monday-based week of the standard
125 * ISO year that has at least 4 days in the new year.
126 * <ul>
127 * <li>If January 1st is Monday then week 1 starts on January 1st
128 * <li>If January 1st is Tuesday then week 1 starts on December 31st of the previous standard year
129 * <li>If January 1st is Wednesday then week 1 starts on December 30th of the previous standard year
130 * <li>If January 1st is Thursday then week 1 starts on December 29th of the previous standard year
131 * <li>If January 1st is Friday then week 1 starts on January 4th
132 * <li>If January 1st is Saturday then week 1 starts on January 3rd
133 * <li>If January 1st is Sunday then week 1 starts on January 2nd
134 * </ul>
135 * There are 52 weeks in most week-based years, however on occasion there are 53 weeks.
136 * <p>
137 * For example:
138 *
139 * <table class=striped style="text-align: left; width: 50%;">
140 * <caption>Examples of Week based Years</caption>
141 * <thead>
142 * <tr><th>Date</th><th>Day-of-week</th><th>Field values</th></tr>
143 * </thead>
144 * <tbody>
145 * <tr><th>2008-12-28</th><td>Sunday</td><td>Week 52 of week-based-year 2008</td></tr>
146 * <tr><th>2008-12-29</th><td>Monday</td><td>Week 1 of week-based-year 2009</td></tr>
147 * <tr><th>2008-12-31</th><td>Wednesday</td><td>Week 1 of week-based-year 2009</td></tr>
148 * <tr><th>2009-01-01</th><td>Thursday</td><td>Week 1 of week-based-year 2009</td></tr>
149 * <tr><th>2009-01-04</th><td>Sunday</td><td>Week 1 of week-based-year 2009</td></tr>
150 * <tr><th>2009-01-05</th><td>Monday</td><td>Week 2 of week-based-year 2009</td></tr>
151 * </tbody>
152 * </table>
153 *
154 * @implSpec
155 * <p>
156 * This class is immutable and thread-safe.
157 *
158 * @since 1.8
159 */
160public final class IsoFields {
161
162    /**
163     * The field that represents the day-of-quarter.
164     * <p>
165     * This field allows the day-of-quarter value to be queried and set.
166     * The day-of-quarter has values from 1 to 90 in Q1 of a standard year, from 1 to 91
167     * in Q1 of a leap year, from 1 to 91 in Q2 and from 1 to 92 in Q3 and Q4.
168     * <p>
169     * The day-of-quarter can only be calculated if the day-of-year, month-of-year and year
170     * are available.
171     * <p>
172     * When setting this field, the value is allowed to be partially lenient, taking any
173     * value from 1 to 92. If the quarter has less than 92 days, then day 92, and
174     * potentially day 91, is in the following quarter.
175     * <p>
176     * In the resolving phase of parsing, a date can be created from a year,
177     * quarter-of-year and day-of-quarter.
178     * <p>
179     * In {@linkplain ResolverStyle#STRICT strict mode}, all three fields are
180     * validated against their range of valid values. The day-of-quarter field
181     * is validated from 1 to 90, 91 or 92 depending on the year and quarter.
182     * <p>
183     * In {@linkplain ResolverStyle#SMART smart mode}, all three fields are
184     * validated against their range of valid values. The day-of-quarter field is
185     * validated between 1 and 92, ignoring the actual range based on the year and quarter.
186     * If the day-of-quarter exceeds the actual range by one day, then the resulting date
187     * is one day later. If the day-of-quarter exceeds the actual range by two days,
188     * then the resulting date is two days later.
189     * <p>
190     * In {@linkplain ResolverStyle#LENIENT lenient mode}, only the year is validated
191     * against the range of valid values. The resulting date is calculated equivalent to
192     * the following three stage approach. First, create a date on the first of January
193     * in the requested year. Then take the quarter-of-year, subtract one, and add the
194     * amount in quarters to the date. Finally, take the day-of-quarter, subtract one,
195     * and add the amount in days to the date.
196     * <p>
197     * This unit is an immutable and thread-safe singleton.
198     */
199    public static final TemporalField DAY_OF_QUARTER = Field.DAY_OF_QUARTER;
200    /**
201     * The field that represents the quarter-of-year.
202     * <p>
203     * This field allows the quarter-of-year value to be queried and set.
204     * The quarter-of-year has values from 1 to 4.
205     * <p>
206     * The quarter-of-year can only be calculated if the month-of-year is available.
207     * <p>
208     * In the resolving phase of parsing, a date can be created from a year,
209     * quarter-of-year and day-of-quarter.
210     * See {@link #DAY_OF_QUARTER} for details.
211     * <p>
212     * This unit is an immutable and thread-safe singleton.
213     */
214    public static final TemporalField QUARTER_OF_YEAR = Field.QUARTER_OF_YEAR;
215    /**
216     * The field that represents the week-of-week-based-year.
217     * <p>
218     * This field allows the week of the week-based-year value to be queried and set.
219     * The week-of-week-based-year has values from 1 to 52, or 53 if the
220     * week-based-year has 53 weeks.
221     * <p>
222     * In the resolving phase of parsing, a date can be created from a
223     * week-based-year, week-of-week-based-year and day-of-week.
224     * <p>
225     * In {@linkplain ResolverStyle#STRICT strict mode}, all three fields are
226     * validated against their range of valid values. The week-of-week-based-year
227     * field is validated from 1 to 52 or 53 depending on the week-based-year.
228     * <p>
229     * In {@linkplain ResolverStyle#SMART smart mode}, all three fields are
230     * validated against their range of valid values. The week-of-week-based-year
231     * field is validated between 1 and 53, ignoring the week-based-year.
232     * If the week-of-week-based-year is 53, but the week-based-year only has
233     * 52 weeks, then the resulting date is in week 1 of the following week-based-year.
234     * <p>
235     * In {@linkplain ResolverStyle#LENIENT lenient mode}, only the week-based-year
236     * is validated against the range of valid values. If the day-of-week is outside
237     * the range 1 to 7, then the resulting date is adjusted by a suitable number of
238     * weeks to reduce the day-of-week to the range 1 to 7. If the week-of-week-based-year
239     * value is outside the range 1 to 52, then any excess weeks are added or subtracted
240     * from the resulting date.
241     * <p>
242     * This unit is an immutable and thread-safe singleton.
243     */
244    public static final TemporalField WEEK_OF_WEEK_BASED_YEAR = Field.WEEK_OF_WEEK_BASED_YEAR;
245    /**
246     * The field that represents the week-based-year.
247     * <p>
248     * This field allows the week-based-year value to be queried and set.
249     * <p>
250     * The field has a range that matches {@link LocalDate#MAX} and {@link LocalDate#MIN}.
251     * <p>
252     * In the resolving phase of parsing, a date can be created from a
253     * week-based-year, week-of-week-based-year and day-of-week.
254     * See {@link #WEEK_OF_WEEK_BASED_YEAR} for details.
255     * <p>
256     * This unit is an immutable and thread-safe singleton.
257     */
258    public static final TemporalField WEEK_BASED_YEAR = Field.WEEK_BASED_YEAR;
259    /**
260     * The unit that represents week-based-years for the purpose of addition and subtraction.
261     * <p>
262     * This allows a number of week-based-years to be added to, or subtracted from, a date.
263     * The unit is equal to either 52 or 53 weeks.
264     * The estimated duration of a week-based-year is the same as that of a standard ISO
265     * year at {@code 365.2425 Days}.
266     * <p>
267     * The rules for addition add the number of week-based-years to the existing value
268     * for the week-based-year field. If the resulting week-based-year only has 52 weeks,
269     * then the date will be in week 1 of the following week-based-year.
270     * <p>
271     * This unit is an immutable and thread-safe singleton.
272     */
273    public static final TemporalUnit WEEK_BASED_YEARS = Unit.WEEK_BASED_YEARS;
274    /**
275     * Unit that represents the concept of a quarter-year.
276     * For the ISO calendar system, it is equal to 3 months.
277     * The estimated duration of a quarter-year is one quarter of {@code 365.2425 Days}.
278     * <p>
279     * This unit is an immutable and thread-safe singleton.
280     */
281    public static final TemporalUnit QUARTER_YEARS = Unit.QUARTER_YEARS;
282
283    /**
284     * Restricted constructor.
285     */
286    private IsoFields() {
287        throw new AssertionError("Not instantiable");
288    }
289
290    //-----------------------------------------------------------------------
291    /**
292     * Implementation of the field.
293     */
294    private static enum Field implements TemporalField {
295        DAY_OF_QUARTER {
296            @Override
297            public TemporalUnit getBaseUnit() {
298                return DAYS;
299            }
300            @Override
301            public TemporalUnit getRangeUnit() {
302                return QUARTER_YEARS;
303            }
304            @Override
305            public ValueRange range() {
306                return ValueRange.of(1, 90, 92);
307            }
308            @Override
309            public boolean isSupportedBy(TemporalAccessor temporal) {
310                return temporal.isSupported(DAY_OF_YEAR) && temporal.isSupported(MONTH_OF_YEAR) &&
311                        temporal.isSupported(YEAR) && isIso(temporal);
312            }
313            @Override
314            public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
315                if (isSupportedBy(temporal) == false) {
316                    throw new UnsupportedTemporalTypeException("Unsupported field: DayOfQuarter");
317                }
318                long qoy = temporal.getLong(QUARTER_OF_YEAR);
319                if (qoy == 1) {
320                    long year = temporal.getLong(YEAR);
321                    return (IsoChronology.INSTANCE.isLeapYear(year) ? ValueRange.of(1, 91) : ValueRange.of(1, 90));
322                } else if (qoy == 2) {
323                    return ValueRange.of(1, 91);
324                } else if (qoy == 3 || qoy == 4) {
325                    return ValueRange.of(1, 92);
326                } // else value not from 1 to 4, so drop through
327                return range();
328            }
329            @Override
330            public long getFrom(TemporalAccessor temporal) {
331                if (isSupportedBy(temporal) == false) {
332                    throw new UnsupportedTemporalTypeException("Unsupported field: DayOfQuarter");
333                }
334                int doy = temporal.get(DAY_OF_YEAR);
335                int moy = temporal.get(MONTH_OF_YEAR);
336                long year = temporal.getLong(YEAR);
337                return doy - QUARTER_DAYS[((moy - 1) / 3) + (IsoChronology.INSTANCE.isLeapYear(year) ? 4 : 0)];
338            }
339            @SuppressWarnings("unchecked")
340            @Override
341            public <R extends Temporal> R adjustInto(R temporal, long newValue) {
342                // calls getFrom() to check if supported
343                long curValue = getFrom(temporal);
344                range().checkValidValue(newValue, this);  // leniently check from 1 to 92 TODO: check
345                return (R) temporal.with(DAY_OF_YEAR, temporal.getLong(DAY_OF_YEAR) + (newValue - curValue));
346            }
347            @Override
348            public ChronoLocalDate resolve(
349                    Map<TemporalField, Long> fieldValues, TemporalAccessor partialTemporal, ResolverStyle resolverStyle) {
350                Long yearLong = fieldValues.get(YEAR);
351                Long qoyLong = fieldValues.get(QUARTER_OF_YEAR);
352                if (yearLong == null || qoyLong == null) {
353                    return null;
354                }
355                int y = YEAR.checkValidIntValue(yearLong);  // always validate
356                long doq = fieldValues.get(DAY_OF_QUARTER);
357                ensureIso(partialTemporal);
358                LocalDate date;
359                if (resolverStyle == ResolverStyle.LENIENT) {
360                    date = LocalDate.of(y, 1, 1).plusMonths(Math.multiplyExact(Math.subtractExact(qoyLong, 1), 3));
361                    doq = Math.subtractExact(doq, 1);
362                } else {
363                    int qoy = QUARTER_OF_YEAR.range().checkValidIntValue(qoyLong, QUARTER_OF_YEAR);  // validated
364                    date = LocalDate.of(y, ((qoy - 1) * 3) + 1, 1);
365                    if (doq < 1 || doq > 90) {
366                        if (resolverStyle == ResolverStyle.STRICT) {
367                            rangeRefinedBy(date).checkValidValue(doq, this);  // only allow exact range
368                        } else {  // SMART
369                            range().checkValidValue(doq, this);  // allow 1-92 rolling into next quarter
370                        }
371                    }
372                    doq--;
373                }
374                fieldValues.remove(this);
375                fieldValues.remove(YEAR);
376                fieldValues.remove(QUARTER_OF_YEAR);
377                return date.plusDays(doq);
378            }
379            @Override
380            public String toString() {
381                return "DayOfQuarter";
382            }
383        },
384        QUARTER_OF_YEAR {
385            @Override
386            public TemporalUnit getBaseUnit() {
387                return QUARTER_YEARS;
388            }
389            @Override
390            public TemporalUnit getRangeUnit() {
391                return YEARS;
392            }
393            @Override
394            public ValueRange range() {
395                return ValueRange.of(1, 4);
396            }
397            @Override
398            public boolean isSupportedBy(TemporalAccessor temporal) {
399                return temporal.isSupported(MONTH_OF_YEAR) && isIso(temporal);
400            }
401            @Override
402            public long getFrom(TemporalAccessor temporal) {
403                if (isSupportedBy(temporal) == false) {
404                    throw new UnsupportedTemporalTypeException("Unsupported field: QuarterOfYear");
405                }
406                long moy = temporal.getLong(MONTH_OF_YEAR);
407                return ((moy + 2) / 3);
408            }
409            public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
410                if (isSupportedBy(temporal) == false) {
411                    throw new UnsupportedTemporalTypeException("Unsupported field: QuarterOfYear");
412                }
413                return super.rangeRefinedBy(temporal);
414            }
415            @SuppressWarnings("unchecked")
416            @Override
417            public <R extends Temporal> R adjustInto(R temporal, long newValue) {
418                // calls getFrom() to check if supported
419                long curValue = getFrom(temporal);
420                range().checkValidValue(newValue, this);  // strictly check from 1 to 4
421                return (R) temporal.with(MONTH_OF_YEAR, temporal.getLong(MONTH_OF_YEAR) + (newValue - curValue) * 3);
422            }
423            @Override
424            public String toString() {
425                return "QuarterOfYear";
426            }
427        },
428        WEEK_OF_WEEK_BASED_YEAR {
429            @Override
430            public String getDisplayName(Locale locale) {
431                Objects.requireNonNull(locale, "locale");
432                LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased()
433                                            .getLocaleResources(locale);
434                ResourceBundle rb = lr.getJavaTimeFormatData();
435                return rb.containsKey("field.week") ? rb.getString("field.week") : toString();
436            }
437
438            @Override
439            public TemporalUnit getBaseUnit() {
440                return WEEKS;
441            }
442            @Override
443            public TemporalUnit getRangeUnit() {
444                return WEEK_BASED_YEARS;
445            }
446            @Override
447            public ValueRange range() {
448                return ValueRange.of(1, 52, 53);
449            }
450            @Override
451            public boolean isSupportedBy(TemporalAccessor temporal) {
452                return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
453            }
454            @Override
455            public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
456                if (isSupportedBy(temporal) == false) {
457                    throw new UnsupportedTemporalTypeException("Unsupported field: WeekOfWeekBasedYear");
458                }
459                return getWeekRange(LocalDate.from(temporal));
460            }
461            @Override
462            public long getFrom(TemporalAccessor temporal) {
463                if (isSupportedBy(temporal) == false) {
464                    throw new UnsupportedTemporalTypeException("Unsupported field: WeekOfWeekBasedYear");
465                }
466                return getWeek(LocalDate.from(temporal));
467            }
468            @SuppressWarnings("unchecked")
469            @Override
470            public <R extends Temporal> R adjustInto(R temporal, long newValue) {
471                // calls getFrom() to check if supported
472                range().checkValidValue(newValue, this);  // lenient range
473                return (R) temporal.plus(Math.subtractExact(newValue, getFrom(temporal)), WEEKS);
474            }
475            @Override
476            public ChronoLocalDate resolve(
477                    Map<TemporalField, Long> fieldValues, TemporalAccessor partialTemporal, ResolverStyle resolverStyle) {
478                Long wbyLong = fieldValues.get(WEEK_BASED_YEAR);
479                Long dowLong = fieldValues.get(DAY_OF_WEEK);
480                if (wbyLong == null || dowLong == null) {
481                    return null;
482                }
483                int wby = WEEK_BASED_YEAR.range().checkValidIntValue(wbyLong, WEEK_BASED_YEAR);  // always validate
484                long wowby = fieldValues.get(WEEK_OF_WEEK_BASED_YEAR);
485                ensureIso(partialTemporal);
486                LocalDate date = LocalDate.of(wby, 1, 4);
487                if (resolverStyle == ResolverStyle.LENIENT) {
488                    long dow = dowLong;  // unvalidated
489                    if (dow > 7) {
490                        date = date.plusWeeks((dow - 1) / 7);
491                        dow = ((dow - 1) % 7) + 1;
492                    } else if (dow < 1) {
493                        date = date.plusWeeks(Math.subtractExact(dow,  7) / 7);
494                        dow = ((dow + 6) % 7) + 1;
495                    }
496                    date = date.plusWeeks(Math.subtractExact(wowby, 1)).with(DAY_OF_WEEK, dow);
497                } else {
498                    int dow = DAY_OF_WEEK.checkValidIntValue(dowLong);  // validated
499                    if (wowby < 1 || wowby > 52) {
500                        if (resolverStyle == ResolverStyle.STRICT) {
501                            getWeekRange(date).checkValidValue(wowby, this);  // only allow exact range
502                        } else {  // SMART
503                            range().checkValidValue(wowby, this);  // allow 1-53 rolling into next year
504                        }
505                    }
506                    date = date.plusWeeks(wowby - 1).with(DAY_OF_WEEK, dow);
507                }
508                fieldValues.remove(this);
509                fieldValues.remove(WEEK_BASED_YEAR);
510                fieldValues.remove(DAY_OF_WEEK);
511                return date;
512            }
513            @Override
514            public String toString() {
515                return "WeekOfWeekBasedYear";
516            }
517        },
518        WEEK_BASED_YEAR {
519            @Override
520            public TemporalUnit getBaseUnit() {
521                return WEEK_BASED_YEARS;
522            }
523            @Override
524            public TemporalUnit getRangeUnit() {
525                return FOREVER;
526            }
527            @Override
528            public ValueRange range() {
529                return YEAR.range();
530            }
531            @Override
532            public boolean isSupportedBy(TemporalAccessor temporal) {
533                return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
534            }
535            @Override
536            public long getFrom(TemporalAccessor temporal) {
537                if (isSupportedBy(temporal) == false) {
538                    throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
539                }
540                return getWeekBasedYear(LocalDate.from(temporal));
541            }
542            public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
543                if (isSupportedBy(temporal) == false) {
544                    throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
545                }
546                return super.rangeRefinedBy(temporal);
547            }
548            @SuppressWarnings("unchecked")
549            @Override
550            public <R extends Temporal> R adjustInto(R temporal, long newValue) {
551                if (isSupportedBy(temporal) == false) {
552                    throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
553                }
554                int newWby = range().checkValidIntValue(newValue, WEEK_BASED_YEAR);  // strict check
555                LocalDate date = LocalDate.from(temporal);
556                int dow = date.get(DAY_OF_WEEK);
557                int week = getWeek(date);
558                if (week == 53 && getWeekRange(newWby) == 52) {
559                    week = 52;
560                }
561                LocalDate resolved = LocalDate.of(newWby, 1, 4);  // 4th is guaranteed to be in week one
562                int days = (dow - resolved.get(DAY_OF_WEEK)) + ((week - 1) * 7);
563                resolved = resolved.plusDays(days);
564                return (R) temporal.with(resolved);
565            }
566            @Override
567            public String toString() {
568                return "WeekBasedYear";
569            }
570        };
571
572        @Override
573        public boolean isDateBased() {
574            return true;
575        }
576
577        @Override
578        public boolean isTimeBased() {
579            return false;
580        }
581
582        @Override
583        public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
584            return range();
585        }
586
587        //-------------------------------------------------------------------------
588        private static final int[] QUARTER_DAYS = {0, 90, 181, 273, 0, 91, 182, 274};
589
590
591        private static void ensureIso(TemporalAccessor temporal) {
592            if (isIso(temporal) == false) {
593                throw new DateTimeException("Resolve requires IsoChronology");
594            }
595        }
596
597        private static ValueRange getWeekRange(LocalDate date) {
598            int wby = getWeekBasedYear(date);
599            return ValueRange.of(1, getWeekRange(wby));
600        }
601
602        private static int getWeekRange(int wby) {
603            LocalDate date = LocalDate.of(wby, 1, 1);
604            // 53 weeks if standard year starts on Thursday, or Wed in a leap year
605            if (date.getDayOfWeek() == THURSDAY || (date.getDayOfWeek() == WEDNESDAY && date.isLeapYear())) {
606                return 53;
607            }
608            return 52;
609        }
610
611        private static int getWeek(LocalDate date) {
612            int dow0 = date.getDayOfWeek().ordinal();
613            int doy0 = date.getDayOfYear() - 1;
614            int doyThu0 = doy0 + (3 - dow0);  // adjust to mid-week Thursday (which is 3 indexed from zero)
615            int alignedWeek = doyThu0 / 7;
616            int firstThuDoy0 = doyThu0 - (alignedWeek * 7);
617            int firstMonDoy0 = firstThuDoy0 - 3;
618            if (firstMonDoy0 < -3) {
619                firstMonDoy0 += 7;
620            }
621            if (doy0 < firstMonDoy0) {
622                return (int) getWeekRange(date.withDayOfYear(180).minusYears(1)).getMaximum();
623            }
624            int week = ((doy0 - firstMonDoy0) / 7) + 1;
625            if (week == 53) {
626                if ((firstMonDoy0 == -3 || (firstMonDoy0 == -2 && date.isLeapYear())) == false) {
627                    week = 1;
628                }
629            }
630            return week;
631        }
632
633        private static int getWeekBasedYear(LocalDate date) {
634            int year = date.getYear();
635            int doy = date.getDayOfYear();
636            if (doy <= 3) {
637                int dow = date.getDayOfWeek().ordinal();
638                if (doy - dow < -2) {
639                    year--;
640                }
641            } else if (doy >= 363) {
642                int dow = date.getDayOfWeek().ordinal();
643                doy = doy - 363 - (date.isLeapYear() ? 1 : 0);
644                if (doy - dow >= 0) {
645                    year++;
646                }
647            }
648            return year;
649        }
650    }
651
652    //-----------------------------------------------------------------------
653    /**
654     * Implementation of the unit.
655     */
656    private static enum Unit implements TemporalUnit {
657
658        /**
659         * Unit that represents the concept of a week-based-year.
660         */
661        WEEK_BASED_YEARS("WeekBasedYears", Duration.ofSeconds(31556952L)),
662        /**
663         * Unit that represents the concept of a quarter-year.
664         */
665        QUARTER_YEARS("QuarterYears", Duration.ofSeconds(31556952L / 4));
666
667        private final String name;
668        private final Duration duration;
669
670        private Unit(String name, Duration estimatedDuration) {
671            this.name = name;
672            this.duration = estimatedDuration;
673        }
674
675        @Override
676        public Duration getDuration() {
677            return duration;
678        }
679
680        @Override
681        public boolean isDurationEstimated() {
682            return true;
683        }
684
685        @Override
686        public boolean isDateBased() {
687            return true;
688        }
689
690        @Override
691        public boolean isTimeBased() {
692            return false;
693        }
694
695        @Override
696        public boolean isSupportedBy(Temporal temporal) {
697            return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
698        }
699
700        @SuppressWarnings("unchecked")
701        @Override
702        public <R extends Temporal> R addTo(R temporal, long amount) {
703            switch (this) {
704                case WEEK_BASED_YEARS:
705                    return (R) temporal.with(WEEK_BASED_YEAR,
706                            Math.addExact(temporal.get(WEEK_BASED_YEAR), amount));
707                case QUARTER_YEARS:
708                    // no overflow (256 is multiple of 4)
709                    return (R) temporal.plus(amount / 256, YEARS)
710                            .plus((amount % 256) * 3, MONTHS);
711                default:
712                    throw new IllegalStateException("Unreachable");
713            }
714        }
715
716        @Override
717        public long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive) {
718            if (temporal1Inclusive.getClass() != temporal2Exclusive.getClass()) {
719                return temporal1Inclusive.until(temporal2Exclusive, this);
720            }
721            switch(this) {
722                case WEEK_BASED_YEARS:
723                    return Math.subtractExact(temporal2Exclusive.getLong(WEEK_BASED_YEAR),
724                            temporal1Inclusive.getLong(WEEK_BASED_YEAR));
725                case QUARTER_YEARS:
726                    return temporal1Inclusive.until(temporal2Exclusive, MONTHS) / 3;
727                default:
728                    throw new IllegalStateException("Unreachable");
729            }
730        }
731
732        @Override
733        public String toString() {
734            return name;
735        }
736    }
737
738    static boolean isIso(TemporalAccessor temporal) {
739        return Chronology.from(temporal).equals(IsoChronology.INSTANCE);
740    }
741}
742