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