feat: implement main application structure with UI and layout management
- Added main application logic in `src/ui/app.rs` to handle state and modal dialogs. - Created module structure for UI components in `src/ui/mod.rs`. - Implemented column configuration screen in `src/ui/screens/configuracao_colunas.rs`. - Developed file import screen in `src/ui/screens/import.rs` for CSV and XLSX files. - Added layout management screen in `src/ui/screens/layouts.rs` for saving and importing layouts. - Created result display screen in `src/ui/screens/resultado.rs` to show analysis results. - Introduced modular organization for screens in `src/ui/screens/mod.rs`.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
use crate::domain::errors::{ErroArquivo, ResumoAvisos};
|
||||
use calamine::{open_workbook, Reader, Xlsx};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
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<Vec<String>>,
|
||||
/// Avisos coletados durante a leitura.
|
||||
pub avisos: ResumoAvisos,
|
||||
}
|
||||
|
||||
/// Lista as abas disponíveis em um arquivo XLSX.
|
||||
pub fn listar_abas(caminho: &Path) -> Result<Vec<String>, ErroArquivo> {
|
||||
verificar_tamanho(caminho)?;
|
||||
|
||||
let workbook: Xlsx<BufReader<File>> = open_workbook::<Xlsx<_>, _>(caminho)
|
||||
.map_err(|e| ErroArquivo::Corrompido(e.to_string()))?;
|
||||
|
||||
Ok(workbook.sheet_names().to_vec())
|
||||
}
|
||||
|
||||
/// Lê os dados de uma aba XLSX a partir de uma linha de início.
|
||||
///
|
||||
/// # 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<ResultadoXlsx, ErroArquivo> {
|
||||
verificar_tamanho(caminho)?;
|
||||
|
||||
let mut workbook: Xlsx<BufReader<File>> = open_workbook::<Xlsx<_>, _>(caminho)
|
||||
.map_err(|e| ErroArquivo::Corrompido(e.to_string()))?;
|
||||
|
||||
let range: calamine::Range<calamine::Data> = workbook
|
||||
.worksheet_range(nome_aba)
|
||||
.map_err(|e| ErroArquivo::Corrompido(e.to_string()))?;
|
||||
|
||||
let mut avisos = ResumoAvisos::default();
|
||||
let mut linhas: Vec<Vec<String>> = 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<String> = 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 })
|
||||
}
|
||||
|
||||
/// 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<Coordenada> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user