defmodule Jalaali do
@moduledoc """
Jalaali module helps converting gregorian dates to jalaali dates.
Jalaali calendar is widely used in Persia and Afganistan.
This module helps you with converting erlang and/or elixir DateTime formats to Jalaali date (and vice versa) and checking for leap years
"""
@days_offset 1_721_060
@breaks [
-61,
9,
38,
199,
426,
686,
756,
818,
1111,
1181,
1210,
1635,
2060,
2097,
2192,
2262,
2324,
2394,
2456,
3178
]
@doc """
Converts erlang or elixir date or dateTime from Gregorian to Jalaali format
## Parameters
- arg1: Date to convert in erlang format (a tuple with three elements)
- arg1: erlang dateTime to convert in erlang format (a tuple with two difftent tuples each with 3 elements)
- ex_dt: Date or DateTime to convert
## Exmaples
```elixir
iex> Jalaali.to_jalaali {2016, 12, 17}
{1395, 9, 27}
iex> Jalaali.to_jalaali {{2016, 12, 17}, {11, 11, 11}}
{{1395, 9, 27}, {11, 11, 11}}
iex> Jalaali.to_jalaali ~D[2016-12-17]
~D[1395-09-27]
```
"""
@spec to_jalaali(tuple() | DateTime.t() | Date.t()) :: tuple() | DateTime.t() | Date.t()
def to_jalaali({gy, gm, gd}) do
d2j(g2d({gy, gm, gd}))
end
def to_jalaali({date, time}) do
{to_jalaali(date), time}
end
def to_jalaali(ex_dt) do
{jy, jm, jd} = to_jalaali({ex_dt.year, ex_dt.month, ex_dt.day})
%{ex_dt | year: jy, month: jm, day: jd}
end
@doc """
Converts erlang or elixir date or dateTime from Jalaali to Gregorian format
## Parameters
- arg1: Date to convert in erlang format (a tuple with three elements)
- arg1: Date to convert in erlang format (a tuple with three elements)
- ex_dt: Date or DateTime to convert
## Exmaples
```elixir
iex> Jalaali.to_gregorian {1395, 9, 27}
{2016, 12, 17}
iex> Jalaali.to_jalaali {{2016, 12, 17}, {11, 11, 11}}
{{1395, 9, 27}, {11, 11, 11}}
iex> Jalaali.to_gregorian ~D[1395-09-27]
~D[2016-12-17]
```
"""
@spec to_gregorian(tuple() | DateTime.t() | Date.t()) :: tuple() | DateTime.t() | Date.t()
def to_gregorian({jy, jm, jd}) do
d2g(j2d({jy, jm, jd}))
end
def to_gregorian({date, time}) do
{to_gregorian(date), time}
end
def to_gregorian(ex_dt) do
{gy, gm, gd} = to_gregorian({ex_dt.year, ex_dt.month, ex_dt.day})
%{ex_dt | year: gy, month: gm, day: gd}
end
@doc """
Checks whether a Jalaali date is valid or not.
## Parameters
- arg1: is a tuple in shape of {jalaali_year, jalaali_month, jalaali_day}
## Examples
```elixir
iex> Jalaali.is_valid_jalaali_date {1395, 9, 27}
true
iex> Jalaali.is_valid_jalaali_date {1395, 91, 27}
false
```
"""
@spec is_valid_jalaali_date(tuple()) :: boolean()
def is_valid_jalaali_date?({jy, jm, jd}) do
year_is_valid = jy <= 3177 && -61 <= jy
month_is_valid = 1 <= jm && jm <= 12
day_is_valid = 1 <= jd && jd <= Jalaali.jalaali_month_length(jy, jm)
year_is_valid && month_is_valid && day_is_valid
end
@doc """
This function is same as `is_valid_jalaali_date?` and is only here
because i forgot to add question mark in `jalaali <= 0.1.1`
Please use `is_valid_jalaali_date?` instead.
"""
def is_valid_jalaali_date(jdate) do
is_valid_jalaali_date?(jdate)
end
@doc """
Checks if a Jalaali year is leap
## Parameters
- jy: Jalaali Year (-61 to 3177)
## Examples
```elixir
iex> Jalaali.is_leap_jalaali_year(1395)
true
iex> Jalaali.is_leap_jalaali_year(1396)
false
iex> Jalaali.is_leap_jalaali_year(1394)
false
```
"""
@spec is_leap_jalaali_year(integer()) :: boolean()
def is_leap_jalaali_year(jy) do
jal_cal(jy).leap == 0
end
@doc """
Number of days in a given month in a Jalaali year.
## Examples
```elixir
iex> Jalaali.jalaali_month_length(1395, 11)
30
iex> Jalaali.jalaali_month_length(1395, 6)
31
iex> Jalaali.jalaali_month_length(1394, 12)
29
iex> Jalaali.jalaali_month_length(1395, 12)
30
```
"""
@spec jalaali_month_length(integer(), integer()) :: integer()
def jalaali_month_length(jy, jm) do
cond do
jm <= 6 -> 31
jm <= 11 -> 30
is_leap_jalaali_year(jy) -> 30
true -> 29
end
end
@doc """
Converts jalaali date to days number
"""
@spec jalaali_to_days(integer(), integer(), integer()) :: integer()
def jalaali_to_days(jy, jm, jd) do
j2d({jy, jm, jd}) - @days_offset
end
@doc """
Converts days number to jalaali date
"""
@spec days_to_jalaali(integer()) :: {integer(), integer(), integer()}
def days_to_jalaali(days) do
d2j(days + @days_offset)
end
@doc """
This function determines if the Jalaali (persian) year is leap(366-day long) or is the common year (365-days),
and finds the day in March (Gregorian calendar) of the first day of the Jalaali year (jy).
## Parameters
- jy: Jalaali Year (-61 to 3177)
"""
@spec jal_cal(integer()) :: map()
def jal_cal(jy) do
gy = jy + 621
if jy < -61 or jy >= 3178 do
raise "Invalid Jalaali year #{jy}"
end
{jump, jp, leap_j} = calc_jlimit(jy, {Enum.at(@breaks, 0), -14}, 1)
n = jy - jp
leap_j1 =
if mod(jump, 33) == 4 && jump - n == 4 do
leap_j + div(n, 33) * 8 + div(mod(n, 33) + 3, 4) + 1
else
leap_j + div(n, 33) * 8 + div(mod(n, 33) + 3, 4)
end
leap_g = div(gy, 4) - div((div(gy, 100) + 1) * 3, 4) - 150
march = 20 + leap_j1 - leap_g
n =
if jump - n < 6 do
n - jump + div(jump + 4, 33) * 33
else
jy - jp
end
leap_c = mod(mod(n + 1, 33) - 1, 4)
leap =
case leap_c do
-1 -> 4
_ -> leap_c
end
%{leap: leap, gy: gy, march: march}
end
@spec calc_jlimit(integer(), {integer(), integer()}, integer()) ::
{integer(), integer(), integer()}
defp calc_jlimit(jy, {jp, leap_j}, index) do
jm = Enum.at(@breaks, index)
jump = jm - jp
if jy < jm do
{jump, jp, leap_j}
else
calc_jlimit(jy, {jm, leap_j + div(jump, 33) * 8 + div(mod(jump, 33), 4)}, index + 1)
end
end
@spec j2d({integer(), integer(), integer()}) :: integer()
defp j2d({jy, jm, jd}) do
r = jal_cal(jy)
g2d({r.gy, 3, r.march}) + (jm - 1) * 31 - div(jm, 7) * (jm - 7) + jd - 1
end
@spec d2j(integer()) :: {integer(), integer(), integer()}
defp d2j(jdn) do
# calculate gregorian year (gy)
gy = elem(d2g(jdn), 0)
jy = gy - 621
r = jal_cal(jy)
jdn1f = g2d({gy, 3, r.march})
# find number of days that passed since 1 farvardin
k = jdn - jdn1f
cond do
k <= 185 && k >= 0 ->
{jy, div(k, 31) + 1, mod(k, 31) + 1}
k >= 0 ->
k = k - 186
jm = 7 + div(k, 30)
jd = mod(k, 30) + 1
{jy, jm, jd}
r.leap == 1 ->
jy = jy - 1
k = k + 180
jm = 7 + div(k, 30)
jd = mod(k, 30) + 1
{jy, jm, jd}
true ->
jy = jy - 1
k = k + 179
jm = 7 + div(k, 30)
jd = mod(k, 30) + 1
{jy, jm, jd}
end
end
@spec g2d({integer(), integer(), integer()}) :: integer()
defp g2d({gy, gm, gd}) do
d =
div((gy + div(gm - 8, 6) + 100_100) * 1461, 4) + div(153 * mod(gm + 9, 12) + 2, 5) + gd -
34_840_408
d - div(div(gy + 100_100 + div(gm - 8, 6), 100) * 3, 4) + 752
end
@spec d2g(integer()) :: {integer(), integer(), integer()}
defp d2g(jdn) do
j = 4 * jdn + 139_361_631 + div(div(4 * jdn + 183_187_720, 146_097) * 3, 4) * 4 - 3908
i = div(mod(j, 1461), 4) * 5 + 308
gd = div(mod(i, 153), 5) + 1
gm = mod(div(i, 153), 12) + 1
gy = div(j, 1461) - 100_100 + div(8 - gm, 6)
{gy, gm, gd}
end
@spec mod(integer(), integer()) :: integer()
defp mod(a, b) do
a - div(a, b) * b
end
end