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 */
016package org.joda.time.chrono;
017
018import java.util.HashMap;
019import java.util.Map;
020
021import org.joda.time.Chronology;
022import org.joda.time.DateTime;
023import org.joda.time.DateTimeConstants;
024import org.joda.time.DateTimeField;
025import org.joda.time.DateTimeZone;
026import org.joda.time.field.SkipDateTimeField;
027
028/**
029 * Implements the Coptic 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 Coptic calendar began on August 29, 284 CE (Julian), thus
035 * Coptic 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 Coptic 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 * CopticChronology is thread-safe and immutable.
044 *
045 * @see <a href="http://en.wikipedia.org/wiki/Coptic_calendar">Wikipedia</a>
046 * @see JulianChronology
047 *
048 * @author Brian S O'Neill
049 * @since 1.0
050 */
051public final class CopticChronology extends BasicFixedMonthChronology {
052
053    /** Serialization lock */
054    private static final long serialVersionUID = -5972804258688333942L;
055
056    /**
057     * Constant value for 'Anno Martyrum' or 'Era of the Martyrs', equivalent
058     * to the value returned for AD/CE.
059     */
060    public static final int AM = DateTimeConstants.CE;
061
062    /** A singleton era field. */
063    private static final DateTimeField ERA_FIELD = new BasicSingleEraDateTimeField("AM");
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 = 292272708;
070
071    /** Cache of zone to chronology arrays */
072    private static final Map<DateTimeZone, CopticChronology[]> cCache = new HashMap<DateTimeZone, CopticChronology[]>();
073
074    /** Singleton instance of a UTC CopticChronology */
075    private static final CopticChronology 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 CopticChronology.
084     * The time zone of the returned instance is UTC.
085     * 
086     * @return a singleton UTC instance of the chronology
087     */
088    public static CopticChronology getInstanceUTC() {
089        return INSTANCE_UTC;
090    }
091
092    /**
093     * Gets an instance of the CopticChronology in the default time zone.
094     * 
095     * @return a chronology in the default time zone
096     */
097    public static CopticChronology getInstance() {
098        return getInstance(DateTimeZone.getDefault(), 4);
099    }
100
101    /**
102     * Gets an instance of the CopticChronology 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 CopticChronology getInstance(DateTimeZone zone) {
108        return getInstance(zone, 4);
109    }
110
111    /**
112     * Gets an instance of the CopticChronology 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 CopticChronology getInstance(DateTimeZone zone, int minDaysInFirstWeek) {
119        if (zone == null) {
120            zone = DateTimeZone.getDefault();
121        }
122        CopticChronology chrono;
123        synchronized (cCache) {
124            CopticChronology[] chronos = cCache.get(zone);
125            if (chronos == null) {
126                chronos = new CopticChronology[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 CopticChronology(null, null, minDaysInFirstWeek);
139                    // Impose lower limit and make another CopticChronology.
140                    DateTime lowerLimit = new DateTime(1, 1, 1, 0, 0, 0, 0, chrono);
141                    chrono = new CopticChronology
142                        (LimitChronology.getInstance(chrono, lowerLimit, null),
143                         null, minDaysInFirstWeek);
144                } else {
145                    chrono = getInstance(DateTimeZone.UTC, minDaysInFirstWeek);
146                    chrono = new CopticChronology
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    CopticChronology(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        int minDays = getMinimumDaysInFirstWeek();
170        minDays = (minDays == 0 ? 4 : minDays);  // handle rename of BaseGJChronology
171        return base == null ?
172                getInstance(DateTimeZone.UTC, minDays) :
173                    getInstance(base.getZone(), minDays);
174    }
175
176    // Conversion
177    //-----------------------------------------------------------------------
178    /**
179     * Gets the Chronology in the UTC time zone.
180     * 
181     * @return the chronology in UTC
182     */
183    public Chronology withUTC() {
184        return INSTANCE_UTC;
185    }
186
187    /**
188     * Gets the Chronology in a specific time zone.
189     * 
190     * @param zone  the zone to get the chronology in, null is default
191     * @return the chronology
192     */
193    public Chronology withZone(DateTimeZone zone) {
194        if (zone == null) {
195            zone = DateTimeZone.getDefault();
196        }
197        if (zone == getZone()) {
198            return this;
199        }
200        return getInstance(zone);
201    }
202
203    //-----------------------------------------------------------------------
204    long calculateFirstDayOfYearMillis(int year) {
205        // Java epoch is 1970-01-01 Gregorian which is 1686-04-23 Coptic.
206        // Calculate relative to the nearest leap year and account for the
207        // difference later.
208
209        int relativeYear = year - 1687;
210        int leapYears;
211        if (relativeYear <= 0) {
212            // Add 3 before shifting right since /4 and >>2 behave differently
213            // on negative numbers.
214            leapYears = (relativeYear + 3) >> 2;
215        } else {
216            leapYears = relativeYear >> 2;
217            // For post 1687 an adjustment is needed as jan1st is before leap day
218            if (!isLeapYear(year)) {
219                leapYears++;
220            }
221        }
222        
223        long millis = (relativeYear * 365L + leapYears)
224            * (long)DateTimeConstants.MILLIS_PER_DAY;
225
226        // Adjust to account for difference between 1687-01-01 and 1686-04-23.
227
228        return millis + (365L - 112) * DateTimeConstants.MILLIS_PER_DAY;
229    }
230
231    //-----------------------------------------------------------------------
232    int getMinYear() {
233        return MIN_YEAR;
234    }
235
236    //-----------------------------------------------------------------------
237    int getMaxYear() {
238        return MAX_YEAR;
239    }
240
241    //-----------------------------------------------------------------------
242    long getApproxMillisAtEpochDividedByTwo() {
243        return (1686L * MILLIS_PER_YEAR + 112L * DateTimeConstants.MILLIS_PER_DAY) / 2;
244    }
245
246    //-----------------------------------------------------------------------
247    protected void assemble(Fields fields) {
248        if (getBase() == null) {
249            super.assemble(fields);
250
251            // Coptic, like Julian, has no year zero.
252            fields.year = new SkipDateTimeField(this, fields.year);
253            fields.weekyear = new SkipDateTimeField(this, fields.weekyear);
254            
255            fields.era = ERA_FIELD;
256            fields.monthOfYear = new BasicMonthOfYearDateTimeField(this, 13);
257            fields.months = fields.monthOfYear.getDurationField();
258        }
259    }
260
261}