tcg_db 0.1.0

Pokemon TCG game engine database and card metadata
Documentation
use std::collections::BTreeSet;

use rusqlite::Connection;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CoverageError {
    #[error("database error: {0}")]
    Database(#[from] rusqlite::Error),
}

#[derive(Debug, Clone)]
pub struct SetCoverage {
    pub set_code: String,
    pub expected: usize,
    pub imported: usize,
    pub missing_numbers: Vec<u32>,
}

#[derive(Debug, Clone)]
pub struct CoverageReport {
    pub sets: Vec<SetCoverage>,
    pub total_expected: usize,
    pub total_imported: usize,
    pub coverage_percent: f64,
}

pub fn generate_coverage_report(conn: &Connection) -> Result<CoverageReport, CoverageError> {
    let expected_sets = [("CG", 100usize), ("DF", 101usize)];
    let mut coverage_sets = Vec::new();
    let mut total_expected = 0;
    let mut total_imported = 0;

    for (set_code, expected) in expected_sets {
        let numbers = load_card_numbers(conn, set_code)?;
        let imported = numbers.len();
        let missing_numbers = missing_number_list(&numbers, expected as u32);

        total_expected += expected;
        total_imported += imported;

        coverage_sets.push(SetCoverage {
            set_code: set_code.to_string(),
            expected,
            imported,
            missing_numbers,
        });
    }

    let coverage_percent = if total_expected == 0 {
        0.0
    } else {
        (total_imported as f64 / total_expected as f64) * 100.0
    };

    Ok(CoverageReport {
        sets: coverage_sets,
        total_expected,
        total_imported,
        coverage_percent,
    })
}

fn load_card_numbers(conn: &Connection, set_code: &str) -> Result<BTreeSet<u32>, CoverageError> {
    let mut stmt = conn.prepare(
        "SELECT number FROM cards
         JOIN sets ON cards.set_id = sets.set_id
         WHERE sets.code = ?1",
    )?;
    let rows = stmt.query_map([set_code], |row| row.get::<_, String>(0))?;
    let mut numbers = BTreeSet::new();
    for row in rows {
        let number_str = row?;
        if let Ok(number) = number_str.parse::<u32>() {
            numbers.insert(number);
        }
    }
    Ok(numbers)
}

fn missing_number_list(numbers: &BTreeSet<u32>, expected: u32) -> Vec<u32> {
    let mut missing = Vec::new();
    for num in 1..=expected {
        if !numbers.contains(&num) {
            missing.push(num);
        }
    }
    missing
}

pub fn format_coverage_report(report: &CoverageReport) -> String {
    let mut lines = Vec::new();
    for set in &report.sets {
        lines.push(format!(
            "{}: {}/{} imported, missing {}",
            set.set_code,
            set.imported,
            set.expected,
            set.missing_numbers.len()
        ));
        if !set.missing_numbers.is_empty() {
            let missing = set
                .missing_numbers
                .iter()
                .map(|n| n.to_string())
                .collect::<Vec<_>>()
                .join(", ");
            lines.push(format!("{} missing numbers: {}", set.set_code, missing));
        }
    }
    lines.push(format!(
        "Total: {}/{} ({:.2}%)",
        report.total_imported, report.total_expected, report.coverage_percent
    ));
    lines.join("\n")
}