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