236 lines
7.0 KiB
Rust
236 lines
7.0 KiB
Rust
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<Vec<String>>,
|
|
/// 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<Vec<String>, 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<ResultadoXlsx, ErroArquivo> {
|
|
verificar_tamanho(caminho)?;
|
|
|
|
let mut workbook = open_workbook_auto(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 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 })
|
|
}
|
|
|
|
/// 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<Vec<Vec<String>>, ErroArquivo> {
|
|
verificar_tamanho(caminho)?;
|
|
|
|
let mut workbook = open_workbook_auto(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 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<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);
|
|
}
|
|
}
|