2347 lines
98 KiB
Java
2347 lines
98 KiB
Java
/*
|
|
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package java.util;
|
|
|
|
import java.io.IOException;
|
|
import java.io.ObjectInputStream;
|
|
import sun.util.locale.provider.CalendarDataUtility;
|
|
import sun.util.calendar.BaseCalendar;
|
|
import sun.util.calendar.CalendarDate;
|
|
import sun.util.calendar.CalendarSystem;
|
|
import sun.util.calendar.CalendarUtils;
|
|
import sun.util.calendar.Era;
|
|
import sun.util.calendar.Gregorian;
|
|
import sun.util.calendar.LocalGregorianCalendar;
|
|
|
|
/**
|
|
* {@code JapaneseImperialCalendar} implements a Japanese
|
|
* calendar system in which the imperial era-based year numbering is
|
|
* supported from the Meiji era. The following are the eras supported
|
|
* by this calendar system.
|
|
* <pre>{@code
|
|
* ERA value Era name Since (in Gregorian)
|
|
* ------------------------------------------------------
|
|
* 0 N/A N/A
|
|
* 1 Meiji 1868-01-01T00:00:00 local time
|
|
* 2 Taisho 1912-07-30T00:00:00 local time
|
|
* 3 Showa 1926-12-25T00:00:00 local time
|
|
* 4 Heisei 1989-01-08T00:00:00 local time
|
|
* 5 Reiwa 2019-05-01T00:00:00 local time
|
|
* ------------------------------------------------------
|
|
* }</pre>
|
|
*
|
|
* <p>{@code ERA} value 0 specifies the years before Meiji and
|
|
* the Gregorian year values are used. Unlike
|
|
* {@link GregorianCalendar}, the Julian to Gregorian transition is not
|
|
* supported because it doesn't make any sense to the Japanese
|
|
* calendar systems used before Meiji. To represent the years before
|
|
* Gregorian year 1, 0 and negative values are used. The Japanese
|
|
* Imperial rescripts and government decrees don't specify how to deal
|
|
* with time differences for applying the era transitions. This
|
|
* calendar implementation assumes local time for all transitions.
|
|
*
|
|
* @author Masayoshi Okutsu
|
|
* @since 1.6
|
|
*/
|
|
class JapaneseImperialCalendar extends Calendar {
|
|
/*
|
|
* Implementation Notes
|
|
*
|
|
* This implementation uses
|
|
* sun.util.calendar.LocalGregorianCalendar to perform most of the
|
|
* calendar calculations.
|
|
*/
|
|
|
|
/**
|
|
* The ERA constant designating the era before Meiji.
|
|
*/
|
|
public static final int BEFORE_MEIJI = 0;
|
|
|
|
/**
|
|
* The ERA constant designating the Meiji era.
|
|
*/
|
|
public static final int MEIJI = 1;
|
|
|
|
/**
|
|
* The ERA constant designating the Taisho era.
|
|
*/
|
|
public static final int TAISHO = 2;
|
|
|
|
/**
|
|
* The ERA constant designating the Showa era.
|
|
*/
|
|
public static final int SHOWA = 3;
|
|
|
|
/**
|
|
* The ERA constant designating the Heisei era.
|
|
*/
|
|
public static final int HEISEI = 4;
|
|
|
|
// Android-changed: Call the New Era it's proper name Reiwa.
|
|
/**
|
|
* The ERA constant designating the Reiwa era.
|
|
*/
|
|
public static final int REIWA = 5;
|
|
|
|
private static final int EPOCH_OFFSET = 719163; // Fixed date of January 1, 1970 (Gregorian)
|
|
|
|
// Useful millisecond constants. Although ONE_DAY and ONE_WEEK can fit
|
|
// into ints, they must be longs in order to prevent arithmetic overflow
|
|
// when performing (bug 4173516).
|
|
private static final int ONE_SECOND = 1000;
|
|
private static final int ONE_MINUTE = 60*ONE_SECOND;
|
|
private static final int ONE_HOUR = 60*ONE_MINUTE;
|
|
private static final long ONE_DAY = 24*ONE_HOUR;
|
|
|
|
// Reference to the sun.util.calendar.LocalGregorianCalendar instance (singleton).
|
|
private static final LocalGregorianCalendar jcal
|
|
= (LocalGregorianCalendar) CalendarSystem.forName("japanese");
|
|
|
|
// Gregorian calendar instance. This is required because era
|
|
// transition dates are given in Gregorian dates.
|
|
private static final Gregorian gcal = CalendarSystem.getGregorianCalendar();
|
|
|
|
// The Era instance representing "before Meiji".
|
|
private static final Era BEFORE_MEIJI_ERA = new Era("BeforeMeiji", "BM", Long.MIN_VALUE, false);
|
|
|
|
// Imperial eras. The sun.util.calendar.LocalGregorianCalendar
|
|
// doesn't have an Era representing before Meiji, which is
|
|
// inconvenient for a Calendar. So, era[0] is a reference to
|
|
// BEFORE_MEIJI_ERA.
|
|
private static final Era[] eras;
|
|
|
|
// Fixed date of the first date of each era.
|
|
private static final long[] sinceFixedDates;
|
|
|
|
// The current era
|
|
private static final int currentEra;
|
|
|
|
/*
|
|
* <pre>
|
|
* Greatest Least
|
|
* Field name Minimum Minimum Maximum Maximum
|
|
* ---------- ------- ------- ------- -------
|
|
* ERA 0 0 1 1
|
|
* YEAR -292275055 1 ? ?
|
|
* MONTH 0 0 11 11
|
|
* WEEK_OF_YEAR 1 1 52* 53
|
|
* WEEK_OF_MONTH 0 0 4* 6
|
|
* DAY_OF_MONTH 1 1 28* 31
|
|
* DAY_OF_YEAR 1 1 365* 366
|
|
* DAY_OF_WEEK 1 1 7 7
|
|
* DAY_OF_WEEK_IN_MONTH -1 -1 4* 6
|
|
* AM_PM 0 0 1 1
|
|
* HOUR 0 0 11 11
|
|
* HOUR_OF_DAY 0 0 23 23
|
|
* MINUTE 0 0 59 59
|
|
* SECOND 0 0 59 59
|
|
* MILLISECOND 0 0 999 999
|
|
* ZONE_OFFSET -13:00 -13:00 14:00 14:00
|
|
* DST_OFFSET 0:00 0:00 0:20 2:00
|
|
* </pre>
|
|
* *: depends on eras
|
|
*/
|
|
static final int MIN_VALUES[] = {
|
|
0, // ERA
|
|
-292275055, // YEAR
|
|
JANUARY, // MONTH
|
|
1, // WEEK_OF_YEAR
|
|
0, // WEEK_OF_MONTH
|
|
1, // DAY_OF_MONTH
|
|
1, // DAY_OF_YEAR
|
|
SUNDAY, // DAY_OF_WEEK
|
|
1, // DAY_OF_WEEK_IN_MONTH
|
|
AM, // AM_PM
|
|
0, // HOUR
|
|
0, // HOUR_OF_DAY
|
|
0, // MINUTE
|
|
0, // SECOND
|
|
0, // MILLISECOND
|
|
-13*ONE_HOUR, // ZONE_OFFSET (UNIX compatibility)
|
|
0 // DST_OFFSET
|
|
};
|
|
static final int LEAST_MAX_VALUES[] = {
|
|
0, // ERA (initialized later)
|
|
0, // YEAR (initialized later)
|
|
JANUARY, // MONTH (Showa 64 ended in January.)
|
|
0, // WEEK_OF_YEAR (Showa 1 has only 6 days which could be 0 weeks.)
|
|
4, // WEEK_OF_MONTH
|
|
28, // DAY_OF_MONTH
|
|
0, // DAY_OF_YEAR (initialized later)
|
|
SATURDAY, // DAY_OF_WEEK
|
|
4, // DAY_OF_WEEK_IN
|
|
PM, // AM_PM
|
|
11, // HOUR
|
|
23, // HOUR_OF_DAY
|
|
59, // MINUTE
|
|
59, // SECOND
|
|
999, // MILLISECOND
|
|
14*ONE_HOUR, // ZONE_OFFSET
|
|
20*ONE_MINUTE // DST_OFFSET (historical least maximum)
|
|
};
|
|
static final int MAX_VALUES[] = {
|
|
0, // ERA
|
|
292278994, // YEAR
|
|
DECEMBER, // MONTH
|
|
53, // WEEK_OF_YEAR
|
|
6, // WEEK_OF_MONTH
|
|
31, // DAY_OF_MONTH
|
|
366, // DAY_OF_YEAR
|
|
SATURDAY, // DAY_OF_WEEK
|
|
6, // DAY_OF_WEEK_IN
|
|
PM, // AM_PM
|
|
11, // HOUR
|
|
23, // HOUR_OF_DAY
|
|
59, // MINUTE
|
|
59, // SECOND
|
|
999, // MILLISECOND
|
|
14*ONE_HOUR, // ZONE_OFFSET
|
|
2*ONE_HOUR // DST_OFFSET (double summer time)
|
|
};
|
|
|
|
// Proclaim serialization compatibility with JDK 1.6
|
|
@SuppressWarnings("FieldNameHidesFieldInSuperclass")
|
|
@java.io.Serial
|
|
private static final long serialVersionUID = -3364572813905467929L;
|
|
|
|
static {
|
|
Era[] es = jcal.getEras();
|
|
int length = es.length + 1;
|
|
eras = new Era[length];
|
|
sinceFixedDates = new long[length];
|
|
|
|
// eras[BEFORE_MEIJI] and sinceFixedDate[BEFORE_MEIJI] are the
|
|
// same as Gregorian.
|
|
int index = BEFORE_MEIJI;
|
|
// Android-removed: Zygote could initialize this class when system has outdated time.
|
|
// int current = index;
|
|
sinceFixedDates[index] = gcal.getFixedDate(BEFORE_MEIJI_ERA.getSinceDate());
|
|
eras[index++] = BEFORE_MEIJI_ERA;
|
|
for (Era e : es) {
|
|
// Android-removed: Zygote could initialize this class when system has outdated time.
|
|
// Android hard-code the current era. Unlike upstream, Android does not add the new era
|
|
// in the code until the new era arrives. Thus, Android can't have newer era than the
|
|
// real world. currentEra is the latest Era that Android knows about.
|
|
// if(e.getSince(TimeZone.NO_TIMEZONE) < System.currentTimeMillis()) {
|
|
// current = index;
|
|
// }
|
|
CalendarDate d = e.getSinceDate();
|
|
sinceFixedDates[index] = gcal.getFixedDate(d);
|
|
eras[index++] = e;
|
|
}
|
|
// Android-changed: Zygote could initialize this class when system has outdated time.
|
|
// currentEra = current;
|
|
currentEra = REIWA;
|
|
|
|
LEAST_MAX_VALUES[ERA] = MAX_VALUES[ERA] = eras.length - 1;
|
|
|
|
// Calculate the least maximum year and least day of Year
|
|
// values. The following code assumes that there's at most one
|
|
// era transition in a Gregorian year.
|
|
int year = Integer.MAX_VALUE;
|
|
int dayOfYear = Integer.MAX_VALUE;
|
|
CalendarDate date = gcal.newCalendarDate(TimeZone.NO_TIMEZONE);
|
|
for (int i = 1; i < eras.length; i++) {
|
|
long fd = sinceFixedDates[i];
|
|
CalendarDate transitionDate = eras[i].getSinceDate();
|
|
date.setDate(transitionDate.getYear(), BaseCalendar.JANUARY, 1);
|
|
long fdd = gcal.getFixedDate(date);
|
|
if (fd != fdd) {
|
|
dayOfYear = Math.min((int)(fd - fdd) + 1, dayOfYear);
|
|
}
|
|
date.setDate(transitionDate.getYear(), BaseCalendar.DECEMBER, 31);
|
|
fdd = gcal.getFixedDate(date);
|
|
if (fd != fdd) {
|
|
dayOfYear = Math.min((int)(fdd - fd) + 1, dayOfYear);
|
|
}
|
|
LocalGregorianCalendar.Date lgd = getCalendarDate(fd - 1);
|
|
int y = lgd.getYear();
|
|
// Unless the first year starts from January 1, the actual
|
|
// max value could be one year short. For example, if it's
|
|
// Showa 63 January 8, 63 is the actual max value since
|
|
// Showa 64 January 8 doesn't exist.
|
|
if (!(lgd.getMonth() == BaseCalendar.JANUARY && lgd.getDayOfMonth() == 1)) {
|
|
y--;
|
|
}
|
|
year = Math.min(y, year);
|
|
}
|
|
LEAST_MAX_VALUES[YEAR] = year; // Max year could be smaller than this value.
|
|
LEAST_MAX_VALUES[DAY_OF_YEAR] = dayOfYear;
|
|
}
|
|
|
|
/**
|
|
* jdate always has a sun.util.calendar.LocalGregorianCalendar.Date instance to
|
|
* avoid overhead of creating it for each calculation.
|
|
*/
|
|
private transient LocalGregorianCalendar.Date jdate;
|
|
|
|
/**
|
|
* Temporary int[2] to get time zone offsets. zoneOffsets[0] gets
|
|
* the GMT offset value and zoneOffsets[1] gets the daylight saving
|
|
* value.
|
|
*/
|
|
private transient int[] zoneOffsets;
|
|
|
|
/**
|
|
* Temporary storage for saving original fields[] values in
|
|
* non-lenient mode.
|
|
*/
|
|
private transient int[] originalFields;
|
|
|
|
/**
|
|
* Constructs a {@code JapaneseImperialCalendar} based on the current time
|
|
* in the given time zone with the given locale.
|
|
*
|
|
* @param zone the given time zone.
|
|
* @param aLocale the given locale.
|
|
*/
|
|
JapaneseImperialCalendar(TimeZone zone, Locale aLocale) {
|
|
super(zone, aLocale);
|
|
jdate = jcal.newCalendarDate(zone);
|
|
setTimeInMillis(System.currentTimeMillis());
|
|
}
|
|
|
|
/**
|
|
* Constructs an "empty" {@code JapaneseImperialCalendar}.
|
|
*
|
|
* @param zone the given time zone
|
|
* @param aLocale the given locale
|
|
* @param flag the flag requesting an empty instance
|
|
*/
|
|
JapaneseImperialCalendar(TimeZone zone, Locale aLocale, boolean flag) {
|
|
super(zone, aLocale);
|
|
jdate = jcal.newCalendarDate(zone);
|
|
}
|
|
|
|
/**
|
|
* Returns {@code "japanese"} as the calendar type of this {@code
|
|
* JapaneseImperialCalendar}.
|
|
*
|
|
* @return {@code "japanese"}
|
|
*/
|
|
@Override
|
|
public String getCalendarType() {
|
|
return "japanese";
|
|
}
|
|
|
|
/**
|
|
* Compares this {@code JapaneseImperialCalendar} to the specified
|
|
* {@code Object}. The result is {@code true} if and
|
|
* only if the argument is a {@code JapaneseImperialCalendar} object
|
|
* that represents the same time value (millisecond offset from
|
|
* the <a href="Calendar.html#Epoch">Epoch</a>) under the same
|
|
* {@code Calendar} parameters.
|
|
*
|
|
* @param obj the object to compare with.
|
|
* @return {@code true} if this object is equal to {@code obj};
|
|
* {@code false} otherwise.
|
|
* @see Calendar#compareTo(Calendar)
|
|
*/
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
return obj instanceof JapaneseImperialCalendar &&
|
|
super.equals(obj);
|
|
}
|
|
|
|
/**
|
|
* Generates the hash code for this
|
|
* {@code JapaneseImperialCalendar} object.
|
|
*/
|
|
@Override
|
|
public int hashCode() {
|
|
return super.hashCode() ^ jdate.hashCode();
|
|
}
|
|
|
|
/**
|
|
* Adds the specified (signed) amount of time to the given calendar field,
|
|
* based on the calendar's rules.
|
|
*
|
|
* <p><em>Add rule 1</em>. The value of {@code field}
|
|
* after the call minus the value of {@code field} before the
|
|
* call is {@code amount}, modulo any overflow that has occurred in
|
|
* {@code field}. Overflow occurs when a field value exceeds its
|
|
* range and, as a result, the next larger field is incremented or
|
|
* decremented and the field value is adjusted back into its range.</p>
|
|
*
|
|
* <p><em>Add rule 2</em>. If a smaller field is expected to be
|
|
* invariant, but it is impossible for it to be equal to its
|
|
* prior value because of changes in its minimum or maximum after
|
|
* {@code field} is changed, then its value is adjusted to be as close
|
|
* as possible to its expected value. A smaller field represents a
|
|
* smaller unit of time. {@code HOUR} is a smaller field than
|
|
* {@code DAY_OF_MONTH}. No adjustment is made to smaller fields
|
|
* that are not expected to be invariant. The calendar system
|
|
* determines what fields are expected to be invariant.</p>
|
|
*
|
|
* @param field the calendar field.
|
|
* @param amount the amount of date or time to be added to the field.
|
|
* @throws IllegalArgumentException if {@code field} is
|
|
* {@code ZONE_OFFSET}, {@code DST_OFFSET}, or unknown,
|
|
* or if any calendar fields have out-of-range values in
|
|
* non-lenient mode.
|
|
*/
|
|
@Override
|
|
public void add(int field, int amount) {
|
|
// If amount == 0, do nothing even the given field is out of
|
|
// range. This is tested by JCK.
|
|
if (amount == 0) {
|
|
return; // Do nothing!
|
|
}
|
|
|
|
if (field < 0 || field >= ZONE_OFFSET) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
|
|
// Sync the time and calendar fields.
|
|
complete();
|
|
|
|
if (field == YEAR) {
|
|
LocalGregorianCalendar.Date d = (LocalGregorianCalendar.Date) jdate.clone();
|
|
d.addYear(amount);
|
|
pinDayOfMonth(d);
|
|
set(ERA, getEraIndex(d));
|
|
set(YEAR, d.getYear());
|
|
set(MONTH, d.getMonth() - 1);
|
|
set(DAY_OF_MONTH, d.getDayOfMonth());
|
|
} else if (field == MONTH) {
|
|
LocalGregorianCalendar.Date d = (LocalGregorianCalendar.Date) jdate.clone();
|
|
d.addMonth(amount);
|
|
pinDayOfMonth(d);
|
|
set(ERA, getEraIndex(d));
|
|
set(YEAR, d.getYear());
|
|
set(MONTH, d.getMonth() - 1);
|
|
set(DAY_OF_MONTH, d.getDayOfMonth());
|
|
} else if (field == ERA) {
|
|
int era = internalGet(ERA) + amount;
|
|
if (era < 0) {
|
|
era = 0;
|
|
} else if (era > eras.length - 1) {
|
|
era = eras.length - 1;
|
|
}
|
|
set(ERA, era);
|
|
} else {
|
|
long delta = amount;
|
|
long timeOfDay = 0;
|
|
switch (field) {
|
|
// Handle the time fields here. Convert the given
|
|
// amount to milliseconds and call setTimeInMillis.
|
|
case HOUR:
|
|
case HOUR_OF_DAY:
|
|
delta *= 60 * 60 * 1000; // hours to milliseconds
|
|
break;
|
|
|
|
case MINUTE:
|
|
delta *= 60 * 1000; // minutes to milliseconds
|
|
break;
|
|
|
|
case SECOND:
|
|
delta *= 1000; // seconds to milliseconds
|
|
break;
|
|
|
|
case MILLISECOND:
|
|
break;
|
|
|
|
// Handle week, day and AM_PM fields which involves
|
|
// time zone offset change adjustment. Convert the
|
|
// given amount to the number of days.
|
|
case WEEK_OF_YEAR:
|
|
case WEEK_OF_MONTH:
|
|
case DAY_OF_WEEK_IN_MONTH:
|
|
delta *= 7;
|
|
break;
|
|
|
|
case DAY_OF_MONTH: // synonym of DATE
|
|
case DAY_OF_YEAR:
|
|
case DAY_OF_WEEK:
|
|
break;
|
|
|
|
case AM_PM:
|
|
// Convert the amount to the number of days (delta)
|
|
// and +12 or -12 hours (timeOfDay).
|
|
delta = amount / 2;
|
|
timeOfDay = 12 * (amount % 2);
|
|
break;
|
|
}
|
|
|
|
// The time fields don't require time zone offset change
|
|
// adjustment.
|
|
if (field >= HOUR) {
|
|
setTimeInMillis(time + delta);
|
|
return;
|
|
}
|
|
|
|
// The rest of the fields (week, day or AM_PM fields)
|
|
// require time zone offset (both GMT and DST) change
|
|
// adjustment.
|
|
|
|
// Translate the current time to the fixed date and time
|
|
// of the day.
|
|
long fd = cachedFixedDate;
|
|
timeOfDay += internalGet(HOUR_OF_DAY);
|
|
timeOfDay *= 60;
|
|
timeOfDay += internalGet(MINUTE);
|
|
timeOfDay *= 60;
|
|
timeOfDay += internalGet(SECOND);
|
|
timeOfDay *= 1000;
|
|
timeOfDay += internalGet(MILLISECOND);
|
|
if (timeOfDay >= ONE_DAY) {
|
|
fd++;
|
|
timeOfDay -= ONE_DAY;
|
|
} else if (timeOfDay < 0) {
|
|
fd--;
|
|
timeOfDay += ONE_DAY;
|
|
}
|
|
|
|
fd += delta; // fd is the expected fixed date after the calculation
|
|
int zoneOffset = internalGet(ZONE_OFFSET) + internalGet(DST_OFFSET);
|
|
setTimeInMillis((fd - EPOCH_OFFSET) * ONE_DAY + timeOfDay - zoneOffset);
|
|
zoneOffset -= internalGet(ZONE_OFFSET) + internalGet(DST_OFFSET);
|
|
// If the time zone offset has changed, then adjust the difference.
|
|
if (zoneOffset != 0) {
|
|
setTimeInMillis(time + zoneOffset);
|
|
long fd2 = cachedFixedDate;
|
|
// If the adjustment has changed the date, then take
|
|
// the previous one.
|
|
if (fd2 != fd) {
|
|
setTimeInMillis(time - zoneOffset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void roll(int field, boolean up) {
|
|
roll(field, up ? +1 : -1);
|
|
}
|
|
|
|
/**
|
|
* Adds a signed amount to the specified calendar field without changing larger fields.
|
|
* A negative roll amount means to subtract from field without changing
|
|
* larger fields. If the specified amount is 0, this method performs nothing.
|
|
*
|
|
* <p>This method calls {@link #complete()} before adding the
|
|
* amount so that all the calendar fields are normalized. If there
|
|
* is any calendar field having an out-of-range value in non-lenient mode, then an
|
|
* {@code IllegalArgumentException} is thrown.
|
|
*
|
|
* @param field the calendar field.
|
|
* @param amount the signed amount to add to {@code field}.
|
|
* @throws IllegalArgumentException if {@code field} is
|
|
* {@code ZONE_OFFSET}, {@code DST_OFFSET}, or unknown,
|
|
* or if any calendar fields have out-of-range values in
|
|
* non-lenient mode.
|
|
* @see #roll(int,boolean)
|
|
* @see #add(int,int)
|
|
* @see #set(int,int)
|
|
*/
|
|
@Override
|
|
public void roll(int field, int amount) {
|
|
// If amount == 0, do nothing even the given field is out of
|
|
// range. This is tested by JCK.
|
|
if (amount == 0) {
|
|
return;
|
|
}
|
|
|
|
if (field < 0 || field >= ZONE_OFFSET) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
|
|
// Sync the time and calendar fields.
|
|
complete();
|
|
|
|
int min = getMinimum(field);
|
|
int max = getMaximum(field);
|
|
|
|
switch (field) {
|
|
case ERA:
|
|
case AM_PM:
|
|
case MINUTE:
|
|
case SECOND:
|
|
case MILLISECOND:
|
|
// These fields are handled simply, since they have fixed
|
|
// minima and maxima. Other fields are complicated, since
|
|
// the range within they must roll varies depending on the
|
|
// date, a time zone and the era transitions.
|
|
break;
|
|
|
|
case HOUR:
|
|
case HOUR_OF_DAY:
|
|
{
|
|
int unit = max + 1; // 12 or 24 hours
|
|
int h = internalGet(field);
|
|
int nh = (h + amount) % unit;
|
|
if (nh < 0) {
|
|
nh += unit;
|
|
}
|
|
time += ONE_HOUR * (nh - h);
|
|
|
|
// The day might have changed, which could happen if
|
|
// the daylight saving time transition brings it to
|
|
// the next day, although it's very unlikely. But we
|
|
// have to make sure not to change the larger fields.
|
|
CalendarDate d = jcal.getCalendarDate(time, getZone());
|
|
if (internalGet(DAY_OF_MONTH) != d.getDayOfMonth()) {
|
|
d.setEra(jdate.getEra());
|
|
d.setDate(internalGet(YEAR),
|
|
internalGet(MONTH) + 1,
|
|
internalGet(DAY_OF_MONTH));
|
|
if (field == HOUR) {
|
|
assert (internalGet(AM_PM) == PM);
|
|
d.addHours(+12); // restore PM
|
|
}
|
|
time = jcal.getTime(d);
|
|
}
|
|
int hourOfDay = d.getHours();
|
|
internalSet(field, hourOfDay % unit);
|
|
if (field == HOUR) {
|
|
internalSet(HOUR_OF_DAY, hourOfDay);
|
|
} else {
|
|
internalSet(AM_PM, hourOfDay / 12);
|
|
internalSet(HOUR, hourOfDay % 12);
|
|
}
|
|
|
|
// Time zone offset and/or daylight saving might have changed.
|
|
int zoneOffset = d.getZoneOffset();
|
|
int saving = d.getDaylightSaving();
|
|
internalSet(ZONE_OFFSET, zoneOffset - saving);
|
|
internalSet(DST_OFFSET, saving);
|
|
return;
|
|
}
|
|
|
|
case YEAR:
|
|
min = getActualMinimum(field);
|
|
max = getActualMaximum(field);
|
|
break;
|
|
|
|
case MONTH:
|
|
// Rolling the month involves both pinning the final value to [0, 11]
|
|
// and adjusting the DAY_OF_MONTH if necessary. We only adjust the
|
|
// DAY_OF_MONTH if, after updating the MONTH field, it is illegal.
|
|
// E.g., <jan31>.roll(MONTH, 1) -> <feb28> or <feb29>.
|
|
{
|
|
if (!isTransitionYear(jdate.getNormalizedYear())) {
|
|
int year = jdate.getYear();
|
|
if (year == getMaximum(YEAR)) {
|
|
CalendarDate jd = jcal.getCalendarDate(time, getZone());
|
|
CalendarDate d = jcal.getCalendarDate(Long.MAX_VALUE, getZone());
|
|
max = d.getMonth() - 1;
|
|
int n = getRolledValue(internalGet(field), amount, min, max);
|
|
if (n == max) {
|
|
// To avoid overflow, use an equivalent year.
|
|
jd.addYear(-400);
|
|
jd.setMonth(n + 1);
|
|
if (jd.getDayOfMonth() > d.getDayOfMonth()) {
|
|
jd.setDayOfMonth(d.getDayOfMonth());
|
|
jcal.normalize(jd);
|
|
}
|
|
if (jd.getDayOfMonth() == d.getDayOfMonth()
|
|
&& jd.getTimeOfDay() > d.getTimeOfDay()) {
|
|
jd.setMonth(n + 1);
|
|
jd.setDayOfMonth(d.getDayOfMonth() - 1);
|
|
jcal.normalize(jd);
|
|
// Month may have changed by the normalization.
|
|
n = jd.getMonth() - 1;
|
|
}
|
|
set(DAY_OF_MONTH, jd.getDayOfMonth());
|
|
}
|
|
set(MONTH, n);
|
|
} else if (year == getMinimum(YEAR)) {
|
|
CalendarDate jd = jcal.getCalendarDate(time, getZone());
|
|
CalendarDate d = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
min = d.getMonth() - 1;
|
|
int n = getRolledValue(internalGet(field), amount, min, max);
|
|
if (n == min) {
|
|
// To avoid underflow, use an equivalent year.
|
|
jd.addYear(+400);
|
|
jd.setMonth(n + 1);
|
|
if (jd.getDayOfMonth() < d.getDayOfMonth()) {
|
|
jd.setDayOfMonth(d.getDayOfMonth());
|
|
jcal.normalize(jd);
|
|
}
|
|
if (jd.getDayOfMonth() == d.getDayOfMonth()
|
|
&& jd.getTimeOfDay() < d.getTimeOfDay()) {
|
|
jd.setMonth(n + 1);
|
|
jd.setDayOfMonth(d.getDayOfMonth() + 1);
|
|
jcal.normalize(jd);
|
|
// Month may have changed by the normalization.
|
|
n = jd.getMonth() - 1;
|
|
}
|
|
set(DAY_OF_MONTH, jd.getDayOfMonth());
|
|
}
|
|
set(MONTH, n);
|
|
} else {
|
|
int mon = (internalGet(MONTH) + amount) % 12;
|
|
if (mon < 0) {
|
|
mon += 12;
|
|
}
|
|
set(MONTH, mon);
|
|
|
|
// Keep the day of month in the range. We
|
|
// don't want to spill over into the next
|
|
// month; e.g., we don't want jan31 + 1 mo ->
|
|
// feb31 -> mar3.
|
|
int monthLen = monthLength(mon);
|
|
if (internalGet(DAY_OF_MONTH) > monthLen) {
|
|
set(DAY_OF_MONTH, monthLen);
|
|
}
|
|
}
|
|
} else {
|
|
int eraIndex = getEraIndex(jdate);
|
|
CalendarDate transition = null;
|
|
if (jdate.getYear() == 1) {
|
|
transition = eras[eraIndex].getSinceDate();
|
|
min = transition.getMonth() - 1;
|
|
} else {
|
|
if (eraIndex < eras.length - 1) {
|
|
transition = eras[eraIndex + 1].getSinceDate();
|
|
if (transition.getYear() == jdate.getNormalizedYear()) {
|
|
max = transition.getMonth() - 1;
|
|
if (transition.getDayOfMonth() == 1) {
|
|
max--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (min == max) {
|
|
// The year has only one month. No need to
|
|
// process further. (Showa Gan-nen (year 1)
|
|
// and the last year have only one month.)
|
|
return;
|
|
}
|
|
int n = getRolledValue(internalGet(field), amount, min, max);
|
|
set(MONTH, n);
|
|
if (n == min) {
|
|
if (!(transition.getMonth() == BaseCalendar.JANUARY
|
|
&& transition.getDayOfMonth() == 1)) {
|
|
if (jdate.getDayOfMonth() < transition.getDayOfMonth()) {
|
|
set(DAY_OF_MONTH, transition.getDayOfMonth());
|
|
}
|
|
}
|
|
} else if (n == max && (transition.getMonth() - 1 == n)) {
|
|
int dom = transition.getDayOfMonth();
|
|
if (jdate.getDayOfMonth() >= dom) {
|
|
set(DAY_OF_MONTH, dom - 1);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
case WEEK_OF_YEAR:
|
|
{
|
|
int y = jdate.getNormalizedYear();
|
|
max = getActualMaximum(WEEK_OF_YEAR);
|
|
set(DAY_OF_WEEK, internalGet(DAY_OF_WEEK)); // update stamp[field]
|
|
int woy = internalGet(WEEK_OF_YEAR);
|
|
int value = woy + amount;
|
|
if (!isTransitionYear(jdate.getNormalizedYear())) {
|
|
int year = jdate.getYear();
|
|
if (year == getMaximum(YEAR)) {
|
|
max = getActualMaximum(WEEK_OF_YEAR);
|
|
} else if (year == getMinimum(YEAR)) {
|
|
min = getActualMinimum(WEEK_OF_YEAR);
|
|
max = getActualMaximum(WEEK_OF_YEAR);
|
|
if (value > min && value < max) {
|
|
set(WEEK_OF_YEAR, value);
|
|
return;
|
|
}
|
|
|
|
}
|
|
// If the new value is in between min and max
|
|
// (exclusive), then we can use the value.
|
|
if (value > min && value < max) {
|
|
set(WEEK_OF_YEAR, value);
|
|
return;
|
|
}
|
|
long fd = cachedFixedDate;
|
|
// Make sure that the min week has the current DAY_OF_WEEK
|
|
long day1 = fd - (7 * (woy - min));
|
|
if (year != getMinimum(YEAR)) {
|
|
if (gcal.getYearFromFixedDate(day1) != y) {
|
|
min++;
|
|
}
|
|
} else {
|
|
CalendarDate d = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
if (day1 < jcal.getFixedDate(d)) {
|
|
min++;
|
|
}
|
|
}
|
|
|
|
// Make sure the same thing for the max week
|
|
fd += 7 * (max - internalGet(WEEK_OF_YEAR));
|
|
if (gcal.getYearFromFixedDate(fd) != y) {
|
|
max--;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Handle transition here.
|
|
long fd = cachedFixedDate;
|
|
long day1 = fd - (7 * (woy - min));
|
|
// Make sure that the min week has the current DAY_OF_WEEK
|
|
LocalGregorianCalendar.Date d = getCalendarDate(day1);
|
|
if (!(d.getEra() == jdate.getEra() && d.getYear() == jdate.getYear())) {
|
|
min++;
|
|
}
|
|
|
|
// Make sure the same thing for the max week
|
|
fd += 7 * (max - woy);
|
|
jcal.getCalendarDateFromFixedDate(d, fd);
|
|
if (!(d.getEra() == jdate.getEra() && d.getYear() == jdate.getYear())) {
|
|
max--;
|
|
}
|
|
// value: the new WEEK_OF_YEAR which must be converted
|
|
// to month and day of month.
|
|
value = getRolledValue(woy, amount, min, max) - 1;
|
|
d = getCalendarDate(day1 + value * 7);
|
|
set(MONTH, d.getMonth() - 1);
|
|
set(DAY_OF_MONTH, d.getDayOfMonth());
|
|
return;
|
|
}
|
|
|
|
case WEEK_OF_MONTH:
|
|
{
|
|
boolean isTransitionYear = isTransitionYear(jdate.getNormalizedYear());
|
|
// dow: relative day of week from the first day of week
|
|
int dow = internalGet(DAY_OF_WEEK) - getFirstDayOfWeek();
|
|
if (dow < 0) {
|
|
dow += 7;
|
|
}
|
|
|
|
long fd = cachedFixedDate;
|
|
long month1; // fixed date of the first day (usually 1) of the month
|
|
int monthLength; // actual month length
|
|
if (isTransitionYear) {
|
|
month1 = getFixedDateMonth1(jdate, fd);
|
|
monthLength = actualMonthLength();
|
|
} else {
|
|
month1 = fd - internalGet(DAY_OF_MONTH) + 1;
|
|
monthLength = jcal.getMonthLength(jdate);
|
|
}
|
|
|
|
// the first day of week of the month.
|
|
long monthDay1st = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(month1 + 6,
|
|
getFirstDayOfWeek());
|
|
// if the week has enough days to form a week, the
|
|
// week starts from the previous month.
|
|
if ((int)(monthDay1st - month1) >= getMinimalDaysInFirstWeek()) {
|
|
monthDay1st -= 7;
|
|
}
|
|
max = getActualMaximum(field);
|
|
|
|
// value: the new WEEK_OF_MONTH value
|
|
int value = getRolledValue(internalGet(field), amount, 1, max) - 1;
|
|
|
|
// nfd: fixed date of the rolled date
|
|
long nfd = monthDay1st + value * 7 + dow;
|
|
|
|
// Unlike WEEK_OF_YEAR, we need to change day of week if the
|
|
// nfd is out of the month.
|
|
if (nfd < month1) {
|
|
nfd = month1;
|
|
} else if (nfd >= (month1 + monthLength)) {
|
|
nfd = month1 + monthLength - 1;
|
|
}
|
|
set(DAY_OF_MONTH, getCalendarDate(nfd).getDayOfMonth());
|
|
return;
|
|
}
|
|
|
|
case DAY_OF_MONTH:
|
|
{
|
|
if (!isTransitionYear(jdate.getNormalizedYear())) {
|
|
max = jcal.getMonthLength(jdate);
|
|
break;
|
|
}
|
|
|
|
// TODO: Need to change the spec to be usable DAY_OF_MONTH rolling...
|
|
|
|
// Transition handling. We can't change year and era
|
|
// values here due to the Calendar roll spec!
|
|
long month1 = getFixedDateMonth1(jdate, cachedFixedDate);
|
|
|
|
// It may not be a regular month. Convert the date and range to
|
|
// the relative values, perform the roll, and
|
|
// convert the result back to the rolled date.
|
|
int value = getRolledValue((int)(cachedFixedDate - month1), amount,
|
|
0, actualMonthLength() - 1);
|
|
LocalGregorianCalendar.Date d = getCalendarDate(month1 + value);
|
|
assert getEraIndex(d) == internalGetEra()
|
|
&& d.getYear() == internalGet(YEAR) && d.getMonth()-1 == internalGet(MONTH);
|
|
set(DAY_OF_MONTH, d.getDayOfMonth());
|
|
return;
|
|
}
|
|
|
|
case DAY_OF_YEAR:
|
|
{
|
|
max = getActualMaximum(field);
|
|
if (!isTransitionYear(jdate.getNormalizedYear())) {
|
|
break;
|
|
}
|
|
|
|
// Handle transition. We can't change year and era values
|
|
// here due to the Calendar roll spec.
|
|
int value = getRolledValue(internalGet(DAY_OF_YEAR), amount, min, max);
|
|
long jan0 = cachedFixedDate - internalGet(DAY_OF_YEAR);
|
|
LocalGregorianCalendar.Date d = getCalendarDate(jan0 + value);
|
|
assert getEraIndex(d) == internalGetEra() && d.getYear() == internalGet(YEAR);
|
|
set(MONTH, d.getMonth() - 1);
|
|
set(DAY_OF_MONTH, d.getDayOfMonth());
|
|
return;
|
|
}
|
|
|
|
case DAY_OF_WEEK:
|
|
{
|
|
int normalizedYear = jdate.getNormalizedYear();
|
|
if (!isTransitionYear(normalizedYear) && !isTransitionYear(normalizedYear - 1)) {
|
|
// If the week of year is in the same year, we can
|
|
// just change DAY_OF_WEEK.
|
|
int weekOfYear = internalGet(WEEK_OF_YEAR);
|
|
if (weekOfYear > 1 && weekOfYear < 52) {
|
|
set(WEEK_OF_YEAR, internalGet(WEEK_OF_YEAR));
|
|
max = SATURDAY;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We need to handle it in a different way around year
|
|
// boundaries and in the transition year. Note that
|
|
// changing era and year values violates the roll
|
|
// rule: not changing larger calendar fields...
|
|
amount %= 7;
|
|
if (amount == 0) {
|
|
return;
|
|
}
|
|
long fd = cachedFixedDate;
|
|
long dowFirst = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(fd, getFirstDayOfWeek());
|
|
fd += amount;
|
|
if (fd < dowFirst) {
|
|
fd += 7;
|
|
} else if (fd >= dowFirst + 7) {
|
|
fd -= 7;
|
|
}
|
|
LocalGregorianCalendar.Date d = getCalendarDate(fd);
|
|
set(ERA, getEraIndex(d));
|
|
set(d.getYear(), d.getMonth() - 1, d.getDayOfMonth());
|
|
return;
|
|
}
|
|
|
|
case DAY_OF_WEEK_IN_MONTH:
|
|
{
|
|
min = 1; // after having normalized, min should be 1.
|
|
if (!isTransitionYear(jdate.getNormalizedYear())) {
|
|
int dom = internalGet(DAY_OF_MONTH);
|
|
int monthLength = jcal.getMonthLength(jdate);
|
|
int lastDays = monthLength % 7;
|
|
max = monthLength / 7;
|
|
int x = (dom - 1) % 7;
|
|
if (x < lastDays) {
|
|
max++;
|
|
}
|
|
set(DAY_OF_WEEK, internalGet(DAY_OF_WEEK));
|
|
break;
|
|
}
|
|
|
|
// Transition year handling.
|
|
long fd = cachedFixedDate;
|
|
long month1 = getFixedDateMonth1(jdate, fd);
|
|
int monthLength = actualMonthLength();
|
|
int lastDays = monthLength % 7;
|
|
max = monthLength / 7;
|
|
int x = (int)(fd - month1) % 7;
|
|
if (x < lastDays) {
|
|
max++;
|
|
}
|
|
int value = getRolledValue(internalGet(field), amount, min, max) - 1;
|
|
fd = month1 + value * 7 + x;
|
|
LocalGregorianCalendar.Date d = getCalendarDate(fd);
|
|
set(DAY_OF_MONTH, d.getDayOfMonth());
|
|
return;
|
|
}
|
|
}
|
|
|
|
set(field, getRolledValue(internalGet(field), amount, min, max));
|
|
}
|
|
|
|
@Override
|
|
public String getDisplayName(int field, int style, Locale locale) {
|
|
if (!checkDisplayNameParams(field, style, SHORT, NARROW_FORMAT, locale,
|
|
ERA_MASK|YEAR_MASK|MONTH_MASK|DAY_OF_WEEK_MASK|AM_PM_MASK)) {
|
|
return null;
|
|
}
|
|
|
|
int fieldValue = get(field);
|
|
|
|
// "GanNen" is supported only in the LONG style.
|
|
if (field == YEAR
|
|
&& (getBaseStyle(style) != LONG || fieldValue != 1 || get(ERA) == 0)) {
|
|
return null;
|
|
}
|
|
|
|
String name = CalendarDataUtility.retrieveFieldValueName(getCalendarType(), field,
|
|
fieldValue, style, locale);
|
|
// If the ERA value is null or empty, then
|
|
// try to get its name or abbreviation from the Era instance.
|
|
if ((name == null || name.isEmpty()) &&
|
|
field == ERA &&
|
|
fieldValue < eras.length) {
|
|
Era era = eras[fieldValue];
|
|
name = (style == SHORT) ? era.getAbbreviation() : era.getName();
|
|
}
|
|
return name;
|
|
}
|
|
|
|
@Override
|
|
public Map<String,Integer> getDisplayNames(int field, int style, Locale locale) {
|
|
if (!checkDisplayNameParams(field, style, ALL_STYLES, NARROW_FORMAT, locale,
|
|
ERA_MASK|YEAR_MASK|MONTH_MASK|DAY_OF_WEEK_MASK|AM_PM_MASK)) {
|
|
return null;
|
|
}
|
|
Map<String, Integer> names;
|
|
names = CalendarDataUtility.retrieveFieldValueNames(getCalendarType(), field, style, locale);
|
|
// If strings[] has fewer than eras[], get more names from eras[].
|
|
if (names != null) {
|
|
if (field == ERA) {
|
|
int size = names.size();
|
|
if (style == ALL_STYLES) {
|
|
Set<Integer> values = new HashSet<>();
|
|
// count unique era values
|
|
for (String key : names.keySet()) {
|
|
values.add(names.get(key));
|
|
}
|
|
size = values.size();
|
|
}
|
|
if (size < eras.length) {
|
|
int baseStyle = getBaseStyle(style);
|
|
for (int i = 0; i < eras.length; i++) {
|
|
if (!names.values().contains(i)) {
|
|
Era era = eras[i];
|
|
if (baseStyle == ALL_STYLES || baseStyle == SHORT
|
|
|| baseStyle == NARROW_FORMAT) {
|
|
names.put(era.getAbbreviation(), i);
|
|
}
|
|
if (baseStyle == ALL_STYLES || baseStyle == LONG) {
|
|
names.put(era.getName(), i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
|
|
/**
|
|
* Returns the minimum value for the given calendar field of this
|
|
* {@code Calendar} instance. The minimum value is
|
|
* defined as the smallest value returned by the
|
|
* {@link Calendar#get(int) get} method for any possible time value,
|
|
* taking into consideration the current values of the
|
|
* {@link Calendar#getFirstDayOfWeek() getFirstDayOfWeek},
|
|
* {@link Calendar#getMinimalDaysInFirstWeek() getMinimalDaysInFirstWeek},
|
|
* and {@link Calendar#getTimeZone() getTimeZone} methods.
|
|
*
|
|
* @param field the calendar field.
|
|
* @return the minimum value for the given calendar field.
|
|
* @see #getMaximum(int)
|
|
* @see #getGreatestMinimum(int)
|
|
* @see #getLeastMaximum(int)
|
|
* @see #getActualMinimum(int)
|
|
* @see #getActualMaximum(int)
|
|
*/
|
|
public int getMinimum(int field) {
|
|
return MIN_VALUES[field];
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum value for the given calendar field of this
|
|
* {@code GregorianCalendar} instance. The maximum value is
|
|
* defined as the largest value returned by the
|
|
* {@link Calendar#get(int) get} method for any possible time value,
|
|
* taking into consideration the current values of the
|
|
* {@link Calendar#getFirstDayOfWeek() getFirstDayOfWeek},
|
|
* {@link Calendar#getMinimalDaysInFirstWeek() getMinimalDaysInFirstWeek},
|
|
* and {@link Calendar#getTimeZone() getTimeZone} methods.
|
|
*
|
|
* @param field the calendar field.
|
|
* @return the maximum value for the given calendar field.
|
|
* @see #getMinimum(int)
|
|
* @see #getGreatestMinimum(int)
|
|
* @see #getLeastMaximum(int)
|
|
* @see #getActualMinimum(int)
|
|
* @see #getActualMaximum(int)
|
|
*/
|
|
public int getMaximum(int field) {
|
|
return switch (field) {
|
|
case YEAR -> {
|
|
// The value should depend on the time zone of this calendar.
|
|
LocalGregorianCalendar.Date d = jcal.getCalendarDate(Long.MAX_VALUE, getZone());
|
|
yield Math.max(LEAST_MAX_VALUES[YEAR], d.getYear());
|
|
}
|
|
default -> MAX_VALUES[field];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the highest minimum value for the given calendar field
|
|
* of this {@code GregorianCalendar} instance. The highest
|
|
* minimum value is defined as the largest value returned by
|
|
* {@link #getActualMinimum(int)} for any possible time value,
|
|
* taking into consideration the current values of the
|
|
* {@link Calendar#getFirstDayOfWeek() getFirstDayOfWeek},
|
|
* {@link Calendar#getMinimalDaysInFirstWeek() getMinimalDaysInFirstWeek},
|
|
* and {@link Calendar#getTimeZone() getTimeZone} methods.
|
|
*
|
|
* @param field the calendar field.
|
|
* @return the highest minimum value for the given calendar field.
|
|
* @see #getMinimum(int)
|
|
* @see #getMaximum(int)
|
|
* @see #getLeastMaximum(int)
|
|
* @see #getActualMinimum(int)
|
|
* @see #getActualMaximum(int)
|
|
*/
|
|
public int getGreatestMinimum(int field) {
|
|
return field == YEAR ? 1 : MIN_VALUES[field];
|
|
}
|
|
|
|
/**
|
|
* Returns the lowest maximum value for the given calendar field
|
|
* of this {@code GregorianCalendar} instance. The lowest
|
|
* maximum value is defined as the smallest value returned by
|
|
* {@link #getActualMaximum(int)} for any possible time value,
|
|
* taking into consideration the current values of the
|
|
* {@link Calendar#getFirstDayOfWeek() getFirstDayOfWeek},
|
|
* {@link Calendar#getMinimalDaysInFirstWeek() getMinimalDaysInFirstWeek},
|
|
* and {@link Calendar#getTimeZone() getTimeZone} methods.
|
|
*
|
|
* @param field the calendar field
|
|
* @return the lowest maximum value for the given calendar field.
|
|
* @see #getMinimum(int)
|
|
* @see #getMaximum(int)
|
|
* @see #getGreatestMinimum(int)
|
|
* @see #getActualMinimum(int)
|
|
* @see #getActualMaximum(int)
|
|
*/
|
|
public int getLeastMaximum(int field) {
|
|
return switch (field) {
|
|
case YEAR -> Math.min(LEAST_MAX_VALUES[YEAR], getMaximum(YEAR));
|
|
default -> LEAST_MAX_VALUES[field];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the minimum value that this calendar field could have,
|
|
* taking into consideration the given time value and the current
|
|
* values of the
|
|
* {@link Calendar#getFirstDayOfWeek() getFirstDayOfWeek},
|
|
* {@link Calendar#getMinimalDaysInFirstWeek() getMinimalDaysInFirstWeek},
|
|
* and {@link Calendar#getTimeZone() getTimeZone} methods.
|
|
*
|
|
* @param field the calendar field
|
|
* @return the minimum of the given field for the time value of
|
|
* this {@code JapaneseImperialCalendar}
|
|
* @see #getMinimum(int)
|
|
* @see #getMaximum(int)
|
|
* @see #getGreatestMinimum(int)
|
|
* @see #getLeastMaximum(int)
|
|
* @see #getActualMaximum(int)
|
|
*/
|
|
public int getActualMinimum(int field) {
|
|
if (!isFieldSet(YEAR_MASK|MONTH_MASK|WEEK_OF_YEAR_MASK, field)) {
|
|
return getMinimum(field);
|
|
}
|
|
|
|
int value = 0;
|
|
JapaneseImperialCalendar jc = getNormalizedCalendar();
|
|
// Get a local date which includes time of day and time zone,
|
|
// which are missing in jc.jdate.
|
|
LocalGregorianCalendar.Date jd = jcal.getCalendarDate(jc.getTimeInMillis(),
|
|
getZone());
|
|
int eraIndex = getEraIndex(jd);
|
|
switch (field) {
|
|
case YEAR -> {
|
|
if (eraIndex > BEFORE_MEIJI) {
|
|
value = 1;
|
|
long since = eras[eraIndex].getSince(getZone());
|
|
CalendarDate d = jcal.getCalendarDate(since, getZone());
|
|
// Use the same year in jd to take care of leap
|
|
// years. i.e., both jd and d must agree on leap
|
|
// or common years.
|
|
jd.setYear(d.getYear());
|
|
jcal.normalize(jd);
|
|
assert jd.isLeapYear() == d.isLeapYear();
|
|
if (getYearOffsetInMillis(jd) < getYearOffsetInMillis(d)) {
|
|
value++;
|
|
}
|
|
} else {
|
|
value = getMinimum(field);
|
|
CalendarDate d = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
// Use an equvalent year of d.getYear() if
|
|
// possible. Otherwise, ignore the leap year and
|
|
// common year difference.
|
|
int y = d.getYear();
|
|
if (y > 400) {
|
|
y -= 400;
|
|
}
|
|
jd.setYear(y);
|
|
jcal.normalize(jd);
|
|
if (getYearOffsetInMillis(jd) < getYearOffsetInMillis(d)) {
|
|
value++;
|
|
}
|
|
}
|
|
}
|
|
case MONTH -> {
|
|
// In Before Meiji and Meiji, January is the first month.
|
|
if (eraIndex > MEIJI && jd.getYear() == 1) {
|
|
long since = eras[eraIndex].getSince(getZone());
|
|
CalendarDate d = jcal.getCalendarDate(since, getZone());
|
|
value = d.getMonth() - 1;
|
|
if (jd.getDayOfMonth() < d.getDayOfMonth()) {
|
|
value++;
|
|
}
|
|
}
|
|
}
|
|
case WEEK_OF_YEAR -> {
|
|
value = 1;
|
|
CalendarDate d = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
// shift 400 years to avoid underflow
|
|
d.addYear(+400);
|
|
jcal.normalize(d);
|
|
jd.setEra(d.getEra());
|
|
jd.setYear(d.getYear());
|
|
jcal.normalize(jd);
|
|
|
|
long jan1 = jcal.getFixedDate(d);
|
|
long fd = jcal.getFixedDate(jd);
|
|
int woy = getWeekNumber(jan1, fd);
|
|
long day1 = fd - (7 * (woy - 1));
|
|
if ((day1 < jan1) ||
|
|
(day1 == jan1 &&
|
|
jd.getTimeOfDay() < d.getTimeOfDay())) {
|
|
value++;
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum value that this calendar field could have,
|
|
* taking into consideration the given time value and the current
|
|
* values of the
|
|
* {@link Calendar#getFirstDayOfWeek() getFirstDayOfWeek},
|
|
* {@link Calendar#getMinimalDaysInFirstWeek() getMinimalDaysInFirstWeek},
|
|
* and
|
|
* {@link Calendar#getTimeZone() getTimeZone} methods.
|
|
* For example, if the date of this instance is Heisei 16February 1,
|
|
* the actual maximum value of the {@code DAY_OF_MONTH} field
|
|
* is 29 because Heisei 16 is a leap year, and if the date of this
|
|
* instance is Heisei 17 February 1, it's 28.
|
|
*
|
|
* @param field the calendar field
|
|
* @return the maximum of the given field for the time value of
|
|
* this {@code JapaneseImperialCalendar}
|
|
* @see #getMinimum(int)
|
|
* @see #getMaximum(int)
|
|
* @see #getGreatestMinimum(int)
|
|
* @see #getLeastMaximum(int)
|
|
* @see #getActualMinimum(int)
|
|
*/
|
|
public int getActualMaximum(int field) {
|
|
final int fieldsForFixedMax = ERA_MASK|DAY_OF_WEEK_MASK|HOUR_MASK|AM_PM_MASK|
|
|
HOUR_OF_DAY_MASK|MINUTE_MASK|SECOND_MASK|MILLISECOND_MASK|
|
|
ZONE_OFFSET_MASK|DST_OFFSET_MASK;
|
|
if ((fieldsForFixedMax & (1<<field)) != 0) {
|
|
return getMaximum(field);
|
|
}
|
|
|
|
JapaneseImperialCalendar jc = getNormalizedCalendar();
|
|
LocalGregorianCalendar.Date date = jc.jdate;
|
|
|
|
return switch (field) {
|
|
case MONTH -> {
|
|
int month = DECEMBER;
|
|
if (isTransitionYear(date.getNormalizedYear())) {
|
|
// TODO: there may be multiple transitions in a year.
|
|
int eraIndex = getEraIndex(date);
|
|
if (date.getYear() != 1) {
|
|
eraIndex++;
|
|
assert eraIndex < eras.length;
|
|
}
|
|
long transition = sinceFixedDates[eraIndex];
|
|
long fd = jc.cachedFixedDate;
|
|
if (fd < transition) {
|
|
LocalGregorianCalendar.Date ldate
|
|
= (LocalGregorianCalendar.Date) date.clone();
|
|
jcal.getCalendarDateFromFixedDate(ldate, transition - 1);
|
|
month = ldate.getMonth() - 1;
|
|
}
|
|
} else {
|
|
LocalGregorianCalendar.Date d = jcal.getCalendarDate(Long.MAX_VALUE, getZone());
|
|
if (date.getEra() == d.getEra() && date.getYear() == d.getYear()) {
|
|
month = d.getMonth() - 1;
|
|
}
|
|
}
|
|
yield month;
|
|
}
|
|
case DAY_OF_MONTH -> jcal.getMonthLength(date);
|
|
case DAY_OF_YEAR -> {
|
|
if (isTransitionYear(date.getNormalizedYear())) {
|
|
// Handle transition year.
|
|
// TODO: there may be multiple transitions in a year.
|
|
int eraIndex = getEraIndex(date);
|
|
if (date.getYear() != 1) {
|
|
eraIndex++;
|
|
assert eraIndex < eras.length;
|
|
}
|
|
long transition = sinceFixedDates[eraIndex];
|
|
long fd = jc.cachedFixedDate;
|
|
CalendarDate d = gcal.newCalendarDate(TimeZone.NO_TIMEZONE);
|
|
d.setDate(date.getNormalizedYear(), BaseCalendar.JANUARY, 1);
|
|
if (fd < transition) {
|
|
yield (int) (transition - gcal.getFixedDate(d));
|
|
}
|
|
d.addYear(1);
|
|
yield (int) (gcal.getFixedDate(d) - transition);
|
|
}
|
|
LocalGregorianCalendar.Date d = jcal.getCalendarDate(Long.MAX_VALUE, getZone());
|
|
if (date.getEra() == d.getEra() && date.getYear() == d.getYear()) {
|
|
long fd = jcal.getFixedDate(d);
|
|
long jan1 = getFixedDateJan1(d, fd);
|
|
yield (int) (fd - jan1) + 1;
|
|
} else if (date.getYear() == getMinimum(YEAR)) {
|
|
CalendarDate d1 = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
long fd1 = jcal.getFixedDate(d1);
|
|
d1.addYear(1);
|
|
d1.setMonth(BaseCalendar.JANUARY).setDayOfMonth(1);
|
|
jcal.normalize(d1);
|
|
long fd2 = jcal.getFixedDate(d1);
|
|
yield (int) (fd2 - fd1);
|
|
} else {
|
|
yield jcal.getYearLength(date);
|
|
}
|
|
}
|
|
case WEEK_OF_YEAR -> {
|
|
if (!isTransitionYear(date.getNormalizedYear())) {
|
|
LocalGregorianCalendar.Date jd = jcal.getCalendarDate(Long.MAX_VALUE, getZone());
|
|
if (date.getEra() == jd.getEra() && date.getYear() == jd.getYear()) {
|
|
long fd = jcal.getFixedDate(jd);
|
|
long jan1 = getFixedDateJan1(jd, fd);
|
|
yield getWeekNumber(jan1, fd);
|
|
} else if (date.getEra() == null && date.getYear() == getMinimum(YEAR)) {
|
|
CalendarDate d = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
// shift 400 years to avoid underflow
|
|
d.addYear(+400);
|
|
jcal.normalize(d);
|
|
jd.setEra(d.getEra());
|
|
jd.setDate(d.getYear() + 1, BaseCalendar.JANUARY, 1);
|
|
jcal.normalize(jd);
|
|
long jan1 = jcal.getFixedDate(d);
|
|
long nextJan1 = jcal.getFixedDate(jd);
|
|
long nextJan1st = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(nextJan1 + 6,
|
|
getFirstDayOfWeek());
|
|
int ndays = (int) (nextJan1st - nextJan1);
|
|
if (ndays >= getMinimalDaysInFirstWeek()) {
|
|
nextJan1st -= 7;
|
|
}
|
|
yield getWeekNumber(jan1, nextJan1st);
|
|
}
|
|
// Get the day of week of January 1 of the year
|
|
CalendarDate d = gcal.newCalendarDate(TimeZone.NO_TIMEZONE);
|
|
d.setDate(date.getNormalizedYear(), BaseCalendar.JANUARY, 1);
|
|
int dayOfWeek = gcal.getDayOfWeek(d);
|
|
// Normalize the day of week with the firstDayOfWeek value
|
|
dayOfWeek -= getFirstDayOfWeek();
|
|
if (dayOfWeek < 0) {
|
|
dayOfWeek += 7;
|
|
}
|
|
int magic = dayOfWeek + getMinimalDaysInFirstWeek() - 1;
|
|
if ((magic == 6) ||
|
|
(date.isLeapYear() && (magic == 5 || magic == 12))) {
|
|
yield 53;
|
|
}
|
|
yield 52;
|
|
}
|
|
|
|
if (jc == this) {
|
|
jc = (JapaneseImperialCalendar) jc.clone();
|
|
}
|
|
int max = getActualMaximum(DAY_OF_YEAR);
|
|
jc.set(DAY_OF_YEAR, max);
|
|
int weekOfYear = jc.get(WEEK_OF_YEAR);
|
|
if (weekOfYear == 1 && max > 7) {
|
|
jc.add(WEEK_OF_YEAR, -1);
|
|
weekOfYear = jc.get(WEEK_OF_YEAR);
|
|
}
|
|
yield weekOfYear;
|
|
}
|
|
case WEEK_OF_MONTH -> {
|
|
LocalGregorianCalendar.Date jd = jcal.getCalendarDate(Long.MAX_VALUE, getZone());
|
|
if (date.getEra() == jd.getEra() && date.getYear() == jd.getYear()) {
|
|
long fd = jcal.getFixedDate(jd);
|
|
long month1 = fd - jd.getDayOfMonth() + 1;
|
|
yield getWeekNumber(month1, fd);
|
|
}
|
|
CalendarDate d = gcal.newCalendarDate(TimeZone.NO_TIMEZONE);
|
|
d.setDate(date.getNormalizedYear(), date.getMonth(), 1);
|
|
int dayOfWeek = gcal.getDayOfWeek(d);
|
|
int monthLength = actualMonthLength();
|
|
dayOfWeek -= getFirstDayOfWeek();
|
|
if (dayOfWeek < 0) {
|
|
dayOfWeek += 7;
|
|
}
|
|
int nDaysFirstWeek = 7 - dayOfWeek; // # of days in the first week
|
|
int weekOfMonth = 3;
|
|
if (nDaysFirstWeek >= getMinimalDaysInFirstWeek()) {
|
|
weekOfMonth++;
|
|
}
|
|
monthLength -= nDaysFirstWeek + 7 * 3;
|
|
if (monthLength > 0) {
|
|
weekOfMonth++;
|
|
if (monthLength > 7) {
|
|
weekOfMonth++;
|
|
}
|
|
}
|
|
yield weekOfMonth;
|
|
}
|
|
case DAY_OF_WEEK_IN_MONTH -> {
|
|
int ndays, dow1;
|
|
int dow = date.getDayOfWeek();
|
|
BaseCalendar.Date d = (BaseCalendar.Date) date.clone();
|
|
ndays = jcal.getMonthLength(d);
|
|
d.setDayOfMonth(1);
|
|
jcal.normalize(d);
|
|
dow1 = d.getDayOfWeek();
|
|
int x = dow - dow1;
|
|
if (x < 0) {
|
|
x += 7;
|
|
}
|
|
ndays -= x;
|
|
yield (ndays + 6) / 7;
|
|
}
|
|
case YEAR -> {
|
|
CalendarDate jd = jcal.getCalendarDate(jc.getTimeInMillis(), getZone());
|
|
CalendarDate d;
|
|
int eraIndex = getEraIndex(date);
|
|
int year;
|
|
if (eraIndex == eras.length - 1) {
|
|
d = jcal.getCalendarDate(Long.MAX_VALUE, getZone());
|
|
year = d.getYear();
|
|
// Use an equivalent year for the
|
|
// getYearOffsetInMillis call to avoid overflow.
|
|
if (year > 400) {
|
|
jd.setYear(year - 400);
|
|
}
|
|
} else {
|
|
d = jcal.getCalendarDate(eras[eraIndex + 1].getSince(getZone()) - 1, getZone());
|
|
year = d.getYear();
|
|
// Use the same year as d.getYear() to be
|
|
// consistent with leap and common years.
|
|
jd.setYear(year);
|
|
}
|
|
jcal.normalize(jd);
|
|
if (getYearOffsetInMillis(jd) > getYearOffsetInMillis(d)) {
|
|
year--;
|
|
}
|
|
yield year;
|
|
}
|
|
default -> throw new ArrayIndexOutOfBoundsException(field);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the millisecond offset from the beginning of the
|
|
* year. In the year for Long.MIN_VALUE, it's a pseudo value
|
|
* beyond the limit. The given CalendarDate object must have been
|
|
* normalized before calling this method.
|
|
*/
|
|
private long getYearOffsetInMillis(CalendarDate date) {
|
|
long t = (jcal.getDayOfYear(date) - 1) * ONE_DAY;
|
|
return t + date.getTimeOfDay() - date.getZoneOffset();
|
|
}
|
|
|
|
public Object clone() {
|
|
JapaneseImperialCalendar other = (JapaneseImperialCalendar) super.clone();
|
|
|
|
other.jdate = (LocalGregorianCalendar.Date) jdate.clone();
|
|
other.originalFields = null;
|
|
other.zoneOffsets = null;
|
|
return other;
|
|
}
|
|
|
|
public TimeZone getTimeZone() {
|
|
TimeZone zone = super.getTimeZone();
|
|
// To share the zone by the CalendarDate
|
|
jdate.setZone(zone);
|
|
return zone;
|
|
}
|
|
|
|
public void setTimeZone(TimeZone zone) {
|
|
super.setTimeZone(zone);
|
|
// To share the zone by the CalendarDate
|
|
jdate.setZone(zone);
|
|
}
|
|
|
|
/**
|
|
* The fixed date corresponding to jdate. If the value is
|
|
* Long.MIN_VALUE, the fixed date value is unknown.
|
|
*/
|
|
private transient long cachedFixedDate = Long.MIN_VALUE;
|
|
|
|
/**
|
|
* Converts the time value (millisecond offset from the <a
|
|
* href="Calendar.html#Epoch">Epoch</a>) to calendar field values.
|
|
* The time is <em>not</em>
|
|
* recomputed first; to recompute the time, then the fields, call the
|
|
* {@code complete} method.
|
|
*
|
|
* @see Calendar#complete
|
|
*/
|
|
protected void computeFields() {
|
|
int mask = 0;
|
|
if (isPartiallyNormalized()) {
|
|
// Determine which calendar fields need to be computed.
|
|
mask = getSetStateFields();
|
|
int fieldMask = ~mask & ALL_FIELDS;
|
|
if (fieldMask != 0 || cachedFixedDate == Long.MIN_VALUE) {
|
|
mask |= computeFields(fieldMask,
|
|
mask & (ZONE_OFFSET_MASK|DST_OFFSET_MASK));
|
|
assert mask == ALL_FIELDS;
|
|
}
|
|
} else {
|
|
// Specify all fields
|
|
mask = ALL_FIELDS;
|
|
computeFields(mask, 0);
|
|
}
|
|
// After computing all the fields, set the field state to `COMPUTED'.
|
|
setFieldsComputed(mask);
|
|
}
|
|
|
|
/**
|
|
* This computeFields implements the conversion from UTC
|
|
* (millisecond offset from the Epoch) to calendar
|
|
* field values. fieldMask specifies which fields to change the
|
|
* setting state to COMPUTED, although all fields are set to
|
|
* the correct values. This is required to fix 4685354.
|
|
*
|
|
* @param fieldMask a bit mask to specify which fields to change
|
|
* the setting state.
|
|
* @param tzMask a bit mask to specify which time zone offset
|
|
* fields to be used for time calculations
|
|
* @return a new field mask that indicates what field values have
|
|
* actually been set.
|
|
*/
|
|
private int computeFields(int fieldMask, int tzMask) {
|
|
int zoneOffset = 0;
|
|
TimeZone tz = getZone();
|
|
if (zoneOffsets == null) {
|
|
zoneOffsets = new int[2];
|
|
}
|
|
if (tzMask != (ZONE_OFFSET_MASK|DST_OFFSET_MASK)) {
|
|
// BEGIN Android-changed: Android doesn't have sun.util.calendar.ZoneInfo.
|
|
// if (tz instanceof ZoneInfo) {
|
|
// zoneOffset = ((ZoneInfo)tz).getOffsets(time, zoneOffsets);
|
|
if (tz instanceof libcore.util.ZoneInfo) {
|
|
zoneOffset = ((libcore.util.ZoneInfo)tz).getOffsetsByUtcTime(time, zoneOffsets);
|
|
// END Android-changed: Android doesn't have sun.util.calendar.ZoneInfo.
|
|
} else {
|
|
zoneOffset = tz.getOffset(time);
|
|
zoneOffsets[0] = tz.getRawOffset();
|
|
zoneOffsets[1] = zoneOffset - zoneOffsets[0];
|
|
}
|
|
}
|
|
if (tzMask != 0) {
|
|
if (isFieldSet(tzMask, ZONE_OFFSET)) {
|
|
zoneOffsets[0] = internalGet(ZONE_OFFSET);
|
|
}
|
|
if (isFieldSet(tzMask, DST_OFFSET)) {
|
|
zoneOffsets[1] = internalGet(DST_OFFSET);
|
|
}
|
|
zoneOffset = zoneOffsets[0] + zoneOffsets[1];
|
|
}
|
|
|
|
// By computing time and zoneOffset separately, we can take
|
|
// the wider range of time+zoneOffset than the previous
|
|
// implementation.
|
|
long fixedDate = zoneOffset / ONE_DAY;
|
|
int timeOfDay = zoneOffset % (int)ONE_DAY;
|
|
fixedDate += time / ONE_DAY;
|
|
timeOfDay += (int) (time % ONE_DAY);
|
|
if (timeOfDay >= ONE_DAY) {
|
|
timeOfDay -= ONE_DAY;
|
|
++fixedDate;
|
|
} else {
|
|
while (timeOfDay < 0) {
|
|
timeOfDay += ONE_DAY;
|
|
--fixedDate;
|
|
}
|
|
}
|
|
fixedDate += EPOCH_OFFSET;
|
|
|
|
// See if we can use jdate to avoid date calculation.
|
|
if (fixedDate != cachedFixedDate || fixedDate < 0) {
|
|
jcal.getCalendarDateFromFixedDate(jdate, fixedDate);
|
|
cachedFixedDate = fixedDate;
|
|
}
|
|
int era = getEraIndex(jdate);
|
|
int year = jdate.getYear();
|
|
|
|
// Always set the ERA and YEAR values.
|
|
internalSet(ERA, era);
|
|
internalSet(YEAR, year);
|
|
int mask = fieldMask | (ERA_MASK|YEAR_MASK);
|
|
|
|
int month = jdate.getMonth() - 1; // 0-based
|
|
int dayOfMonth = jdate.getDayOfMonth();
|
|
|
|
// Set the basic date fields.
|
|
if ((fieldMask & (MONTH_MASK|DAY_OF_MONTH_MASK|DAY_OF_WEEK_MASK))
|
|
!= 0) {
|
|
internalSet(MONTH, month);
|
|
internalSet(DAY_OF_MONTH, dayOfMonth);
|
|
internalSet(DAY_OF_WEEK, jdate.getDayOfWeek());
|
|
mask |= MONTH_MASK|DAY_OF_MONTH_MASK|DAY_OF_WEEK_MASK;
|
|
}
|
|
|
|
if ((fieldMask & (HOUR_OF_DAY_MASK|AM_PM_MASK|HOUR_MASK
|
|
|MINUTE_MASK|SECOND_MASK|MILLISECOND_MASK)) != 0) {
|
|
if (timeOfDay != 0) {
|
|
int hours = timeOfDay / ONE_HOUR;
|
|
internalSet(HOUR_OF_DAY, hours);
|
|
internalSet(AM_PM, hours / 12); // Assume AM == 0
|
|
internalSet(HOUR, hours % 12);
|
|
int r = timeOfDay % ONE_HOUR;
|
|
internalSet(MINUTE, r / ONE_MINUTE);
|
|
r %= ONE_MINUTE;
|
|
internalSet(SECOND, r / ONE_SECOND);
|
|
internalSet(MILLISECOND, r % ONE_SECOND);
|
|
} else {
|
|
internalSet(HOUR_OF_DAY, 0);
|
|
internalSet(AM_PM, AM);
|
|
internalSet(HOUR, 0);
|
|
internalSet(MINUTE, 0);
|
|
internalSet(SECOND, 0);
|
|
internalSet(MILLISECOND, 0);
|
|
}
|
|
mask |= (HOUR_OF_DAY_MASK|AM_PM_MASK|HOUR_MASK
|
|
|MINUTE_MASK|SECOND_MASK|MILLISECOND_MASK);
|
|
}
|
|
|
|
if ((fieldMask & (ZONE_OFFSET_MASK|DST_OFFSET_MASK)) != 0) {
|
|
internalSet(ZONE_OFFSET, zoneOffsets[0]);
|
|
internalSet(DST_OFFSET, zoneOffsets[1]);
|
|
mask |= (ZONE_OFFSET_MASK|DST_OFFSET_MASK);
|
|
}
|
|
|
|
if ((fieldMask & (DAY_OF_YEAR_MASK|WEEK_OF_YEAR_MASK
|
|
|WEEK_OF_MONTH_MASK|DAY_OF_WEEK_IN_MONTH_MASK)) != 0) {
|
|
int normalizedYear = jdate.getNormalizedYear();
|
|
// If it's a year of an era transition, we need to handle
|
|
// irregular year boundaries.
|
|
boolean transitionYear = isTransitionYear(jdate.getNormalizedYear());
|
|
int dayOfYear;
|
|
long fixedDateJan1;
|
|
if (transitionYear) {
|
|
fixedDateJan1 = getFixedDateJan1(jdate, fixedDate);
|
|
dayOfYear = (int)(fixedDate - fixedDateJan1) + 1;
|
|
} else if (normalizedYear == MIN_VALUES[YEAR]) {
|
|
CalendarDate dx = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
fixedDateJan1 = jcal.getFixedDate(dx);
|
|
dayOfYear = (int)(fixedDate - fixedDateJan1) + 1;
|
|
} else {
|
|
dayOfYear = (int) jcal.getDayOfYear(jdate);
|
|
fixedDateJan1 = fixedDate - dayOfYear + 1;
|
|
}
|
|
long fixedDateMonth1 = transitionYear ?
|
|
getFixedDateMonth1(jdate, fixedDate) : fixedDate - dayOfMonth + 1;
|
|
|
|
internalSet(DAY_OF_YEAR, dayOfYear);
|
|
internalSet(DAY_OF_WEEK_IN_MONTH, (dayOfMonth - 1) / 7 + 1);
|
|
|
|
int weekOfYear = getWeekNumber(fixedDateJan1, fixedDate);
|
|
|
|
// The spec is to calculate WEEK_OF_YEAR in the
|
|
// ISO8601-style. This creates problems, though.
|
|
if (weekOfYear == 0) {
|
|
// If the date belongs to the last week of the
|
|
// previous year, use the week number of "12/31" of
|
|
// the "previous" year. Again, if the previous year is
|
|
// a transition year, we need to take care of it.
|
|
// Usually the previous day of the first day of a year
|
|
// is December 31, which is not always true in the
|
|
// Japanese imperial calendar system.
|
|
long fixedDec31 = fixedDateJan1 - 1;
|
|
long prevJan1;
|
|
LocalGregorianCalendar.Date d = getCalendarDate(fixedDec31);
|
|
if (!(transitionYear || isTransitionYear(d.getNormalizedYear()))) {
|
|
prevJan1 = fixedDateJan1 - 365;
|
|
if (d.isLeapYear()) {
|
|
--prevJan1;
|
|
}
|
|
} else if (transitionYear) {
|
|
if (jdate.getYear() == 1) {
|
|
// As of Reiwa (since Meiji) there's no case
|
|
// that there are multiple transitions in a
|
|
// year. Historically there was such
|
|
// case. There might be such case again in the
|
|
// future.
|
|
if (era > REIWA) {
|
|
CalendarDate pd = eras[era - 1].getSinceDate();
|
|
if (normalizedYear == pd.getYear()) {
|
|
d.setMonth(pd.getMonth()).setDayOfMonth(pd.getDayOfMonth());
|
|
}
|
|
} else {
|
|
d.setMonth(LocalGregorianCalendar.JANUARY).setDayOfMonth(1);
|
|
}
|
|
jcal.normalize(d);
|
|
prevJan1 = jcal.getFixedDate(d);
|
|
} else {
|
|
prevJan1 = fixedDateJan1 - 365;
|
|
if (d.isLeapYear()) {
|
|
--prevJan1;
|
|
}
|
|
}
|
|
} else {
|
|
CalendarDate cd = eras[getEraIndex(jdate)].getSinceDate();
|
|
d.setMonth(cd.getMonth()).setDayOfMonth(cd.getDayOfMonth());
|
|
jcal.normalize(d);
|
|
prevJan1 = jcal.getFixedDate(d);
|
|
}
|
|
weekOfYear = getWeekNumber(prevJan1, fixedDec31);
|
|
} else {
|
|
if (!transitionYear) {
|
|
// Regular years
|
|
if (weekOfYear >= 52) {
|
|
long nextJan1 = fixedDateJan1 + 365;
|
|
if (jdate.isLeapYear()) {
|
|
nextJan1++;
|
|
}
|
|
long nextJan1st = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(nextJan1 + 6,
|
|
getFirstDayOfWeek());
|
|
int ndays = (int)(nextJan1st - nextJan1);
|
|
if (ndays >= getMinimalDaysInFirstWeek() && fixedDate >= (nextJan1st - 7)) {
|
|
// The first days forms a week in which the date is included.
|
|
weekOfYear = 1;
|
|
}
|
|
}
|
|
} else {
|
|
LocalGregorianCalendar.Date d = (LocalGregorianCalendar.Date) jdate.clone();
|
|
long nextJan1;
|
|
if (jdate.getYear() == 1) {
|
|
d.addYear(+1);
|
|
d.setMonth(LocalGregorianCalendar.JANUARY).setDayOfMonth(1);
|
|
nextJan1 = jcal.getFixedDate(d);
|
|
} else {
|
|
int nextEraIndex = getEraIndex(d) + 1;
|
|
CalendarDate cd = eras[nextEraIndex].getSinceDate();
|
|
d.setEra(eras[nextEraIndex]);
|
|
d.setDate(1, cd.getMonth(), cd.getDayOfMonth());
|
|
jcal.normalize(d);
|
|
nextJan1 = jcal.getFixedDate(d);
|
|
}
|
|
long nextJan1st = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(nextJan1 + 6,
|
|
getFirstDayOfWeek());
|
|
int ndays = (int)(nextJan1st - nextJan1);
|
|
if (ndays >= getMinimalDaysInFirstWeek() && fixedDate >= (nextJan1st - 7)) {
|
|
// The first days forms a week in which the date is included.
|
|
weekOfYear = 1;
|
|
}
|
|
}
|
|
}
|
|
internalSet(WEEK_OF_YEAR, weekOfYear);
|
|
internalSet(WEEK_OF_MONTH, getWeekNumber(fixedDateMonth1, fixedDate));
|
|
mask |= (DAY_OF_YEAR_MASK|WEEK_OF_YEAR_MASK|WEEK_OF_MONTH_MASK|DAY_OF_WEEK_IN_MONTH_MASK);
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of weeks in a period between fixedDay1 and
|
|
* fixedDate. The getFirstDayOfWeek-getMinimalDaysInFirstWeek rule
|
|
* is applied to calculate the number of weeks.
|
|
*
|
|
* @param fixedDay1 the fixed date of the first day of the period
|
|
* @param fixedDate the fixed date of the last day of the period
|
|
* @return the number of weeks of the given period
|
|
*/
|
|
private int getWeekNumber(long fixedDay1, long fixedDate) {
|
|
// We can always use `jcal' since Julian and Gregorian are the
|
|
// same thing for this calculation.
|
|
long fixedDay1st = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(fixedDay1 + 6,
|
|
getFirstDayOfWeek());
|
|
int ndays = (int)(fixedDay1st - fixedDay1);
|
|
assert ndays <= 7;
|
|
if (ndays >= getMinimalDaysInFirstWeek()) {
|
|
fixedDay1st -= 7;
|
|
}
|
|
int normalizedDayOfPeriod = (int)(fixedDate - fixedDay1st);
|
|
if (normalizedDayOfPeriod >= 0) {
|
|
return normalizedDayOfPeriod / 7 + 1;
|
|
}
|
|
return CalendarUtils.floorDivide(normalizedDayOfPeriod, 7) + 1;
|
|
}
|
|
|
|
/**
|
|
* Converts calendar field values to the time value (millisecond
|
|
* offset from the <a href="Calendar.html#Epoch">Epoch</a>).
|
|
*
|
|
* @throws IllegalArgumentException if any calendar fields are invalid.
|
|
*/
|
|
protected void computeTime() {
|
|
// In non-lenient mode, perform brief checking of calendar
|
|
// fields which have been set externally. Through this
|
|
// checking, the field values are stored in originalFields[]
|
|
// to see if any of them are normalized later.
|
|
if (!isLenient()) {
|
|
if (originalFields == null) {
|
|
originalFields = new int[FIELD_COUNT];
|
|
}
|
|
for (int field = 0; field < FIELD_COUNT; field++) {
|
|
int value = internalGet(field);
|
|
if (isExternallySet(field)) {
|
|
// Quick validation for any out of range values
|
|
if (value < getMinimum(field) || value > getMaximum(field)) {
|
|
throw new IllegalArgumentException(getFieldName(field));
|
|
}
|
|
}
|
|
originalFields[field] = value;
|
|
}
|
|
}
|
|
|
|
// Let the super class determine which calendar fields to be
|
|
// used to calculate the time.
|
|
int fieldMask = selectFields();
|
|
|
|
int year;
|
|
int era;
|
|
|
|
if (isSet(ERA)) {
|
|
era = internalGet(ERA);
|
|
year = isSet(YEAR) ? internalGet(YEAR) : 1;
|
|
} else {
|
|
if (isSet(YEAR)) {
|
|
era = currentEra;
|
|
year = internalGet(YEAR);
|
|
} else {
|
|
// Equivalent to 1970 (Gregorian)
|
|
era = SHOWA;
|
|
year = 45;
|
|
}
|
|
}
|
|
|
|
// Calculate the time of day. We rely on the convention that
|
|
// an UNSET field has 0.
|
|
long timeOfDay = 0;
|
|
if (isFieldSet(fieldMask, HOUR_OF_DAY)) {
|
|
timeOfDay += (long) internalGet(HOUR_OF_DAY);
|
|
} else {
|
|
timeOfDay += internalGet(HOUR);
|
|
// The default value of AM_PM is 0 which designates AM.
|
|
if (isFieldSet(fieldMask, AM_PM)) {
|
|
timeOfDay += 12 * internalGet(AM_PM);
|
|
}
|
|
}
|
|
timeOfDay *= 60;
|
|
timeOfDay += internalGet(MINUTE);
|
|
timeOfDay *= 60;
|
|
timeOfDay += internalGet(SECOND);
|
|
timeOfDay *= 1000;
|
|
timeOfDay += internalGet(MILLISECOND);
|
|
|
|
// Convert the time of day to the number of days and the
|
|
// millisecond offset from midnight.
|
|
long fixedDate = timeOfDay / ONE_DAY;
|
|
timeOfDay %= ONE_DAY;
|
|
while (timeOfDay < 0) {
|
|
timeOfDay += ONE_DAY;
|
|
--fixedDate;
|
|
}
|
|
|
|
// Calculate the fixed date since January 1, 1 (Gregorian).
|
|
fixedDate += getFixedDate(era, year, fieldMask);
|
|
|
|
// millis represents local wall-clock time in milliseconds.
|
|
long millis = (fixedDate - EPOCH_OFFSET) * ONE_DAY + timeOfDay;
|
|
|
|
// Compute the time zone offset and DST offset. There are two potential
|
|
// ambiguities here. We'll assume a 2:00 am (wall time) switchover time
|
|
// for discussion purposes here.
|
|
// 1. The transition into DST. Here, a designated time of 2:00 am - 2:59 am
|
|
// can be in standard or in DST depending. However, 2:00 am is an invalid
|
|
// representation (the representation jumps from 1:59:59 am Std to 3:00:00 am DST).
|
|
// We assume standard time.
|
|
// 2. The transition out of DST. Here, a designated time of 1:00 am - 1:59 am
|
|
// can be in standard or DST. Both are valid representations (the rep
|
|
// jumps from 1:59:59 DST to 1:00:00 Std).
|
|
// Again, we assume standard time.
|
|
// We use the TimeZone object, unless the user has explicitly set the ZONE_OFFSET
|
|
// or DST_OFFSET fields; then we use those fields.
|
|
TimeZone zone = getZone();
|
|
if (zoneOffsets == null) {
|
|
zoneOffsets = new int[2];
|
|
}
|
|
int tzMask = fieldMask & (ZONE_OFFSET_MASK|DST_OFFSET_MASK);
|
|
if (tzMask != (ZONE_OFFSET_MASK|DST_OFFSET_MASK)) {
|
|
// Android-changed: Android doesn't have sun.util.calendar.ZoneInfo.
|
|
// if (zone instanceof ZoneInfo) {
|
|
// ((ZoneInfo)zone).getOffsetsByWall(millis, zoneOffsets);
|
|
// } else {
|
|
zone.getOffsets(millis - zone.getRawOffset(), zoneOffsets);
|
|
// }
|
|
}
|
|
if (tzMask != 0) {
|
|
if (isFieldSet(tzMask, ZONE_OFFSET)) {
|
|
zoneOffsets[0] = internalGet(ZONE_OFFSET);
|
|
}
|
|
if (isFieldSet(tzMask, DST_OFFSET)) {
|
|
zoneOffsets[1] = internalGet(DST_OFFSET);
|
|
}
|
|
}
|
|
|
|
// Adjust the time zone offset values to get the UTC time.
|
|
millis -= zoneOffsets[0] + zoneOffsets[1];
|
|
|
|
// Set this calendar's time in milliseconds
|
|
time = millis;
|
|
|
|
int mask = computeFields(fieldMask | getSetStateFields(), tzMask);
|
|
|
|
if (!isLenient()) {
|
|
for (int field = 0; field < FIELD_COUNT; field++) {
|
|
if (!isExternallySet(field)) {
|
|
continue;
|
|
}
|
|
if (originalFields[field] != internalGet(field)) {
|
|
int wrongValue = internalGet(field);
|
|
// Restore the original field values
|
|
System.arraycopy(originalFields, 0, fields, 0, fields.length);
|
|
throw new IllegalArgumentException(getFieldName(field) + "=" + wrongValue
|
|
+ ", expected " + originalFields[field]);
|
|
}
|
|
}
|
|
}
|
|
setFieldsNormalized(mask);
|
|
}
|
|
|
|
/**
|
|
* Computes the fixed date under either the Gregorian or the
|
|
* Julian calendar, using the given year and the specified calendar fields.
|
|
*
|
|
* @param era era index
|
|
* @param year the normalized year number, with 0 indicating the
|
|
* year 1 BCE, -1 indicating 2 BCE, etc.
|
|
* @param fieldMask the calendar fields to be used for the date calculation
|
|
* @return the fixed date
|
|
* @see Calendar#selectFields
|
|
*/
|
|
private long getFixedDate(int era, int year, int fieldMask) {
|
|
int month = JANUARY;
|
|
int firstDayOfMonth = 1;
|
|
if (isFieldSet(fieldMask, MONTH)) {
|
|
// No need to check if MONTH has been set (no isSet(MONTH)
|
|
// call) since its unset value happens to be JANUARY (0).
|
|
month = internalGet(MONTH);
|
|
|
|
// If the month is out of range, adjust it into range.
|
|
if (month > DECEMBER) {
|
|
year += month / 12;
|
|
month %= 12;
|
|
} else if (month < JANUARY) {
|
|
int[] rem = new int[1];
|
|
year += CalendarUtils.floorDivide(month, 12, rem);
|
|
month = rem[0];
|
|
}
|
|
} else {
|
|
if (year == 1 && era != 0) {
|
|
CalendarDate d = eras[era].getSinceDate();
|
|
month = d.getMonth() - 1;
|
|
firstDayOfMonth = d.getDayOfMonth();
|
|
}
|
|
}
|
|
|
|
// Adjust the base date if year is the minimum value.
|
|
if (year == MIN_VALUES[YEAR]) {
|
|
CalendarDate dx = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
int m = dx.getMonth() - 1;
|
|
if (month < m) {
|
|
month = m;
|
|
}
|
|
if (month == m) {
|
|
firstDayOfMonth = dx.getDayOfMonth();
|
|
}
|
|
}
|
|
|
|
LocalGregorianCalendar.Date date = jcal.newCalendarDate(TimeZone.NO_TIMEZONE);
|
|
date.setEra(era > 0 ? eras[era] : null);
|
|
date.setDate(year, month + 1, firstDayOfMonth);
|
|
jcal.normalize(date);
|
|
|
|
// Get the fixed date since Jan 1, 1 (Gregorian). We are on
|
|
// the first day of either `month' or January in 'year'.
|
|
long fixedDate = jcal.getFixedDate(date);
|
|
|
|
if (isFieldSet(fieldMask, MONTH)) {
|
|
// Month-based calculations
|
|
if (isFieldSet(fieldMask, DAY_OF_MONTH)) {
|
|
// We are on the "first day" of the month (which may
|
|
// not be 1). Just add the offset if DAY_OF_MONTH is
|
|
// set. If the isSet call returns false, that means
|
|
// DAY_OF_MONTH has been selected just because of the
|
|
// selected combination. We don't need to add any
|
|
// since the default value is the "first day".
|
|
if (isSet(DAY_OF_MONTH)) {
|
|
// To avoid underflow with DAY_OF_MONTH-firstDayOfMonth, add
|
|
// DAY_OF_MONTH, then subtract firstDayOfMonth.
|
|
fixedDate += internalGet(DAY_OF_MONTH);
|
|
fixedDate -= firstDayOfMonth;
|
|
}
|
|
} else {
|
|
if (isFieldSet(fieldMask, WEEK_OF_MONTH)) {
|
|
long firstDayOfWeek = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(fixedDate + 6,
|
|
getFirstDayOfWeek());
|
|
// If we have enough days in the first week, then
|
|
// move to the previous week.
|
|
if ((firstDayOfWeek - fixedDate) >= getMinimalDaysInFirstWeek()) {
|
|
firstDayOfWeek -= 7;
|
|
}
|
|
if (isFieldSet(fieldMask, DAY_OF_WEEK)) {
|
|
firstDayOfWeek = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(firstDayOfWeek + 6,
|
|
internalGet(DAY_OF_WEEK));
|
|
}
|
|
// In lenient mode, we treat days of the previous
|
|
// months as a part of the specified
|
|
// WEEK_OF_MONTH. See 4633646.
|
|
fixedDate = firstDayOfWeek + 7 * (internalGet(WEEK_OF_MONTH) - 1);
|
|
} else {
|
|
int dayOfWeek;
|
|
if (isFieldSet(fieldMask, DAY_OF_WEEK)) {
|
|
dayOfWeek = internalGet(DAY_OF_WEEK);
|
|
} else {
|
|
dayOfWeek = getFirstDayOfWeek();
|
|
}
|
|
// We are basing this on the day-of-week-in-month. The only
|
|
// trickiness occurs if the day-of-week-in-month is
|
|
// negative.
|
|
int dowim;
|
|
if (isFieldSet(fieldMask, DAY_OF_WEEK_IN_MONTH)) {
|
|
dowim = internalGet(DAY_OF_WEEK_IN_MONTH);
|
|
} else {
|
|
dowim = 1;
|
|
}
|
|
if (dowim >= 0) {
|
|
fixedDate = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(fixedDate + (7 * dowim) - 1,
|
|
dayOfWeek);
|
|
} else {
|
|
// Go to the first day of the next week of
|
|
// the specified week boundary.
|
|
int lastDate = monthLength(month, year) + (7 * (dowim + 1));
|
|
// Then, get the day of week date on or before the last date.
|
|
fixedDate = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(fixedDate + lastDate - 1,
|
|
dayOfWeek);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// We are on the first day of the year.
|
|
if (isFieldSet(fieldMask, DAY_OF_YEAR)) {
|
|
if (isTransitionYear(date.getNormalizedYear())) {
|
|
fixedDate = getFixedDateJan1(date, fixedDate);
|
|
}
|
|
// Add the offset, then subtract 1. (Make sure to avoid underflow.)
|
|
fixedDate += internalGet(DAY_OF_YEAR);
|
|
fixedDate--;
|
|
} else {
|
|
long firstDayOfWeek = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(fixedDate + 6,
|
|
getFirstDayOfWeek());
|
|
// If we have enough days in the first week, then move
|
|
// to the previous week.
|
|
if ((firstDayOfWeek - fixedDate) >= getMinimalDaysInFirstWeek()) {
|
|
firstDayOfWeek -= 7;
|
|
}
|
|
if (isFieldSet(fieldMask, DAY_OF_WEEK)) {
|
|
int dayOfWeek = internalGet(DAY_OF_WEEK);
|
|
if (dayOfWeek != getFirstDayOfWeek()) {
|
|
firstDayOfWeek = LocalGregorianCalendar.getDayOfWeekDateOnOrBefore(firstDayOfWeek + 6,
|
|
dayOfWeek);
|
|
}
|
|
}
|
|
fixedDate = firstDayOfWeek + 7 * ((long)internalGet(WEEK_OF_YEAR) - 1);
|
|
}
|
|
}
|
|
return fixedDate;
|
|
}
|
|
|
|
/**
|
|
* Returns the fixed date of the first day of the year (usually
|
|
* January 1) before the specified date.
|
|
*
|
|
* @param date the date for which the first day of the year is
|
|
* calculated. The date has to be in the cut-over year.
|
|
* @param fixedDate the fixed date representation of the date
|
|
*/
|
|
private long getFixedDateJan1(LocalGregorianCalendar.Date date, long fixedDate) {
|
|
Era era = date.getEra();
|
|
if (date.getEra() != null && date.getYear() == 1) {
|
|
for (int eraIndex = getEraIndex(date); eraIndex > 0; eraIndex--) {
|
|
CalendarDate d = eras[eraIndex].getSinceDate();
|
|
long fd = gcal.getFixedDate(d);
|
|
// There might be multiple era transitions in a year.
|
|
if (fd > fixedDate) {
|
|
continue;
|
|
}
|
|
return fd;
|
|
}
|
|
}
|
|
CalendarDate d = gcal.newCalendarDate(TimeZone.NO_TIMEZONE);
|
|
d.setDate(date.getNormalizedYear(), Gregorian.JANUARY, 1);
|
|
return gcal.getFixedDate(d);
|
|
}
|
|
|
|
/**
|
|
* Returns the fixed date of the first date of the month (usually
|
|
* the 1st of the month) before the specified date.
|
|
*
|
|
* @param date the date for which the first day of the month is
|
|
* calculated. The date must be in the era transition year.
|
|
* @param fixedDate the fixed date representation of the date
|
|
*/
|
|
private long getFixedDateMonth1(LocalGregorianCalendar.Date date,
|
|
long fixedDate) {
|
|
int eraIndex = getTransitionEraIndex(date);
|
|
if (eraIndex != -1) {
|
|
long transition = sinceFixedDates[eraIndex];
|
|
// If the given date is on or after the transition date, then
|
|
// return the transition date.
|
|
if (transition <= fixedDate) {
|
|
return transition;
|
|
}
|
|
}
|
|
|
|
// Otherwise, we can use the 1st day of the month.
|
|
return fixedDate - date.getDayOfMonth() + 1;
|
|
}
|
|
|
|
/**
|
|
* Returns a LocalGregorianCalendar.Date produced from the specified fixed date.
|
|
*
|
|
* @param fd the fixed date
|
|
*/
|
|
private static LocalGregorianCalendar.Date getCalendarDate(long fd) {
|
|
LocalGregorianCalendar.Date d = jcal.newCalendarDate(TimeZone.NO_TIMEZONE);
|
|
jcal.getCalendarDateFromFixedDate(d, fd);
|
|
return d;
|
|
}
|
|
|
|
/**
|
|
* Returns the length of the specified month in the specified
|
|
* Gregorian year. The year number must be normalized.
|
|
*
|
|
* @see GregorianCalendar#isLeapYear(int)
|
|
*/
|
|
private int monthLength(int month, int gregorianYear) {
|
|
return CalendarUtils.isGregorianLeapYear(gregorianYear) ?
|
|
GregorianCalendar.LEAP_MONTH_LENGTH[month] : GregorianCalendar.MONTH_LENGTH[month];
|
|
}
|
|
|
|
/**
|
|
* Returns the length of the specified month in the year provided
|
|
* by internalGet(YEAR).
|
|
*
|
|
* @see GregorianCalendar#isLeapYear(int)
|
|
*/
|
|
private int monthLength(int month) {
|
|
assert jdate.isNormalized();
|
|
return jdate.isLeapYear() ?
|
|
GregorianCalendar.LEAP_MONTH_LENGTH[month] : GregorianCalendar.MONTH_LENGTH[month];
|
|
}
|
|
|
|
private int actualMonthLength() {
|
|
int length = jcal.getMonthLength(jdate);
|
|
int eraIndex = getTransitionEraIndex(jdate);
|
|
if (eraIndex != -1) {
|
|
long transitionFixedDate = sinceFixedDates[eraIndex];
|
|
CalendarDate d = eras[eraIndex].getSinceDate();
|
|
if (transitionFixedDate <= cachedFixedDate) {
|
|
length -= d.getDayOfMonth() - 1;
|
|
} else {
|
|
length = d.getDayOfMonth() - 1;
|
|
}
|
|
}
|
|
return length;
|
|
}
|
|
|
|
/**
|
|
* Returns the index to the new era if the given date is in a
|
|
* transition month. For example, if the give date is Heisei 1
|
|
* (1989) January 20, then the era index for Heisei is
|
|
* returned. Likewise, if the given date is Showa 64 (1989)
|
|
* January 3, then the era index for Heisei is returned. If the
|
|
* given date is not in any transition month, then -1 is returned.
|
|
*/
|
|
private static int getTransitionEraIndex(LocalGregorianCalendar.Date date) {
|
|
int eraIndex = getEraIndex(date);
|
|
CalendarDate transitionDate = eras[eraIndex].getSinceDate();
|
|
if (transitionDate.getYear() == date.getNormalizedYear() &&
|
|
transitionDate.getMonth() == date.getMonth()) {
|
|
return eraIndex;
|
|
}
|
|
if (eraIndex < eras.length - 1) {
|
|
transitionDate = eras[++eraIndex].getSinceDate();
|
|
if (transitionDate.getYear() == date.getNormalizedYear() &&
|
|
transitionDate.getMonth() == date.getMonth()) {
|
|
return eraIndex;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private boolean isTransitionYear(int normalizedYear) {
|
|
for (int i = eras.length - 1; i > 0; i--) {
|
|
int transitionYear = eras[i].getSinceDate().getYear();
|
|
if (normalizedYear == transitionYear) {
|
|
return true;
|
|
}
|
|
if (normalizedYear > transitionYear) {
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static int getEraIndex(LocalGregorianCalendar.Date date) {
|
|
Era era = date.getEra();
|
|
for (int i = eras.length - 1; i > 0; i--) {
|
|
if (eras[i] == era) {
|
|
return i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Returns this object if it's normalized (all fields and time are
|
|
* in sync). Otherwise, a cloned object is returned after calling
|
|
* complete() in lenient mode.
|
|
*/
|
|
private JapaneseImperialCalendar getNormalizedCalendar() {
|
|
JapaneseImperialCalendar jc;
|
|
if (isFullyNormalized()) {
|
|
jc = this;
|
|
} else {
|
|
// Create a clone and normalize the calendar fields
|
|
jc = (JapaneseImperialCalendar) this.clone();
|
|
jc.setLenient(true);
|
|
jc.complete();
|
|
}
|
|
return jc;
|
|
}
|
|
|
|
/**
|
|
* After adjustments such as add(MONTH), add(YEAR), we don't want the
|
|
* month to jump around. E.g., we don't want Jan 31 + 1 month to go to Mar
|
|
* 3, we want it to go to Feb 28. Adjustments which might run into this
|
|
* problem call this method to retain the proper month.
|
|
*/
|
|
private void pinDayOfMonth(LocalGregorianCalendar.Date date) {
|
|
int year = date.getYear();
|
|
int dom = date.getDayOfMonth();
|
|
if (year != getMinimum(YEAR)) {
|
|
date.setDayOfMonth(1);
|
|
jcal.normalize(date);
|
|
int monthLength = jcal.getMonthLength(date);
|
|
if (dom > monthLength) {
|
|
date.setDayOfMonth(monthLength);
|
|
} else {
|
|
date.setDayOfMonth(dom);
|
|
}
|
|
jcal.normalize(date);
|
|
} else {
|
|
LocalGregorianCalendar.Date d = jcal.getCalendarDate(Long.MIN_VALUE, getZone());
|
|
LocalGregorianCalendar.Date realDate = jcal.getCalendarDate(time, getZone());
|
|
long tod = realDate.getTimeOfDay();
|
|
// Use an equivalent year.
|
|
realDate.addYear(+400);
|
|
realDate.setMonth(date.getMonth());
|
|
realDate.setDayOfMonth(1);
|
|
jcal.normalize(realDate);
|
|
int monthLength = jcal.getMonthLength(realDate);
|
|
if (dom > monthLength) {
|
|
realDate.setDayOfMonth(monthLength);
|
|
} else {
|
|
if (dom < d.getDayOfMonth()) {
|
|
realDate.setDayOfMonth(d.getDayOfMonth());
|
|
} else {
|
|
realDate.setDayOfMonth(dom);
|
|
}
|
|
}
|
|
if (realDate.getDayOfMonth() == d.getDayOfMonth() && tod < d.getTimeOfDay()) {
|
|
realDate.setDayOfMonth(Math.min(dom + 1, monthLength));
|
|
}
|
|
// restore the year.
|
|
date.setDate(year, realDate.getMonth(), realDate.getDayOfMonth());
|
|
// Don't normalize date here so as not to cause underflow.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the new value after 'roll'ing the specified value and amount.
|
|
*/
|
|
private static int getRolledValue(int value, int amount, int min, int max) {
|
|
assert value >= min && value <= max;
|
|
int range = max - min + 1;
|
|
amount %= range;
|
|
int n = value + amount;
|
|
if (n > max) {
|
|
n -= range;
|
|
} else if (n < min) {
|
|
n += range;
|
|
}
|
|
assert n >= min && n <= max;
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* Returns the ERA. We need a special method for this because the
|
|
* default ERA is the current era, but a zero (unset) ERA means before Meiji.
|
|
*/
|
|
private int internalGetEra() {
|
|
return isSet(ERA) ? internalGet(ERA) : currentEra;
|
|
}
|
|
|
|
/**
|
|
* Updates internal state.
|
|
*/
|
|
@java.io.Serial
|
|
private void readObject(ObjectInputStream stream)
|
|
throws IOException, ClassNotFoundException {
|
|
stream.defaultReadObject();
|
|
if (jdate == null) {
|
|
jdate = jcal.newCalendarDate(getZone());
|
|
cachedFixedDate = Long.MIN_VALUE;
|
|
}
|
|
}
|
|
}
|