--- /dev/null
+use std::error::Error;
+use std::path::Path;
+use std::collections::{HashMap, HashSet};
+
+#[derive(Copy, Clone, Debug)]
+struct Link {
+ c1: [u8; 2],
+ c2: [u8; 2],
+}
+
+impl Link {
+ pub fn new(link: &str) -> Self {
+ let (c1, c2) = link.split_once("-").unwrap();
+ let c1 = c1.as_bytes()[0..2].try_into().unwrap();
+ let c2 = c2.as_bytes()[0..2].try_into().unwrap();
+ Self { c1, c2 }
+ }
+
+ fn contains(&self, c: &[u8]) -> bool {
+ self.c1 == *c || self.c2 == *c
+ }
+}
+
+struct Puzzle {
+ links: Vec<Link>,
+ graph: HashMap<[u8; 2], HashSet<[u8; 2]>>,
+}
+
+impl Puzzle {
+ pub fn new(input: &str) -> Self {
+ let mut links: Vec<Link> = Vec::new();
+ let mut graph: HashMap<[u8; 2], HashSet<[u8; 2]>> = HashMap::new();
+ input.lines().for_each(|l| {
+ let link = Link::new(l);
+ links.push(link);
+
+ match graph.get_mut(&link.c1) {
+ Some(c_set) => { c_set.insert(link.c2); },
+ None => {
+ let mut c_set: HashSet<[u8; 2]> = HashSet::new();
+ c_set.insert(link.c2);
+ graph.insert(link.c1, c_set);
+ }
+ }
+
+ match graph.get_mut(&link.c2) {
+ Some(c_set) => { c_set.insert(link.c1); },
+ None => {
+ let mut c_set: HashSet<[u8; 2]> = HashSet::new();
+ c_set.insert(link.c1);
+ graph.insert(link.c2, c_set);
+ }
+ }
+ });
+
+ Self { links, graph }
+ }
+
+ fn cycles_of_size_3(&self) -> Vec<HashSet<[u8; 2]>> {
+ let mut cycles: Vec<HashSet<[u8; 2]>> = Vec::new();
+ self.graph.iter().for_each(|(c0, linked)| {
+ linked.iter().for_each(|c1| {
+ linked.iter()
+ .filter(|c2| *c2 != c1)
+ .for_each(|c2| {
+ if let Some(set1) = self.graph.get(c1) {
+ if set1.contains(c2) {
+ let cycle: HashSet<[u8; 2]> = HashSet::from([*c0, *c1, *c2]);
+ if !cycles.contains(&cycle) {
+ cycles.push(cycle);
+ }
+ }
+ }
+ });
+ });
+ });
+ cycles
+ }
+}
+
+fn run_part1(input: &str) -> Result<u64, Box<dyn Error>> {
+ println!("Running {} - part 1", get_day());
+
+ let puzzle = Puzzle::new(input);
+ let cycles = puzzle.cycles_of_size_3();
+ let res = cycles.iter()
+ .filter(|s| s.iter().any(|c| c[0] == b't'))
+ .count() as u64;
+ Ok(res)
+}
+
+fn run_part2(input: &str) -> Result<u32, Box<dyn Error>> {
+ println!("Running {} - part 2", get_day());
+
+ Ok(0)
+}
+
+pub fn run(input: &str) -> Result<(), Box<dyn Error>> {
+ let res = run_part1(input)?;
+ println!("{res}");
+
+ let res = run_part2(input)?;
+ println!("{res}");
+
+ Ok(())
+}
+
+fn get_day() -> String {
+ let filename = file!();
+ Path::new(filename).file_stem().unwrap().to_str().unwrap().to_string()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ static TEXT_INPUT: &str = "\
+kh-tc
+qp-kh
+de-cg
+ka-co
+yn-aq
+qp-ub
+cg-tb
+vc-aq
+tb-ka
+wh-tc
+yn-cg
+kh-ub
+ta-co
+de-co
+tc-td
+tb-wq
+wh-td
+ta-ka
+td-qp
+aq-cg
+wq-ub
+ub-vc
+de-ta
+wq-aq
+wq-vc
+wh-yn
+ka-de
+kh-ta
+co-tc
+wh-qp
+tb-vc
+td-yn";
+
+ #[test]
+ fn test_part1() {
+ assert_eq!(7, run_part1(TEXT_INPUT).unwrap());
+ }
+
+ #[test]
+ fn test_part2() {
+ assert_eq!(0, run_part2(TEXT_INPUT).unwrap());
+ }
+}