xrpl/utils/
time_conversion.rs

1//! Conversions between the XRP Ledger's 'Ripple Epoch' time and native time
2//! data types.
3
4use crate::utils::exceptions::XRPLTimeRangeException;
5use chrono::TimeZone;
6use chrono::Utc;
7use chrono::{DateTime, LocalResult};
8
9use super::exceptions::XRPLUtilsResult;
10
11/// The "Ripple Epoch" of 2000-01-01T00:00:00 UTC
12pub const RIPPLE_EPOCH: i64 = 946684800;
13/// The maximum time that can be expressed on the XRPL
14pub const MAX_XRPL_TIME: i64 = i64::pow(2, 32);
15
16/// Ensures time does not exceed max representable on XRPL.
17fn _ripple_check_max<T>(time: i64, ok: T) -> XRPLUtilsResult<T> {
18    if !(0..=MAX_XRPL_TIME).contains(&time) {
19        Err(XRPLTimeRangeException::UnexpectedTimeOverflow {
20            max: MAX_XRPL_TIME,
21            found: time,
22        }
23        .into())
24    } else {
25        Ok(ok)
26    }
27}
28
29/// Convert from XRP Ledger 'Ripple Epoch' time to a UTC datetime.
30/// Used internally.
31/// See [`chrono::DateTime`]
32///
33/// [`chrono::DateTime`]: mod@chrono::DateTime
34/// ```
35pub(crate) fn ripple_time_to_datetime(ripple_time: i64) -> XRPLUtilsResult<DateTime<Utc>> {
36    let datetime = Utc.timestamp_opt(ripple_time + RIPPLE_EPOCH, 0);
37    match datetime {
38        LocalResult::Single(dt) => _ripple_check_max(ripple_time, dt),
39        _ => Err(XRPLTimeRangeException::InvalidLocalTime.into()),
40    }
41}
42
43/// Convert from a [`chrono::DateTime`] object to an XRP Ledger
44/// 'Ripple Epoch' time.
45/// Used internally.
46///
47/// [`chrono::DateTime`]: mod@chrono::DateTime
48/// ```
49pub(crate) fn datetime_to_ripple_time(dt: DateTime<Utc>) -> XRPLUtilsResult<i64> {
50    let ripple_time = dt.timestamp() - RIPPLE_EPOCH;
51    _ripple_check_max(ripple_time, ripple_time)
52}
53
54/// Convert from XRP Ledger 'Ripple Epoch' time to a POSIX-like
55/// integer timestamp.
56///
57/// # Examples
58///
59/// ## Basic usage
60///
61/// ```
62/// use xrpl::utils::ripple_time_to_posix;
63/// use xrpl::utils::exceptions::{XRPLTimeRangeException, XRPLUtilsException};
64///
65/// let posix: Option<i64> = match ripple_time_to_posix(946684801) {
66///     Ok(time) => Some(time),
67///     Err(e) => match e {
68///         XRPLUtilsException::XRPLTimeRangeError(XRPLTimeRangeException::InvalidTimeBeforeEpoch { min: _, found: _}) => None,
69///         XRPLUtilsException::XRPLTimeRangeError(XRPLTimeRangeException::UnexpectedTimeOverflow { max: _, found: _ }) => None,
70///         _ => None,
71///     },
72/// };
73///
74/// assert_eq!(Some(1893369601), posix);
75/// ```
76pub fn ripple_time_to_posix(ripple_time: i64) -> XRPLUtilsResult<i64> {
77    _ripple_check_max(ripple_time, ripple_time + RIPPLE_EPOCH)
78}
79
80/// Convert from a POSIX-like timestamp to an XRP Ledger
81/// 'Ripple Epoch' time.
82///
83/// # Examples
84///
85/// ## Basic usage
86///
87/// ```
88/// use xrpl::utils::posix_to_ripple_time;
89/// use xrpl::utils::exceptions::{XRPLTimeRangeException, XRPLUtilsException};
90///
91/// let timestamp: Option<i64> = match posix_to_ripple_time(946684801) {
92///     Ok(time) => Some(time),
93///     Err(e) => match e {
94///         XRPLUtilsException::XRPLTimeRangeError(XRPLTimeRangeException::InvalidTimeBeforeEpoch { min: _, found: _}) => None,
95///         XRPLUtilsException::XRPLTimeRangeError(XRPLTimeRangeException::UnexpectedTimeOverflow { max: _, found: _ }) => None,
96///         _ => None,
97///     },
98/// };
99///
100/// assert_eq!(Some(1), timestamp);
101/// ```
102pub fn posix_to_ripple_time(timestamp: i64) -> XRPLUtilsResult<i64> {
103    let ripple_time = timestamp - RIPPLE_EPOCH;
104    _ripple_check_max(ripple_time, ripple_time)
105}
106
107#[cfg(test)]
108mod test {
109    use super::*;
110
111    #[test]
112    fn test_ripple_time_to_datetime() {
113        let success: DateTime<Utc> = ripple_time_to_datetime(RIPPLE_EPOCH).unwrap();
114        assert_eq!(success.timestamp(), RIPPLE_EPOCH + RIPPLE_EPOCH);
115    }
116
117    #[test]
118    fn test_datetime_to_ripple_time() {
119        let actual = match Utc.timestamp_opt(RIPPLE_EPOCH, 0) {
120            LocalResult::Single(dt) => datetime_to_ripple_time(dt),
121            _ => Err(XRPLTimeRangeException::InvalidLocalTime.into()),
122        };
123        assert_eq!(Ok(0_i64), actual);
124    }
125
126    #[test]
127    fn test_ripple_time_to_posix() {
128        assert_eq!(
129            ripple_time_to_posix(RIPPLE_EPOCH),
130            Ok(RIPPLE_EPOCH + RIPPLE_EPOCH)
131        );
132    }
133
134    #[test]
135    fn test_posix_to_ripple_time() {
136        assert_eq!(posix_to_ripple_time(RIPPLE_EPOCH), Ok(0_i64));
137    }
138
139    #[test]
140    fn accept_posix_round_trip() {
141        let current_time: i64 = Utc::now().timestamp();
142        let ripple_time: i64 = posix_to_ripple_time(current_time).unwrap();
143        let round_trip_time = ripple_time_to_posix(ripple_time);
144
145        assert_eq!(Ok(current_time), round_trip_time);
146    }
147
148    #[test]
149    fn accept_datetime_round_trip() -> XRPLUtilsResult<()> {
150        let current_time: DateTime<Utc> = match Utc.timestamp_opt(Utc::now().timestamp(), 0) {
151            LocalResult::Single(dt) => dt,
152            _ => return Err(XRPLTimeRangeException::InvalidLocalTime.into()),
153        };
154        let ripple_time: i64 = datetime_to_ripple_time(current_time).unwrap();
155        let round_trip_time = ripple_time_to_datetime(ripple_time);
156
157        assert_eq!(Ok(current_time), round_trip_time);
158
159        Ok(())
160    }
161
162    #[test]
163    fn accept_ripple_epoch() -> XRPLUtilsResult<()> {
164        let expected = match Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0) {
165            LocalResult::Single(dt) => dt,
166            _ => return Err(XRPLTimeRangeException::InvalidLocalTime.into()),
167        };
168        assert_eq!(Ok(expected), ripple_time_to_datetime(0));
169
170        Ok(())
171    }
172
173    /// "Ripple Epoch" time starts in the year 2000
174    #[test]
175    fn accept_datetime_underflow() -> XRPLUtilsResult<()> {
176        let datetime: DateTime<Utc> = match Utc.with_ymd_and_hms(1999, 1, 1, 0, 0, 0) {
177            LocalResult::Single(dt) => dt,
178            _ => return Err(XRPLTimeRangeException::InvalidLocalTime.into()),
179        };
180        assert!(datetime_to_ripple_time(datetime).is_err());
181
182        Ok(())
183    }
184
185    /// "Ripple Epoch" time starts in the year 2000
186    #[test]
187    fn accept_posix_underflow() -> XRPLUtilsResult<()> {
188        let datetime: DateTime<Utc> = match Utc.with_ymd_and_hms(1999, 1, 1, 0, 0, 0) {
189            LocalResult::Single(dt) => dt,
190            _ => return Err(XRPLTimeRangeException::InvalidLocalTime.into()),
191        };
192        assert!(posix_to_ripple_time(datetime.timestamp()).is_err());
193
194        Ok(())
195    }
196
197    /// "Ripple Epoch" time's equivalent to the
198    /// "Year 2038 problem" is not until 2136
199    /// because it uses an *unsigned* 32-bit int
200    /// starting 30 years after UNIX time's signed
201    /// 32-bit int.
202    #[test]
203    fn accept_datetime_overflow() -> XRPLUtilsResult<()> {
204        let datetime: DateTime<Utc> = match Utc.with_ymd_and_hms(2137, 1, 1, 0, 0, 0) {
205            LocalResult::Single(dt) => dt,
206            _ => return Err(XRPLTimeRangeException::InvalidLocalTime.into()),
207        };
208        assert!(datetime_to_ripple_time(datetime).is_err());
209
210        Ok(())
211    }
212
213    #[test]
214    fn accept_posix_overflow() -> XRPLUtilsResult<()> {
215        let datetime: DateTime<Utc> = match Utc.with_ymd_and_hms(2137, 1, 1, 0, 0, 0) {
216            LocalResult::Single(dt) => dt,
217            _ => return Err(XRPLTimeRangeException::InvalidLocalTime.into()),
218        };
219        assert!(posix_to_ripple_time(datetime.timestamp()).is_err());
220
221        Ok(())
222    }
223}