rustoku_lib/
format.rs

1//! Formatting module for Rustoku data structures.
2//!
3//! This module provides functions to format the Sudoku board and its solve path
4//! in various ways.
5
6use crate::core::{Board, Solution, SolvePath, TechniqueFlags};
7use std::fmt;
8
9/// Formats the solution into a human-readable string representation.
10impl fmt::Display for Solution {
11    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
12        writeln!(f, "{}", self.board)?;
13        writeln!(f, "{}", self.solve_path)?;
14        Ok(())
15    }
16}
17
18/// Formats the board into a human-readable string representation.
19impl fmt::Display for Board {
20    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
21        writeln!(f, "{}", format_grid(&self.cells).join("\n"))?;
22        writeln!(f, "Line format: {}", format_line(&self.cells))?;
23        Ok(())
24    }
25}
26
27/// Formats the technique mask into a human-readable string representation.
28impl fmt::Display for TechniqueFlags {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        if self.is_empty() {
31            return write!(f, "None");
32        }
33        if self.is_all() {
34            return write!(f, "All Techniques");
35        }
36
37        let mut techniques = Vec::new();
38
39        if self.contains(TechniqueFlags::NAKED_SINGLES) {
40            techniques.push("Naked Singles");
41        }
42        if self.contains(TechniqueFlags::HIDDEN_SINGLES) {
43            techniques.push("Hidden Singles");
44        }
45        if self.contains(TechniqueFlags::NAKED_PAIRS) {
46            techniques.push("Naked Pairs");
47        }
48        if self.contains(TechniqueFlags::HIDDEN_PAIRS) {
49            techniques.push("Hidden Pairs");
50        }
51        if self.contains(TechniqueFlags::LOCKED_CANDIDATES) {
52            techniques.push("Locked Candidates");
53        }
54        if self.contains(TechniqueFlags::XWING) {
55            techniques.push("X-Wing");
56        }
57
58        write!(f, "{}", techniques.join(", "))
59    }
60}
61
62impl fmt::Display for SolvePath {
63    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64        let path: Vec<(usize, usize, u8, TechniqueFlags, &str)> = self
65            .steps
66            .iter()
67            .map(|step| match step {
68                crate::core::SolveStep::Placement {
69                    row,
70                    col,
71                    value,
72                    flags,
73                } => (*row, *col, *value, *flags, step.code()),
74                crate::core::SolveStep::CandidateElimination {
75                    row,
76                    col,
77                    value,
78                    flags,
79                } => (*row, *col, *value, *flags, step.code()),
80            })
81            .collect();
82
83        let formatted_lines = format_solve_path(&path, 5);
84        write!(f, "{}", formatted_lines.join("\n"))
85    }
86}
87
88/// Formats the Sudoku board into a grid representation.
89///
90/// This function takes a 9x9 Sudoku board and formats it into a grid with
91/// horizontal and vertical separators to visually distinguish the 3x3 boxes.
92/// Each cell is represented by its number, with empty cells shown as a dot (`.`).
93pub fn format_grid(board: &[[u8; 9]; 9]) -> Vec<String> {
94    let mut grid = Vec::new();
95    let horizontal_line = "+-------+-------+-------+";
96
97    grid.push(horizontal_line.to_string()); // Top line
98
99    for (r, row) in board.iter().enumerate().take(9) {
100        let mut line = String::from("|"); // Start of the row
101        for (c, &cell) in row.iter().enumerate().take(9) {
102            match cell {
103                0 => line.push_str(" ."), // Empty cell, two spaces for alignment
104                n => line.push_str(&format!(" {}", n)), // Number, two spaces for alignment
105            }
106            if (c + 1) % 3 == 0 {
107                line.push_str(" |"); // Vertical separator after every 3rd column
108            }
109        }
110        grid.push(line); // Add the row to the grid
111
112        if (r + 1) % 3 == 0 {
113            grid.push(horizontal_line.to_string()); // Horizontal separator after every 3rd row
114        }
115    }
116
117    grid
118}
119
120/// Formats the Sudoku board into a single line string representation.
121///
122/// This function converts the board into a single string where each number is
123/// represented by its digit, and empty cells are represented by a dot (`.`).
124pub fn format_line(board: &[[u8; 9]; 9]) -> String {
125    board
126        .iter()
127        .flatten()
128        .map(|&n| (n + b'0') as char)
129        .collect()
130}
131
132/// Formats a path of moves in the Sudoku solving process into a vector of strings.
133///
134/// This function takes a vector of tuples representing moves in the format `(row, column, value)`
135/// and formats them into a human-readable string. Each move is represented as `(row, column, value)`,
136/// where `row` and `column` are 1-based indices, and `value` is the number placed in that cell.
137pub fn format_solve_path(
138    path: &[(usize, usize, u8, TechniqueFlags, &str)],
139    chunk_size: usize,
140) -> Vec<String> {
141    if path.is_empty() {
142        return vec!["(No moves recorded)".to_string()];
143    }
144
145    let mut result = Vec::new();
146    let mut current_technique = None;
147    let mut current_moves = Vec::new();
148
149    for (r, c, val, flags, action) in path {
150        let technique_name = format!("{}", flags);
151
152        if current_technique.as_ref() != Some(&technique_name) {
153            // Flush previous technique's moves
154            if let Some(tech) = current_technique {
155                result.push(format!("{}:", tech));
156                // Break moves into chunks of 5 per line
157                for chunk in current_moves.chunks(chunk_size) {
158                    result.push(format!("  {}", chunk.join(" ")));
159                }
160                current_moves.clear();
161            }
162            current_technique = Some(technique_name);
163        }
164
165        current_moves.push(format!("R{}C{}={},A={}", r + 1, c + 1, val, action));
166    }
167
168    // Flush final technique
169    if let Some(tech) = current_technique {
170        result.push(format!("{}:", tech));
171        for chunk in current_moves.chunks(chunk_size) {
172            result.push(format!("  {}", chunk.join(" ")));
173        }
174    }
175
176    result
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_format_grid() {
185        let board = [
186            [5, 3, 0, 6, 7, 8, 9, 1, 2],
187            [6, 7, 2, 1, 9, 5, 3, 4, 8],
188            [1, 9, 8, 3, 4, 2, 5, 6, 7],
189            [8, 5, 9, 7, 6, 1, 4, 2, 3],
190            [4, 2, 6, 8, 5, 3, 7, 9, 1],
191            [7, 1, 3, 9, 2, 4, 8, 5, 6],
192            [9, 6, 1, 5, 3, 7, 2, 8, 4],
193            [2, 8, 7, 4, 1, 9, 6, 3, 5],
194            [3, 4, 5, 2, 8, 6, 1, 7, 9],
195        ];
196
197        let expected = vec![
198            "+-------+-------+-------+",
199            "| 5 3 . | 6 7 8 | 9 1 2 |",
200            "| 6 7 2 | 1 9 5 | 3 4 8 |",
201            "| 1 9 8 | 3 4 2 | 5 6 7 |",
202            "+-------+-------+-------+",
203            "| 8 5 9 | 7 6 1 | 4 2 3 |",
204            "| 4 2 6 | 8 5 3 | 7 9 1 |",
205            "| 7 1 3 | 9 2 4 | 8 5 6 |",
206            "+-------+-------+-------+",
207            "| 9 6 1 | 5 3 7 | 2 8 4 |",
208            "| 2 8 7 | 4 1 9 | 6 3 5 |",
209            "| 3 4 5 | 2 8 6 | 1 7 9 |",
210            "+-------+-------+-------+",
211        ];
212
213        assert_eq!(expected, format_grid(&board));
214    }
215
216    #[test]
217    fn test_format_line() {
218        let board = [
219            [5, 3, 0, 6, 7, 8, 9, 1, 2],
220            [6, 7, 2, 1, 9, 5, 3, 4, 8],
221            [1, 9, 8, 3, 4, 2, 5, 6, 7],
222            [8, 5, 9, 7, 6, 1, 4, 2, 3],
223            [4, 2, 6, 8, 5, 3, 7, 9, 1],
224            [7, 1, 3, 9, 2, 4, 8, 5, 6],
225            [9, 6, 1, 5, 3, 7, 2, 8, 4],
226            [2, 8, 7, 4, 1, 9, 6, 3, 5],
227            [3, 4, 5, 2, 8, 6, 1, 7, 9],
228        ];
229
230        let expected =
231            "530678912672195348198342567859761423426853791713924856961537284287419635345286179";
232        assert_eq!(expected, format_line(&board));
233    }
234
235    #[test]
236    fn test_format_grid_empty_board() {
237        let board = [[0; 9]; 9];
238
239        let expected = vec![
240            "+-------+-------+-------+",
241            "| . . . | . . . | . . . |",
242            "| . . . | . . . | . . . |",
243            "| . . . | . . . | . . . |",
244            "+-------+-------+-------+",
245            "| . . . | . . . | . . . |",
246            "| . . . | . . . | . . . |",
247            "| . . . | . . . | . . . |",
248            "+-------+-------+-------+",
249            "| . . . | . . . | . . . |",
250            "| . . . | . . . | . . . |",
251            "| . . . | . . . | . . . |",
252            "+-------+-------+-------+",
253        ];
254
255        assert_eq!(expected, format_grid(&board));
256    }
257
258    #[test]
259    fn test_format_line_empty_board() {
260        let board = [[0; 9]; 9];
261        let expected =
262            "000000000000000000000000000000000000000000000000000000000000000000000000000000000";
263        assert_eq!(expected, format_line(&board));
264    }
265
266    #[test]
267    fn test_display_empty_mask() {
268        let mask = TechniqueFlags::empty();
269        assert_eq!(format!("{}", mask), "None");
270    }
271
272    #[test]
273    fn test_display_single_technique() {
274        let mask = TechniqueFlags::NAKED_SINGLES;
275        assert_eq!(format!("{}", mask), "Naked Singles");
276
277        let mask = TechniqueFlags::XWING;
278        assert_eq!(format!("{}", mask), "X-Wing");
279    }
280
281    #[test]
282    fn test_display_multiple_techniques() {
283        let mask = TechniqueFlags::EASY;
284        assert_eq!(format!("{}", mask), "Naked Singles, Hidden Singles");
285
286        let mask = TechniqueFlags::NAKED_SINGLES
287            | TechniqueFlags::XWING
288            | TechniqueFlags::LOCKED_CANDIDATES;
289        assert_eq!(
290            format!("{}", mask),
291            "Naked Singles, Locked Candidates, X-Wing"
292        );
293    }
294}