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