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::X_WING) {
55 techniques.push("X-Wing");
56 }
57 if self.contains(TechniqueFlags::SWORDFISH) {
58 techniques.push("Swordfish");
59 }
60 if self.contains(TechniqueFlags::XY_WING) {
61 techniques.push("XY-Wing");
62 }
63 if self.contains(TechniqueFlags::XYZ_WING) {
64 techniques.push("XYZ-Wing");
65 }
66 if self.contains(TechniqueFlags::W_WING) {
67 techniques.push("W-Wing");
68 }
69 if self.contains(TechniqueFlags::NAKED_TRIPLES) {
70 techniques.push("Naked Triples");
71 }
72 if self.contains(TechniqueFlags::HIDDEN_TRIPLES) {
73 techniques.push("Hidden Triples");
74 }
75 if self.contains(TechniqueFlags::NAKED_QUADS) {
76 techniques.push("Naked Quads");
77 }
78 if self.contains(TechniqueFlags::HIDDEN_QUADS) {
79 techniques.push("Hidden Quads");
80 }
81 if self.contains(TechniqueFlags::JELLYFISH) {
82 techniques.push("Jellyfish");
83 }
84 if self.contains(TechniqueFlags::SKYSCRAPER) {
85 techniques.push("Skyscraper");
86 }
87 if self.contains(TechniqueFlags::ALTERNATING_INFERENCE_CHAIN) {
88 techniques.push("Alternating Inference Chain");
89 }
90
91 write!(f, "{}", techniques.join(", "))
92 }
93}
94
95impl fmt::Display for SolvePath {
97 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98 let formatted_lines = format_solve_path(self, 5);
99 write!(f, "{}", formatted_lines.join("\n"))
100 }
101}
102
103impl fmt::Display for SolveStep {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 SolveStep::Placement {
108 row,
109 col,
110 value,
111 flags,
112 step_number,
113 candidates_eliminated,
114 related_cell_count,
115 difficulty_point,
116 } => {
117 write!(
118 f,
119 "#{:3} | Value {value} is placed on R{row}C{col} by {flags} | elim:{} related:{} diff:{}",
120 step_number + 1,
121 bin(*candidates_eliminated).count_ones(),
122 related_cell_count,
123 difficulty_point
124 )
125 }
126 SolveStep::CandidateElimination {
127 row,
128 col,
129 value,
130 flags,
131 step_number,
132 candidates_eliminated,
133 related_cell_count,
134 difficulty_point,
135 } => {
136 write!(
137 f,
138 "#{:3} | Value {value} is eliminated from R{row}C{col} by {flags} | elim:{} related:{} diff:{}",
139 step_number + 1,
140 bin(*candidates_eliminated).count_ones() + 1, related_cell_count,
142 difficulty_point
143 )
144 }
145 }
146 }
147}
148
149fn bin(x: u32) -> u32 {
151 x
152}
153
154pub(crate) fn format_grid(board: &Board) -> Vec<String> {
160 let mut grid = Vec::new();
161 let horizontal_line = "+-------+-------+-------+";
162
163 grid.push(horizontal_line.to_string()); for (r, row) in board.cells.iter().enumerate().take(9) {
166 let mut line = String::from("|"); for (c, &cell) in row.iter().enumerate().take(9) {
168 match cell {
169 0 => line.push_str(" ."), n => line.push_str(&format!(" {n}")), }
172 if (c + 1) % 3 == 0 {
173 line.push_str(" |"); }
175 }
176 grid.push(line); if (r + 1) % 3 == 0 {
179 grid.push(horizontal_line.to_string()); }
181 }
182
183 grid
184}
185
186pub fn format_line(board: &Board) -> String {
191 board
192 .cells
193 .iter()
194 .flatten()
195 .map(|&n| (n + b'0') as char)
196 .collect()
197}
198
199pub(crate) fn format_solve_path(solve_path: &SolvePath, _chunk_size: usize) -> Vec<String> {
204 if solve_path.steps.is_empty() {
205 return vec!["(No moves recorded)".to_string()];
206 }
207
208 let mut result = Vec::new();
209 let mut current_technique = None;
210 let mut current_moves = Vec::new();
211
212 for step in &solve_path.steps {
213 let flags = match step {
214 SolveStep::Placement { flags, .. } | SolveStep::CandidateElimination { flags, .. } => {
215 *flags
216 }
217 };
218
219 let difficulty = flags.difficulty_name();
220 let technique_name = format!("{} ({})", flags, difficulty);
221
222 if current_technique.as_ref() != Some(&technique_name) {
223 if let Some(tech) = current_technique {
225 result.push(format!("{tech}:"));
226 for chunk in current_moves.chunks(1) {
228 let formatted_chunk: Vec<String> =
230 chunk.iter().map(|s| format!("{:<7}", s)).collect();
231 result.push(format!(" {}", formatted_chunk.join("")));
232 }
233 current_moves.clear();
234 }
235 current_technique = Some(technique_name);
236 }
237
238 let step_str = match step {
240 SolveStep::Placement {
241 row,
242 col,
243 value,
244 step_number,
245 candidates_eliminated,
246 related_cell_count,
247 difficulty_point,
248 ..
249 } => {
250 format!(
251 "#{} R{}C{}={} [E:{} R:{} D:{}]",
252 step_number + 1,
253 row + 1,
254 col + 1,
255 value,
256 candidates_eliminated,
257 related_cell_count,
258 difficulty_point
259 )
260 }
261 SolveStep::CandidateElimination {
262 row,
263 col,
264 value,
265 step_number,
266 candidates_eliminated,
267 related_cell_count,
268 difficulty_point,
269 ..
270 } => {
271 let total_elim = *candidates_eliminated + 1;
272 format!(
273 "#{} -{}@R{}C{} [E:{} R:{} D:{}]",
274 step_number + 1,
275 value,
276 row + 1,
277 col + 1,
278 total_elim,
279 related_cell_count,
280 difficulty_point
281 )
282 }
283 };
284
285 current_moves.push(step_str);
286 }
287
288 if let Some(tech) = current_technique {
290 result.push(format!("{tech}:"));
291 for chunk in current_moves.chunks(1) {
292 let formatted_chunk: Vec<String> = chunk.iter().map(|s| format!("{:<7}", s)).collect();
293 result.push(format!(" {}", formatted_chunk.join("")));
294 }
295 }
296
297 result
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::core::{SolvePath, SolveStep, TechniqueFlags};
304
305 #[test]
306 fn test_format_grid() {
307 let board = Board::new([
308 [5, 3, 0, 6, 7, 8, 9, 1, 2],
309 [6, 7, 2, 1, 9, 5, 3, 4, 8],
310 [1, 9, 8, 3, 4, 2, 5, 6, 7],
311 [8, 5, 9, 7, 6, 1, 4, 2, 3],
312 [4, 2, 6, 8, 5, 3, 7, 9, 1],
313 [7, 1, 3, 9, 2, 4, 8, 5, 6],
314 [9, 6, 1, 5, 3, 7, 2, 8, 4],
315 [2, 8, 7, 4, 1, 9, 6, 3, 5],
316 [3, 4, 5, 2, 8, 6, 1, 7, 9],
317 ]);
318
319 let expected = vec![
320 "+-------+-------+-------+",
321 "| 5 3 . | 6 7 8 | 9 1 2 |",
322 "| 6 7 2 | 1 9 5 | 3 4 8 |",
323 "| 1 9 8 | 3 4 2 | 5 6 7 |",
324 "+-------+-------+-------+",
325 "| 8 5 9 | 7 6 1 | 4 2 3 |",
326 "| 4 2 6 | 8 5 3 | 7 9 1 |",
327 "| 7 1 3 | 9 2 4 | 8 5 6 |",
328 "+-------+-------+-------+",
329 "| 9 6 1 | 5 3 7 | 2 8 4 |",
330 "| 2 8 7 | 4 1 9 | 6 3 5 |",
331 "| 3 4 5 | 2 8 6 | 1 7 9 |",
332 "+-------+-------+-------+",
333 ];
334
335 assert_eq!(expected, format_grid(&board));
336 }
337
338 #[test]
339 fn test_format_line() {
340 let board = Board::new([
341 [5, 3, 0, 6, 7, 8, 9, 1, 2],
342 [6, 7, 2, 1, 9, 5, 3, 4, 8],
343 [1, 9, 8, 3, 4, 2, 5, 6, 7],
344 [8, 5, 9, 7, 6, 1, 4, 2, 3],
345 [4, 2, 6, 8, 5, 3, 7, 9, 1],
346 [7, 1, 3, 9, 2, 4, 8, 5, 6],
347 [9, 6, 1, 5, 3, 7, 2, 8, 4],
348 [2, 8, 7, 4, 1, 9, 6, 3, 5],
349 [3, 4, 5, 2, 8, 6, 1, 7, 9],
350 ]);
351
352 let expected =
353 "530678912672195348198342567859761423426853791713924856961537284287419635345286179";
354 assert_eq!(expected, format_line(&board));
355 }
356
357 #[test]
358 fn test_format_grid_empty_board() {
359 let board = Board::default();
360
361 let expected = vec![
362 "+-------+-------+-------+",
363 "| . . . | . . . | . . . |",
364 "| . . . | . . . | . . . |",
365 "| . . . | . . . | . . . |",
366 "+-------+-------+-------+",
367 "| . . . | . . . | . . . |",
368 "| . . . | . . . | . . . |",
369 "| . . . | . . . | . . . |",
370 "+-------+-------+-------+",
371 "| . . . | . . . | . . . |",
372 "| . . . | . . . | . . . |",
373 "| . . . | . . . | . . . |",
374 "+-------+-------+-------+",
375 ];
376
377 assert_eq!(expected, format_grid(&board));
378 }
379
380 #[test]
381 fn test_format_line_empty_board() {
382 let board = Board::default();
383 let expected =
384 "000000000000000000000000000000000000000000000000000000000000000000000000000000000";
385 assert_eq!(expected, format_line(&board));
386 }
387
388 #[test]
389 fn test_display_empty_mask() {
390 let mask = TechniqueFlags::empty();
391 assert_eq!(format!("{mask}"), "None");
392 }
393
394 #[test]
395 fn test_display_single_technique() {
396 let mask = TechniqueFlags::NAKED_SINGLES;
397 assert_eq!(format!("{mask}"), "Naked Singles");
398
399 let mask = TechniqueFlags::X_WING;
400 assert_eq!(format!("{mask}"), "X-Wing");
401 }
402
403 #[test]
404 fn test_display_multiple_techniques() {
405 let mask = TechniqueFlags::EASY;
406 assert_eq!(format!("{mask}"), "Naked Singles, Hidden Singles");
407
408 let mask = TechniqueFlags::NAKED_SINGLES
409 | TechniqueFlags::X_WING
410 | TechniqueFlags::LOCKED_CANDIDATES;
411 assert_eq!(
412 format!("{mask}"),
413 "Naked Singles, Locked Candidates, X-Wing"
414 );
415 }
416
417 #[test]
418 fn test_empty_path() {
419 let solve_path = SolvePath { steps: Vec::new() }; let expected = vec!["(No moves recorded)"];
421 assert_eq!(format_solve_path(&solve_path, 5), expected);
422 }
423
424 #[test]
425 fn test_single_technique_multiple_moves_with_chunking() {
426 let steps = vec![
427 SolveStep::Placement {
428 row: 0,
429 col: 0,
430 value: 1,
431 flags: TechniqueFlags::NAKED_SINGLES,
432 step_number: 0,
433 candidates_eliminated: 9,
434 related_cell_count: 6,
435 difficulty_point: 1,
436 },
437 SolveStep::Placement {
438 row: 0,
439 col: 1,
440 value: 2,
441 flags: TechniqueFlags::NAKED_SINGLES,
442 step_number: 1,
443 candidates_eliminated: 8,
444 related_cell_count: 6,
445 difficulty_point: 1,
446 },
447 SolveStep::Placement {
448 row: 0,
449 col: 2,
450 value: 3,
451 flags: TechniqueFlags::NAKED_SINGLES,
452 step_number: 2,
453 candidates_eliminated: 7,
454 related_cell_count: 6,
455 difficulty_point: 1,
456 },
457 SolveStep::Placement {
458 row: 0,
459 col: 3,
460 value: 4,
461 flags: TechniqueFlags::NAKED_SINGLES,
462 step_number: 3,
463 candidates_eliminated: 6,
464 related_cell_count: 6,
465 difficulty_point: 1,
466 },
467 ];
468 let solve_path = SolvePath { steps };
469
470 let formatted = format_solve_path(&solve_path, 3);
471 assert_eq!(formatted[0], "Naked Singles (Easy):");
472 assert!(formatted[1].contains("#1 R1C1=1"));
474 assert!(formatted[2].contains("#2 R1C2=2"));
475 assert!(formatted[3].contains("#3 R1C3=3"));
476 assert!(formatted[4].contains("#4 R1C4=4"));
477 }
478
479 #[test]
480 fn test_multiple_techniques_and_mixed_chunking() {
481 let steps = vec![
482 SolveStep::Placement {
483 row: 0,
484 col: 0,
485 value: 1,
486 flags: TechniqueFlags::NAKED_SINGLES,
487 step_number: 0,
488 candidates_eliminated: 9,
489 related_cell_count: 6,
490 difficulty_point: 1,
491 },
492 SolveStep::Placement {
493 row: 1,
494 col: 0,
495 value: 3,
496 flags: TechniqueFlags::HIDDEN_SINGLES,
497 step_number: 1,
498 candidates_eliminated: 8,
499 related_cell_count: 9,
500 difficulty_point: 2,
501 },
502 SolveStep::CandidateElimination {
503 row: 2,
504 col: 0,
505 value: 6,
506 flags: TechniqueFlags::HIDDEN_PAIRS,
507 step_number: 2,
508 candidates_eliminated: 3,
509 related_cell_count: 4,
510 difficulty_point: 3,
511 },
512 ];
513 let solve_path = SolvePath { steps };
514
515 let formatted = format_solve_path(&solve_path, 3);
516 assert_eq!(formatted[0], "Naked Singles (Easy):");
517 assert!(formatted[1].contains("#1 R1C1=1"));
518 assert_eq!(formatted[2], "Hidden Singles (Easy):");
519 assert!(formatted[3].contains("#2 R2C1=3"));
520 assert_eq!(formatted[4], "Hidden Pairs (Medium):");
521 assert!(formatted[5].contains("#3 -6@R3C1"));
522 }
523
524 #[test]
525 fn test_all_techniques_formatted() {
526 let all_flags = vec![
527 (TechniqueFlags::NAKED_SINGLES, "Naked Singles", "Easy"),
528 (TechniqueFlags::HIDDEN_SINGLES, "Hidden Singles", "Easy"),
529 (TechniqueFlags::NAKED_PAIRS, "Naked Pairs", "Medium"),
530 (TechniqueFlags::HIDDEN_PAIRS, "Hidden Pairs", "Medium"),
531 (
532 TechniqueFlags::LOCKED_CANDIDATES,
533 "Locked Candidates",
534 "Medium",
535 ),
536 (TechniqueFlags::NAKED_TRIPLES, "Naked Triples", "Medium"),
537 (TechniqueFlags::HIDDEN_TRIPLES, "Hidden Triples", "Medium"),
538 (TechniqueFlags::X_WING, "X-Wing", "Hard"),
539 (TechniqueFlags::NAKED_QUADS, "Naked Quads", "Hard"),
540 (TechniqueFlags::HIDDEN_QUADS, "Hidden Quads", "Hard"),
541 (TechniqueFlags::SWORDFISH, "Swordfish", "Hard"),
542 (TechniqueFlags::JELLYFISH, "Jellyfish", "Hard"),
543 (TechniqueFlags::SKYSCRAPER, "Skyscraper", "Hard"),
544 (TechniqueFlags::W_WING, "W-Wing", "Expert"),
545 (TechniqueFlags::XY_WING, "XY-Wing", "Expert"),
546 (TechniqueFlags::XYZ_WING, "XYZ-Wing", "Expert"),
547 (
548 TechniqueFlags::ALTERNATING_INFERENCE_CHAIN,
549 "Alternating Inference Chain",
550 "Expert",
551 ),
552 ];
553
554 for (flag, expected_name, expected_difficulty) in all_flags {
555 let formatted_name = format!("{}", flag);
556 assert!(
557 formatted_name.contains(expected_name),
558 "Flag {:?} should contain name {}, but got {}",
559 flag,
560 expected_name,
561 formatted_name
562 );
563
564 let difficulty_name = flag.difficulty_name();
565 assert_eq!(
566 difficulty_name, expected_difficulty,
567 "Flag {:?} should have difficulty {}, but got {}",
568 flag, expected_difficulty, difficulty_name
569 );
570 }
571 }
572}