use crate::domain::entities::nota::Nota; use crate::domain::entities::resultado_analise::IntervaloSerie; /// Limite de faltantes por série antes de solicitar confirmação do usuário (RF04). pub const LIMITE_FALTANTES: u64 = 10_000; /// Calcula o intervalo de faltantes de uma lista de notas de uma mesma série, /// sem materializar a lista completa. /// /// Retorna `None` se não há notas. pub fn calcular_intervalo(notas: &[&Nota]) -> Option { if notas.is_empty() { return None; } let mut numeros: Vec = notas.iter().map(|n| n.numero).collect(); numeros.sort_unstable(); numeros.dedup(); // Ignorar duplicatas no cálculo de sequência let minimo = *numeros.first().unwrap(); let maximo = *numeros.last().unwrap(); // Contar faltantes de forma incremental let mut faltantes: u64 = 0; for w in numeros.windows(2) { let a = w[0]; let b = w[1]; if b > a + 1 { faltantes += b - a - 1; } } Some(IntervaloSerie { minimo, maximo, contagem_faltantes: faltantes, }) } /// Materializa a lista completa de números faltantes para uma série. /// Deve ser chamado apenas após confirmação do usuário quando o intervalo /// excede `LIMITE_FALTANTES`. /// /// A lista é retornada em ordem crescente. pub fn detectar_faltantes(notas: &[&Nota]) -> Vec { if notas.is_empty() { return Vec::new(); } let mut numeros: Vec = notas.iter().map(|n| n.numero).collect(); numeros.sort_unstable(); numeros.dedup(); if numeros.len() <= 1 { return Vec::new(); } let mut faltantes = Vec::new(); for w in numeros.windows(2) { let a = w[0]; let b = w[1]; for faltante in (a + 1)..b { faltantes.push(faltante); } } faltantes } /// Agrupa uma lista **ordenada** de faltantes em intervalos contíguos. /// /// Retorna pares `(inicio, fim)`. Números isolados têm `inicio == fim`. /// /// # Exemplo /// `[1, 2, 3, 5, 8, 9]` → `[(1, 3), (5, 5), (8, 9)]` pub fn agrupar_contiguos(faltantes: &[u64]) -> Vec<(u64, u64)> { if faltantes.is_empty() { return Vec::new(); } let mut grupos = Vec::new(); let mut inicio = faltantes[0]; let mut anterior = faltantes[0]; for &num in &faltantes[1..] { if num != anterior + 1 { grupos.push((inicio, anterior)); inicio = num; } anterior = num; } grupos.push((inicio, anterior)); grupos } #[cfg(test)] mod tests { use super::*; use crate::domain::entities::nota::Nota; use rust_decimal::Decimal; fn nota(numero: u64) -> Nota { Nota::new(numero, "001".to_string(), None, None, None) } #[test] fn sem_faltantes() { let notas = vec![nota(1), nota(2), nota(3)]; let refs: Vec<&Nota> = notas.iter().collect(); assert_eq!(detectar_faltantes(&refs), Vec::::new()); } #[test] fn com_faltante_simples() { let notas = vec![nota(1), nota(2), nota(3), nota(5)]; let refs: Vec<&Nota> = notas.iter().collect(); assert_eq!(detectar_faltantes(&refs), vec![4u64]); } #[test] fn com_multiplos_faltantes() { let notas = vec![nota(1), nota(5)]; let refs: Vec<&Nota> = notas.iter().collect(); assert_eq!(detectar_faltantes(&refs), vec![2u64, 3, 4]); } #[test] fn serie_com_um_registro() { let notas = vec![nota(7)]; let refs: Vec<&Nota> = notas.iter().collect(); assert_eq!(detectar_faltantes(&refs), Vec::::new()); } #[test] fn vazio() { let refs: Vec<&Nota> = vec![]; assert_eq!(detectar_faltantes(&refs), Vec::::new()); } #[test] fn intervalo_correto() { let notas = vec![nota(1), nota(2), nota(5)]; let refs: Vec<&Nota> = notas.iter().collect(); let intervalo = calcular_intervalo(&refs).unwrap(); assert_eq!(intervalo.minimo, 1); assert_eq!(intervalo.maximo, 5); assert_eq!(intervalo.contagem_faltantes, 2); } #[test] fn intervalo_excede_limite() { let notas = vec![nota(1), nota(20_000)]; let refs: Vec<&Nota> = notas.iter().collect(); let intervalo = calcular_intervalo(&refs).unwrap(); assert!(intervalo.excede_limite(LIMITE_FALTANTES)); } #[test] fn duplicatas_ignoradas_no_calculo() { // Duplicados não devem gerar faltantes falsos let notas = vec![nota(1), nota(1), nota(2), nota(3)]; let refs: Vec<&Nota> = notas.iter().collect(); assert_eq!(detectar_faltantes(&refs), Vec::::new()); } #[test] fn agrupar_vazio() { assert_eq!(agrupar_contiguos(&[]), Vec::<(u64, u64)>::new()); } #[test] fn agrupar_isolados() { assert_eq!(agrupar_contiguos(&[1, 3, 5]), vec![(1, 1), (3, 3), (5, 5)]); } #[test] fn agrupar_um_intervalo() { assert_eq!(agrupar_contiguos(&[1, 2, 3]), vec![(1, 3)]); } #[test] fn agrupar_misto() { assert_eq!( agrupar_contiguos(&[1, 2, 3, 5, 8, 9]), vec![(1, 3), (5, 5), (8, 9)] ); } }