ccxt_core/
time.rs

1//! Time utilities for CCXT
2//!
3//! This module provides time-related utility functions for timestamp conversion,
4//! date parsing, and formatting. All timestamps are in milliseconds since Unix epoch
5//! unless otherwise specified.
6//!
7//! # Key Features
8//!
9//! - Millisecond/second/microsecond timestamp generation
10//! - ISO 8601 date parsing and formatting
11//! - Multiple date format support
12//! - UTC timezone handling
13//!
14//! # Example
15//!
16//! ```rust
17//! use ccxt_core::time::{milliseconds, iso8601, parse_date};
18//!
19//! // Get current timestamp in milliseconds
20//! let now = milliseconds();
21//!
22//! // Convert timestamp to ISO 8601 string
23//! let iso_str = iso8601(now).unwrap();
24//!
25//! // Parse ISO 8601 string back to timestamp
26//! let parsed = parse_date(&iso_str).unwrap();
27//! assert_eq!(now, parsed);
28//! ```
29
30use crate::error::{ParseError, Result};
31use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
32
33/// Returns the current time in milliseconds since the Unix epoch
34///
35/// # Example
36///
37/// ```rust
38/// use ccxt_core::time::milliseconds;
39///
40/// let now = milliseconds();
41/// assert!(now > 0);
42/// ```
43#[inline]
44pub fn milliseconds() -> i64 {
45    Utc::now().timestamp_millis()
46}
47
48/// Returns the current time in seconds since the Unix epoch
49///
50/// # Example
51///
52/// ```rust
53/// use ccxt_core::time::seconds;
54///
55/// let now = seconds();
56/// assert!(now > 0);
57/// ```
58#[inline]
59pub fn seconds() -> i64 {
60    milliseconds() / 1000
61}
62
63/// Returns the current time in microseconds since the Unix epoch
64///
65/// # Example
66///
67/// ```rust
68/// use ccxt_core::time::microseconds;
69///
70/// let now = microseconds();
71/// assert!(now > 0);
72/// ```
73#[inline]
74pub fn microseconds() -> i64 {
75    Utc::now().timestamp_micros()
76}
77
78/// Converts a timestamp in milliseconds to an ISO 8601 formatted string
79///
80/// # Arguments
81///
82/// * `timestamp` - Timestamp in milliseconds since Unix epoch
83///
84/// # Returns
85///
86/// ISO 8601 formatted string in UTC timezone (e.g., "2024-01-01T12:00:00.000Z")
87///
88/// # Example
89///
90/// ```rust
91/// use ccxt_core::time::iso8601;
92///
93/// let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
94/// let iso_str = iso8601(timestamp).unwrap();
95/// assert_eq!(iso_str, "2024-01-01T12:00:00.000Z");
96/// ```
97pub fn iso8601(timestamp: i64) -> Result<String> {
98    if timestamp <= 0 {
99        return Err(ParseError::timestamp("Invalid timestamp: must be positive").into());
100    }
101
102    // Convert milliseconds to seconds and nanoseconds
103    let secs = timestamp / 1000;
104    let nsecs = ((timestamp % 1000) * 1_000_000) as u32;
105
106    let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
107        .ok_or_else(|| ParseError::timestamp_owned(format!("Invalid timestamp: {}", timestamp)))?;
108
109    Ok(datetime.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
110}
111
112/// Parses a date string and returns the timestamp in milliseconds since Unix epoch
113///
114/// Supports multiple date formats:
115/// - ISO 8601: "2024-01-01T12:00:00.000Z" or "2024-01-01T12:00:00Z"
116/// - Space-separated: "2024-01-01 12:00:00"
117/// - Without timezone: "2024-01-01T12:00:00.389"
118///
119/// # Arguments
120///
121/// * `datetime` - Date string in one of the supported formats
122///
123/// # Returns
124///
125/// Timestamp in milliseconds since Unix epoch
126///
127/// # Example
128///
129/// ```rust
130/// use ccxt_core::time::parse_date;
131///
132/// let ts1 = parse_date("2024-01-01T12:00:00.000Z").unwrap();
133/// let ts2 = parse_date("2024-01-01 12:00:00").unwrap();
134/// assert!(ts1 > 0);
135/// assert!(ts2 > 0);
136/// ```
137pub fn parse_date(datetime: &str) -> Result<i64> {
138    if datetime.is_empty() {
139        return Err(ParseError::timestamp("Empty datetime string").into());
140    }
141
142    // List of supported date formats
143    let formats = [
144        "%Y-%m-%d %H:%M:%S",      // "2024-01-01 12:00:00"
145        "%Y-%m-%dT%H:%M:%S%.3fZ", // "2024-01-01T12:00:00.000Z"
146        "%Y-%m-%dT%H:%M:%SZ",     // "2024-01-01T12:00:00Z"
147        "%Y-%m-%dT%H:%M:%S%.3f",  // "2024-01-01T12:00:00.389"
148        "%Y-%m-%dT%H:%M:%S",      // "2024-01-01T12:00:00"
149        "%Y-%m-%d %H:%M:%S%.3f",  // "2024-01-01 12:00:00.389"
150    ];
151
152    // Try parsing with each format
153    for format in &formats {
154        if let Ok(naive) = NaiveDateTime::parse_from_str(datetime, format) {
155            let dt = Utc.from_utc_datetime(&naive);
156            return Ok(dt.timestamp_millis());
157        }
158    }
159
160    // Try parsing as RFC3339 (handles timezone offsets)
161    if let Ok(dt) = DateTime::parse_from_rfc3339(datetime) {
162        return Ok(dt.timestamp_millis());
163    }
164
165    Err(ParseError::timestamp_owned(format!("Unable to parse datetime: {}", datetime)).into())
166}
167
168/// Parses an ISO 8601 date string and returns the timestamp in milliseconds
169///
170/// This function handles various ISO 8601 formats and strips timezone offsets
171/// like "+00:00" before parsing.
172///
173/// # Arguments
174///
175/// * `datetime` - ISO 8601 formatted date string
176///
177/// # Returns
178///
179/// Timestamp in milliseconds since Unix epoch
180///
181/// # Example
182///
183/// ```rust
184/// use ccxt_core::time::parse_iso8601;
185///
186/// let ts = parse_iso8601("2024-01-01T12:00:00.000Z").unwrap();
187/// assert!(ts > 0);
188///
189/// let ts2 = parse_iso8601("2024-01-01T12:00:00+00:00").unwrap();
190/// assert!(ts2 > 0);
191/// ```
192pub fn parse_iso8601(datetime: &str) -> Result<i64> {
193    if datetime.is_empty() {
194        return Err(ParseError::timestamp("Empty datetime string").into());
195    }
196
197    // Remove "+00:00" or similar timezone offsets if present
198    let cleaned = if datetime.contains("+0") {
199        datetime.split('+').next().unwrap_or(datetime)
200    } else {
201        datetime
202    };
203
204    // Try RFC3339 format first
205    if let Ok(dt) = DateTime::parse_from_rfc3339(cleaned) {
206        return Ok(dt.timestamp_millis());
207    }
208
209    // Try parsing without timezone
210    let formats = [
211        "%Y-%m-%dT%H:%M:%S%.3f", // "2024-01-01T12:00:00.389"
212        "%Y-%m-%d %H:%M:%S%.3f", // "2024-01-01 12:00:43.928"
213        "%Y-%m-%dT%H:%M:%S",     // "2024-01-01T12:00:00"
214        "%Y-%m-%d %H:%M:%S",     // "2024-01-01 12:00:00"
215    ];
216
217    for format in &formats {
218        if let Ok(naive) = NaiveDateTime::parse_from_str(cleaned, format) {
219            let dt = Utc.from_utc_datetime(&naive);
220            return Ok(dt.timestamp_millis());
221        }
222    }
223
224    Err(
225        ParseError::timestamp_owned(format!("Unable to parse ISO 8601 datetime: {}", datetime))
226            .into(),
227    )
228}
229
230/// Formats a timestamp as "yyyy-MM-dd HH:mm:ss"
231///
232/// # Arguments
233///
234/// * `timestamp` - Timestamp in milliseconds since Unix epoch
235/// * `separator` - Optional separator between date and time (defaults to " ")
236///
237/// # Returns
238///
239/// Formatted date string
240///
241/// # Example
242///
243/// ```rust
244/// use ccxt_core::time::ymdhms;
245///
246/// let ts = 1704110400000; // 2024-01-01 12:00:00 UTC
247/// let formatted = ymdhms(ts, None).unwrap();
248/// assert_eq!(formatted, "2024-01-01 12:00:00");
249///
250/// let formatted_t = ymdhms(ts, Some("T")).unwrap();
251/// assert_eq!(formatted_t, "2024-01-01T12:00:00");
252/// ```
253pub fn ymdhms(timestamp: i64, separator: Option<&str>) -> Result<String> {
254    if timestamp <= 0 {
255        return Err(ParseError::timestamp("Invalid timestamp: must be positive").into());
256    }
257
258    let sep = separator.unwrap_or(" ");
259    let secs = timestamp / 1000;
260    let nsecs = ((timestamp % 1000) * 1_000_000) as u32;
261
262    let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
263        .ok_or_else(|| ParseError::timestamp_owned(format!("Invalid timestamp: {}", timestamp)))?;
264
265    Ok(format!(
266        "{}{}{}",
267        datetime.format("%Y-%m-%d"),
268        sep,
269        datetime.format("%H:%M:%S")
270    ))
271}
272
273/// Formats a timestamp as "yyyy-MM-dd"
274///
275/// # Arguments
276///
277/// * `timestamp` - Timestamp in milliseconds since Unix epoch
278/// * `separator` - Optional separator between year, month, day (defaults to "-")
279///
280/// # Example
281///
282/// ```rust
283/// use ccxt_core::time::yyyymmdd;
284///
285/// let ts = 1704110400000;
286/// let formatted = yyyymmdd(ts, None).unwrap();
287/// assert_eq!(formatted, "2024-01-01");
288///
289/// let formatted_slash = yyyymmdd(ts, Some("/")).unwrap();
290/// assert_eq!(formatted_slash, "2024/01/01");
291/// ```
292pub fn yyyymmdd(timestamp: i64, separator: Option<&str>) -> Result<String> {
293    if timestamp <= 0 {
294        return Err(ParseError::timestamp("Invalid timestamp: must be positive").into());
295    }
296
297    let sep = separator.unwrap_or("-");
298    let secs = timestamp / 1000;
299    let nsecs = ((timestamp % 1000) * 1_000_000) as u32;
300
301    let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
302        .ok_or_else(|| ParseError::timestamp_owned(format!("Invalid timestamp: {}", timestamp)))?;
303
304    Ok(format!(
305        "{}{}{}{}{}",
306        datetime.format("%Y"),
307        sep,
308        datetime.format("%m"),
309        sep,
310        datetime.format("%d")
311    ))
312}
313
314/// Formats a timestamp as "yy-MM-dd"
315///
316/// # Arguments
317///
318/// * `timestamp` - Timestamp in milliseconds since Unix epoch
319/// * `separator` - Optional separator between year, month, day (defaults to "")
320///
321/// # Example
322///
323/// ```rust
324/// use ccxt_core::time::yymmdd;
325///
326/// let ts = 1704110400000;
327/// let formatted = yymmdd(ts, None).unwrap();
328/// assert_eq!(formatted, "240101");
329///
330/// let formatted_dash = yymmdd(ts, Some("-")).unwrap();
331/// assert_eq!(formatted_dash, "24-01-01");
332/// ```
333pub fn yymmdd(timestamp: i64, separator: Option<&str>) -> Result<String> {
334    if timestamp <= 0 {
335        return Err(ParseError::timestamp("Invalid timestamp: must be positive").into());
336    }
337
338    let sep = separator.unwrap_or("");
339    let secs = timestamp / 1000;
340    let nsecs = ((timestamp % 1000) * 1_000_000) as u32;
341
342    let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
343        .ok_or_else(|| ParseError::timestamp_owned(format!("Invalid timestamp: {}", timestamp)))?;
344
345    Ok(format!(
346        "{}{}{}{}{}",
347        datetime.format("%y"),
348        sep,
349        datetime.format("%m"),
350        sep,
351        datetime.format("%d")
352    ))
353}
354
355/// Alias for `yyyymmdd` function
356///
357/// # Example
358///
359/// ```rust
360/// use ccxt_core::time::ymd;
361///
362/// let ts = 1704110400000;
363/// let formatted = ymd(ts, None).unwrap();
364/// assert_eq!(formatted, "2024-01-01");
365/// ```
366#[inline]
367pub fn ymd(timestamp: i64, separator: Option<&str>) -> Result<String> {
368    yyyymmdd(timestamp, separator)
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_milliseconds() {
377        let now = milliseconds();
378        assert!(now > 1_600_000_000_000); // After 2020
379        assert!(now < 2_000_000_000_000); // Before 2033
380    }
381
382    #[test]
383    fn test_seconds() {
384        let now = seconds();
385        assert!(now > 1_600_000_000); // After 2020
386        assert!(now < 2_000_000_000); // Before 2033
387    }
388
389    #[test]
390    fn test_microseconds() {
391        let now = microseconds();
392        assert!(now > 1_600_000_000_000_000); // After 2020
393    }
394
395    #[test]
396    fn test_iso8601() {
397        let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
398        let result = iso8601(timestamp).unwrap();
399        assert_eq!(result, "2024-01-01T12:00:00.000Z");
400    }
401
402    #[test]
403    fn test_iso8601_with_millis() {
404        let timestamp = 1704110400123; // 2024-01-01 12:00:00.123 UTC
405        let result = iso8601(timestamp).unwrap();
406        assert_eq!(result, "2024-01-01T12:00:00.123Z");
407    }
408
409    #[test]
410    fn test_iso8601_invalid() {
411        let result = iso8601(-1);
412        assert!(result.is_err());
413    }
414
415    #[test]
416    fn test_parse_date_iso8601() {
417        let result = parse_date("2024-01-01T12:00:00.000Z").unwrap();
418        assert_eq!(result, 1704110400000);
419    }
420
421    #[test]
422    fn test_parse_date_space_separated() {
423        let result = parse_date("2024-01-01 12:00:00").unwrap();
424        assert_eq!(result, 1704110400000);
425    }
426
427    #[test]
428    fn test_parse_date_without_timezone() {
429        let result = parse_date("2024-01-01T12:00:00.389").unwrap();
430        assert_eq!(result, 1704110400389);
431    }
432
433    #[test]
434    fn test_parse_iso8601() {
435        let result = parse_iso8601("2024-01-01T12:00:00.000Z").unwrap();
436        assert_eq!(result, 1704110400000);
437    }
438
439    #[test]
440    fn test_parse_iso8601_with_offset() {
441        let result = parse_iso8601("2024-01-01T12:00:00+00:00").unwrap();
442        assert_eq!(result, 1704110400000);
443    }
444
445    #[test]
446    fn test_parse_iso8601_space_separated() {
447        let result = parse_iso8601("2024-01-01 12:00:00.389").unwrap();
448        assert_eq!(result, 1704110400389);
449    }
450
451    #[test]
452    fn test_ymdhms_default_separator() {
453        let timestamp = 1704110400000;
454        let result = ymdhms(timestamp, None).unwrap();
455        assert_eq!(result, "2024-01-01 12:00:00");
456    }
457
458    #[test]
459    fn test_ymdhms_custom_separator() {
460        let timestamp = 1704110400000;
461        let result = ymdhms(timestamp, Some("T")).unwrap();
462        assert_eq!(result, "2024-01-01T12:00:00");
463    }
464
465    #[test]
466    fn test_yyyymmdd_default_separator() {
467        let timestamp = 1704110400000;
468        let result = yyyymmdd(timestamp, None).unwrap();
469        assert_eq!(result, "2024-01-01");
470    }
471
472    #[test]
473    fn test_yyyymmdd_custom_separator() {
474        let timestamp = 1704110400000;
475        let result = yyyymmdd(timestamp, Some("/")).unwrap();
476        assert_eq!(result, "2024/01/01");
477    }
478
479    #[test]
480    fn test_yymmdd_no_separator() {
481        let timestamp = 1704110400000;
482        let result = yymmdd(timestamp, None).unwrap();
483        assert_eq!(result, "240101");
484    }
485
486    #[test]
487    fn test_yymmdd_with_separator() {
488        let timestamp = 1704110400000;
489        let result = yymmdd(timestamp, Some("-")).unwrap();
490        assert_eq!(result, "24-01-01");
491    }
492
493    #[test]
494    fn test_ymd_alias() {
495        let timestamp = 1704110400000;
496        let result = ymd(timestamp, None).unwrap();
497        assert_eq!(result, "2024-01-01");
498    }
499
500    #[test]
501    fn test_round_trip() {
502        let original = 1704110400000;
503        let iso_str = iso8601(original).unwrap();
504        let parsed = parse_date(&iso_str).unwrap();
505        assert_eq!(original, parsed);
506    }
507}