1use crate::core::{Board, Solution, SolvePath, SolveStep, TechniqueFlags};
7use std::fmt;
8
9impl 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
18impl 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
27impl 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 {
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
70impl 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, related_cell_count,
109 difficulty_point
110 )
111 }
112 }
113 }
114}
115
116fn bin(x: u32) -> u32 {
118 x
119}
120
121pub(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()); for (r, row) in board.cells.iter().enumerate().take(9) {
133 let mut line = String::from("|"); for (c, &cell) in row.iter().enumerate().take(9) {
135 match cell {
136 0 => line.push_str(" ."), n => line.push_str(&format!(" {n}")), }
139 if (c + 1) % 3 == 0 {
140 line.push_str(" |"); }
142 }
143 grid.push(line); if (r + 1) % 3 == 0 {
146 grid.push(horizontal_line.to_string()); }
148 }
149
150 grid
151}
152
153pub(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
166pub(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 if let Some(tech) = current_technique {
191 result.push(format!("{tech}:"));
192 for chunk in current_moves.chunks(1) {
194 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 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 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() }; 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 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}