Skip to main content

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