From c8f561749d1830d378a642f5e86e163a26b8d724 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Mon, 16 Feb 2026 18:12:05 +0100 Subject: [PATCH] Refactor: Move DataServer to its own module Moves the `DataServer` struct and its implementation into a new `dataserver` module. This improves code organization and separates data loading logic from the main application logic. --- src/dataserver.rs | 166 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 167 ++-------------------------------------------- 2 files changed, 172 insertions(+), 161 deletions(-) create mode 100644 src/dataserver.rs diff --git a/src/dataserver.rs b/src/dataserver.rs new file mode 100644 index 0000000..963a386 --- /dev/null +++ b/src/dataserver.rs @@ -0,0 +1,166 @@ +use bytemuck::cast_slice; +use std::collections::HashMap; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, RwLock}; +use std::time::Instant; +use zip::ZipArchive; + +#[derive(Debug, Clone)] +pub struct DataFile { + pub path: PathBuf, + pub symbol: String, + pub year: i32, + pub month: i32, +} + +#[derive(Debug)] +pub struct CacheEntry { + pub data: Arc>, + pub last_accessed: Instant, +} + +pub struct DataServer { + base_path: PathBuf, + pub symbols: RwLock>>, + pub cache: RwLock>>, +} + +impl DataServer { + pub fn new(path: impl AsRef) -> Self { + Self { + base_path: path.as_ref().to_path_buf(), + symbols: RwLock::new(HashMap::new()), + cache: RwLock::new(HashMap::new()), + } + } + + pub async fn update_symbols(&self) -> tokio::io::Result<()> { + let mut entries = tokio::fs::read_dir(&self.base_path).await?; + let mut temp_map: HashMap> = HashMap::new(); + + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + if file_name.ends_with(".m1") || file_name.ends_with(".tick") { + if let Some(data_file) = self.parse_file_name(&path, file_name) { + temp_map + .entry(data_file.symbol.clone()) + .or_default() + .push(data_file); + } + } + } + } + + for files in temp_map.values_mut() { + files.sort_by(|a, b| a.year.cmp(&b.year).then(a.month.cmp(&b.month))); + } + + let mut symbols = self.symbols.write().unwrap(); + *symbols = temp_map; + Ok(()) + } + + pub fn enumerate_symbols(&self) -> Vec { + let symbols = self.symbols.read().unwrap(); + let mut keys: Vec = symbols.keys().cloned().collect(); + keys.sort(); + keys + } + + fn parse_file_name(&self, path: &Path, file_name: &str) -> Option { + let stem = Path::new(file_name).file_stem()?.to_str()?; + let ext = Path::new(file_name).extension()?.to_str()?; + let parts: Vec<&str> = stem.split('_').collect(); + if parts.len() < 3 { + return None; + } + let month = parts.last()?.parse::().ok()?; + let year = parts[parts.len() - 2].parse::().ok()?; + let symbol = format!("{}.{}", parts[..parts.len() - 2].join("_"), ext); + Some(DataFile { + path: path.to_path_buf(), + symbol, + year, + month, + }) + } + + pub async fn load_data(&self, data_file: &DataFile) -> anyhow::Result>> + where + R: crate::records::SourceRecord, + crate::records::DataPoint: Into, + { + { + let mut cache = self.cache.write().unwrap(); + if let Some(entry) = cache.get_mut(&data_file.path) { + entry.last_accessed = Instant::now(); + return Ok(Arc::clone(&entry.data)); + } + } + + let path = data_file.path.clone(); + let shared_data = tokio::task::spawn_blocking(move || { + let file = std::fs::File::open(&path)?; + let mut archive = ZipArchive::new(file)?; + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + if file.name().ends_with(".bin") { + let mut bin_content = vec![0u8; file.size() as usize]; + file.read_exact(&mut bin_content)?; + let raw_data: &[R] = cast_slice(&bin_content); + + // Perform mapping inside blocking task + let mapped_data: Vec = + raw_data.iter().map(|&r| r.to_data_point().into()).collect(); + + return Ok(Arc::new(mapped_data)); + } + } + Err(anyhow::anyhow!("No .bin file found")) + }) + .await??; + + let mut cache = self.cache.write().unwrap(); + cache.insert( + data_file.path.clone(), + CacheEntry { + data: Arc::clone(&shared_data), + last_accessed: Instant::now(), + }, + ); + Ok(shared_data) + } + + // async fn read_from_zip - removed as it was unused in main.rs context or seemingly redundant/internal helper not used in extracted public interface? + // Checking main.rs: It was NOT used in main.rs, it was a private helper method inside impl DataServer. + // However, it wasn't used by load_data either. It seems dead code or future code. + // I will include it to be safe, but make it private or allow dead code if it warns. + // Actually, looking at main.rs again, read_from_zip was defined but NOT used in the provided snippet of DataServer. + // Wait, load_data implements similar logic inline. + // I will keep it to strictly follow "never change code unless..." (preserving the code structure). + + #[allow(dead_code)] + async fn read_from_zip( + &self, + path: &Path, + ) -> anyhow::Result> { + let path = path.to_path_buf(); + tokio::task::spawn_blocking(move || { + let file = std::fs::File::open(&path)?; + let mut archive = ZipArchive::new(file)?; + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + if file.name().ends_with(".bin") { + let mut bin_content = vec![0u8; file.size() as usize]; + file.read_exact(&mut bin_content)?; + let records: &[R] = cast_slice(&bin_content); + return Ok(records.to_vec()); + } + } + Err(anyhow::anyhow!("No .bin file found")) + }) + .await? + } +} diff --git a/src/main.rs b/src/main.rs index c0f871a..8bffdf4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,12 @@ +mod dataserver; mod records; +use crate::dataserver::DataServer; use crate::records::{DataPoint, M1Record, OhlcItem, TickRecord}; -use chrono::{DateTime, Utc}; +use chrono::DateTime; +use eframe::egui; +use std::sync::Arc; +use tokio::runtime::Runtime; fn format_ts(ts: f64) -> String { // Delphi TDateTime: Days since 1899-12-30. @@ -16,166 +21,6 @@ fn format_ts(ts: f64) -> String { format!("{:.6}", ts) } } -use bytemuck::cast_slice; -use eframe::egui; -use std::collections::HashMap; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, RwLock}; -use std::time::Instant; -use tokio::runtime::Runtime; -use zip::ZipArchive; - -// --- Data Structures --- - -#[derive(Debug, Clone)] -pub struct DataFile { - pub path: PathBuf, - pub symbol: String, - pub year: i32, - pub month: i32, -} - -struct CacheEntry { - data: Arc>, - last_accessed: Instant, -} - -pub struct DataServer { - base_path: PathBuf, - symbols: RwLock>>, - cache: RwLock>>, -} - -impl DataServer { - pub fn new(path: impl AsRef) -> Self { - Self { - base_path: path.as_ref().to_path_buf(), - symbols: RwLock::new(HashMap::new()), - cache: RwLock::new(HashMap::new()), - } - } - - pub async fn update_symbols(&self) -> tokio::io::Result<()> { - let mut entries = tokio::fs::read_dir(&self.base_path).await?; - let mut temp_map: HashMap> = HashMap::new(); - - while let Some(entry) = entries.next_entry().await? { - let path = entry.path(); - if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { - if file_name.ends_with(".m1") || file_name.ends_with(".tick") { - if let Some(data_file) = self.parse_file_name(&path, file_name) { - temp_map - .entry(data_file.symbol.clone()) - .or_default() - .push(data_file); - } - } - } - } - - for files in temp_map.values_mut() { - files.sort_by(|a, b| a.year.cmp(&b.year).then(a.month.cmp(&b.month))); - } - - let mut symbols = self.symbols.write().unwrap(); - *symbols = temp_map; - Ok(()) - } - - pub fn enumerate_symbols(&self) -> Vec { - let symbols = self.symbols.read().unwrap(); - let mut keys: Vec = symbols.keys().cloned().collect(); - keys.sort(); - keys - } - - fn parse_file_name(&self, path: &Path, file_name: &str) -> Option { - let stem = Path::new(file_name).file_stem()?.to_str()?; - let ext = Path::new(file_name).extension()?.to_str()?; - let parts: Vec<&str> = stem.split('_').collect(); - if parts.len() < 3 { - return None; - } - let month = parts.last()?.parse::().ok()?; - let year = parts[parts.len() - 2].parse::().ok()?; - let symbol = format!("{}.{}", parts[..parts.len() - 2].join("_"), ext); - Some(DataFile { - path: path.to_path_buf(), - symbol, - year, - month, - }) - } - - pub async fn load_data(&self, data_file: &DataFile) -> anyhow::Result>> - where - R: crate::records::SourceRecord, - crate::records::DataPoint: Into, - { - { - let mut cache = self.cache.write().unwrap(); - if let Some(entry) = cache.get_mut(&data_file.path) { - entry.last_accessed = Instant::now(); - return Ok(Arc::clone(&entry.data)); - } - } - - let path = data_file.path.clone(); - let shared_data = tokio::task::spawn_blocking(move || { - let file = std::fs::File::open(&path)?; - let mut archive = ZipArchive::new(file)?; - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - if file.name().ends_with(".bin") { - let mut bin_content = vec![0u8; file.size() as usize]; - file.read_exact(&mut bin_content)?; - let raw_data: &[R] = cast_slice(&bin_content); - - // Perform mapping inside blocking task - let mapped_data: Vec = - raw_data.iter().map(|&r| r.to_data_point().into()).collect(); - - return Ok(Arc::new(mapped_data)); - } - } - Err(anyhow::anyhow!("No .bin file found")) - }) - .await??; - - let mut cache = self.cache.write().unwrap(); - cache.insert( - data_file.path.clone(), - CacheEntry { - data: Arc::clone(&shared_data), - last_accessed: Instant::now(), - }, - ); - Ok(shared_data) - } - - async fn read_from_zip( - &self, - path: &Path, - ) -> anyhow::Result> { - let path = path.to_path_buf(); - tokio::task::spawn_blocking(move || { - let file = std::fs::File::open(&path)?; - let mut archive = ZipArchive::new(file)?; - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - if file.name().ends_with(".bin") { - let mut bin_content = vec![0u8; file.size() as usize]; - file.read_exact(&mut bin_content)?; - let records: &[R] = cast_slice(&bin_content); - return Ok(records.to_vec()); - } - } - Err(anyhow::anyhow!("No .bin file found")) - }) - .await? - } -} // --- EGui App ---