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    }