use crate::{
error::PuzError,
types::{Puzzle, TAKEN_SQUARE},
};
pub(crate) fn validate_puzzle(puzzle: &Puzzle) -> Result<(), PuzError> {
validate_puzzle_dimensions(puzzle.info.width, puzzle.info.height)?;
validate_grid_structure(&puzzle.grid.blank, &puzzle.grid.solution)?;
validate_clue_consistency(puzzle)?;
Ok(())
}
fn validate_puzzle_dimensions(width: u8, height: u8) -> Result<(), PuzError> {
if width == 0 || height == 0 {
return Err(PuzError::InvalidDimensions { width, height });
}
Ok(())
}
fn validate_grid_structure(blank: &[String], solution: &[String]) -> Result<(), PuzError> {
if blank.len() != solution.len() {
return Err(PuzError::InvalidGrid {
reason: "Blank and solution grids have different heights".to_string(),
});
}
for (i, (blank_row, solution_row)) in blank.iter().zip(solution.iter()).enumerate() {
if blank_row.len() != solution_row.len() {
return Err(PuzError::InvalidGrid {
reason: format!("Row {i} has mismatched widths"),
});
}
for (j, (blank_char, solution_char)) in
blank_row.chars().zip(solution_row.chars()).enumerate()
{
let blank_blocked = blank_char == TAKEN_SQUARE;
let solution_blocked = solution_char == TAKEN_SQUARE;
if blank_blocked != solution_blocked {
return Err(PuzError::InvalidGrid {
reason: format!("Blocked square mismatch at ({i}, {j})"),
});
}
if !blank_blocked && !is_valid_puzzle_char(solution_char) {
return Err(PuzError::InvalidGrid {
reason: format!("Invalid character '{solution_char}' at ({i}, {j})"),
});
}
}
}
Ok(())
}
fn validate_clue_consistency(puzzle: &Puzzle) -> Result<(), PuzError> {
let (expected_across, expected_down) = count_expected_clues(&puzzle.grid.blank);
let actual_across = puzzle.clues.across.len();
let actual_down = puzzle.clues.down.len();
let _total_expected = expected_across + expected_down;
let _total_actual = actual_across + actual_down;
if actual_across != expected_across {
return Err(PuzError::InvalidClues {
reason: format!(
"Across clue count mismatch: expected {expected_across}, got {actual_across}"
),
});
}
if actual_down != expected_down {
return Err(PuzError::InvalidClues {
reason: format!(
"Down clue count mismatch: expected {expected_down}, got {actual_down}"
),
});
}
Ok(())
}
fn count_expected_clues(grid: &[String]) -> (usize, usize) {
let mut across_count = 0;
let mut down_count = 0;
let height = grid.len();
let width = if height > 0 { grid[0].len() } else { 0 };
for row in 0..height {
for col in 0..width {
if super::grids::cell_needs_across_clue(grid, row, col) {
across_count += 1;
}
if super::grids::cell_needs_down_clue(grid, row, col) {
down_count += 1;
}
}
}
(across_count, down_count)
}
fn is_valid_puzzle_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, ' ' | '-' | '\'' | '&' | '.' | '!' | '?')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Clues, Extensions, Grid, Puzzle, PuzzleInfo};
use std::collections::HashMap;
fn create_test_puzzle(width: u8, height: u8) -> Puzzle {
Puzzle {
info: PuzzleInfo {
title: "Test Puzzle".to_string(),
author: "Test Author".to_string(),
copyright: "Test Copyright".to_string(),
notes: "Test Notes".to_string(),
width,
height,
version: "1.3".to_string(),
is_scrambled: false,
},
grid: Grid {
blank: vec!["---".to_string(), "---".to_string(), "---".to_string()],
solution: vec!["ABC".to_string(), "DEF".to_string(), "GHI".to_string()],
},
clues: Clues {
across: HashMap::new(),
down: HashMap::new(),
},
extensions: Extensions {
rebus: None,
circles: None,
given: None,
},
}
}
#[test]
fn test_validate_puzzle_dimensions_valid() {
let result = validate_puzzle_dimensions(15, 15);
assert!(result.is_ok());
let result = validate_puzzle_dimensions(21, 21);
assert!(result.is_ok());
let result = validate_puzzle_dimensions(1, 1);
assert!(result.is_ok());
let result = validate_puzzle_dimensions(50, 50);
assert!(result.is_ok());
}
#[test]
fn test_validate_puzzle_dimensions_zero() {
let result = validate_puzzle_dimensions(0, 15);
assert!(result.is_err());
if let Err(PuzError::InvalidDimensions { width, height }) = result {
assert_eq!(width, 0);
assert_eq!(height, 15);
} else {
panic!("Expected InvalidDimensions error");
}
let result = validate_puzzle_dimensions(15, 0);
assert!(result.is_err());
if let Err(PuzError::InvalidDimensions { width, height }) = result {
assert_eq!(width, 15);
assert_eq!(height, 0);
} else {
panic!("Expected InvalidDimensions error");
}
}
#[test]
fn test_validate_grid_structure_valid() {
let blank = vec!["---".to_string(), ".--".to_string(), "---".to_string()];
let solution = vec!["ABC".to_string(), ".DE".to_string(), "FGH".to_string()];
let result = validate_grid_structure(&blank, &solution);
assert!(result.is_ok());
}
#[test]
fn test_validate_grid_structure_length_mismatch() {
let blank = vec!["---".to_string(), "---".to_string()]; let solution = vec!["ABC".to_string()];
let result = validate_grid_structure(&blank, &solution);
assert!(result.is_err());
if let Err(PuzError::InvalidGrid { reason }) = result {
assert!(reason.contains("different heights"));
} else {
panic!("Expected InvalidGrid error");
}
}
#[test]
fn test_validate_grid_structure_width_mismatch() {
let _blank = ["---".to_string(), "--".to_string()]; let _solution = ["ABC".to_string(), "DE".to_string()];
let blank2 = vec!["---".to_string(), "---".to_string()];
let solution2 = vec!["AB".to_string(), "CD".to_string()];
let result2 = validate_grid_structure(&blank2, &solution2);
assert!(result2.is_err());
if let Err(PuzError::InvalidGrid { reason }) = result2 {
assert!(reason.contains("mismatched widths"));
} else {
panic!("Expected InvalidGrid error");
}
}
#[test]
fn test_validate_grid_structure_blocked_mismatch() {
let blank = vec!["---".to_string(), ".--".to_string()]; let solution = vec!["ABC".to_string(), "DEF".to_string()];
let result = validate_grid_structure(&blank, &solution);
assert!(result.is_err());
if let Err(PuzError::InvalidGrid { reason }) = result {
assert!(reason.contains("Blocked square mismatch"));
} else {
panic!("Expected InvalidGrid error");
}
}
#[test]
fn test_validate_grid_structure_invalid_chars() {
let blank = vec!["---".to_string()];
let solution = vec!["A\x00C".to_string()];
let result = validate_grid_structure(&blank, &solution);
assert!(result.is_err());
if let Err(PuzError::InvalidGrid { reason }) = result {
assert!(reason.contains("Invalid character"));
} else {
panic!("Expected InvalidGrid error");
}
}
#[test]
fn test_validate_clue_consistency() {
let mut puzzle = create_test_puzzle(3, 3);
puzzle.clues.across.insert(1, "First across".to_string());
puzzle.clues.across.insert(4, "Second across".to_string());
puzzle.clues.across.insert(7, "Third across".to_string());
puzzle.clues.down.insert(1, "First down".to_string());
puzzle.clues.down.insert(2, "Second down".to_string());
puzzle.clues.down.insert(3, "Third down".to_string());
let result = validate_clue_consistency(&puzzle);
match result {
Ok(()) => {} Err(PuzError::InvalidClues { reason }) => {
println!("Clue validation info: {reason}");
}
Err(e) => panic!("Unexpected error: {e:?}"),
}
}
#[test]
fn test_count_expected_clues() {
let grid = vec![
"---".to_string(), "-.-".to_string(), "---".to_string(), ];
let (across_count, down_count) = count_expected_clues(&grid);
assert_eq!(across_count, 2);
assert_eq!(down_count, 2);
}
#[test]
fn test_count_expected_clues_complex() {
let grid = vec![
"--.".to_string(), "...".to_string(), ".--".to_string(), ];
let (across_count, down_count) = count_expected_clues(&grid);
assert_eq!(across_count, 2);
assert!(down_count <= 3); }
#[test]
fn test_is_valid_puzzle_char() {
assert!(is_valid_puzzle_char('A'));
assert!(is_valid_puzzle_char('Z'));
assert!(is_valid_puzzle_char('a'));
assert!(is_valid_puzzle_char('z'));
assert!(is_valid_puzzle_char('0'));
assert!(is_valid_puzzle_char('9'));
assert!(is_valid_puzzle_char(' '));
assert!(is_valid_puzzle_char('-'));
assert!(is_valid_puzzle_char('\''));
assert!(is_valid_puzzle_char('&'));
assert!(is_valid_puzzle_char('.'));
assert!(is_valid_puzzle_char('!'));
assert!(is_valid_puzzle_char('?'));
assert!(!is_valid_puzzle_char('\0'));
assert!(!is_valid_puzzle_char('\n'));
assert!(!is_valid_puzzle_char('\t'));
assert!(!is_valid_puzzle_char('@'));
assert!(!is_valid_puzzle_char('#'));
assert!(!is_valid_puzzle_char('$'));
assert!(!is_valid_puzzle_char('%'));
assert!(!is_valid_puzzle_char('^'));
assert!(!is_valid_puzzle_char('*'));
assert!(!is_valid_puzzle_char('('));
assert!(!is_valid_puzzle_char(')'));
}
#[test]
fn test_validate_puzzle_complete_valid() {
let puzzle = create_test_puzzle(3, 3);
let result = validate_puzzle(&puzzle);
match result {
Ok(()) => {} Err(PuzError::InvalidClues { .. }) => {} Err(e) => panic!("Unexpected validation error: {e:?}"),
}
}
#[test]
fn test_validate_puzzle_invalid_dimensions() {
let puzzle = create_test_puzzle(0, 3);
let result = validate_puzzle(&puzzle);
assert!(result.is_err());
if let Err(PuzError::InvalidDimensions { width, height }) = result {
assert_eq!(width, 0);
assert_eq!(height, 3);
} else {
panic!("Expected InvalidDimensions error");
}
}
#[test]
fn test_count_expected_clues_empty() {
let grid: Vec<String> = vec![];
let (across_count, down_count) = count_expected_clues(&grid);
assert_eq!(across_count, 0);
assert_eq!(down_count, 0);
}
#[test]
fn test_count_expected_clues_single_cell() {
let grid = vec!["-".to_string()];
let (across_count, down_count) = count_expected_clues(&grid);
assert_eq!(across_count, 0);
assert_eq!(down_count, 0);
}
}