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