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) 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.DateTimeException;
73 import java.time.Duration;
74 import java.time.LocalDate;
75 import java.time.chrono.ChronoLocalDate;
76 import java.time.chrono.Chronology;
77 import java.time.chrono.IsoChronology;
78 import java.time.format.ResolverStyle;
79 import java.util.Locale;
80 import java.util.Map;
81 import java.util.Objects;
82 import java.util.ResourceBundle;
83
84 import sun.util.locale.provider.CalendarDataUtility;
85 import sun.util.locale.provider.LocaleProviderAdapter;
86 import sun.util.locale.provider.LocaleResources;
87
88 /**
89  * Fields and units specific to the ISO-8601 calendar system,
90  * including quarter-of-year and week-based-year.
91  * <p>
92  * This class defines fields and units that are specific to the ISO calendar system.
93  *
94  * <h3>Quarter of year</h3>
95  * The ISO-8601 standard is based on the standard civic 12 month year.
96  * This is commonly divided into four quarters, often abbreviated as Q1, Q2, Q3 and Q4.
97  * <p>
98  * January, February and March are in Q1.
99  * April, May and June are in Q2.
100  * July, August and September are in Q3.
101  * October, November and December are in Q4.
102  * <p>
103  * The complete date is expressed using three fields:
104  * <ul>
105  * <li>{@link #DAY_OF_QUARTER DAY_OF_QUARTER} - the day within the quarter, from 1 to 90, 91 or 92
106  * <li>{@link #QUARTER_OF_YEAR QUARTER_OF_YEAR} - the quarter within the year, from 1 to 4
107  * <li>{@link ChronoField#YEAR YEAR} - the standard ISO year
108  * </ul>
109  *
110  * <h3>Week based years</h3>
111  * The ISO-8601 standard was originally intended as a data interchange format,
112  * defining a string format for dates and times. However, it also defines an
113  * alternate way of expressing the date, based on the concept of week-based-year.
114  * <p>
115  * The date is expressed using three fields:
116  * <ul>
117  * <li>{@link ChronoField#DAY_OF_WEEK DAY_OF_WEEK} - the standard field defining the
118  *  day-of-week from Monday (1) to Sunday (7)
119  * <li>{@link #WEEK_OF_WEEK_BASED_YEAR} - the week within the week-based-year
120  * <li>{@link #WEEK_BASED_YEAR WEEK_BASED_YEAR} - the week-based-year
121  * </ul>
122  * The week-based-year itself is defined relative to the standard ISO proleptic year.
123  * It differs from the standard year in that it always starts on a Monday.
124  * <p>
125  * The first week of a week-based-year is the first Monday-based week of the standard
126  * ISO year that has at least 4 days in the new year.
127  * <ul>
128  * <li>If January 1st is Monday then week 1 starts on January 1st
129  * <li>If January 1st is Tuesday then week 1 starts on December 31st of the previous standard year
130  * <li>If January 1st is Wednesday then week 1 starts on December 30th of the previous standard year
131  * <li>If January 1st is Thursday then week 1 starts on December 29th of the previous standard year
132  * <li>If January 1st is Friday then week 1 starts on January 4th
133  * <li>If January 1st is Saturday then week 1 starts on January 3rd
134  * <li>If January 1st is Sunday then week 1 starts on January 2nd
135  * </ul>
136  * There are 52 weeks in most week-based years, however on occasion there are 53 weeks.
137  * <p>
138  * For example:
139  *
140  * <table class=striped style="text-align: left">
141  * <caption>Examples of Week based Years</caption>
142  * <thead>
143  * <tr><th scope="col">Date</th><th scope="col">Day-of-week</th><th scope="col">Field values</th></tr>
144  * </thead>
145  * <tbody>
146  * <tr><th scope="row">2008-12-28</th><td>Sunday</td><td>Week 52 of week-based-year 2008</td></tr>
147  * <tr><th scope="row">2008-12-29</th><td>Monday</td><td>Week 1 of week-based-year 2009</td></tr>
148  * <tr><th scope="row">2008-12-31</th><td>Wednesday</td><td>Week 1 of week-based-year 2009</td></tr>
149  * <tr><th scope="row">2009-01-01</th><td>Thursday</td><td>Week 1 of week-based-year 2009</td></tr>
150  * <tr><th scope="row">2009-01-04</th><td>Sunday</td><td>Week 1 of week-based-year 2009</td></tr>
151  * <tr><th scope="row">2009-01-05</th><td>Monday</td><td>Week 2 of week-based-year 2009</td></tr>
152  * </tbody>
153  * </table>
154  *
155  * @implSpec
156  * <p>
157  * This class is immutable and thread-safe.
158  *
159  * @since 1.8
160  */

161 public final class IsoFields {
162
163     /**
164      * The field that represents the day-of-quarter.
165      * <p>
166      * This field allows the day-of-quarter value to be queried and set.
167      * The day-of-quarter has values from 1 to 90 in Q1 of a standard year, from 1 to 91
168      * in Q1 of a leap year, from 1 to 91 in Q2 and from 1 to 92 in Q3 and Q4.
169      * <p>
170      * The day-of-quarter can only be calculated if the day-of-year, month-of-year and year
171      * are available.
172      * <p>
173      * When setting this field, the value is allowed to be partially lenient, taking any
174      * value from 1 to 92. If the quarter has less than 92 days, then day 92, and
175      * potentially day 91, is in the following quarter.
176      * <p>
177      * In the resolving phase of parsing, a date can be created from a year,
178      * quarter-of-year and day-of-quarter.
179      * <p>
180      * In {@linkplain ResolverStyle#STRICT strict mode}, all three fields are
181      * validated against their range of valid values. The day-of-quarter field
182      * is validated from 1 to 90, 91 or 92 depending on the year and quarter.
183      * <p>
184      * In {@linkplain ResolverStyle#SMART smart mode}, all three fields are
185      * validated against their range of valid values. The day-of-quarter field is
186      * validated between 1 and 92, ignoring the actual range based on the year and quarter.
187      * If the day-of-quarter exceeds the actual range by one day, then the resulting date
188      * is one day later. If the day-of-quarter exceeds the actual range by two days,
189      * then the resulting date is two days later.
190      * <p>
191      * In {@linkplain ResolverStyle#LENIENT lenient mode}, only the year is validated
192      * against the range of valid values. The resulting date is calculated equivalent to
193      * the following three stage approach. First, create a date on the first of January
194      * in the requested year. Then take the quarter-of-year, subtract one, and add the
195      * amount in quarters to the date. Finally, take the day-of-quarter, subtract one,
196      * and add the amount in days to the date.
197      * <p>
198      * This unit is an immutable and thread-safe singleton.
199      */

200     public static final TemporalField DAY_OF_QUARTER = Field.DAY_OF_QUARTER;
201     /**
202      * The field that represents the quarter-of-year.
203      * <p>
204      * This field allows the quarter-of-year value to be queried and set.
205      * The quarter-of-year has values from 1 to 4.
206      * <p>
207      * The quarter-of-year can only be calculated if the month-of-year is available.
208      * <p>
209      * In the resolving phase of parsing, a date can be created from a year,
210      * quarter-of-year and day-of-quarter.
211      * See {@link #DAY_OF_QUARTER} for details.
212      * <p>
213      * This unit is an immutable and thread-safe singleton.
214      */

215     public static final TemporalField QUARTER_OF_YEAR = Field.QUARTER_OF_YEAR;
216     /**
217      * The field that represents the week-of-week-based-year.
218      * <p>
219      * This field allows the week of the week-based-year value to be queried and set.
220      * The week-of-week-based-year has values from 1 to 52, or 53 if the
221      * week-based-year has 53 weeks.
222      * <p>
223      * In the resolving phase of parsing, a date can be created from a
224      * week-based-year, week-of-week-based-year and day-of-week.
225      * <p>
226      * In {@linkplain ResolverStyle#STRICT strict mode}, all three fields are
227      * validated against their range of valid values. The week-of-week-based-year
228      * field is validated from 1 to 52 or 53 depending on the week-based-year.
229      * <p>
230      * In {@linkplain ResolverStyle#SMART smart mode}, all three fields are
231      * validated against their range of valid values. The week-of-week-based-year
232      * field is validated between 1 and 53, ignoring the week-based-year.
233      * If the week-of-week-based-year is 53, but the week-based-year only has
234      * 52 weeks, then the resulting date is in week 1 of the following week-based-year.
235      * <p>
236      * In {@linkplain ResolverStyle#LENIENT lenient mode}, only the week-based-year
237      * is validated against the range of valid values. If the day-of-week is outside
238      * the range 1 to 7, then the resulting date is adjusted by a suitable number of
239      * weeks to reduce the day-of-week to the range 1 to 7. If the week-of-week-based-year
240      * value is outside the range 1 to 52, then any excess weeks are added or subtracted
241      * from the resulting date.
242      * <p>
243      * This unit is an immutable and thread-safe singleton.
244      */

245     public static final TemporalField WEEK_OF_WEEK_BASED_YEAR = Field.WEEK_OF_WEEK_BASED_YEAR;
246     /**
247      * The field that represents the week-based-year.
248      * <p>
249      * This field allows the week-based-year value to be queried and set.
250      * <p>
251      * The field has a range that matches {@link LocalDate#MAX} and {@link LocalDate#MIN}.
252      * <p>
253      * In the resolving phase of parsing, a date can be created from a
254      * week-based-year, week-of-week-based-year and day-of-week.
255      * See {@link #WEEK_OF_WEEK_BASED_YEAR} for details.
256      * <p>
257      * This unit is an immutable and thread-safe singleton.
258      */

259     public static final TemporalField WEEK_BASED_YEAR = Field.WEEK_BASED_YEAR;
260     /**
261      * The unit that represents week-based-years for the purpose of addition and subtraction.
262      * <p>
263      * This allows a number of week-based-years to be added to, or subtracted from, a date.
264      * The unit is equal to either 52 or 53 weeks.
265      * The estimated duration of a week-based-year is the same as that of a standard ISO
266      * year at {@code 365.2425 Days}.
267      * <p>
268      * The rules for addition add the number of week-based-years to the existing value
269      * for the week-based-year field. If the resulting week-based-year only has 52 weeks,
270      * then the date will be in week 1 of the following week-based-year.
271      * <p>
272      * This unit is an immutable and thread-safe singleton.
273      */

274     public static final TemporalUnit WEEK_BASED_YEARS = Unit.WEEK_BASED_YEARS;
275     /**
276      * Unit that represents the concept of a quarter-year.
277      * For the ISO calendar system, it is equal to 3 months.
278      * The estimated duration of a quarter-year is one quarter of {@code 365.2425 Days}.
279      * <p>
280      * This unit is an immutable and thread-safe singleton.
281      */

282     public static final TemporalUnit QUARTER_YEARS = Unit.QUARTER_YEARS;
283
284     /**
285      * Restricted constructor.
286      */

287     private IsoFields() {
288         throw new AssertionError("Not instantiable");
289     }
290
291     //-----------------------------------------------------------------------
292     /**
293      * Implementation of the field.
294      */

295     private static enum Field implements TemporalField {
296         DAY_OF_QUARTER {
297             @Override
298             public TemporalUnit getBaseUnit() {
299                 return DAYS;
300             }
301             @Override
302             public TemporalUnit getRangeUnit() {
303                 return QUARTER_YEARS;
304             }
305             @Override
306             public ValueRange range() {
307                 return ValueRange.of(1, 90, 92);
308             }
309             @Override
310             public boolean isSupportedBy(TemporalAccessor temporal) {
311                 return temporal.isSupported(DAY_OF_YEAR) && temporal.isSupported(MONTH_OF_YEAR) &&
312                         temporal.isSupported(YEAR) && isIso(temporal);
313             }
314             @Override
315             public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
316                 if (isSupportedBy(temporal) == false) {
317                     throw new UnsupportedTemporalTypeException("Unsupported field: DayOfQuarter");
318                 }
319                 long qoy = temporal.getLong(QUARTER_OF_YEAR);
320                 if (qoy == 1) {
321                     long year = temporal.getLong(YEAR);
322                     return (IsoChronology.INSTANCE.isLeapYear(year) ? ValueRange.of(1, 91) : ValueRange.of(1, 90));
323                 } else if (qoy == 2) {
324                     return ValueRange.of(1, 91);
325                 } else if (qoy == 3 || qoy == 4) {
326                     return ValueRange.of(1, 92);
327                 } // else value not from 1 to 4, so drop through
328                 return range();
329             }
330             @Override
331             public long getFrom(TemporalAccessor temporal) {
332                 if (isSupportedBy(temporal) == false) {
333                     throw new UnsupportedTemporalTypeException("Unsupported field: DayOfQuarter");
334                 }
335                 int doy = temporal.get(DAY_OF_YEAR);
336                 int moy = temporal.get(MONTH_OF_YEAR);
337                 long year = temporal.getLong(YEAR);
338                 return doy - QUARTER_DAYS[((moy - 1) / 3) + (IsoChronology.INSTANCE.isLeapYear(year) ? 4 : 0)];
339             }
340             @SuppressWarnings("unchecked")
341             @Override
342             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
343                 // calls getFrom() to check if supported
344                 long curValue = getFrom(temporal);
345                 range().checkValidValue(newValue, this);  // leniently check from 1 to 92 TODO: check
346                 return (R) temporal.with(DAY_OF_YEAR, temporal.getLong(DAY_OF_YEAR) + (newValue - curValue));
347             }
348             @Override
349             public ChronoLocalDate resolve(
350                     Map<TemporalField, Long> fieldValues, TemporalAccessor partialTemporal, ResolverStyle resolverStyle) {
351                 Long yearLong = fieldValues.get(YEAR);
352                 Long qoyLong = fieldValues.get(QUARTER_OF_YEAR);
353                 if (yearLong == null || qoyLong == null) {
354                     return null;
355                 }
356                 int y = YEAR.checkValidIntValue(yearLong);  // always validate
357                 long doq = fieldValues.get(DAY_OF_QUARTER);
358                 ensureIso(partialTemporal);
359                 LocalDate date;
360                 if (resolverStyle == ResolverStyle.LENIENT) {
361                     date = LocalDate.of(y, 1, 1).plusMonths(Math.multiplyExact(Math.subtractExact(qoyLong, 1), 3));
362                     doq = Math.subtractExact(doq, 1);
363                 } else {
364                     int qoy = QUARTER_OF_YEAR.range().checkValidIntValue(qoyLong, QUARTER_OF_YEAR);  // validated
365                     date = LocalDate.of(y, ((qoy - 1) * 3) + 1, 1);
366                     if (doq < 1 || doq > 90) {
367                         if (resolverStyle == ResolverStyle.STRICT) {
368                             rangeRefinedBy(date).checkValidValue(doq, this);  // only allow exact range
369                         } else {  // SMART
370                             range().checkValidValue(doq, this);  // allow 1-92 rolling into next quarter
371                         }
372                     }
373                     doq--;
374                 }
375                 fieldValues.remove(this);
376                 fieldValues.remove(YEAR);
377                 fieldValues.remove(QUARTER_OF_YEAR);
378                 return date.plusDays(doq);
379             }
380             @Override
381             public String toString() {
382                 return "DayOfQuarter";
383             }
384         },
385         QUARTER_OF_YEAR {
386             @Override
387             public TemporalUnit getBaseUnit() {
388                 return QUARTER_YEARS;
389             }
390             @Override
391             public TemporalUnit getRangeUnit() {
392                 return YEARS;
393             }
394             @Override
395             public ValueRange range() {
396                 return ValueRange.of(1, 4);
397             }
398             @Override
399             public boolean isSupportedBy(TemporalAccessor temporal) {
400                 return temporal.isSupported(MONTH_OF_YEAR) && isIso(temporal);
401             }
402             @Override
403             public long getFrom(TemporalAccessor temporal) {
404                 if (isSupportedBy(temporal) == false) {
405                     throw new UnsupportedTemporalTypeException("Unsupported field: QuarterOfYear");
406                 }
407                 long moy = temporal.getLong(MONTH_OF_YEAR);
408                 return ((moy + 2) / 3);
409             }
410             public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
411                 if (isSupportedBy(temporal) == false) {
412                     throw new UnsupportedTemporalTypeException("Unsupported field: QuarterOfYear");
413                 }
414                 return super.rangeRefinedBy(temporal);
415             }
416             @SuppressWarnings("unchecked")
417             @Override
418             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
419                 // calls getFrom() to check if supported
420                 long curValue = getFrom(temporal);
421                 range().checkValidValue(newValue, this);  // strictly check from 1 to 4
422                 return (R) temporal.with(MONTH_OF_YEAR, temporal.getLong(MONTH_OF_YEAR) + (newValue - curValue) * 3);
423             }
424             @Override
425             public String toString() {
426                 return "QuarterOfYear";
427             }
428         },
429         WEEK_OF_WEEK_BASED_YEAR {
430             @Override
431             public String getDisplayName(Locale locale) {
432                 Objects.requireNonNull(locale, "locale");
433                 LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased()
434                                             .getLocaleResources(
435                                                 CalendarDataUtility
436                                                     .findRegionOverride(locale));
437                 ResourceBundle rb = lr.getJavaTimeFormatData();
438                 return rb.containsKey("field.week") ? rb.getString("field.week") : toString();
439             }
440
441             @Override
442             public TemporalUnit getBaseUnit() {
443                 return WEEKS;
444             }
445             @Override
446             public TemporalUnit getRangeUnit() {
447                 return WEEK_BASED_YEARS;
448             }
449             @Override
450             public ValueRange range() {
451                 return ValueRange.of(1, 52, 53);
452             }
453             @Override
454             public boolean isSupportedBy(TemporalAccessor temporal) {
455                 return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
456             }
457             @Override
458             public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
459                 if (isSupportedBy(temporal) == false) {
460                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekOfWeekBasedYear");
461                 }
462                 return getWeekRange(LocalDate.from(temporal));
463             }
464             @Override
465             public long getFrom(TemporalAccessor temporal) {
466                 if (isSupportedBy(temporal) == false) {
467                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekOfWeekBasedYear");
468                 }
469                 return getWeek(LocalDate.from(temporal));
470             }
471             @SuppressWarnings("unchecked")
472             @Override
473             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
474                 // calls getFrom() to check if supported
475                 range().checkValidValue(newValue, this);  // lenient range
476                 return (R) temporal.plus(Math.subtractExact(newValue, getFrom(temporal)), WEEKS);
477             }
478             @Override
479             public ChronoLocalDate resolve(
480                     Map<TemporalField, Long> fieldValues, TemporalAccessor partialTemporal, ResolverStyle resolverStyle) {
481                 Long wbyLong = fieldValues.get(WEEK_BASED_YEAR);
482                 Long dowLong = fieldValues.get(DAY_OF_WEEK);
483                 if (wbyLong == null || dowLong == null) {
484                     return null;
485                 }
486                 int wby = WEEK_BASED_YEAR.range().checkValidIntValue(wbyLong, WEEK_BASED_YEAR);  // always validate
487                 long wowby = fieldValues.get(WEEK_OF_WEEK_BASED_YEAR);
488                 ensureIso(partialTemporal);
489                 LocalDate date = LocalDate.of(wby, 1, 4);
490                 if (resolverStyle == ResolverStyle.LENIENT) {
491                     long dow = dowLong;  // unvalidated
492                     if (dow > 7) {
493                         date = date.plusWeeks((dow - 1) / 7);
494                         dow = ((dow - 1) % 7) + 1;
495                     } else if (dow < 1) {
496                         date = date.plusWeeks(Math.subtractExact(dow,  7) / 7);
497                         dow = ((dow + 6) % 7) + 1;
498                     }
499                     date = date.plusWeeks(Math.subtractExact(wowby, 1)).with(DAY_OF_WEEK, dow);
500                 } else {
501                     int dow = DAY_OF_WEEK.checkValidIntValue(dowLong);  // validated
502                     if (wowby < 1 || wowby > 52) {
503                         if (resolverStyle == ResolverStyle.STRICT) {
504                             getWeekRange(date).checkValidValue(wowby, this);  // only allow exact range
505                         } else {  // SMART
506                             range().checkValidValue(wowby, this);  // allow 1-53 rolling into next year
507                         }
508                     }
509                     date = date.plusWeeks(wowby - 1).with(DAY_OF_WEEK, dow);
510                 }
511                 fieldValues.remove(this);
512                 fieldValues.remove(WEEK_BASED_YEAR);
513                 fieldValues.remove(DAY_OF_WEEK);
514                 return date;
515             }
516             @Override
517             public String toString() {
518                 return "WeekOfWeekBasedYear";
519             }
520         },
521         WEEK_BASED_YEAR {
522             @Override
523             public TemporalUnit getBaseUnit() {
524                 return WEEK_BASED_YEARS;
525             }
526             @Override
527             public TemporalUnit getRangeUnit() {
528                 return FOREVER;
529             }
530             @Override
531             public ValueRange range() {
532                 return YEAR.range();
533             }
534             @Override
535             public boolean isSupportedBy(TemporalAccessor temporal) {
536                 return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
537             }
538             @Override
539             public long getFrom(TemporalAccessor temporal) {
540                 if (isSupportedBy(temporal) == false) {
541                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
542                 }
543                 return getWeekBasedYear(LocalDate.from(temporal));
544             }
545             public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
546                 if (isSupportedBy(temporal) == false) {
547                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
548                 }
549                 return super.rangeRefinedBy(temporal);
550             }
551             @SuppressWarnings("unchecked")
552             @Override
553             public <R extends Temporal> R adjustInto(R temporal, long newValue) {
554                 if (isSupportedBy(temporal) == false) {
555                     throw new UnsupportedTemporalTypeException("Unsupported field: WeekBasedYear");
556                 }
557                 int newWby = range().checkValidIntValue(newValue, WEEK_BASED_YEAR);  // strict check
558                 LocalDate date = LocalDate.from(temporal);
559                 int dow = date.get(DAY_OF_WEEK);
560                 int week = getWeek(date);
561                 if (week == 53 && getWeekRange(newWby) == 52) {
562                     week = 52;
563                 }
564                 LocalDate resolved = LocalDate.of(newWby, 1, 4);  // 4th is guaranteed to be in week one
565                 int days = (dow - resolved.get(DAY_OF_WEEK)) + ((week - 1) * 7);
566                 resolved = resolved.plusDays(days);
567                 return (R) temporal.with(resolved);
568             }
569             @Override
570             public String toString() {
571                 return "WeekBasedYear";
572             }
573         };
574
575         @Override
576         public boolean isDateBased() {
577             return true;
578         }
579
580         @Override
581         public boolean isTimeBased() {
582             return false;
583         }
584
585         @Override
586         public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
587             return range();
588         }
589
590         //-------------------------------------------------------------------------
591         private static final int[] QUARTER_DAYS = {0, 90, 181, 273, 0, 91, 182, 274};
592
593
594         private static void ensureIso(TemporalAccessor temporal) {
595             if (isIso(temporal) == false) {
596                 throw new DateTimeException("Resolve requires IsoChronology");
597             }
598         }
599
600         private static ValueRange getWeekRange(LocalDate date) {
601             int wby = getWeekBasedYear(date);
602             return ValueRange.of(1, getWeekRange(wby));
603         }
604
605         private static int getWeekRange(int wby) {
606             LocalDate date = LocalDate.of(wby, 1, 1);
607             // 53 weeks if standard year starts on Thursday, or Wed in a leap year
608             if (date.getDayOfWeek() == THURSDAY || (date.getDayOfWeek() == WEDNESDAY && date.isLeapYear())) {
609                 return 53;
610             }
611             return 52;
612         }
613
614         private static int getWeek(LocalDate date) {
615             int dow0 = date.getDayOfWeek().ordinal();
616             int doy0 = date.getDayOfYear() - 1;
617             int doyThu0 = doy0 + (3 - dow0);  // adjust to mid-week Thursday (which is 3 indexed from zero)
618             int alignedWeek = doyThu0 / 7;
619             int firstThuDoy0 = doyThu0 - (alignedWeek * 7);
620             int firstMonDoy0 = firstThuDoy0 - 3;
621             if (firstMonDoy0 < -3) {
622                 firstMonDoy0 += 7;
623             }
624             if (doy0 < firstMonDoy0) {
625                 return (int) getWeekRange(date.withDayOfYear(180).minusYears(1)).getMaximum();
626             }
627             int week = ((doy0 - firstMonDoy0) / 7) + 1;
628             if (week == 53) {
629                 if ((firstMonDoy0 == -3 || (firstMonDoy0 == -2 && date.isLeapYear())) == false) {
630                     week = 1;
631                 }
632             }
633             return week;
634         }
635
636         private static int getWeekBasedYear(LocalDate date) {
637             int year = date.getYear();
638             int doy = date.getDayOfYear();
639             if (doy <= 3) {
640                 int dow = date.getDayOfWeek().ordinal();
641                 if (doy - dow < -2) {
642                     year--;
643                 }
644             } else if (doy >= 363) {
645                 int dow = date.getDayOfWeek().ordinal();
646                 doy = doy - 363 - (date.isLeapYear() ? 1 : 0);
647                 if (doy - dow >= 0) {
648                     year++;
649                 }
650             }
651             return year;
652         }
653     }
654
655     //-----------------------------------------------------------------------
656     /**
657      * Implementation of the unit.
658      */

659     private static enum Unit implements TemporalUnit {
660
661         /**
662          * Unit that represents the concept of a week-based-year.
663          */

664         WEEK_BASED_YEARS("WeekBasedYears", Duration.ofSeconds(31556952L)),
665         /**
666          * Unit that represents the concept of a quarter-year.
667          */

668         QUARTER_YEARS("QuarterYears", Duration.ofSeconds(31556952L / 4));
669
670         private final String name;
671         private final Duration duration;
672
673         private Unit(String name, Duration estimatedDuration) {
674             this.name = name;
675             this.duration = estimatedDuration;
676         }
677
678         @Override
679         public Duration getDuration() {
680             return duration;
681         }
682
683         @Override
684         public boolean isDurationEstimated() {
685             return true;
686         }
687
688         @Override
689         public boolean isDateBased() {
690             return true;
691         }
692
693         @Override
694         public boolean isTimeBased() {
695             return false;
696         }
697
698         @Override
699         public boolean isSupportedBy(Temporal temporal) {
700             return temporal.isSupported(EPOCH_DAY) && isIso(temporal);
701         }
702
703         @SuppressWarnings("unchecked")
704         @Override
705         public <R extends Temporal> R addTo(R temporal, long amount) {
706             switch (this) {
707                 case WEEK_BASED_YEARS:
708                     return (R) temporal.with(WEEK_BASED_YEAR,
709                             Math.addExact(temporal.get(WEEK_BASED_YEAR), amount));
710                 case QUARTER_YEARS:
711                     return (R) temporal.plus(amount / 4, YEARS)
712                             .plus((amount % 4) * 3, MONTHS);
713                 default:
714                     throw new IllegalStateException("Unreachable");
715             }
716         }
717
718         @Override
719         public long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive) {
720             if (temporal1Inclusive.getClass() != temporal2Exclusive.getClass()) {
721                 return temporal1Inclusive.until(temporal2Exclusive, this);
722             }
723             switch(this) {
724                 case WEEK_BASED_YEARS:
725                     return Math.subtractExact(temporal2Exclusive.getLong(WEEK_BASED_YEAR),
726                             temporal1Inclusive.getLong(WEEK_BASED_YEAR));
727                 case QUARTER_YEARS:
728                     return temporal1Inclusive.until(temporal2Exclusive, MONTHS) / 3;
729                 default:
730                     throw new IllegalStateException("Unreachable");
731             }
732         }
733
734         @Override
735         public String toString() {
736             return name;
737         }
738     }
739
740     static boolean isIso(TemporalAccessor temporal) {
741         return Chronology.from(temporal).equals(IsoChronology.INSTANCE);
742     }
743 }
744