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(×tamp)
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}