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 } => {
80 write!(f, "Value {value} is placed on R{row}C{col} by {flags}")
81 }
82 SolveStep::CandidateElimination {
83 row,
84 col,
85 value,
86 flags,
87 } => {
88 write!(
89 f,
90 "Value {value} is eliminated from R{row}C{col} by {flags}"
91 )
92 }
93 }
94 }
95}
96
97pub(crate) fn format_grid(board: &Board) -> Vec<String> {
103 let mut grid = Vec::new();
104 let horizontal_line = "+-------+-------+-------+";
105
106 grid.push(horizontal_line.to_string()); for (r, row) in board.cells.iter().enumerate().take(9) {
109 let mut line = String::from("|"); for (c, &cell) in row.iter().enumerate().take(9) {
111 match cell {
112 0 => line.push_str(" ."), n => line.push_str(&format!(" {n}")), }
115 if (c + 1) % 3 == 0 {
116 line.push_str(" |"); }
118 }
119 grid.push(line); if (r + 1) % 3 == 0 {
122 grid.push(horizontal_line.to_string()); }
124 }
125
126 grid
127}
128
129pub(crate) fn format_line(board: &Board) -> String {
134 board
135 .cells
136 .iter()
137 .flatten()
138 .map(|&n| (n + b'0') as char)
139 .collect()
140}
141
142pub(crate) fn format_solve_path(solve_path: &SolvePath, chunk_size: usize) -> Vec<String> {
148 if solve_path.steps.is_empty() {
149 return vec!["(No moves recorded)".to_string()];
150 }
151
152 let mut result = Vec::new();
153 let mut current_technique = None;
154 let mut current_moves = Vec::new();
155
156 for step in &solve_path.steps {
157 let (r, c, val, flags, action_code) = match step {
159 SolveStep::Placement {
160 row,
161 col,
162 value,
163 flags,
164 } => (*row, *col, *value, *flags, step.code()),
165 SolveStep::CandidateElimination {
166 row,
167 col,
168 value,
169 flags,
170 } => (*row, *col, *value, *flags, step.code()),
171 };
172
173 let technique_name = format!("{flags}");
174
175 if current_technique.as_ref() != Some(&technique_name) {
176 if let Some(tech) = current_technique {
178 result.push(format!("{tech}:"));
179 for chunk in current_moves.chunks(chunk_size) {
181 result.push(format!(" {}", chunk.join(" ")));
182 }
183 current_moves.clear();
184 }
185 current_technique = Some(technique_name);
186 }
187
188 current_moves.push(format!("R{}C{}={},A={}", r + 1, c + 1, val, action_code));
189 }
190
191 if let Some(tech) = current_technique {
193 result.push(format!("{tech}:"));
194 for chunk in current_moves.chunks(chunk_size) {
195 result.push(format!(" {}", chunk.join(" ")));
196 }
197 }
198
199 result
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::core::{SolvePath, SolveStep, TechniqueFlags};
206
207 #[test]
208 fn test_format_grid() {
209 let board = Board::new([
210 [5, 3, 0, 6, 7, 8, 9, 1, 2],
211 [6, 7, 2, 1, 9, 5, 3, 4, 8],
212 [1, 9, 8, 3, 4, 2, 5, 6, 7],
213 [8, 5, 9, 7, 6, 1, 4, 2, 3],
214 [4, 2, 6, 8, 5, 3, 7, 9, 1],
215 [7, 1, 3, 9, 2, 4, 8, 5, 6],
216 [9, 6, 1, 5, 3, 7, 2, 8, 4],
217 [2, 8, 7, 4, 1, 9, 6, 3, 5],
218 [3, 4, 5, 2, 8, 6, 1, 7, 9],
219 ]);
220
221 let expected = vec![
222 "+-------+-------+-------+",
223 "| 5 3 . | 6 7 8 | 9 1 2 |",
224 "| 6 7 2 | 1 9 5 | 3 4 8 |",
225 "| 1 9 8 | 3 4 2 | 5 6 7 |",
226 "+-------+-------+-------+",
227 "| 8 5 9 | 7 6 1 | 4 2 3 |",
228 "| 4 2 6 | 8 5 3 | 7 9 1 |",
229 "| 7 1 3 | 9 2 4 | 8 5 6 |",
230 "+-------+-------+-------+",
231 "| 9 6 1 | 5 3 7 | 2 8 4 |",
232 "| 2 8 7 | 4 1 9 | 6 3 5 |",
233 "| 3 4 5 | 2 8 6 | 1 7 9 |",
234 "+-------+-------+-------+",
235 ];
236
237 assert_eq!(expected, format_grid(&board));
238 }
239
240 #[test]
241 fn test_format_line() {
242 let board = Board::new([
243 [5, 3, 0, 6, 7, 8, 9, 1, 2],
244 [6, 7, 2, 1, 9, 5, 3, 4, 8],
245 [1, 9, 8, 3, 4, 2, 5, 6, 7],
246 [8, 5, 9, 7, 6, 1, 4, 2, 3],
247 [4, 2, 6, 8, 5, 3, 7, 9, 1],
248 [7, 1, 3, 9, 2, 4, 8, 5, 6],
249 [9, 6, 1, 5, 3, 7, 2, 8, 4],
250 [2, 8, 7, 4, 1, 9, 6, 3, 5],
251 [3, 4, 5, 2, 8, 6, 1, 7, 9],
252 ]);
253
254 let expected =
255 "530678912672195348198342567859761423426853791713924856961537284287419635345286179";
256 assert_eq!(expected, format_line(&board));
257 }
258
259 #[test]
260 fn test_format_grid_empty_board() {
261 let board = Board::default();
262
263 let expected = vec![
264 "+-------+-------+-------+",
265 "| . . . | . . . | . . . |",
266 "| . . . | . . . | . . . |",
267 "| . . . | . . . | . . . |",
268 "+-------+-------+-------+",
269 "| . . . | . . . | . . . |",
270 "| . . . | . . . | . . . |",
271 "| . . . | . . . | . . . |",
272 "+-------+-------+-------+",
273 "| . . . | . . . | . . . |",
274 "| . . . | . . . | . . . |",
275 "| . . . | . . . | . . . |",
276 "+-------+-------+-------+",
277 ];
278
279 assert_eq!(expected, format_grid(&board));
280 }
281
282 #[test]
283 fn test_format_line_empty_board() {
284 let board = Board::default();
285 let expected =
286 "000000000000000000000000000000000000000000000000000000000000000000000000000000000";
287 assert_eq!(expected, format_line(&board));
288 }
289
290 #[test]
291 fn test_display_empty_mask() {
292 let mask = TechniqueFlags::empty();
293 assert_eq!(format!("{mask}"), "None");
294 }
295
296 #[test]
297 fn test_display_single_technique() {
298 let mask = TechniqueFlags::NAKED_SINGLES;
299 assert_eq!(format!("{mask}"), "Naked Singles");
300
301 let mask = TechniqueFlags::XWING;
302 assert_eq!(format!("{mask}"), "X-Wing");
303 }
304
305 #[test]
306 fn test_display_multiple_techniques() {
307 let mask = TechniqueFlags::EASY;
308 assert_eq!(format!("{mask}"), "Naked Singles, Hidden Singles");
309
310 let mask = TechniqueFlags::NAKED_SINGLES
311 | TechniqueFlags::XWING
312 | TechniqueFlags::LOCKED_CANDIDATES;
313 assert_eq!(
314 format!("{mask}"),
315 "Naked Singles, Locked Candidates, X-Wing"
316 );
317 }
318
319 #[test]
320 fn test_empty_path() {
321 let solve_path = SolvePath { steps: Vec::new() }; let expected = vec!["(No moves recorded)"];
323 assert_eq!(format_solve_path(&solve_path, 5), expected);
324 }
325
326 #[test]
327 fn test_single_technique_multiple_moves_with_chunking() {
328 let steps = vec![
329 SolveStep::Placement {
331 row: 0,
332 col: 0,
333 value: 1,
334 flags: TechniqueFlags::NAKED_SINGLES,
335 },
336 SolveStep::Placement {
337 row: 0,
338 col: 1,
339 value: 2,
340 flags: TechniqueFlags::NAKED_SINGLES,
341 },
342 SolveStep::Placement {
343 row: 0,
344 col: 2,
345 value: 3,
346 flags: TechniqueFlags::NAKED_SINGLES,
347 },
348 SolveStep::Placement {
349 row: 0,
350 col: 3,
351 value: 4,
352 flags: TechniqueFlags::NAKED_SINGLES,
353 },
354 SolveStep::Placement {
355 row: 0,
356 col: 4,
357 value: 5,
358 flags: TechniqueFlags::NAKED_SINGLES,
359 },
360 SolveStep::Placement {
361 row: 0,
362 col: 5,
363 value: 6,
364 flags: TechniqueFlags::NAKED_SINGLES,
365 },
366 ];
367 let solve_path = SolvePath { steps }; let chunk_size = 2; let expected = vec![
371 "Naked Singles:",
372 " R1C1=1,A=plac R1C2=2,A=plac",
373 " R1C3=3,A=plac R1C4=4,A=plac",
374 " R1C5=5,A=plac R1C6=6,A=plac",
375 ];
376 assert_eq!(format_solve_path(&solve_path, chunk_size), expected);
377 }
378
379 #[test]
380 fn test_multiple_techniques_and_mixed_chunking() {
381 let steps = vec![
382 SolveStep::Placement {
383 row: 0,
384 col: 0,
385 value: 1,
386 flags: TechniqueFlags::NAKED_SINGLES,
387 },
388 SolveStep::Placement {
389 row: 0,
390 col: 1,
391 value: 2,
392 flags: TechniqueFlags::NAKED_SINGLES,
393 },
394 SolveStep::Placement {
395 row: 1,
396 col: 0,
397 value: 3,
398 flags: TechniqueFlags::HIDDEN_SINGLES,
399 },
400 SolveStep::Placement {
401 row: 1,
402 col: 1,
403 value: 4,
404 flags: TechniqueFlags::HIDDEN_SINGLES,
405 },
406 SolveStep::Placement {
407 row: 1,
408 col: 2,
409 value: 5,
410 flags: TechniqueFlags::HIDDEN_SINGLES,
411 },
412 SolveStep::CandidateElimination {
413 row: 2,
414 col: 0,
415 value: 6,
416 flags: TechniqueFlags::HIDDEN_PAIRS,
417 }, ];
419 let solve_path = SolvePath { steps }; let chunk_size = 3; let expected = vec![
423 "Naked Singles:",
424 " R1C1=1,A=plac R1C2=2,A=plac",
425 "Hidden Singles:",
426 " R2C1=3,A=plac R2C2=4,A=plac R2C3=5,A=plac",
427 "Hidden Pairs:",
428 " R3C1=6,A=elim",
429 ];
430 assert_eq!(format_solve_path(&solve_path, chunk_size), expected);
431 }
432}