Skip to content

Commit

Permalink
Add Ethiopian and Coptic calendars (#8)
Browse files Browse the repository at this point in the history
* Add Ethiopian and Coptic calendars

* Fix month names

* Fix Coptic and Ethiopian leap year computation

* Add Ethiopian and Coptic limits

* Alphabetize

* Move note

* Move note again
  • Loading branch information
sbooth authored Nov 5, 2023
1 parent 2104cdd commit 4bf3ce8
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 8 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# JulianDayNumber

Julian day number (JDN) and Julian date (JD) calculations supporting the following calendars:
- Julian
- Gregorian
- Astronomical
- Islamic
- Coptic
- Egyptian
- Ethiopian
- Gregorian
- Islamic
- Julian

The JDN conversion algorithms are adapted from Richards, E.G. 2012, "[Calendars](https://aa.usno.navy.mil/downloads/c15_usb_online.pdf)," 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.

Expand All @@ -32,7 +34,7 @@ let jd = AstronomicalCalendar.dateToJulianDate(year: 1919, month: 5, day: 29)
```

> [!NOTE]
> `AstronomicalCalendar` uses the Julian calendar for dates before 1582-10-15 and the Gregorian calendar for later dates.
> The astronomical calendar is a hybrid calendar using the Julian calendar for dates before October 15, 1582 and the Gregorian calendar for later dates.
2. Convert the Julian date 2422107.5 to a `Date` instance.

Expand All @@ -57,21 +59,25 @@ The following table summarizes the **absolute limit** for 64-bit integer Julian

| Calendar | Minimum JDN | Maximum JDN |
| --- | --- | --- |
| Julian | -9223372036854775664 | 2305843009213692550 |
| Coptic | -9223372036854775664 | 2305843009213693827 |
| Egyptian | -9223372036854775514 | 9223372036854775760 |
| Ethiopian | -9223372036854775664 | 2305843009213693827 |
| Gregorian | -9223372036854719351 | 2305795661307959247 |
| Islamic | -9223372036854775352 | 307445734561818195 |
| Egyptian | -9223372036854775514 | 9223372036854775760 |
| Julian | -9223372036854775664 | 2305843009213692550 |

### Julian Dates

The following table summarizes the **absolute limit** for 64-bit floating-point Julian dates. Julian dates outside these values will cause an arithmetic overflow in `julianDateToDate`.

| Calendar | Minimum JD | Maximum JD |
| --- | --- | --- |
| Julian | -0x1.fffffffffffffp+62 | 0x1.ffffffffffffap+60 |
| Coptic | -0x1.fffffffffffffp+62 | 0x1.fffffffffffffp+60 |
| Egyptian | -0x1.fffffffffffffp+62 | 0x1.fffffffffffffp+62 |
| Ethiopian | -0x1.fffffffffffffp+62 | 0x1.fffffffffffffp+60 |
| Gregorian | -0x1.fffffffffffc8p+62 | 0x1.fffd4eff4e5d7p+60 |
| Islamic | -0x1.fffffffffffffp+62 | 0x1.1111111111099p+58 |
| Egyptian | -0x1.fffffffffffffp+62 | 0x1.fffffffffffffp+62 |
| Julian | -0x1.fffffffffffffp+62 | 0x1.ffffffffffffap+60 |

## License

Expand Down
104 changes: 104 additions & 0 deletions Sources/JulianDayNumber/CopticCalendar+JDN.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Copyright © 2021-2023 Stephen F. Booth <[email protected]>
// Part of https://github.com/sbooth/JulianDayNumber
// MIT license
//

import Foundation

// Algorithm 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.

/// The number of years in a cycle of the Coptic calendar.
///
/// A cycle in the Coptic calendar consists of 3 common years and 1 leap year.
let copticCalendarCycleYears = 4

/// The number of days in a cycle of the Coptic calendar.
///
/// A cycle in the Coptic calendar consists of 3 years of 365 days and 1 leap year of 366 days.
let copticCalendarCycleDays = 1461

extension CopticCalendar: JulianDayNumberConverting {
/// Converts a year, month, and day in the Coptic calendar to a Julian day number.
///
/// - note: No validation checks are performed on the date values.
///
/// - parameter Y: A year number.
/// - parameter M: A month number between `1` (Tut) and `13` (Nissieh).
/// - parameter D: A day number between `1` and the maximum number of days in month `M` for year `Y`.
///
/// - returns: The Julian day number corresponding to the specified year, month, and day.
public static func dateToJulianDayNumber(year Y: Int, month M: Int, day D: Int) -> JulianDayNumber {
var Y = Y
var ΔcalendarCycles = 0

// Richards' algorithm is only valid for positive JDNs.
// JDN 0 is -4996-05-05 in the Coptic calendar.
// Adjust the year of earlier dates forward in time by a multiple of
// the calendar's cycle before calculating the JDN, and then translate
// the result backward in time by the period of adjustment.
if Y < -4996 || (Y == -4996 && (M < 5 || (M == 5 && D < 5))) {
ΔcalendarCycles = (-4997 - Y) / copticCalendarCycleYears + 1
Y += ΔcalendarCycles * copticCalendarCycleYears
}

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
var J = e + (s * f + t) / u

if ΔcalendarCycles > 0 {
J -= ΔcalendarCycles * copticCalendarCycleDays
}

return J
}

/// Converts a Julian day number to a year, month, and day in the Coptic calendar.
///
/// - parameter J: A Julian day number.
///
/// - returns: The year, month, and day corresponding to the specified Julian day number.
public static func julianDayNumberToDate(_ J: JulianDayNumber) -> (year: Int, month: Int, day: Int) {
var J = J
var ΔcalendarCycles = 0

// Richards' algorithm is only valid for positive JDNs.
// Adjust negative JDNs forward in time by a multiple of
// the calendar's cycle before calculating the JDN, and then translate
// the result backward in time by the period of adjustment.
if J < 0 {
ΔcalendarCycles = -J / copticCalendarCycleDays + 1
J += ΔcalendarCycles * copticCalendarCycleDays
}

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
var Y = e / p - y + (n + m - M) / n

if ΔcalendarCycles > 0 {
Y -= ΔcalendarCycles * copticCalendarCycleYears
}

return (Y, M, D)
}
}

// Constants for Coptic calendar conversions
private let y = 4996
private let j = 124
private let m = 0
private let n = 13
private let r = 4
private let p = 1461
private let q = 0
private let v = 3
private let u = 1
private let s = 30
private let t = 0
private let w = 0
76 changes: 76 additions & 0 deletions Sources/JulianDayNumber/CopticCalendar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// Copyright © 2021-2023 Stephen F. Booth <[email protected]>
// Part of https://github.com/sbooth/JulianDayNumber
// MIT license
//

import Foundation

/// The Coptic calendar.
///
/// The Coptic calendar is a solar calendar of 365 days in every year.
///
/// The Coptic calendar epoch in the Coptic calendar is 1 Tut 1.
/// The Coptic calendar epoch in the Julian calendar is August 29, 284 AD.
///
/// - seealso: [Coptic calendar](https://en.wikipedia.org/wiki/Coptic_calendar)
public struct CopticCalendar {
/// The year, month, and day of the epoch of the Coptic calendar.
///
/// The Coptic calendar epoch in the Coptic calendar is 1 Tut 1.
/// The Coptic calendar epoch in the Julian calendar is August 29, 284 AD.
public static let epochDate = (year: 1, month: 1, day: 1)

/// The Julian day number of the epoch of the Coptic calendar.
///
/// This JDN corresponds to noon on 1 Tut 1 in the Coptic calendar.
public static let epochJulianDayNumber: JulianDayNumber = 1825030

/// The Julian date of the epoch of the Coptic calendar.
///
/// This JD corresponds to midnight on 1 Tut 1 in the Coptic calendar.
public static let epochJulianDate: JulianDate = 1825029.5

/// Returns `true` if the specified year, month, and day form a valid date in the Coptic calendar.
///
/// - parameter Y: A year number.
/// - parameter M: A month number between `1` (Tut) and `13` (Nissieh).
/// - parameter D: A day number between `1` and the maximum number of days in month `M` for year `Y`.
///
/// - returns: `true` if the specified year, month, and day form a valid date in the Coptic calendar.
public static func isDateValid(year Y: Int, month M: Int, day D: Int) -> Bool {
M > 0 && M <= 13 && D > 0 && D <= daysInMonth(year: Y, month: M)
}

/// Returns `true` if the specified year is a leap year in the Coptic calendar.
///
/// A Coptic year is a leap year if its numerical designation plus one is divisible by 4.
///
/// - parameter Y: A year number.
///
/// - returns: `true` if the specified year is a leap year in the Coptic calendar.
public static func isLeapYear(_ Y: Int) -> Bool {
(Y + 1) % 4 == 0
}

/// The number of days in each month indexed from `0` (Tut) to `12` (Nissieh).
static let monthLengths = [ 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 5 ]

/// Returns the number of days in the specified month and year in the Coptic calendar.
///
/// - parameter Y: A year number.
/// - parameter M: A month number between `1` (Tut) and `13` (Nissieh).
///
/// - returns: The number of days in the specified month and year.
public static func daysInMonth(year Y: Int, month M: Int) -> Int {
guard M > 0, M <= 13 else {
return 0
}

if M == 13 {
return isLeapYear(Y) ? 6 : 5
} else {
return monthLengths[M - 1]
}
}
}
104 changes: 104 additions & 0 deletions Sources/JulianDayNumber/EthiopianCalendar+JDN.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Copyright © 2021-2023 Stephen F. Booth <[email protected]>
// Part of https://github.com/sbooth/JulianDayNumber
// MIT license
//

import Foundation

// Algorithm 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.

/// The number of years in a cycle of the Ethiopian calendar.
///
/// A cycle in the Ethiopian calendar consists of 3 common years and 1 leap year.
let ethiopianCalendarCycleYears = 4

/// The number of days in a cycle of the Ethiopian calendar.
///
/// A cycle in the Ethiopian calendar consists of 3 years of 365 days and 1 leap year of 366 days.
let ethiopianCalendarCycleDays = 1461

extension EthiopianCalendar: JulianDayNumberConverting {
/// Converts a year, month, and day in the Ethiopian calendar to a Julian day number.
///
/// - note: No validation checks are performed on the date values.
///
/// - parameter Y: A year number.
/// - parameter M: A month number between `1` (Mäskäräm) and `13` (Ṗagumen).
/// - parameter D: A day number between `1` and the maximum number of days in month `M` for year `Y`.
///
/// - returns: The Julian day number corresponding to the specified year, month, and day.
public static func dateToJulianDayNumber(year Y: Int, month M: Int, day D: Int) -> JulianDayNumber {
var Y = Y
var ΔcalendarCycles = 0

// Richards' algorithm is only valid for positive JDNs.
// JDN 0 is -4720-05-05 in the Ethiopian calendar.
// Adjust the year of earlier dates forward in time by a multiple of
// the calendar's cycle before calculating the JDN, and then translate
// the result backward in time by the period of adjustment.
if Y < -4720 || (Y == -4720 && (M < 5 || (M == 5 && D < 5))) {
ΔcalendarCycles = (-4721 - Y) / ethiopianCalendarCycleYears + 1
Y += ΔcalendarCycles * ethiopianCalendarCycleYears
}

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
var J = e + (s * f + t) / u

if ΔcalendarCycles > 0 {
J -= ΔcalendarCycles * ethiopianCalendarCycleDays
}

return J
}

/// Converts a Julian day number to a year, month, and day in the Ethiopian calendar.
///
/// - parameter J: A Julian day number.
///
/// - returns: The year, month, and day corresponding to the specified Julian day number.
public static func julianDayNumberToDate(_ J: JulianDayNumber) -> (year: Int, month: Int, day: Int) {
var J = J
var ΔcalendarCycles = 0

// Richards' algorithm is only valid for positive JDNs.
// Adjust negative JDNs forward in time by a multiple of
// the calendar's cycle before calculating the JDN, and then translate
// the result backward in time by the period of adjustment.
if J < 0 {
ΔcalendarCycles = -J / ethiopianCalendarCycleDays + 1
J += ΔcalendarCycles * ethiopianCalendarCycleDays
}

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
var Y = e / p - y + (n + m - M) / n

if ΔcalendarCycles > 0 {
Y -= ΔcalendarCycles * ethiopianCalendarCycleYears
}

return (Y, M, D)
}
}

// Constants for Ethiopian calendar conversions
private let y = 4720
private let j = 124
private let m = 0
private let n = 13
private let r = 4
private let p = 1461
private let q = 0
private let v = 3
private let u = 1
private let s = 30
private let t = 0
private let w = 0
Loading

0 comments on commit 4bf3ce8

Please sign in to comment.