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