1use crate::puzzle::{Part, Puzzle};
9use std::{fs, path::PathBuf};
10
11pub trait AnswerCache {
13 fn correct(&self, puzzle: Puzzle, part: Part) -> Option<String>;
15
16 fn record(&self, puzzle: Puzzle, part: Part, answer: &str);
18}
19
20#[derive(Debug, Clone)]
22pub struct FileCache {
23 root: PathBuf,
24}
25
26impl FileCache {
27 #[must_use]
29 pub fn new(state_dir: impl Into<PathBuf>) -> Self {
30 Self {
31 root: state_dir.into().join("answers"),
32 }
33 }
34
35 fn path(&self, puzzle: Puzzle, part: Part) -> PathBuf {
36 self.root.join(format!(
37 "{}-{:02}-part{}",
38 puzzle.year.get(),
39 puzzle.day.get(),
40 part.number()
41 ))
42 }
43}
44
45impl AnswerCache for FileCache {
46 fn correct(&self, puzzle: Puzzle, part: Part) -> Option<String> {
47 let answer = fs::read_to_string(self.path(puzzle, part)).ok()?;
48 let answer = answer.trim().to_owned();
49
50 (!answer.is_empty()).then_some(answer)
51 }
52
53 fn record(&self, puzzle: Puzzle, part: Part, answer: &str) {
54 let path = self.path(puzzle, part);
55 if fs::create_dir_all(&self.root).is_ok() {
56 let _ = fs::write(path, answer);
57 }
58 }
59}
60
61#[cfg(test)]
62pub(crate) mod memory {
63 use super::{AnswerCache, Part, Puzzle};
64 use std::cell::RefCell;
65 use std::collections::HashMap;
66
67 #[derive(Debug, Default)]
68 pub(crate) struct MemoryCache {
69 entries: RefCell<HashMap<(Puzzle, Part), String>>,
70 }
71
72 impl MemoryCache {
73 pub(crate) fn new() -> Self {
74 Self::default()
75 }
76
77 pub(crate) fn seeded(puzzle: Puzzle, part: Part, answer: &str) -> Self {
78 let cache = Self::new();
79 cache.record(puzzle, part, answer);
80 cache
81 }
82 }
83
84 impl AnswerCache for MemoryCache {
85 fn correct(&self, puzzle: Puzzle, part: Part) -> Option<String> {
86 self.entries.borrow().get(&(puzzle, part)).cloned()
87 }
88
89 fn record(&self, puzzle: Puzzle, part: Part, answer: &str) {
90 self.entries
91 .borrow_mut()
92 .insert((puzzle, part), answer.to_owned());
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::puzzle::{Day, Year};
101
102 fn puzzle() -> Puzzle {
103 Puzzle::new(
104 Year::new(2024).expect("valid year"),
105 Day::new(7).expect("valid day"),
106 )
107 .expect("2024 has a day 7")
108 }
109
110 #[test]
111 fn remembers_answers_across_instances() {
112 let dir = tempfile::tempdir().expect("temp dir");
113 let cache = FileCache::new(dir.path());
114
115 assert_eq!(cache.correct(puzzle(), Part::One), None);
116 cache.record(puzzle(), Part::One, "1227");
117
118 let reopened = FileCache::new(dir.path());
119 assert_eq!(
120 reopened.correct(puzzle(), Part::One).as_deref(),
121 Some("1227")
122 );
123 assert_eq!(reopened.correct(puzzle(), Part::Two), None);
124 }
125
126 #[test]
127 fn keeps_parts_and_puzzles_apart() {
128 let dir = tempfile::tempdir().expect("temp dir");
129 let cache = FileCache::new(dir.path());
130 let other = Puzzle::new(
131 Year::new(2023).expect("valid year"),
132 Day::new(7).expect("valid day"),
133 )
134 .expect("2023 has a day 7");
135
136 cache.record(puzzle(), Part::One, "a");
137 cache.record(puzzle(), Part::Two, "b");
138 cache.record(other, Part::One, "c");
139
140 assert_eq!(cache.correct(puzzle(), Part::One).as_deref(), Some("a"));
141 assert_eq!(cache.correct(puzzle(), Part::Two).as_deref(), Some("b"));
142 assert_eq!(cache.correct(other, Part::One).as_deref(), Some("c"));
143 }
144
145 #[test]
146 fn an_unwritable_location_is_not_an_error() {
147 let dir = tempfile::tempdir().expect("temp dir");
148 let blocked = dir.path().join("state");
149 fs::write(&blocked, "a file, not a directory").expect("block the state directory");
150
151 let cache = FileCache::new(&blocked);
152
153 cache.record(puzzle(), Part::One, "1227");
154 assert_eq!(cache.correct(puzzle(), Part::One), None);
155 }
156}