rusty_bin/monitor/storage/
naming.rs

1//!
2//! File naming strategy and parsing logic.
3
4use rusty_common::time::days_since_epoch_to_date;
5use smartstring::alias::String as SmartString;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8/// Simple date structure to replace chrono::NaiveDate
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub struct SimpleDate {
11    /// Year component (e.g., 2024)
12    pub year: u32,
13    /// Month component (1-12)
14    pub month: u32,
15    /// Day component (1-31)
16    pub day: u32,
17}
18
19impl SimpleDate {
20    /// Create a new SimpleDate
21    #[must_use]
22    pub fn new(year: u32, month: u32, day: u32) -> Option<Self> {
23        if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
24            return None;
25        }
26        Some(Self { year, month, day })
27    }
28
29    /// Create SimpleDate from SystemTime
30    #[must_use]
31    pub fn from_system_time(time: SystemTime) -> Self {
32        let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
33        let days = duration.as_secs() / 86400;
34        let (year, month, day) = days_since_epoch_to_date(days);
35        Self { year, month, day }
36    }
37
38    /// Get current date
39    #[must_use]
40    pub fn today() -> Self {
41        Self::from_system_time(SystemTime::now())
42    }
43
44    /// Format date as YYYYMMDD
45    #[must_use]
46    pub fn format_yyyymmdd(&self) -> String {
47        format!("{:04}{:02}{:02}", self.year, self.month, self.day)
48    }
49
50    /// Parse date from YYYYMMDD format
51    #[must_use]
52    pub fn parse_yyyymmdd(date_str: &str) -> Option<Self> {
53        if date_str.len() != 8 {
54            return None;
55        }
56        let year = date_str[0..4].parse().ok()?;
57        let month = date_str[4..6].parse().ok()?;
58        let day = date_str[6..8].parse().ok()?;
59        Self::new(year, month, day)
60    }
61}
62
63/// File naming strategy
64#[derive(Debug, Clone)]
65pub struct FileNaming {
66    /// Exchange name (e.g., "binance", "coinbase")
67    pub exchange: SmartString,
68    /// Trading symbol (e.g., "BTCUSDT", "ETH-USD")
69    pub symbol: SmartString,
70    /// Type of data stored (e.g., "trades", "orderbook")
71    pub data_type: String,
72    /// Date of the data
73    pub date: SimpleDate,
74    /// File extension (default: "fb" for flatbuffer)
75    pub extension: String,
76}
77
78impl FileNaming {
79    /// Create a new file naming strategy
80    #[must_use]
81    pub fn new(
82        exchange: impl Into<SmartString>,
83        symbol: impl Into<SmartString>,
84        data_type: impl Into<String>,
85        date: SimpleDate,
86    ) -> Self {
87        Self {
88            exchange: exchange.into(),
89            symbol: symbol.into(),
90            data_type: data_type.into(),
91            date,
92            extension: "fb".to_string(),
93        }
94    }
95
96    /// Generate the filename
97    #[must_use]
98    pub fn filename(&self) -> String {
99        format!(
100            "{}_{}_{}_{}.{}",
101            self.exchange,
102            self.symbol,
103            self.data_type,
104            self.date.format_yyyymmdd(),
105            self.extension
106        )
107    }
108
109    /// Generate the compressed filename
110    #[must_use]
111    pub fn compressed_filename(&self) -> String {
112        format!("{}.zst", self.filename())
113    }
114
115    /// Generate the metadata filename
116    #[must_use]
117    pub fn metadata_filename(&self) -> String {
118        format!(
119            "{}_{}_metadata.json",
120            self.exchange,
121            self.date.format_yyyymmdd()
122        )
123    }
124
125    /// Parse filename to extract components
126    #[must_use]
127    pub fn parse_filename(filename: &str) -> Option<Self> {
128        let parts: Vec<&str> = filename.split('_').collect();
129        if parts.len() < 4 {
130            return None;
131        }
132
133        let exchange = parts[0];
134        let symbol = parts[1];
135        let data_type = parts[2];
136
137        // Extract date and extension
138        let date_ext = parts[3];
139        let date_parts: Vec<&str> = date_ext.split('.').collect();
140        if date_parts.len() < 2 {
141            return None;
142        }
143
144        let date_str = date_parts[0];
145        let extension = date_parts[1];
146
147        let date = SimpleDate::parse_yyyymmdd(date_str)?;
148
149        Some(Self {
150            exchange: exchange.into(),
151            symbol: symbol.into(),
152            data_type: data_type.to_string(),
153            date,
154            extension: extension.to_string(),
155        })
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_simple_date() {
165        let date = SimpleDate::new(2024, 12, 1).unwrap();
166        assert_eq!(date.format_yyyymmdd(), "20241201");
167
168        let parsed = SimpleDate::parse_yyyymmdd("20241201").unwrap();
169        assert_eq!(parsed, date);
170    }
171
172    #[test]
173    fn test_file_naming() {
174        let naming = FileNaming::new(
175            "binance",
176            "BTCUSDT",
177            "trades",
178            SimpleDate::new(2024, 12, 1).unwrap(),
179        );
180
181        assert_eq!(naming.filename(), "binance_BTCUSDT_trades_20241201.fb");
182        assert_eq!(
183            naming.compressed_filename(),
184            "binance_BTCUSDT_trades_20241201.fb.zst"
185        );
186    }
187
188    #[test]
189    fn test_filename_parsing() {
190        let filename = "binance_BTCUSDT_trades_20241201.fb";
191        let naming = FileNaming::parse_filename(filename).unwrap();
192
193        assert_eq!(naming.exchange, "binance");
194        assert_eq!(naming.symbol, "BTCUSDT");
195        assert_eq!(naming.data_type, "trades");
196        assert_eq!(naming.date, SimpleDate::new(2024, 12, 1).unwrap());
197        assert_eq!(naming.extension, "fb");
198    }
199}