AoC 2024 days 1..7
authorPat Thoyts <pat.thoyts@gmail.com>
Sat, 7 Dec 2024 22:32:40 +0000 (22:32 +0000)
committerPat Thoyts <pat.thoyts@gmail.com>
Sat, 7 Dec 2024 22:32:40 +0000 (22:32 +0000)
21 files changed:
.gitignore [new file with mode: 0644]
day1/Cargo.toml [new file with mode: 0755]
day1/src/main.rs [new file with mode: 0755]
day2/Cargo.toml [new file with mode: 0644]
day2/__init__.py [new file with mode: 0644]
day2/__main__.py [new file with mode: 0644]
day2/src/main.rs [new file with mode: 0644]
day2/src/reports.rs [new file with mode: 0644]
day3/__init__.py [new file with mode: 0755]
day3/__main__.py [new file with mode: 0644]
day4/__init__.py [new file with mode: 0755]
day4/__main__.py [new file with mode: 0644]
day5/__init__.py [new file with mode: 0644]
day5/__main__.py [new file with mode: 0644]
day5/test_problem.py [new file with mode: 0644]
day6/__init__.py [new file with mode: 0644]
day6/__main__.py [new file with mode: 0644]
day7/__init__.py [new file with mode: 0644]
day7/__main__.py [new file with mode: 0644]
day_template/__init__.py [new file with mode: 0644]
day_template/__main__.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..fec9490
--- /dev/null
@@ -0,0 +1,5 @@
+Cargo.lock
+target/
+data/
+__pycache__/
+.pytest_cache/
diff --git a/day1/Cargo.toml b/day1/Cargo.toml
new file mode 100755 (executable)
index 0000000..fdd2f19
--- /dev/null
@@ -0,0 +1,7 @@
+[package]
+name = "day1"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clap = { version = "4.5.22", features = ["derive"] }
diff --git a/day1/src/main.rs b/day1/src/main.rs
new file mode 100755 (executable)
index 0000000..5e8ae7d
--- /dev/null
@@ -0,0 +1,104 @@
+extern crate clap;
+
+use std::error::Error;
+use std::fs::File;
+use std::io::{prelude::*, BufReader};
+use std::collections::BTreeMap;
+
+use clap::{Parser, ArgAction};
+
+/// Advent of Code 2023 day 4
+#[derive(Parser, Default, Debug)]
+struct Arguments {
+    /// specify the input data filename
+    filename: String,
+    #[arg(short, long, action=ArgAction::SetTrue)]
+    /// enable additional debug output
+    debug: Option<bool>,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct ParseRowError;
+
+fn from_str(s: &str) -> Result<(i64, i64), ParseRowError> {
+    let (a, b) = s.trim().split_once(' ').ok_or(ParseRowError)?;
+    let l = a.trim().parse::<i64>().unwrap();
+    let r = b.trim().parse::<i64>().unwrap();
+    Ok((l, r))
+}
+
+fn main() -> Result<(), Box<dyn Error>> {
+    let args = Arguments::parse();
+
+    let input = File::open(args.filename).expect("no such file");
+    let buffered = BufReader::new(input);
+
+    let (mut a, mut b): (Vec<_>,Vec<_>) = buffered.lines()
+        .map(|x| x.expect("invalid line"))
+        .map(|line| from_str(&line).unwrap())
+        .into_iter()
+        .unzip();
+    a.sort();
+    b.sort();
+    let cols = a.iter().zip(b).collect::<Vec<_>>();
+    let part1 = cols.iter().map(|(a, b)| (b - *a).abs()).sum::<i64>();
+    println!("part 1: {}", part1);
+
+    //let mut lset = BTreeMap::new();
+    let mut rset = BTreeMap::new();
+    for (_, r) in cols {
+        // lset.entry(l).and_modify(|v| *v += 1).or_insert(1_i64);
+        rset.entry(r).and_modify(|v| *v += 1).or_insert(1_i64);
+    }
+
+    let mut part2 = 0_i64;
+    for k in a {
+        let count = *match rset.get(&k) {
+            Some(val) => val,
+            None => &0_i64
+        };
+        let s = k * count;
+        if args.debug.unwrap() {
+            println!("k:{} n:{} s:{}", k, count, s);
+        }
+        part2 += s;
+    }
+    println!("part 2: {}", part2);
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_lists_parse_rows() {
+        let input = "3   4\n4   3\n2   5\n1   3\n3   9\n3   3";
+
+        let (mut a, mut b): (Vec<_>,Vec<_>) = input.split('\n')
+            .map(|line| line.trim().split_once(' ').unwrap())
+            .map(|(a, b)| (a.trim().parse::<i64>().unwrap(), b.trim().parse::<i64>().unwrap()))
+            .into_iter()
+            .unzip();
+
+        a.sort();
+        assert_eq!(a, vec![1, 2, 3, 3, 3, 4]);
+        b.sort();
+        assert_eq!(b, vec![3, 3, 3, 4, 5, 9]);
+        
+        let mut cols = a.into_iter().zip(b);
+        //assert_eq!(cols.next(), Some((1_i64, 3_i64)));
+        //assert_eq!(cols.next(), Some((2, 3)));
+        //assert_eq!(cols.next(), Some((3, 3)));
+        //assert_eq!(cols.next(), Some((3, 4)));
+        //assert_eq!(cols.next(), Some((3, 5)));
+        //assert_eq!(cols.next(), Some((4, 9)));
+
+        let sum = cols.map(|(a, b)| (b - a).abs()).sum::<i64>();
+        assert_eq!(sum, 11);
+    }
+
+    //vec![1, 2, 3, 3, 3, 4],
+    //vec![3, 3, 3, 4, 5, 9],
+}
\ No newline at end of file
diff --git a/day2/Cargo.toml b/day2/Cargo.toml
new file mode 100644 (file)
index 0000000..7d48314
--- /dev/null
@@ -0,0 +1,7 @@
+[package]
+name = "day2"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clap = { version = "4.5.22", features = ["derive"] }
diff --git a/day2/__init__.py b/day2/__init__.py
new file mode 100644 (file)
index 0000000..d0deec4
--- /dev/null
@@ -0,0 +1,65 @@
+from typing import List
+from dataclasses import dataclass
+
+
+def is_safe(diff) -> bool:
+    rising = all([d > 0 for d in diff])
+    falling = all([d < 0 for d in diff])
+    small = all([abs(d) < 4 for d in diff])
+    return (rising or falling) and small
+
+
+@dataclass
+class Record:
+    data: List[int]
+    diff: List[int]
+
+    def is_safe(self) -> bool:
+        return is_safe(self.diff)
+    
+    def is_safe_dampened(self, debug=False) -> bool:
+        safe = False
+        for index, value in enumerate(self.data):
+            test = self.data.copy()
+            del test[index]  # remove element by index
+            diff = Record.difference(test)
+            safe = is_safe(diff)
+            if debug:
+                print(f"value: {value} test:{test} diff:{diff} result:{safe}")
+            if safe:
+                break
+        return safe
+
+    @staticmethod
+    def difference(data: List[int]) -> List[int]:
+        diff = []
+        a = data[0]
+        for d in data[1:]:
+            diff.append(d - a)
+            a = d
+        return diff
+
+    @staticmethod
+    def from_str(s: str) -> 'Record':
+        data = [int(v) for v in s.strip().split(' ')]
+        diff = Record.difference(data)
+        return Record(data, diff)
+
+
+@dataclass
+class Problem:
+    records: List[Record]
+
+    def run(self, part2=False, debug=False) -> int:
+        safe = 0
+        for record in self.records:
+            if record.is_safe():
+                safe += 1
+            elif part2 and record.is_safe_dampened(debug=debug):
+                safe += 1
+        return safe
+
+    @staticmethod
+    def from_stream(stream):
+       return Problem([Record.from_str(line) for line in stream])
\ No newline at end of file
diff --git a/day2/__main__.py b/day2/__main__.py
new file mode 100644 (file)
index 0000000..1f52518
--- /dev/null
@@ -0,0 +1,28 @@
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description="AOC 2024 day 2")
+    parser.add_argument('filename', type=str)
+    parser.add_argument('-d', '--debug', action='store_true')
+    parser.add_argument('-2', '--part2', action='store_true')
+    options = parser.parse_args(args)
+
+    with open(options.filename) as f:
+        problem = Problem.from_stream(f)
+    # if options.debug:
+    #     for record in problem.records:
+    #         print(record.data, record.diff, record.is_safe())
+
+    if options.part2:
+        print(f"part2: {problem.run(part2=True, debug=options.debug)} safe reports.")
+    else:
+        print(f"part1: {problem.run()} safe reports.")
+        
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/day2/src/main.rs b/day2/src/main.rs
new file mode 100644 (file)
index 0000000..5e4db68
--- /dev/null
@@ -0,0 +1,52 @@
+extern crate clap;
+
+pub mod reports;
+use reports::Report;
+
+use std::error::Error;
+use std::fs::File;
+use std::io::{prelude::*, BufReader};
+use std::str::FromStr;
+
+use clap::{Parser, ArgAction};
+
+/// Advent of Code 2024 day 2
+#[derive(Parser, Default, Debug)]
+struct Arguments {
+    /// specify the input file name
+    filename: String,
+    #[arg(short, long, action=ArgAction::SetTrue)]
+    /// enable debug output
+    debug: Option<bool>,
+}
+
+
+fn parse_input(filename: &str) -> Vec<Report> {
+    let input = File::open(filename).expect("file not found");
+    let buffered = BufReader::new(input);
+    buffered.lines()
+        .map(|line| Report::from_str(line.unwrap().as_str()).unwrap())
+        .collect()
+}
+
+fn part1(data: &Vec<Report>, debug: bool) -> i64 {
+    if debug {
+        for datum in data {
+            for d in datum.data.iter() {
+                print!("{} ", d);
+            }
+            println!("");   
+        }
+    }
+    0_i64
+}
+
+fn main() -> Result<(), Box<dyn Error>> {
+    let args = Arguments::parse();
+    let debug = args.debug.unwrap_or(false);
+    let input = parse_input(&args.filename);
+    println!("part 1: {}", part1(&input, debug));
+    //println!("part 2: {}", part2(&args.filename));
+
+    Ok(())
+}
diff --git a/day2/src/reports.rs b/day2/src/reports.rs
new file mode 100644 (file)
index 0000000..4c396e8
--- /dev/null
@@ -0,0 +1,89 @@
+use std::str::FromStr;
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct ParseReportError;
+
+pub type Level = i64;
+// pub type Report = Vec<Level>;
+
+#[derive(Debug, PartialEq)]
+pub struct Report {
+    pub data: Vec<Level>
+}
+
+impl FromStr for Report {
+    type Err = ParseReportError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let report = s
+            .trim()
+            .split_whitespace()
+            .map(|x| x.parse::<Level>().unwrap())
+            .collect::<Vec<Level>>();
+        Ok( Report {data: report })
+    }
+}
+
+impl Report {
+    pub fn is_safe(&self) -> bool {
+
+        let mut i = self.data.iter();
+        let a = i.next();
+        let y = i.scan(a, |state, &x| {
+            let xx = x - *state;
+            *state = x;
+            Some(xx)
+        });
+
+        for x in y {
+            print!("{}, ", x);
+        }
+
+        let deltas = self.data
+            .chunks_exact(2)
+            .map(|x| x.to_vec())
+            .map(|x| x[1] - x[0])
+            .collect::<Vec<i64>>();
+
+        print!("[ ");
+        for x in deltas.clone() {
+            print!("{} ", x);
+        }
+
+        let allinc = deltas.clone().iter().all(|x| x > &0);
+        let alldec = deltas.clone().iter().all(|x| x < &0);
+        let allsmall = deltas.clone().iter().all(|x| x.abs() < 3);
+        println!("] inc {} dec {} small {}", allinc, alldec, allsmall);
+        (allinc || alldec) && allsmall
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_report_from_strln() {
+        let input = "7 6 4 2 1\n";
+        let result = Report::from_str(input).unwrap();
+        assert_eq!(result.data, vec![7_i64, 6, 4, 2, 1]);
+    }
+    #[test]
+    fn test_report_from_str() {
+        let input = "7 6 4 2 1";
+        let result = Report::from_str(input).unwrap();
+        assert_eq!(result.data, vec![7_i64, 6, 4, 2, 1]);
+    }
+    #[test]
+    fn test_report_is_safe_safe() {
+        let input = "7 6 4 2 1";
+        let result = Report::from_str(input).unwrap();
+        assert_eq!(result.is_safe(), true);
+    }   
+    #[test]
+    fn test_report_is_safe_unsafe() {
+        let input = "1 2 7 8 9";
+        let result = Report::from_str(input).unwrap();
+        assert_eq!(result.is_safe(), false);
+    }
+}
diff --git a/day3/__init__.py b/day3/__init__.py
new file mode 100755 (executable)
index 0000000..ef1c9d0
--- /dev/null
@@ -0,0 +1,89 @@
+from enum import Enum\r
+from dataclasses import dataclass\r
+\r
+\r
+class State(Enum):\r
+    Junk = 0,\r
+    M = 1,\r
+    U = 2,\r
+    L = 3,\r
+    Open = 4,\r
+    Second = 5\r
+    D = 6\r
+    Oh = 7\r
+    N = 8\r
+    Apos = 9\r
+    T = 10\r
+    Open2 = 11\r
+\r
+\r
+@dataclass\r
+class Problem:\r
+    data: list\r
+\r
+    def run(self) -> int:\r
+        return sum([a * b for (a, b) in self.data])\r
+\r
+    @staticmethod\r
+    def from_stream(stream, part2=False):\r
+        data = []\r
+        state = State.Junk\r
+        dostate = State.Junk\r
+        enabled = True\r
+        while True:\r
+            c = stream.read(1)\r
+            if not c:\r
+                break\r
+            if part2 and state == State.Junk:\r
+                if c == 'd' and dostate == State.Junk:\r
+                    dostate = State.D\r
+                elif c == 'o' and dostate == State.D:\r
+                    dostate = State.Oh\r
+                elif c == '(' and dostate == State.Oh:\r
+                    dostate = State.Open\r
+                elif c == ')' and dostate == State.Open:\r
+                    enabled = True\r
+                    dostate = State.Junk\r
+                elif c == 'n' and dostate == State.Oh:\r
+                    dostate = State.N\r
+                elif c == '\'' and dostate == State.N:\r
+                    dostate = State.Apos\r
+                elif c == 't' and dostate == State.Apos:\r
+                    dostate = State.T\r
+                elif c == '(' and dostate == State.T:\r
+                    dostate = State.Open2\r
+                elif c == ')' and dostate == State.Open2:\r
+                    dostate = State.Junk\r
+                    enabled = False\r
+                elif c == 'm':\r
+                    state = State.M\r
+            elif state == State.Junk and c == 'm':\r
+                state = State.M\r
+            elif state == State.M and c == 'u':\r
+                state = State.U\r
+            elif state == State.U and c == 'l':\r
+                state = State.L\r
+            elif state == State.L and c == '(':\r
+                state = State.Open\r
+                pair = ["", ""]\r
+            elif state == State.Open:\r
+                if c == ',':\r
+                    state = State.Second\r
+                elif c.isdigit() and len(pair[0]) < 3:\r
+                    pair[0] += c\r
+                else:\r
+                    state = State.Junk\r
+            elif state == State.Second:\r
+                if c == ')':\r
+                    if enabled:\r
+                        step = int(pair[0]), int(pair[1])\r
+                        data.append(step)\r
+                    state = State.Junk\r
+                elif c.isdigit() and len(pair[1]) < 3:\r
+                    pair[1] += c\r
+                else:\r
+                    state = State.Junk\r
+            else:\r
+                state = State.Junk\r
+\r
+        return Problem(data)\r
diff --git a/day3/__main__.py b/day3/__main__.py
new file mode 100644 (file)
index 0000000..ccf8c8e
--- /dev/null
@@ -0,0 +1,23 @@
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description="AOC 2024 day 3")
+    parser.add_argument('filename', type=str)
+    parser.add_argument('-d', '--debug', action='store_true')
+    parser.add_argument('-2', '--part2', action='store_true')
+    options = parser.parse_args(args)
+
+    with open(options.filename) as f:
+        problem = Problem.from_stream(f, options.part2)
+    if options.debug:
+        for row in problem.data:
+            print(row)
+    print(f"result {problem.run()}")
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/day4/__init__.py b/day4/__init__.py
new file mode 100755 (executable)
index 0000000..647feda
--- /dev/null
@@ -0,0 +1,95 @@
+from enum import Enum\r
+from dataclasses import dataclass\r
+from typing import List\r
+\r
+\r
+@dataclass\r
+class Problem:\r
+    data: List[str]\r
+    word: str = 'XMAS'\r
+\r
+    def run(self, part2: bool = False) -> int:\r
+        if part2:\r
+            return self.run_part2()\r
+        else:\r
+            return self.run_part1()\r
+\r
+    def run_part1(self) -> int:\r
+        result = 0\r
+        rows = len(self.data)\r
+        cols = len(self.data[0])\r
+        for r in range(rows):\r
+            for c in range(cols):\r
+                result += self.wordcount(r, c)\r
+        return result\r
+\r
+    def wordcount(self, row: int, col: int) -> int:\r
+        """if the current position is the start of our word """\r
+        result = 0\r
+        if self.data[row][col] == self.word[0]:\r
+            result += self.find_word(row, col, self.word[1])\r
+        return result\r
+\r
+    def find_word(self, row: int, col: int, want: str) -> int:\r
+        """check the Moore neighbourhood for the given letter and return\r
+        the number of words starting here."""\r
+        count = 0\r
+        for dr in [-1, 0, 1]:\r
+            r = row + dr\r
+            if r >= 0 and r < len(self.data):\r
+                for dc in [-1, 0, 1]:\r
+                    c = col + dc\r
+                    if c >= 0 and c < len(self.data[0]):\r
+                        letter = self.data[r][c]\r
+                        if letter == want:\r
+                            dir = (dr, dc)\r
+                            if self.verify((r, c), dir, 2):\r
+                                count += 1\r
+        return count\r
+\r
+    def verify(self, pos: tuple, dir: tuple, start: int) -> bool:\r
+        """Given a position and direction vector check that we have a full\r
+        word returning True if so or False if not."""\r
+        for n in range(start, len(self.word)):\r
+            newr = pos[0] + dir[0]\r
+            newc = pos[1] + dir[1]\r
+            if newr >= 0 and newr < len(self.data) and newc >= 0 and newc < len(self.data[0]):\r
+                pos = (newr, newc)\r
+                check = self.data[pos[0]][pos[1]]\r
+                if self.word[n] != check:\r
+                    return False\r
+            else:\r
+                return False\r
+        return True\r
+\r
+    def run_part2(self) -> int:\r
+        result = 0\r
+        rows = len(self.data)\r
+        cols = len(self.data[0])\r
+        for r in range(rows):\r
+            for c in range(cols):\r
+                if self.data[r][c] == 'A':\r
+                    result += self.find_mas(r, c)\r
+        return result\r
+\r
+    def find_mas(self, row: int, col: int) -> int:\r
+        count = 0\r
+        for dr in [-1, 1]:\r
+            r = row + dr\r
+            r2 = row - dr\r
+            if r >= 0 and r < len(self.data) and r2 >= 0 and r2 < len(self.data):\r
+                for dc in [-1, 1]:\r
+                    c = col + dc\r
+                    c2 = col - dc\r
+                    if c >= 0 and c < len(self.data[0]) and c2 >= 0 and c2 < len(self.data):\r
+                        letter = self.data[r][c]\r
+                        opposite = self.data[r2][c2]\r
+                        if letter == 'M' and opposite == 'S':\r
+                            # print((row, col), (r, c), (r2, c2), letter, 'A', opposite)\r
+                            count += 1\r
+        return 1 if count == 2 else 0\r
+\r
+    @staticmethod\r
+    def from_stream(stream) -> 'Problem':\r
+        data = [line.strip() for line in stream.readlines()]\r
+        return Problem(data)\r
diff --git a/day4/__main__.py b/day4/__main__.py
new file mode 100644 (file)
index 0000000..f34fb38
--- /dev/null
@@ -0,0 +1,24 @@
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description="AOC 2024 day 4")
+    parser.add_argument('filename', type=str)
+    parser.add_argument('-d', '--debug', action='store_true')
+    parser.add_argument('-2', '--part2', action='store_true')
+    options = parser.parse_args(args)
+
+    with open(options.filename) as f:
+        problem = Problem.from_stream(f)
+    if options.debug:
+        for row in problem.data:
+            print(row)
+
+    print(f"result {problem.run(part2=options.part2)}")
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/day5/__init__.py b/day5/__init__.py
new file mode 100644 (file)
index 0000000..18b661a
--- /dev/null
@@ -0,0 +1,63 @@
+from dataclasses import dataclass
+from enum import Enum
+from collections import defaultdict
+from typing import List
+
+
+class State(Enum):
+    Rules = 0,
+    Updates = 1
+
+
+@dataclass
+class Problem:
+    rules: List[int]
+    updates: List[int]
+    preceding: defaultdict
+
+    _disordered = []
+
+    def run(self) -> int:
+        result = sum([self.validate(ndx, pageset) \
+                    for ndx, pageset in enumerate(self.updates)])
+        result2 = [self.revalidate(ndx) for ndx in self._disordered]
+        return result
+
+    def validate(self, update_index: int, pageset: List[int]) -> int:
+        """If the pageset is valid, return the middle page number or 0 if invalid"""
+        # pageset = self.updates[update_index]
+        disallowed = set()
+        for page in pageset:
+            if page in disallowed:
+                self._disordered.append(update_index)
+                return 0
+            disallowed |= self.preceding[page]
+        return pageset[len(pageset)//2]
+
+    def revalidate(self, update_index: int) -> int:
+        pageset = self.updates[update_index]
+        # for page in pageset:        
+        pass
+
+    @staticmethod
+    def from_stream(stream) -> 'Problem':
+        state = State.Rules
+        rules = []
+        updates = []
+        for line in stream:
+            line = line.strip()
+            if len(line) == 0:
+                if state == State.Rules:
+                    state = State.Updates
+                continue
+            if state == State.Rules:
+                rule = [int(a) for a in [x for x in line.split('|')]]
+                rules.append(rule)
+            elif state == State.Updates:
+                pageset = [int(p) for p in line.split(',')]
+                updates.append(pageset)
+
+        preceding = defaultdict(set)
+        for a, b in rules:
+            preceding[b].add(a)
+        return Problem(rules, updates, preceding)
diff --git a/day5/__main__.py b/day5/__main__.py
new file mode 100644 (file)
index 0000000..e4b09b7
--- /dev/null
@@ -0,0 +1,26 @@
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description="AOC 2024 day 5")
+    parser.add_argument('filename', type=str)
+    parser.add_argument('-d', '--debug', action='store_true')
+    parser.add_argument('-2', '--part2', action='store_true')
+    options = parser.parse_args(args)
+
+    with open(options.filename) as f:
+        problem = Problem.from_stream(f)
+    if options.debug:
+        for rule in problem.rules:
+            print(rule)
+        for update in problem.updates:
+            print(update)
+
+    print(f"result {problem.run()}")
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/day5/test_problem.py b/day5/test_problem.py
new file mode 100644 (file)
index 0000000..0366b7a
--- /dev/null
@@ -0,0 +1,18 @@
+import unittest
+import os
+from . import Problem
+
+
+class TestProblem(unittest.TestCase):
+    testfile = os.path.join(os.path.dirname(__file__), r'data', r'test_input')
+    def test_from_str(self):
+        with open(self.testfile, 'rt') as fd:
+            problem = Problem.from_stream(fd)
+        self.assertEqual(len(problem.rules), 21)
+        self.assertSequenceEqual(problem.rules[0], [47, 53])
+        self.assertEqual(len(problem.updates), 6)
+        self.assertSequenceEqual(problem.updates[0], [75, 47, 61, 53, 29])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/day6/__init__.py b/day6/__init__.py
new file mode 100644 (file)
index 0000000..5d72098
--- /dev/null
@@ -0,0 +1,133 @@
+from dataclasses import dataclass
+from typing import List, Tuple
+from copy import copy, deepcopy
+from enum import IntFlag
+
+
+class Direction(IntFlag):
+    Left = 1
+    Up = 2
+    Right = 4
+    Down = 8
+
+
+@dataclass
+class Guard:
+    row: int
+    col: int
+    dir: str
+
+    def move(self):
+        if self.dir == '<':
+            self.col -= 1
+        elif self.dir == '^':
+            self.row -= 1
+        elif self.dir == '>':    
+            self.col += 1
+        elif self.dir == 'v':
+            self.row += 1
+
+DIRMAP = {
+    '<': Direction.Left,
+    '^': Direction.Up,
+    '>': Direction.Right,
+    'v': Direction.Down
+}
+
+@dataclass
+class Problem:
+    data: List[int]
+    start: Guard
+
+    def run(self, trace=False) -> int:
+        count = 1
+        guard = copy(self.start)
+        self.data[guard.row][guard.col] = '0'
+        while True:
+            g = copy(guard)
+            g.move()
+            if self.outside(g):
+                break
+            next_tile = self.data[g.row][g.col]
+            if next_tile == '#':
+                if guard.dir == '^':
+                    guard.dir = '>'
+                elif guard.dir == '>':
+                    guard.dir = 'v'
+                elif guard.dir == 'v':
+                    guard.dir = '<'
+                elif guard.dir == '<':
+                    guard.dir = '^'
+            else:
+                if self.data[g.row][g.col] in ('.', '0'):
+                    count += 1
+                    self.data[g.row][g.col] = '%c' % 0x60
+
+                # if the new tile has already been visited in the same
+                # direction then we are on a loop.
+                tileval = ord(self.data[g.row][g.col])
+                if tileval & DIRMAP[g.dir]:
+                    self.data[g.row][g.col] = 'O'
+                    return -1
+
+                tileval |= DIRMAP[g.dir]
+                self.data[g.row][g.col] = '%c' % tileval  # mark as visited
+                guard = g
+
+            if self.outside(guard):
+                break
+        return count
+    
+    def next(self) -> str:
+        g = copy(self.guard)
+        g.move()
+        return self.data[g.row][g.col]
+    
+    def outside(self, guard: Guard) -> bool:
+        return (
+            guard.row < 0
+            or guard.col < 0
+            or guard.row >= len(self.data)
+            or guard.col >= len(self.data[0])
+        )
+    
+    def unblocked(self):
+        for row, tiles in enumerate(self.data):
+            for col, tile in enumerate(tiles):
+                if tile == '.':
+                    yield (row, col)
+
+    def print_map(self, score) -> None:
+        print("".join(self.data[0]), score)
+        for row in self.data[1:]:
+            print("".join(row))
+        print()
+
+    def run_part2(self, debug=False) -> int:
+        count = 0
+        old = deepcopy(self.data)
+        for coord in self.unblocked():
+            self.data = deepcopy(old)
+            row, col = coord
+            self.data[row][col] = '#'
+            score = self.run(trace=debug)
+            if debug:
+                self.print_map(score)
+            if score < 0:
+                count += 1
+        return count
+
+    @staticmethod
+    def from_stream(stream) -> 'Problem':
+        data = []
+        start = None
+        for row, line in enumerate(stream):
+            line = line.strip()
+            col = [n for n, c in enumerate(line) if c in ('<', '^', '>', 'v')]
+            if col:
+                start = (row, col[0])
+            data.append([c for c in line])
+        assert data
+        assert start is not None
+        guard = Guard(start[0], start[1], data[start[0]][start[1]])
+        return Problem(data, guard)
diff --git a/day6/__main__.py b/day6/__main__.py
new file mode 100644 (file)
index 0000000..6e575df
--- /dev/null
@@ -0,0 +1,36 @@
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description="AOC 2024 day 6")
+    parser.add_argument('filename', type=str)
+    parser.add_argument('-d', '--debug', action='store_true')
+    parser.add_argument('-t', '--trace', action='store_true')
+    parser.add_argument('-1', '--part1', action='store_true')
+    parser.add_argument('-2', '--part2', action='store_true')
+    options = parser.parse_args(args)
+
+    with open(options.filename) as f:
+        problem = Problem.from_stream(f)
+    if options.debug and options.part1:
+        print(problem.start)
+        for row in problem.data:
+            print("".join(row))
+
+    if options.part1:
+        print(f"result {problem.run(trace=options.trace)}")
+    if options.part2:
+        print(f"result {problem.run_part2(debug=options.debug)}")
+
+
+    if options.debug and options.part1:
+        for row in problem.data:
+            print("".join(row))
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/day7/__init__.py b/day7/__init__.py
new file mode 100644 (file)
index 0000000..43e39f0
--- /dev/null
@@ -0,0 +1,45 @@
+from dataclasses import dataclass
+from typing import List, Tuple
+
+
+__title__ = 'Day 7: Bridge Repair'
+
+
+@dataclass
+class Problem:
+    data: List[Tuple[int, List[int]]]
+
+    def run(self, part2=False) -> int:
+        total = 0
+        for result, nums in self.data:
+            total += Problem.validate(result, nums, part2)
+        return total
+
+    @staticmethod
+    def validate(result: int, nums: List[int], with_concat=False) -> int:
+        queue = [(1, nums[0])]
+        while queue:
+            index, value = queue.pop()
+            if index == len(nums):
+                if value == result:
+                    return result
+                continue
+            calcs = [value + nums[index],
+                     value * nums[index]]
+            if with_concat:
+                calcs.append(
+                     int(str(value) + str(nums[index])))
+            for calc in calcs:
+                if calc <= result:
+                    queue.append( (index + 1, calc))
+        return 0
+
+    @staticmethod
+    def from_stream(stream) -> 'Problem':
+        data = []
+        for line in stream:
+            result, nums = line.strip().split(':')
+            result = int(result)
+            nums = [int(n) for n in nums.strip().split(' ')]
+            data.append((result, tuple(nums)))
+        return Problem(data)
diff --git a/day7/__main__.py b/day7/__main__.py
new file mode 100644 (file)
index 0000000..7f7504d
--- /dev/null
@@ -0,0 +1,24 @@
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description="AOC 2024 day 7")
+    parser.add_argument('filename', type=str)
+    parser.add_argument('-d', '--debug', action='store_true')
+    parser.add_argument('-2', '--part2', action='store_true')
+    options = parser.parse_args(args)
+
+    with open(options.filename) as f:
+        problem = Problem.from_stream(f)
+    if options.debug:
+        for row in problem.data:
+            print(row)
+
+    print(f"result {problem.run(part2=options.part2)}")
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/day_template/__init__.py b/day_template/__init__.py
new file mode 100644 (file)
index 0000000..eba9d6e
--- /dev/null
@@ -0,0 +1,15 @@
+from dataclasses import dataclass
+from typing import List
+
+
+@dataclass
+class Problem:
+    data: List[int]
+
+    def run(self) -> int:
+        return 0
+
+    @staticmethod
+    def from_stream(stream) -> 'Problem':
+        data = [line.strip() for line in stream]
+        return Problem(data)
diff --git a/day_template/__main__.py b/day_template/__main__.py
new file mode 100644 (file)
index 0000000..eb12339
--- /dev/null
@@ -0,0 +1,23 @@
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description="AOC 2024 day ?")
+    parser.add_argument('filename', type=str)
+    parser.add_argument('-d', '--debug', action='store_true')
+    options = parser.parse_args(args)
+
+    with open(options.filename) as f:
+        problem = Problem.from_stream(f)
+    if options.debug:
+        for row in problem.data:
+            print(row)
+
+    print(f"result {problem.run()}")
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))