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}