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
76 write!(f, "{}", techniques.join(", "))
77 }
78}
79
80impl fmt::Display for SolvePath {
82 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83 let formatted_lines = format_solve_path(self, 5);
84 write!(f, "{}", formatted_lines.join("\n"))
85 }
86}
87
88impl fmt::Display for SolveStep {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 SolveStep::Placement {
93 row,
94 col,
95 value,
96 flags,
97 step_number,
98 candidates_eliminated,
99 related_cell_count,
100 difficulty_point,
101 } => {
102 write!(
103 f,
104 "#{:3} | Value {value} is placed on R{row}C{col} by {flags} | elim:{} related:{} diff:{}",
105 step_number + 1,
106 bin(*candidates_eliminated).count_ones(),
107 related_cell_count,
108 difficulty_point
109 )
110 }
111 SolveStep::CandidateElimination {
112 row,
113 col,
114 value,
115 flags,
116 step_number,
117 candidates_eliminated,
118 related_cell_count,
119 difficulty_point,
120 } => {
121 write!(
122 f,
123 "#{:3} | Value {value} is eliminated from R{row}C{col} by {flags} | elim:{} related:{} diff:{}",
124 step_number + 1,
125 bin(*candidates_eliminated).count_ones() + 1, related_cell_count,
127 difficulty_point
128 )
129 }
130 }
131 }
132}
133
134fn bin(x: u32) -> u32 {
136 x
137}
138
139pub(crate) fn format_grid(board: &Board) -> Vec<String> {
145 let mut grid = Vec::new();
146 let horizontal_line = "+-------+-------+-------+";
147
148 grid.push(horizontal_line.to_string()); for (r, row) in board.cells.iter().enumerate().take(9) {
151 let mut line = String::from("|"); for (c, &cell) in row.iter().enumerate().take(9) {
153 match cell {
154 0 => line.push_str(" ."), n => line.push_str(&format!(" {n}")), }
157 if (c + 1) % 3 == 0 {
158 line.push_str(" |"); }
160 }
161 grid.push(line); if (r + 1) % 3 == 0 {
164 grid.push(horizontal_line.to_string()); }
166 }
167
168 grid
169}
170
171pub(crate) fn format_line(board: &Board) -> String {
176 board
177 .cells
178 .iter()
179 .flatten()
180 .map(|&n| (n + b'0') as char)
181 .collect()
182}
183
184pub(crate) fn format_solve_path(solve_path: &SolvePath, _chunk_size: usize) -> Vec<String> {
189 if solve_path.steps.is_empty() {
190 return vec!["(No moves recorded)".to_string()];
191 }
192
193 let mut result = Vec::new();
194 let mut current_technique = None;
195 let mut current_moves = Vec::new();
196
197 for step in &solve_path.steps {
198 let flags = match step {
199 SolveStep::Placement { flags, .. } | SolveStep::CandidateElimination { flags, .. } => {
200 *flags
201 }
202 };
203
204 let technique_name = format!("{flags}");
205
206 if current_technique.as_ref() != Some(&technique_name) {
207 if let Some(tech) = current_technique {
209 result.push(format!("{tech}:"));
210 for chunk in current_moves.chunks(1) {
212 let formatted_chunk: Vec<String> =
214 chunk.iter().map(|s| format!("{:<5}", s)).collect();
215 result.push(format!(" {}", formatted_chunk.join("")));
216 }
217 current_moves.clear();
218 }
219 current_technique = Some(technique_name);
220 }
221
222 let step_str = match step {
224 SolveStep::Placement {
225 row,
226 col,
227 value,
228 step_number,
229 candidates_eliminated,
230 related_cell_count,
231 difficulty_point,
232 ..
233 } => {
234 format!(
235 "#{} R{}C{}={} [E:{} R:{} D:{}]",
236 step_number + 1,
237 row + 1,
238 col + 1,
239 value,
240 candidates_eliminated,
241 related_cell_count,
242 difficulty_point
243 )
244 }
245 SolveStep::CandidateElimination {
246 row,
247 col,
248 value,
249 step_number,
250 candidates_eliminated,
251 related_cell_count,
252 difficulty_point,
253 ..
254 } => {
255 let total_elim = *candidates_eliminated + 1;
256 format!(
257 "#{} -{}@R{}C{} [E:{} R:{} D:{}]",
258 step_number + 1,
259 value,
260 row + 1,
261 col + 1,
262 total_elim,
263 related_cell_count,
264 difficulty_point
265 )
266 }
267 };
268
269 current_moves.push(step_str);
270 }
271
272 if let Some(tech) = current_technique {
274 result.push(format!("{tech}:"));
275 for chunk in current_moves.chunks(1) {
276 let formatted_chunk: Vec<String> = chunk.iter().map(|s| format!("{:<5}", s)).collect();
277 result.push(format!(" {}", formatted_chunk.join("")));
278 }
279 }
280
281 result
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::core::{SolvePath, SolveStep, TechniqueFlags};
288
289 #[test]
290 fn test_format_grid() {
291 let board = Board::new([
292 [5, 3, 0, 6, 7, 8, 9, 1, 2],
293 [6, 7, 2, 1, 9, 5, 3, 4, 8],
294 [1, 9, 8, 3, 4, 2, 5, 6, 7],
295 [8, 5, 9, 7, 6, 1, 4, 2, 3],
296 [4, 2, 6, 8, 5, 3, 7, 9, 1],
297 [7, 1, 3, 9, 2, 4, 8, 5, 6],
298 [9, 6, 1, 5, 3, 7, 2, 8, 4],
299 [2, 8, 7, 4, 1, 9, 6, 3, 5],
300 [3, 4, 5, 2, 8, 6, 1, 7, 9],
301 ]);
302
303 let expected = vec![
304 "+-------+-------+-------+",
305 "| 5 3 . | 6 7 8 | 9 1 2 |",
306 "| 6 7 2 | 1 9 5 | 3 4 8 |",
307 "| 1 9 8 | 3 4 2 | 5 6 7 |",
308 "+-------+-------+-------+",
309 "| 8 5 9 | 7 6 1 | 4 2 3 |",
310 "| 4 2 6 | 8 5 3 | 7 9 1 |",
311 "| 7 1 3 | 9 2 4 | 8 5 6 |",
312 "+-------+-------+-------+",
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
319 assert_eq!(expected, format_grid(&board));
320 }
321
322 #[test]
323 fn test_format_line() {
324 let board = Board::new([
325 [5, 3, 0, 6, 7, 8, 9, 1, 2],
326 [6, 7, 2, 1, 9, 5, 3, 4, 8],
327 [1, 9, 8, 3, 4, 2, 5, 6, 7],
328 [8, 5, 9, 7, 6, 1, 4, 2, 3],
329 [4, 2, 6, 8, 5, 3, 7, 9, 1],
330 [7, 1, 3, 9, 2, 4, 8, 5, 6],
331 [9, 6, 1, 5, 3, 7, 2, 8, 4],
332 [2, 8, 7, 4, 1, 9, 6, 3, 5],
333 [3, 4, 5, 2, 8, 6, 1, 7, 9],
334 ]);
335
336 let expected =
337 "530678912672195348198342567859761423426853791713924856961537284287419635345286179";
338 assert_eq!(expected, format_line(&board));
339 }
340
341 #[test]
342 fn test_format_grid_empty_board() {
343 let board = Board::default();
344
345 let expected = vec![
346 "+-------+-------+-------+",
347 "| . . . | . . . | . . . |",
348 "| . . . | . . . | . . . |",
349 "| . . . | . . . | . . . |",
350 "+-------+-------+-------+",
351 "| . . . | . . . | . . . |",
352 "| . . . | . . . | . . . |",
353 "| . . . | . . . | . . . |",
354 "+-------+-------+-------+",
355 "| . . . | . . . | . . . |",
356 "| . . . | . . . | . . . |",
357 "| . . . | . . . | . . . |",
358 "+-------+-------+-------+",
359 ];
360
361 assert_eq!(expected, format_grid(&board));
362 }
363
364 #[test]
365 fn test_format_line_empty_board() {
366 let board = Board::default();
367 let expected =
368 "000000000000000000000000000000000000000000000000000000000000000000000000000000000";
369 assert_eq!(expected, format_line(&board));
370 }
371
372 #[test]
373 fn test_display_empty_mask() {
374 let mask = TechniqueFlags::empty();
375 assert_eq!(format!("{mask}"), "None");
376 }
377
378 #[test]
379 fn test_display_single_technique() {
380 let mask = TechniqueFlags::NAKED_SINGLES;
381 assert_eq!(format!("{mask}"), "Naked Singles");
382
383 let mask = TechniqueFlags::X_WING;
384 assert_eq!(format!("{mask}"), "X-Wing");
385 }
386
387 #[test]
388 fn test_display_multiple_techniques() {
389 let mask = TechniqueFlags::EASY;
390 assert_eq!(format!("{mask}"), "Naked Singles, Hidden Singles");
391
392 let mask = TechniqueFlags::NAKED_SINGLES
393 | TechniqueFlags::X_WING
394 | TechniqueFlags::LOCKED_CANDIDATES;
395 assert_eq!(
396 format!("{mask}"),
397 "Naked Singles, Locked Candidates, X-Wing"
398 );
399 }
400
401 #[test]
402 fn test_empty_path() {
403 let solve_path = SolvePath { steps: Vec::new() }; let expected = vec!["(No moves recorded)"];
405 assert_eq!(format_solve_path(&solve_path, 5), expected);
406 }
407
408 #[test]
409 fn test_single_technique_multiple_moves_with_chunking() {
410 let steps = vec![
411 SolveStep::Placement {
412 row: 0,
413 col: 0,
414 value: 1,
415 flags: TechniqueFlags::NAKED_SINGLES,
416 step_number: 0,
417 candidates_eliminated: 9,
418 related_cell_count: 6,
419 difficulty_point: 1,
420 },
421 SolveStep::Placement {
422 row: 0,
423 col: 1,
424 value: 2,
425 flags: TechniqueFlags::NAKED_SINGLES,
426 step_number: 1,
427 candidates_eliminated: 8,
428 related_cell_count: 6,
429 difficulty_point: 1,
430 },
431 SolveStep::Placement {
432 row: 0,
433 col: 2,
434 value: 3,
435 flags: TechniqueFlags::NAKED_SINGLES,
436 step_number: 2,
437 candidates_eliminated: 7,
438 related_cell_count: 6,
439 difficulty_point: 1,
440 },
441 SolveStep::Placement {
442 row: 0,
443 col: 3,
444 value: 4,
445 flags: TechniqueFlags::NAKED_SINGLES,
446 step_number: 3,
447 candidates_eliminated: 6,
448 related_cell_count: 6,
449 difficulty_point: 1,
450 },
451 ];
452 let solve_path = SolvePath { steps };
453
454 let formatted = format_solve_path(&solve_path, 3);
455 assert_eq!(formatted[0], "Naked Singles:");
456 assert!(formatted[1].contains("#1 R1C1=1"));
458 assert!(formatted[2].contains("#2 R1C2=2"));
459 assert!(formatted[3].contains("#3 R1C3=3"));
460 assert!(formatted[4].contains("#4 R1C4=4"));
461 }
462
463 #[test]
464 fn test_multiple_techniques_and_mixed_chunking() {
465 let steps = vec![
466 SolveStep::Placement {
467 row: 0,
468 col: 0,
469 value: 1,
470 flags: TechniqueFlags::NAKED_SINGLES,
471 step_number: 0,
472 candidates_eliminated: 9,
473 related_cell_count: 6,
474 difficulty_point: 1,
475 },
476 SolveStep::Placement {
477 row: 1,
478 col: 0,
479 value: 3,
480 flags: TechniqueFlags::HIDDEN_SINGLES,
481 step_number: 1,
482 candidates_eliminated: 8,
483 related_cell_count: 9,
484 difficulty_point: 2,
485 },
486 SolveStep::CandidateElimination {
487 row: 2,
488 col: 0,
489 value: 6,
490 flags: TechniqueFlags::HIDDEN_PAIRS,
491 step_number: 2,
492 candidates_eliminated: 3,
493 related_cell_count: 4,
494 difficulty_point: 3,
495 },
496 ];
497 let solve_path = SolvePath { steps };
498
499 let formatted = format_solve_path(&solve_path, 3);
500 assert_eq!(formatted[0], "Naked Singles:");
501 assert!(formatted[1].contains("#1 R1C1=1"));
502 assert_eq!(formatted[2], "Hidden Singles:");
503 assert!(formatted[3].contains("#2 R2C1=3"));
504 assert_eq!(formatted[4], "Hidden Pairs:");
505 assert!(formatted[5].contains("#3 -6@R3C1"));
506 }
507}