julian_day_converter/
lib.rs

1use chrono::{DateTime, NaiveDateTime};
2use std::fmt;
3
4/// Public constant that may be useful to library users
5/// 1970-01-01 00:00:00 UTC
6pub const JULIAN_DAY_UNIX_EPOCH_DAYS: f64 = 2440587.5;
7
8/// The weekday index for the Unix Epoch (1970-01-01 UTC) is Thursday (4)
9pub const JULIAN_DAY_UNIX_EPOCH_WEEKDAY: u8 = 4;
10
11/// Minimum Julian Day value for date-time conversion.
12/// Note: 0 is -4713-11-24 12:00:00 UTC
13/// Note: Some databases may not support dates before -4713
14/// -9999-01-01 00:00:00 UTC
15pub const JULIAN_DAY_MIN_SUPPORTED: f64 = -1_930_999.5;
16
17/// Maximum Julian Day value for date-time conversion.
18/// Note: 0 is -4713-11-24 12:00:00 UTC
19/// 9999-12-31 23:59:59 UTC
20pub const JULIAN_DAY_MAX_SUPPORTED: f64 = 5_373_484.499999;
21
22/// Custom Error Type for date range conversion errors
23#[derive(Debug)]
24pub struct DateRangeConversionError;
25
26impl fmt::Display for DateRangeConversionError {
27    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28        write!(
29            f,
30            "DateRangeConversionError: The provided Julian Day is out of the supported range."
31        )
32    }
33}
34
35/// Convert a Unix timestamp milliseconds as a 64-bit integer to Julian days as a 64-bit float
36///
37/// ## Example:
38/// ```
39/// use julian_day_converter::*;
40///
41/// let julian_day: f64 = unix_millis_to_julian_day(1_672_929_282_000);
42/// ```
43pub fn unix_millis_to_julian_day(ms: i64) -> f64 {
44    (ms as f64 / 86_400_000.0) + JULIAN_DAY_UNIX_EPOCH_DAYS
45}
46
47/// Convert a Unix timestamp seconds as a 64-bit integer to Julian days as a 64-bit float
48///
49/// ## Example:
50/// ```
51/// use julian_day_converter::*;
52///
53/// let julian_day: f64 = unixtime_to_julian_day(1_672_929_282);
54/// ```
55pub fn unixtime_to_julian_day(ms: i64) -> f64 {
56    (ms as f64 / 86_400.0) + JULIAN_DAY_UNIX_EPOCH_DAYS
57}
58
59/// Convert Julian day as a 64-bit float to Unix timestamp milliseconds as a signed 64-bit integer
60///
61/// ## Example:
62/// ```
63/// use julian_day_converter::*;
64///
65/// let julian_day: f64 = 2_460_258.488768587;
66/// let unix_millis: i64 = julian_day_to_unix_millis(julian_day);
67/// ```
68pub fn julian_day_to_unix_millis(jd: f64) -> i64 {
69    ((jd - JULIAN_DAY_UNIX_EPOCH_DAYS) * 86_400_000.0).round() as i64
70}
71
72/// Convert Julian day as a 64-bit float to Unix timestamp seconds as a signed 64-bit integer
73///
74/// ## Example:
75/// ```
76/// use julian_day_converter::*;
77///
78/// let julian_day: f64 = 2_460_258.488768587;
79/// let unix_seconds: i64 = julian_day_to_unixtime(julian_day);
80/// ```
81pub fn julian_day_to_unixtime(jd: f64) -> i64 {
82    ((jd - JULIAN_DAY_UNIX_EPOCH_DAYS) * 86_400.0).round() as i64
83}
84
85/// Convert Julian day as a 64-bit float to a timezone-neutral chrono::NaiveDateTime object
86///
87/// ## Example:
88/// ```
89/// use chrono::NaiveDateTime;
90/// use julian_day_converter::*;
91///
92/// let julian_day: f64 = 2460258.488768587;
93/// if let Ok(date_time) = julian_day_to_datetime(julian_day) {
94///     println!("The date time is {}", date_time.format("%Y-%m-%d %H:%M:%S"));
95/// }
96/// ```
97pub fn julian_day_to_datetime(jd: f64) -> Result<NaiveDateTime, DateRangeConversionError> {
98    if (JULIAN_DAY_MIN_SUPPORTED..=JULIAN_DAY_MAX_SUPPORTED).contains(&jd) {
99        let milliseconds = julian_day_to_unix_millis(jd);
100        if let Some(dt) = DateTime::from_timestamp_millis(milliseconds) {
101            return Ok(dt.naive_utc());
102        }
103    }
104    Err(DateRangeConversionError)
105}
106
107/// This trait may be implemented by any Date or DateTime object
108/// An implementation for chrono::NaiveDateTime is provided below
109pub trait JulianDay {
110    /// Convert from DateTime Object to a Julian Day as f64
111    fn to_jd(&self) -> f64;
112
113    /// Convert from a Julian Day as f64 to DateTime Object
114    fn from_jd(jd: f64) -> Option<Self>
115    where
116        Self: Sized;
117}
118
119impl JulianDay for NaiveDateTime {
120    /// Convert datetime object to a Julian day as a 64-bit float
121    ///
122    /// ## Example:
123    /// ```
124    /// use chrono::NaiveDateTime;
125    /// use julian_day_converter::*;
126    ///
127    /// if let Ok(date_time) = NaiveDateTime::parse_from_str("2023-11-08 12:53:31", "%Y-%m-%d %H:%M:%S") {
128    ///     println!("The astronomical application needs this value {}", date_time.to_jd());
129    /// }
130    /// ```
131    fn to_jd(&self) -> f64 {
132        unix_millis_to_julian_day(self.and_utc().timestamp_millis())
133    }
134
135    /// Construct a DateTime object from a Julian day value (64-bit float)
136    ///
137    /// ## Example:
138    /// ```
139    /// use chrono::NaiveDateTime;
140    /// use julian_day_converter::*;
141    ///
142    /// let jd: f64 = 2321789.393736365;
143    /// if let Some(date_time) = NaiveDateTime::from_jd(jd) {
144    ///     println!("The Julian day {} translates to {}", jd, date_time.format("%Y-%m-%d %H:%M:%S"));
145    /// }
146    /// ```
147    fn from_jd(jd: f64) -> Option<Self> {
148        julian_day_to_datetime(jd).ok()
149    }
150}
151
152/// This trait may be implemented by any Date or DateTime object
153/// An implementation for chrono::NaiveDateTime is provided below
154pub trait WeekdayIndex {
155    /// Current weekday index, where Sunday = 0, Monday = 1, and Saturday = 6
156    /// The local weekday index depends on the timezone offset in seconds
157    /// West of UTC => Negative hour offset * 3600, e.g. -18000 => UTC-5
158    /// East of UTC => Positive hour offset * 3600, e.g. +3600 => UTC+1
159    fn weekday_index(&self, offset_secs: i32) -> u8;
160
161    /// ISO 8601 and Java/C# style day of week index starting from Monday = 1 to Sunday = 7
162    /// NB: Python's datetime.weekday() method returns 0 for Monday and 6 for Sunday
163    fn weekday_number(&self, offset_secs: i32) -> u8;
164}
165
166impl WeekdayIndex for NaiveDateTime {
167    /// Return the weekday index (Sun = 0, Mon = 1 ... Sat = 6) in a timezone-neutral context
168    fn weekday_index(&self, offset_secs: i32) -> u8 {
169        julian_day_to_weekday_index(self.to_jd(), offset_secs)
170    }
171
172    /// Return the weekday index (Mon = 1, Tue = 2 ... Sun = 7) in a timezone-neutral context
173    fn weekday_number(&self, offset_secs: i32) -> u8 {
174        julian_day_to_weekday_number(self.to_jd(), offset_secs)
175    }
176}
177
178/// Calculate the weekday index from a given Julian Day with timezone offsets in seconds
179/// This is zero-based, where Sunday = 0, Monday = 1, ..., Saturday = 6
180pub fn julian_day_to_weekday_index(jd: f64, offset_secs: i32) -> u8 {
181    let ref_jd = jd + (offset_secs as f64 / 86400.0);
182    let days_since_1970 = ref_jd - JULIAN_DAY_UNIX_EPOCH_DAYS;
183    let ds = (days_since_1970 as u64) % 7;
184    let days_since_index = if ds < 7 { ds as u8 } else { 0u8 };
185    (days_since_index + JULIAN_DAY_UNIX_EPOCH_WEEKDAY) % 7
186}
187
188/// Return the weekday number (Mon = 1 to Sun = 7) in a timezone-neutral context
189/// as used in Java, C# and ISO 8601 (Python's datetime.weekday() method is 0-based from Monday)
190pub fn julian_day_to_weekday_number(jd: f64, offset_secs: i32) -> u8 {
191    let index = julian_day_to_weekday_index(jd, offset_secs);
192    if index == 0 {
193        7
194    } else {
195        index
196    }
197}