| // © 2018 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html |
| package com.ibm.icu.impl; |
| |
| import java.util.Arrays; |
| |
| import com.ibm.icu.util.ICUException; |
| import com.ibm.icu.util.TimeZone; |
| import com.ibm.icu.util.UResourceBundle; |
| import com.ibm.icu.util.UResourceBundleIterator; |
| |
| /** |
| * <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 numEras; |
| private int currentEra; |
| |
| private EraRules(int[] startDates, int numEras) { |
| this.startDates = startDates; |
| this.numEras = numEras; |
| initCurrentEra(); |
| } |
| |
| public static EraRules getInstance(CalType calType, 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(calType.getId()); |
| UResourceBundle erasRes = calendarTypeRes.get("eras"); |
| |
| int numEras = erasRes.getSize(); |
| int firstTentativeIdx = Integer.MAX_VALUE; // first tentative era index |
| int[] startDates = new int[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("Invald era rule key:" + eraIdxStr + " in era rule data for " + calType.getId()); |
| } |
| if (eraIdx < 0 || eraIdx >= numEras) { |
| throw new ICUException("Era rule key:" + eraIdxStr + " in era rule data for " + calType.getId() |
| + " must be in range [0, " + (numEras - 1) + "]"); |
| } |
| if (isSet(startDates[eraIdx])) { |
| throw new ICUException( |
| "Dupulicated era rule for rule key:" + eraIdxStr + " in era rule data for " + calType.getId()); |
| } |
| |
| 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 " |
| + calType.getId()); |
| } |
| startDates[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(startDates[eraIdx])) { |
| if (hasEnd) { |
| // This implementation assumes either start or end is available, not both. |
| // For now, just ignore the end rule. |
| } |
| } else { |
| if (hasEnd) { |
| if (eraIdx != 0) { |
| // This implementation does not support end only rule for eras other than |
| // the first one. |
| throw new ICUException( |
| "Era data for " + eraIdxStr + " in era rule data for " + calType.getId() |
| + " has only end rule."); |
| } |
| startDates[eraIdx] = MIN_ENCODED_START; |
| } else { |
| throw new ICUException("Missing era start/end rule date for key:" + eraIdxStr + " in era rule data for " |
| + calType.getId()); |
| } |
| } |
| |
| 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; |
| } |
| } |
| } |
| |
| if (firstTentativeIdx < Integer.MAX_VALUE && !includeTentativeEra) { |
| return new EraRules(startDates, firstTentativeIdx); |
| } |
| |
| return new EraRules(startDates, numEras); |
| } |
| |
| /** |
| * Gets number of effective eras |
| * @return number of effective eras |
| */ |
| public int getNumberOfEras() { |
| return numEras; |
| } |
| |
| /** |
| * Gets start date of an era |
| * @param eraIdx Era index |
| * @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 eraIdx, int[] fillIn) { |
| if (eraIdx < 0 || eraIdx >= numEras) { |
| throw new IllegalArgumentException("eraIdx is out of range"); |
| } |
| return decodeDate(startDates[eraIdx], fillIn); |
| } |
| |
| /** |
| * Gets start year of an era |
| * @param eraIdx Era index |
| * @return The first year of an era. When a era has no start date, minimum integer |
| * value is returned. |
| */ |
| public int getStartYear(int eraIdx) { |
| if (eraIdx < 0 || eraIdx >= numEras) { |
| throw new IllegalArgumentException("eraIdx is out of range"); |
| } |
| int[] fields = decodeDate(startDates[eraIdx], null); |
| return fields[0]; |
| } |
| |
| /** |
| * Returns era index for the specified year/month/day. |
| * @param year Year |
| * @param month Month (1-base) |
| * @param day Day of month |
| * @return era index (or 0, when the specified date is before the first era) |
| */ |
| public int getEraIndex(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); |
| } |
| int high = numEras; // last index + 1 |
| int low; |
| |
| // Short circuit for recent years. Most modern computations will |
| // occur in the last few eras. |
| if (compareEncodedDateWithYMD(startDates[getCurrentEraIndex()], year, month, day) <= 0) { |
| low = getCurrentEraIndex(); |
| } else { |
| low = 0; |
| } |
| |
| // Do binary search |
| while (low < high - 1) { |
| int i = (low + high) / 2; |
| if (compareEncodedDateWithYMD(startDates[i], year, month, day) <= 0) { |
| low = i; |
| } else { |
| high = i; |
| } |
| } |
| return low; |
| } |
| |
| /** |
| * Gets the current era index. 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 0, when current date is before the first era) |
| */ |
| public int getCurrentEraIndex() { |
| return currentEra; |
| } |
| |
| private void initCurrentEra() { |
| long localMillis = System.currentTimeMillis(); |
| TimeZone zone = TimeZone.getDefault(); |
| localMillis += zone.getOffset(localMillis); |
| |
| int[] fields = Grego.timeToFields(localMillis, null); |
| int currentEncodedDate = encodeDate(fields[0], fields[1] + 1 /* changes to 1-base */, fields[2]); |
| int eraIdx = numEras - 1; |
| while (eraIdx > 0) { |
| if (currentEncodedDate >= startDates[eraIdx]) { |
| break; |
| } |
| eraIdx--; |
| } |
| // Note: current era could be before the first era. |
| // In this case, this implementation returns the first era index (0). |
| currentEra = eraIdx; |
| } |
| |
| // |
| // 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; |
| } |
| } |
| } |
| } |