/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (AstronomicalHijriData.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.calendar;
import net.time4j.PlainDate;
import net.time4j.base.ResourceLoader;
import net.time4j.engine.CalendarEra;
import net.time4j.engine.EpochDays;
import net.time4j.format.expert.Iso8601Format;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
/**
* <p>Calendar system for astronomical hijri data of any variant. </p>
*
* <p>Users can easily take care of deviations (usually +/- 3 days) if they manage copies of the underlying data
* and make their individual adjustments to lengths of months. The name of the copied change file is the new
* variant name. Such a file has the extension ".data" and is located in the data-directory relative
* to the class path. </p>
*
* @since 3.5/4.3
*/
final class AstronomicalHijriData
implements EraYearMonthDaySystem<HijriCalendar> {
//~ Statische Felder/Initialisierungen --------------------------------
static final AstronomicalHijriData UMALQURA;
static {
try {
UMALQURA = new AstronomicalHijriData("islamic-umalqura"); // prefetch
} catch (IOException ioe) {
throw new IllegalStateException(ioe);
}
}
//~ Instanzvariablen --------------------------------------------------
private final String variant;
private final String version;
private final int minYear;
private final int maxYear;
private final long minUTC;
private final long maxUTC;
private final int[] lengthOfMonth;
private final long[] firstOfMonth;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Creates a new instance for given variant loading its resource data. </p>
*
* @param variant name of calendar variant
* @throws IOException in case of any data inconsistencies
*/
AstronomicalHijriData(String variant) throws IOException {
super();
this.variant = variant;
String name = "data/" + variant.replace('-', '_') + ".data";
URI uri = ResourceLoader.getInstance().locate("calendar", AstronomicalHijriData.class, name);
InputStream is = ResourceLoader.getInstance().load(uri, true);
if (is == null) {
is = ResourceLoader.getInstance().load(AstronomicalHijriData.class, name, true);
}
try {
Properties properties = new Properties();
properties.load(is);
String calendarType = properties.getProperty("type");
if (!variant.equals(calendarType)) {
throw new IOException("Wrong hijri variant: expected=" + variant + ", found=" + calendarType);
}
this.version = properties.getProperty("version", "1.0");
String isoStart = properties.getProperty("iso-start", "");
PlainDate startDate = Iso8601Format.EXTENDED_CALENDAR_DATE.parse(isoStart);
this.minUTC = startDate.get(EpochDays.UTC);
int min = Integer.parseInt(properties.getProperty("min", "1"));
this.minYear = min;
int max = Integer.parseInt(properties.getProperty("max", "0"));
this.maxYear = max;
int count = (max - min + 1) * 12;
int[] mlen = new int[count];
long[] mutc = new long[count];
int i = 0;
long v = this.minUTC;
for (int year = min; year <= max; year++) {
String row = properties.getProperty(String.valueOf(year));
if (row == null) {
throw new IOException("Wrong file format: " + name + " (missing year=" + year + ")");
}
String[] monthLengths = row.split(" ");
for (int m = 0; m < Math.min(monthLengths.length, 12); m++) {
mlen[i] = Integer.parseInt(monthLengths[m]);
mutc[i] = v;
v += mlen[i];
i++;
}
if (monthLengths.length < 12) {
int[] buf1 = new int[i];
long[] buf2 = new long[i];
System.arraycopy(mlen, 0, buf1, 0, i);
System.arraycopy(mutc, 0, buf2, 0, i);
mlen = buf1;
mutc = buf2;
break;
}
}
this.maxUTC = v - 1;
this.lengthOfMonth = mlen;
this.firstOfMonth = mutc;
} catch (ParseException | NumberFormatException pe) {
throw new IOException("Wrong file format: " + name, pe);
} finally {
try {
is.close();
} catch (IOException ioe) {
ioe.printStackTrace(System.err);
}
}
}
//~ Methoden ----------------------------------------------------------
@Override
public HijriCalendar transform(long utcDays) {
int monthStart = search(utcDays, this.firstOfMonth);
if (monthStart >= 0) {
if (
(monthStart < this.firstOfMonth.length - 1)
|| (this.firstOfMonth[monthStart] + this.lengthOfMonth[monthStart] > utcDays)
) {
int hyear = (monthStart / 12) + this.minYear;
int hmonth = (monthStart % 12) + 1;
int hdom = (int) (utcDays - this.firstOfMonth[monthStart] + 1);
return HijriCalendar.of(this.variant, hyear, hmonth, hdom);
}
}
throw new IllegalArgumentException("Out of range: " + utcDays);
}
@Override
public long transform(HijriCalendar date) {
if (!date.getVariant().equals(this.variant)) {
throw new IllegalArgumentException(
"Given date does not belong to this calendar system: "
+ date
+ " (calendar variants are different).");
}
int index = (date.getYear() - this.minYear) * 12 + date.getMonth().getValue() - 1;
return this.firstOfMonth[index] + date.getDayOfMonth() - 1;
}
@Override
public long getMinimumSinceUTC() {
return this.minUTC;
}
@Override
public long getMaximumSinceUTC() {
return this.maxUTC;
}
@Override
public List<CalendarEra> getEras() {
CalendarEra era = HijriEra.ANNO_HEGIRAE;
return Collections.singletonList(era);
}
@Override
public boolean isValid(
CalendarEra era,
int hyear,
int hmonth,
int hdom
) {
if (
(era != HijriEra.ANNO_HEGIRAE)
|| (hyear < this.minYear)
|| (hyear > this.maxYear)
|| (hmonth < 1)
|| (hmonth > 12)
|| (hdom < 1)
) {
return false;
}
if ((hyear - this.minYear) * 12 + hmonth - 1 >= this.lengthOfMonth.length) {
return false;
}
return (hdom <= this.getLengthOfMonth(era, hyear, hmonth));
}
@Override
public int getLengthOfMonth(
CalendarEra era,
int hyear,
int hmonth
) {
if (era != HijriEra.ANNO_HEGIRAE) {
throw new IllegalArgumentException("Wrong era: " + era);
}
int index = (hyear - this.minYear) * 12 + hmonth - 1;
if (index < 0 || index >= this.lengthOfMonth.length) {
throw new IllegalArgumentException("Out of bounds: year=" + hyear + ", month=" + hmonth);
}
return this.lengthOfMonth[index];
}
@Override
public int getLengthOfYear(
CalendarEra era,
int hyear
) {
if (era != HijriEra.ANNO_HEGIRAE) {
throw new IllegalArgumentException("Wrong era: " + era);
}
if ((hyear < this.minYear) || (hyear > this.maxYear)) {
throw new IllegalArgumentException("Out of bounds: yearOfEra=" + hyear);
}
int max = 0;
for (int m = 1; m <= 12; m++) {
int index = (hyear - this.minYear) * 12 + m - 1;
if (index >= this.lengthOfMonth.length) {
throw new IllegalArgumentException("Year range is not fully covered by underlying data: " + hyear);
}
max += this.lengthOfMonth[index];
}
return max;
}
/**
* <p>Yields the version attribute of the underlying data. </p>
*
* @return String
*/
String getVersion() {
return this.version;
}
// returns index of month-start associated with utcDays
private static int search(
long utcDays,
long[] firstOfMonth
) {
int low = 0;
int high = firstOfMonth.length - 1;
while (low <= high) {
int middle = (low + high) / 2;
if (firstOfMonth[middle] <= utcDays) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return low - 1;
}
}