ccxt_core/
time.rs

1//! Time utilities for CCXT
2//!
3//! This module provides comprehensive time-related utility functions for timestamp conversion,
4//! date parsing, formatting, and validation. All timestamps are standardized to use `i64` type
5//! representing milliseconds since Unix epoch unless otherwise specified.
6//!
7//! # Key Features
8//!
9//! - **Standardized i64 timestamps**: All timestamps use `i64` for consistency
10//! - **Comprehensive validation**: Range checking and overflow protection
11//! - **Migration support**: Conversion utilities for u64 to i64 migration
12//! - **Multiple format support**: ISO 8601, space-separated, and custom formats
13//! - **UTC timezone handling**: All operations in UTC for consistency
14//! - **Error handling**: Proper error types for all failure modes
15//!
16//! # Timestamp Format Standard
17//!
18//! - **Type**: `i64`
19//! - **Unit**: Milliseconds since Unix Epoch (January 1, 1970, 00:00:00 UTC)
20//! - **Range**: 0 to ~292,277,026,596 (year ~294,276)
21//! - **Validation**: Must be >= 0 for valid Unix timestamps
22//!
23//! # Example
24//!
25//! ```rust
26//! use ccxt_core::time::{TimestampUtils, milliseconds, iso8601, parse_date};
27//!
28//! // Get current timestamp in milliseconds
29//! let now = milliseconds();
30//!
31//! // Validate timestamp
32//! let validated = TimestampUtils::validate_timestamp(now).unwrap();
33//!
34//! // Convert timestamp to ISO 8601 string
35//! let iso_str = iso8601(validated).unwrap();
36//!
37//! // Parse ISO 8601 string back to timestamp
38//! let parsed = parse_date(&iso_str).unwrap();
39//! assert_eq!(validated, parsed);
40//!
41//! // Migration support: convert u64 to i64
42//! let old_timestamp: u64 = 1704110400000;
43//! let new_timestamp = TimestampUtils::u64_to_i64(old_timestamp).unwrap();
44//! ```
45
46use crate::error::{Error, ParseError, Result};
47use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
48use std::time::{SystemTime, UNIX_EPOCH};
49use tracing::warn;
50
51/// Returns the current time in milliseconds since the Unix epoch
52///
53/// This function is now a wrapper around `TimestampUtils::now_ms()` for consistency.
54/// Consider using `TimestampUtils::now_ms()` directly for new code.
55///
56/// # Example
57///
58/// ```rust
59/// use ccxt_core::time::milliseconds;
60///
61/// let now = milliseconds();
62/// assert!(now > 0);
63/// ```
64#[inline]
65pub fn milliseconds() -> i64 {
66    TimestampUtils::now_ms()
67}
68
69/// Returns the current time in seconds since the Unix epoch
70///
71/// This function is now a wrapper around `TimestampUtils` for consistency.
72/// Consider using `TimestampUtils::ms_to_seconds(TimestampUtils::now_ms())` for new code.
73///
74/// # Example
75///
76/// ```rust
77/// use ccxt_core::time::seconds;
78///
79/// let now = seconds();
80/// assert!(now > 0);
81/// ```
82#[inline]
83pub fn seconds() -> i64 {
84    TimestampUtils::ms_to_seconds(TimestampUtils::now_ms())
85}
86
87/// Returns the current time in microseconds since the Unix epoch
88///
89/// # Example
90///
91/// ```rust
92/// use ccxt_core::time::microseconds;
93///
94/// let now = microseconds();
95/// assert!(now > 0);
96/// ```
97#[inline]
98pub fn microseconds() -> i64 {
99    Utc::now().timestamp_micros()
100}
101
102/// Converts a timestamp in milliseconds to an ISO 8601 formatted string
103///
104/// This function now uses `TimestampUtils::format_iso8601()` internally for consistency.
105/// Consider using `TimestampUtils::format_iso8601()` directly for new code.
106///
107/// # Arguments
108///
109/// * `timestamp` - Timestamp in milliseconds since Unix epoch
110///
111/// # Returns
112///
113/// ISO 8601 formatted string in UTC timezone (e.g., "2024-01-01T12:00:00.000Z")
114///
115/// # Example
116///
117/// ```rust
118/// use ccxt_core::time::iso8601;
119///
120/// let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
121/// let iso_str = iso8601(timestamp).unwrap();
122/// assert_eq!(iso_str, "2024-01-01T12:00:00.000Z");
123/// ```
124pub fn iso8601(timestamp: i64) -> Result<String> {
125    TimestampUtils::format_iso8601(timestamp)
126}
127
128/// Parses a date string and returns the timestamp in milliseconds since Unix epoch
129///
130/// Supports multiple date formats:
131/// - ISO 8601: "2024-01-01T12:00:00.000Z" or "2024-01-01T12:00:00Z"
132/// - Space-separated: "2024-01-01 12:00:00"
133/// - Without timezone: "2024-01-01T12:00:00.389"
134///
135/// # Arguments
136///
137/// * `datetime` - Date string in one of the supported formats
138///
139/// # Returns
140///
141/// Timestamp in milliseconds since Unix epoch
142///
143/// # Example
144///
145/// ```rust
146/// use ccxt_core::time::parse_date;
147///
148/// let ts1 = parse_date("2024-01-01T12:00:00.000Z").unwrap();
149/// let ts2 = parse_date("2024-01-01 12:00:00").unwrap();
150/// assert!(ts1 > 0);
151/// assert!(ts2 > 0);
152/// ```
153pub fn parse_date(datetime: &str) -> Result<i64> {
154    if datetime.is_empty() {
155        return Err(ParseError::timestamp("Empty datetime string").into());
156    }
157
158    // List of supported date formats
159    let formats = [
160        "%Y-%m-%d %H:%M:%S",      // "2024-01-01 12:00:00"
161        "%Y-%m-%dT%H:%M:%S%.3fZ", // "2024-01-01T12:00:00.000Z"
162        "%Y-%m-%dT%H:%M:%SZ",     // "2024-01-01T12:00:00Z"
163        "%Y-%m-%dT%H:%M:%S%.3f",  // "2024-01-01T12:00:00.389"
164        "%Y-%m-%dT%H:%M:%S",      // "2024-01-01T12:00:00"
165        "%Y-%m-%d %H:%M:%S%.3f",  // "2024-01-01 12:00:00.389"
166    ];
167
168    // Try parsing with each format
169    for format in &formats {
170        if let Ok(naive) = NaiveDateTime::parse_from_str(datetime, format) {
171            let dt = Utc.from_utc_datetime(&naive);
172            return Ok(dt.timestamp_millis());
173        }
174    }
175
176    // Try parsing as RFC3339 (handles timezone offsets)
177    if let Ok(dt) = DateTime::parse_from_rfc3339(datetime) {
178        return Ok(dt.timestamp_millis());
179    }
180
181    Err(ParseError::timestamp_owned(format!("Unable to parse datetime: {datetime}")).into())
182}
183
184/// Parses an ISO 8601 date string and returns the timestamp in milliseconds
185///
186/// This function handles various ISO 8601 formats and strips timezone offsets
187/// like "+00:00" before parsing.
188///
189/// # Arguments
190///
191/// * `datetime` - ISO 8601 formatted date string
192///
193/// # Returns
194///
195/// Timestamp in milliseconds since Unix epoch
196///
197/// # Example
198///
199/// ```rust
200/// use ccxt_core::time::parse_iso8601;
201///
202/// let ts = parse_iso8601("2024-01-01T12:00:00.000Z").unwrap();
203/// assert!(ts > 0);
204///
205/// let ts2 = parse_iso8601("2024-01-01T12:00:00+00:00").unwrap();
206/// assert!(ts2 > 0);
207/// ```
208pub fn parse_iso8601(datetime: &str) -> Result<i64> {
209    if datetime.is_empty() {
210        return Err(ParseError::timestamp("Empty datetime string").into());
211    }
212
213    // Remove "+00:00" or similar timezone offsets if present
214    let cleaned = if datetime.contains("+0") {
215        datetime.split('+').next().unwrap_or(datetime)
216    } else {
217        datetime
218    };
219
220    // Try RFC3339 format first
221    if let Ok(dt) = DateTime::parse_from_rfc3339(cleaned) {
222        return Ok(dt.timestamp_millis());
223    }
224
225    // Try parsing without timezone
226    let formats = [
227        "%Y-%m-%dT%H:%M:%S%.3f", // "2024-01-01T12:00:00.389"
228        "%Y-%m-%d %H:%M:%S%.3f", // "2024-01-01 12:00:43.928"
229        "%Y-%m-%dT%H:%M:%S",     // "2024-01-01T12:00:00"
230        "%Y-%m-%d %H:%M:%S",     // "2024-01-01 12:00:00"
231    ];
232
233    for format in &formats {
234        if let Ok(naive) = NaiveDateTime::parse_from_str(cleaned, format) {
235            let dt = Utc.from_utc_datetime(&naive);
236            return Ok(dt.timestamp_millis());
237        }
238    }
239
240    Err(
241        ParseError::timestamp_owned(format!("Unable to parse ISO 8601 datetime: {datetime}"))
242            .into(),
243    )
244}
245
246/// Formats a timestamp as "yyyy-MM-dd HH:mm:ss"
247///
248/// This function now includes validation using `TimestampUtils::validate_timestamp()`.
249///
250/// # Arguments
251///
252/// * `timestamp` - Timestamp in milliseconds since Unix epoch
253/// * `separator` - Optional separator between date and time (defaults to " ")
254///
255/// # Returns
256///
257/// Formatted date string
258///
259/// # Example
260///
261/// ```rust
262/// use ccxt_core::time::ymdhms;
263///
264/// let ts = 1704110400000; // 2024-01-01 12:00:00 UTC
265/// let formatted = ymdhms(ts, None).unwrap();
266/// assert_eq!(formatted, "2024-01-01 12:00:00");
267///
268/// let formatted_t = ymdhms(ts, Some("T")).unwrap();
269/// assert_eq!(formatted_t, "2024-01-01T12:00:00");
270/// ```
271pub fn ymdhms(timestamp: i64, separator: Option<&str>) -> Result<String> {
272    let validated = TimestampUtils::validate_timestamp(timestamp)?;
273
274    let sep = separator.unwrap_or(" ");
275    let secs = validated / 1000;
276    #[allow(clippy::cast_possible_truncation)]
277    let nsecs = ((validated % 1000) * 1_000_000) as u32;
278
279    let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
280        .ok_or_else(|| Error::invalid_request(format!("Invalid timestamp: {validated}")))?;
281
282    Ok(format!(
283        "{}{}{}",
284        datetime.format("%Y-%m-%d"),
285        sep,
286        datetime.format("%H:%M:%S")
287    ))
288}
289
290/// Formats a timestamp as "yyyy-MM-dd"
291///
292/// This function now includes validation using `TimestampUtils::validate_timestamp()`.
293///
294/// # Arguments
295///
296/// * `timestamp` - Timestamp in milliseconds since Unix epoch
297/// * `separator` - Optional separator between year, month, day (defaults to "-")
298///
299/// # Example
300///
301/// ```rust
302/// use ccxt_core::time::yyyymmdd;
303///
304/// let ts = 1704110400000;
305/// let formatted = yyyymmdd(ts, None).unwrap();
306/// assert_eq!(formatted, "2024-01-01");
307///
308/// let formatted_slash = yyyymmdd(ts, Some("/")).unwrap();
309/// assert_eq!(formatted_slash, "2024/01/01");
310/// ```
311pub fn yyyymmdd(timestamp: i64, separator: Option<&str>) -> Result<String> {
312    let validated = TimestampUtils::validate_timestamp(timestamp)?;
313
314    let sep = separator.unwrap_or("-");
315    let secs = validated / 1000;
316    #[allow(clippy::cast_possible_truncation)]
317    let nsecs = ((validated % 1000) * 1_000_000) as u32;
318
319    let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
320        .ok_or_else(|| Error::invalid_request(format!("Invalid timestamp: {validated}")))?;
321
322    Ok(format!(
323        "{}{}{}{}{}",
324        datetime.format("%Y"),
325        sep,
326        datetime.format("%m"),
327        sep,
328        datetime.format("%d")
329    ))
330}
331
332/// Formats a timestamp as "yy-MM-dd"
333///
334/// This function now includes validation using `TimestampUtils::validate_timestamp()`.
335///
336/// # Arguments
337///
338/// * `timestamp` - Timestamp in milliseconds since Unix epoch
339/// * `separator` - Optional separator between year, month, day (defaults to "")
340///
341/// # Example
342///
343/// ```rust
344/// use ccxt_core::time::yymmdd;
345///
346/// let ts = 1704110400000;
347/// let formatted = yymmdd(ts, None).unwrap();
348/// assert_eq!(formatted, "240101");
349///
350/// let formatted_dash = yymmdd(ts, Some("-")).unwrap();
351/// assert_eq!(formatted_dash, "24-01-01");
352/// ```
353pub fn yymmdd(timestamp: i64, separator: Option<&str>) -> Result<String> {
354    let validated = TimestampUtils::validate_timestamp(timestamp)?;
355
356    let sep = separator.unwrap_or("");
357    let secs = validated / 1000;
358    #[allow(clippy::cast_possible_truncation)]
359    let nsecs = ((validated % 1000) * 1_000_000) as u32;
360
361    let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
362        .ok_or_else(|| Error::invalid_request(format!("Invalid timestamp: {validated}")))?;
363
364    Ok(format!(
365        "{}{}{}{}{}",
366        datetime.format("%y"),
367        sep,
368        datetime.format("%m"),
369        sep,
370        datetime.format("%d")
371    ))
372}
373
374/// Alias for `yyyymmdd` function
375///
376/// # Example
377///
378/// ```rust
379/// use ccxt_core::time::ymd;
380///
381/// let ts = 1704110400000;
382/// let formatted = ymd(ts, None).unwrap();
383/// assert_eq!(formatted, "2024-01-01");
384/// ```
385#[inline]
386pub fn ymd(timestamp: i64, separator: Option<&str>) -> Result<String> {
387    yyyymmdd(timestamp, separator)
388}
389
390/// Comprehensive timestamp utilities for consistent i64 handling
391///
392/// This struct provides a collection of utility functions for working with i64 timestamps,
393/// including validation, conversion, and formatting operations. All functions are designed
394/// to work with milliseconds since Unix epoch.
395///
396/// # Design Principles
397///
398/// - **Type Safety**: All operations use i64 for consistency
399/// - **Validation**: Range checking prevents invalid timestamps
400/// - **Migration Support**: Conversion utilities for u64 to i64 migration
401/// - **Error Handling**: Proper error types for all failure modes
402///
403/// # Example
404///
405/// ```rust
406/// use ccxt_core::time::TimestampUtils;
407///
408/// // Get current timestamp
409/// let now = TimestampUtils::now_ms();
410///
411/// // Validate timestamp
412/// let validated = TimestampUtils::validate_timestamp(now).unwrap();
413///
414/// // Convert units
415/// let seconds = TimestampUtils::ms_to_seconds(validated);
416/// let back_to_ms = TimestampUtils::seconds_to_ms(seconds);
417/// // Note: conversion to seconds loses millisecond precision
418/// assert!(validated - back_to_ms < 1000);
419/// ```
420pub struct TimestampUtils;
421
422impl TimestampUtils {
423    /// Maximum reasonable timestamp (year 2100) to prevent far-future timestamps
424    pub const YEAR_2100_MS: i64 = 4_102_444_800_000;
425
426    /// Minimum reasonable timestamp (year 1970) - Unix epoch start
427    pub const UNIX_EPOCH_MS: i64 = 0;
428
429    /// Get current timestamp in milliseconds as i64
430    ///
431    /// Returns the current system time as milliseconds since Unix epoch.
432    /// This is the primary function for getting current timestamps in the library.
433    ///
434    /// # Example
435    ///
436    /// ```rust
437    /// use ccxt_core::time::TimestampUtils;
438    ///
439    /// let now = TimestampUtils::now_ms();
440    /// assert!(now > 1_600_000_000_000); // After 2020
441    /// ```
442    pub fn now_ms() -> i64 {
443        // SAFETY: SystemTime::now().duration_since(UNIX_EPOCH) can fail if the system
444        // clock is set to a time before January 1, 1970 (Unix epoch).
445        // Instead of panicking, we return the negative timestamp to avoid crashing.
446        #[allow(clippy::cast_possible_truncation)]
447        match SystemTime::now().duration_since(UNIX_EPOCH) {
448            Ok(duration) => duration.as_millis() as i64,
449            Err(e) => {
450                warn!("System clock is set before UNIX_EPOCH (1970); returning negative timestamp");
451                // e.duration() returns the difference, so we negate it
452                -(e.duration().as_millis() as i64)
453            }
454        }
455    }
456
457    /// Convert seconds to milliseconds
458    ///
459    /// # Arguments
460    ///
461    /// * `seconds` - Timestamp in seconds since Unix epoch
462    ///
463    /// # Returns
464    ///
465    /// Timestamp in milliseconds since Unix epoch
466    ///
467    /// # Example
468    ///
469    /// ```rust
470    /// use ccxt_core::time::TimestampUtils;
471    ///
472    /// let seconds = 1704110400; // 2024-01-01 12:00:00 UTC
473    /// let milliseconds = TimestampUtils::seconds_to_ms(seconds);
474    /// assert_eq!(milliseconds, 1704110400000);
475    /// ```
476    pub fn seconds_to_ms(seconds: i64) -> i64 {
477        seconds.saturating_mul(1000)
478    }
479
480    /// Convert milliseconds to seconds
481    ///
482    /// # Arguments
483    ///
484    /// * `ms` - Timestamp in milliseconds since Unix epoch
485    ///
486    /// # Returns
487    ///
488    /// Timestamp in seconds since Unix epoch
489    ///
490    /// # Example
491    ///
492    /// ```rust
493    /// use ccxt_core::time::TimestampUtils;
494    ///
495    /// let milliseconds = 1704110400000; // 2024-01-01 12:00:00 UTC
496    /// let seconds = TimestampUtils::ms_to_seconds(milliseconds);
497    /// assert_eq!(seconds, 1704110400);
498    /// ```
499    pub fn ms_to_seconds(ms: i64) -> i64 {
500        ms / 1000
501    }
502
503    /// Validate timestamp is within reasonable bounds
504    ///
505    /// Ensures the timestamp is:
506    /// - Not negative (valid Unix timestamp)
507    /// - Not too far in the future (before year 2100)
508    ///
509    /// # Arguments
510    ///
511    /// * `timestamp` - Timestamp in milliseconds to validate
512    ///
513    /// # Returns
514    ///
515    /// The validated timestamp if valid, or an error if invalid
516    ///
517    /// # Errors
518    ///
519    /// - `Error::InvalidRequest` if timestamp is negative
520    /// - `Error::InvalidRequest` if timestamp is too far in future
521    ///
522    /// # Example
523    ///
524    /// ```rust
525    /// use ccxt_core::time::TimestampUtils;
526    ///
527    /// // Valid timestamp
528    /// let valid = TimestampUtils::validate_timestamp(1704110400000).unwrap();
529    /// assert_eq!(valid, 1704110400000);
530    ///
531    /// // Invalid timestamp (negative)
532    /// let invalid = TimestampUtils::validate_timestamp(-1);
533    /// assert!(invalid.is_err());
534    /// ```
535    pub fn validate_timestamp(timestamp: i64) -> Result<i64> {
536        if timestamp < Self::UNIX_EPOCH_MS {
537            return Err(Error::invalid_request("Timestamp cannot be negative"));
538        }
539
540        if timestamp > Self::YEAR_2100_MS {
541            return Err(Error::invalid_request(
542                "Timestamp too far in future (after year 2100)",
543            ));
544        }
545
546        Ok(timestamp)
547    }
548
549    /// Parse timestamp from string (handles various formats)
550    ///
551    /// Attempts to parse a timestamp from string format, supporting:
552    /// - Integer strings (milliseconds): "1704110400000"
553    /// - Decimal strings (seconds with fractional part): "1704110400.123"
554    /// - Scientific notation: "1.7041104e12"
555    ///
556    /// # Arguments
557    ///
558    /// * `s` - String representation of timestamp
559    ///
560    /// # Returns
561    ///
562    /// Parsed and validated timestamp in milliseconds
563    ///
564    /// # Errors
565    ///
566    /// - `Error::Parse` if string cannot be parsed as number
567    /// - `Error::InvalidRequest` if parsed timestamp is invalid
568    ///
569    /// # Example
570    ///
571    /// ```rust
572    /// use ccxt_core::time::TimestampUtils;
573    ///
574    /// // Parse integer milliseconds
575    /// let ts1 = TimestampUtils::parse_timestamp("1704110400000").unwrap();
576    /// assert_eq!(ts1, 1704110400000);
577    ///
578    /// // Parse decimal seconds
579    /// let ts2 = TimestampUtils::parse_timestamp("1704110400.123").unwrap();
580    /// assert_eq!(ts2, 1704110400123);
581    /// ```
582    pub fn parse_timestamp(s: &str) -> Result<i64> {
583        if s.is_empty() {
584            return Err(Error::invalid_request("Empty timestamp string"));
585        }
586
587        // Try parsing as i64 first (milliseconds)
588        if let Ok(ts) = s.parse::<i64>() {
589            return Self::validate_timestamp(ts);
590        }
591
592        // Try parsing as f64 (seconds with fractional part)
593        if let Ok(ts_f64) = s.parse::<f64>()
594            && ts_f64.is_finite()
595        {
596            #[allow(clippy::cast_possible_truncation)]
597            let ts = (ts_f64 * 1000.0) as i64;
598            return Self::validate_timestamp(ts);
599        }
600
601        Err(Error::invalid_request(format!(
602            "Invalid timestamp format: {s}"
603        )))
604    }
605
606    /// Format timestamp as ISO 8601 string
607    ///
608    /// Converts a timestamp to ISO 8601 format with millisecond precision.
609    /// Always uses UTC timezone and includes milliseconds.
610    ///
611    /// # Arguments
612    ///
613    /// * `timestamp` - Timestamp in milliseconds since Unix epoch
614    ///
615    /// # Returns
616    ///
617    /// ISO 8601 formatted string (e.g., "2024-01-01T12:00:00.000Z")
618    ///
619    /// # Example
620    ///
621    /// ```rust
622    /// use ccxt_core::time::TimestampUtils;
623    ///
624    /// let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
625    /// let iso_str = TimestampUtils::format_iso8601(timestamp).unwrap();
626    /// assert_eq!(iso_str, "2024-01-01T12:00:00.000Z");
627    /// ```
628    pub fn format_iso8601(timestamp: i64) -> Result<String> {
629        let validated = Self::validate_timestamp(timestamp)?;
630
631        let secs = validated / 1000;
632        #[allow(clippy::cast_possible_truncation)]
633        let nsecs = ((validated % 1000) * 1_000_000) as u32;
634
635        let datetime = DateTime::<Utc>::from_timestamp(secs, nsecs)
636            .ok_or_else(|| Error::invalid_request(format!("Invalid timestamp: {validated}")))?;
637
638        Ok(datetime.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
639    }
640
641    /// Check if a timestamp represents a reasonable date
642    ///
643    /// Validates that the timestamp represents a date between 1970 and 2100,
644    /// which covers all reasonable use cases for cryptocurrency trading.
645    ///
646    /// # Arguments
647    ///
648    /// * `timestamp` - Timestamp in milliseconds to check
649    ///
650    /// # Returns
651    ///
652    /// `true` if timestamp is reasonable, `false` otherwise
653    ///
654    /// # Example
655    ///
656    /// ```rust
657    /// use ccxt_core::time::TimestampUtils;
658    ///
659    /// assert!(TimestampUtils::is_reasonable_timestamp(1704110400000)); // 2024
660    /// assert!(!TimestampUtils::is_reasonable_timestamp(-1)); // Before 1970
661    /// ```
662    pub fn is_reasonable_timestamp(timestamp: i64) -> bool {
663        (Self::UNIX_EPOCH_MS..=Self::YEAR_2100_MS).contains(&timestamp)
664    }
665
666    /// Convert u64 to i64 with overflow checking
667    ///
668    /// This function is provided for migration support when converting from
669    /// old u64 timestamp code to new i64 timestamp code. It performs overflow
670    /// checking to ensure the conversion is safe.
671    ///
672    /// # Arguments
673    ///
674    /// * `timestamp` - u64 timestamp to convert
675    ///
676    /// # Returns
677    ///
678    /// Converted i64 timestamp if within valid range
679    ///
680    /// # Errors
681    ///
682    /// - `Error::InvalidRequest` if u64 value exceeds i64::MAX
683    ///
684    /// # Example
685    ///
686    /// ```rust
687    /// use ccxt_core::time::TimestampUtils;
688    ///
689    /// let old_timestamp: u64 = 1704110400000;
690    /// let new_timestamp = TimestampUtils::u64_to_i64(old_timestamp).unwrap();
691    /// assert_eq!(new_timestamp, 1704110400000i64);
692    /// ```
693    #[deprecated(since = "0.1.0", note = "Use i64 timestamps directly")]
694    pub fn u64_to_i64(timestamp: u64) -> Result<i64> {
695        if timestamp > i64::MAX as u64 {
696            return Err(Error::invalid_request(format!(
697                "Timestamp overflow: {timestamp} exceeds maximum i64 value"
698            )));
699        }
700        let converted = timestamp as i64;
701        Self::validate_timestamp(converted)
702    }
703
704    /// Convert i64 to u64 with underflow checking
705    ///
706    /// This function is provided for backward compatibility when interfacing
707    /// with legacy code that expects u64 timestamps. It performs underflow
708    /// checking to ensure the conversion is safe.
709    ///
710    /// # Arguments
711    ///
712    /// * `timestamp` - i64 timestamp to convert
713    ///
714    /// # Returns
715    ///
716    /// Converted u64 timestamp if non-negative
717    ///
718    /// # Errors
719    ///
720    /// - `Error::InvalidRequest` if i64 value is negative
721    ///
722    /// # Example
723    ///
724    /// ```rust
725    /// use ccxt_core::time::TimestampUtils;
726    ///
727    /// let new_timestamp: i64 = 1704110400000;
728    /// let old_timestamp = TimestampUtils::i64_to_u64(new_timestamp).unwrap();
729    /// assert_eq!(old_timestamp, 1704110400000u64);
730    /// ```
731    pub fn i64_to_u64(timestamp: i64) -> Result<u64> {
732        if timestamp < 0 {
733            return Err(Error::invalid_request(
734                "Cannot convert negative timestamp to u64",
735            ));
736        }
737        Ok(timestamp as u64)
738    }
739
740    /// Get timestamp for start of day (00:00:00 UTC)
741    ///
742    /// Given a timestamp, returns the timestamp for the start of that day in UTC.
743    ///
744    /// # Arguments
745    ///
746    /// * `timestamp` - Any timestamp within the target day
747    ///
748    /// # Returns
749    ///
750    /// Timestamp for 00:00:00 UTC of the same day
751    ///
752    /// # Example
753    ///
754    /// ```rust
755    /// use ccxt_core::time::TimestampUtils;
756    ///
757    /// let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
758    /// let start_of_day = TimestampUtils::start_of_day(timestamp).unwrap();
759    /// // Should be 2024-01-01 00:00:00 UTC
760    /// ```
761    pub fn start_of_day(timestamp: i64) -> Result<i64> {
762        let validated = Self::validate_timestamp(timestamp)?;
763
764        let secs = validated / 1000;
765        let datetime = DateTime::<Utc>::from_timestamp(secs, 0)
766            .ok_or_else(|| Error::invalid_request(format!("Invalid timestamp: {validated}")))?;
767
768        let start_of_day = datetime
769            .date_naive()
770            .and_hms_opt(0, 0, 0)
771            .ok_or_else(|| Error::invalid_request("Failed to create start of day"))?;
772
773        let start_of_day_utc = Utc.from_utc_datetime(&start_of_day);
774        Ok(start_of_day_utc.timestamp_millis())
775    }
776
777    /// Get timestamp for end of day (23:59:59.999 UTC)
778    ///
779    /// Given a timestamp, returns the timestamp for the end of that day in UTC.
780    ///
781    /// # Arguments
782    ///
783    /// * `timestamp` - Any timestamp within the target day
784    ///
785    /// # Returns
786    ///
787    /// Timestamp for 23:59:59.999 UTC of the same day
788    ///
789    /// # Example
790    ///
791    /// ```rust
792    /// use ccxt_core::time::TimestampUtils;
793    ///
794    /// let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
795    /// let end_of_day = TimestampUtils::end_of_day(timestamp).unwrap();
796    /// // Should be 2024-01-01 23:59:59.999 UTC
797    /// ```
798    pub fn end_of_day(timestamp: i64) -> Result<i64> {
799        let validated = Self::validate_timestamp(timestamp)?;
800
801        let secs = validated / 1000;
802        let datetime = DateTime::<Utc>::from_timestamp(secs, 0)
803            .ok_or_else(|| Error::invalid_request(format!("Invalid timestamp: {validated}")))?;
804
805        let end_of_day = datetime
806            .date_naive()
807            .and_hms_milli_opt(23, 59, 59, 999)
808            .ok_or_else(|| Error::invalid_request("Failed to create end of day"))?;
809
810        let end_of_day_utc = Utc.from_utc_datetime(&end_of_day);
811        Ok(end_of_day_utc.timestamp_millis())
812    }
813}
814
815/// Extension trait for Option<u64> to Option<i64> conversion
816///
817/// This trait provides convenient methods for converting Option<u64> timestamps
818/// to Option<i64> timestamps during migration. It handles the conversion and
819/// validation in a single operation.
820///
821/// # Example
822///
823/// ```rust
824/// use ccxt_core::time::TimestampConversion;
825///
826/// let old_timestamp: Option<u64> = Some(1704110400000);
827/// let new_timestamp = old_timestamp.to_i64().unwrap();
828/// assert_eq!(new_timestamp, Some(1704110400000i64));
829///
830/// let none_timestamp: Option<u64> = None;
831/// let converted = none_timestamp.to_i64().unwrap();
832/// assert_eq!(converted, None);
833/// ```
834pub trait TimestampConversion {
835    /// Convert Option<u64> to Option<i64> with validation
836    fn to_i64(self) -> Result<Option<i64>>;
837}
838
839impl TimestampConversion for Option<u64> {
840    #[allow(deprecated)]
841    fn to_i64(self) -> Result<Option<i64>> {
842        match self {
843            Some(ts) => Ok(Some(TimestampUtils::u64_to_i64(ts)?)),
844            None => Ok(None),
845        }
846    }
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852
853    #[test]
854    fn test_milliseconds() {
855        let now = milliseconds();
856        assert!(now > 1_600_000_000_000); // After 2020
857        assert!(now < 2_000_000_000_000); // Before 2033
858    }
859
860    #[test]
861    fn test_seconds() {
862        let now = seconds();
863        assert!(now > 1_600_000_000); // After 2020
864        assert!(now < 2_000_000_000); // Before 2033
865    }
866
867    #[test]
868    fn test_microseconds() {
869        let now = microseconds();
870        assert!(now > 1_600_000_000_000_000); // After 2020
871    }
872
873    #[test]
874    fn test_iso8601() {
875        let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
876        let result = iso8601(timestamp).unwrap();
877        assert_eq!(result, "2024-01-01T12:00:00.000Z");
878    }
879
880    #[test]
881    fn test_iso8601_with_millis() {
882        let timestamp = 1704110400123; // 2024-01-01 12:00:00.123 UTC
883        let result = iso8601(timestamp).unwrap();
884        assert_eq!(result, "2024-01-01T12:00:00.123Z");
885    }
886
887    #[test]
888    fn test_iso8601_invalid() {
889        let result = iso8601(-1);
890        assert!(result.is_err());
891    }
892
893    #[test]
894    fn test_parse_date_iso8601() {
895        let result = parse_date("2024-01-01T12:00:00.000Z").unwrap();
896        assert_eq!(result, 1704110400000);
897    }
898
899    #[test]
900    fn test_parse_date_space_separated() {
901        let result = parse_date("2024-01-01 12:00:00").unwrap();
902        assert_eq!(result, 1704110400000);
903    }
904
905    #[test]
906    fn test_parse_date_without_timezone() {
907        let result = parse_date("2024-01-01T12:00:00.389").unwrap();
908        assert_eq!(result, 1704110400389);
909    }
910
911    #[test]
912    fn test_parse_iso8601() {
913        let result = parse_iso8601("2024-01-01T12:00:00.000Z").unwrap();
914        assert_eq!(result, 1704110400000);
915    }
916
917    #[test]
918    fn test_parse_iso8601_with_offset() {
919        let result = parse_iso8601("2024-01-01T12:00:00+00:00").unwrap();
920        assert_eq!(result, 1704110400000);
921    }
922
923    #[test]
924    fn test_parse_iso8601_space_separated() {
925        let result = parse_iso8601("2024-01-01 12:00:00.389").unwrap();
926        assert_eq!(result, 1704110400389);
927    }
928
929    #[test]
930    fn test_ymdhms_default_separator() {
931        let timestamp = 1704110400000;
932        let result = ymdhms(timestamp, None).unwrap();
933        assert_eq!(result, "2024-01-01 12:00:00");
934    }
935
936    #[test]
937    fn test_ymdhms_custom_separator() {
938        let timestamp = 1704110400000;
939        let result = ymdhms(timestamp, Some("T")).unwrap();
940        assert_eq!(result, "2024-01-01T12:00:00");
941    }
942
943    #[test]
944    fn test_yyyymmdd_default_separator() {
945        let timestamp = 1704110400000;
946        let result = yyyymmdd(timestamp, None).unwrap();
947        assert_eq!(result, "2024-01-01");
948    }
949
950    #[test]
951    fn test_yyyymmdd_custom_separator() {
952        let timestamp = 1704110400000;
953        let result = yyyymmdd(timestamp, Some("/")).unwrap();
954        assert_eq!(result, "2024/01/01");
955    }
956
957    #[test]
958    fn test_yymmdd_no_separator() {
959        let timestamp = 1704110400000;
960        let result = yymmdd(timestamp, None).unwrap();
961        assert_eq!(result, "240101");
962    }
963
964    #[test]
965    fn test_yymmdd_with_separator() {
966        let timestamp = 1704110400000;
967        let result = yymmdd(timestamp, Some("-")).unwrap();
968        assert_eq!(result, "24-01-01");
969    }
970
971    #[test]
972    fn test_ymd_alias() {
973        let timestamp = 1704110400000;
974        let result = ymd(timestamp, None).unwrap();
975        assert_eq!(result, "2024-01-01");
976    }
977
978    #[test]
979    fn test_round_trip() {
980        let original = 1704110400000;
981        let iso_str = iso8601(original).unwrap();
982        let parsed = parse_date(&iso_str).unwrap();
983        assert_eq!(original, parsed);
984    }
985
986    // ==================== TimestampUtils Tests ====================
987
988    #[test]
989    fn test_timestamp_utils_now_ms() {
990        let now = TimestampUtils::now_ms();
991        assert!(now > 1_600_000_000_000); // After 2020
992        assert!(now < 2_000_000_000_000); // Before 2033
993    }
994
995    #[test]
996    fn test_timestamp_utils_seconds_to_ms() {
997        let seconds = 1704110400; // 2024-01-01 12:00:00 UTC
998        let milliseconds = TimestampUtils::seconds_to_ms(seconds);
999        assert_eq!(milliseconds, 1704110400000);
1000    }
1001
1002    #[test]
1003    fn test_timestamp_utils_ms_to_seconds() {
1004        let milliseconds = 1704110400000; // 2024-01-01 12:00:00 UTC
1005        let seconds = TimestampUtils::ms_to_seconds(milliseconds);
1006        assert_eq!(seconds, 1704110400);
1007    }
1008
1009    #[test]
1010    fn test_timestamp_utils_validate_timestamp_valid() {
1011        let valid_timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
1012        let result = TimestampUtils::validate_timestamp(valid_timestamp).unwrap();
1013        assert_eq!(result, valid_timestamp);
1014    }
1015
1016    #[test]
1017    fn test_timestamp_utils_validate_timestamp_negative() {
1018        let result = TimestampUtils::validate_timestamp(-1);
1019        assert!(result.is_err());
1020        assert!(result.unwrap_err().to_string().contains("negative"));
1021    }
1022
1023    #[test]
1024    fn test_timestamp_utils_validate_timestamp_too_far_future() {
1025        let far_future = TimestampUtils::YEAR_2100_MS + 1;
1026        let result = TimestampUtils::validate_timestamp(far_future);
1027        assert!(result.is_err());
1028        assert!(
1029            result
1030                .unwrap_err()
1031                .to_string()
1032                .contains("too far in future")
1033        );
1034    }
1035
1036    #[test]
1037    fn test_timestamp_utils_parse_timestamp_integer() {
1038        let result = TimestampUtils::parse_timestamp("1704110400000").unwrap();
1039        assert_eq!(result, 1704110400000);
1040    }
1041
1042    #[test]
1043    fn test_timestamp_utils_parse_timestamp_decimal() {
1044        let result = TimestampUtils::parse_timestamp("1704110400.123").unwrap();
1045        assert_eq!(result, 1704110400123);
1046    }
1047
1048    #[test]
1049    fn test_timestamp_utils_parse_timestamp_invalid() {
1050        let result = TimestampUtils::parse_timestamp("invalid");
1051        assert!(result.is_err());
1052    }
1053
1054    #[test]
1055    fn test_timestamp_utils_parse_timestamp_empty() {
1056        let result = TimestampUtils::parse_timestamp("");
1057        assert!(result.is_err());
1058    }
1059
1060    #[test]
1061    fn test_timestamp_utils_format_iso8601() {
1062        let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
1063        let result = TimestampUtils::format_iso8601(timestamp).unwrap();
1064        assert_eq!(result, "2024-01-01T12:00:00.000Z");
1065    }
1066
1067    #[test]
1068    fn test_timestamp_utils_format_iso8601_with_millis() {
1069        let timestamp = 1704110400123; // 2024-01-01 12:00:00.123 UTC
1070        let result = TimestampUtils::format_iso8601(timestamp).unwrap();
1071        assert_eq!(result, "2024-01-01T12:00:00.123Z");
1072    }
1073
1074    #[test]
1075    fn test_timestamp_utils_is_reasonable_timestamp() {
1076        assert!(TimestampUtils::is_reasonable_timestamp(1704110400000)); // 2024
1077        assert!(TimestampUtils::is_reasonable_timestamp(0)); // Unix epoch
1078        assert!(!TimestampUtils::is_reasonable_timestamp(-1)); // Before epoch
1079        assert!(!TimestampUtils::is_reasonable_timestamp(
1080            TimestampUtils::YEAR_2100_MS + 1
1081        )); // Too far future
1082    }
1083
1084    #[test]
1085    #[allow(deprecated)]
1086    fn test_timestamp_utils_u64_to_i64_valid() {
1087        let old_timestamp: u64 = 1704110400000;
1088        let result = TimestampUtils::u64_to_i64(old_timestamp).unwrap();
1089        assert_eq!(result, 1704110400000i64);
1090    }
1091
1092    #[test]
1093    #[allow(deprecated)]
1094    fn test_timestamp_utils_u64_to_i64_overflow() {
1095        let overflow_timestamp = u64::MAX;
1096        let result = TimestampUtils::u64_to_i64(overflow_timestamp);
1097        assert!(result.is_err());
1098        assert!(result.unwrap_err().to_string().contains("overflow"));
1099    }
1100
1101    #[test]
1102    #[allow(deprecated)]
1103    fn test_timestamp_utils_i64_to_u64_valid() {
1104        let new_timestamp: i64 = 1704110400000;
1105        let result = TimestampUtils::i64_to_u64(new_timestamp).unwrap();
1106        assert_eq!(result, 1704110400000u64);
1107    }
1108
1109    #[test]
1110    #[allow(deprecated)]
1111    fn test_timestamp_utils_i64_to_u64_negative() {
1112        let negative_timestamp: i64 = -1;
1113        let result = TimestampUtils::i64_to_u64(negative_timestamp);
1114        assert!(result.is_err());
1115        assert!(result.unwrap_err().to_string().contains("negative"));
1116    }
1117
1118    #[test]
1119    fn test_timestamp_utils_start_of_day() {
1120        let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
1121        let start_of_day = TimestampUtils::start_of_day(timestamp).unwrap();
1122
1123        // Should be 2024-01-01 00:00:00 UTC = 1704067200000
1124        let expected = 1704067200000;
1125        assert_eq!(start_of_day, expected);
1126    }
1127
1128    #[test]
1129    fn test_timestamp_utils_end_of_day() {
1130        let timestamp = 1704110400000; // 2024-01-01 12:00:00 UTC
1131        let end_of_day = TimestampUtils::end_of_day(timestamp).unwrap();
1132
1133        // Should be 2024-01-01 23:59:59.999 UTC = 1704153599999
1134        let expected = 1704153599999;
1135        assert_eq!(end_of_day, expected);
1136    }
1137
1138    // ==================== TimestampConversion Tests ====================
1139
1140    #[test]
1141    fn test_timestamp_conversion_some() {
1142        let old_timestamp: Option<u64> = Some(1704110400000);
1143        let result = old_timestamp.to_i64().unwrap();
1144        assert_eq!(result, Some(1704110400000i64));
1145    }
1146
1147    #[test]
1148    fn test_timestamp_conversion_none() {
1149        let old_timestamp: Option<u64> = None;
1150        let result = old_timestamp.to_i64().unwrap();
1151        assert_eq!(result, None);
1152    }
1153
1154    #[test]
1155    fn test_timestamp_conversion_overflow() {
1156        let old_timestamp: Option<u64> = Some(u64::MAX);
1157        let result = old_timestamp.to_i64();
1158        assert!(result.is_err());
1159    }
1160
1161    // ==================== Integration Tests ====================
1162
1163    #[test]
1164    fn test_timestamp_round_trip_with_validation() {
1165        let original = 1704110400000;
1166
1167        // Validate original
1168        let validated = TimestampUtils::validate_timestamp(original).unwrap();
1169        assert_eq!(validated, original);
1170
1171        // Format to ISO 8601
1172        let iso_str = TimestampUtils::format_iso8601(validated).unwrap();
1173
1174        // Parse back
1175        let parsed = parse_date(&iso_str).unwrap();
1176        assert_eq!(parsed, original);
1177    }
1178
1179    #[test]
1180    fn test_migration_workflow() {
1181        // Simulate migration from u64 to i64
1182        let old_timestamp: u64 = 1704110400000;
1183
1184        // Convert to i64
1185        #[allow(deprecated)]
1186        let new_timestamp = TimestampUtils::u64_to_i64(old_timestamp).unwrap();
1187
1188        // Validate
1189        let validated = TimestampUtils::validate_timestamp(new_timestamp).unwrap();
1190
1191        // Use in formatting
1192        let formatted = TimestampUtils::format_iso8601(validated).unwrap();
1193        assert_eq!(formatted, "2024-01-01T12:00:00.000Z");
1194
1195        // Convert back for legacy code if needed
1196        #[allow(deprecated)]
1197        let back_to_u64 = TimestampUtils::i64_to_u64(validated).unwrap();
1198        assert_eq!(back_to_u64, old_timestamp);
1199    }
1200
1201    #[test]
1202    fn test_edge_cases() {
1203        // Test Unix epoch
1204        let epoch = 0i64;
1205        let validated = TimestampUtils::validate_timestamp(epoch).unwrap();
1206        assert_eq!(validated, epoch);
1207
1208        // Test year 2100 boundary
1209        let year_2100 = TimestampUtils::YEAR_2100_MS;
1210        let validated = TimestampUtils::validate_timestamp(year_2100).unwrap();
1211        assert_eq!(validated, year_2100);
1212
1213        // Test just over year 2100 boundary
1214        let over_2100 = TimestampUtils::YEAR_2100_MS + 1;
1215        let result = TimestampUtils::validate_timestamp(over_2100);
1216        assert!(result.is_err());
1217    }
1218
1219    #[test]
1220    fn test_consistency_between_functions() {
1221        let timestamp = 1704110400000;
1222
1223        // Test that all formatting functions use the same validation
1224        let ymdhms_result = ymdhms(timestamp, None);
1225        let yyyymmdd_result = yyyymmdd(timestamp, None);
1226        let yymmdd_result = yymmdd(timestamp, None);
1227        let iso8601_result = iso8601(timestamp);
1228
1229        assert!(ymdhms_result.is_ok());
1230        assert!(yyyymmdd_result.is_ok());
1231        assert!(yymmdd_result.is_ok());
1232        assert!(iso8601_result.is_ok());
1233
1234        // Test that all fail for invalid timestamps
1235        let invalid = -1i64;
1236        assert!(ymdhms(invalid, None).is_err());
1237        assert!(yyyymmdd(invalid, None).is_err());
1238        assert!(yymmdd(invalid, None).is_err());
1239        assert!(iso8601(invalid).is_err());
1240    }
1241}