**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.
189 lines
5.1 KiB
Rust
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)]
|
|
);
|
|
}
|
|
}
|