EraRules.java
// © 2018 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.UResourceBundle;
import com.ibm.icu.util.UResourceBundleIterator;
import java.util.ArrayList;
import java.util.Arrays;
/**
* <code>EraRules</code> represents calendar era rules specified in supplementalData/calendarData.
*
* @author Yoshito Umaoka
*/
public class EraRules {
private static final int MAX_ENCODED_START_YEAR = 32767;
private static final int MIN_ENCODED_START_YEAR = -32768;
public static final int MIN_ENCODED_START = encodeDate(MIN_ENCODED_START_YEAR, 1, 1);
private static final int YEAR_MASK = 0xFFFF0000;
private static final int MONTH_MASK = 0x0000FF00;
private static final int DAY_MASK = 0x000000FF;
private int[] startDates;
private int minEra; // minimum valid era code, for first entry in startDates[]
private int
numEras; // number of valid era codes (not necessarily the same as startDates.length)
private int currentEra;
private EraRules(int[] startDates, int minEra, int numEras) {
this.startDates = startDates;
this.minEra = minEra;
this.numEras = numEras;
initCurrentEra();
}
public static EraRules getInstance(CalType calType, boolean includeTentativeEra) {
return getInstance(calType.getId(), includeTentativeEra);
}
public static EraRules getInstance(String calId, boolean includeTentativeEra) {
UResourceBundle supplementalDataRes =
UResourceBundle.getBundleInstance(
ICUData.ICU_BASE_NAME,
"supplementalData",
ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle calendarDataRes = supplementalDataRes.get("calendarData");
UResourceBundle calendarTypeRes = calendarDataRes.get(calId);
UResourceBundle erasRes = calendarTypeRes.get("eras");
int numEras = erasRes.getSize();
int firstTentativeIdx = Integer.MAX_VALUE; // first tentative era index
ArrayList<Integer> eraStartDates = new ArrayList<>(numEras);
UResourceBundleIterator itr = erasRes.getIterator();
while (itr.hasNext()) {
UResourceBundle eraRuleRes = itr.next();
String eraIdxStr = eraRuleRes.getKey();
int eraIdx = -1;
try {
eraIdx = Integer.parseInt(eraIdxStr);
} catch (NumberFormatException e) {
throw new ICUException(
"Invalid era rule key:" + eraIdxStr + " in era rule data for " + calId);
}
if (eraIdx < 0) {
throw new ICUException(
"Era rule key:"
+ eraIdxStr
+ " in era rule data for "
+ calId
+ " must be >= 0");
}
if (eraIdx + 1 > eraStartDates.size()) {
eraStartDates.ensureCapacity(eraIdx + 1); // needed only to minimize expansions
// Fill in zeros for all added slots
while (eraStartDates.size() < eraIdx + 1) {
eraStartDates.add(0);
}
}
// Now set the startDate that we just read
if (isSet(eraStartDates.get(eraIdx).intValue())) {
throw new ICUException(
"Duplicated era rule for rule key:"
+ eraIdxStr
+ " in era rule data for "
+ calId);
}
boolean hasName = true;
boolean hasEnd = false;
UResourceBundleIterator ruleItr = eraRuleRes.getIterator();
while (ruleItr.hasNext()) {
UResourceBundle res = ruleItr.next();
String key = res.getKey();
if (key.equals("start")) {
int[] fields = res.getIntVector();
if (fields.length != 3
|| !isValidRuleStartDate(fields[0], fields[1], fields[2])) {
throw new ICUException(
"Invalid era rule date data:"
+ Arrays.toString(fields)
+ " in era rule data for "
+ calId);
}
eraStartDates.set(eraIdx, encodeDate(fields[0], fields[1], fields[2]));
} else if (key.equals("named")) {
String val = res.getString();
if (val.equals("false")) {
hasName = false;
}
} else if (key.equals("end")) {
hasEnd = true;
}
}
if (isSet(eraStartDates.get(eraIdx).intValue())) {
if (hasEnd) {
// This implementation assumes either start or end is available, not both.
// For now, just ignore the end rule.
}
} else {
if (hasEnd) {
// The islamic calendars now have an end-only rule for the
// second (and final) entry; basically they are in reverse order.
eraStartDates.set(eraIdx, MIN_ENCODED_START);
} else {
throw new ICUException(
"Missing era start/end rule date for key:"
+ eraIdxStr
+ " in era rule data for "
+ calId);
}
}
if (hasName) {
if (eraIdx >= firstTentativeIdx) {
throw new ICUException(
"Non-tentative era("
+ eraIdx
+ ") must be placed before the first tentative era");
}
} else {
if (eraIdx < firstTentativeIdx) {
firstTentativeIdx = eraIdx;
}
}
}
// Remove from eraStartDates any tentative eras if they should not be included
// (these would be the last entries). Also reduce numEras appropriately.
if (!includeTentativeEra) {
while (firstTentativeIdx < eraStartDates.size()) {
int lastEraIdx = eraStartDates.size() - 1;
if (isSet(
eraStartDates.get(
lastEraIdx))) { // If there are multiple tentativeEras, some may be
// unset
numEras--;
}
eraStartDates.remove(lastEraIdx);
}
// Remove any remaining trailing unSet entries
// (can only have these if tentativeEras have been removed)
while (eraStartDates.size() > 0
&& !isSet(eraStartDates.get(eraStartDates.size() - 1))) {
eraStartDates.remove(eraStartDates.size() - 1);
}
}
// Remove from eraStartDates any initial 0 entries, keeping the original index (eraCode)
// of the first non-zero entry as minEra; then we can add that back to the offset in the
// compressed array to get the correct eraCode.
int minEra = 0;
while (eraStartDates.size() > 0 && !isSet(eraStartDates.get(0))) {
eraStartDates.remove(0);
minEra++;
}
// Convert eraStartDates to int[] startDates and pass to EraRules constructor,
// along with minEra and numEras (which may be different from startDates.length)
int[] startDates = new int[eraStartDates.size()];
for (int eraIdx = 0; eraIdx < eraStartDates.size(); eraIdx++) {
startDates[eraIdx] = eraStartDates.get(eraIdx).intValue();
}
;
return new EraRules(startDates, minEra, numEras);
}
/**
* Gets number of effective eras
*
* @return number of effective eras (not the same as max era code)
*/
public int getNumberOfEras() {
return numEras;
}
/**
* Gets maximum defined era code for the current calendar
*
* @return maximum defined era code
*/
public int getMaxEraCode() {
return minEra + startDates.length - 1;
}
/**
* Gets start date of an era
*
* @param eraCode Era code
* @param fillIn Receives date fields if supplied. If null, or size of array is less than 3,
* then a new int[] will be newly allocated.
* @return An int array including values of year, month, day of month in this order. When an era
* has no start date, the result will be January 1st in year whose value is minimum integer.
*/
public int[] getStartDate(int eraCode, int[] fillIn) {
int startDate = 0;
if (eraCode >= minEra) {
int startIdx = eraCode - minEra;
if (startIdx < startDates.length) {
startDate = startDates[startIdx];
}
}
if (isSet(startDate)) {
return decodeDate(startDate, fillIn);
}
// We did not find the requested eraCode in our data
throw new IllegalArgumentException("eraCode is not found in our data");
}
/**
* Gets start year of an era
*
* @param eraCode Era code
* @return The first year of an era. When a era has no start date, minimum integer value is
* returned.
*/
public int getStartYear(int eraCode) {
int startDate = 0;
if (eraCode >= minEra) {
int startIdx = eraCode - minEra;
if (startIdx < startDates.length) {
startDate = startDates[startIdx];
}
}
if (isSet(startDate)) {
int[] fields = decodeDate(startDate, null);
return fields[0];
}
// We did not find the requested eraCode in our data
throw new IllegalArgumentException("eraCode is not found in our data");
}
/**
* Returns era code for the specified year/month/day.
*
* @param year Year
* @param month Month (1-base)
* @param day Day of month
* @return era code (or code of earliest era when date is before that era)
*/
public int getEraCode(int year, int month, int day) {
if (month < 1 || month > 12 || day < 1 || day > 31) {
throw new IllegalArgumentException(
"Illegal date - year:" + year + "month:" + month + "day:" + day);
}
if (numEras > 1 && startDates[startDates.length - 1] == MIN_ENCODED_START) {
// Multiple eras in reverse order, linear search from beginning.
// Currently only for islamic.
for (int startIdx = 0; startIdx < startDates.length; startIdx++) {
if (!isSet(startDates[startIdx])) {
continue;
}
if (compareEncodedDateWithYMD(startDates[startIdx], year, month, day) <= 0) {
return minEra + startIdx;
}
}
}
// Linear search from the end, which should hit the most likely eras first.
// Also this is the most efficient for any era if we have < 8 or so eras, so only less
// efficient for early eras in Japanese calendar (while we still have them). Formerly
// this used binary search which would only be better for those early Japanese eras,
// but now that is much more difficult since there may be holes in the sorted list.
// Note with this change, this no longer uses or depends on currentEra.
for (int startIdx = startDates.length; startIdx > 0; ) {
if (!isSet(startDates[--startIdx])) {
continue;
}
if (compareEncodedDateWithYMD(startDates[startIdx], year, month, day) <= 0) {
return minEra + startIdx;
}
}
return minEra;
}
/**
* Gets the current era code. This is calculated only once for an instance of EraRules. The
* current era calculation is based on the default time zone at the time of instantiation.
*
* @return era index of current era (or era code of earliest era when current date is before any
* era)
*/
public int getCurrentEraCode() {
return currentEra;
}
private void initCurrentEra() {
long localMillis = System.currentTimeMillis();
TimeZone zone = TimeZone.getDefault();
localMillis += zone.getOffset(localMillis);
int[] fields = Grego.timeToFields(localMillis, null);
// Now that getEraCode no longer depends on currentEra, we can just do this:
currentEra = getEraCode(fields[0], fields[1] + 1 /* changes to 1-base */, fields[2]);
}
//
// private methods
//
private static boolean isSet(int startDate) {
return startDate != 0;
}
private static boolean isValidRuleStartDate(int year, int month, int day) {
return year >= MIN_ENCODED_START_YEAR
&& year <= MAX_ENCODED_START_YEAR
&& month >= 1
&& month <= 12
&& day >= 1
&& day <= 31;
}
/**
* Encode year/month/date to a single integer. year is high 16 bits (-32768 to 32767), month is
* next 8 bits and day of month is last 8 bits.
*
* @param year year
* @param month month (1-base)
* @param day day of month
* @return an encoded date.
*/
private static int encodeDate(int year, int month, int day) {
return year << 16 | month << 8 | day;
}
private static int[] decodeDate(int encodedDate, int[] fillIn) {
int year, month, day;
if (encodedDate == MIN_ENCODED_START) {
year = Integer.MIN_VALUE;
month = 1;
day = 1;
} else {
year = (encodedDate & YEAR_MASK) >> 16;
month = (encodedDate & MONTH_MASK) >> 8;
day = encodedDate & DAY_MASK;
}
if (fillIn != null && fillIn.length >= 3) {
fillIn[0] = year;
fillIn[1] = month;
fillIn[2] = day;
return fillIn;
}
int[] result = {year, month, day};
return result;
}
/**
* Compare an encoded date with another date specified by year/month/day.
*
* @param encoded An encoded date
* @param year Year of another date
* @param month Month of another date
* @param day Day of another date
* @return -1 when encoded date is earlier, 0 when two dates are same, and 1 when encoded date
* is later.
*/
private static int compareEncodedDateWithYMD(int encoded, int year, int month, int day) {
if (year < MIN_ENCODED_START_YEAR) {
if (encoded == MIN_ENCODED_START) {
if (year > Integer.MIN_VALUE || month > 1 || day > 1) {
return -1;
}
return 0;
} else {
return 1;
}
} else if (year > MAX_ENCODED_START_YEAR) {
return -1;
} else {
int tmp = encodeDate(year, month, day);
if (encoded < tmp) {
return -1;
} else if (encoded == tmp) {
return 0;
} else {
return 1;
}
}
}
}