1 /* 2 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * * Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * * Neither the name of JSR-310 nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package org.threeten.bp.chrono; 33 34 import static org.threeten.bp.temporal.ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH; 35 import static org.threeten.bp.temporal.ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR; 36 import static org.threeten.bp.temporal.ChronoField.ALIGNED_WEEK_OF_MONTH; 37 import static org.threeten.bp.temporal.ChronoField.ALIGNED_WEEK_OF_YEAR; 38 import static org.threeten.bp.temporal.ChronoField.DAY_OF_MONTH; 39 import static org.threeten.bp.temporal.ChronoField.DAY_OF_WEEK; 40 import static org.threeten.bp.temporal.ChronoField.DAY_OF_YEAR; 41 import static org.threeten.bp.temporal.ChronoField.EPOCH_DAY; 42 import static org.threeten.bp.temporal.ChronoField.ERA; 43 import static org.threeten.bp.temporal.ChronoField.MONTH_OF_YEAR; 44 import static org.threeten.bp.temporal.ChronoField.PROLEPTIC_MONTH; 45 import static org.threeten.bp.temporal.ChronoField.YEAR; 46 import static org.threeten.bp.temporal.ChronoField.YEAR_OF_ERA; 47 import static org.threeten.bp.temporal.ChronoUnit.DAYS; 48 import static org.threeten.bp.temporal.ChronoUnit.MONTHS; 49 import static org.threeten.bp.temporal.ChronoUnit.WEEKS; 50 import static org.threeten.bp.temporal.TemporalAdjusters.nextOrSame; 51 52 import java.io.Serializable; 53 import java.util.Arrays; 54 import java.util.List; 55 import java.util.Locale; 56 import java.util.Map; 57 58 import org.threeten.bp.Clock; 59 import org.threeten.bp.DateTimeException; 60 import org.threeten.bp.DayOfWeek; 61 import org.threeten.bp.Instant; 62 import org.threeten.bp.LocalDate; 63 import org.threeten.bp.ZoneId; 64 import org.threeten.bp.format.ResolverStyle; 65 import org.threeten.bp.jdk8.Jdk8Methods; 66 import org.threeten.bp.temporal.ChronoField; 67 import org.threeten.bp.temporal.TemporalAccessor; 68 import org.threeten.bp.temporal.TemporalField; 69 import org.threeten.bp.temporal.ValueRange; 70 71 /** 72 * The Minguo calendar system. 73 * <p> 74 * This chronology defines the rules of the Minguo calendar system. 75 * This calendar system is primarily used in the Republic of China, often known as Taiwan. 76 * Dates are aligned such that {@code 0001-01-01 (Minguo)} is {@code 1912-01-01 (ISO)}. 77 * <p> 78 * The fields are defined as follows: 79 * <p><ul> 80 * <li>era - There are two eras, the current 'Republic' (ROC) and the previous era (BEFORE_ROC). 81 * <li>year-of-era - The year-of-era for the current era increases uniformly from the epoch at year one. 82 * For the previous era the year increases from one as time goes backwards. 83 * The value for the current era is equal to the ISO proleptic-year minus 1911. 84 * <li>proleptic-year - The proleptic year is the same as the year-of-era for the 85 * current era. For the previous era, years have zero, then negative values. 86 * The value is equal to the ISO proleptic-year minus 1911. 87 * <li>month-of-year - The Minguo month-of-year exactly matches ISO. 88 * <li>day-of-month - The Minguo day-of-month exactly matches ISO. 89 * <li>day-of-year - The Minguo day-of-year exactly matches ISO. 90 * <li>leap-year - The Minguo leap-year pattern exactly matches ISO, such that the two calendars 91 * are never out of step. 92 * </ul><p> 93 * 94 * <h3>Specification for implementors</h3> 95 * This class is immutable and thread-safe. 96 */ 97 public final class MinguoChronology extends Chronology implements Serializable { 98 99 /** 100 * Singleton instance for the Minguo chronology. 101 */ 102 public static final MinguoChronology INSTANCE = new MinguoChronology(); 103 104 /** 105 * Serialization version. 106 */ 107 private static final long serialVersionUID = 1039765215346859963L; 108 /** 109 * The difference in years between ISO and Minguo. 110 */ 111 static final int YEARS_DIFFERENCE = 1911; 112 113 /** 114 * Restricted constructor. 115 */ MinguoChronology()116 private MinguoChronology() { 117 } 118 119 /** 120 * Resolve singleton. 121 * 122 * @return the singleton instance, not null 123 */ readResolve()124 private Object readResolve() { 125 return INSTANCE; 126 } 127 128 //----------------------------------------------------------------------- 129 /** 130 * Gets the ID of the chronology - 'Minguo'. 131 * <p> 132 * The ID uniquely identifies the {@code Chronology}. 133 * It can be used to lookup the {@code Chronology} using {@link #of(String)}. 134 * 135 * @return the chronology ID - 'Minguo' 136 * @see #getCalendarType() 137 */ 138 @Override getId()139 public String getId() { 140 return "Minguo"; 141 } 142 143 /** 144 * Gets the calendar type of the underlying calendar system - 'roc'. 145 * <p> 146 * The calendar type is an identifier defined by the 147 * <em>Unicode Locale Data Markup Language (LDML)</em> specification. 148 * It can be used to lookup the {@code Chronology} using {@link #of(String)}. 149 * It can also be used as part of a locale, accessible via 150 * {@link Locale#getUnicodeLocaleType(String)} with the key 'ca'. 151 * 152 * @return the calendar system type - 'roc' 153 * @see #getId() 154 */ 155 @Override getCalendarType()156 public String getCalendarType() { 157 return "roc"; 158 } 159 160 //----------------------------------------------------------------------- 161 @Override // override with covariant return type date(Era era, int yearOfEra, int month, int dayOfMonth)162 public MinguoDate date(Era era, int yearOfEra, int month, int dayOfMonth) { 163 return (MinguoDate) super.date(era, yearOfEra, month, dayOfMonth); 164 } 165 166 @Override // override with covariant return type date(int prolepticYear, int month, int dayOfMonth)167 public MinguoDate date(int prolepticYear, int month, int dayOfMonth) { 168 return new MinguoDate(LocalDate.of(prolepticYear + YEARS_DIFFERENCE, month, dayOfMonth)); 169 } 170 171 @Override // override with covariant return type dateYearDay(Era era, int yearOfEra, int dayOfYear)172 public MinguoDate dateYearDay(Era era, int yearOfEra, int dayOfYear) { 173 return (MinguoDate) super.dateYearDay(era, yearOfEra, dayOfYear); 174 } 175 176 @Override // override with covariant return type dateYearDay(int prolepticYear, int dayOfYear)177 public MinguoDate dateYearDay(int prolepticYear, int dayOfYear) { 178 return new MinguoDate(LocalDate.ofYearDay(prolepticYear + YEARS_DIFFERENCE, dayOfYear)); 179 } 180 181 @Override dateEpochDay(long epochDay)182 public MinguoDate dateEpochDay(long epochDay) { 183 return new MinguoDate(LocalDate.ofEpochDay(epochDay)); 184 } 185 186 //----------------------------------------------------------------------- 187 @Override // override with covariant return type date(TemporalAccessor temporal)188 public MinguoDate date(TemporalAccessor temporal) { 189 if (temporal instanceof MinguoDate) { 190 return (MinguoDate) temporal; 191 } 192 return new MinguoDate(LocalDate.from(temporal)); 193 } 194 195 @SuppressWarnings("unchecked") 196 @Override // override with covariant return type localDateTime(TemporalAccessor temporal)197 public ChronoLocalDateTime<MinguoDate> localDateTime(TemporalAccessor temporal) { 198 return (ChronoLocalDateTime<MinguoDate>) super.localDateTime(temporal); 199 } 200 201 @SuppressWarnings("unchecked") 202 @Override // override with covariant return type zonedDateTime(TemporalAccessor temporal)203 public ChronoZonedDateTime<MinguoDate> zonedDateTime(TemporalAccessor temporal) { 204 return (ChronoZonedDateTime<MinguoDate>) super.zonedDateTime(temporal); 205 } 206 207 @SuppressWarnings("unchecked") 208 @Override // override with covariant return type zonedDateTime(Instant instant, ZoneId zone)209 public ChronoZonedDateTime<MinguoDate> zonedDateTime(Instant instant, ZoneId zone) { 210 return (ChronoZonedDateTime<MinguoDate>) super.zonedDateTime(instant, zone); 211 } 212 213 //----------------------------------------------------------------------- 214 @Override // override with covariant return type dateNow()215 public MinguoDate dateNow() { 216 return (MinguoDate) super.dateNow(); 217 } 218 219 @Override // override with covariant return type dateNow(ZoneId zone)220 public MinguoDate dateNow(ZoneId zone) { 221 return (MinguoDate) super.dateNow(zone); 222 } 223 224 @Override // override with covariant return type dateNow(Clock clock)225 public MinguoDate dateNow(Clock clock) { 226 Jdk8Methods.requireNonNull(clock, "clock"); 227 return (MinguoDate) super.dateNow(clock); 228 } 229 230 //----------------------------------------------------------------------- 231 /** 232 * Checks if the specified year is a leap year. 233 * <p> 234 * Minguo leap years occur exactly in line with ISO leap years. 235 * This method does not validate the year passed in, and only has a 236 * well-defined result for years in the supported range. 237 * 238 * @param prolepticYear the proleptic-year to check, not validated for range 239 * @return true if the year is a leap year 240 */ 241 @Override isLeapYear(long prolepticYear)242 public boolean isLeapYear(long prolepticYear) { 243 return IsoChronology.INSTANCE.isLeapYear(prolepticYear + YEARS_DIFFERENCE); 244 } 245 246 @Override prolepticYear(Era era, int yearOfEra)247 public int prolepticYear(Era era, int yearOfEra) { 248 if (era instanceof MinguoEra == false) { 249 throw new ClassCastException("Era must be MinguoEra"); 250 } 251 return (era == MinguoEra.ROC ? yearOfEra : 1 - yearOfEra); 252 } 253 254 @Override eraOf(int eraValue)255 public MinguoEra eraOf(int eraValue) { 256 return MinguoEra.of(eraValue); 257 } 258 259 @Override eras()260 public List<Era> eras() { 261 return Arrays.<Era>asList(MinguoEra.values()); 262 } 263 264 //----------------------------------------------------------------------- 265 @Override range(ChronoField field)266 public ValueRange range(ChronoField field) { 267 switch (field) { 268 case PROLEPTIC_MONTH: { 269 ValueRange range = PROLEPTIC_MONTH.range(); 270 return ValueRange.of(range.getMinimum() - YEARS_DIFFERENCE * 12L, range.getMaximum() - YEARS_DIFFERENCE * 12L); 271 } 272 case YEAR_OF_ERA: { 273 ValueRange range = YEAR.range(); 274 return ValueRange.of(1, range.getMaximum() - YEARS_DIFFERENCE, -range.getMinimum() + 1 + YEARS_DIFFERENCE); 275 } 276 case YEAR: { 277 ValueRange range = YEAR.range(); 278 return ValueRange.of(range.getMinimum() - YEARS_DIFFERENCE, range.getMaximum() - YEARS_DIFFERENCE); 279 } 280 } 281 return field.range(); 282 } 283 284 @Override resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle)285 public MinguoDate resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle) { 286 if (fieldValues.containsKey(EPOCH_DAY)) { 287 return dateEpochDay(fieldValues.remove(EPOCH_DAY)); 288 } 289 290 // normalize fields 291 Long prolepticMonth = fieldValues.remove(PROLEPTIC_MONTH); 292 if (prolepticMonth != null) { 293 if (resolverStyle != ResolverStyle.LENIENT) { 294 PROLEPTIC_MONTH.checkValidValue(prolepticMonth); 295 } 296 updateResolveMap(fieldValues, MONTH_OF_YEAR, Jdk8Methods.floorMod(prolepticMonth, 12) + 1); 297 updateResolveMap(fieldValues, YEAR, Jdk8Methods.floorDiv(prolepticMonth, 12)); 298 } 299 300 // eras 301 Long yoeLong = fieldValues.remove(YEAR_OF_ERA); 302 if (yoeLong != null) { 303 if (resolverStyle != ResolverStyle.LENIENT) { 304 YEAR_OF_ERA.checkValidValue(yoeLong); 305 } 306 Long era = fieldValues.remove(ERA); 307 if (era == null) { 308 Long year = fieldValues.get(YEAR); 309 if (resolverStyle == ResolverStyle.STRICT) { 310 // do not invent era if strict, but do cross-check with year 311 if (year != null) { 312 updateResolveMap(fieldValues, YEAR, (year > 0 ? yoeLong: Jdk8Methods.safeSubtract(1, yoeLong))); 313 } else { 314 // reinstate the field removed earlier, no cross-check issues 315 fieldValues.put(YEAR_OF_ERA, yoeLong); 316 } 317 } else { 318 // invent era 319 updateResolveMap(fieldValues, YEAR, (year == null || year > 0 ? yoeLong: Jdk8Methods.safeSubtract(1, yoeLong))); 320 } 321 } else if (era.longValue() == 1L) { 322 updateResolveMap(fieldValues, YEAR, yoeLong); 323 } else if (era.longValue() == 0L) { 324 updateResolveMap(fieldValues, YEAR, Jdk8Methods.safeSubtract(1, yoeLong)); 325 } else { 326 throw new DateTimeException("Invalid value for era: " + era); 327 } 328 } else if (fieldValues.containsKey(ERA)) { 329 ERA.checkValidValue(fieldValues.get(ERA)); // always validated 330 } 331 332 // build date 333 if (fieldValues.containsKey(YEAR)) { 334 if (fieldValues.containsKey(MONTH_OF_YEAR)) { 335 if (fieldValues.containsKey(DAY_OF_MONTH)) { 336 int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR)); 337 if (resolverStyle == ResolverStyle.LENIENT) { 338 long months = Jdk8Methods.safeSubtract(fieldValues.remove(MONTH_OF_YEAR), 1); 339 long days = Jdk8Methods.safeSubtract(fieldValues.remove(DAY_OF_MONTH), 1); 340 return date(y, 1, 1).plusMonths(months).plusDays(days); 341 } else { 342 int moy = range(MONTH_OF_YEAR).checkValidIntValue(fieldValues.remove(MONTH_OF_YEAR), MONTH_OF_YEAR); 343 int dom = range(DAY_OF_MONTH).checkValidIntValue(fieldValues.remove(DAY_OF_MONTH), DAY_OF_MONTH); 344 if (resolverStyle == ResolverStyle.SMART && dom > 28) { 345 dom = Math.min(dom, date(y, moy, 1).lengthOfMonth()); 346 } 347 return date(y, moy, dom); 348 } 349 } 350 if (fieldValues.containsKey(ALIGNED_WEEK_OF_MONTH)) { 351 if (fieldValues.containsKey(ALIGNED_DAY_OF_WEEK_IN_MONTH)) { 352 int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR)); 353 if (resolverStyle == ResolverStyle.LENIENT) { 354 long months = Jdk8Methods.safeSubtract(fieldValues.remove(MONTH_OF_YEAR), 1); 355 long weeks = Jdk8Methods.safeSubtract(fieldValues.remove(ALIGNED_WEEK_OF_MONTH), 1); 356 long days = Jdk8Methods.safeSubtract(fieldValues.remove(ALIGNED_DAY_OF_WEEK_IN_MONTH), 1); 357 return date(y, 1, 1).plus(months, MONTHS).plus(weeks, WEEKS).plus(days, DAYS); 358 } 359 int moy = MONTH_OF_YEAR.checkValidIntValue(fieldValues.remove(MONTH_OF_YEAR)); 360 int aw = ALIGNED_WEEK_OF_MONTH.checkValidIntValue(fieldValues.remove(ALIGNED_WEEK_OF_MONTH)); 361 int ad = ALIGNED_DAY_OF_WEEK_IN_MONTH.checkValidIntValue(fieldValues.remove(ALIGNED_DAY_OF_WEEK_IN_MONTH)); 362 MinguoDate date = date(y, moy, 1).plus((aw - 1) * 7 + (ad - 1), DAYS); 363 if (resolverStyle == ResolverStyle.STRICT && date.get(MONTH_OF_YEAR) != moy) { 364 throw new DateTimeException("Strict mode rejected date parsed to a different month"); 365 } 366 return date; 367 } 368 if (fieldValues.containsKey(DAY_OF_WEEK)) { 369 int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR)); 370 if (resolverStyle == ResolverStyle.LENIENT) { 371 long months = Jdk8Methods.safeSubtract(fieldValues.remove(MONTH_OF_YEAR), 1); 372 long weeks = Jdk8Methods.safeSubtract(fieldValues.remove(ALIGNED_WEEK_OF_MONTH), 1); 373 long days = Jdk8Methods.safeSubtract(fieldValues.remove(DAY_OF_WEEK), 1); 374 return date(y, 1, 1).plus(months, MONTHS).plus(weeks, WEEKS).plus(days, DAYS); 375 } 376 int moy = MONTH_OF_YEAR.checkValidIntValue(fieldValues.remove(MONTH_OF_YEAR)); 377 int aw = ALIGNED_WEEK_OF_MONTH.checkValidIntValue(fieldValues.remove(ALIGNED_WEEK_OF_MONTH)); 378 int dow = DAY_OF_WEEK.checkValidIntValue(fieldValues.remove(DAY_OF_WEEK)); 379 MinguoDate date = date(y, moy, 1).plus(aw - 1, WEEKS).with(nextOrSame(DayOfWeek.of(dow))); 380 if (resolverStyle == ResolverStyle.STRICT && date.get(MONTH_OF_YEAR) != moy) { 381 throw new DateTimeException("Strict mode rejected date parsed to a different month"); 382 } 383 return date; 384 } 385 } 386 } 387 if (fieldValues.containsKey(DAY_OF_YEAR)) { 388 int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR)); 389 if (resolverStyle == ResolverStyle.LENIENT) { 390 long days = Jdk8Methods.safeSubtract(fieldValues.remove(DAY_OF_YEAR), 1); 391 return dateYearDay(y, 1).plusDays(days); 392 } 393 int doy = DAY_OF_YEAR.checkValidIntValue(fieldValues.remove(DAY_OF_YEAR)); 394 return dateYearDay(y, doy); 395 } 396 if (fieldValues.containsKey(ALIGNED_WEEK_OF_YEAR)) { 397 if (fieldValues.containsKey(ALIGNED_DAY_OF_WEEK_IN_YEAR)) { 398 int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR)); 399 if (resolverStyle == ResolverStyle.LENIENT) { 400 long weeks = Jdk8Methods.safeSubtract(fieldValues.remove(ALIGNED_WEEK_OF_YEAR), 1); 401 long days = Jdk8Methods.safeSubtract(fieldValues.remove(ALIGNED_DAY_OF_WEEK_IN_YEAR), 1); 402 return date(y, 1, 1).plus(weeks, WEEKS).plus(days, DAYS); 403 } 404 int aw = ALIGNED_WEEK_OF_YEAR.checkValidIntValue(fieldValues.remove(ALIGNED_WEEK_OF_YEAR)); 405 int ad = ALIGNED_DAY_OF_WEEK_IN_YEAR.checkValidIntValue(fieldValues.remove(ALIGNED_DAY_OF_WEEK_IN_YEAR)); 406 MinguoDate date = date(y, 1, 1).plusDays((aw - 1) * 7 + (ad - 1)); 407 if (resolverStyle == ResolverStyle.STRICT && date.get(YEAR) != y) { 408 throw new DateTimeException("Strict mode rejected date parsed to a different year"); 409 } 410 return date; 411 } 412 if (fieldValues.containsKey(DAY_OF_WEEK)) { 413 int y = YEAR.checkValidIntValue(fieldValues.remove(YEAR)); 414 if (resolverStyle == ResolverStyle.LENIENT) { 415 long weeks = Jdk8Methods.safeSubtract(fieldValues.remove(ALIGNED_WEEK_OF_YEAR), 1); 416 long days = Jdk8Methods.safeSubtract(fieldValues.remove(DAY_OF_WEEK), 1); 417 return date(y, 1, 1).plus(weeks, WEEKS).plus(days, DAYS); 418 } 419 int aw = ALIGNED_WEEK_OF_YEAR.checkValidIntValue(fieldValues.remove(ALIGNED_WEEK_OF_YEAR)); 420 int dow = DAY_OF_WEEK.checkValidIntValue(fieldValues.remove(DAY_OF_WEEK)); 421 MinguoDate date = date(y, 1, 1).plus(aw - 1, WEEKS).with(nextOrSame(DayOfWeek.of(dow))); 422 if (resolverStyle == ResolverStyle.STRICT && date.get(YEAR) != y) { 423 throw new DateTimeException("Strict mode rejected date parsed to a different month"); 424 } 425 return date; 426 } 427 } 428 } 429 return null; 430 } 431 432 } 433