Files
comparador-notas/src/domain/services/detector_sequencia.rs
T
FelipeCN b9052e073f Implementado (3 features)
**Agrupamento de faltantes contíguos** (`detector_sequencia.rs`)
- Nova função pública `agrupar_contiguos(&[u64]) -> Vec<(u64, u64)>` com
  4 testes
- Na tela de resultado, faltantes agora aparecem como `• 100–104 (5
  notas)` em vez de 5 linhas separadas

**Estatísticas de completude** (`resultado.rs`)
- Cada série exibe: `Série 001 — 10 faltante(s) — 990/1000 notas (99.0%
  completo):`

**Copiar para clipboard** (`resultado.rs`)
- Botão `📋 Copiar` ao lado de cada série nos faltantes — copia todos os
  números (não só a página atual)
- Botão `📋 Copiar` nas duplicatas — copia no formato `1234 (3x), 5678
  (2x)`

Total: **42 → 46 testes**, todos passando.
2026-03-03 16:41:32 -03:00

189 lines
5.1 KiB
Rust

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<IntervaloSerie> {
if notas.is_empty() {
return None;
}
let mut numeros: Vec<u64> = 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<u64> {
if notas.is_empty() {
return Vec::new();
}
let mut numeros: Vec<u64> = 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::<u64>::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::<u64>::new());
}
#[test]
fn vazio() {
let refs: Vec<&Nota> = vec![];
assert_eq!(detectar_faltantes(&refs), Vec::<u64>::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::<u64>::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)]
);
}
}