1use crate::models::{Bar, ImportResult, StatRow, Tick};
2
3pub fn print_stats(rows: &[StatRow], db_size: Option<u64>) {
5 if rows.is_empty() {
6 println!("No data found.");
7 return;
8 }
9
10 let w_ex = rows
12 .iter()
13 .map(|r| r.exchange.len())
14 .max()
15 .unwrap_or(8)
16 .max(8);
17 let w_sym = rows
18 .iter()
19 .map(|r| r.symbol.len())
20 .max()
21 .unwrap_or(6)
22 .max(6);
23 let w_type = rows
24 .iter()
25 .map(|r| r.data_type.len())
26 .max()
27 .unwrap_or(4)
28 .max(4);
29 let w_count = 12;
30 let w_ts = 19;
31
32 let header = format!(
33 " {:w_ex$} │ {:w_sym$} │ {:w_type$} │ {:>w_count$} │ {:w_ts$} │ {:w_ts$}",
34 "Exchange", "Symbol", "Type", "Count", "From", "To",
35 );
36 let sep = format!(
37 "─{:─>w_ex$}─┼─{:─>w_sym$}─┼─{:─>w_type$}─┼─{:─>w_count$}─┼─{:─>w_ts$}─┼─{:─>w_ts$}─",
38 "", "", "", "", "", "",
39 );
40
41 println!();
42 println!("{header}");
43 println!("{sep}");
44
45 for row in rows {
46 let ts_min = row.ts_min.format("%Y-%m-%d %H:%M:%S").to_string();
47 let ts_max = row.ts_max.format("%Y-%m-%d %H:%M:%S").to_string();
48 println!(
49 " {:w_ex$} │ {:w_sym$} │ {:w_type$} │ {:>w_count$} │ {:w_ts$} │ {:w_ts$}",
50 row.exchange,
51 row.symbol,
52 row.data_type,
53 format_count(row.count),
54 ts_min,
55 ts_max,
56 );
57 }
58
59 let exchanges: std::collections::HashSet<&str> =
61 rows.iter().map(|r| r.exchange.as_str()).collect();
62 let symbols: std::collections::HashSet<&str> = rows.iter().map(|r| r.symbol.as_str()).collect();
63 println!();
64 print!(
65 " Total: {} dataset(s), {} exchange(s), {} symbol(s)",
66 rows.len(),
67 exchanges.len(),
68 symbols.len(),
69 );
70 if let Some(bytes) = db_size {
71 print!(" │ Database size: {}", format_bytes(bytes));
72 }
73 println!();
74}
75
76pub fn print_ticks(exchange: &str, symbol: &str, ticks: &[Tick], total_count: u64) {
78 println!(
79 "\nExchange: {} │ Symbol: {} │ Ticks │ Showing {} of {}\n",
80 exchange,
81 symbol,
82 ticks.len(),
83 format_count(total_count),
84 );
85
86 if ticks.is_empty() {
87 println!(" (no data)");
88 return;
89 }
90
91 println!(
92 " {:26} │ {:>12} │ {:>12} │ {:>12} │ {:>10} │ {:>5}",
93 "Timestamp (UTC)", "Bid", "Ask", "Last", "Volume", "Flags",
94 );
95 println!(
96 "─{:─>26}─┼─{:─>12}─┼─{:─>12}─┼─{:─>12}─┼─{:─>10}─┼─{:─>5}─",
97 "", "", "", "", "", "",
98 );
99
100 for tick in ticks {
101 let ts = tick.ts.format("%Y-%m-%d %H:%M:%S%.3f").to_string();
102 println!(
103 " {:26} │ {:>12} │ {:>12} │ {:>12} │ {:>10} │ {:>5}",
104 ts,
105 fmt_opt_f64(tick.bid),
106 fmt_opt_f64(tick.ask),
107 fmt_opt_f64(tick.last),
108 fmt_opt_f64(tick.volume),
109 fmt_opt_i32(tick.flags),
110 );
111 }
112}
113
114pub fn print_bars(exchange: &str, symbol: &str, tf: &str, bars: &[Bar], total_count: u64) {
116 println!(
117 "\nExchange: {} │ Symbol: {} │ Bars ({}) │ Showing {} of {}\n",
118 exchange,
119 symbol,
120 tf,
121 bars.len(),
122 format_count(total_count),
123 );
124
125 if bars.is_empty() {
126 println!(" (no data)");
127 return;
128 }
129
130 println!(
131 " {:19} │ {:>12} │ {:>12} │ {:>12} │ {:>12} │ {:>8} │ {:>8} │ {:>6}",
132 "Timestamp (UTC)", "Open", "High", "Low", "Close", "TickVol", "Vol", "Spread",
133 );
134 println!(
135 "─{:─>19}─┼─{:─>12}─┼─{:─>12}─┼─{:─>12}─┼─{:─>12}─┼─{:─>8}─┼─{:─>8}─┼─{:─>6}─",
136 "", "", "", "", "", "", "", "",
137 );
138
139 for bar in bars {
140 let ts = bar.ts.format("%Y-%m-%d %H:%M:%S").to_string();
141 println!(
142 " {:19} │ {:>12.2} │ {:>12.2} │ {:>12.2} │ {:>12.2} │ {:>8} │ {:>8} │ {:>6}",
143 ts, bar.open, bar.high, bar.low, bar.close, bar.tick_vol, bar.volume, bar.spread,
144 );
145 }
146}
147
148pub fn print_import_result(result: &ImportResult) {
150 let elapsed = if result.elapsed.as_secs() >= 1 {
151 format!("{:.1}s", result.elapsed.as_secs_f64())
152 } else {
153 format!("{}ms", result.elapsed.as_millis())
154 };
155
156 println!(" Imported {}", result.file);
157 println!(
158 " Exchange: {} │ Symbol: {}",
159 result.exchange, result.symbol,
160 );
161 println!(
162 " Parsed: {} │ Inserted: {} │ Skipped (dup): {}",
163 format_count(result.rows_parsed as u64),
164 format_count(result.rows_inserted as u64),
165 format_count(result.rows_skipped as u64),
166 );
167 println!(" Elapsed: {elapsed}");
168}
169
170pub fn print_delete_result(data_type: &str, exchange: &str, detail: &str, count: usize) {
172 println!(
173 "Removed {} {} row(s) for {}/{}",
174 format_count(count as u64),
175 data_type,
176 exchange,
177 detail,
178 );
179}
180
181fn format_bytes(bytes: u64) -> String {
183 const KB: u64 = 1024;
184 const MB: u64 = 1024 * KB;
185 const GB: u64 = 1024 * MB;
186 if bytes >= GB {
187 format!("{:.1} GB", bytes as f64 / GB as f64)
188 } else if bytes >= MB {
189 format!("{:.1} MB", bytes as f64 / MB as f64)
190 } else if bytes >= KB {
191 format!("{:.1} KB", bytes as f64 / KB as f64)
192 } else {
193 format!("{} B", bytes)
194 }
195}
196
197fn format_count(n: u64) -> String {
199 let s = n.to_string();
200 let mut result = String::with_capacity(s.len() + s.len() / 3);
201 for (i, c) in s.chars().rev().enumerate() {
202 if i > 0 && i % 3 == 0 {
203 result.push(',');
204 }
205 result.push(c);
206 }
207 result.chars().rev().collect()
208}
209
210fn fmt_opt_f64(v: Option<f64>) -> String {
211 v.map_or(String::new(), |f| format!("{:.2}", f))
212}
213
214fn fmt_opt_i32(v: Option<i32>) -> String {
215 v.map_or(String::new(), |i| i.to_string())
216}