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 a way that is suitable for terminals.
5
6use crate::core::{Board, Solution, SolvePath, SolveStep, 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        write!(f, "\n{}", 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).join("\n"))?;
22        write!(f, "Line format: {}", format_line(self))?;
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
62/// Formats the solve path into a human-readable string representation.
63impl fmt::Display for SolvePath {
64    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65        let formatted_lines = format_solve_path(self, 5);
66        write!(f, "{}", formatted_lines.join("\n"))
67    }
68}
69
70/// Formats the solve step into a human-readable string representation.
71impl fmt::Display for SolveStep {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            SolveStep::Placement {
75                row,
76                col,
77                value,
78                flags,
79            } => {
80                write!(f, "Value {value} is placed on R{row}C{col} by {flags}")
81            }
82            SolveStep::CandidateElimination {
83                row,
84                col,
85                value,
86                flags,
87            } => {
88                write!(
89                    f,
90                    "Value {value} is eliminated from R{row}C{col} by {flags}"
91                )
92            }
93        }
94    }
95}
96
97/// Formats the Sudoku board into a grid representation.
98///
99/// This function takes a 9x9 Sudoku board and formats it into a grid with
100/// horizontal and vertical separators to visually distinguish the 3x3 boxes.
101/// Each cell is represented by its number, with empty cells shown as a dot (`.`).
102pub(crate) fn format_grid(board: &Board) -> Vec<String> {
103    let mut grid = Vec::new();
104    let horizontal_line = "+-------+-------+-------+";
105
106    grid.push(horizontal_line.to_string()); // Top line
107
108    for (r, row) in board.cells.iter().enumerate().take(9) {
109        let mut line = String::from("|"); // Start of the row
110        for (c, &cell) in row.iter().enumerate().take(9) {
111            match cell {
112                0 => line.push_str(" ."), // Empty cell, two spaces for alignment
113                n => line.push_str(&format!(" {n}")), // Number, two spaces for alignment
114            }
115            if (c + 1) % 3 == 0 {
116                line.push_str(" |"); // Vertical separator after every 3rd column
117            }
118        }
119        grid.push(line); // Add the row to the grid
120
121        if (r + 1) % 3 == 0 {
122            grid.push(horizontal_line.to_string()); // Horizontal separator after every 3rd row
123        }
124    }
125
126    grid
127}
128
129/// Formats the Sudoku board into a single line string representation.
130///
131/// This function converts the board into a single string where each number is
132/// represented by its digit, and empty cells are represented by a dot (`.`).
133pub(crate) fn format_line(board: &Board) -> String {
134    board
135        .cells
136        .iter()
137        .flatten()
138        .map(|&n| (n + b'0') as char)
139        .collect()
140}
141
142/// Formats a path of moves in the Sudoku solving process into a vector of strings.
143///
144/// This function takes a `SolvePath` struct and formats its moves into a human-readable string.
145/// Each move is represented as `(row, column, value)`, where `row` and `column` are 1-based indices,
146/// and `value` is the number placed in that cell.
147pub(crate) fn format_solve_path(solve_path: &SolvePath, chunk_size: usize) -> Vec<String> {
148    if solve_path.steps.is_empty() {
149        return vec!["(No moves recorded)".to_string()];
150    }
151
152    let mut result = Vec::new();
153    let mut current_technique = None;
154    let mut current_moves = Vec::new();
155
156    for step in &solve_path.steps {
157        // Iterate directly over the steps
158        let (r, c, val, flags, action_code) = match step {
159            SolveStep::Placement {
160                row,
161                col,
162                value,
163                flags,
164            } => (*row, *col, *value, *flags, step.code()),
165            SolveStep::CandidateElimination {
166                row,
167                col,
168                value,
169                flags,
170            } => (*row, *col, *value, *flags, step.code()),
171        };
172
173        let technique_name = format!("{flags}");
174
175        if current_technique.as_ref() != Some(&technique_name) {
176            // Flush previous technique's moves
177            if let Some(tech) = current_technique {
178                result.push(format!("{tech}:"));
179                // Break moves into chunks of 5 per line
180                for chunk in current_moves.chunks(chunk_size) {
181                    result.push(format!("  {}", chunk.join(" ")));
182                }
183                current_moves.clear();
184            }
185            current_technique = Some(technique_name);
186        }
187
188        current_moves.push(format!("R{}C{}={},A={}", r + 1, c + 1, val, action_code));
189    }
190
191    // Flush final technique
192    if let Some(tech) = current_technique {
193        result.push(format!("{tech}:"));
194        for chunk in current_moves.chunks(chunk_size) {
195            result.push(format!("  {}", chunk.join(" ")));
196        }
197    }
198
199    result
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::core::{SolvePath, SolveStep, TechniqueFlags};
206
207    #[test]
208    fn test_format_grid() {
209        let board = Board::new([
210            [5, 3, 0, 6, 7, 8, 9, 1, 2],
211            [6, 7, 2, 1, 9, 5, 3, 4, 8],
212            [1, 9, 8, 3, 4, 2, 5, 6, 7],
213            [8, 5, 9, 7, 6, 1, 4, 2, 3],
214            [4, 2, 6, 8, 5, 3, 7, 9, 1],
215            [7, 1, 3, 9, 2, 4, 8, 5, 6],
216            [9, 6, 1, 5, 3, 7, 2, 8, 4],
217            [2, 8, 7, 4, 1, 9, 6, 3, 5],
218            [3, 4, 5, 2, 8, 6, 1, 7, 9],
219        ]);
220
221        let expected = vec![
222            "+-------+-------+-------+",
223            "| 5 3 . | 6 7 8 | 9 1 2 |",
224            "| 6 7 2 | 1 9 5 | 3 4 8 |",
225            "| 1 9 8 | 3 4 2 | 5 6 7 |",
226            "+-------+-------+-------+",
227            "| 8 5 9 | 7 6 1 | 4 2 3 |",
228            "| 4 2 6 | 8 5 3 | 7 9 1 |",
229            "| 7 1 3 | 9 2 4 | 8 5 6 |",
230            "+-------+-------+-------+",
231            "| 9 6 1 | 5 3 7 | 2 8 4 |",
232            "| 2 8 7 | 4 1 9 | 6 3 5 |",
233            "| 3 4 5 | 2 8 6 | 1 7 9 |",
234            "+-------+-------+-------+",
235        ];
236
237        assert_eq!(expected, format_grid(&board));
238    }
239
240    #[test]
241    fn test_format_line() {
242        let board = Board::new([
243            [5, 3, 0, 6, 7, 8, 9, 1, 2],
244            [6, 7, 2, 1, 9, 5, 3, 4, 8],
245            [1, 9, 8, 3, 4, 2, 5, 6, 7],
246            [8, 5, 9, 7, 6, 1, 4, 2, 3],
247            [4, 2, 6, 8, 5, 3, 7, 9, 1],
248            [7, 1, 3, 9, 2, 4, 8, 5, 6],
249            [9, 6, 1, 5, 3, 7, 2, 8, 4],
250            [2, 8, 7, 4, 1, 9, 6, 3, 5],
251            [3, 4, 5, 2, 8, 6, 1, 7, 9],
252        ]);
253
254        let expected =
255            "530678912672195348198342567859761423426853791713924856961537284287419635345286179";
256        assert_eq!(expected, format_line(&board));
257    }
258
259    #[test]
260    fn test_format_grid_empty_board() {
261        let board = Board::default();
262
263        let expected = vec![
264            "+-------+-------+-------+",
265            "| . . . | . . . | . . . |",
266            "| . . . | . . . | . . . |",
267            "| . . . | . . . | . . . |",
268            "+-------+-------+-------+",
269            "| . . . | . . . | . . . |",
270            "| . . . | . . . | . . . |",
271            "| . . . | . . . | . . . |",
272            "+-------+-------+-------+",
273            "| . . . | . . . | . . . |",
274            "| . . . | . . . | . . . |",
275            "| . . . | . . . | . . . |",
276            "+-------+-------+-------+",
277        ];
278
279        assert_eq!(expected, format_grid(&board));
280    }
281
282    #[test]
283    fn test_format_line_empty_board() {
284        let board = Board::default();
285        let expected =
286            "000000000000000000000000000000000000000000000000000000000000000000000000000000000";
287        assert_eq!(expected, format_line(&board));
288    }
289
290    #[test]
291    fn test_display_empty_mask() {
292        let mask = TechniqueFlags::empty();
293        assert_eq!(format!("{mask}"), "None");
294    }
295
296    #[test]
297    fn test_display_single_technique() {
298        let mask = TechniqueFlags::NAKED_SINGLES;
299        assert_eq!(format!("{mask}"), "Naked Singles");
300
301        let mask = TechniqueFlags::XWING;
302        assert_eq!(format!("{mask}"), "X-Wing");
303    }
304
305    #[test]
306    fn test_display_multiple_techniques() {
307        let mask = TechniqueFlags::EASY;
308        assert_eq!(format!("{mask}"), "Naked Singles, Hidden Singles");
309
310        let mask = TechniqueFlags::NAKED_SINGLES
311            | TechniqueFlags::XWING
312            | TechniqueFlags::LOCKED_CANDIDATES;
313        assert_eq!(
314            format!("{mask}"),
315            "Naked Singles, Locked Candidates, X-Wing"
316        );
317    }
318
319    #[test]
320    fn test_empty_path() {
321        let solve_path = SolvePath { steps: Vec::new() }; // Create an empty SolvePath
322        let expected = vec!["(No moves recorded)"];
323        assert_eq!(format_solve_path(&solve_path, 5), expected);
324    }
325
326    #[test]
327    fn test_single_technique_multiple_moves_with_chunking() {
328        let steps = vec![
329            // Use the actual SolveStep enum variants
330            SolveStep::Placement {
331                row: 0,
332                col: 0,
333                value: 1,
334                flags: TechniqueFlags::NAKED_SINGLES,
335            },
336            SolveStep::Placement {
337                row: 0,
338                col: 1,
339                value: 2,
340                flags: TechniqueFlags::NAKED_SINGLES,
341            },
342            SolveStep::Placement {
343                row: 0,
344                col: 2,
345                value: 3,
346                flags: TechniqueFlags::NAKED_SINGLES,
347            },
348            SolveStep::Placement {
349                row: 0,
350                col: 3,
351                value: 4,
352                flags: TechniqueFlags::NAKED_SINGLES,
353            },
354            SolveStep::Placement {
355                row: 0,
356                col: 4,
357                value: 5,
358                flags: TechniqueFlags::NAKED_SINGLES,
359            },
360            SolveStep::Placement {
361                row: 0,
362                col: 5,
363                value: 6,
364                flags: TechniqueFlags::NAKED_SINGLES,
365            },
366        ];
367        let solve_path = SolvePath { steps }; // Create SolvePath with these steps
368        let chunk_size = 2; // Each line will have 2 moves
369
370        let expected = vec![
371            "Naked Singles:",
372            "  R1C1=1,A=plac R1C2=2,A=plac",
373            "  R1C3=3,A=plac R1C4=4,A=plac",
374            "  R1C5=5,A=plac R1C6=6,A=plac",
375        ];
376        assert_eq!(format_solve_path(&solve_path, chunk_size), expected);
377    }
378
379    #[test]
380    fn test_multiple_techniques_and_mixed_chunking() {
381        let steps = vec![
382            SolveStep::Placement {
383                row: 0,
384                col: 0,
385                value: 1,
386                flags: TechniqueFlags::NAKED_SINGLES,
387            },
388            SolveStep::Placement {
389                row: 0,
390                col: 1,
391                value: 2,
392                flags: TechniqueFlags::NAKED_SINGLES,
393            },
394            SolveStep::Placement {
395                row: 1,
396                col: 0,
397                value: 3,
398                flags: TechniqueFlags::HIDDEN_SINGLES,
399            },
400            SolveStep::Placement {
401                row: 1,
402                col: 1,
403                value: 4,
404                flags: TechniqueFlags::HIDDEN_SINGLES,
405            },
406            SolveStep::Placement {
407                row: 1,
408                col: 2,
409                value: 5,
410                flags: TechniqueFlags::HIDDEN_SINGLES,
411            },
412            SolveStep::CandidateElimination {
413                row: 2,
414                col: 0,
415                value: 6,
416                flags: TechniqueFlags::HIDDEN_PAIRS,
417            }, // Changed to CandidateElimination to match `elim` action code
418        ];
419        let solve_path = SolvePath { steps }; // Create SolvePath with these steps
420        let chunk_size = 3; // Each line will have 3 moves
421
422        let expected = vec![
423            "Naked Singles:",
424            "  R1C1=1,A=plac R1C2=2,A=plac",
425            "Hidden Singles:",
426            "  R2C1=3,A=plac R2C2=4,A=plac R2C3=5,A=plac",
427            "Hidden Pairs:",
428            "  R3C1=6,A=elim",
429        ];
430        assert_eq!(format_solve_path(&solve_path, chunk_size), expected);
431    }
432}