Skip to content

Commit

Permalink
Add support for Islamic calendar (#3)
Browse files Browse the repository at this point in the history
* Add Islamic calendar support

* Add isIslamicCalendarLeapYear()

* Basic Islamic leap year tests

* Preliminary Islamic calendar to JDN tests

* Add round-trip testing for Islamic calendar

* Improve documentation

* Reword comment

* Reword documentation comments

* Add Islamic Calendar JD support

* Add Islamic calendar to README
  • Loading branch information
sbooth authored Oct 31, 2023
1 parent a043bf8 commit 945f78c
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 36 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# JulianDayNumber

Julian day number (JDN) and Julian date (JD) calculations supporting proleptic Julian and Gregorian calendars.
Julian day number (JDN) and Julian date (JD) calculations supporting Julian, Gregorian, and Islamic calendars.

Conversion to and from dates in the range `-9999-01-01` to `99999-12-31` is supported.

Expand Down
2 changes: 1 addition & 1 deletion Sources/JulianDayNumber/GregorianChangeover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public let gregorianCalendarChangeoverJDN = 2299161
/// The changeover occurred on 1582-10-15. Julian Thursday, 1582-10-04
/// was followed by Gregorian 1582-10-15.
///
/// JD values less than this value are typically interpreted in the Julian calendar while
/// Julian date values less than this value are typically interpreted in the Julian calendar while
/// greater or equal JD values are interpreted in the Gregorian calendar.
///
/// This JD corresponds to 1582-10-15 00:00 in the Gregorian calendar.
Expand Down
93 changes: 71 additions & 22 deletions Sources/JulianDayNumber/JD.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@

import Foundation

/// Converts a calendar date to a Julian date.
/// Converts a date in the Julian or Gregorian calendar to a Julian date.
///
/// The Julian date (JD) of an instant is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added. For example, the Julian date for 2013-01-01 00:30:00.0 UT
/// is 2456293.520833.
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// Dates before 1582-10-15 are interpreted in the Julian calendar while later dates are interpreted in the Gregorian calendar.
///
Expand All @@ -28,11 +27,10 @@ public func calendarDateToJulianDate(year Y: Int, month M: Int, day D: Int, hour
Double(calendarDateToJulianDayNumber(year: Y, month: M, day: D)) - 0.5 + timeToFractionalDay(hour: h, minute: m, second: s)
}

/// Converts a calendar date to a Julian date.
/// Converts a date in the Julian or Gregorian calendar to a Julian date.
///
/// The Julian date (JD) of an instant is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added. For example, the Julian date for 2013-01-01 00:30:00.0 UT
/// is 2456293.520833.
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// Dates before 1582-10-15 are interpreted in the Julian calendar while later dates are interpreted in the Gregorian calendar.
///
Expand All @@ -50,9 +48,8 @@ public func calendarDateToJulianDate(year Y: Int, month M: Int, day D: Double) -

/// Converts a date in the Julian calendar to a Julian date.
///
/// The Julian date (JD) of an instant is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added. For example, the Julian date for 2013-01-01 00:30:00.0 UT
/// is 2456293.520833.
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// - note: No validation checks are performed on the date values.
///
Expand All @@ -70,9 +67,8 @@ public func julianCalendarDateToJulianDate(year Y: Int, month M: Int, day D: Int

/// Converts a date in the Julian calendar to a Julian date.
///
/// The Julian date (JD) of an instant is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added. For example, the Julian date for 2013-01-01 00:30:00.0 UT
/// is 2456293.520833.
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// - note: No validation checks are performed on the date values.
///
Expand All @@ -88,9 +84,8 @@ public func julianCalendarDateToJulianDate(year Y: Int, month M: Int, day D: Dou

/// Converts a date in the Gregorian calendar to a Julian date.
///
/// The Julian date (JD) of an instant is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added. For example, the Julian date for 2013-01-01 00:30:00.0 UT
/// is 2456293.520833.
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// - note: No validation checks are performed on the date values.
///
Expand All @@ -108,9 +103,8 @@ public func gregorianCalendarDateToJulianDate(year Y: Int, month M: Int, day D:

/// Converts a date in the Gregorian calendar to a Julian date.
///
/// The Julian date (JD) of an instant is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added. For example, the Julian date for 2013-01-01 00:30:00.0 UT
/// is 2456293.520833.
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// - note: No validation checks are performed on the date values.
///
Expand All @@ -124,6 +118,42 @@ public func gregorianCalendarDateToJulianDate(year Y: Int, month M: Int, day D:
return Double(gregorianCalendarDateToJulianDayNumber(year: Y, month: M, day: Int(day))) - 0.5 + dayFraction
}

/// Converts a date in the Islamic calendar to a Julian date.
///
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// - note: No validation checks are performed on the date values.
///
/// - parameter Y: A year number between `-9999` and `99999`.
/// - parameter M: A month number between `1` (Muharram) and `12` (Dhu ́’l-Hijjab).
/// - parameter D: A day number between `1` and the maximum number of days in month `M` for year `Y`.
/// - parameter h: An hour number between `0` and `23`.
/// - parameter m: A minute number between `0` and `59`.
/// - parameter s: A second number between `0` and `59`.
///
/// - returns: The JD corresponding to the requested date.
public func islamicCalendarDateToJulianDate(year Y: Int, month M: Int, day D: Int, hour h: Int = 0, minute m: Int = 0, second s: Double = 0) -> Double {
Double(islamicCalendarDateToJulianDayNumber(year: Y, month: M, day: D)) - 0.5 + timeToFractionalDay(hour: h, minute: m, second: s)
}

/// Converts a date in the Islamic calendar to a Julian date.
///
/// The Julian date (JD) is the Julian Day Number (JDN) plus the fraction of a day since the preceding noon in Universal Time.
/// Julian dates are expressed as a JDN with a decimal fraction added.
///
/// - note: No validation checks are performed on the date values.
///
/// - parameter Y: A year number between `-9999` and `99999`.
/// - parameter M: A month number between `1` (Muharram) and `12` (Dhu ́’l-Hijjab).
/// - parameter D: A decimal day between `1` and the maximum number of days in month `M` for year `Y`.
///
/// - returns: The JD corresponding to the requested date.
public func islamicCalendarDateToJulianDate(year Y: Int, month M: Int, day D: Double) -> Double {
let (day, dayFraction) = modf(D)
return Double(islamicCalendarDateToJulianDayNumber(year: Y, month: M, day: Int(day))) - 0.5 + dayFraction
}

/// The earliest supported JD.
///
/// This JD corresponds to -9999-01-01 00:00:00 in the Julian calendar.
Expand All @@ -134,9 +164,9 @@ let earliestSupportedJD = -1931076.5
/// This JD corresponds to 99999-12-31 00:00:00 in the Gregorian calendar.
let latestSupportedJD = 38245308.5

/// Converts the Julian date `JD` to a calendar date.
/// Converts the Julian date `JD` to a date in the Julian or Gregorian calendar.
///
/// JD values less than `2299160.5` are interpreted in the Julian calendar while greater or equal JD values are interpreted in the Gregorian calendar.
/// Julian date values less than `2299160.5` are interpreted in the Julian calendar while greater or equal Julian date values are interpreted in the Gregorian calendar.
///
/// - parameter JD: A Julian date between `-1931076.5` and `38245308.5`.
///
Expand Down Expand Up @@ -183,6 +213,25 @@ public func julianDateToGregorianCalendarDate(_ JD: Double) -> (year: Int, month
convertJDToCalendarDate(JD, usingJDNConversionFunction: julianDayNumberToGregorianCalendarDate)
}

/// The earliest supported JD using the Islamic calendar.
///
/// This JD corresponds to -9999-01-01 00:00:00 in the Islamic calendar.
let earliestSupportedIslamicCalendarJD = -1595227.5

/// The latest supported JD using the Islamic calendar.
///
/// This JD corresponds to 99999-12-29 00:00:00 in the Islamic calendar.
let latestSupportedIslamicCalendarJD = 37384750.5

/// Converts the Julian date `JD` to a date in the Islamic calendar.
///
/// - parameter JD: A Julian date between `-1595227.5` and `37384750.5`.
///
/// - returns: A tuple specifying the requested date.
public func julianDateToIslamicCalendarDate(_ JD: Double) -> (year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Double) {
convertJDToCalendarDate(JD, usingJDNConversionFunction: julianDayNumberToIslamicCalendarDate)
}

/// Returns the decimal fractional day represented by the time comprised of hour `h`, minute `m`, and second `s`.
///
/// - parameter h: An hour number between `0` and `23`.
Expand Down
7 changes: 4 additions & 3 deletions Sources/JulianDayNumber/JDN+GregorianCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import Foundation
/// Converts a date in the Gregorian calendar to a Julian day number.
///
/// The Julian day number (JDN) is the integer assigned to a whole solar day in the Julian day count starting from noon Universal Time,
/// with JDN 0 assigned to the day starting at noon on November 24, 4714 BC (-4713-11-24 12:00:00) in the proleptic Gregorian calendar.
/// with JDN 0 assigned to the day starting at noon on Monday, January 1, 4713 BC (-4712-01-01 12:00:00) in the proleptic Julian calendar.
/// This date is November 24, 4714 BC (-4713-11-24 12:00:00) in the proleptic Gregorian calendar.
///
/// - note: No validation checks are performed on the date values.
///
Expand Down Expand Up @@ -43,12 +44,12 @@ public func gregorianCalendarDateToJulianDayNumber(year Y: Int, month M: Int, da
return J
}

/// The earliest supported JDN using the Gregorian calendar.
/// The earliest supported Julian day number using the Gregorian calendar.
///
/// This JDN corresponds to -9999-01-01 12:00:00 in the Gregorian calendar.
let earliestSupportedGregorianCalendarJDN = -1930999

/// The latest supported JDN using the Gregorian calendar.
/// The latest supported Julian day number using the Gregorian calendar.
///
/// This JDN corresponds to 99999-12-31 12:00:00 in the Gregorian calendar.
let latestSupportedGregorianCalendarJDN = latestSupportedJDN
Expand Down
99 changes: 99 additions & 0 deletions Sources/JulianDayNumber/JDN+IslamicCalendar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
//
// Copyright © 2021-2023 Stephen F. Booth <[email protected]>
// Part of https://github.com/sbooth/JulianDayNumber
// MIT license
//

import Foundation

/// Converts a date in the Islamic calendar to a Julian day number.
///
/// The Julian day number (JDN) is the integer assigned to a whole solar day in the Julian day count starting from noon Universal Time,
/// with JDN 0 assigned to the day starting at noon on Monday, January 1, 4713 BC (-4712-01-01 12:00:00) in the proleptic Julian calendar.
/// This date is Shaabán 17, 5499 B.H. (-5498-08-16 12:00:00) in the proleptic Islamic calendar.
///
/// - note: No validation checks are performed on the date values.
///
/// - parameter Y: A year number between `-9999` and `99999`.
/// - parameter M: A month number between `1` (Muharram) and `12` (Dhu ́’l-Hijjab).
/// - parameter D: A day number between `1` and the maximum number of days in month `M` for year `Y`.
///
/// - returns: The JDN corresponding to the requested date.
public func islamicCalendarDateToJulianDayNumber(year Y: Int, month M: Int, day D: Int) -> Int {
// Richards' algorithm is only valid for positive JDNs.
// JDN 0 is -5498-08-16 in the proleptic Islamic calendar.
// Adjust the year of earlier dates forward in time by a multiple of
// 30 (to account for leap years in the Islamic calendar)
// before calculating the JDN and then translate the result backward
// in time by the period of adjustment.
if Y < -5498 || (Y == -5498 && (M < 8 || (M == 8 && D < 16))) {
// 30 years = 10,631 days (19 years of 354 days and 11 leap years of 355 days)
let periods = (-5498 - Y) / 30 + 1
let mappedY = Y + periods * 30
let mappedJ = islamicCalendarDateToJulianDayNumber(year: mappedY, month: M, day: D)
return mappedJ - periods * 10631
}

let h = M - m
let g = Y + y - (n - h) / n
let f = (h - 1 + n) % n
let e = (p * g + q) / r + D - 1 - j
let J = e + (s * f + t) / u

return J
}

/// The earliest supported Julian day number using the Islamic calendar.
///
/// This JDN corresponds to -9999-01-01 12:00:00 in the Islamic calendar
let earliestSupportedIslamicCalendarJDN = -1595227

/// The latest supported Julian day number using the Islamic calendar.
///
/// This JDN corresponds to 99999-12-29 12:00:00 in the Islamic calendar
let latestSupportedIslamicCalendarJDN = 37384751

/// Converts the Julian day number `J` to a date in the Islamic calendar.
///
/// - parameter J: A Julian day number between `-1595227` and `37384751`.
///
/// - returns: The calendar date corresponding to `J`.
public func julianDayNumberToIslamicCalendarDate(_ J: Int) -> (year: Int, month: Int, day: Int) {
// Richards' algorithm is only valid for positive JDNs.
// Adjust negative JDNs forward in time by a multiple of
// 30 years (to account for leap years in the Islamic calendar)
// before calculating the proleptic Islamic date and then translate
// the result backward in time by the amount of forward adjustment.
if J < 0 {
// 30 years = 10,631 days (19 years of 354 days and 11 leap years of 355 days)
let periods = -J / 10631 + 1
let mappedJ = J + periods * 10631
let mappedYMD = julianDayNumberToIslamicCalendarDate(mappedJ)
return (mappedYMD.year - periods * 30, mappedYMD.month, mappedYMD.day)
}

let f = J + j
let e = r * f + v
let g = (e % p) / r
let h = u * g + w
let D = (h % s) / u + 1
let M = ((h / s + m) % n) + 1
let Y = e / p - y + (n + m - M) / n

return (Y, M, D)
}

// Constants for Islamic calendar conversions
private let y = 5519
private let j = 7664
private let m = 0
private let n = 12
private let r = 30
private let p = 10631
private let q = 14
private let v = 15
private let u = 100
private let s = 2951
private let t = 51
private let w = 10
6 changes: 3 additions & 3 deletions Sources/JulianDayNumber/JDN+JulianCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
/// Converts a date in the Julian calendar to a Julian day number.
///
/// The Julian day number (JDN) is the integer assigned to a whole solar day in the Julian day count starting from noon Universal Time,
/// with JDN 0 assigned to the day starting at noon on Monday, January 1, 4713 BC (-4712-01-01 12:00:00) in the proleptic Julian calendar
/// with JDN 0 assigned to the day starting at noon on Monday, January 1, 4713 BC (-4712-01-01 12:00:00) in the proleptic Julian calendar.
///
/// - note: No validation checks are performed on the date values.
///
Expand Down Expand Up @@ -42,12 +42,12 @@ public func julianCalendarDateToJulianDayNumber(year Y: Int, month M: Int, day D
return J
}

/// The earliest supported JDN using the Julian calendar.
/// The earliest supported Julian day number using the Julian calendar.
///
/// This JDN corresponds to -9999-01-01 12:00:00 in the Julian calendar
let earliestSupportedJulianCalendarJDN = earliestSupportedJDN

/// The latest supported JDN using the Julian calendar.
/// The latest supported Julian day number using the Julian calendar.
///
/// This JDN corresponds to 99999-12-31 12:00:00 in the Julian calendar
let latestSupportedJulianCalendarJDN = 38246057
Expand Down
11 changes: 5 additions & 6 deletions Sources/JulianDayNumber/JDN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import Foundation

// From the Explanatory Supplement to the Astronomical Almanac, 3rd edition, S.E Urban and P.K. Seidelmann eds., (Mill Valley, CA: University Science Books), Chapter 15, pp. 585-624.

/// Converts a calendar date to a Julian day number.
/// Converts a date in the Julian or Gregorian calendar to a Julian day number.
///
/// The Julian day number (JDN) is the integer assigned to a whole solar day in the Julian day count starting from noon Universal Time,
/// with JDN 0 assigned to the day starting at noon on Monday, January 1, 4713 BC (-4712-01-01 12:00:00) in the proleptic Julian calendar
/// (November 24, 4714 BC [-4713-11-24 12:00:00] in the proleptic Gregorian calendar).
/// with JDN 0 assigned to the day starting at noon on Monday, January 1, 4713 BC (-4712-01-01 12:00:00) in the proleptic Julian calendar.
///
/// Dates before 1582-10-15 are interpreted in the Julian calendar while later dates are interpreted in the Gregorian calendar.
///
Expand All @@ -31,17 +30,17 @@ public func calendarDateToJulianDayNumber(year Y: Int, month M: Int, day D: Int)
}
}

/// The earliest supported JDN.
/// The earliest supported Julian day number.
///
/// This JDN corresponds to -9999-01-01 12:00:00 in the Julian calendar
let earliestSupportedJDN = -1931076

/// The latest supported JDN.
/// The latest supported Julian day number.
///
/// This JDN corresponds to 99999-12-31 12:00:00 in the Gregorian calendar.
let latestSupportedJDN = 38245309

/// Converts the Julian day number `J` to a calendar date.
/// Converts the Julian day number `J` to a date in the Julian or Gregorian calendar.
///
/// JDN values less than `2299161` are interpreted in the Julian calendar while greater or equal JDN values are interpreted in the Gregorian calendar.
///
Expand Down
14 changes: 14 additions & 0 deletions Sources/JulianDayNumber/LeapYear.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,17 @@ public func isGregorianCalendarLeapYear(_ Y: Int) -> Bool {
public func isJulianCalendarLeapYear(_ Y: Int) -> Bool {
Y % 4 == 0
}

/// Returns `true` if `Y` is a leap year according to the Islamic calendar.
///
/// There are eleven leap years in a cycle of thirty years.
/// These are years 2, 5, 7, 10, 13, 16, 18, 21, 24, 26, and 29 of the cycle.
/// The year 1 A.H. was the first of a cycle.
///
/// - parameter Y: A year number.
///
/// - returns: `true` if `Y` is a leap year in the Islamic calendar.
func isIslamicCalendarLeapYear(_ year: Int) -> Bool {
let yearInCycle = (year - 1) % 30 + (year < 1 ? 31 : 1)
return yearInCycle == 2 || yearInCycle == 5 || yearInCycle == 7 || yearInCycle == 10 || yearInCycle == 13 || yearInCycle == 16 || yearInCycle == 18 || yearInCycle == 21 || yearInCycle == 24 || yearInCycle == 26 || yearInCycle == 29
}
Loading

0 comments on commit 945f78c

Please sign in to comment.