001 /*
002 * Copyright 2001-2009 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("EE");
064
065 /** The lowest year that can be fully supported. */
066 private static final int MIN_YEAR = -292269337;
067
068 /** The highest year that can be fully supported. */
069 private static final int MAX_YEAR = 292272984;
070
071 /** Cache of zone to chronology arrays */
072 private static final Map<DateTimeZone, EthiopicChronology[]> cCache = new HashMap<DateTimeZone, EthiopicChronology[]>();
073
074 /** Singleton instance of a UTC EthiopicChronology */
075 private static final EthiopicChronology INSTANCE_UTC;
076 static {
077 // init after static fields
078 INSTANCE_UTC = getInstance(DateTimeZone.UTC);
079 }
080
081 //-----------------------------------------------------------------------
082 /**
083 * Gets an instance of the EthiopicChronology.
084 * The time zone of the returned instance is UTC.
085 *
086 * @return a singleton UTC instance of the chronology
087 */
088 public static EthiopicChronology getInstanceUTC() {
089 return INSTANCE_UTC;
090 }
091
092 /**
093 * Gets an instance of the EthiopicChronology in the default time zone.
094 *
095 * @return a chronology in the default time zone
096 */
097 public static EthiopicChronology getInstance() {
098 return getInstance(DateTimeZone.getDefault(), 4);
099 }
100
101 /**
102 * Gets an instance of the EthiopicChronology in the given time zone.
103 *
104 * @param zone the time zone to get the chronology in, null is default
105 * @return a chronology in the specified time zone
106 */
107 public static EthiopicChronology getInstance(DateTimeZone zone) {
108 return getInstance(zone, 4);
109 }
110
111 /**
112 * Gets an instance of the EthiopicChronology in the given time zone.
113 *
114 * @param zone the time zone to get the chronology in, null is default
115 * @param minDaysInFirstWeek minimum number of days in first week of the year; default is 4
116 * @return a chronology in the specified time zone
117 */
118 public static EthiopicChronology getInstance(DateTimeZone zone, int minDaysInFirstWeek) {
119 if (zone == null) {
120 zone = DateTimeZone.getDefault();
121 }
122 EthiopicChronology chrono;
123 synchronized (cCache) {
124 EthiopicChronology[] chronos = cCache.get(zone);
125 if (chronos == null) {
126 chronos = new EthiopicChronology[7];
127 cCache.put(zone, chronos);
128 }
129 try {
130 chrono = chronos[minDaysInFirstWeek - 1];
131 } catch (ArrayIndexOutOfBoundsException e) {
132 throw new IllegalArgumentException
133 ("Invalid min days in first week: " + minDaysInFirstWeek);
134 }
135 if (chrono == null) {
136 if (zone == DateTimeZone.UTC) {
137 // First create without a lower limit.
138 chrono = new EthiopicChronology(null, null, minDaysInFirstWeek);
139 // Impose lower limit and make another EthiopicChronology.
140 DateTime lowerLimit = new DateTime(1, 1, 1, 0, 0, 0, 0, chrono);
141 chrono = new EthiopicChronology
142 (LimitChronology.getInstance(chrono, lowerLimit, null),
143 null, minDaysInFirstWeek);
144 } else {
145 chrono = getInstance(DateTimeZone.UTC, minDaysInFirstWeek);
146 chrono = new EthiopicChronology
147 (ZonedChronology.getInstance(chrono, zone), null, minDaysInFirstWeek);
148 }
149 chronos[minDaysInFirstWeek - 1] = chrono;
150 }
151 }
152 return chrono;
153 }
154
155 // Constructors and instance variables
156 //-----------------------------------------------------------------------
157 /**
158 * Restricted constructor.
159 */
160 EthiopicChronology(Chronology base, Object param, int minDaysInFirstWeek) {
161 super(base, param, minDaysInFirstWeek);
162 }
163
164 /**
165 * Serialization singleton.
166 */
167 private Object readResolve() {
168 Chronology base = getBase();
169 return base == null ?
170 getInstance(DateTimeZone.UTC, getMinimumDaysInFirstWeek()) :
171 getInstance(base.getZone(), getMinimumDaysInFirstWeek());
172 }
173
174 // Conversion
175 //-----------------------------------------------------------------------
176 /**
177 * Gets the Chronology in the UTC time zone.
178 *
179 * @return the chronology in UTC
180 */
181 public Chronology withUTC() {
182 return INSTANCE_UTC;
183 }
184
185 /**
186 * Gets the Chronology in a specific time zone.
187 *
188 * @param zone the zone to get the chronology in, null is default
189 * @return the chronology
190 */
191 public Chronology withZone(DateTimeZone zone) {
192 if (zone == null) {
193 zone = DateTimeZone.getDefault();
194 }
195 if (zone == getZone()) {
196 return this;
197 }
198 return getInstance(zone);
199 }
200
201 //-----------------------------------------------------------------------
202 long calculateFirstDayOfYearMillis(int year) {
203 // Java epoch is 1970-01-01 Gregorian which is 1962-04-23 Ethiopic.
204 // Calculate relative to the nearest leap year and account for the
205 // difference later.
206
207 int relativeYear = year - 1963;
208 int leapYears;
209 if (relativeYear <= 0) {
210 // Add 3 before shifting right since /4 and >>2 behave differently
211 // on negative numbers.
212 leapYears = (relativeYear + 3) >> 2;
213 } else {
214 leapYears = relativeYear >> 2;
215 // For post 1963 an adjustment is needed as jan1st is before leap day
216 if (!isLeapYear(year)) {
217 leapYears++;
218 }
219 }
220
221 long millis = (relativeYear * 365L + leapYears)
222 * (long)DateTimeConstants.MILLIS_PER_DAY;
223
224 // Adjust to account for difference between 1963-01-01 and 1962-04-23.
225
226 return millis + (365L - 112) * DateTimeConstants.MILLIS_PER_DAY;
227 }
228
229 //-----------------------------------------------------------------------
230 int getMinYear() {
231 return MIN_YEAR;
232 }
233
234 //-----------------------------------------------------------------------
235 int getMaxYear() {
236 return MAX_YEAR;
237 }
238
239 //-----------------------------------------------------------------------
240 long getApproxMillisAtEpochDividedByTwo() {
241 return (1962L * MILLIS_PER_YEAR + 112L * DateTimeConstants.MILLIS_PER_DAY) / 2;
242 }
243
244 //-----------------------------------------------------------------------
245 protected void assemble(Fields fields) {
246 if (getBase() == null) {
247 super.assemble(fields);
248
249 // Ethiopic, like Julian, has no year zero.
250 fields.year = new SkipDateTimeField(this, fields.year);
251 fields.weekyear = new SkipDateTimeField(this, fields.weekyear);
252
253 fields.era = ERA_FIELD;
254 fields.monthOfYear = new BasicMonthOfYearDateTimeField(this, 13);
255 fields.months = fields.monthOfYear.getDurationField();
256 }
257 }
258
259 }