this post was submitted on 03 Dec 2023
20 points (95.5% liked)

Advent Of Code

763 readers
1 users here now

An unofficial home for the advent of code community on programming.dev!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

AoC 2023

Solution Threads

M T W T F S S
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 1 year ago
MODERATORS
20
submitted 11 months ago* (last edited 11 months ago) by Ategon to c/advent_of_code
 

Day 3: Gear Ratios


Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ or pastebin (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)

FAQ


🔒This post will be unlocked when there is a decent amount of submissions on the leaderboard to avoid cheating for top spots

🔓 Edit: Post has been unlocked after 11 minutes

all 39 comments
sorted by: hot top controversial new old
[–] [email protected] 8 points 11 months ago (3 children)
[–] [email protected] 2 points 11 months ago (1 children)

I like it, it's simple and to the point. I've learned that one of the most helpful things to do when solving these puzzles is to not make it more complicated than it needs to be, and you certainly succeeded better at that today than I did.

[–] [email protected] 1 points 11 months ago

My solution for day 1 part 1 was simple and to the point. The other ones are getting increasingly less so. You're right that sometimes it's best not to get too fancy, but I think soon I may have to break out such advanced programming techniques as "functions" and maybe "objects", instead of writing increasingly convoluted piles of nested loops. xD

[–] [email protected] 1 points 11 months ago

Hi there! Looks like you linked to a Lemmy community using a URL instead of its name, which doesn't work well for people on different instances. Try fixing it like this: [email protected]

[–] [email protected] 1 points 11 months ago* (last edited 11 months ago)

nested loops

This is the way

[–] [email protected] 6 points 11 months ago* (last edited 11 months ago)

That was not fun to solve. Lots of ugly code. This is a less ugly second version. Language: Nim.

day_03.nim

[–] [email protected] 5 points 11 months ago* (last edited 11 months ago)

I wrote today's program in Python. (I am going to do a different language each day.) The only thing that gave me a little trouble was I was counting "\n" as a part label. Once I realized that I was able to get both problems done very quickly.

My code for the two parts is on github:-

[–] [email protected] 5 points 11 months ago

Rust

I've been using Regexes for every day so far, this time it helped in finding numbers along with their start and end position in a line. For the second part I mostly went with the approach of part 1 which was to look at all numbers and then figure out if it has a part symbol around it. Only in part 2 I saved all numbers next to a gear * in a hash table that maps each gear position to a list of adjacent numbers. Then in the end I can just look at all gears with exactly 2 numbers attached.

Also it has to be said, multiplying two numbers is the exact opposite of getting their ratio!

[–] [email protected] 3 points 11 months ago* (last edited 11 months ago)

Language: C#

I aimed at keeping it as simple and short as reasonably possible this time, no overbuilding here!

I even used a goto to let me break out of multiple loops at once 🤮 (I had to look up how they worked!) I would totally fail me in a code review!

One solution for both

internal class Day3 : IRunnable
    {
        public void Run()
        {
            var input = File.ReadAllLines("Days/Three/Day3Input.txt");
            int sum = 0;
            string numStr = "";
            var starMap = new Dictionary<(int,int),List>();
            for (int i = 0; i < input.Length; i++)           
                for (int j = 0; j < input[i].Length; j++)
                {
                    if (char.IsDigit(input[i][j]))                    
                        numStr += input[i][j];                    
                    if (numStr.Length > 0 && (j == input[i].Length - 1 || !char.IsDigit(input[i][j + 1])))
                    {
                        for (int k = Math.Max(0, i - 1); k < Math.Min(i + 2, input.Length); k++)                        
                            for (int l = Math.Max(0, j - numStr.Length); l < Math.Min(j + 2, input[i].Length); l++)                            
                                if (!char.IsDigit(input[k][l]) && input[k][l] != '.')
                                {
                                    sum += int.Parse(numStr);
                                    if (input[k][l] == '*')
                                    {
                                        if (starMap.ContainsKey((k, l)))                                        
                                            starMap[(k, l)].Add(int.Parse(numStr));                                        
                                        else
                                            starMap.Add((k,l),new List { int.Parse(numStr) });
                                    }
                                    goto endSymbSearch;
                                }                           
                    endSymbSearch:
                        numStr = "";
                    }
                }            
            Console.WriteLine("Result1:"+sum.ToString());
            Console.WriteLine("Result2:" + starMap.Where(sm => sm.Value.Count == 2).Sum(sm => sm.Value[0] * sm.Value[1]));
        }
    }

[–] [email protected] 3 points 11 months ago

My Python solution for part 1 and part 2. I really practice my regex skills.

spoiler

#!/usr/bin/python3

import re

value_re = '(\d+)'
symbol_re = '[^\d.]'
gear_re = '(\*)'

def main():
    input = list()
    with open("input.txt", 'r') as in_file:
        for line in in_file:
            input.append(line.strip('\n'))
    length = len(input)
    width = len(input[0])
    value_sum = 0
    for idx, line in enumerate(input):
        for match in re.finditer(value_re, line):
            for line_mask in input[max(idx - 1, 0):min(idx + 2, length)]:
                valid_chars = line_mask[max(match.span()[0] - 1, 0):min(match.span()[1] + 1, width)]
                if re.search(symbol_re, valid_chars):
                    value_sum += int(match[0])
                    break
    print(f"Value sum = {value_sum}")

    gear_ratio = 0
    for idx, line in enumerate(input):
        for match in re.finditer(gear_re, line):
            valid_lines = input[max(idx - 1, 0):min(idx + 2, length)]
            min_range = max(match.span()[0] - 1, 0)
            max_range = min(match.span()[1], width)
            num_of_adjacent = 0
            temp_gear_ratio = 1
            for valid_line in valid_lines:
                for match in re.finditer(value_re, valid_line):
                    if match.span()[0] in range(min_range,max_range + 1) or match.span()[1] - 1 in range(min_range,max_range + 1):
                        num_of_adjacent += 1
                        temp_gear_ratio *= int(match[0])
            if num_of_adjacent == 2:
                gear_ratio += temp_gear_ratio
    print(f"Gear ratio = {gear_ratio}")

if __name__ == '__main__':
    main()

[–] bugsmith 3 points 11 months ago* (last edited 11 months ago)

Edit: Updated now with part 2.

Managed to have a crack at this a bit earlier today, I've only done Part 01 so far. I'll update with Part 02 later.

I tackled this with the personal challenge of not loading the entire puzzle input into memory, which would have made this a bit easier.

Solution in Rust 🦀

View formatted on GitLab

use std::{
    env, fs,
    io::{self, BufRead, BufReader, Read},
};

fn main() -> io::Result<()> {
    let args: Vec = env::args().collect();
    let filename = &args[1];
    let file1 = fs::File::open(filename)?;
    let file2 = fs::File::open(filename)?;
    let reader1 = BufReader::new(file1);
    let reader2 = BufReader::new(file2);

    println!("Part one: {}", process_part_one(reader1));
    println!("Part two: {}", process_part_two(reader2));
    Ok(())
}

fn process_part_one(reader: BufReader) -> u32 {
    let mut lines = reader.lines().peekable();
    let mut prev_line: Option = None;
    let mut sum = 0;
    while let Some(line) = lines.next() {
        let current_line = line.expect("line exists");
        let next_line = match lines.peek() {
            Some(Ok(line)) => Some(line),
            Some(Err(_)) => None,
            None => None,
        };
        match (prev_line, next_line) {
            (None, Some(next)) => {
                let lines = vec![¤t_line, next];
                sum += parse_lines(lines, true);
            }
            (Some(prev), Some(next)) => {
                let lines = vec![&prev, ¤t_line, next];
                sum += parse_lines(lines, false);
            }
            (Some(prev), None) => {
                let lines = vec![&prev, ¤t_line];
                sum += parse_lines(lines, false);
            }
            (None, None) => {}
        }

        prev_line = Some(current_line);
    }
    sum
}

fn process_part_two(reader: BufReader) -> u32 {
    let mut lines = reader.lines().peekable();
    let mut prev_line: Option = None;
    let mut sum = 0;
    while let Some(line) = lines.next() {
        let current_line = line.expect("line exists");
        let next_line = match lines.peek() {
            Some(Ok(line)) => Some(line),
            Some(Err(_)) => None,
            None => None,
        };
        match (prev_line, next_line) {
            (None, Some(next)) => {
                let lines = vec![¤t_line, next];
                sum += parse_lines_for_gears(lines, true);
            }
            (Some(prev), Some(next)) => {
                let lines = vec![&prev, ¤t_line, next];
                sum += parse_lines_for_gears(lines, false);
            }
            (Some(prev), None) => {
                let lines = vec![&prev, ¤t_line];
                sum += parse_lines_for_gears(lines, false);
            }
            (None, None) => {}
        }

        prev_line = Some(current_line);
    }

    sum
}

fn parse_lines(lines: Vec<&String>, first_line: bool) -> u32 {
    let mut sum = 0;
    let mut num = 0;
    let mut valid = false;
    let mut char_vec: Vec> = Vec::new();
    for line in lines {
        char_vec.push(line.chars().collect());
    }
    let chars = match first_line {
        true => &char_vec[0],
        false => &char_vec[1],
    };
    for i in 0..chars.len() {
        if chars[i].is_digit(10) {
            // Add the digit to the number
            num = num * 10 + chars[i].to_digit(10).expect("is digit");

            // Check the surrounding character for non-period symbols
            for &x in &[-1, 0, 1] {
                for chars in &char_vec {
                    if (i as isize + x).is_positive() && ((i as isize + x) as usize) < chars.len() {
                        let index = (i as isize + x) as usize;
                        if !chars[index].is_digit(10) && chars[index] != '.' {
                            valid = true;
                        }
                    }
                }
            }
        } else {
            if valid {
                sum += num;
            }
            valid = false;
            num = 0;
        }
    }
    if valid {
        sum += num;
    }
    sum
}

fn parse_lines_for_gears(lines: Vec<&String>, first_line: bool) -> u32 {
    let mut sum = 0;
    let mut char_vec: Vec> = Vec::new();
    for line in &lines {
        char_vec.push(line.chars().collect());
    }
    let chars = match first_line {
        true => &char_vec[0],
        false => &char_vec[1],
    };
    for i in 0..chars.len() {
        if chars[i] == '*' {
            let surrounding_nums = get_surrounding_numbers(&lines, i);
            let product = match surrounding_nums.len() {
                0 | 1 => 0,
                _ => surrounding_nums.iter().product(),
            };
            sum += product;
        }
    }
    sum
}

fn get_surrounding_numbers(lines: &Vec<&String>, gear_pos: usize) -> Vec {
    let mut nums: Vec = Vec::new();
    let mut num: u32 = 0;
    let mut valid = false;
    for line in lines {
        for (i, char) in line.chars().enumerate() {
            if char.is_digit(10) {
                num = num * 10 + char.to_digit(10).expect("is digit");
                if [gear_pos - 1, gear_pos, gear_pos + 1].contains(&i) {
                    valid = true;
                }
            } else if num > 0 && valid {
                nums.push(num);
                num = 0;
                valid = false;
            } else {
                num = 0;
                valid = false;
            }
        }
        if num > 0 && valid {
            nums.push(num);
        }
        num = 0;
        valid = false;
    }
    nums
}

#[cfg(test)]
mod tests {
    use super::*;

    const INPUT: &str = "467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..";

    #[test]
    fn test_process_part_one() {
        let input_bytes = INPUT.as_bytes();
        assert_eq!(4361, process_part_one(BufReader::new(input_bytes)));
    }

    #[test]
    fn test_process_part_two() {
        let input_bytes = INPUT.as_bytes();
        assert_eq!(467835, process_part_two(BufReader::new(input_bytes)));
    }
}
[–] [email protected] 3 points 11 months ago

Python

Questions and comments welcome!

import collections
import re

from .solver import Solver

class Day03(Solver):
  def __init__(self):
    super().__init__(3)
    self.lines = []

  def presolve(self, input: str):
    self.lines = input.rstrip().split('\n')

  def solve_first_star(self):
    adjacent_to_symbols = set()
    for i, line in enumerate(self.lines):
      for j, sym in enumerate(line):
        if sym in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'):
          continue
        for di in (-1, 0, 1):
          for dj in (-1, 0, 1):
            adjacent_to_symbols.add((i + di, j + dj))
    numbers = []
    for i, line in enumerate(self. lines):
      for number_match in re.finditer(r'\d+', line):
        is_adjacent_to_symbol = False
        for j in range(number_match.start(), number_match.end()):
          if (i, j) in adjacent_to_symbols:
            is_adjacent_to_symbol = True
        if is_adjacent_to_symbol:
          numbers.append(int(number_match.group()))
    return sum(numbers)

  def solve_second_star(self):
    gear_numbers = collections.defaultdict(list)
    adjacent_to_gears = {}
    for i, line in enumerate(self.lines):
      for j, sym in enumerate(line):
        if sym == '*':
          for di in (-1, 0, 1):
            for dj in (-1, 0, 1):
              adjacent_to_gears[(i + di, j + dj)] = (i, j)
    for i, line in enumerate(self. lines):
      for number_match in re.finditer(r'\d+', line):
        adjacent_to_gear = None
        for j in range(number_match.start(), number_match.end()):
          if (i, j) in adjacent_to_gears:
            adjacent_to_gear = adjacent_to_gears[(i, j)]
        if adjacent_to_gear:
          gear_numbers[adjacent_to_gear].append(int(number_match.group()))
    ratios = []
    for gear_numbers in gear_numbers.values():
      match gear_numbers:
        case [a, b]:
          ratios.append(a * b)
    return sum(ratios)

[–] [email protected] 3 points 11 months ago

Python solution

just more parsing problems

[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

Language: C++

Efficiency? Elegant Code? Nope but It works. Luckily I did part 1 by looking at the symbols first anyway, so extending to part two was trivial. Also originally had a bug where I treated all symbols as cogs, not only '*'. Interestingly it worked anyway as only '*'s had two adjacent numbers in my data. It is fixed in this version. Hacked together combined code (originally I did each part as separate programs but they shared so much that I ended up combining then so the post is shorter): https://pastebin.com/Dij2XSYe

Edit: anything in angle brackets is not displaying even with backslashes, idk why but i have moved the code to a pastebin.

[–] [email protected] 2 points 11 months ago* (last edited 11 months ago) (1 children)

Dart Solution

Holy moley, if this year is intended to be impervious to AI solution, it's also working pretty well as a filter to this paltry Human Intelligence.

Find interesting symbols, look around for digits, and expand these into numbers. A dirty hacky solution leaning on a re-used Grid class. Not recommended.

late ListGrid grid;

Index look(Index ix, Index dir) {
  var here = ix;
  var next = here + dir;
  while (grid.isInBounds(next) && '1234567890'.contains(grid.at(next))) {
    here = next;
    next = here + dir;
  }
  return here;
}

/// Return start and end indices of a number at a point.
(Index, Index) expandNumber(Index ix) =>
    (look(ix, Grid.dirs['L']!), look(ix, Grid.dirs['R']!));

int parseNumber((Index, Index) e) => int.parse([
      for (var i = e.$1; i != e.$2 + Grid.dirs['R']!; i += Grid.dirs['R']!)
        grid.at(i)
    ].join());

/// Return de-duplicated positions of all numbers near the given symbols.
nearSymbols(Set syms) => [
      for (var ix in grid.indexes.where((i) => syms.contains(grid.at(i))))
        {
          for (var n in grid
              .near8(ix)
              .where((n) => ('1234567890'.contains(grid.at(n)))))
            expandNumber(n)
        }.toList()
    ];

part1(List lines) {
  grid = ListGrid([for (var e in lines) e.split('')]);
  var syms = lines
      .join('')
      .split('')
      .toSet()
      .difference('0123456789.'.split('').toSet());
  // Find distinct number locations near symbols and sum them.
  return {
    for (var ns in nearSymbols(syms))
      for (var n in ns) n
  }.map(parseNumber).sum;
}

part2(List lines) {
  grid = ListGrid([for (var e in lines) e.split('')]);
  // Look for _pairs_ of numbers near '*' and multiply them.
  var products = [
    for (var ns in nearSymbols({'*'}).where((e) => e.length == 2))
      ns.map(parseNumber).reduce((s, t) => s * t)
  ];
  return products.sum;
}

[–] [email protected] 2 points 11 months ago (1 children)

what language is this? I don't recognize it

[–] [email protected] 1 points 11 months ago

Sorry, it’s Dart. I’ll update it.

[–] Andy 2 points 11 months ago* (last edited 10 months ago)

Factor on github (with comments and imports):

: symbol-indices ( line -- seq )
  [ ".0123456789" member? not ] find-all [ first ] map
;

: num-spans ( line -- seq )
  >array [ over digit? [ nip ] [ 2drop f ] if ] map-index
  { f } split harvest
  [ [ first ] [ last ] bi 2array ] map
;

: adjacent? ( num-span symbol-indices -- ? )
  swap [ first 1 - ] [ last 1 + ] bi [a,b]
  '[ _ interval-contains? ] any?
;

: part-numbers ( line nearby-symbol-indices -- seq )
  [ dup num-spans ] dip
  '[ _ adjacent? ] filter
  swap '[ first2 1 + _ subseq string>number ] map
;

: part1 ( -- )
  "vocab:aoc-2023/day03/input.txt" utf8 file-lines
  [ [ symbol-indices ] map ] keep
  [
    pick swap [ 1 - ?nth-of ] [ nth-of ] [ 1 + ?nth-of ] 2tri
    3append part-numbers sum
  ] map-index sum nip .
;

: star-indices ( line -- seq )
  [ CHAR: * = ] find-all [ first ] map
;

: gears ( line prev-line next-line -- seq-of-pairs )
  pick star-indices
  [ 1array '[ _ part-numbers ] [ 3dup ] dip tri@ 3append ]
  [ length 2 = ] map-filter [ 3drop ] dip
;

: part2 ( -- )
  "vocab:aoc-2023/day03/input.txt" utf8 file-lines
  dup [
    pick swap [ 1 - ?nth-of ] [ 1 + ?nth-of ] 2bi
    gears [ product ] map-sum
  ] map-index sum nip .
;
[–] [email protected] 2 points 11 months ago

F# solution

Had an off-by-one error that took me a while to find

[–] Ategon 2 points 11 months ago* (last edited 11 months ago)

[Rust] Harder one today, for part 1 I ended up getting stuck for a bit since I wasnt taking numbers at the end of lines into account and in part 2 I defined my gears vector in the wrong spot and spent a bit debugging that

Code(lemmy removes some chars, all chars are in code link)

use std::fs;

fn part1(input: String) -> u32 {
    let lines = input.lines().collect::>();
    let mut sum = 0;

    for i in 0..lines.len() {
        let mut num = 0;
        let mut valid = false;
        let chars = lines[i].chars().collect::>();

        for j in 0..chars.len() {
            let character = chars[j];
            let parts = ['*', '#', '+', '$', '/', '%', '=', '-', '&', '@'];

            if character.is_digit(10) {
                num = num * 10 + character.to_digit(10).unwrap();

                if i > 0 {
                    if parts.contains(&lines[i - 1].chars().collect::>()[j]) {
                        valid = true;
                    }

                    if j > 0 {
                        if parts.contains(&lines[i - 1].chars().collect::>()[j - 1]) {
                            valid = true;
                        }
                    }

                    if j < chars.len() - 1 {
                        if parts.contains(&lines[i - 1].chars().collect::>()[j + 1]) {
                            valid = true;
                        }
                    }
                }

                if i < lines.len() - 1 {
                    if parts.contains(&lines[i + 1].chars().collect::>()[j]) {
                        valid = true;
                    }

                    if j > 0 {
                        if parts.contains(&lines[i + 1].chars().collect::>()[j - 1]) {
                            valid = true;
                        }
                    }

                    if j < chars.len() - 1 {
                        if parts.contains(&lines[i + 1].chars().collect::>()[j + 1]) {
                            valid = true;
                        }
                    }
                }

                if j > 0 {
                    if parts.contains(&lines[i].chars().collect::>()[j - 1]) {
                        valid = true;
                    }
                }

                if j < chars.len() - 1 {
                    if parts.contains(&lines[i].chars().collect::>()[j + 1]) {
                        valid = true;
                    }
                }
            }
            else {
                if valid == true {
                    sum += num;
                }

                num = 0;
                valid = false;
            }

            if j == chars.len() - 1 {
                if valid == true {
                    sum += num;
                }

                num = 0;
                valid = false;
            }
        }
    }

    return sum;
}

fn part2(input: String) -> u32 {
    let lines = input.lines().collect::>();
    let mut gears: Vec<(usize, usize, u32)> = Vec::new();
    let mut sum = 0;

    for i in 0..lines.len() {
        let mut num = 0;
        let chars = lines[i].chars().collect::>();
        let mut pos: (usize, usize) = (0, 0);
        let mut valid = false;

        for j in 0..chars.len() {
            let character = chars[j];
            let parts = ['*'];

            if character.is_digit(10) {
                num = num * 10 + character.to_digit(10).unwrap();

                if i > 0 {
                    if parts.contains(&lines[i - 1].chars().collect::>()[j]) {
                        valid = true;
                        pos = (i - 1, j);
                    }

                    if j > 0 {
                        if parts.contains(&lines[i - 1].chars().collect::>()[j - 1]) {
                            valid = true;
                            pos = (i - 1, j - 1);
                        }
                    }

                    if j < chars.len() - 1 {
                        if parts.contains(&lines[i - 1].chars().collect::>()[j + 1]) {
                            valid = true;
                            pos = (i - 1, j + 1);
                        }
                    }
                }

                if i < lines.len() - 1 {
                    if parts.contains(&lines[i + 1].chars().collect::>()[j]) {
                        valid = true;
                        pos = (i + 1, j);
                    }

                    if j > 0 {
                        if parts.contains(&lines[i + 1].chars().collect::>()[j - 1]) {
                            valid = true;
                            pos = (i + 1, j - 1);
                        }
                    }

                    if j < chars.len() - 1 {
                        if parts.contains(&lines[i + 1].chars().collect::>()[j + 1]) {
                            valid = true;
                            pos = (i + 1, j + 1);
                        }
                    }
                }

                if j > 0 {
                    if parts.contains(&lines[i].chars().collect::>()[j - 1]) {
                        valid = true;
                        pos = (i, j - 1);
                    }
                }

                if j < chars.len() - 1 {
                    if parts.contains(&lines[i].chars().collect::>()[j + 1]) {
                        valid = true;
                        pos = (i, j + 1);
                    }
                }
            }
            else {
                if valid == true {
                    let mut current_gear = false;
                    
                    for gear in &gears {
                        if gear.0 == pos.0 && gear.1 == pos.1 {
                            sum += num * gear.2;
                            current_gear = true;
                            break;
                        }
                    }
                    
                    if !current_gear {
                        let tuple_to_push = (pos.0.clone(), pos.1.clone(), num.clone());
                        gears.push((pos.0.clone(), pos.1.clone(), num.clone()));
                    }
                }

                num = 0;
                valid = false;
            }

            if j == chars.len() - 1 {
                if valid == true {
                    let mut current_gear = false;
                    
                    for gear in &gears {
                        if gear.0 == pos.0 && gear.1 == pos.1 {
                            sum += num * gear.2;
                            current_gear = true;
                            break;
                        }
                    }
                    
                    if !current_gear {
                        let tuple_to_push = (pos.0.clone(), pos.1.clone(), num.clone());
                        gears.push((pos.0.clone(), pos.1.clone(), num.clone()));
                    }
                }

                num = 0;
                valid = false;
            }
        }
    }

    return sum;
}

fn main() {
    let input = fs::read_to_string("data/input.txt").unwrap();

    println!("{}", part1(input.clone()));
    println!("{}", part2(input.clone()));
}

Code Link

[–] snowe 2 points 11 months ago (1 children)
[–] [email protected] 1 points 11 months ago

Hi there! Looks like you linked to a Lemmy community using a URL instead of its name, which doesn't work well for people on different instances. Try fixing it like this: [email protected]

[–] [email protected] 2 points 11 months ago

I get the feeling that I should include some default types for handling 2D maps in my boilerplate, it's a very recurring problem in AoC after all.

My solution is reasonably simplistic - and therefore also a bit slow, but the design meant I could do part 2 with just a few extra lines of code on the already processed data, here's the functional part of it; (I push the previous days solution as part of my workflow for starting with the current day so the full code won't be up until tomorrow)

RubyThe code has been compressed for brevity.

Point = Struct.new('Point', :x, :y)
PartNumber = Struct.new('PartNumber', :number, :adjacent) do
  def adjacent?(to); adjacent.include?(to); end
  def irrelevant?; adjacent.empty?; end
  def to_i; number; end
end

class Implementation
  def initialize
    @map = []; @dim = { width: 0, height: 0 }; @symbols = []; @numbers = []
  end

  def input(line)
    @dim[:width] = line.size; @dim[:height] += 1
    @map += line.chars
  end

  def calc
    for y in (0..@dim[:height]-1) do
      for x in (0..@dim[:width]-1) do
        chr = get(x, y); next if chr =~ /\d/ || chr == '.'
        @symbols << Point.new(x, y)
      end
    end

    for y in (0..@dim[:height]-1) do
      buf = ""; adj = []
      for x in (0..@dim[:width]) do # Going one over, to fake a non-number as an end char on all lines
        chr = get(x, y)
        if chr =~ /\d/
          buf += chr
          (-1..1).each do |adj_x|
            (-1..1).each do |adj_y|
              next if adj_x == 0 && adj_y == 0 ||
                (x + adj_x < 0) || (x + adj_x >= @dim[:width]) ||
                (y + adj_y < 0) || (y + adj_y >= @dim[:height])
              sym = Point.new(x + adj_x, y + adj_y)
              adj << sym if @symbols.any? sym
            end
          end
        elsif !buf.empty?
          @numbers << PartNumber.new(buf.to_i, adj)
          buf = ""; adj = []
        end
      end
    end
  end

  def output
    part1 = @numbers.reject(&:irrelevant?).map(&:to_i).sum
    puts "Part 1:", part1

    gears = @symbols.select do |sym|
      next unless get(sym) == '*'
      next unless @numbers.select { |num| num.adjacent? sym }.size == 2
      true
    end
    part2 = gears.sum { |gear| @numbers.select { |num| num.adjacent? gear }.map(&:to_i).inject(:*) }

    puts "Part 2:", part2
  end

  private

  def get(x, y = -1)
    y = x.y if x.is_a?(Point)
    x = x.x if x.is_a?(Point)
    return unless (0..@dim[:width]-1).include?(x) && (0..@dim[:height]-1).include?(y)

    @map[y * @dim[:width] + x % @dim[:width]]
  end
end

[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

Crystal

My computer crashed right most of the way through and I lost everything, so this was even more frustrating than it should have been
Also damn, lemmy's tabs are massive

will post part 2 when I get to it

input = File.read("input.txt")

lines = input.lines.map(&.chars)

sum = 0
num_marker = nil
lines.each_with_index do |line, y|
	line.each_with_index do |char, x|
		num_marker ||= x if char.number?
	
		if (!char.number? || x == line.size-1) && num_marker
			if check_symbol(y, (num_marker-1)..x, lines)
				sum += lines[y][(char.number? ? num_marker..x : num_marker...x)].join.to_i 
			end
			num_marker = nil
end end end
puts sum

def check_symbol(y, rx, lines)
	carr = [ lines[y][rx.begin]?, lines[y][rx.end]? ]
	carr += rx.map {|x| lines[y-1][x]? } if y > 0  
	carr += rx.map {|x| lines[y+1][x]? } if y < lines.size-1 

	carr.each {|c| return true if c && c != '.' && !c.number?}
	false
end
[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

Input parsing AGAIN?

Lua

-- SPDX-FileCopyrightText: 2023 Jummit
--
-- SPDX-License-Identifier: GPL-3.0-or-later

local lines = {}
for line in io.open("3.input"):lines() do
	table.insert(lines, "."..line..".")
end
local width = #lines[1]
local height = #lines
local function at(x, y, w)
	if y < 1 or y > height then return nil end
	return lines[y]:sub(x, x + w - 1)
end
local sum = 0
local gears = {}
for y, line in ipairs(lines) do
	local start = 1
	local outLine = line
	while true do
		local newStart, numEnd = line:find("%d+", start)
		if not newStart then break end
		local symbol = false
		local num = tonumber(line:sub(newStart, numEnd))
		for y = y - 1, y + 1 do
			local surrounding = at(newStart - 1, y, numEnd - newStart + 3)
			if surrounding then
				if surrounding and surrounding:match("[^.%d]") then
					symbol = true
				end
				for i = 1, #surrounding do
					local gear = surrounding:sub(i, i) == "*"
					if gear then
						if not gears[y] then
							gears[y] = {}
						end
						local x = i + newStart - 2
						if not gears[y][x] then
							gears[y][i + newStart - 2] = {}
						end
						table.insert(gears[y][x], num)
					end
				end
			end
		end
		if symbol then
			sum = sum + num
		end
		start = numEnd + 1
	end
end
print(sum)
local ratio = 0
for _, line in pairs(gears) do
	for _, gears in pairs(line) do
		if #gears == 2 then
			ratio = ratio + gears[1] * gears[2]
		end
	end
end
print(ratio)

Hare (Part one only)

// SPDX-FileCopyrightText: 2023 Jummit
//
// SPDX-License-Identifier: GPL-3.0-or-later

use strings;
use regex;
use fmt;
use os;
use bufio;
use io;
use strconv;
use types;

fn star_in(lines: []str, x: uint, y: uint, w: uint) bool = {
	let start = y;
	if (start > 0) start -= 1;
	let end = y + 1;
	if (end >= len(lines)) end -= 1;
	const re = regex::compile(`[^.0-9]`)!;
	for (let h = start; h <= end; h += 1) {
		fmt::println(strings::sub(lines[h], x, x + w))!;
		if (regex::test(&re, strings::sub(lines[h], x, x + w))) {
			fmt::println("")!;
			return true;
		};
	};
	fmt::println("")!;
	return false;
};

export fn main() void = {
	const file = os::open("3.input")!;
	defer io::close(file)!;
	const buf = bufio::newscanner(file, types::SIZE_MAX);
	let lines: []str = [];
	defer strings::freeall(lines);
	for (true) {
		match (bufio::scan_line(&buf)!) {
		case io::EOF =>
			break;
		case let line: const str =>
			append(lines, strings::dup(line));
		};
	};
	const height = len(lines);
	const width = len(lines[0]);
	let sum: uint = 0;
	let gears: [](uint, uint) = [];
	const num_re = regex::compile(`[0-9]+`)!;
	for (let y = 0u; y < len(lines); y += 1) {
		let nums = regex::findall(&num_re, lines[y]);
		defer regex::result_freeall(nums);
		for (let i = 0z; i < len(nums); i += 1) {
			for (let j = 0z; j < len(nums[i]); j += 1) {
				const find = nums[i][j];
				const num = strconv::stou(find.content)!;
				let start = find.start: uint;
				let w = len(find.content): uint + 2;
				if (start > 0) {
					start -= 1;
				} else {
					w -= 1;
				};
				if (star_in(lines, start, y, w)) {
					sum += num;
				};
			};
		};
	};
	fmt::printfln("{}", sum)!;
};

[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

Language: C

Part 2 stumped me for a little bit, it wasn't an obvious extension of part 1. Part 1 was about numbers (with one or more ...) while part 2 worked from the symbols (with exactly two ...). Going the other way would require more bookkeeping to avoid double counting.

And for the implementation: if you loop over the grid and check surrounding cells for digits you'd have to account for a bunch of cases, e.g. NW/N or N/NE being part of the same number or NW and NE being part of separate numbers. And you'd have to parse the numbers again. But building a graph or reference list of some sort is both unergonomic with C and not necessarily any simpler.

I ended up just writing out the cases, and honestly it didn't turn out too bad.

GitHub link

Abridged code

int main(int argc, char **argv)
{
	static char G[GSZ][GSZ];
	static int N[GSZ][GSZ];
	int p1=0,p2=0, h=0, x,y, dx,dy, n=0,sym=0,r;
	
	for (h=0; fgets(&G[h+1][1], GSZ-1, stdin); h++)
		assert(h < GSZ);

	/*
	 * Pass 1: parse numbers and solve part 1. For every digit in
	 * G, the full number it is part of is stored in N.
	 */
	for (y=1; y<=h; y++)
	for (x=1; G[y][x]; x++)
		if (isdigit(G[y][x])) {
			n = n*10 + G[y][x]-'0';

			for (dy=-1; dy<2; dy++)
			for (dx=-1; dx<2; dx++)
				sym = sym || (x && y &&
				    G[y+dy][x+dx] != '.' &&
				    ispunct(G[y+dy][x+dx]));
		} else {
			for (dx=-1; isdigit(G[y][x+dx]); dx--)
				N[y][x+dx] = n;
			if (sym)
				p1 += n;
			n = sym = 0;
		}

	/*
	 * Pass 2: solve part 2 by finding all '*', then counting and
	 * multiplying adjecent numbers.
	 *
	 * Horizontal adjecency is trivial but vertical/diagonal has
	 * two situations: if there's a digit directly North of the '+',
	 * it must be a single number: NW and NE would connect to it.
	 * If N isn't a digit, digits in NW and NE belong to separate
	 * numbers.
	 */
	for (y=1; y<=h; y++)
	for (x=1; G[y][x]; x++) {
		if (G[y][x] != '*')
			continue;

		n = 0; r = 1;

		if (N[y][x-1]) { n++; r *= N[y][x-1]; }
		if (N[y][x+1]) { n++; r *= N[y][x+1]; }

		if (N[y-1][x]) { n++; r *= N[y-1][x]; } else {
			if (N[y-1][x-1]) { n++; r *= N[y-1][x-1]; }
			if (N[y-1][x+1]) { n++; r *= N[y-1][x+1]; }
		}

		if (N[y+1][x]) { n++; r *= N[y+1][x]; } else {
			if (N[y+1][x-1]) { n++; r *= N[y+1][x-1]; }
			if (N[y+1][x+1]) { n++; r *= N[y+1][x+1]; }
		}

		if (n == 2)
			p2 += r;
	}

	printf("%d %d\n", p1, p2);
	return 0;
}

[–] Massahud 2 points 11 months ago
[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

Language: Python

Classic AoC grid problem... Tedious as usual, but very doable. Took my time and I'm pretty happy with the result. :]

Part 1

For the first part, I decided to break the problem into: 1. Reading the schematic, 2. Finding the numbers, 3. Finding the parts. This was useful for Part 2 as I could re-use my read_schematic and find_numbers functions.

Two things I typically do for grid problems:

  1. Pad the grid so you can avoid annoying boundary checks.
  2. I have a DIRECTIONS list I loop through so I can check easily check the neighbors.
Schematic  = list[str]
Number     = tuple[int, int, int]

DIRECTIONS = (
    (-1, -1),
    (-1,  0),
    (-1,  1),
    ( 0, -1),
    ( 0,  1),
    ( 1, -1),
    ( 1,  0),
    ( 1,  1),
)

def read_schematic(stream=sys.stdin) -> Schematic:
    schematic = [line.strip() for line in stream]
    columns   = len(schematic[0]) + 2
    return [
        '.'*columns,
        *['.' + line + '.' for line in schematic],
        '.'*columns,
    ]

def is_symbol(s: str) -> bool:
    return not (s.isdigit() or s == '.')

def find_numbers(schematic: Schematic) -> Iterator[Number]:
    rows    = len(schematic)
    columns = len(schematic[0])

    for r in range(1, rows):
        for number in re.finditer(r'[0-9]+', schematic[r]):
            yield (r, *number.span())

def find_parts(schematic: Schematic, numbers: Iterator[Number]) -> Iterator[int]:
    for r, c_head, c_tail in numbers:
        part = int(schematic[r][c_head:c_tail])
        for c in range(c_head, c_tail):
            neighbors = (schematic[r + dr][c + dc] for dr, dc in DIRECTIONS)
            if any(is_symbol(neighbor) for neighbor in neighbors):
                yield part
                break

def main(stream=sys.stdin) -> None:
    schematic = read_schematic(stream)
    numbers   = find_numbers(schematic)
    parts     = find_parts(schematic, numbers)
    print(sum(parts))

Part 2

For the second part, I just found the stars, and then I found the gears by checking if the stars are next to two numbers (which I had found previously).

def find_stars(schematic: Schematic) -> Iterator[Star]:
    rows    = len(schematic)
    columns = len(schematic[0])

    for r in range(1, rows):
        for c in range(1, columns):
            token = schematic[r][c]
            if token == '*':
                yield (r, c)

def find_gears(schematic: Schematic, stars: Iterator[Star], numbers: list[Number]) -> Iterator[int]:
    for star_r, star_c in stars:
        gears = [                                                                                                                      
            int(schematic[number_r][number_c_head:number_c_tail])
            for number_r, number_c_head, number_c_tail in numbers
            if any(star_r + dr == number_r and number_c_head <= (star_c + dc) < number_c_tail for dr, dc in DIRECTIONS)
        ]
        if len(gears) == 2:
            yield gears[0] * gears[1]

def main(stream=sys.stdin) -> None:
    schematic = read_schematic(stream)
    numbers   = find_numbers(schematic)
    stars     = find_stars(schematic)
    gears     = find_gears(schematic, stars, list(numbers))
    print(sum(gears))

GitHub Repo

[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

Only had the time to solve part 1, but had a lot of fun with it. Here's my solution: d03/p1

[–] capitalpb 2 points 11 months ago

Another day of the 2023 Advent of Code, and another day where I hate looking at my code. This year just seems like it is starting off a lot more complex than I remember in previous years. This one was a little tricky, but I got there without any major setbacks. Another one I am excited to come back to and clean up, but this first pass is all about getting a solution, and this one works.

https://github.com/capitalpb/advent_of_code_2023/blob/main/src/solvers/day03.rs

#[derive(Clone, Copy, Debug)]
struct Location {
    row: usize,
    start_col: usize,
    end_col: usize,
}

#[derive(Debug)]
struct EngineSchematic {
    schematic: Vec>,
    numbers: Vec,
    symbols: Vec,
}

impl EngineSchematic {
    fn from(input: &str) -> EngineSchematic {
        let schematic: Vec> = input.lines().map(|line| line.chars().collect()).collect();
        let mut numbers = vec![];
        let mut symbols = vec![];
        let mut location: Option = None;

        for (row_index, row) in schematic.iter().enumerate() {
            for (col, ch) in row.iter().enumerate() {
                match ch {
                    ch if ch.is_ascii_punctuation() => {
                        if let Some(location) = location {
                            numbers.push(location);
                        }
                        location = None;

                        if ch != &'.' {
                            symbols.push(Location {
                                row: row_index,
                                start_col: col,
                                end_col: col,
                            });
                        }
                    }
                    ch if ch.is_digit(10) => {
                        if let Some(mut_location) = location.as_mut() {
                            mut_location.end_col = col;
                        } else {
                            location = Some(Location {
                                row: row_index,
                                start_col: col,
                                end_col: col,
                            });
                        }
                    }
                    _ => {
                        unreachable!("malformed input");
                    }
                }
            }

            if let Some(location) = location {
                numbers.push(location);
            }
            location = None;
        }

        EngineSchematic {
            schematic,
            numbers,
            symbols,
        }
    }

    fn get_number_value(&self, location: &Location) -> u32 {
        self.schematic[location.row][location.start_col..=location.end_col]
            .iter()
            .collect::()
            .parse::()
            .unwrap()
    }

    fn is_gear(&self, location: &Location) -> bool {
        self.schematic[location.row][location.start_col] == '*'
    }

    fn are_adjacent(&self, location: &Location, other_location: &Location) -> bool {
        location.start_col >= other_location.start_col.checked_sub(1).unwrap_or(0)
            && location.end_col <= other_location.end_col + 1
            && (location.row == other_location.row
                || location.row == other_location.row.checked_sub(1).unwrap_or(0)
                || location.row == other_location.row + 1)
    }
}

pub struct Day03;

impl Solver for Day03 {
    fn star_one(&self, input: &str) -> String {
        let schematic = EngineSchematic::from(input);

        schematic
            .numbers
            .iter()
            .filter(|number| {
                schematic
                    .symbols
                    .iter()
                    .any(|symbol| schematic.are_adjacent(symbol, number))
            })
            .map(|number| schematic.get_number_value(number))
            .sum::()
            .to_string()
    }

    fn star_two(&self, input: &str) -> String {
        let schematic = EngineSchematic::from(input);

        schematic
            .symbols
            .iter()
            .filter(|symbol| schematic.is_gear(symbol))
            .map(|symbol| {
                let adjacent_numbers = schematic
                    .numbers
                    .iter()
                    .filter(|number| schematic.are_adjacent(symbol, number))
                    .collect::>();
                if adjacent_numbers.len() == 2 {
                    schematic.get_number_value(adjacent_numbers[0])
                        * schematic.get_number_value(adjacent_numbers[1])
                } else {
                    0
                }
            })
            .sum::()
            .to_string()
    }
}
[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

I only have part 1 finished so far, thanks to a single segfault and me procrastinating on downloading Valgrind..

Anyways here it is!

https://git.sr.ht/~aidenisik/aoc23/tree/master/item/day3

C

EDIT: Part 2 is now also uploaded

[–] asyncrosaurus 2 points 11 months ago

[LANGUAGE: C#]

I kept trying to create clever solutions, but ended up falling back on regex when it was taking to long. THE TLDR is we scan the list of strings for a symbol, then parse the three lines above, below and inline with the symbol for digits. Then we try and match the indexes of the match and the area around the symbol. Part 2 was a small modification, and was mostly about getting the existing code to conform the data into a pattern for each of the three lines.

Part 1

static char[] Symbols = { '@', '#', '$', '%', '&', '*', '/', '+', '-', '=' };
string pattern = @"\d+";
static List? list;
list = new List((await File.ReadAllLinesAsync(@".\Day 3\PuzzleInput.txt")));

int count = 0;
for (int row = 0; row < list.Count; row++)
{
    for (int col = 0; col < list[row].Length; col++)
    {
        var c = list[row][col];
        if (c == '.')
        {
            continue;
        }

        if (Symbols.Contains(c))
        {
            var res = Calculate(list[row - 1], col);
            res += Calculate(list[row], col);
            res += Calculate(list[row + 1], col);
            count += res;
        }

    }
}
Console.WriteLine(count);

private static int Calculate(string line, int col)
{
    List indexesToCheck = new List { col - 1, col, col + 1 };
    int count = 0;
    MatchCollection matches = Regex.Matches(line, pattern);

    foreach (Match match in matches)
    {
        string number = match.Value;

        if (AnyIndexInList(indexesToCheck, match.Index, match.Length))
        {
            count += Int32.Parse(number);
        }
    }
    return count;
}

static bool AnyIndexInList(List list, int startIndex, int length)
{
    for (int i = startIndex; i < startIndex + length; i++)
    {
        if (list.Contains(i))
        {
            return true;
        }
    }
    return false;
}

Part 2:

list = new List((await File.ReadAllLinesAsync(@".\Day 3\PuzzleInput.txt")));

int count = 0;
for (int row = 0; row < list.Count; row++)
{
    for (int col = 0; col < list[row].Length; col++)
    {
        var c = list[row][col];
        if (c == '.')
            continue;
        
        if (c == '*')
        {
            var res1 = Calculate2(list[row - 1], col);
            var res2 = Calculate2(list[row], col);
            var res3 = Calculate2(list[row + 1], col);

            count += (res1, res2, res3) switch 
            {
                {res1: not null, res2: null, res3: null } when  res1[1] != null => res1[0].Value * res1[1].Value,
                {res1:  null, res2: not null, res3: null } when res2[1] != null => res2[0].Value * res2[1].Value,
                {res1:  null, res2: null, res3: not null } when res3[1] != null => res3[0].Value * res3[1].Value,

                {res1: not null, res2: not null, res3: null } => res1[0].Value * res2[0].Value,
                {res1: not null, res2: null, res3: not null } => res1[0].Value * res3[0].Value,
                {res1: null, res2: not null, res3: not null } => res2[0].Value * res3[0].Value,
                {res1: not null, res2: not null, res3: not null } => res1[0].Value * res2[0].Value * res3[0].Value,

                _ => 0
            } ;
        }
    }
}
                
Console.WriteLine(count);


private static int?[]? Calculate2(string line, int col)
{
    List indexesToCheck = new List { col - 1, col, col + 1 };
    int?[]? count = null;
    MatchCollection matches = Regex.Matches(line, pattern);

    foreach (Match match in matches)
    {
        string number = match.Value;

        if (AnyIndexInList(indexesToCheck, match.Index, match.Length))
        {
            if (count == null)
                count = new int?[2] { Int32.Parse(number), null };
            else {
                count[1] = Int32.Parse(number);
            };
        }
    }
    return count;
}
[–] kartoffelsaft 1 points 11 months ago* (last edited 11 months ago) (2 children)

Did this in Odin

Here's a tip: if you are using a language / standard library that doesn't have a set, you can mimic it with a map from your key to a nullary (in this case an empty struct)

formatted code

package day3

import "core:fmt"
import "core:strings"
import "core:unicode"
import "core:strconv"

flood_get_num :: proc(s: string, i: int) -> (parsed: int, pos: int) {
    if !unicode.is_digit(rune(s[i])) do return -99999, -1

    pos = strings.last_index_proc(s[:i+1], proc(r:rune)->bool{return !unicode.is_digit(r)})
    pos += 1

    ok: bool
    parsed, ok = strconv.parse_int(s[pos:])

    return parsed, pos
}

p1 :: proc(input: []string) {
    // wow what a gnarly type
    foundNumSet := make(map[[2]int]struct{})
    defer delete(foundNumSet)

    total := 0

    for y in 0..
[–] [email protected] 3 points 11 months ago (1 children)

Here’s a tip: if you are using a language / standard library that doesn’t have a map,

Probably meant to write 'a set'? Good trick though.

[–] kartoffelsaft 1 points 11 months ago

Oh yeah, I misspoke, gonna edit.

[–] kartoffelsaft 1 points 11 months ago (1 children)

hmm, my code keeps getting truncated at for y in .., anyone have any idea why? Maybe the "<" right after that confuses a parser somewhere?

[–] Ategon 2 points 11 months ago

Lemmy doesn't handle certain characters well currently such as left angle brackets and ampersands due to some sanitization in the back end to stop scripting attacks

[–] [email protected] 1 points 11 months ago

[Language: Lean4]

I'll only post the actual parsing and solution. I have written some helpers which are in other files, as is the main function. For the full code, please see my github repo.

Here I used HashMap and HashSet, but that's just an optimization. I'm not even sure if they are faster than just using lists here...

Solution

import Lean.Data.HashSet
import Lean.Data.HashMap

namespace Day3
structure Coordinate : Type 0 where
  x : Nat
  y : Nat
  deriving Repr, BEq, Ord, Hashable

def Coordinate.default : Coordinate := {x := 0, y := 0}

/--Returns the adjacent fields, row-major order (this is important)-/
def Coordinate.adjacents : Coordinate → List Coordinate
| { x := 0, y := 0} => [
                     ⟨1,0⟩,
    ⟨0,1⟩,           ⟨1,1⟩
  ]
| { x := 0, y := y} => [
    ⟨0,y.pred⟩,      ⟨1,y.pred⟩,
                     ⟨1,y⟩,
    ⟨0,y.succ⟩,      ⟨1,y.succ⟩
  ]
| { x := x, y := 0} => [
    ⟨x.pred,0⟩,                  ⟨x.succ,0⟩,
    ⟨x.pred,1⟩,      ⟨x,1⟩,      ⟨x.succ,1⟩
  ]
| { x := x, y := y} => [
    ⟨x.pred,y.pred⟩, ⟨x,y.pred⟩, ⟨x.succ,y.pred⟩,
    ⟨x.pred,y⟩,                  ⟨x.succ,y⟩,
    ⟨x.pred,y.succ⟩, ⟨x,y.succ⟩, ⟨x.succ,y.succ⟩
  ]

structure Part : Type 0 where
  symbol : Char
  position : Coordinate
  deriving Repr

structure PartNumber : Type 0 where
  value : Nat
  positions : List Coordinate
  deriving Repr, BEq

-- Schematic is just using lists, because at this point it's not clear what kind of lookup
-- is needed in part 2... Probably some kind of HashMap Coordinate Whatever, but that's
-- guesswork for now...
-- Parts can refine the data further, anyhow.
structure Schematic : Type 0 where
  parts : List Part
  numbers : List PartNumber
  deriving Repr

/-- nextByChar gives the coordinate of the **next** character. -/
private def Coordinate.nextByChar : Coordinate → Char → Coordinate
| {x := _, y := oldY}, '\n' => { x := 0, y := oldY + 1 }
| {x := oldX, y := oldY}, _ => { x := oldX + 1, y := oldY }

private def extractParts : List (Coordinate × Char) → List Part :=
  (List.map (uncurry $ flip Part.mk)) ∘ (List.filter $ not ∘ λ (sc : Coordinate × Char) ↦ sc.snd.isWhitespace || sc.snd.isDigit || sc.snd == '.')

private def extractPartNumbers (input : List (Coordinate × Char)) : List PartNumber :=
  let rec helper := λ
  | [], none => []
  | [], some acc => [acc] -- if we are still accumulating a number at the end, commit
  | a :: as, none =>
    if p: a.snd.isDigit then
      --start accumulating a PartNumber
      helper as $ some {value := a.snd.asDigit p, positions := [a.fst]}
    else
      --not accumulating, not a digit, not of interest.
      helper as none
  | a :: as, some acc =>
    if p: a.snd.isDigit then
      --keep accumulating
      helper as $ some {value := acc.value * 10 + a.snd.asDigit p, positions := a.fst :: acc.positions }
    else
      -- we were accumulating, aren't on a number any more -> commit
      acc :: helper as none
  helper input none

def parse (schematic : String) : Option Schematic := do
  -- I think this one is easier if we don't split the input in lines. Because:
  let charsWithCoordinates ← match schematic.toList with
    | [] => none
    | c :: cs => pure $ cs.scan (λ s c ↦ (uncurry Coordinate.nextByChar s, c)) (Coordinate.default, c)
  -- Whitespaces are **intentionally** left in. This makes extracting the numbers easier.
  pure $ {
    parts := extractParts charsWithCoordinates
    numbers := extractPartNumbers charsWithCoordinates
  }

def part1 (schematic : Schematic) : Nat :=
  -- fast lookup: We need to know if a part is at a given coordinate
  open Lean(HashSet) in
  let partCoordinates := HashSet.insertMany HashSet.empty $ schematic.parts.map Part.position
  let partNumbers := schematic.numbers.filter λnumber ↦
    number.positions.any λposition ↦
      position.adjacents.any partCoordinates.contains
  partNumbers.foldl (· + PartNumber.value ·) 0

def part2 (schematic : Schematic) : Nat :=
  -- and now it is obvious that keeping the parsed input free of opinions was a good idea.
  -- because here we need quick lookup for the numbers, not the parts.
  open Lean(HashMap) in
  let numberCoordinates : HashMap Coordinate PartNumber :=
    Lean.HashMap.ofList $ schematic.numbers.bind $ λ pn ↦ pn.positions.map (·, pn)
  let gearSymbols := schematic.parts.filter (Part.symbol · == '*')
  -- but the symbols aren't enough, they need to be adjacent to **exactly** 2 numbers
  let numbersNextGearSymbols := List.dedup &lt;$> gearSymbols.map λgs ↦
    gs.position.adjacents.filterMap numberCoordinates.find?
  let gearSymbols := numbersNextGearSymbols.filter (List.length · == 2)
  let gearRatios := gearSymbols.map $ List.foldl (· * PartNumber.value ·) 1
  gearRatios.foldl (· + ·) 0