use crate::domain::errors::{ErroArquivo, ResumoAvisos}; use calamine::{open_workbook_auto, Reader}; use std::path::Path; const LIMITE_BYTES: u64 = 50 * 1024 * 1024; /// Representa uma coordenada de célula (coluna base-0, linha base-0). #[derive(Debug, Clone, Copy)] pub struct Coordenada { pub coluna: u32, pub linha: u32, } /// Resultado da leitura de um arquivo XLSX. pub struct ResultadoXlsx { /// Linhas de dados, a partir da posição de início. /// Cada linha é um vetor de strings (indexado pela posição da coluna de início). pub linhas: Vec>, /// Avisos coletados durante a leitura. pub avisos: ResumoAvisos, } /// Lista as abas disponíveis em um arquivo XLSX (ou XLS com extensão .xls mas conteúdo XLSX). pub fn listar_abas(caminho: &Path) -> Result, ErroArquivo> { verificar_tamanho(caminho)?; let workbook = open_workbook_auto(caminho) .map_err(|e| ErroArquivo::Corrompido(e.to_string()))?; Ok(workbook.sheet_names().to_vec()) } /// Lê os dados de uma aba XLSX (ou XLS com extensão .xls mas conteúdo XLSX) a partir de uma linha de início. /// /// O formato real é detectado pelo conteúdo do arquivo (magic bytes), não pela extensão. /// /// # Parâmetros /// - `caminho`: caminho do arquivo /// - `nome_aba`: nome da aba a ser lida /// - `linha_inicio`: número da linha de início dos dados (base 1, ex: 3 para "B3") pub fn ler_xlsx( caminho: &Path, nome_aba: &str, linha_inicio: u32, ) -> Result { verificar_tamanho(caminho)?; let mut workbook = open_workbook_auto(caminho) .map_err(|e| ErroArquivo::Corrompido(e.to_string()))?; let range: calamine::Range = workbook .worksheet_range(nome_aba) .map_err(|e| ErroArquivo::Corrompido(e.to_string()))?; let avisos = ResumoAvisos::default(); let mut linhas: Vec> = Vec::new(); let linha_inicio_base0 = (linha_inicio.saturating_sub(1)) as usize; for (row_idx, row) in range.rows().enumerate() { if row_idx < linha_inicio_base0 { continue; } let campos: Vec = row .iter() .map(|cell| { use calamine::Data; match cell { Data::Empty => String::new(), Data::String(s) => s.clone(), Data::Float(f) => { if f.fract() == 0.0 { format!("{}", *f as i64) } else { format!("{}", f) } } Data::Int(i) => i.to_string(), Data::Bool(b) => b.to_string(), Data::DateTime(dt) => dt.to_string(), Data::DateTimeIso(s) => s.clone(), Data::DurationIso(s) => s.clone(), Data::Error(_) => String::new(), } }) .collect(); // Ignorar linhas completamente em branco if campos.iter().all(|s: &String| s.trim().is_empty()) { continue; } linhas.push(campos); } Ok(ResultadoXlsx { linhas, avisos }) } /// Retorna as primeiras 5 linhas de uma aba XLSX, a partir da linha 1. /// Usado exclusivamente para pré-visualização na UI. pub fn preview_xlsx( caminho: &Path, nome_aba: &str, ) -> Result>, ErroArquivo> { verificar_tamanho(caminho)?; let mut workbook = open_workbook_auto(caminho) .map_err(|e| ErroArquivo::Corrompido(e.to_string()))?; let range: calamine::Range = workbook .worksheet_range(nome_aba) .map_err(|e| ErroArquivo::Corrompido(e.to_string()))?; let linhas = range .rows() .take(5) .map(|row| { row.iter() .map(|cell| { use calamine::Data; match cell { Data::Empty => String::new(), Data::String(s) => s.clone(), Data::Float(f) => { if f.fract() == 0.0 { format!("{}", *f as i64) } else { format!("{}", f) } } Data::Int(i) => i.to_string(), Data::Bool(b) => b.to_string(), Data::DateTime(dt) => dt.to_string(), Data::DateTimeIso(s) => s.clone(), Data::DurationIso(s) => s.clone(), Data::Error(_) => String::new(), } }) .collect() }) .collect(); Ok(linhas) } /// Converte uma notação LetraLinha (ex: "B3") para (coluna_base0, linha_base1). /// /// Retorna `None` se a notação for inválida. pub fn parsear_letra_linha(s: &str) -> Option { let s = s.trim().to_uppercase(); if s.is_empty() { return None; } let pos_numero = s.find(|c: char| c.is_ascii_digit())?; let (letras, numeros) = s.split_at(pos_numero); if letras.is_empty() || numeros.is_empty() { return None; } // Converter letras para índice de coluna (base 0) // A=0, B=1, ..., Z=25, AA=26, ... let coluna = letras .chars() .try_fold(0u32, |acc, c| { if c.is_ascii_uppercase() { Some(acc * 26 + (c as u32 - 'A' as u32 + 1)) } else { None } })? .checked_sub(1)?; let linha: u32 = numeros.parse().ok()?; if linha == 0 { return None; } Some(Coordenada { coluna, linha }) } fn verificar_tamanho(caminho: &Path) -> Result<(), ErroArquivo> { let metadata = std::fs::metadata(caminho) .map_err(|e| ErroArquivo::ErroLeitura(e.to_string()))?; if metadata.len() > LIMITE_BYTES { return Err(ErroArquivo::TamanhoExcedido(metadata.len())); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn parsear_b3() { let coord = parsear_letra_linha("B3").unwrap(); assert_eq!(coord.coluna, 1); // B = coluna 1 (base 0) assert_eq!(coord.linha, 3); } #[test] fn parsear_a1() { let coord = parsear_letra_linha("A1").unwrap(); assert_eq!(coord.coluna, 0); assert_eq!(coord.linha, 1); } #[test] fn parsear_z1() { let coord = parsear_letra_linha("Z1").unwrap(); assert_eq!(coord.coluna, 25); } #[test] fn parsear_aa1() { let coord = parsear_letra_linha("AA1").unwrap(); assert_eq!(coord.coluna, 26); } #[test] fn parsear_invalido() { assert!(parsear_letra_linha("").is_none()); assert!(parsear_letra_linha("3B").is_none()); assert!(parsear_letra_linha("123").is_none()); } #[test] fn parsear_minusculo() { let coord = parsear_letra_linha("b3").unwrap(); assert_eq!(coord.coluna, 1); } }