View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.common;
20  
21  import java.util.Arrays;
22  import java.util.Date;
23  import java.util.List;
24  import java.util.Locale;
25  
26  import org.apache.commons.lang3.StringUtils;
27  import org.apache.logging.log4j.LogManager;
28  import org.apache.logging.log4j.Logger;
29  
30  import com.ibm.icu.text.SimpleDateFormat;
31  import com.ibm.icu.util.BuddhistCalendar;
32  import com.ibm.icu.util.Calendar;
33  import com.ibm.icu.util.CopticCalendar;
34  import com.ibm.icu.util.EthiopicCalendar;
35  import com.ibm.icu.util.GregorianCalendar;
36  import com.ibm.icu.util.HebrewCalendar;
37  import com.ibm.icu.util.IslamicCalendar;
38  import com.ibm.icu.util.JapaneseCalendar;
39  import com.ibm.icu.util.ULocale;
40  
41  /**
42   * This class implements all methods for handling calendars in MyCoRe objects
43   * and data models. It is licensed by <a href="http://source.icu-project.org/repos/icu/icu/trunk/license.html">ICU License</a>.
44   *
45   * @author Jens Kupferschmidt
46   * @author Thomas Junge
47   * @version $Revision: 1.8 $ $Date: 2008/05/28 13:43:31 $
48   * @see <a href="http://site.icu-project.org/home">http://site.icu-project.org/home</a>
49   */
50  public class MCRCalendar {
51  
52      /**
53       * Logger
54       */
55      private static final Logger LOGGER = LogManager.getLogger(MCRCalendar.class.getName());
56  
57      /**
58       * Tag for Buddhistic calendar
59       */
60      public static final String TAG_BUDDHIST = "buddhist";
61  
62      /**
63       * Tag for Chinese calendar
64       */
65      public static final String TAG_CHINESE = "chinese";
66  
67      /**
68       * Tag for Coptic calendar
69       */
70      public static final String TAG_COPTIC = "coptic";
71  
72      /**
73       * Tag for Ethiopic calendar
74       */
75      public static final String TAG_ETHIOPIC = "ethiopic";
76  
77      /**
78       * Tag for Gregorian calendar
79       */
80      public static final String TAG_GREGORIAN = "gregorian";
81  
82      /**
83       * Tag for Hebrew calendar
84       */
85      public static final String TAG_HEBREW = "hebrew";
86  
87      /**
88       * Tag for Islamic calendar
89       */
90      public static final String TAG_ISLAMIC = "islamic";
91  
92      /**
93       * Tag for Japanese calendar
94       */
95      public static final String TAG_JAPANESE = "japanese";
96  
97      /**
98       * Tag for Julian calendar
99       */
100     public static final String TAG_JULIAN = "julian";
101 
102     /**
103      * Tag for Persic calendar
104      */
105     public static final String TAG_PERSIC = "persic";
106 
107     /**
108      * Tag for Armenian calendar
109      */
110     public static final String TAG_ARMENIAN = "armenian";
111 
112     /**
113      * Tag for Egyptian calendar
114      */
115     public static final String TAG_EGYPTIAN = "egyptian";
116 
117     /**
118      * Minimum Julian Day number is 0 = 01.01.4713 BC
119      */
120     public static final int MIN_JULIAN_DAY_NUMBER = 0;
121 
122     /**
123      * Maximum Julian Day number is 3182057 = 28.01.4000
124      */
125     public static final int MAX_JULIAN_DAY_NUMBER = 3182057;
126 
127     /**
128      * a list of calendar tags they are supported in this class
129      */
130     public static final List<String> CALENDARS_LIST = List.of(
131         TAG_GREGORIAN, TAG_JULIAN, TAG_ISLAMIC, TAG_BUDDHIST, TAG_COPTIC, TAG_ETHIOPIC, TAG_PERSIC, TAG_JAPANESE,
132         TAG_ARMENIAN, TAG_EGYPTIAN, TAG_HEBREW);
133 
134     /**
135      * the Julian day of the first day in the armenian calendar, 1.1.1 arm = 13.7.552 greg
136      */
137     public static final int FIRST_ARMENIAN_DAY;
138 
139     /**
140      * the Julian day of the first day in the egyptian calendar, 1.1.1 eg = 18.2.747 BC greg
141      */
142     public static final int FIRST_EGYPTIAN_DAY;
143 
144     static {
145         final Calendar firstArmenian = GregorianCalendar.getInstance();
146         firstArmenian.set(552, GregorianCalendar.JULY, 13);
147         FIRST_ARMENIAN_DAY = firstArmenian.get(Calendar.JULIAN_DAY);
148 
149         final Calendar firstEgypt = GregorianCalendar.getInstance();
150         firstEgypt.set(747, GregorianCalendar.FEBRUARY, 18);
151         firstEgypt.set(Calendar.ERA, GregorianCalendar.BC);
152         FIRST_EGYPTIAN_DAY = firstEgypt.get(Calendar.JULIAN_DAY);
153     }
154 
155     private static final String MSG_CALENDAR_UNSUPPORTED = "Calendar %s is not supported!";
156 
157     /**
158      * @see #getHistoryDateAsCalendar(String, boolean, String)
159      */
160     public static Calendar getHistoryDateAsCalendar(String input, boolean last, CalendarType calendarType) {
161         LOGGER.debug("Input of getHistoryDateAsCalendar: {}  {}  {}", input, calendarType, Boolean.toString(last));
162 
163         final String dateString = StringUtils.trim(input);
164         // check dateString
165         if (StringUtils.isBlank(dateString)) {
166             throw new MCRException("The ancient date string is null or empty");
167         }
168 
169         final Calendar out;
170         if (dateString.equals("4713-01-01 BC")) {
171             LOGGER.debug("Date string contains MIN_JULIAN_DAY_NUMBER");
172             out = new GregorianCalendar();
173             out.set(Calendar.JULIAN_DAY, MCRCalendar.MIN_JULIAN_DAY_NUMBER);
174             return out;
175         }
176         if (dateString.equals("4000-01-28 AD")) {
177             LOGGER.debug("Date string contains MAX_JULIAN_DAY_NUMBER");
178             out = new GregorianCalendar();
179             out.set(Calendar.JULIAN_DAY, MCRCalendar.MAX_JULIAN_DAY_NUMBER);
180             return out;
181         }
182 
183         switch (calendarType) {
184         case Armenian: {
185             out = getCalendarFromArmenianDate(dateString, last);
186             break;
187         }
188         case Buddhist: {
189             out = getCalendarFromBuddhistDate(dateString, last);
190             break;
191         }
192         case Coptic: {
193             out = getCalendarFromCopticDate(dateString, last);
194             break;
195         }
196         case Egyptian: {
197             out = getCalendarFromEgyptianDate(dateString, last);
198             break;
199         }
200         case Ethiopic: {
201             out = getCalendarFromEthiopicDate(dateString, last);
202             break;
203         }
204         case Gregorian: {
205             out = getCalendarFromGregorianDate(dateString, last);
206             break;
207         }
208         case Hebrew: {
209             out = getCalendarFromHebrewDate(dateString, last);
210             break;
211         }
212         case Islamic: {
213             out = getCalendarFromIslamicDate(dateString, last);
214             break;
215         }
216         case Japanese: {
217             out = getCalendarFromJapaneseDate(dateString, last);
218             break;
219         }
220         case Julian: {
221             out = getCalendarFromJulianDate(dateString, last);
222             break;
223         }
224         case Persic: {
225             out = getCalendarFromPersicDate(dateString, last);
226             break;
227         }
228         default: {
229             throw new MCRException("Calendar type " + calendarType + " not supported!");
230         }
231         }
232 
233         LOGGER.debug("Output of getHistoryDateAsCalendar: {}", getCalendarDateToFormattedString(out));
234         return out;
235     }
236 
237     /**
238      * This method check an ancient date string for the given calendar. For
239      * syntax of the date string see javadocs of calendar methods.
240      *
241      * @param dateString     the date as string.
242      * @param last           the value is true if the date should be filled with the
243      *                       highest value of month or day like 12 or 31 else it fill the
244      *                       date with the lowest value 1 for month and day.
245      * @param calendarString the calendar name as String, kind of the calendars are
246      *                       ('gregorian', 'julian', 'islamic', 'buddhist', 'coptic',
247      *                       'ethiopic', 'persic', 'japanese', 'armenian' or 'egyptian' )
248      * @return the ICU Calendar instance of the concrete calendar type or null if an error was occurred.
249      * @throws MCRException if parsing has an error
250      */
251     public static Calendar getHistoryDateAsCalendar(String dateString, boolean last, String calendarString)
252         throws MCRException {
253         return getHistoryDateAsCalendar(dateString, last, CalendarType.of(calendarString));
254     }
255 
256     /**
257      * Check the date string for julian or gregorian calendar
258      *
259      * @param dateString the date string
260      * @param last       the flag for first / last day
261      * @return an integer array with [0] = year; [1] = month; [2] = day; [3] = era : -1 = BC : +1 = AC
262      */
263     private static int[] checkDateStringForJulianCalendar(String dateString, boolean last, CalendarType calendarType) {
264         // look for BC
265         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(dateString, Locale.ROOT));
266 
267         final boolean bc = beforeZero(dateTrimmed, calendarType);
268         final String cleanDate = cleanDate(dateTrimmed, calendarType);
269         final int[] fields = parseDateString(cleanDate, last, calendarType);
270         final int year = fields[0];
271         final int mon = fields[1];
272         final int day = fields[2];
273         final int era = bc ? -1 : 1;
274 
275         // test of the monthly
276         if (mon > GregorianCalendar.DECEMBER || mon < GregorianCalendar.JANUARY) {
277             throw new MCRException("The month of the date is inadmissible.");
278         }
279 
280         // Test of the daily
281         if (day > 31) {
282             throw new MCRException("The day of the date is inadmissible.");
283         } else if ((day > 30) && (mon == GregorianCalendar.APRIL || mon == GregorianCalendar.JUNE ||
284             mon == GregorianCalendar.SEPTEMBER || mon == GregorianCalendar.NOVEMBER)) {
285             throw new MCRException("The day of the date is inadmissible.");
286         } else if ((day > 29) && (mon == GregorianCalendar.FEBRUARY)) {
287             throw new MCRException("The day of the date is inadmissible.");
288         } else if ((day > 28) && (mon == GregorianCalendar.FEBRUARY) && !isLeapYear(year, calendarType)) {
289             throw new MCRException("The day of the date is inadmissible.");
290         }
291 
292         return new int[] { year, mon, day, era };
293     }
294 
295     /**
296      * This method convert a ancient date to a general Calendar value. The
297      * syntax for the gregorian input is: <br>
298      * <ul>
299      * <li> [[[t]t.][m]m.][yyy]y [v. Chr.]</li>
300      * <li> [[[t]t.][m]m.][yyy]y [AD|BC]</li>
301      * <li> [-|AD|BC] [[[t]t.][m]m.][yyy]y</li>
302      * <li> [[[t]t/][m]m/][yyy]y [AD|BC]</li>
303      * <li> [-|AD|BC] [[[t]t/][m]m/][yyy]y</li>
304      * <li> y[yyy][-m[m][-t[t]]] [v. Chr.]</li>
305      * <li> y[yyy][-m[m][-t[t]]] [AD|BC]</li>
306      * <li> [-|AD|BC] y[yyy][-m[m][-t[t]]]</li>
307      * </ul>
308      *
309      * @param dateString
310      *            the date as string.
311      * @param last
312      *            the value is true if the date should be filled with the
313      *            highest value of month or day like 12 or 31 else it fill the
314      *            date with the lowest value 1 for month and day.
315      *
316      * @return the GregorianCalendar date value or null if an error was
317      *         occurred.
318      * @exception MCRException if parsing has an error
319      */
320     protected static GregorianCalendar getCalendarFromGregorianDate(String dateString, boolean last)
321         throws MCRException {
322         try {
323             int[] fields = checkDateStringForJulianCalendar(dateString, last, CalendarType.Gregorian);
324             GregorianCalendar calendar = new GregorianCalendar();
325             calendar.set(fields[0], fields[1], fields[2]);
326             if (fields[3] == -1) {
327                 calendar.set(Calendar.ERA, GregorianCalendar.BC);
328             } else {
329                 calendar.set(Calendar.ERA, GregorianCalendar.AD);
330             }
331             return calendar;
332         } catch (Exception e) {
333             throw new MCRException("The ancient gregorian date is false.", e);
334         }
335     }
336 
337     /**
338      * This method convert a JulianCalendar date to a general Calendar value.
339      * The syntax for the julian input is: <br>
340      * <ul>
341      * <li> [[[t]t.][m]m.][yyy]y [v. Chr.|n. Chr.]</li>
342      * <li> [[[t]t.][m]m.][yyy]y [AD|BC]</li>
343      * <li> [-|AD|BC] [[[t]t.][m]m.][yyy]y</li>
344      * <li> [[[t]t/][m]m/][yyy]y [AD|BC]</li>
345      * <li> [-|AD|BC] [[[t]t/][m]m/][yyy]y</li>
346      * <li> y[yyy][-m[m][-t[t]]] [v. Chr.|n. Chr.]</li>
347      * <li> y[yyy][-m[m][-t[t]]] [AD|BC]</li>
348      * <li> [-|AD|BC] y[yyy][-m[m][-t[t]]]</li>
349      * </ul>
350      *
351      * @param dateString
352      *            the date as string.
353      * @param last
354      *            the value is true if the date should be filled with the
355      *            highest value of month or day like 12 or 31 else it fill the
356      *            date with the lowest value 1 for month and day.
357      *
358      * @return the GregorianCalendar date value or null if an error was
359      *         occurred.
360      * @exception MCRException if parsing has an error
361      */
362     protected static Calendar getCalendarFromJulianDate(String dateString, boolean last) throws MCRException {
363         try {
364             int[] fields = checkDateStringForJulianCalendar(dateString, last, CalendarType.Julian);
365             final Calendar calendar = Calendar.getInstance(CalendarType.Julian.getLocale());
366             ((GregorianCalendar) calendar).setGregorianChange(new Date(Long.MAX_VALUE));
367             calendar.set(fields[0], fields[1], fields[2]);
368             if (fields[3] == -1) {
369                 calendar.set(Calendar.ERA, GregorianCalendar.BC);
370             } else {
371                 calendar.set(Calendar.ERA, GregorianCalendar.AD);
372             }
373 
374             return calendar;
375         } catch (Exception e) {
376             throw new MCRException("The ancient julian date is false.", e);
377         }
378     }
379 
380     /**
381      * This method converts an islamic calendar date to a IslamicCalendar value civil mode.
382      * The syntax for the islamic input is: <br>
383      * <ul>
384      * <li> [[[t]t.][m]m.][yyy]y [H.|h.]</li>
385      * <li> [.\u0647 | .\u0647 .\u0642] [[[t]t.][m]m.][yyy]y</li>
386      * <li> y[yyy][-m[m][-t[t]]] H.|h.</li>
387      * </ul>
388      *
389      * @param dateString
390      *            the date as string.
391      * @param last
392      *            the value is true if the date should be filled with the
393      *            highest value of month or day like 12 or 30 else it fill the
394      *            date with the lowest value 1 for month and day.
395      *
396      * @return the IslamicCalendar date value or null if an error was occurred.
397      * @exception MCRException if parsing has an error
398      */
399     protected static IslamicCalendar getCalendarFromIslamicDate(String dateString, boolean last) {
400         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(dateString, Locale.ROOT));
401 
402         final boolean before = beforeZero(dateTrimmed, CalendarType.Islamic);
403         final String cleanDate = cleanDate(dateTrimmed, CalendarType.Islamic);
404         final int[] fields = parseDateString(cleanDate, last, CalendarType.Islamic);
405         int year = fields[0];
406         final int mon = fields[1];
407         final int day = fields[2];
408 
409         if (before) {
410             year = -year + 1;
411         }
412 
413         // test of the monthly
414         if (mon > 11 || mon < 0) {
415             throw new MCRException("The month of the date is inadmissible.");
416         }
417         // Test of the daily
418         if (day > 30 || mon % 2 == 1 && mon < 11 && day > 29 || day < 1) {
419             throw new MCRException("The day of the date is inadmissible.");
420         }
421 
422         IslamicCalendar calendar = new IslamicCalendar();
423         calendar.setCivil(true);
424         calendar.set(year, mon, day);
425 
426         return calendar;
427     }
428 
429     /**
430      * This method convert a HebrewCalendar date to a HebrewCalendar value. The
431      * syntax for the hebrew input is [[t]t.][m]m.][yyy]y] or
432      * [[yyy]y-[[m]m]-[[t]t].
433      *
434      * @param datestr
435      *            the date as string.
436      * @param last
437      *            the value is true if the date should be filled with the
438      *            highest value of month or day like 13 or 30 else it fill the
439      *            date with the lowest value 1 for month and day.
440      *
441      * @return the HebrewCalendar date value or null if an error was occurred.
442      * @exception MCRException if parsing has an error
443      */
444 
445     protected static HebrewCalendar getCalendarFromHebrewDate(String datestr, boolean last) {
446         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(datestr, Locale.ROOT));
447 
448         final boolean before = beforeZero(dateTrimmed, CalendarType.Hebrew);
449         if (before) {
450             throw new MCRException("Dates before 1 not supported in Hebrew calendar!");
451         }
452 
453         final String cleanDate = cleanDate(dateTrimmed, CalendarType.Hebrew);
454         final int[] fields = parseDateString(cleanDate, last, CalendarType.Hebrew);
455         final int year = fields[0];
456         final int mon = fields[1];
457         final int day = fields[2];
458 
459         HebrewCalendar hcal = new HebrewCalendar();
460         hcal.set(year, mon, day);
461 
462         return hcal;
463     }
464 
465     /**
466      * Check the date string for ethiopic or coptic calendar
467      *
468      * @param dateString the date string
469      * @param last       the flag for first / last day
470      * @return an integer array with [0] = year; [1] = month; [2] = day; [3] = era : -1 = B.M.: +1 = A.M.
471      */
472     private static int[] checkDateStringForCopticCalendar(String dateString, boolean last, CalendarType calendarType) {
473         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(dateString, Locale.ROOT));
474 
475         final boolean bm = beforeZero(dateTrimmed, calendarType);
476         final String cleanDate = cleanDate(dateTrimmed, calendarType);
477         final int[] fields = parseDateString(cleanDate, last, calendarType);
478         int year = fields[0];
479         final int mon = fields[1];
480         final int day = fields[2];
481         final int era = bm ? -1 : 1;
482 
483         if (bm) {
484             year = -year + 1;
485         }
486 
487         // test of the monthly
488         if (mon > 12 || mon < 0) {
489             throw new MCRException("The month of the date is inadmissible.");
490         }
491         // Test of the daily
492         if (day > 30 || day < 1 || day > 6 && mon == 12) {
493             throw new MCRException("The day of the date is inadmissible.");
494         }
495 
496         return new int[] { year, mon, day, era };
497     }
498 
499     /**
500      * This method convert a CopticCalendar date to a CopticCalendar value. The
501      * syntax for the coptic input is: <br>
502      * <ul>
503      * <li> [[[t]t.][m]m.][yyy]y [[A.|a.]M.]</li>
504      * <li> y[yyy][-m[m][-t[t]]] [A.|a.]M.]</li>
505      * </ul>
506      *
507      * @param dateString
508      *            the date as string.
509      * @param last
510      *            the value is true if the date should be filled with the
511      *            highest value of month or day like 12 or 30 else it fill the
512      *            date with the lowest value 1 for month and day.
513      *
514      * @return the CopticCalendar date value or null if an error was occurred.
515      * @exception MCRException if parsing has an error
516      */
517     protected static CopticCalendar getCalendarFromCopticDate(String dateString, boolean last) {
518         try {
519             final int[] fields = checkDateStringForCopticCalendar(dateString, last, CalendarType.Coptic);
520             CopticCalendar calendar = new CopticCalendar();
521             calendar.set(fields[0], fields[1], fields[2]);
522             return calendar;
523         } catch (Exception e) {
524             throw new MCRException("The ancient coptic calendar date is false.", e);
525         }
526     }
527 
528     /**
529      * This method convert a EthiopicCalendar date to a EthiopicCalendar value.
530      * The syntax for the ethiopic input is: <br>
531      * <ul>
532      * <li> [[[t]t.][m]m.][yyy]y [E.E.]</li>
533      * <li> y[yyy][-m[m][-t[t]]] [E.E.]</li>
534      * </ul>
535      *
536      * @param dateString
537      *            the date as string.
538      * @param last
539      *            the value is true if the date should be filled with the
540      *            highest value of month or day like 13 or 30 else it fill the
541      *            date with the lowest value 1 for month and day.
542      *
543      * @return the EthiopicCalendar date value or null if an error was occurred.
544      * @exception MCRException if parsing has an error
545      */
546     protected static EthiopicCalendar getCalendarFromEthiopicDate(String dateString, boolean last) {
547         try {
548             final int[] fields = checkDateStringForCopticCalendar(dateString, last, CalendarType.Ethiopic);
549             EthiopicCalendar calendar = new EthiopicCalendar();
550             calendar.set(fields[0], fields[1], fields[2]);
551             return calendar;
552         } catch (Exception e) {
553             throw new MCRException("The ancient ethiopic calendar date is false.", e);
554         }
555     }
556 
557     /**
558      * This method convert a JapaneseCalendar date to a JapaneseCalendar value.
559      * The syntax for the japanese input is: <br>
560      * <ul>
561      * <li> [[[t]t.][m]m.][H|M|S|T|R][yyy]y <br>
562      * H: Heisei; M: Meiji, S: Showa, T: Taiso, R: Reiwa
563      * </li>
564      * <li> [H|M|S|T|R]y[yyy][-m[m][-t[t]]]</li>
565      * </ul>
566      *
567      * @param datestr
568      *            the date as string.
569      * @param last
570      *            the value is true if the date should be filled with the
571      *            highest value of month or day like 12 or 30 else it fill the
572      *            date with the lowest value 1 for month and day.
573      *
574      * @return the JapaneseCalendar date value or null if an error was occurred.
575      * @exception MCRException if parsing has an error
576      */
577     protected static JapaneseCalendar getCalendarFromJapaneseDate(String datestr, boolean last) {
578         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(datestr, Locale.ROOT));
579         final String cleanDate = cleanDate(dateTrimmed, CalendarType.Japanese);
580 
581         // japanese dates contain the era statement directly in the year e.g. 1.1.H2
582         // before parsing we have to remove this
583         final String eraToken;
584         final int era;
585         if (StringUtils.contains(cleanDate, "M")) {
586             eraToken = "M";
587             era = JapaneseCalendar.MEIJI;
588         } else if (StringUtils.contains(cleanDate, "T")) {
589             eraToken = "T";
590             era = JapaneseCalendar.TAISHO;
591         } else if (StringUtils.contains(cleanDate, "S")) {
592             eraToken = "S";
593             era = JapaneseCalendar.SHOWA;
594         } else if (StringUtils.contains(cleanDate, "H")) {
595             eraToken = "H";
596             era = JapaneseCalendar.HEISEI;
597         } else if (StringUtils.contains(cleanDate, "R")) {
598             eraToken = "R";
599             era = JapaneseCalendar.REIWA;
600         } else {
601             throw new MCRException("Japanese date " + datestr + " does not contain era statement!");
602         }
603 
604         final String firstPart = StringUtils.substringBefore(cleanDate, eraToken);
605         final String secondPart = StringUtils.substringAfter(cleanDate, eraToken);
606 
607         final int[] fields = parseDateString(firstPart + secondPart, last, CalendarType.Japanese);
608         final int year = fields[0];
609         final int mon = fields[1];
610         final int day = fields[2];
611 
612         JapaneseCalendar jcal = new JapaneseCalendar();
613         jcal.set(year, mon, day);
614         jcal.set(Calendar.ERA, era);
615 
616         return jcal;
617     }
618 
619     /**
620      * This method convert a BuddhistCalendar date to a IslamicCalendar value.
621      * The syntax for the buddhist input is: <br>
622      * <ul>
623      * <li> [-][[[t]t.][m]m.][yyy]y [B.E.]</li>
624      * <li> [-] [[[t]t.][m]m.][yyy]y</li>
625      * <li> [-] y[yyy][-m[m][-t[t]]] [B.E.]</li>
626      * <li> [-] y[yyy][-m[m][-t[t]]]</li>
627      * </ul>
628      *
629      * @param datestr
630      *            the date as string.
631      * @param last
632      *            the value is true if the date should be filled with the
633      *            highest value of month or day like 12 or 31 else it fill the
634      *            date with the lowest value 1 for month and day.
635      *
636      * @return the BuddhistCalendar date value or null if an error was occurred.
637      * @exception MCRException if parsing has an error
638      */
639     protected static BuddhistCalendar getCalendarFromBuddhistDate(String datestr, boolean last) {
640         // test before Buddhas
641         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(datestr, Locale.ROOT));
642 
643         final boolean bb = beforeZero(dateTrimmed, CalendarType.Buddhist);
644         final String cleanDate = cleanDate(dateTrimmed, CalendarType.Buddhist);
645         final int[] fields = parseDateString(cleanDate, last, CalendarType.Buddhist);
646         int year = fields[0];
647         int mon = fields[1];
648         int day = fields[2];
649 
650         if (bb) {
651             year = -year + 1; // if before Buddha
652         }
653 
654         if (year == 2125 && mon == 9 && day >= 5 && day < 15) {
655             day = 15;
656         }
657 
658         BuddhistCalendar budcal = new BuddhistCalendar();
659         budcal.set(year, mon, day);
660 
661         return budcal;
662     }
663 
664     /**
665      * This method convert a PersicCalendar date to a GregorianCalendar value.
666      * The syntax for the persian input is: <br>
667      * <ul>
668      * <li> [-] [[[t]t.][m]m.][yyy]y</li>
669      * <li> [-] y[yyy][-m[m][-t[t]]]</li>
670      * </ul>
671      *
672      * @param datestr
673      *            the date as string.
674      * @param last
675      *            the value is true if the date should be filled with the
676      *            highest value of month or day like 13 or 30 else it fill the
677      *            date with the lowest value 1 for month and day.
678      *
679      * @return the GregorianCalendar date value or null if an error was
680      *         occurred.
681      * @exception MCRException if parsing has an error
682      */
683     protected static GregorianCalendar getCalendarFromPersicDate(String datestr, boolean last) {
684         try {
685             final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(datestr, Locale.ROOT));
686 
687             final boolean bb = beforeZero(dateTrimmed, CalendarType.Persic);
688             final String cleanDate = cleanDate(dateTrimmed, CalendarType.Persic);
689             final int[] fields = parseDateString(cleanDate, last, CalendarType.Persic);
690             final int year = fields[0];
691             final int mon = fields[1];
692             final int day = fields[2];
693 
694             final int njahr;
695             if (bb) {
696                 njahr = -year + 1 + 621;
697             } else {
698                 njahr = year + 621;
699             }
700 
701             GregorianCalendar newdate = new GregorianCalendar();
702             newdate.clear();
703             newdate.set(njahr, Calendar.MARCH, 20); // yearly beginning to 20.3.
704 
705             // beginning of the month (day to year)
706             int begday = 0;
707             if (mon == 1) {
708                 begday = 31;
709             }
710             if (mon == 2) {
711                 begday = 62;
712             }
713             if (mon == 3) {
714                 begday = 93;
715             }
716             if (mon == 4) {
717                 begday = 124;
718             }
719             if (mon == 5) {
720                 begday = 155;
721             }
722             if (mon == 6) {
723                 begday = 186;
724             }
725             if (mon == 7) {
726                 begday = 216;
727             }
728             if (mon == 8) {
729                 begday = 246;
730             }
731             if (mon == 9) {
732                 begday = 276;
733             }
734             if (mon == 10) {
735                 begday = 306;
736             }
737             if (mon == 11) {
738                 begday = 336;
739             }
740             begday += day - 1;
741 
742             int jh = njahr / 100; // century
743             int b = jh % 4;
744             int c = njahr % 100; // year of the century
745             int d = c / 4; // count leap year of the century
746 
747             final int min = b * 360 + 350 * c - d * 1440 + 720;
748             if (njahr >= 0) {
749                 newdate.add(Calendar.MINUTE, min); // minute of day
750                 newdate.add(Calendar.DATE, begday); // day of the year
751             } else {
752                 newdate.add(Calendar.DATE, begday + 2); // day of the year
753                 newdate.add(Calendar.MINUTE, min); // minute of day
754             }
755 
756             return newdate;
757         } catch (Exception e) {
758             throw new MCRException("The ancient persian date is false.", e);
759         }
760     }
761 
762     /**
763      * This method convert a ArmenianCalendar date to a GregorianCalendar value.
764      * The syntax for the Armenian input is [-][[t]t.][m]m.][yyy]y] or
765      * [-][[yyy]y-[[m]m]-[[t]t].
766      *
767      * <ul>
768      * <li> [-] [[[t]t.][m]m.][yyy]y</li>
769      * <li> [-] y[yyy][-m[m][-t[t]]]</li>
770      * </ul>
771      *
772      * @param datestr
773      *            the date as string.
774      * @param last
775      *            the value is true if the date should be filled with the
776      *            highest value of month or day like 13 or 30 else it fill the
777      *            date with the lowest value 1 for month and day.
778      *
779      * @return the GregorianCalendar date value or null if an error was
780      *         occurred.
781      * @exception MCRException if parsing has an error
782      */
783     protected static GregorianCalendar getCalendarFromArmenianDate(String datestr, boolean last) {
784         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(datestr, Locale.ROOT));
785 
786         final boolean before = beforeZero(dateTrimmed, CalendarType.Armenian);
787         final String cleanDate = cleanDate(dateTrimmed, CalendarType.Armenian);
788         final int[] fields = parseDateString(cleanDate, last, CalendarType.Armenian);
789         int year = fields[0];
790         int mon = fields[1];
791         int day = fields[2];
792 
793         if (before) {
794             year = -year + 1;
795         }
796 
797         // Armenian calendar has every year an invariant of 365 days - these are added to the beginning of the
798         // calendar defined in FIRST_ARMENIAN_DAY (13.7.552)
799         int addedDays = (year - 1) * 365;
800         if (mon == 12) {
801             addedDays += 12 * 30;
802         } else {
803             addedDays += mon * 30;
804         }
805         addedDays += day - 1;
806 
807         final GregorianCalendar result = new GregorianCalendar();
808         result.set(Calendar.JULIAN_DAY, (FIRST_ARMENIAN_DAY + addedDays));
809 
810         return result;
811     }
812 
813     /**
814      * This method convert a EgyptianCalendar date to a GregorianCalendar value.
815      * The syntax for the egyptian (Nabonassar) input is: <br>
816      * <ul>
817      * <li> [-][[[t]t.][m]m.][yyy]y [A.N.]</li>
818      * <li> [-] [[[t]t.][m]m.][yyy]y</li>
819      * <li> [-] y[yyy][-m[m][-t[t]]] [A.N.]</li>
820      * <li> [-] y[yyy][-m[m][-t[t]]]</li>
821      * </ul>
822      * <p>
823      * For calculating the resulting Gregorian date, February, 18 747 BC is used as initial date for Egyptian calendar.
824      *
825      * @param datestr the date as string.
826      * @param last    the value is true if the date should be filled with the
827      *                highest value of month or day like 13 or 30 else it fill the
828      *                date with the lowest value 1 for month and day.
829      * @return the GregorianCalendar date value or null if an error was
830      * occurred.
831      */
832     protected static GregorianCalendar getCalendarFromEgyptianDate(String datestr, boolean last) {
833         final String dateTrimmed = StringUtils.trim(StringUtils.upperCase(datestr, Locale.ROOT));
834 
835         final boolean ba = beforeZero(dateTrimmed, CalendarType.Egyptian);
836         final String cleanDate = cleanDate(dateTrimmed, CalendarType.Egyptian);
837         final int[] fields = parseDateString(cleanDate, last, CalendarType.Egyptian);
838         int year = fields[0];
839         final int mon = fields[1];
840         final int day = fields[2];
841 
842         if (ba) {
843             year = -year + 1;
844         }
845 
846         int addedDays = (year - 1) * 365;
847         if (mon == 12) {
848             addedDays += 12 * 30;
849         } else {
850             addedDays += mon * 30;
851         }
852         addedDays += day - 1;
853 
854         final GregorianCalendar result = new GregorianCalendar();
855         result.set(Calendar.JULIAN_DAY, (FIRST_EGYPTIAN_DAY + addedDays));
856 
857         return result;
858     }
859 
860     /**
861      * This method return the Julian Day number for a given Calendar instance.
862      *
863      * @return the Julian Day number as Integer
864      */
865     public static int getJulianDayNumber(Calendar calendar) {
866         return calendar.get(Calendar.JULIAN_DAY);
867     }
868 
869     /**
870      * This method return the Julian Day number for a given Calendar instance.
871      *
872      * @return the Julian Day number as String
873      */
874     public static String getJulianDayNumberAsString(Calendar calendar) {
875         return Integer.toString(calendar.get(Calendar.JULIAN_DAY));
876     }
877 
878     /**
879      * This method get the Gregorian calendar form a given calendar
880      *
881      * @param calendar
882      *            an instance of a Calendar
883      * @return a Gregorian calendar
884      */
885     public static GregorianCalendar getGregorianCalendarOfACalendar(Calendar calendar) {
886         int julianDay = getJulianDayNumber(calendar);
887         GregorianCalendar ret = new GregorianCalendar();
888         ret.set(Calendar.JULIAN_DAY, julianDay);
889         return ret;
890     }
891 
892     /**
893      * This method returns the date as string in format 'yy-MM-dd G'.
894      *
895      * @return the date string
896      */
897     public static String getCalendarDateToFormattedString(Calendar calendar) {
898         if (calendar instanceof IslamicCalendar) {
899             return getCalendarDateToFormattedString(calendar, "dd.MM.yyyy");
900         } else if (calendar instanceof GregorianCalendar) {
901             return getCalendarDateToFormattedString(calendar, "yyyy-MM-dd G");
902         }
903         return getCalendarDateToFormattedString(calendar, "yyyy-MM-dd G");
904     }
905 
906     /**
907      * This method returns the date as string.
908      *
909      * @param calendar
910      *            the Calendar date
911      * @param format
912      *            the format of the date as String
913      *
914      * @return the date string in the format. If the format is wrong dd.MM.yyyy
915      *         G is set. If the date is wrong an empty string will be returned.
916      *         The output is depending on calendar type. For Calendar it will use
917      *         the Julian Calendar to 05.10.1582. Then it use the Gregorian Calendar.
918      */
919     public static String getCalendarDateToFormattedString(Calendar calendar, String format) {
920         if (calendar == null || format == null || format.trim().length() == 0) {
921             return "";
922         }
923         SimpleDateFormat formatter = null;
924         try {
925             if (calendar instanceof IslamicCalendar) {
926                 formatter = new SimpleDateFormat(format, new Locale("en"));
927             } else if (calendar instanceof GregorianCalendar) {
928                 formatter = new SimpleDateFormat(format, new Locale("en"));
929             } else {
930                 formatter = new SimpleDateFormat(format, new Locale("en"));
931             }
932         } catch (Exception e) {
933             formatter = new SimpleDateFormat("dd.MM.yyyy G", new Locale("en"));
934         }
935         try {
936             formatter.setCalendar(calendar);
937             if (calendar instanceof IslamicCalendar) {
938                 return formatter.format(calendar.getTime()) + " h.";
939             } else if (calendar instanceof CopticCalendar) {
940                 return formatter.format(calendar.getTime()) + " A.M.";
941             } else if (calendar instanceof EthiopicCalendar) {
942                 return formatter.format(calendar.getTime()) + " E.E.";
943             } else {
944                 return formatter.format(calendar.getTime());
945             }
946         } catch (Exception e) {
947             return "";
948         }
949     }
950 
951     /**
952      * The method get a date String in format yyyy-MM-ddThh:mm:ssZ for ancient date values.
953      *
954      * @param date         the date string
955      * @param useLastValue as boolean
956      *                     - true if incomplete dates should be filled up with last month or last day
957      * @param calendarName the name if the calendar defined in MCRCalendar
958      * @return the date in format yyyy-MM-ddThh:mm:ssZ
959      */
960     public static String getISODateToFormattedString(String date, boolean useLastValue, String calendarName) {
961         String formattedDate = null;
962         try {
963             Calendar calendar = MCRCalendar.getHistoryDateAsCalendar(date, useLastValue, calendarName);
964             GregorianCalendar gregorianCalendar = MCRCalendar.getGregorianCalendarOfACalendar(calendar);
965             formattedDate = MCRCalendar.getCalendarDateToFormattedString(gregorianCalendar, "yyyy-MM-dd")
966                 + "T00:00:00.000Z";
967             if (gregorianCalendar.get(Calendar.ERA) == GregorianCalendar.BC) {
968                 formattedDate = "-" + formattedDate;
969             }
970         } catch (Exception e) {
971             String errorMsg = "Error while converting date string : " + date + " - " + useLastValue +
972                 " - " + calendarName;
973             if (LOGGER.isDebugEnabled()) {
974                 LOGGER.debug(errorMsg, e);
975             }
976             LOGGER.warn(errorMsg);
977             return "";
978         }
979         return formattedDate;
980     }
981 
982     /**
983      * This method returns the calendar type as string.
984      *
985      * @param calendar the Calendar date
986      * @return The calendar type as string. If Calendar is empty an empty string will be returned.
987      */
988     public static String getCalendarTypeString(Calendar calendar) {
989         if (calendar == null) {
990             return "";
991         }
992         if (calendar instanceof IslamicCalendar) {
993             return TAG_ISLAMIC;
994         } else if (calendar instanceof BuddhistCalendar) {
995             return TAG_BUDDHIST;
996         } else if (calendar instanceof CopticCalendar) {
997             return TAG_COPTIC;
998         } else if (calendar instanceof EthiopicCalendar) {
999             return TAG_ETHIOPIC;
1000         } else if (calendar instanceof HebrewCalendar) {
1001             return TAG_HEBREW;
1002         } else if (calendar instanceof JapaneseCalendar) {
1003             return TAG_JAPANESE;
1004         } else if (calendar instanceof GregorianCalendar) {
1005             return TAG_GREGORIAN;
1006         } else {
1007             return TAG_JULIAN;
1008         }
1009     }
1010 
1011     /**
1012      * Parses a clean date string in German (d.m.y), English (d/m/y) or ISO (y-m-d) form and returns the year, month
1013      * and day as an array.
1014      *
1015      * @param dateString   the date to parse
1016      * @param last         flag to determine if the last month or day of a month is to be used when no month
1017      *                     or day is given
1018      * @param calendarType the calendar type to parse the date string for
1019      * @return a field containing year, month and day statements
1020      */
1021     public static int[] parseDateString(String dateString, boolean last, CalendarType calendarType) {
1022         // German, English or ISO?
1023         final boolean iso = isoFormat(dateString);
1024         final String delimiter = delimiter(dateString);
1025 
1026         // check for positions of year and month delimiters
1027         final int firstdot = StringUtils.indexOf(dateString, delimiter, 1);
1028         final int secdot = StringUtils.indexOf(dateString, delimiter, firstdot + 1);
1029 
1030         final int day;
1031         final int mon;
1032         final int year;
1033         if (secdot != -1) {
1034             // we have a date in the form of d.m.yy or y/m/d
1035             final int firstPart = Integer.parseInt(StringUtils.substring(dateString, 0, firstdot));
1036             final int secondPart = Integer.parseInt(StringUtils.substring(dateString, firstdot + 1, secdot));
1037             final int thirdPart = Integer.parseInt(StringUtils.substring(dateString, secdot + 1));
1038             if (iso) {
1039                 year = firstPart;
1040                 mon = secondPart - 1;
1041                 day = thirdPart;
1042             } else {
1043                 day = firstPart;
1044                 mon = secondPart - 1;
1045                 year = thirdPart;
1046             }
1047         } else {
1048             if (firstdot != -1) {
1049                 // we have a date in form of m.y or y/m
1050                 final int firstPart = Integer.parseInt(StringUtils.substring(dateString, 0, firstdot));
1051                 final int secondPart = Integer.parseInt(StringUtils.substring(dateString, firstdot + 1));
1052                 if (iso) {
1053                     year = firstPart;
1054                     mon = secondPart - 1;
1055                 } else {
1056                     mon = firstPart - 1;
1057                     year = secondPart;
1058                 }
1059 
1060                 if (last) {
1061                     day = getLastDayOfMonth(mon, year, calendarType);
1062                 } else {
1063                     day = 1;
1064                 }
1065             } else {
1066                 // we have just a year statement
1067                 year = Integer.parseInt(dateString);
1068                 if (last) {
1069                     mon = getLastMonth(year, calendarType);
1070                     day = getLastDayOfMonth(mon, year, calendarType);
1071                 } else {
1072                     mon = getFirstMonth(calendarType);
1073                     day = 1;
1074                 }
1075             }
1076         }
1077 
1078         return new int[] { year, mon, day };
1079     }
1080 
1081     /**
1082      * Returns true if the given input date is in ISO format (xx-xx-xx), otherwise false.
1083      *
1084      * @param input the input date to check
1085      * @return true if the given input date is in ISO format (xx-xx-xx), otherwise false
1086      */
1087     public static boolean isoFormat(String input) {
1088         return -1 != StringUtils.indexOf(input, "-", 1);
1089     }
1090 
1091     /**
1092      * Cleans a given date by removing era statements like -, AD, B.E. etc.
1093      *
1094      * @param input        the date to clean
1095      * @param calendarType the calendar type of the given date
1096      * @return the cleaned date containing only day, month and year statements
1097      */
1098     public static String cleanDate(String input, CalendarType calendarType) {
1099         final String date = StringUtils.trim(StringUtils.upperCase(input, Locale.ROOT));
1100         final int start;
1101         final int end;
1102         final int length = StringUtils.length(date);
1103 
1104         if (StringUtils.startsWith(date, "-")) {
1105             start = 1;
1106             end = length;
1107         } else {
1108             final int[] borders;
1109             switch (calendarType) {
1110             case Armenian: {
1111                 borders = calculateArmenianDateBorders(date);
1112                 break;
1113             }
1114             case Buddhist: {
1115                 borders = calculateBuddhistDateBorders(date);
1116                 break;
1117             }
1118             case Coptic:
1119             case Ethiopic: {
1120                 borders = calculateCopticDateBorders(date);
1121                 break;
1122             }
1123             case Egyptian: {
1124                 borders = calculateEgyptianDateBorders(date);
1125                 break;
1126             }
1127             case Gregorian:
1128             case Julian: {
1129                 borders = calculateGregorianDateBorders(date);
1130                 break;
1131             }
1132             case Hebrew: {
1133                 borders = calculateHebrewDateBorders(date);
1134                 break;
1135             }
1136             case Islamic: {
1137                 borders = calculateIslamicDateBorders(date);
1138                 break;
1139             }
1140             case Japanese: {
1141                 borders = calculateJapaneseDateBorders(date);
1142                 break;
1143             }
1144             case Persic: {
1145                 borders = calculatePersianDateBorders(date);
1146                 break;
1147             }
1148             default: {
1149                 throw new MCRException(String.format(Locale.ROOT, MSG_CALENDAR_UNSUPPORTED, calendarType));
1150             }
1151             }
1152 
1153             start = borders[0];
1154             end = borders[1];
1155         }
1156 
1157         return StringUtils.trim(StringUtils.substring(date, start, end));
1158     }
1159 
1160     /**
1161      * Calculates the borders of an egyptian date.
1162      *
1163      * @param datestr the egyptian date contain era statements like -, A.N.
1164      * @return the indexes of the date string containing the date without era statements
1165      */
1166     public static int[] calculateEgyptianDateBorders(String datestr) {
1167         final int start;
1168         final int ende;
1169         final int length = StringUtils.length(datestr);
1170 
1171         if (StringUtils.startsWith(datestr, "-")) {
1172             start = 1;
1173         } else {
1174             start = 0;
1175         }
1176 
1177         if (StringUtils.contains(datestr, "A.N.")) {
1178             ende = StringUtils.indexOf(datestr, "A.N.");
1179         } else {
1180             ende = length;
1181         }
1182 
1183         return new int[] { start, ende };
1184     }
1185 
1186     /**
1187      * Calculates the borders of an armenian date.
1188      *
1189      * @param input the armenian date contain era statements like -
1190      * @return the indexes of the date string containing the date without era statements
1191      */
1192     public static int[] calculateArmenianDateBorders(String input) {
1193         final int start;
1194         if (StringUtils.startsWith(input, "-")) {
1195             start = 1;
1196         } else {
1197             start = 0;
1198         }
1199 
1200         return new int[] { start, StringUtils.length(input) };
1201     }
1202 
1203     /**
1204      * Calculates the borders of a japanese date.
1205      *
1206      * @param input the japanese date contain era statements like -
1207      * @return the indexes of the date string containing the date without era statements
1208      */
1209     public static int[] calculateJapaneseDateBorders(String input) {
1210         final int start;
1211         if (StringUtils.startsWith(input, "-")) {
1212             start = 1;
1213         } else {
1214             start = 0;
1215         }
1216 
1217         return new int[] { start, StringUtils.length(input) };
1218     }
1219 
1220     /**
1221      * Calculates the borders of a persian date.
1222      *
1223      * @param dateStr the persina date contain era statements like -
1224      * @return the indexes of the date string containing the date without era statements
1225      */
1226     public static int[] calculatePersianDateBorders(String dateStr) {
1227         final int start;
1228         if (StringUtils.startsWith(dateStr, "-")) {
1229             start = 1;
1230         } else {
1231             start = 0;
1232         }
1233 
1234         return new int[] { start, StringUtils.length(dateStr) };
1235     }
1236 
1237     /**
1238      * Calculates the borders of a coptic/ethiopian date.
1239      *
1240      * @param input the coptic/ethiopian date contain era statements like -, A.M, A.E.
1241      * @return the indexes of the date string containing the date without era statements
1242      */
1243     public static int[] calculateCopticDateBorders(String input) {
1244         final int start;
1245         final int end;
1246         final int length = StringUtils.length(input);
1247 
1248         if (StringUtils.startsWith(input, "-")) {
1249             start = 1;
1250             end = length;
1251         } else {
1252             start = 0;
1253 
1254             if (StringUtils.contains(input, "A.M")) {
1255                 end = StringUtils.indexOf(input, "A.M.");
1256             } else if (StringUtils.contains(input, "E.E.")) {
1257                 end = StringUtils.indexOf(input, "E.E.");
1258             } else {
1259                 end = length;
1260             }
1261         }
1262 
1263         return new int[] { start, end };
1264     }
1265 
1266     /**
1267      * Calculates the borders of a hebrew date.
1268      *
1269      * @param input the hebrew date contain era statements like -
1270      * @return the indexes of the date string containing the date without era statements
1271      */
1272     public static int[] calculateHebrewDateBorders(String input) {
1273         return new int[] { 0, StringUtils.length(input) };
1274     }
1275 
1276     /**
1277      * Calculates the borders of an islamic date.
1278      *
1279      * @param dateString the islamic date contain era statements like -
1280      * @return the indexes of the date string containing the date without era statements
1281      */
1282     public static int[] calculateIslamicDateBorders(String dateString) {
1283         int start = 0;
1284         int ende = dateString.length();
1285         int i = dateString.indexOf("H.");
1286         if (i != -1) {
1287             ende = i;
1288         }
1289         if (dateString.length() > 10) {
1290             i = dateString.indexOf(".\u0647.\u0642");
1291             if (i != -1) {
1292                 start = 3;
1293             } else {
1294                 i = dateString.indexOf(".\u0647");
1295                 if (i != -1) {
1296                     start = 2;
1297                 }
1298             }
1299         }
1300 
1301         return new int[] { start, ende };
1302     }
1303 
1304     /**
1305      * Calculates the date borders for a Gregorian date in the form d.m.y [N. CHR|V.CHR|AD|BC]
1306      *
1307      * @param dateString the date string to parse
1308      * @return a field containing the start position of the date string in index 0 and the end position in index 1
1309      */
1310     public static int[] calculateGregorianDateBorders(String dateString) {
1311         final int start;
1312         final int end;
1313         final int length = StringUtils.length(dateString);
1314 
1315         if (StringUtils.contains(dateString, "N. CHR") || StringUtils.contains(dateString, "V. CHR")) {
1316             final int positionNChr = StringUtils.indexOf(dateString, "N. CHR");
1317             final int positionVChr = StringUtils.indexOf(dateString, "V. CHR");
1318             if (-1 != positionNChr) {
1319                 if (0 == positionNChr) {
1320                     start = 7;
1321                     end = length;
1322                 } else {
1323                     start = 0;
1324                     end = positionNChr - 1;
1325                 }
1326             } else if (-1 != positionVChr) {
1327                 if (0 == positionVChr) {
1328                     start = 7;
1329                     end = length;
1330                 } else {
1331                     start = 0;
1332                     end = positionVChr - 1;
1333                 }
1334             } else {
1335                 start = 0;
1336                 end = length;
1337             }
1338         } else if (StringUtils.contains(dateString, "AD") || StringUtils.contains(dateString, "BC")) {
1339             final int positionAD = StringUtils.indexOf(dateString, "AD");
1340             final int positionBC = StringUtils.indexOf(dateString, "BC");
1341             if (-1 != positionAD) {
1342                 if (0 == positionAD) {
1343                     start = 2;
1344                     end = length;
1345                 } else {
1346                     start = 0;
1347                     end = positionAD - 1;
1348                 }
1349             } else if (-1 != positionBC) {
1350                 if (0 == positionBC) {
1351                     start = 2;
1352                     end = length;
1353                 } else {
1354                     start = 0;
1355                     end = positionBC - 1;
1356                 }
1357             } else {
1358                 start = 0;
1359                 end = length;
1360             }
1361         } else {
1362             start = 0;
1363             end = length;
1364         }
1365 
1366         return new int[] { start, end };
1367     }
1368 
1369     /**
1370      * Calculates the date borders for a Buddhist date in the form d.m.y [B.E.]
1371      *
1372      * @param datestr the date string to parse
1373      * @return a field containing the start position of the date string in index 0 and the end position in index 1
1374      */
1375     public static int[] calculateBuddhistDateBorders(String datestr) {
1376         final int start;
1377         final int end;
1378         final int length = StringUtils.length(datestr);
1379 
1380         if (StringUtils.startsWith(datestr, "-")) {
1381             start = 1;
1382             end = length;
1383         } else {
1384             start = 0;
1385 
1386             if (StringUtils.contains(datestr, "B.E.")) {
1387                 end = StringUtils.indexOf(datestr, "B.E.");
1388             } else {
1389                 end = length;
1390             }
1391         }
1392 
1393         return new int[] { start, end };
1394     }
1395 
1396     /**
1397      * Returns the delimiter for the given date input: ., - or /.
1398      *
1399      * @param input the date input to check
1400      * @return the delimiter for the given date input
1401      */
1402     public static String delimiter(String input) {
1403         if (-1 != StringUtils.indexOf(input, "-", 1)) {
1404             return "-";
1405         } else if (-1 != StringUtils.indexOf(input, "/", 1)) {
1406             return "/";
1407         } else {
1408             return ".";
1409         }
1410     }
1411 
1412     /**
1413      * Returns true if the given date input is before the year zero of the given calendar type.
1414      * <p>
1415      * Examples:
1416      * <ul>
1417      * <li>1 BC is before zero for gregorian/julian calendars</li>
1418      * <li>-1 is before zero for all calendar types</li>
1419      * <li>1 AD is after zero for gregorian/julian calendars</li>
1420      * <li>1 is after zero for all calendar types</li>
1421      * </ul>
1422      *
1423      * @param input        the input date to check
1424      * @param calendarType the calendar type
1425      * @return true if the given input date is for the calendars zero date
1426      */
1427     public static boolean beforeZero(String input, CalendarType calendarType) {
1428         if (StringUtils.startsWith(input, "-")) {
1429             return true;
1430         }
1431 
1432         switch (calendarType) {
1433         case Buddhist: {
1434             return StringUtils.contains(input, "B.E.");
1435         }
1436         case Gregorian:
1437         case Julian: {
1438             return StringUtils.contains(input, "BC") || StringUtils.contains(input, "V. CHR");
1439         }
1440         case Coptic:
1441         case Hebrew:
1442         case Ethiopic:
1443         case Persic:
1444         case Chinese:
1445         case Islamic:
1446         case Armenian:
1447         case Egyptian:
1448         case Japanese: {
1449             // these calendars do not allow for an era statement other than -
1450             return false;
1451         }
1452         default: {
1453             throw new MCRException(String.format(Locale.ROOT, MSG_CALENDAR_UNSUPPORTED, calendarType));
1454         }
1455         }
1456     }
1457 
1458     /**
1459      * Returns the last day number for the given month, e.g. {@link GregorianCalendar#FEBRUARY} has 28 in normal years
1460      * and 29 days in leap years.
1461      *
1462      * @param month        the month number
1463      * @param year         the year
1464      * @param calendarType the calendar type to evaluate the last day for
1465      * @return the last day number for the given month
1466      */
1467     public static int getLastDayOfMonth(int month, int year, CalendarType calendarType) {
1468         final Calendar cal = Calendar.getInstance(calendarType.getLocale());
1469 
1470         if (calendarType == CalendarType.Julian) {
1471             ((GregorianCalendar) cal).setGregorianChange(new Date(Long.MAX_VALUE));
1472         }
1473 
1474         cal.set(Calendar.MONTH, month);
1475         cal.set(Calendar.YEAR, year);
1476 
1477         return cal.getActualMaximum(Calendar.DAY_OF_MONTH);
1478     }
1479 
1480     /**
1481      * Returns the first month of a year for the given calendar type, e.g. January for gregorian calendars.
1482      *
1483      * @param calendarType the calendar type to evaluate the first month for
1484      * @return the first month of a year for the given calendar type
1485      */
1486     public static int getFirstMonth(CalendarType calendarType) {
1487         switch (calendarType) {
1488         case Buddhist:
1489         case Gregorian:
1490         case Julian: {
1491             return GregorianCalendar.JANUARY;
1492         }
1493         case Coptic:
1494         case Egyptian: {
1495             return CopticCalendar.TOUT;
1496         }
1497         case Ethiopic: {
1498             return EthiopicCalendar.MESKEREM;
1499         }
1500         case Hebrew: {
1501             return HebrewCalendar.TISHRI;
1502         }
1503         case Islamic: {
1504             return IslamicCalendar.MUHARRAM;
1505         }
1506         case Armenian:
1507         case Persic: {
1508             return 0;
1509         }
1510         default: {
1511             throw new MCRException(String.format(Locale.ROOT, MSG_CALENDAR_UNSUPPORTED, calendarType));
1512         }
1513         }
1514     }
1515 
1516     /**
1517      * Returns the last month number of the given year for the given calendar type.
1518      *
1519      * @param year         the year to calculate last month number for
1520      * @param calendarType the calendar type
1521      * @return the last month number of the given year for the given calendar type
1522      */
1523     public static int getLastMonth(int year, CalendarType calendarType) {
1524         final Calendar cal = Calendar.getInstance(calendarType.getLocale());
1525         cal.set(Calendar.YEAR, year);
1526 
1527         return cal.getActualMaximum(Calendar.MONTH);
1528     }
1529 
1530     /**
1531      * Returns true, if the given year is a leap year in the given calendar type.
1532      *
1533      * @param year         the year to analyse
1534      * @param calendarType the calendar type
1535      * @return true, if the given year is a leap year in the given calendar type; otherwise false
1536      */
1537     public static boolean isLeapYear(int year, CalendarType calendarType) {
1538         switch (calendarType) {
1539         case Gregorian: {
1540             final GregorianCalendar cal = new GregorianCalendar();
1541             return cal.isLeapYear(year);
1542         }
1543         case Julian: {
1544             final GregorianCalendar cal = new GregorianCalendar();
1545             cal.setGregorianChange(new Date(Long.MAX_VALUE));
1546             return cal.isLeapYear(year);
1547         }
1548         default: {
1549             throw new MCRException(String.format(Locale.ROOT, MSG_CALENDAR_UNSUPPORTED, calendarType));
1550         }
1551         }
1552     }
1553 
1554     public enum CalendarType {
1555         Buddhist(TAG_BUDDHIST, new ULocale("@calendar=buddhist")),
1556         Chinese(TAG_CHINESE, new ULocale("@calendar=chinese")),
1557         Coptic(TAG_COPTIC, new ULocale("@calendar=coptic")),
1558         Ethiopic(TAG_ETHIOPIC, new ULocale("@calendar=ethiopic")),
1559         Gregorian(TAG_GREGORIAN, new ULocale("@calendar=gregorian")),
1560         Hebrew(TAG_HEBREW, new ULocale("@calendar=hebrew")),
1561         Islamic(TAG_ISLAMIC, new ULocale("@calendar=islamic-civil")),
1562         Japanese(TAG_JAPANESE, new ULocale("@calendar=japanese")),
1563         Julian(TAG_JULIAN, new ULocale("@calendar=gregorian")),
1564         Persic(TAG_PERSIC, new ULocale("@calendar=persian")),
1565         // Armenian calendar uses coptic calendar as a base, since both have 12 months + 5 days
1566         Armenian(TAG_ARMENIAN, new ULocale("@calendar=coptic")),
1567         // Egyptian calendar uses coptic calendar as a base, since both have 12 months + 5 days
1568         Egyptian(TAG_EGYPTIAN, new ULocale("@calendar=coptic"));
1569 
1570         private final String type;
1571 
1572         private final ULocale locale;
1573 
1574         CalendarType(String type, ULocale locale) {
1575             this.type = type;
1576             this.locale = locale;
1577         }
1578 
1579         public ULocale getLocale() {
1580             return locale;
1581         }
1582 
1583         public String getType() {
1584             return type;
1585         }
1586 
1587         public static CalendarType of(String type) {
1588             return Arrays.stream(CalendarType.values())
1589                 .filter(current -> StringUtils.equals(current.getType(), type))
1590                 .findFirst().orElseThrow();
1591         }
1592     }
1593 }