rust_sudoku/
lib.rs

1//! # Rust sudoku solver
2//!
3//! Simple cli program to solve sudoku puzzles.
4//!
5//! ## Usage
6//!
7//! The program reads a puzzle reads a puzzle from a file or stdin using the
8//! following format:
9//!
10//! The puzzle is filled in left-right top-bottom where every number 1-9 is
11//! put into the current place in the puzzle, 0 or any other letter is considered
12//! a blank space, and any whitespace is ignored.
13
14/// The configuration of the project gotten from CLI.
15pub mod cli;
16
17use crate::cli::{Config, OutputStyle};
18use anyhow::{anyhow, Context};
19use std::{
20    fs::File,
21    io::{self, BufRead, Write},
22    path::PathBuf,
23};
24
25/// Sudoku solver. Takes in a config and will solve the puzzle.
26#[derive(Debug, Clone)]
27pub struct SudokuSolver {
28    config: Config,
29    root: Node,
30    solution: Option<Box<Node>>,
31}
32
33impl SudokuSolver {
34    /// Create solution from [`Config`].
35    pub fn new(config: Config) -> anyhow::Result<Self> {
36        let starting_point = parse(&config.input)?;
37        Ok(Self {
38            config,
39            root: Node {
40                candidate: starting_point,
41                ..Default::default()
42            },
43            solution: None,
44        })
45    }
46
47    /// Solves the puzzle and writes output to file specified in config.
48    pub fn solve(&mut self) -> anyhow::Result<()> {
49        self.solution = self.root.backtrack(&self.config);
50
51        self.output()?;
52
53        Ok(())
54    }
55
56    /// Get the solution the solver generates.
57    pub fn solution(&self) -> Option<[u8; 81]> {
58        let candidate = self.solution.as_ref()?.candidate;
59        Some(candidate)
60    }
61
62    fn output(&mut self) -> anyhow::Result<()> {
63        let solution = self
64            .solution
65            .as_ref()
66            .ok_or_else(|| anyhow!("Couldn't solve puzzle."))?;
67
68        solution.print_solution(&self.config)?;
69        Ok(())
70    }
71}
72
73#[derive(Debug, Clone)]
74struct Node {
75    /// This node's candidate puzzle.
76    pub candidate: [u8; 81],
77    /// Index of most recent child.
78    pub most_recent: usize,
79    /// Descendants of this node. The progression of the node.
80    pub children: [Option<Box<Node>>; 9],
81}
82
83impl Default for Node {
84    fn default() -> Self {
85        Self {
86            candidate: [0; 81],
87            // children: [None, None, None, None, None, None, None, None, None],
88            most_recent: 0,
89            children: Default::default(),
90        }
91    }
92}
93
94impl Node {
95    /// Recursively solve the puzzle.
96    fn backtrack(&mut self, config: &Config) -> Option<Box<Node>> {
97        // Print the partial solutions and add delay
98        if let Some(del) = config.delay {
99            std::thread::sleep(std::time::Duration::from_millis(del));
100        }
101        if config.print_partials {
102            self.print_solution(config).unwrap();
103        }
104
105        // Base cases
106        if self.reject() {
107            return None;
108        } else if self.accept() {
109            return Some(Box::new(self.clone()));
110        }
111
112        // recursively solve the puzzle
113        let to_change = self.first();
114        let mut next = self.children[self.most_recent].as_mut();
115
116        // create next 8 children
117        while let Some(child) = next {
118            if let Some(solution) = child.backtrack(config) {
119                return Some(solution);
120            }
121            if self.next(to_change).is_some() {
122                next = self.children[self.most_recent].as_mut();
123            } else {
124                break;
125            }
126        }
127
128        None
129    }
130
131    /// Generate first child of parent.
132    fn first(&mut self) -> usize {
133        let mut child = Node {
134            candidate: self.candidate,
135            ..Default::default()
136        };
137        let to_change = child
138            .candidate
139            .iter()
140            .position(|&x| x == 0)
141            .expect("Already checked that puzzle isn't complete in `backtrack`");
142
143        child.candidate[to_change] = 1;
144
145        // self.most_recent += 1;
146        self.children[self.most_recent] = Some(Box::new(child));
147
148        to_change
149    }
150
151    /// Generate next child after prev.
152    fn next(&mut self, to_change: usize) -> Option<()> {
153        if self.most_recent >= 8 {
154            return None;
155        }
156        let prev = &mut self.children[self.most_recent]
157            .as_mut()
158            .expect("Known to be valid since we're calling this next");
159
160        let mut child = Node {
161            candidate: prev.candidate,
162            ..Default::default()
163        };
164        child.candidate[to_change] += 1;
165
166        self.most_recent += 1;
167        self.children[self.most_recent] = Some(Box::new(child));
168
169        Some(())
170    }
171
172    /// Check if everysquare is full.
173    fn accept(&self) -> bool {
174        !self.candidate.contains(&0)
175    }
176
177    /// Check if candidate is still valid.
178    fn reject(&self) -> bool {
179        let mut counter = [0u8; 10]; // first element is for blanks
180        let candidate = &self.candidate;
181
182        // Check horizontal rows
183        for i in 0..9 {
184            for j in 0..9 {
185                counter[candidate[(i * 9 + j) as usize] as usize] += 1;
186            }
187
188            for j in counter.iter_mut().skip(1) {
189                if *j > 1 {
190                    return true;
191                }
192
193                *j = 0;
194            }
195        }
196
197        // check vertical rows
198        for i in 0..9 {
199            for j in 0..9 {
200                counter[candidate[(j * 9 + i) as usize] as usize] += 1;
201            }
202
203            for j in counter.iter_mut().skip(1) {
204                if *j > 1 {
205                    return true;
206                }
207
208                *j = 0;
209            }
210        }
211
212        // check squares
213        // traverse rows
214        for i in 0..3 {
215            // traverse columns
216            for j in 0..3 {
217                let offset: usize = (i * 27) + (j * 3);
218                // traverse row in square
219                for k in 0..3 {
220                    counter[candidate[offset + k * 9] as usize] += 1;
221                    counter[candidate[offset + k * 9 + 1] as usize] += 1;
222                    counter[candidate[offset + k * 9 + 2] as usize] += 1;
223                }
224
225                for k in counter.iter_mut().skip(1) {
226                    if *k > 1 {
227                        return true;
228                    }
229
230                    *k = 0;
231                }
232            }
233        }
234
235        false
236    }
237
238    /// Output puzzle.
239    fn print_solution(&self, config: &Config) -> anyhow::Result<()> {
240        let puzzle = &self.candidate;
241        let mut output = match &config.output {
242            Some(path) => Box::new(File::create(path).context("Could not open output file.")?)
243                as Box<dyn Write>,
244            None => Box::new(io::stdout()) as Box<dyn Write>,
245        };
246        match config.style {
247            OutputStyle::Bordered => {
248                for i in 0..3 {
249                    let mut offset = i * 27;
250                    output.write_all(b"+-------+-------+-------+\n")?;
251                    for _ in 0..3 {
252                        writeln!(
253                            output,
254                            "| {} {} {} | {} {} {} | {} {} {} |",
255                            puzzle[offset],
256                            puzzle[offset + 1],
257                            puzzle[offset + 2],
258                            puzzle[offset + 3],
259                            puzzle[offset + 4],
260                            puzzle[offset + 5],
261                            puzzle[offset + 6],
262                            puzzle[offset + 7],
263                            puzzle[offset + 8]
264                        )?;
265                        offset += 9;
266                    }
267                }
268                output.write_all(b"+-------+-------+-------+\n")?;
269            }
270            OutputStyle::MultiLine => {
271                for i in 0..9 {
272                    let offset = i * 9;
273                    writeln!(
274                        output,
275                        "{} {} {} {} {} {} {} {} {}",
276                        puzzle[offset],
277                        puzzle[offset + 1],
278                        puzzle[offset + 2],
279                        puzzle[offset + 3],
280                        puzzle[offset + 4],
281                        puzzle[offset + 5],
282                        puzzle[offset + 6],
283                        puzzle[offset + 7],
284                        puzzle[offset + 8]
285                    )?;
286                }
287                output.write_all(b"\n")?;
288            }
289            OutputStyle::Simple => {
290                for i in puzzle {
291                    write!(output, "{}", i)?;
292                }
293                writeln!(output)?;
294            }
295        }
296        Ok(())
297    }
298}
299
300/// Read input from file.
301fn parse(path: &Option<PathBuf>) -> anyhow::Result<[u8; 81]> {
302    let contents = match path {
303        Some(path) => std::fs::read_to_string(path).context("Could not read input file.")?,
304        None => {
305            let mut buf = String::new();
306            for line in io::stdin().lock().lines() {
307                buf.push_str(line?.as_str())
308            }
309            buf
310        }
311    };
312
313    let mut i = 0;
314    let mut puzzle = [0; 81];
315    for c in contents.chars() {
316        if c.is_whitespace() {
317            continue;
318        }
319        let this_square = c.to_digit(10).unwrap_or(0);
320        puzzle[i] = this_square as u8;
321        i += 1;
322        if i == 81 {
323            break;
324        }
325    }
326
327    if i < 81 {
328        Err(anyhow!(
329            "Not enough input. The file was not long enough to construct a complete puzzle."
330        ))
331    } else {
332        Ok(puzzle)
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use crate::cli::OutputStyle;
339
340    use super::*;
341
342    #[test]
343    fn sudoku1() {
344        let config = Config {
345            input: Some(PathBuf::from("puzzle/sudoku1.txt")),
346            output: None,
347            style: OutputStyle::Bordered,
348            print_partials: false,
349            delay: None,
350        };
351        let mut solution = SudokuSolver::new(config).unwrap();
352        solution.solve().unwrap();
353
354        let confirmed_solution: [u8; 81] = [
355            1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9, 1, 2, 3, 7, 8, 9, 1, 2, 3, 4, 5, 6, 2, 1,
356            4, 3, 6, 5, 8, 9, 7, 3, 6, 5, 8, 9, 7, 2, 1, 4, 8, 9, 7, 2, 1, 4, 3, 6, 5, 5, 3, 1, 6,
357            4, 2, 9, 7, 8, 6, 4, 2, 9, 7, 8, 5, 3, 1, 9, 7, 8, 5, 3, 1, 6, 4, 2,
358        ];
359        assert_eq!(solution.solution().unwrap(), confirmed_solution);
360    }
361
362    #[test]
363    #[should_panic]
364    fn not_enough_input() {
365        let config = Config {
366            input: Some(PathBuf::from("puzzle/sudoku8.txt")),
367            output: None,
368            print_partials: false,
369            style: OutputStyle::Bordered,
370            delay: None,
371        };
372        let mut solution = SudokuSolver::new(config).unwrap();
373        solution.solve().unwrap();
374    }
375}