Skip to main content

nautilus_core/python/
datetime.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Date/time utility wrappers exposed to Python.
17
18use pyo3::prelude::*;
19use pyo3_stub_gen::derive::gen_stub_pyfunction;
20
21use super::to_pyvalue_err;
22use crate::{
23    UnixNanos,
24    datetime::{
25        is_within_last_24_hours, last_weekday_nanos, micros_to_nanos, millis_to_nanos,
26        nanos_to_micros, nanos_to_millis, nanos_to_secs, secs_to_millis, secs_to_nanos,
27        unix_nanos_to_iso8601, unix_nanos_to_iso8601_millis,
28    },
29};
30
31/// Converts seconds to nanoseconds (ns).
32///
33/// # Errors
34///
35/// Returns an error if `secs` is non-finite or cannot be represented as `u64` nanoseconds.
36#[pyfunction(name = "secs_to_nanos")]
37#[gen_stub_pyfunction(module = "nautilus_trader.core")]
38pub fn py_secs_to_nanos(secs: f64) -> PyResult<u64> {
39    secs_to_nanos(secs).map_err(to_pyvalue_err)
40}
41
42/// Converts seconds to milliseconds (ms).
43///
44/// # Errors
45///
46/// Returns an error if `secs` is non-finite or cannot be represented as `u64` milliseconds.
47#[pyfunction(name = "secs_to_millis")]
48#[gen_stub_pyfunction(module = "nautilus_trader.core")]
49pub fn py_secs_to_millis(secs: f64) -> PyResult<u64> {
50    secs_to_millis(secs).map_err(to_pyvalue_err)
51}
52
53/// Converts milliseconds (ms) to nanoseconds (ns).
54///
55/// Casting f64 to u64 by truncating the fractional part is intentional for unit conversion,
56/// which may lose precision and drop negative values after clamping.
57///
58/// # Errors
59///
60/// Returns an error if `millis` is non-finite or cannot be represented as `u64` nanoseconds.
61#[pyfunction(name = "millis_to_nanos")]
62#[gen_stub_pyfunction(module = "nautilus_trader.core")]
63pub fn py_millis_to_nanos(millis: f64) -> PyResult<u64> {
64    millis_to_nanos(millis).map_err(to_pyvalue_err)
65}
66
67/// Converts microseconds (μs) to nanoseconds (ns).
68///
69/// Casting f64 to u64 by truncating the fractional part is intentional for unit conversion,
70/// which may lose precision and drop negative values after clamping.
71///
72/// # Errors
73///
74/// Returns an error if `micros` is non-finite or cannot be represented as `u64` nanoseconds.
75#[pyfunction(name = "micros_to_nanos")]
76#[gen_stub_pyfunction(module = "nautilus_trader.core")]
77pub fn py_micros_to_nanos(micros: f64) -> PyResult<u64> {
78    micros_to_nanos(micros).map_err(to_pyvalue_err)
79}
80
81/// Converts nanoseconds (ns) to seconds.
82///
83/// Casting u64 to f64 may lose precision for large values,
84/// but is acceptable when computing fractional seconds.
85#[must_use]
86#[pyfunction(name = "nanos_to_secs")]
87#[gen_stub_pyfunction(module = "nautilus_trader.core")]
88pub fn py_nanos_to_secs(nanos: u64) -> f64 {
89    nanos_to_secs(nanos)
90}
91
92/// Converts nanoseconds (ns) to milliseconds (ms).
93#[must_use]
94#[pyfunction(name = "nanos_to_millis")]
95#[gen_stub_pyfunction(module = "nautilus_trader.core")]
96pub const fn py_nanos_to_millis(nanos: u64) -> u64 {
97    nanos_to_millis(nanos)
98}
99
100/// Converts nanoseconds (ns) to microseconds (μs).
101#[must_use]
102#[pyfunction(name = "nanos_to_micros")]
103#[gen_stub_pyfunction(module = "nautilus_trader.core")]
104pub const fn py_nanos_to_micros(nanos: u64) -> u64 {
105    nanos_to_micros(nanos)
106}
107
108/// Converts a UNIX nanoseconds timestamp to an ISO 8601 (RFC 3339) format string.
109///
110/// All `UnixNanos` values are representable by this formatter.
111#[pyfunction(
112    name = "unix_nanos_to_iso8601",
113    signature = (timestamp_ns, nanos_precision=Some(true))
114)]
115#[gen_stub_pyfunction(module = "nautilus_trader.core")]
116pub fn py_unix_nanos_to_iso8601(
117    timestamp_ns: u64,
118    nanos_precision: Option<bool>,
119) -> PyResult<String> {
120    let unix_nanos = UnixNanos::from(timestamp_ns);
121    let formatted = if nanos_precision.unwrap_or(true) {
122        unix_nanos_to_iso8601(unix_nanos)
123    } else {
124        unix_nanos_to_iso8601_millis(unix_nanos)
125    };
126
127    Ok(formatted)
128}
129
130/// Calculates the last weekday (Mon-Fri) from the given `year`, `month`, and `day`.
131///
132/// # Errors
133///
134/// Returns an error if the date is invalid.
135#[pyfunction(name = "last_weekday_nanos")]
136#[gen_stub_pyfunction(module = "nautilus_trader.core")]
137pub fn py_last_weekday_nanos(year: i32, month: u32, day: u32) -> PyResult<u64> {
138    Ok(last_weekday_nanos(year, month, day)
139        .map_err(to_pyvalue_err)?
140        .as_u64())
141}
142
143/// Check whether the given UNIX nanoseconds timestamp is within the last 24 hours.
144///
145/// # Errors
146///
147/// Returns an error if the timestamp is invalid.
148#[pyfunction(name = "is_within_last_24_hours")]
149#[gen_stub_pyfunction(module = "nautilus_trader.core")]
150pub fn py_is_within_last_24_hours(timestamp_ns: u64) -> PyResult<bool> {
151    is_within_last_24_hours(UnixNanos::from(timestamp_ns)).map_err(to_pyvalue_err)
152}
153
154#[cfg(test)]
155mod tests {
156    use jiff::Timestamp;
157    use pyo3::ffi::c_str;
158    use rstest::rstest;
159
160    use super::*;
161
162    #[rstest]
163    #[case(Some(true), 30)]
164    #[case(Some(false), 24)]
165    fn test_py_unix_nanos_to_iso8601_supports_full_range(
166        #[case] nanos_precision: Option<bool>,
167        #[case] expected_len: usize,
168    ) {
169        let result = py_unix_nanos_to_iso8601(u64::MAX, nanos_precision);
170        assert_eq!(result.unwrap().len(), expected_len);
171    }
172
173    #[rstest]
174    fn test_py_unix_nanos_to_iso8601_formats_valid_timestamp() {
175        let output = py_unix_nanos_to_iso8601(0, Some(false)).unwrap();
176        assert_eq!(output, "1970-01-01T00:00:00.000Z");
177    }
178
179    #[rstest]
180    fn test_jiff_timestamp_python_input_accepts_aware_non_utc_datetime() {
181        Python::initialize();
182        Python::attach(|py| {
183            let datetime = py
184                .eval(
185                    c_str!(
186                        "__import__('datetime').datetime(2024, 1, 15, 13, 30, 45, tzinfo=__import__('datetime').timezone(__import__('datetime').timedelta(hours=-5)))"
187                    ),
188                    None,
189                    None,
190                )
191                .unwrap();
192
193            let timestamp = datetime.extract::<Timestamp>().unwrap();
194            assert_eq!(timestamp, "2024-01-15T18:30:45Z".parse().unwrap());
195        });
196    }
197
198    #[rstest]
199    fn test_jiff_timestamp_python_input_accepts_zoneinfo_datetime() {
200        Python::initialize();
201        Python::attach(|py| {
202            let datetime = py
203                .eval(
204                    c_str!(
205                        "__import__('datetime').datetime(2024, 1, 15, 13, 30, 45, tzinfo=__import__('zoneinfo').ZoneInfo('America/New_York'))"
206                    ),
207                    None,
208                    None,
209                )
210                .unwrap();
211
212            let timestamp = datetime.extract::<Timestamp>().unwrap();
213            assert_eq!(timestamp, "2024-01-15T18:30:45Z".parse().unwrap());
214        });
215    }
216
217    #[rstest]
218    fn test_jiff_timestamp_python_input_rejects_naive_datetime() {
219        Python::initialize();
220        Python::attach(|py| {
221            let datetime = py
222                .eval(
223                    c_str!("__import__('datetime').datetime(2024, 1, 15, 13, 30, 45)"),
224                    None,
225                    None,
226                )
227                .unwrap();
228
229            assert!(datetime.extract::<Timestamp>().is_err());
230        });
231    }
232}