com.github.mfathi91.time.PersianDate Maven / Gradle / Ivy
Show all versions of persian-date-time Show documentation
package com.github.mfathi91.time;
import net.jcip.annotations.Immutable;
import java.time.*;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.ChronoPeriod;
import java.time.chrono.Chronology;
import java.time.temporal.*;
import java.util.Objects;
import static java.time.temporal.ChronoField.*;
/**
* This is an implementation of Solar Hijri calendar (also known as Jalali calendar,
* Persian calendar).
*
* {@code PersianDate} is an immutable date-time object that represents a date,
* often viewed as year-month-day.
*
* In order to simplify usage of this class, it is tried to make API of this class
* the same as JDK8 {@link LocalDate} class. Since some methods of {@link LocalDate} were
* useful for Persian calendar system, they have been exactly copied. Some other methods of
* {@link java.time.chrono.HijrahDate} and {@link java.time.chrono.JapaneseDate} have been
* modified and used in this class.
*
* This class is immutable and can be used in multi-threaded programs.
*
* @author Mahmoud Fathi
*/
@Immutable
public final class PersianDate implements ChronoLocalDate {
/**
* The minimum supported persian date {@code 0001-01-01}.
*/
public static final PersianDate MIN =
PersianDate.of((int) PersianChronology.INSTANCE.range(YEAR).getMinimum(), 1, 1);
/**
* The maximum supported persian date {@code 1999-12-29}.
*/
public static final PersianDate MAX =
PersianDate.of((int) PersianChronology.INSTANCE.range(YEAR).getMaximum(), 12, 29);
/**
* 1970-01-01 to julidan day.
*/
private static final long JULIAN_DAY_TO_1970 = 2440587L;
/**
* The year.
*/
private final int year;
/**
* The month-of-year.
*/
private final int month;
/**
* The day-of-month.
*/
private final int day;
/**
* @return the year
*/
public int getYear() {
return year;
}
/**
* @return the month-of-year field using the {@code Month} enum.
* @see #getMonthValue()
*/
public PersianMonth getMonth() {
return PersianMonth.of(month);
}
/**
* @return the month-of-year, from 1 to 12
* @see #getMonth()
*/
public int getMonthValue() {
return month;
}
/**
* @return day-of-month, from 1 to 31
*/
public int getDayOfMonth() {
return day;
}
/**
* @return day-of-year, from 1 to 365 or 366 in a leap year
*/
public int getDayOfYear() {
return PersianMonth.of(month).daysToFirstOfMonth() + day;
}
/**
* Returns day-of-week as an enum {@link DayOfWeek}. This avoids confusion as to what
* {@code int} means. If you need access to the primitive {@code int} value then the
* enum provides the {@link DayOfWeek#getValue() int value}.
*
* @return day-of-week, which is an enum {@link DayOfWeek}
*/
public DayOfWeek getDayOfWeek() {
return DayOfWeek.of((int) (((toJulianDay() + 1) % 7) + 1));
}
/**
* Obtains current Persian date from the system clock in the default time zone.
*
* @return current Persian date from the system clock in the default time zone
*/
public static PersianDate now() {
return ofEpochDay(LocalDate.now().toEpochDay());
}
/**
* Obtains an instance of {@code PersianDate} with year, month and day of month.
* The value of month must be between {@code 1} and {@code 12}. Value {@code 1} would
* be {@link PersianMonth#FARVARDIN} and value {@code 12} represents
* {@link PersianMonth#ESFAND}.
*
* @param year the year to represent, from 1 to MAX_YEAR
* @param month the value of month, from 1 to 12
* @param dayOfMonth the dayOfMonth to represent, from 1 to 31
* @return an instance of {@code PersianDate}
* @throws DateTimeException if the passed parameters do not form a valid date or time.
*/
public static PersianDate of(int year, int month, int dayOfMonth) {
return new PersianDate(year, month, dayOfMonth);
}
/**
* Obtains an instance of {@code PersianDate} with year, month and day of month.
*
* @param year the year to represent, from 1 to MAX_YEAR
* @param month the month-of-year to represent, an instance of {@link PersianMonth}
* @param dayOfMonth the dayOfMonth to represent, from 1 to 31
* @return an instance of {@code PersianDate}
* @throws DateTimeException if the passed parameters do not form a valid date or time.
*/
public static PersianDate of(int year, PersianMonth month, int dayOfMonth) {
Objects.requireNonNull(month, "month");
return new PersianDate(year, month.getValue(), dayOfMonth);
}
/**
* Returns an instance of {@code PersianDate} that is correspondent to the gregorian
* date of parameter {@code localDate}.
*
* @param localDate Gregorian date and time, not null
* @return an equivalent Persian date and time as an instance of {@link PersianDate}
*/
public static PersianDate fromGregorian(LocalDate localDate) {
Objects.requireNonNull(localDate, "localDate");
return ofEpochDay(localDate.toEpochDay());
}
/**
* Returns an instance of {@link PersianDate}, based on number of epoch days,
* which is from 1970-01-01. For example passing {@code 17468} as the parameter
* results a persian date of 1396-08-07.
*
* @param epochDays epoch days
* @return an instance of {@link PersianDate}
*/
public static PersianDate ofEpochDay(long epochDays) {
return ofJulianDays(epochDays + JULIAN_DAY_TO_1970);
}
/**
* Returns an instance of {@link PersianDate}, based on number of julian days.
* For example passing {@code 2458054} as the parameter will cause to get a
* Persian date of "1396-8-6".
*
* @param julianDays julian days
* @return an instance of {@link PersianDate}
* @see calendar convertor
*/
public static PersianDate ofJulianDays(long julianDays) {
MyUtils.longRequirePositive(julianDays, "julianDays");
long depoch = julianDays - 2121445L;
long cycle = depoch / 1029983L;
long cyear = depoch % 1029983L;
long ycycle, aux1, aux2;
if (cyear == 1029982L) {
ycycle = 2820L;
} else {
aux1 = cyear / 366L;
aux2 = cyear % 366L;
ycycle = (((2134L * aux1) + (2816L * aux2) + 2815L) / (1028522L)) + aux1;
ycycle = (ycycle >= 0) ? ycycle + 1L : ycycle;
}
// Check year '474'
ycycle = !MyUtils.isBetween(julianDays, 2121079, 2121444) ? ycycle : 0;
long pYear = ycycle + (2820L * cycle) + 474L;
int yday = (int) (julianDays - PersianDate.of((int) pYear, 1, 1).toJulianDay() + 1);
int pMonth = (int) Math.ceil((yday <= 186) ? yday / 31.0 : (yday - 6) / 30.0);
int pDay = (int) (julianDays - PersianDate.of((int) pYear, pMonth, 1).toJulianDay() + 1);
return PersianDate.of((int) pYear, pMonth, pDay);
}
/**
* Constructor.
*
* @param year the year to represent, from 1 to MAX_YEAR
* @param month the month-of-year to represent, not null, from {@link PersianMonth} enum
* @param dayOfMonth the dayOfMonth-of-month to represent, from 1 to 31
* @throws DateTimeException if the passed parameters do not form a valid date or time.
*/
private PersianDate(int year, int month, int dayOfMonth) {
PersianChronology.INSTANCE.checkValidValue(year, YEAR);
PersianChronology.INSTANCE.checkValidValue(month, MONTH_OF_YEAR);
boolean leapYear = PersianChronology.INSTANCE.isLeapYear(year);
int maxDaysOfMonth = PersianMonth.of(month).length(leapYear);
if (dayOfMonth > maxDaysOfMonth) {
if (month == 12 && dayOfMonth == 30 && !leapYear) {
throw new DateTimeException("Invalid date ESFAND 30, as " + year + " is not a leap year");
}
throw new DateTimeException("Invalid date " + PersianMonth.of(month).name() + " " + dayOfMonth);
}
this.year = year;
this.month = month;
this.day = dayOfMonth;
}
//-----------------------------------------------------------------------
/**
* Gets the chronology of this date, which is the Persian calendar system.
*
* The {@code Chronology} represents the calendar system in use.
* The era and other fields in {@link ChronoField} are defined by the chronology.
*
* @return the Persian chronology, not null
*/
@Override
public Chronology getChronology() {
return PersianChronology.INSTANCE;
}
/**
* Returns the length of the month represented by this date.
*
* This returns the length of the month in days.
*
* @return the length of the month in days
*/
@Override
public int lengthOfMonth() {
PersianMonth pm = PersianMonth.of(month);
return PersianChronology.INSTANCE.isLeapYear(year) ? pm.maxLength() : pm.minLength();
}
/**
* Calculates the amount of time until another date in terms of the specified unit.
*
* This calculates the amount of time between two {@code PersianDate}
* objects in terms of a single {@code TemporalUnit}.
* The start and end points are {@code this} and the specified date.
* The result will be negative if the end is before the start.
* The {@code Temporal} passed to this method is converted to a
* {@code PersianDate} using {@link #from(TemporalAccessor)}.
* For example, the amount in days between two dates can be calculated
* using {@code startDate.until(endDate, DAYS)}.
*
* The calculation returns a whole number, representing the number of
* complete units between the two dates.
* For example, the amount in months between 1396-06-15 and 1396-08-14
* will only be one month as it is one day short of two months.
*
* There are two equivalent ways of using this method.
* The first is to invoke this method.
* The second is to use {@link TemporalUnit#between(Temporal, Temporal)}:
*
* // these two lines are equivalent
* amount = start.until(end, MONTHS);
* amount = MONTHS.between(start, end);
*
* The choice should be made based on which makes the code more readable.
*
* The calculation is implemented in this method for {@link ChronoUnit}.
* The units {@code DAYS}, {@code WEEKS}, {@code MONTHS}, {@code YEARS},
* {@code DECADES}, {@code CENTURIES}, {@code MILLENNIA} and {@code ERAS}
* are supported. Other {@code ChronoUnit} values will throw an exception.
*
* If the unit is not a {@code ChronoUnit}, then the result of this method
* is obtained by invoking {@code TemporalUnit.between(Temporal, Temporal)}
* passing {@code this} as the first argument and the converted input temporal
* as the second argument.
*
* This instance is immutable and unaffected by this method call.
*
* @param endExclusive the end date, exclusive, which is converted to a {@code PersianDate}, not null
* @param unit the unit to measure the amount in, not null
* @return the amount of time between this date and the end date
* @throws DateTimeException if the amount cannot be calculated, or the end
* temporal cannot be converted to a {@code PersianDate}
* @throws UnsupportedTemporalTypeException if the unit is not supported
* @throws ArithmeticException if numeric overflow occurs
*/
@Override
public long until(Temporal endExclusive, TemporalUnit unit) {
Objects.requireNonNull(endExclusive, "endExclusive");
Objects.requireNonNull(unit, "unit");
PersianDate end = (PersianDate) getChronology().date(endExclusive);
if (unit instanceof ChronoUnit) {
switch ((ChronoUnit) unit) {
case DAYS: return daysUntil(end);
case WEEKS: return daysUntil(end) / 7;
case MONTHS: return monthsUntil(end);
case YEARS: return monthsUntil(end) / 12;
case DECADES: return monthsUntil(end) / 120;
case CENTURIES: return monthsUntil(end) / 1200;
case MILLENNIA: return monthsUntil(end) / 12000;
case ERAS: return end.getLong(ERA) - getLong(ERA);
}
throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
}
return unit.between(this, end);
}
private long daysUntil(PersianDate end) {
return end.toEpochDay() - toEpochDay(); // no overflow
}
private long monthsUntil(PersianDate end) {
long packed1 = getLong(PROLEPTIC_MONTH) * 32L + getDayOfMonth(); // no overflow
long packed2 = end.getLong(PROLEPTIC_MONTH) * 32L + end.getDayOfMonth(); // no overflow
return (packed2 - packed1) / 32;
}
/**
* Calculates the period between this date and another date as a {@code Period}.
*
* This calculates the period between two dates in terms of years, months and days.
* The start and end points are {@code this} and the specified date.
* The result will be negative if the end is before the start.
* The negative sign will be the same in each of year, month and day.
*
* The calculation is performed using the ISO calendar system.
* If necessary, the input date will be converted to ISO.
*
* The start date is included, but the end date is not.
* The period is calculated by removing complete months, then calculating
* the remaining number of days, adjusting to ensure that both have the same sign.
* The number of months is then normalized into years and months based on a 12 month year.
* A month is considered to be complete if the end day-of-month is greater
* than or equal to the start day-of-month.
* For example, from {@code 2010-01-15} to {@code 2011-03-18} is "1 year, 2 months and 3 days".
*
* There are two equivalent ways of using this method.
* The first is to invoke this method.
* The second is to use {@link Period#between(LocalDate, LocalDate)}:
*
* // these two lines are equivalent
* period = start.until(end);
* period = Period.between(start, end);
*
* The choice should be made based on which makes the code more readable.
*
* @param endDateExclusive the end date, exclusive, which may be in any chronology, not null
* @return the period between this date and the end date, not null
*/
@Override
public ChronoPeriod until(ChronoLocalDate endDateExclusive) {
Objects.requireNonNull(endDateExclusive, "endDateExclusive");
PersianDate end = PersianChronology.INSTANCE.date(endDateExclusive);
long totalMonths = end.getLong(PROLEPTIC_MONTH) - this.getLong(PROLEPTIC_MONTH); // safe
int days = end.day - this.day;
if (totalMonths > 0 && days < 0) {
totalMonths--;
PersianDate calcDate = this.plusMonths(totalMonths);
days = (int) (end.toEpochDay() - calcDate.toEpochDay()); // safe
} else if (totalMonths < 0 && days > 0) {
totalMonths++;
days -= end.lengthOfMonth();
}
long years = totalMonths / 12; // safe
int months = (int) (totalMonths % 12); // safe
return Period.of(Math.toIntExact(years), months, days);
}
/**
* Gets the value of the specified field from this date as a {@code long}.
*
* This queries this date for the value for the specified field.
* If it is not possible to return the value, because the field is not supported
* or for some other reason, an exception is thrown.
*
* If the field is a {@link ChronoField} then the query is implemented here.
* The {@link #isSupported(TemporalField) supported fields} will return valid
* values based on this date.
* All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}.
*
* If the field is not a {@code ChronoField}, then the result of this method
* is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
* passing {@code this} as the argument. Whether the value can be obtained,
* and what the value represents, is determined by the field.
*
* @param field the field to get, not null
* @return the value for the field
* @throws DateTimeException if a value for the field cannot be obtained
* @throws UnsupportedTemporalTypeException if the field is not supported
* @throws ArithmeticException if numeric overflow occurs
*/
@Override
public long getLong(TemporalField field) {
if (field instanceof ChronoField) {
switch ((ChronoField) field) {
case DAY_OF_WEEK: return getDayOfWeek().getValue();
case ALIGNED_DAY_OF_WEEK_IN_MONTH: return ((day - 1) % 7) + 1;
case ALIGNED_DAY_OF_WEEK_IN_YEAR: return ((getDayOfYear() - 1) % 7) + 1;
case DAY_OF_MONTH: return this.day;
case DAY_OF_YEAR: return this.getDayOfYear();
case EPOCH_DAY: return this.toEpochDay();
case ALIGNED_WEEK_OF_MONTH: return ((day - 1) / 7) + 1;
case ALIGNED_WEEK_OF_YEAR: return ((getDayOfYear() - 1) / 7) + 1;
case MONTH_OF_YEAR: return month;
case PROLEPTIC_MONTH: return (year * 12L + month - 1);
case YEAR_OF_ERA: return (year >= 1 ? year : 1 - year);
case YEAR: return year;
case ERA: return (year >= 1 ? 1 : 0);
}
}
throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
}
/**
* Returns a copy of this {@code PersianDate} with the specified period in years added.
*
* This method adds the specified amount to the years field in three steps:
*
* - Add the input years to the year field
* - Check if the resulting date would be invalid
* - Adjust the day-of-month to the last valid day if necessary
*
*
* For example, 1387-12-30 (leap year) plus one year would result in the
* invalid date 1388-12-30 (standard year). Instead of returning an invalid
* result, the last valid day of the month, 1388-12-29, is selected instead.
*
* This instance is immutable and unaffected by this method call.
*
* @param yearsToAdd the years to add, may be negative
* @return a {@code PersianDate} based on this date with the years added, not null
* @throws DateTimeException if the result exceeds the supported date range
*/
public PersianDate plusYears(long yearsToAdd) {
return plusMonths(yearsToAdd * 12);
}
/**
* Returns a copy of this {@code PersianDate} with the specified period in months added.
*
* This method adds the specified amount to the months field in three steps:
*
* - Add the input months to the month-of-year field
* - Check if the resulting date would be invalid
* - Adjust the day-of-month to the last valid day if necessary
*
*
* For example, 1388-11-30 plus one month would result in the invalid date
* 1388-12-30. Instead of returning an invalid result, the last valid day
* of the month, 1388-12-29, is selected instead.
*
* This instance is immutable and unaffected by this method call.
*
* @param monthsToAdd the months to add, may be negative
* @return a {@code PersianDate} based on this date with the months added, not null
* @throws DateTimeException if the result exceeds the supported date range
*/
public PersianDate plusMonths(long monthsToAdd) {
if (monthsToAdd == 0) {
return this;
}
long monthCount = year * 12L + (month - 1);
long calcMonths = monthCount + monthsToAdd;
int newYear = (int) Math.floorDiv(calcMonths, 12L);
int newMonth = (int) Math.floorMod(calcMonths, 12L) + 1;
return resolvePreviousValid(newYear, newMonth, day);
}
/**
* Returns a copy of this {@code PersianDate} with the specified number of days added.
*
* This method adds the specified amount to the days field incrementing the
* month and year fields as necessary to ensure the result remains valid.
* The result is only invalid if the maximum/minimum year is exceeded.
*
* For example, 1396-12-29 plus one day would result in 1397-01-01.
*
* This instance is immutable and unaffected by this method call.
*
* @param daysToAdd the days to add, may be negative
* @return a {@code PersianDate} based on this date with the days added, not null
* @throws DateTimeException if the result exceeds the supported date range
*/
public PersianDate plusDays(long daysToAdd) {
if (daysToAdd == 0) {
return this;
}
return ofJulianDays(toJulianDay() + daysToAdd);
}
/**
* Returns true if {@code year} is a leap year in Persian calendar.
*
* @return true if {@code year} is a leap year in Persian calendar
*/
@Override
public boolean isLeapYear() {
return PersianChronology.INSTANCE.isLeapYear(year);
}
/**
* Resolves the date, resolving days past the end of month.
*
* @param year the year to represent
* @param month the month-of-year to represent, validated from 1 to 12
* @param day the day-of-month to represent, validated from 1 to 31
* @return the resolved date, not null
*/
private PersianDate resolvePreviousValid(int year, int month, int day) {
boolean leapYear = PersianChronology.INSTANCE.isLeapYear(year);
int maxDaysOfMonth = PersianMonth.of(month).length(leapYear);
if (day > maxDaysOfMonth) {
day = maxDaysOfMonth;
}
return PersianDate.of(year, month, day);
}
/**
* Returns an equivalent Gregorian date and time as an instance of {@link LocalDate}.
* Calling this method has no effect on the object that calls this.
*
* @return the equivalent Gregorian date as an instance of {@link LocalDate}
*/
public LocalDate toGregorian() {
return LocalDate.from(this);
}
@Override
public long toEpochDay() {
return toJulianDay() - JULIAN_DAY_TO_1970;
}
/**
* Returns number of corresponding julian days. For number of juliand days of
* PersianDate.of(1396, 8, 6) is 2458054.
*
* Calling this method has no effect on this instance.
*
* @return number of corresponding julian days
* @see calendar convertor
*/
public long toJulianDay() {
return toJulianDay(year, month, day);
}
/**
* Returns number of corresponding julian days. For number of juliand days of
* PersianDate.of(1396, 8, 6) is 2458054. This method is provided in order to
* prevent creating unnecessary instances of {@code PersianDate} only to calculate
* julian day.
*
* @return number of corresponding julian days
* @see calendar convertor
*/
static long toJulianDay(int year, int month, int dayOfMonth) {
int epbase = year - 474;
int epyear = 474 + (epbase % 2820);
return dayOfMonth + PersianMonth.of(month).daysToFirstOfMonth() +
(epyear * 682 - 110) / 2816 +
(epyear - 1) * 365 +
(epbase / 2820 * 1029983) +
(1948320 - 1);
}
//-----------------------------------------------------------------------
/**
* Checks if this date is equal to another date.
*
* Compares this {@code PersianDate} with another ensuring that the date is the same.
*
* @param obj the object to check, null returns false
* @return true if this is equal to the other date
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof PersianDate) {
return compareTo((PersianDate) obj) == 0;
}
return false;
}
/**
* A hash code for this persian date.
*
* @return a suitable hash code
*/
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
//-----------------------------------------------------------------------
/**
* Returns the string representation of this persian date. The string contains of ten
* characters whose format is "XXXX-YY-ZZ", where XXXX is the year, YY is the
* month-of-year and ZZ is day-of-month. (Each of the capital characters represents a
* single decimal digit.)
*
* If any of the three parts of this persian date is too small to fill up its field,
* the field is padded with leading zeros.
*
* @return a suitable representation of this persian date
*/
public String toString() {
return String.format("%04d-%02d-%02d", year, month, day);
}
}