001: /*
002: * Copyright 2001-2005 Stephen Colebourne
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.joda.time.chrono;
017:
018: import java.util.HashMap;
019: import java.util.Map;
020:
021: import org.joda.time.Chronology;
022: import org.joda.time.DateTime;
023: import org.joda.time.DateTimeConstants;
024: import org.joda.time.DateTimeField;
025: import org.joda.time.DateTimeZone;
026: import org.joda.time.field.SkipDateTimeField;
027:
028: /**
029: * Implements the Ethiopic calendar system, which defines every fourth year as
030: * leap, much like the Julian calendar. The year is broken down into 12 months,
031: * each 30 days in length. An extra period at the end of the year is either 5
032: * or 6 days in length. In this implementation, it is considered a 13th month.
033: * <p>
034: * Year 1 in the Ethiopic calendar began on August 29, 8 CE (Julian), thus
035: * Ethiopic years do not begin at the same time as Julian years. This chronology
036: * is not proleptic, as it does not allow dates before the first Ethiopic year.
037: * <p>
038: * This implementation defines a day as midnight to midnight exactly as per
039: * the ISO chronology. Some references indicate that a coptic day starts at
040: * sunset on the previous ISO day, but this has not been confirmed and is not
041: * implemented.
042: * <p>
043: * EthiopicChronology is thread-safe and immutable.
044: *
045: * @see <a href="http://en.wikipedia.org/wiki/Ethiopian_calendar">Wikipedia</a>
046: *
047: * @author Brian S O'Neill
048: * @author Stephen Colebourne
049: * @since 1.2
050: */
051: public final class EthiopicChronology extends BasicFixedMonthChronology {
052:
053: /** Serialization lock */
054: private static final long serialVersionUID = -5972804258688333942L;
055:
056: /**
057: * Constant value for 'Ethiopean Era', equivalent
058: * to the value returned for AD/CE.
059: */
060: public static final int EE = DateTimeConstants.CE;
061:
062: /** A singleton era field. */
063: private static final DateTimeField ERA_FIELD = new BasicSingleEraDateTimeField(
064: "EE");
065:
066: /** The lowest year that can be fully supported. */
067: private static final int MIN_YEAR = -292269337;
068:
069: /** The highest year that can be fully supported. */
070: private static final int MAX_YEAR = 292272984;
071:
072: /** Cache of zone to chronology arrays */
073: private static final Map cCache = new HashMap();
074:
075: /** Singleton instance of a UTC EthiopicChronology */
076: private static final EthiopicChronology INSTANCE_UTC;
077: static {
078: // init after static fields
079: INSTANCE_UTC = getInstance(DateTimeZone.UTC);
080: }
081:
082: //-----------------------------------------------------------------------
083: /**
084: * Gets an instance of the EthiopicChronology.
085: * The time zone of the returned instance is UTC.
086: *
087: * @return a singleton UTC instance of the chronology
088: */
089: public static EthiopicChronology getInstanceUTC() {
090: return INSTANCE_UTC;
091: }
092:
093: /**
094: * Gets an instance of the EthiopicChronology in the default time zone.
095: *
096: * @return a chronology in the default time zone
097: */
098: public static EthiopicChronology getInstance() {
099: return getInstance(DateTimeZone.getDefault(), 4);
100: }
101:
102: /**
103: * Gets an instance of the EthiopicChronology in the given time zone.
104: *
105: * @param zone the time zone to get the chronology in, null is default
106: * @return a chronology in the specified time zone
107: */
108: public static EthiopicChronology getInstance(DateTimeZone zone) {
109: return getInstance(zone, 4);
110: }
111:
112: /**
113: * Gets an instance of the EthiopicChronology in the given time zone.
114: *
115: * @param zone the time zone to get the chronology in, null is default
116: * @param minDaysInFirstWeek minimum number of days in first week of the year; default is 4
117: * @return a chronology in the specified time zone
118: */
119: public static EthiopicChronology getInstance(DateTimeZone zone,
120: int minDaysInFirstWeek) {
121: if (zone == null) {
122: zone = DateTimeZone.getDefault();
123: }
124: EthiopicChronology chrono;
125: synchronized (cCache) {
126: EthiopicChronology[] chronos = (EthiopicChronology[]) cCache
127: .get(zone);
128: if (chronos == null) {
129: chronos = new EthiopicChronology[7];
130: cCache.put(zone, chronos);
131: }
132: try {
133: chrono = chronos[minDaysInFirstWeek - 1];
134: } catch (ArrayIndexOutOfBoundsException e) {
135: throw new IllegalArgumentException(
136: "Invalid min days in first week: "
137: + minDaysInFirstWeek);
138: }
139: if (chrono == null) {
140: if (zone == DateTimeZone.UTC) {
141: // First create without a lower limit.
142: chrono = new EthiopicChronology(null, null,
143: minDaysInFirstWeek);
144: // Impose lower limit and make another EthiopicChronology.
145: DateTime lowerLimit = new DateTime(1, 1, 1, 0, 0,
146: 0, 0, chrono);
147: chrono = new EthiopicChronology(LimitChronology
148: .getInstance(chrono, lowerLimit, null),
149: null, minDaysInFirstWeek);
150: } else {
151: chrono = getInstance(DateTimeZone.UTC,
152: minDaysInFirstWeek);
153: chrono = new EthiopicChronology(ZonedChronology
154: .getInstance(chrono, zone), null,
155: minDaysInFirstWeek);
156: }
157: chronos[minDaysInFirstWeek - 1] = chrono;
158: }
159: }
160: return chrono;
161: }
162:
163: // Constructors and instance variables
164: //-----------------------------------------------------------------------
165: /**
166: * Restricted constructor.
167: */
168: EthiopicChronology(Chronology base, Object param,
169: int minDaysInFirstWeek) {
170: super (base, param, minDaysInFirstWeek);
171: }
172:
173: /**
174: * Serialization singleton.
175: */
176: private Object readResolve() {
177: Chronology base = getBase();
178: return base == null ? getInstance(DateTimeZone.UTC,
179: getMinimumDaysInFirstWeek()) : getInstance(base
180: .getZone(), getMinimumDaysInFirstWeek());
181: }
182:
183: // Conversion
184: //-----------------------------------------------------------------------
185: /**
186: * Gets the Chronology in the UTC time zone.
187: *
188: * @return the chronology in UTC
189: */
190: public Chronology withUTC() {
191: return INSTANCE_UTC;
192: }
193:
194: /**
195: * Gets the Chronology in a specific time zone.
196: *
197: * @param zone the zone to get the chronology in, null is default
198: * @return the chronology
199: */
200: public Chronology withZone(DateTimeZone zone) {
201: if (zone == null) {
202: zone = DateTimeZone.getDefault();
203: }
204: if (zone == getZone()) {
205: return this ;
206: }
207: return getInstance(zone);
208: }
209:
210: //-----------------------------------------------------------------------
211: long calculateFirstDayOfYearMillis(int year) {
212: // Java epoch is 1970-01-01 Gregorian which is 1962-04-23 Ethiopic.
213: // Calculate relative to the nearest leap year and account for the
214: // difference later.
215:
216: int relativeYear = year - 1963;
217: int leapYears;
218: if (relativeYear <= 0) {
219: // Add 3 before shifting right since /4 and >>2 behave differently
220: // on negative numbers.
221: leapYears = (relativeYear + 3) >> 2;
222: } else {
223: leapYears = relativeYear >> 2;
224: // For post 1963 an adjustment is needed as jan1st is before leap day
225: if (!isLeapYear(year)) {
226: leapYears++;
227: }
228: }
229:
230: long millis = (relativeYear * 365L + leapYears)
231: * (long) DateTimeConstants.MILLIS_PER_DAY;
232:
233: // Adjust to account for difference between 1963-01-01 and 1962-04-23.
234:
235: return millis + (365L - 112) * DateTimeConstants.MILLIS_PER_DAY;
236: }
237:
238: //-----------------------------------------------------------------------
239: int getMinYear() {
240: return MIN_YEAR;
241: }
242:
243: //-----------------------------------------------------------------------
244: int getMaxYear() {
245: return MAX_YEAR;
246: }
247:
248: //-----------------------------------------------------------------------
249: long getApproxMillisAtEpochDividedByTwo() {
250: return (1962L * MILLIS_PER_YEAR + 112L * DateTimeConstants.MILLIS_PER_DAY) / 2;
251: }
252:
253: //-----------------------------------------------------------------------
254: protected void assemble(Fields fields) {
255: if (getBase() == null) {
256: super .assemble(fields);
257:
258: // Ethiopic, like Julian, has no year zero.
259: fields.year = new SkipDateTimeField(this , fields.year);
260: fields.weekyear = new SkipDateTimeField(this ,
261: fields.weekyear);
262:
263: fields.era = ERA_FIELD;
264: fields.monthOfYear = new BasicMonthOfYearDateTimeField(
265: this , 13);
266: fields.months = fields.monthOfYear.getDurationField();
267: }
268: }
269:
270: }
|