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 exceeds `MAX_SECS_FOR_NANOS`.
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 exceeds `MAX_SECS_FOR_MILLIS`.
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 exceeds `MAX_MILLIS_FOR_NANOS`.
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 exceeds `MAX_MICROS_FOR_NANOS`.
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/// Returns the raw nanosecond value as a string if it exceeds the representable
111/// datetime range (`i64::MAX`, approximately year 2262).
112#[pyfunction(
113    name = "unix_nanos_to_iso8601",
114    signature = (timestamp_ns, nanos_precision=Some(true))
115)]
116#[gen_stub_pyfunction(module = "nautilus_trader.core")]
117pub fn py_unix_nanos_to_iso8601(
118    timestamp_ns: u64,
119    nanos_precision: Option<bool>,
120) -> PyResult<String> {
121    if timestamp_ns > i64::MAX as u64 {
122        return Err(to_pyvalue_err(
123            "timestamp_ns is out of range for conversion",
124        ));
125    }
126
127    let unix_nanos = UnixNanos::from(timestamp_ns);
128    let formatted = if nanos_precision.unwrap_or(true) {
129        unix_nanos_to_iso8601(unix_nanos)
130    } else {
131        unix_nanos_to_iso8601_millis(unix_nanos)
132    };
133
134    Ok(formatted)
135}
136
137/// Calculates the last weekday (Mon-Fri) from the given `year`, `month` and `day`.
138///
139/// # Errors
140///
141/// Returns an error if the date is invalid.
142#[pyfunction(name = "last_weekday_nanos")]
143#[gen_stub_pyfunction(module = "nautilus_trader.core")]
144pub fn py_last_weekday_nanos(year: i32, month: u32, day: u32) -> PyResult<u64> {
145    Ok(last_weekday_nanos(year, month, day)
146        .map_err(to_pyvalue_err)?
147        .as_u64())
148}
149
150/// Check whether the given UNIX nanoseconds timestamp is within the last 24 hours.
151///
152/// # Errors
153///
154/// Returns an error if the timestamp is invalid.
155#[pyfunction(name = "is_within_last_24_hours")]
156#[gen_stub_pyfunction(module = "nautilus_trader.core")]
157pub fn py_is_within_last_24_hours(timestamp_ns: u64) -> PyResult<bool> {
158    is_within_last_24_hours(UnixNanos::from(timestamp_ns)).map_err(to_pyvalue_err)
159}
160
161#[cfg(test)]
162mod tests {
163    use rstest::rstest;
164
165    use super::*;
166
167    #[rstest]
168    fn test_py_unix_nanos_to_iso8601_errors_on_out_of_range_timestamp() {
169        let result = py_unix_nanos_to_iso8601((i64::MAX as u64) + 1, Some(true));
170        assert!(result.is_err());
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}