#AoC 2023 | Day 3 | Solutions & Spoilers

1810 messages · Page 2 of 2 (latest)

cyan needle
lapis robin
cyan needle
#

So docstrings are literally strings you find at the top of your file, classes, and functions.

lapis robin
#

Oh is it the """ """

cyan needle
#
def get_number(seen,x,y):
    """ this uses regular expressions to find all the digits that are around x,y
        and turns them into a number and returns it, it also updates a set of seen
        coordinates to avoid double chekcing the same number"""
    b = ''.join(re.findall('\d+$', aocdata[x][:y]))
    a = ''.join(re.findall('^\d+', aocdata[x][y:]))
    for z in range(y-len(b),y+len(a)): seen.add((x,z))
    return int(b+a)
lapis robin
#

I think I have a problem of overcommenting because when I started learning I was always worried I'd forget what something was for and delete it

#

"started learning" makes it sound like ive been doing thius for awhile. I think I started at the end of Oct

cyan needle
lapis robin
#

Its been alot of fun so far!

lyric storm
cyan needle
lapis robin
cyan needle
cyan needle
lyric storm
crude ivy
#

Did someone say

#

verbosity

lyric storm
#

I did

crude ivy
#

That is nice

lyric storm
#

I had fun filling out every little element for Sphinx to pick up on

crude ivy
#

amazing documentation

crude ivy
#

sweet

lyric storm
#

It makes me so happy

#

But everyone hates it because its "too much"

crude ivy
#

oh no

#

nothing is ever "too much"

strange niche
#

so I'm feeling a bit lost with part one

#

it does the example just fine

crude ivy
proud rover
#

this is the core of my code, just trying to build a list with the values and whether we are going to sum them or not

def part_one(schematic):
    map = np.array([list(line.strip()) for line in schematic])
    h,l = map.shape

    digits = ['0','1','2','3','4','5','6','7','8','9']
    symbols = ['*','#','$','+']

    num = ''
    nums = []
    for (x,y), char in np.ndenumerate(map):
        to_sum = False
        if char in digits: 
            num += char
            for r in [-1,0,1]:
                for c in [-1,0,1]:
                    if x+r in range(0,h) and y+c in range(0, l):
                        if map[x+r][y+c] in symbols: 
                            to_sum = True

        else:
            if num:
                nums.append((int(num), to_sum))
                num = ''
    print(nums)

Even with the simplest example like 123# it recognizes the symbol but to_sum is always False no matter what. Probably some issue related to going way deeper in for loops than your supposed to

proud rover
#

it do be

#

hence why i mentioned it probably being related

lavish night
#

7 indent gang 😎

proud rover
#

just messed something up in the logic because of that

lavish night
#

at least if i counted right
mobile is tough

crude ivy
#

7 looks right

strange niche
crude ivy
#

have you tested it?

lavish night
light igloo
proud rover
lavish night
#

do you add that under the if where you set it to true?

proud rover
#

Oh, its because to get to the else branch it gets set to False before that

proud rover
#

all fixed

#

but with cursed nesting

#

actually still broken because of cases where it rolls over to the next line

#

not sure why the newline isnt catching that

cyan needle
#

don't share inputs. aoc explicitly asks you not to.

strange niche
#

ah

cyan needle
#

paste your code instead and I can check it on my input. but that number sounds close.

strange niche
#

it is. i just checked with some code i stole from here. 957 difference

wind wedge
#

@strange niche I recommend using the example input and check if it works.

wind wedge
#

Yeah also had this issue.. the example input is kinda bad.

strange niche
#

well apparently i'm missing a lot more then I thought

cyan needle
#

if the example works, make sure you're checking the last column as wellm as the example doesn't test that.

dim frigate
#

i'm curious why people are having bugs with the last column. i didn't encounter that

proud rover
serene scarab
#

Doesn't work when you're on the last column

dim frigate
#

ah. the falling edge thing

cyan needle
rocky drum
#

can someone posto a working solution for part b

serene scarab
#

Yeah, classic running-total mistake

dim frigate
strange niche
#

so just checked. yes I'm 967 off... missing some numbers

cyan needle
#

There is no wrapping in this puzzle

#

later puzzles might wrap. 🙂

serene scarab
strange niche
#

wrapping as in donut topography?

dim frigate
#

i parsed the numbers with

-- [((start-ind, end-ind), num)]
-- inclusive indexes
-- Extract all numbers with start and end index
findNumbers :: String -> [((Int, Int), String)]
findNumbers line = foldl solve [] (zip [0..] line)
    where solve acc (i, c) = if isDigit c
                        then case acc of
                            [] -> [((i, i), [c])]
                            all@(((s, e), num) : xs) -> if e == i - 1
                                then ((s, i), num ++ [c]) : xs
                                else ((i, i), [c]) : all
                        else acc

-- [(Length, X, Y)]
numParse :: [String] -> [((Int, Int, Int), Int)]
numParse inp = zip [0..] (map findNumbers inp) >>= broadcastRow
    where broadcastRow (r, nums) = map (\ ((start, end), num) -> ((end - start + 1, start, r), read num)) nums
cyan needle
serene scarab
dim frigate
strange niche
#

I stole some of my code from my implementation of game of life. just had to remove the wrap around

cyan needle
strange niche
#

interesting. well I should sleep. good night.

cyan needle
serene scarab
cyan needle
strange niche
#

sorry tired.

#

misunterstood

cyan needle
#

!paste a link to your code and some of us might look at it and see something you missed.

lilac epochBOT
#
Pasting large amounts of code

If your code is too long to fit in a codeblock in Discord, you can paste your code here:
https://paste.pythondiscord.com/

After pasting your code, save it by clicking the Paste! button in the bottom left, or by pressing CTRL + S. After doing that, you will be navigated to the new paste's page. Copy the URL and post it here so others can see it.

strange niche
#

and my code is kind of cancer

#

as I said. not pretty

rocky drum
#

can you explain this code for me. I don't quite understand

cyan needle
#

so board is the input file.
chars is a dictionary of the symbols with empty lists
then he goes through the board looking for numbers, and when he finds them adds them to the symbol's list in chars
finally he prints part1 and part2 using the data in that dictionary

strange niche
#

nope splits fine. well not sure what I do wrong

lyric storm
serene scarab
#

this time its even better™

lyric storm
#

I've been wanting to get into langdev

#

It makes my head hurt though

serene scarab
#

just start off simple, i was scared for the longest time because i kept thinking of all the complicated things

#

for instance, write a language that literally just lets you do

let x = 1
let y = 2
#

build up from there

hallow thunder
#

and crafting interpreters is always good for a proper guided approach

serene scarab
hallow thunder
cyan needle
strange niche
#

Huh. Yeah that might make problems

cyan needle
strange niche
#

I think there might also be a problem if I have two times the same number in the row

cyan needle
strange niche
#

Weird. Because it would always find the first instance of that number in the row. So presumably error when you have one digit numbers

cyan needle
strange niche
#

50.+50 should error

#
467.50.+50
...*......
..35..633.
......*100
617*......
.....+.58.
....2.....
......755.
...$.*....
.664.5....
cyan needle
# strange niche 50.+50 should error

here try this:

467..50+50
...*......
..35..633.
......*100
617*......
.....+.58.
2.2.2.....
......755.
...$.*....
.664.5....

you don't find the correct '2' on the line with the bad 2s

strange niche
#

Yes that makes sense. I should replace the number each time I do find with spaces

#

Or "." I guess. Then it should be fixed

jovial timber
#

So there I was, wanting to use regex for all the days. I thought, "This should be easy." Then the fact that all lookbehinds must be fixed width existed. So now I present probably the worst way of doing part 1. Still haven't looked at part 2, and I am very scared. ```py
import re;d,g,h,t=r"[^\d\n.]","(?<=%s%s)%s","|%s(?=%s%s)",open(0).read();l=len(t.split("\n")[0]);r="(?s)"+"|".join("|".join(g%(a:=(d,f".{{{i+l-j-1}}}",(e:=f"(\d{{{j+1}}})")))+h%a[::-1] for i in range(j+3))+"|"+g%(p:=(d,"",e))+h%p[::-1]for j in range(3)[::-1]);print(sum([int([*filter(None,x)][0]) for x in re.findall(r,t)]))

strange niche
#

Oh neat. Regex is a lot more readable like that

#

Didn't know that one

jovial timber
#

I probably would have pulled my hair out if not for regex101.

cyan needle
light igloo
#

... there's no way that regex works? to solve the puzzle?

cyan needle
#

looks like it works for part 1. Good luck with part 2

cyan needle
jovial timber
# light igloo ... there's no way that regex works? to solve the puzzle?

I got the right answer, so https://cdn.discordapp.com/emojis/811812701244358668.webp?size=48&name=Shrug&quality=lossless . It's conceptually the same as (?<=[^\d\n.].{1,4})(\d)|(?<=[^\d\n.])(\d)|(\d)(?=[^\d\n.].{1,4})|(\d)(?=[^\d\n.]) just massively complicated by the fact that lookbehinds must be fixed lengths, I threw in arbitrary line length size so it could also work for the example, and there are 3 different number sizes to account for.

strange niche
#

I store number (as a string but still) row, start of number and length

cyan needle
#

I might be misreading it

strange niche
#

It finds the index and then stores it. But I'm not sure how I am supposed to get that otherwise. Aim was to get the start of the number in the current line. And I screwed up because duplicates interfere. So I'll have to do a replace of the number on the line but just once

dim frigate
#

i originally thought part 2 would be some more difficult math due to the existence of the other symbols, +, -, / i think were there

daring dove
#

looking forward to day 4, but does it progress in difficulty?

dim frigate
#

something like that

cyan needle
cyan needle
# strange niche Don't I already do that? Should be in in indexesandlength

here try this:

indexesandlength = []
for rownum,line in enumerate(numrowssearchlist):
    index = 0
    while index<len(line):
        num = ''
        start = index
        while index<len(line) and line[index].isdigit():
            num+=line[index]
            index += 1
        if num:
            indexesandlength.append((num, rownum, start, len(num)))
        index += 1
print(indexesandlength)

should correctly put the indexes you want into indexesandlength and then your code works correctly for your input.

#

would be a bit easier with regex.

onyx crest
lyric storm
# onyx crest why do you even have a util for this? 🥴

I had a shit ton of duplicate code that I was trying to consolidate, and this was some low hanging fruit

It also gives me a really simple package to test tools on.
For example, it was my first PEP 517 compliant package, and it’s where I learned most of what I know about Sphinx.

onyx crest
#

I mean I guess it's nice for practice with that

#

but as a util it's pretty 🥴 to me

#

for some reason it reminds me of something like

# Increments the value of x by the integer one, resulting in a value of 1 more than the current value of x.
x += 1 
onyx pelican
#

I mean, having a util module that you just add generalized functions to in case you need to yoink em for future projects is actually incredibly useful

#

Anyway, here's my initial solution for p2:

import re
from typing import (
    Dict,
    Tuple,
    List,
    Set,
)
import math

def f(prompt: str) -> int:
    parts: Dict[Tuple[int, int], int] = dict()
    gears: List[Tuple[int, int]] = list()

    # populate
    for y, line in enumerate(prompt.strip().splitlines()):
        for m in re.finditer(r"\d+|\*", line):
            if m.group() == "*":
                gears.append((m.start(), y))
                continue
            value = int(m.group())
            for x in range(m.start(), m.end()):
                parts[(x, y)] = value

    # process
    total: int = 0

    for gear_x, gear_y in gears:
        neighbors: Set[int] = set()
        for y in range(gear_y - 1, gear_y + 2):
            for x in range(gear_x - 1, gear_x + 2):
                if (x, y) in parts:
                    neighbors.add(parts[(x, y)])
        if len(neighbors) >= 2:
            total += math.prod(neighbors)

    return total
onyx crest
onyx pelican
onyx crest
#

extracting general patterns is for sure useful

onyx pelican
#

I found an entire function dedicated to:

for substring in substrings:
    text = text.replace(substring, "")

lmfao

onyx crest
#

there are interesting things you can improve on for a "ReplaceAll" style function

onyx pelican
#

like with regex?

#

would definitely be faster for larger strings

lyric storm
onyx crest
# onyx pelican like with regex?

no, questions like what should something like this do?

ReplaceAll("abracadabra", [("ab", "blah"), ("abraca", "blahblah"), ("cadabra", "bleh")])
onyx pelican
#

ahh

onyx crest
#

common prefixes, overlapping patterns

#

if you don't allow overlaps other than prefixes you can actually do something nice by just sorting replacements in reverse

#

but for overlaps things get hairier

wise agate
#

Input

with open('day3/input.txt','r') as f:
    inputlines = f.read().splitlines()```

**Part One**
```py
symbols = "! @ # $ % ^ & * / + - = ".split()
r_length = len(inputlines)
c_length = len(inputlines[0])
numbers = []
parts = []

for r_index,row in enumerate(inputlines):
    for c_index, char in enumerate(row):
        if char in symbols:
            for r_offset in range(-1,2):
                for c_offset in range(-1,2):
                    r,c = r_index+r_offset,c_index+c_offset
                    if r < 0 or c < 0:
                        continue
                    if r >= r_length or c >= c_length:
                        continue
                    if inputlines[r][c].isdigit():
                        part = inputlines[r][c]
                        p_index = (r,c)
                        for offset in [1,2]:
                            if inputlines[r][c-offset].isdigit():
                                part = inputlines[r][c-offset] + part
                                p_index = (r,c-offset)
                            else:
                                break
                        for offset in [1,2]:
                            if inputlines[r][c+offset].isdigit():
                                part = part + inputlines[r][c+offset]
                            else:
                                break
                        part = (int(part),p_index)
                        if part not in parts: parts.append(part)

ans = sum([part[0] for part in parts])
print(ans)```
#

Part 2

r_length = len(inputlines)
c_length = len(inputlines[0])
ratios = []

for r_index,row in enumerate(inputlines):
    for c_index, char in enumerate(row):
        if char == '*':
            parts = []
            for r_offset in range(-1,2):
                for c_offset in range(-1,2):
                    r,c = r_index+r_offset,c_index+c_offset
                    if r < 0 or c < 0:
                        continue
                    if r >= r_length or c >= c_length:
                        continue
                    if inputlines[r][c].isdigit():
                        part = inputlines[r][c]
                        p_index = (r,c)
                        for offset in [1,2]:
                            if inputlines[r][c-offset].isdigit():
                                part = inputlines[r][c-offset] + part
                                p_index = (r,c-offset)
                            else:
                                break
                        for offset in [1,2]:
                            if inputlines[r][c+offset].isdigit():
                                part = part + inputlines[r][c+offset]
                            else:
                                break
                        part = (int(part),p_index)
                        if part not in parts: parts.append(part)
            if len(parts) == 2:
                ratios.append(parts[0][0] * parts[1][0])

ans = sum(ratios)
print(ans)```
#

Struggled a lot with this one and making it actually efficient/clean, couldn't manage to, would appreciate tips!

onyx crest
#

I think a usual choice is to greedily pick and consume the earliest and longest substring appearing

onyx crest
#

the offset thing feels like it would break for something like

12345.
.....+
wise agate
#

yeah

#

that could be changed pretty easily by just incrementing the offset by one and then breaking when you see a non digit char

#

this was just easier

onyx crest
#
while thing[offset].isdigit():
  ...
wise agate
#

Yeah exactly

onyx crest
#

I think this also breaks

.1
+.
#

with an indexing error

#

(and unintentional wraparound in the mirrored case)

wise agate
#
                    r,c = r_index+r_offset,c_index+c_offset
                    if r < 0 or c < 0:
                        continue
                    if r >= r_length or c >= c_length:
                        continue```
this*
onyx crest
#

can index outside the grid afaict

wise agate
#

hm

#

it does seem like that yeah, I'm surprised that didn't come up in the input at all

#

like the case never occured

onyx crest
#

I would probably have gone for this kind of pattern if I had wanted to do this

start = ...
while start-1 > 0 and s[start-1].isdigit():
  start -= 1
end = ...
while end < length and s[end].isdigit():
  end += 1

your_string = s[start:end]
#

I guess another thing, you could just use a set for parts in part 1 which would avoid doing an expensive in check in the list

#

if you want unique elements then that's what set is kinda for

proud rover
#

ok, so i rewrote my code so it is less egregiously nested, still running into the wrapping issue where:

23.$456
789....

returns 456789 because enumerate just keeps chugging along

def is_part(map, idxs):
    symbols = ['*','#','$','+','-', '=', '/', '@', '%']
    h,l = map.shape

    for idx in idxs:
        x,y = idx
        for r in [-1,0,1]:
            for c in [-1,0,1]:
                if x+r in range(0,h) and y+c in range(0, l) and map[x+r][y+c] in symbols:
                    return True
    return False     

def part_one(schematic):
    map = np.array([list(line.strip()) for line in schematic])
    digits = ['0','1','2','3','4','5','6','7','8','9']

    num_str = ''
    num_idxs = []
    sum = 0

    for (x,y), char in np.ndenumerate(map):
        if char in digits:
            num_str += char
            num_idxs.append((x,y))

        else:
            if num_str and is_part(map, num_idxs): 
                sum += int(num_str)
            num_str = ''
            num_idxs.clear()      

    print(sum) 
weak seal
#

ARGH

#

another goddamn day where I forget something simple and have to dig through the big dataset to figre out what the fuck I forgot

#

It was stupid and simple but that doesnt' mean I'm not still salty

#

I forgot to terminate parsing a number at the end of a line

lyric storm
#

Oh I did that too

weak seal
#

the example doesnt' have any numbers taht reach the end of the line -.-

onyx crest
#

pad and forget about edge cases 😌

weak seal
#

I built something to print out a color-coded dump of the stuff and that revealed it

lyric storm
weak seal
#

part 2 went flawlessly so I didn't need to adjust it to debug that

lyric storm
#

I blew up mine on part 2

#

Because I’m a dumbass I used the numbers as dict keys

#

Turns out they aren’t unique

weak seal
#

I caught that one early

#

trying to do the drawing

#

turns out one of my first numbers was a dupe and I was like "oh.. great."

dim frigate
onyx crest
#

ah, I did a groupWith isDigit so no weird edge cases

weak seal
#

so then I just stored [(word, coordinates)]

dim frigate
onyx crest
dim frigate
#

i don't think they have hashmaps builtin. Map is a search tree

#
findNumbers line = foldl solve [] (zip [0..] line)
    where solve acc (i, c) = if isDigit c
                        then case acc of
                            [] -> [((i, i), [c])]
                            all@(((s, e), num) : xs) -> if e == i - 1
                                then ((s, i), num ++ [c]) : xs
                                else ((i, i), [c]) : all
                        else acc
``` i kinda implemented my own groupby here 🥴
onyx crest
#

(and Sequence is also a tree)

#

finger tree iirc

weak seal
#

with part 2

dim frigate
#

it's like a christmas tree 🥺

dim frigate
onyx crest
wise agate
weak seal
wise agate
onyx crest
dim frigate
#

i had fun with comprehensions generating the surrounding chars

onyx crest
#

I switched to pre-parsing for part 2

#

well, I switched the entire approach

dim frigate
#

i guessed we would need to do something with the symbols since some of them looked like math operations and got lucky

onyx crest
#

only moderately cursed parsing

lilac epochBOT
#

day03/main2.hs lines 21 to 29

parse_line' digits (x:xs) | isDigit x = parse_line' (x:digits) xs
                          | x == '.'  = to_ints digits++empty:parse_line' [] xs
                          | x == '*'  = to_ints digits++gear :parse_line' [] xs
                          | otherwise = to_ints digits++other:parse_line' [] xs
parse_line' digits [] = to_ints digits
parse_line digits = parse_line' "" digits

parse :: [String] -> [[Int]]
parse grid = map parse_line grid```
dim frigate
#

i really like the ' tbh. saves me from needing to add _helper like in other langs

#

oh that kinda looks like what i did

weak seal
dim frigate
#

huh. what are you using currently to do the coloring?

weak seal
#

A grid of characters that I edit the colors of before printing

dim frigate
#

like, how do you edit though

weak seal
#

by giving them a color

dim frigate
#

like, manually ?

weak seal
#

I have a list of numbers, a list of part numbers

#

I loop through the part number and assign each matching character slot a color

#

The irritating part is I don't save the gear-making character

#

In order to mark them, I'd have to pass along their coordinates and the character through with each gearset, which gets complex enough I should probably use a dataclass instead of the tuple that's already there

dim frigate
#

ah that makes sense

#

when i parsed i kept the character around

onyx crest
#

I enjoyed the fact that you can just do local reasoning for part 2

#

i.e. you can look at all 3x3 neighborhoods independently

dim frigate
#

but you still have to map stuff back to a number which might span a neighborhood 🤔

onyx crest
#

you saw my parsing

#

I have the number info there already

#
12..
..*7

[12, 12,  0, 0]
[ 0,  0, -2, 7]
#

that kind of thing

dim frigate
#

ohh. i read it wrong

onyx crest
#

(and I pad my stuff, so this would be surrounded by a frame of 0s)

#

it puts the full int value in all the cells yeah

#

that's the weird part of the parsing

dim frigate
onyx crest
#

yeah

dim frigate
#

my linter keeps telling me to do stuff like this but idk if it's more readable that way

proud rover
#

are these are the symbols used?
['*','#','$','+','-', '=', '/', '@', '%']

onyx crest
#

you could just check whether it's ., a digit, *, or anything else

#

c != '.' and not c.isdigit()

dim frigate
#

i made the mistake of manually typing out all the ones i could find at first 😩

#

i have no idea why i did that. must have been tired or something 🥴

onyx crest
#

too tired to be lazy

dim frigate
#

i think i thought they would all just be symbols that you could get by shift-[0-9] on my keyboard?

#

that got me a wrong answer on part 1 lol

lavish night
#

something gotta love regex for too

onyx crest
#

@dim frigate coding without LSP is an interesting blast from the past

#

lots of searching on hoogle and elsewhere today

proud rover
#

so if the test case passes, but the puzzle input is too small, what factors could be at play? already went through the debugger and the logic works fine. the input is too small which would mean certain valid numbers are not being accepted for whatever reason

proud rover
lavish night
serene scarab
#

copilot / codewhisperer kinda make up for it

#

for my personal stuff my language doesn't have an LSP, for work stuff our code setups are such a nightmare setting up an LSP is it's own whole thing so I've just given up

#

raw-dogging programming

lavish night
#

numbers at edge of matrix. symbols at edge of matrix. multiple symbols to a number. multiple numbers to a symbol. symbols with no numbers. bumbers with no symbols. etc

#

come up with your own sample input

#

something small enough to do by hand to verify your code does what you intend

onyx crest
#

i.e. write a test suite

#

I ended up writing a few cases to debug part 2

#

(I had forgotten to even deal with number on the same row as the gear)

lavish night
#

i probably should do a tests.py but i've been lazy and prefer just raw dog getting the solution

onyx crest
#

like, I thought I added it, but apparently I forgot

#

I don't write my own tests unless I get it wrong 😛

dim frigate
#

^

serene scarab
#

I'm curious, what's the general approach you all took for part 2? Mine was:

gear_map: Map<gear, list of numbers>

loop over the grid:
   for current number, keep track of adjacent gears
   after number is done, add the number to the list for the adjacent gears in gear_map

for each gear in gear_map:
   if we have 2 numbers, add prod to the total
proud rover
#

looks correct when i calculate, will try to come up with more scenarios

onyx crest
proud rover
#

theres & too

dim frigate
# serene scarab I'm curious, what's the general approach you all took for part 2? Mine was: ``` ...
partTwo inp = sum [product v | v <- Map.elems counts, length v == 2]
    where counts = Map.fromListWith (++) $ nums >>= surroundingChars
          surroundingChars ((length, x, y), n) = [(p, [n]) | (p, c) <- adjacentSymbols sm length x y]
          sm = symbolMap inp
          nums = validNumbers sm $ numParse inp
``` for all numbers find adjacent symbols, if one's a gear then make a tuple with (location of gear, number) then join these into a map from gear to number by concatenating the lists
onyx crest
serene scarab
#

collect all the numbers around it
How do you "collect all the numbers"? Do you pre-parse the numbers and store them in a lookup table? Or do you parse the numbers on the fly by "expanding" any digits you see in the 3x3 neighbourhood?

proud rover
#

so that was fun!

spiral stump
dim frigate
#

oh that's like what fenix did

serene scarab
#

I was zoned-in to only thinking of the grid as a grid of characters

serene scarab
#

minus being completely imperative lol

dim frigate
#

i ditched the chars and just worked with the numbers and symbols by positions

onyx crest
#

I worked with a grid, which is painful in haskell

dim frigate
#

yeah, i wanted to avoid that lol

serene scarab
#

yeah my grid was just set as &Vector<str> so didn't even think to change the str to be a nested list

#

skill issue

serene scarab
onyx crest
#

oh lol, I read that as rust

dim frigate
#

it looks quite rusty

#

except Vector

serene scarab
#

it's kinda-rust syntax but C semantics basically

dim frigate
#

huh

#

so no safety?

serene scarab
#

nope

#

basically my thought process was I really like the precise control and freedom you get to do whatever the hell you want in C, but syntactically it's kind of a mess, and don't even get me started on header files and the whole (lack of) a proper module system and namespacing

#

so i just made that

dim frigate
#

do you have generics and RAII and stuff

onyx crest
#

RAII my beloved

serene scarab
#

i have basic templates (a la C++), no fancy traits or whatever

#

no RAII

#

once again, C-style programming

dim frigate
#

C style sadness

serene scarab
#

RAII is nice, but not what I was going for

#

there's a defer statement which you can kinda use, but that's about it

dim frigate
#

what's that do

onyx crest
#

"do this at end of scope"

dim frigate
#

interesting

serene scarab
#
let f = File::open(...)
defer f.close()

if (...) return   // closes file

return  // closes file
onyx crest
#

it still leaves doing stuff to the programmer, and I don't trust people with cleanup 😛

serene scarab
proud rover
#

well uh... it works?

def find_gear(map, idxs):
    h,l = map.shape

    for idx in idxs:
        x,y = idx
        for r in [-1,0,1]:
            for c in [-1,0,1]:
                if x+r in range(0,h) and y+c in range(0, l) and map[x+r][y+c] == '*':
                    return (x+r,y+c)
    return None          

def part_two(schematic):
    map = np.array([list(line.strip()) for line in schematic])
    digits = ['0','1','2','3','4','5','6','7','8','9']

    num_str = ''
    num_idxs = []
    sum = 0
    gears = defaultdict(list)

    for (x,y), char in np.ndenumerate(map):
        if char in digits:
            num_str += char
            num_idxs.append((x,y))
        
            if y == map.shape[1]-1:
                if find_gear(map, num_idxs): gears[find_gear(map, num_idxs)].append(int(num_str))
                num_str = ''
                num_idxs.clear()

        elif num_str and find_gear(map, num_idxs): 
            gears[find_gear(map, num_idxs)].append(int(num_str))
            num_str = ''
            num_idxs.clear()
        else:
            num_str = ''
            num_idxs.clear()
            
    for g in gears:
        if len(gears[g]) == 2: sum += math.prod(gears[g])
    print(f"part two sum: {sum}")    
hollow jolt
#

my solution for this one probablly could have been better

onyx crest
hollow jolt
#

the way I made it, it knows it's found the end of a number by checking if the next character is not a number

#

I was too lazy to try to add some logic for that so I just did this

onyx crest
#

yeah, that can break at end of rows

hollow jolt
#

oh man, that could have been happening to every row

#

i didn't actually think of that

#

amazing it worked though

onyx crest
#

depending how you read lines maybe you were saved by newline characters

hollow jolt
#

i believe splitlines does remove the newlines

onyx crest
#

ok no, the strip would have removed that anyways

#

or wait

#

wrong code

onyx crest
hollow jolt
#

it seems somehow there are some lines that do have numbers on the end

#

how it managed to get those, idk

#

unless they don't have symbols adjacent and didn't affect the output

#

nope, they do

#

strange

forest zenith
#

lemme just contribute my code before the next day starts

import re 

with open("input.txt", "r") as file:
  inp = file.read().split("\n")
  
  
part1 = 0

inp = ["."*len(inp[0])] + inp + ["."*len(inp[0])]
for index, line in enumerate(inp):
  inp[index] = "." + line + "."
  

pattern = re.compile(r"\d+")

for x, line in enumerate(inp):
  for match in pattern.finditer(line):
    start, stop = match.span()
    surroundings = inp[x-1][start-1:stop+1] + line[start-1] + line[stop] + inp[x+1][start-1:stop+1]

    if re.findall(r"[^\d\.]", surroundings):
      part1 += int(match.group())
      

print(part1)


part2 = 0

for x, line in enumerate(inp):
  for match in re.finditer("\*", line):
    gear_start, gear_stop = match.span()
    nums = []
    
    for row in (1, -1):
      for num_match in pattern.finditer(inp[x+row]):
        num_start, num_stop = num_match.span()
      
        if gear_start-1 <= num_start <= gear_stop or gear_start <= num_stop <= gear_stop+1:
          nums.append(num_match.group())
    
    for num_match in pattern.finditer(line):
      num_start, num_stop = num_match.span()
      
      if num_stop == gear_start or num_start == gear_stop:
        nums.append(num_match.group())
  
    if len(nums) == 2:
      part2 += int(nums[0]) * int(nums[1])
    
    
print(part2)
strange niche
peak oar
#

Part 1```py
lst = [x.replace("\n", "") for x in open("2023/day3/data.txt", "r").readlines()]
tot, used_nbrs = 0, []
for i, x in enumerate(lst):
for v, y in enumerate(x):
if not y.isdigit() and y != ".":
for p in ((-1, 0), (-1, -1), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)):
row = lst[i + p[0]]
if row[v + p[1]].isdigit() and (i + p[0], v + p[1]) not in used_nbrs:
new_int = row[v + p[1]]
for dir in (1, -1):
for l in range(1, len(row) - v - 1):
cur_ind = v + p[1] + dir * l
if row[cur_ind].isdigit():
used_nbrs.append((i + p[0], cur_ind))
new_int = new_int + row[cur_ind] if dir > 0 else row[cur_ind] + new_int
else:
break
tot += int(new_int)
print(tot)

#

Part 2: ```py
lst = [x.replace("\n", "") for x in open("2023/day3/data.txt", "r").readlines()]
tot = 0
for i, x in enumerate(lst):
for v, y in enumerate(x):
if y == "*":
ind, nmbs = [], []
for p in ((-1, 0), (-1, -1), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)):
row = lst[i + p[0]]
if row[v + p[1]].isdigit() and (i + p[0], v + p[1]) not in ind:
new_int = row[v + p[1]]
for dir in (1, -1):
for l in range(1, len(row) - v - 1):
cur_ind = v + p[1] + dir * l
if row[cur_ind].isdigit():
ind.append((i + p[0], cur_ind))
new_int = new_int + row[cur_ind] if dir > 0 else row[cur_ind] + new_int
else:
break
nmbs.append(int(new_int))
if (len(nmbs) == 2):
tot += nmbs[0] * nmbs[1]
print(tot)

zenith kite
#

puzzle_a.py

from typing import Iterable

from common import Schematic, SchematicNumber, get_schematic


def main() -> None:
    schematic = get_schematic()
    part_numbers = find_part_numbers(schematic)
    part_numbers_sum = sum(part_number.number for part_number in part_numbers)

    print(f'The sum of part number for {schematic.file_name} is {part_numbers_sum}')


def find_part_numbers(schematic: Schematic) -> Iterable[SchematicNumber]:
    symbol_locations = {symbol.location for symbol in schematic.symbols}

    for number in schematic.numbers:
        if symbol_locations & number.bounding_box:
            yield number


if __name__ == '__main__':
    main()
#

puzzle_b.py

from collections import defaultdict
from typing import Iterable

from common import Schematic, get_schematic


def main() -> None:
    schematic = get_schematic()
    gear_ratio_sum = sum(get_gear_ratios(schematic))

    print(f'The sum of gear ratios for {schematic.file_name} is {gear_ratio_sum}')


def get_gear_ratios(schematic: Schematic) -> Iterable[int]:
    numbers_by_bounding_box_location = defaultdict(list)
    for number in schematic.numbers:
        for location in number.bounding_box:
            numbers_by_bounding_box_location[location].append(number) 

    valid_gear_locations = {
        location for (location, numbers) in numbers_by_bounding_box_location.items()
        if len(numbers) == 2
    }

    for symbol in schematic.symbols:
        if symbol.symbol == '*' and symbol.location in valid_gear_locations:
            first_number, last_number = numbers_by_bounding_box_location[symbol.location]
            yield first_number.number * last_number.number


if __name__ == '__main__':
    main()
#

common.py

import string
from argparse import ArgumentParser
from dataclasses import dataclass
from typing import Iterable


@dataclass(frozen=True)
class SchematicNumber:
    number: int
    line_number: int
    start_index: int
    end_index: int

    @property
    def bounding_box(self) -> set[tuple[int, int]]:
        box_locations = {
            (self.start_index + x_offset, self.line_number + y_offset)
            for x_offset in range(-1, self.end_index - self.start_index + 2)
            for y_offset in range(-1, 2)
        }

        number_locations = {
            (self.start_index + x_offset, self.line_number)
            for x_offset in range(self.end_index - self.start_index + 1)
        }

        return box_locations - number_locations


@dataclass(frozen=True)
class SchematicSymbol:
    symbol: str
    line_number: int
    index: int

    @property
    def location(self):
        return (self.index, self.line_number)


@dataclass
class Schematic:
    file_name: str
    lines: list[str]
    numbers: list[SchematicNumber]
    symbols: list[SchematicSymbol]


def get_schematic() -> Schematic:
    file_name = _get_arguments()
    lines = _read_file(file_name)
    numbers, symbols = _parse_lines(lines)

    return Schematic(
        file_name,
        lines,
        numbers,
        symbols,
    )
    

def _get_arguments() -> str:
    parser = ArgumentParser()
    parser.add_argument('file_name')

    args = parser.parse_args()

    return args.file_name


def _read_file(file_name: str) -> list[str]:
    with open(file_name) as file:
        return file.read().splitlines()
#


def _parse_lines(lines: list[str]) -> tuple[list[SchematicNumber], list[SchematicSymbol]]:
    numbers: list[SchematicNumber] = []
    symbols: list[SchematicSymbol] = []

    for line_number, line in enumerate(lines):
        for number_or_symbol in _parse_line(line_number, line):
            if isinstance(number_or_symbol, SchematicNumber):
                numbers.append(number_or_symbol)
            else:
                symbols.append(number_or_symbol)

    return numbers, symbols


def _parse_line(line_number: int, line: str) -> Iterable[SchematicNumber | SchematicSymbol]:
    current_number: list[tuple[int, str]] = []

    def create_schematic_number(end_index: int) -> SchematicNumber:
        return SchematicNumber(
            int(''.join(x[1] for x in current_number)),
            line_number,
            current_number[0][0],
            end_index,
        )
    
    for index, char in enumerate(line):
        if char in string.digits:
            current_number.append((index, char))
            continue

        if current_number:
            yield create_schematic_number(index - 1)
            current_number.clear()

        if char != '.':
            yield SchematicSymbol(char, line_number, index)

    if current_number:
        yield create_schematic_number(len(line) - 1)
#

Perhaps, over-engineered, but I ended up fully parsing the input, getting a more useful representation of every symbol and number which then meant that the processing for each part of the problem was quite simple after that.

#

I'm aiming for good practices/readability in the advent challenge (excepting when I try golfing the solution) so don't mind it being a bit longer.

quartz perch
bright wedge
#

was one of those days where you see part 2 and instantly know that you chose the worst possible approach to part 1 😛

#

I ended up redoing my part 1

#

If you assign an id to every part of a number, during parsing, it makes both parts fairly straightforward

lilac jewel
bright wedge
#

Yeah, I was always pretty into the grid approach. But when I did part 1, I did fairly naive parsing (just parsed each grid cell to a part, digit, or nothing), and then I looped over it, and handled things by number

#

It was reasonable but as soon as I read part 2, I realized that part practically had to be done handling things by part

full forum
spiral stump
#

I might return to this day and try and do a regex solution

lilac jewel
#

as is using regex

spiral stump
#

difficulty is the pattern tho since you have variable digit lengths

full forum
#

I actually encountered a bug with regex because I had +-/, so it included .

#

also the france string

lilac jewel
#

because i haven't actually checked if the input data allowed for repeats of the same number, i just attached an ID to each number

#

so then when looking at the neighbours around a tile, you can remove any duplicate-ID nums for part 1

#

the parser is a monstrosity

bright wedge
#

I debated between the two approaches and think it's all around cleaner though to only store the ID, and then a separate map for ID -> value

lilac jewel
#

ooh, that is kinda clean

bright wedge
#
fn parse_grid(input: &str) -> (Grid<Entry, 2>, Vec<i64>) {
#

that's why my parser's signature looks like this

#
#[derive(Debug, Clone, Copy)]
enum Entry {
    Nothing,
    Part,
    MaybeGear,
    Number(usize),
}
#

Number is only storing an ID, and then you just get the value from the Vec (the ID's are just getting incremented so they go 0 to N-1

lilac jewel
#

i was kinda so fed up that i'd missed the deadline of completing it before day 4 came out that i was just throwing whatever ideas came into my head together :P

digital pebble
bright wedge
#

fair enough. I was also pretty irritated at rewriting the whole thing for part b) 😛

#

I remember looking at part a) and being like "okay, do iterate over parts, or over numbers?"

#

bad choice lol

digital pebble
#

part 2 was easy with what i chose

bright wedge
#

Yeah, I started by doing naive parsing (just into part/digit) and then part a was identifying numbers, and searching around the whole border for a part

#

probably actually my part a) was a little less clean with my original approach, but I'd guess that it's much faster

digital pebble
#

i did that for both parts though, searching around the border of a number

bright wedge
#

in the new approach I have to keep a HashSet of ID's

#

you did that for b)? That seems a lot more painful

digital pebble
#

i just made one dict and used it for both parts

#
def find_symbol(y, x1, x2):
    for v in [y - 1, y, y + 1]:
        for u in range(x1 - 1, x2 + 1):
            if 0 <= v < H and 0 <= u < W and GRID[v][u] not in ".0123456789":
                return v, u

def find_all_symbols():
    for y, line in enumerate(MATCHES):
        for part, (x1, x2) in line:
            if coord := find_symbol(y, x1, x2):
                COORDS.setdefault(coord, []).append(part)

find_all_symbols()

def part_one():
    return sum(sum(parts) for parts in COORDS.values())

def part_two():
    return sum(a * b[0] for a, *b in COORDS.values() if b)
#

MATCHES are just spans of numbers

bright wedge
#

I guess I don't quite follow. If you're searching around the borders of numbers, you'd have to have some extra persistent state to go back later and see if the *'s you found around the number are really gears

digital pebble
#

when i find a symbol, i store it in a dict with its coordinate as the key, and the value a list of parts

#

i do that while i'm going around the border of a number

bright wedge
#

okay, sure, that's what I mean

#

It seemed a lot cleaner to me, once I read part b) to do things part-oriented isntead

#

I never have to go around the borders of numbers anymore, which I found was the ugliest part of my original approach

digital pebble
#

don't you have to go around the border of a symbol though?

bright wedge
#

you do, but going around the borders of parts is simpler/cleaner

acoustic fjord
#

^

bright wedge
#

because it's always the same

digital pebble
#

why

bright wedge
#

a part is always one square

#

a number has arbitraryw idth

digital pebble
#

yeah, but it's hard to find the start of a number it touches

bright wedge
#

why is it hard to find the start of the number?

#

oh, you mean, once you find a digit, you have to go back and parse the whole thing?

digital pebble
#

yeah

bright wedge
#

yeah, my approach avoids that too

digital pebble
#

dunno, going around a number seems just as easy as going around a part

bright wedge
#

when I'm doing the initial parsing, when I find a run of digits, I give them all the same ID. And then I push their value into a separate container

digital pebble
#

like, the same loop

bright wedge
#

so if if have

.....
.123.
.....
#

then after parsing, what I'll have there is

.....
.000.
.....
#

and then I push the value 123 into a separate array

digital pebble
#

preprocessing numbers also seems harder

bright wedge
#

it's not really. Whenever you deal with it, you'll need to deal with computing the value of each number

#

it's somewhat ugly no matter what

digital pebble
#

not hard, just harder, i think both ways end up doing about the same amount of work

#

i suppose there's less symbols to search around

bright wedge
#

idk, I switched from one to the other in part a) and this approach was cleaner IMHO. And part a) was the one where it's more of a toss up, for b) it's even easier. but it also depends on your tolerance for creating extra dicts

digital pebble
#

i just need the one dict though

#

i feel like my solution was clean

#

not as clean as my rect solution

bright wedge
#

I guess I didn't really follow how it's one dict. it's a bit hard for me to understand the code there

digital pebble
#

COORDS is dict[tuple[int, int], list[int]]

bright wedge
#

it's a map from coordinates to what exactly?

digital pebble
#

the tuple[int, int] is the coordinate of the symbol that was touching a number

#

and the list is the number(s)

bright wedge
#

okay, so for all symbols that were touching something, it's a map of symbol coordinates to numbers

digital pebble
#

yeah

bright wedge
#

So you built this dict by going around parts directly during parsing

digital pebble
#

yeah

bright wedge
#

yeah, I mean that's fine, I just didn't like having to do that

#

both because I don't like having to go around a number, but also because I didn't really like the idea of building a reverse lookup like that

digital pebble
#

i had most of this written for part 1, so it didn't take much to add part two

bright wedge
#

because of fairly arbitrary preferences around efficiency

#

Yeah, so it was different for me, because I didn't, and didn't want to build the reverse lookup

#

I was going around parts for part a) (not during parsing), and simply immediately accumulating

#

sorry, around numbers

#

for part 1) I'm not sure what motivated you to build the reverse lookup; you don't need it for anything

digital pebble
#

no i didn't have it for part 1

bright wedge
#

okay, sorry, you just said "i had most of this written for part 1"

digital pebble
#

i just accumulated, but i had the loops around the numbers already

#

yeah, to add the dictionary was like 2-3 more lines

bright wedge
#

yeah, I thought about doing that, I just didn't like it

digital pebble
#

since i was already looping around numbers i just needed a way to remember which symbols i had seen

bright wedge
#

right

#

for me, just assigning an ID per number was a lot simpler. You had to anyway traverse the number to accumulate the full value right

#

so I'm not sure what you mean by "preprocessing numbers seems harder"

digital pebble
#

yeah, just creating the id -> number, is an extra step

bright wedge
#

well, that part is basically one line, it's the easiest part

digital pebble
#

but like both approaches are doing this

bright wedge
#
            if row.chars().next().unwrap().is_digit(10) {
                let non_digit = row.find(|x: char| !x.is_digit(10)).unwrap_or(row.len());
                values.push(row[..non_digit].parse().unwrap());
                row = &row[non_digit..];
                let non_digit = non_digit as i64;
                for i in col_index..(col_index + non_digit) {
                    grid[[i, row_index as i64]] = Entry::Number(id_num);
                }
                col_index += non_digit;
                id_num += 1;
                continue;
            }
#

the values.push(...) is where the ID -> number mapping is created

digital pebble
#

have you seen a solution use rectangular regions?

from rects import Rect, Region

operators = Region()
for y, line in enumerate(GRID):
    for m in re.finditer(r"([^\d\.])", line):
        operators |= Region.from_rect(Rect(m.start(), y, 1, 1))

print(sum(
    int(m[1])
    for y, line in enumerate(GRID)
    for m in re.finditer(r"(\d+)", line)
    if Region.from_rect(Rect(m.start() - 1, y - 1, len(m[1]) + 2, 3)) & operators
))
#

just make a big set of rectangles around all the operators and intersect each rectangle around a number with that set

bright wedge
#

operators?

#

you mean parts?

digital pebble
#

the symbols

#

sorry, they look like binary operators

bright wedge
#

I can't even begin to understand how this works 😛

digital pebble
#

its like using line sweeps, library is meant for, like, widget regions in a gui

bright wedge
#

So I guess it makes operators the set of points that are rectangles with border 1 around digits

digital pebble
#

yeah

#

around symbols

#

not the digits, but you could also do it around digits

#

it would work either way

bright wedge
#

In the first part isn't it doing \d for digit?

#

oh ^ not

#

I hate regex

digital pebble
#

yeah, same

bright wedge
#

the opposite would be signficantly more annoying because then when you scan the symbols in part two you wouldn't have the values

digital pebble
#

actually i should've done around digits instead of spans of numbers cause its simpler

bright wedge
#

so you'd have to store that somewhere

digital pebble
#

yeah i didn't think of how to do part 2 with this

bright wedge
#

and then it would start to look more like other solutions

digital pebble
#

but i thought it was cool it could do part 1

bright wedge
#

but obviously the rectangle library is doing a lot of the heavy lifting here

#

also I gotta say I really hate golfed python 😦

digital pebble
#

yeah, it also takes like 5 or 6 seconds to do all these intersections

bright wedge
#

these long list comprehensions are so hard to read compared to their equivalents in other languages since everything is backwards

regal mesa
#

Horrible solution

def part01(schematic):
    lines = schematic.split("\n")
    total = 0
    height = len(lines)
    width = len(lines[0]) if height > 0 else 0
    symbols = {"*", "#", "+", "$", "=", "%", "/", "@", "-", "&"}

    def is_symbol_adjacent(x, y, length):
        for i in range(length):
            if any(
                check(x + i, y)
                for check in [
                    is_symbol_left,
                    is_symbol_right,
                    is_symbol_above,
                    is_symbol_below,
                    is_symbol_diagonal_up_left,
                    is_symbol_diagonal_up_right,
                    is_symbol_diagonal_down_left,
                    is_symbol_diagonal_down_right,
                ]
            ):
                return True
        return False

    def is_symbol_left(x, y):
        return x > 0 and lines[y][x - 1] in symbols

    def is_symbol_right(x, y):
        return x < width - 1 and lines[y][x + 1] in symbols

    def is_symbol_above(x, y):
        return y > 0 and lines[y - 1][x] in symbols

    def is_symbol_below(x, y):
        return y < height - 1 and lines[y + 1][x] in symbols

    def is_symbol_diagonal_up_left(x, y):
        return x > 0 and y > 0 and lines[y - 1][x - 1] in symbols

    def is_symbol_diagonal_up_right(x, y):
        return x < width - 1 and y > 0 and lines[y - 1][x + 1] in symbols

    def is_symbol_diagonal_down_left(x, y):
        return x > 0 and y < height - 1 and lines[y + 1][x - 1] in symbols

    def is_symbol_diagonal_down_right(x, y):
        return x < width - 1 and y < height - 1 and lines[y + 1][x + 1] in symbols

    for y, line in enumerate(lines):
        matches = re.finditer(r"\b\d{1,3}\b", line)
        for match in matches:
            number = match.group()
            start = match.start()
            if is_symbol_adjacent(start, y, len(number)):
                total += int(number)

    return total
#
def part02(schematic):
    lines = schematic.split("\n")
    height = len(lines)
    width = len(lines[0])
    total_ratio_sum = 0
    pattern = re.compile(r"\d{1,3}")  # Pattern to find numbers up to 3 digits

    for x, line in enumerate(lines):
        for match in re.finditer("\*", line):
            gear_start, gear_stop = match.span()
            nums = []

            # Check vertically (above and below)
            for row in (1, -1):
                if 0 <= x + row < height:  # Ensure within grid bounds
                    for num_match in pattern.finditer(lines[x + row]):
                        num_start, num_stop = num_match.span()
                        if (
                            gear_start - 1 <= num_start <= gear_stop
                            or gear_start <= num_stop <= gear_stop + 1
                        ):
                            nums.append(num_match.group())

            # Check immediate left and right on the same line
            for num_match in pattern.finditer(line):
                num_start, num_stop = num_match.span()
                if num_stop == gear_start or num_start == gear_stop:
                    nums.append(num_match.group())

            # Calculate gear ratio if two numbers are found
            if len(nums) == 2:
                total_ratio_sum += int(nums[0]) * int(nums[1])

    return total_ratio_sum
tall heart
#

I'm quite late to the party!

#

a solution in google sheets !

celest nimbus
#

not my proudest solution, but here: ```py
def part_1(inp: str) -> int:
items, grid = parse(inp)

numbers = [
    int(item.value)
    for item in items
    if item.value.isdecimal() and touching_symbol(grid, item)[0]
]

return sum(numbers)

def part_2(inp: str) -> int:
items, grid = parse(inp)

for item in items:
    if item.value.isdecimal():
        touching = touching_symbol(grid, item)

        if touching[0] and touching[1].value == "*":  # type: ignore
            sym = typing.cast(GridItem, touching[1])
            num = next(i for i in items if i.row == sym.row and i.span[0] == sym.span[0])
            num.touching.append(int(item.value))

gears = [i.touching for i in items if len(i.touching) == 2]
return sum(math.prod(i) for i in gears)
#

items are all GridItems ```py
@dataclass
class GridItem:
value: str
row: int
span: tuple[int, int]
touching: list[int] = field(default_factory=list)

#

got some time away from school to finish up my solution 😌

stable leaf
#

Serious question, how ashamed of this code should i be

with open("day3_input.txt", "r") as file:
    doc = file.read().split("\n")
    doc.pop()  # last child is empty
    max_i = len(doc) - 1
    max_j = len(doc[0]) - 1
    information = []
    for i, line in enumerate(doc):
        found_num = False
        for j in range(len(line)):
            if not found_num and line[j].isdigit():
                found_num = True
                start = j
            if found_num and j == max_j or found_num and not line[j + 1].isdigit():  # end of line or end of number
#
                if 0 < i < max_i and 0 < j < max_j and 0 < start < max_j:  # within
                    matrix = [doc[i - 1][start - 1:j + 2],
                              doc[i][start - 1:j + 2],
                              doc[i + 1][start - 1:j + 2]]
                elif i == 0 and 0 < j < max_j and 0 < start < max_j:  # top
                    matrix = [doc[i][start - 1:j + 2],
                              doc[i + 1][start - 1:j + 2]]
                elif 0 < i < max_i and j == max_j:  # right
                    matrix = [doc[i - 1][start - 1:j + 1],
                              doc[i][start - 1:j + 1],
                              doc[i + 1][start - 1:j + 1]]
                elif i == max_i and 0 < j < max_j and 0 < start < max_j:  # bottom
                    matrix = [doc[i - 1][start - 1:j + 2],
                              doc[i][start - 1:j + 2]]
                elif 0 < i < max_i and start == 0:  # left
                    matrix = [doc[i - 1][start:j + 2],
                              doc[i][start:j + 2],
                              doc[i + 1][start:j + 2]]
                elif i == 0 and start == 0:  # top left
                    matrix = [doc[i][start:j + 2],
                              doc[i + 1][start:j + 2]]
                elif i == 0 and j == max_j:  # top right
                    matrix = [doc[i][start - 1:j + 1],
                              doc[i + 1][start - 1:j + 1]]
                elif i == max_i and j == max_j:  # bottom right
                    matrix = [doc[i - 1][start - 1:j + 1],
                              doc[i][start - 1:j + 1]]
                elif i == max_i and start == 0:  # bottom left
                    matrix = [doc[i - 1][start:j + 2],
                              doc[i][start:j + 2]]
                found_num = False
                info = Info(int(line[start:j+1]), matrix)
                if info.is_part_number():
                    information.append(info.get_value())
print(sum(information))```
crude ivy
forest zenith
#

it's still just part 1, haven't found a way to do part 2

acoustic fjord
#

Did you not have anything better to do?? (/lh)

forest zenith
#

LOL

jovial timber
forest zenith
#

i have no idea why non-fixed length lookaheads are allowed but lookbehinds aren't

jovial timber
#

@forest zenith @cyan needle I have done the thing, part 2 using only regex. The generated regex is 33,679 characters long and is pasted here . The code that generates it is pasted here . The worst part is that it's faster than my original solution.

jovial timber
crude ivy
#

verbosity

#

Dear god

#

this is why people don't like regex

jovial timber
#

Regex is just close enough to being a programming language that it's frustrating when it isn't. (especially the flavors that have recursion.)

daring dove
#

I love using regex. It can be fun to solve problems with it

vapid forge
#

but apparently any number adjacent to a symbol, even diagonally,
oh, fuck you

#

guess I'll have to learn about matrixes now huh?

vapid forge
jovial timber
# vapid forge so uh, did you write that pattern by hand? lmfao

I didn’t write the 33k by hand, but I did write all the cases by hand. That’s why it’s formatted like it is in the code with white space that gets removed, otherwise it would have been impossible to deal with. I also had to re-write it three times, once I only did 5x3 instead of 7x3, and then I didn’t factor in all the [\d*.]s, I had them as just more \.s.

vapid forge
#

did you generate it programatically first then?

#

because jesus fuck, that thing is huge. I consider myself a regex lover, and even that is too extreme for me lmao

jovial timber
#

I’m not sure I understand the question. I manually wrote all the possible vertical, left, right, null, and vertical special cases. itertools.product then generates the necessary combinations of those, each of which is a mini case that has one above, left, right, and below component. Those mini cases then get combined into the giant regex, which is then applied to solve the problem.

#

For what it does, I feel like I actually wrote very clean code (especially when compared to my part 1 regex generator).

vapid forge
#

the fact I have no idea what you're talking about really says a lot about my level of understanding lmao

#

but thank you for humoring me lol

daring dove
#

you'll have real fun at day 5

vapid forge
#

just from reading day 3 puzzle, I'm thinking of tackling it with numpy matrices

jovial timber
# vapid forge the fact I have no idea what you're talking about really says a lot about my lev...

The key part to understand is this line py pattern.append(rf"(?:(?<={above}.{{134}}{left})\*(?={right}.{{134}}{below}))")
It’s in a non-capturing group (?:…) because otherwise all the characters in the entire three rows would be in the match, and that would stop re.findall from using any of that space again, preventing any two cogs in the same row from both matching.
The first part is a look behind (?<=…) where above is a seven character long statement that is one of the possible cases. This is then followed by 134 of any character, which puts the match head right before the start of the 3 character long group to the left of the *. That is what left is for , and then that ends the look behind. This is followed by an escaped \* to match the cog, then the reverse of that pattern for the lookahead (?=…). All of these get joined together with ors |, which is the other reason it’s all in a non capturing group, since that groups it into one or-able statement. All of these combined are preceded by a (?s) to make . match newlines, and that’s the final regex.

#

If you look at the product statements right above that, you’ll see that all of them have two non-null parts and two null parts, except for the vertical specials which have 3 nulls. This means that every match will only have two capture groups, which is the two numbers that get multiplied. The vertical specials are with three nulls because those are the cases where both numbers are on the same line above/below the cog.

crude ivy
#

You don't have to go down that approach but it's probably the easiest one to code and to reason about in general.

vapid forge
# crude ivy I would suggest not to, tbh. IMO, your best bet would be just to preprocess the ...

without having tried yet, here's what I intend on doing:

  1. Create a class for the numbers. Each instance of the class will be an object that will store the matrix coordinates of all digits in the number, and will have methods to search all coordinates around all digits
  2. Process the input to create a 2d numpy array, and process it to instantiate the class
  3. Execute the method and sum the correct numbers
crude ivy
vapid forge
crude ivy
#

What the values are depends on what you want to save but the keys would be your coordinates

vapid forge
#

so like

#

"3,5": 5 for example, and then proc that string to separate at the comma and int the digits?

spiral stump
#

strings are hard to work with

crude ivy
#

Yeah, would be better to use an object, like a tuple or dataclass

#

Or you can use the inbuilt complex number type, even though you really shouldn’t :3

vapid forge
spiral stump
#

essentially a way to make lightweight classes quickly without boilerplate

#

!e

from dataclasses import dataclass
@dataclass
class Coord:
    x: int
    y: int
    def __add__(self, other):
        return Coord(self.x + other.x, self.y + other.y)
    p1 = Coord(3, 4)
    p2 = Coord(1, 2)
    p3 = p1 + p2
    print(p1, p2, p3)
    print(p1.x, p2.y, p3.x + p3.y)
#

...

#

did I really just do that

#

!e

from dataclasses import dataclass
@dataclass
class Coord:
    x: int
    y: int
    def __add__(self, other):
        return Coord(self.x + other.x, self.y + other.y)
p1 = Coord(3, 4)
p2 = Coord(1, 2)
p3 = p1 + p2
print(p1, p2, p3)
print(p1.x, p2.y, p3.x + p3.y)
lilac epochBOT
#

@spiral stump :white_check_mark: Your 3.12 eval job has completed with return code 0.

001 | Coord(x=3, y=4) Coord(x=1, y=2) Coord(x=4, y=6)
002 | 3 2 10
spiral stump
vapid forge
spiral stump
acoustic fjord
vapid forge
#

cool

acoustic fjord
#

(parameters to the decorator)

vapid forge
vapid forge
spiral stump
#

slots=True is a good one

acoustic fjord
spiral stump
#

isn't total ordering functools

acoustic fjord
acoustic fjord
#

Dataclasses can just straight up generate the entire thing

#

You do have to explicitly ask for order=True though

spiral stump
#

ah

#

nice

acoustic fjord
#

You ask for frozen=True and now when you create one you aren't allowed to modify that instance (you have to create a new one)

crude ivy
#

But it’s AOC so whatever :)

#

Although I didn’t appreciate that .real and .imag are floating points

spiral stump
#

semantically?

crude ivy
#

As in, these aren’t actually complex numbers but you are using a complex type to represent them

spiral stump
#

eh
the mapping between 2 dim vectors and complex numbers is pretty ubiquitous across math imo

#

the argand diagram is very popular and that's essentially just the 2d plot of real and imag

#

heck treating complex numbers as 2d vectors was a part of my hs math course

acoustic fjord
spiral stump
#

yeah that's what I was referring to

#

just without the math terminology

acoustic fjord
#

Lmao fair enough

#

I was more pointing out it's not usually 'a mapping' and more that they're literally just the same thing

#

(two different sets of arithmetic on the same field)

crude ivy
#

I did learn complex and argand at school. So I do get how these can be represented by complex numbers. I just never really considered them as the same thing

#

Maybe that’s my lack of math knowledge but my outlook was just because you can represent them as complex numbers, doesn’t mean that that is what they are

spiral stump
#

they're the same for addition, but different for other things, like C has multiplication C x C -> C whereas R2 doesn't have that notion built in

acoustic fjord
spiral stump
#

AyameShrug I don't feel it's proper to say that because part of the field is its operations
R2 isn't a field anyhow so

acoustic fjord
#

Yeah I possibly mean set

spiral stump
# crude ivy Maybe that’s my lack of math knowledge but my outlook was just because you can r...

my personal take on this is that stuff is defined according to what properties they have
we have some intuitive notion of a 2d point in space, and whatever we find that works the same way as we would expect those 2d points to work is effectively a representation of those 2d points
representing 2d points as a tuple of x y coordinates, as complex numbers, as a tuple of radius and phase (polar coordinates), as a ring of polynomials with 1+x^2=0, are all fine representations of the concept of 2d points to me
then picking a representation is just a choice deciding which one is easiest to use

crude ivy
#

Fair enough. I’ll defer to your opinion on this one. I still think the complex number type is not perfect because it’s floating point though

#

Even if I only learned this fact a few mins ago and even if the floating point resolution is probably fine for the AOC problems

spiral stump
#

I mean you don't have to defer to anything
if you feel it's unnatural, then you feel it's unnatural

#

I do agree with the FP thing

#

it's not large enough to affect results, but just that they're floats is annoying

#

like I worked on day 17 for a bit like 5 hours ago and converting in between ints and floats looks ugly

crude ivy
wind lion
spiral stump
acoustic fjord
#

Lmao

vapid forge
#

btw @crude ivy I've been doing that thing you told me, to write a logic for example first, and then applying to the real input

#

it has been great

crude ivy
#

It is pretty good lol

vapid forge
#

much easier to work with. thanks for the tip

crude ivy
#

Except when the example works and the input doesn’t

#

That’s my current hell for day 17

vapid forge
#

I'll worry about it when I get there lol

crude ivy
#

It might be worth considering going one step further and using a unit test framework

#

You can write unit tests in a certain way and use either unittest or pytest packages to automatically run the tests

vapid forge
crude ivy
#

Here’s my day 1

#

Notice how there’s a test file?

#

I’ve written tests in there according to the pytest framework style

#

With one command I can run any/all the tests to see if they pass or fail

old canopy
#

@crude ivy What do you think?

crude ivy
#

Checking - currently on my phone so ain’t easy :)

#

GH is easier to view from since it has an app

old canopy
#

TIL

#

if it's a bother don't worry about it

crude ivy
#

Right off the bat I can say you don’t need all your code nested in the with

#

If you are going to read the entire thing in one line then that’s all that’s needed under the with

old canopy
#

Yeah I just sort of ran with it haha.

crude ivy
#

Everything else can be dedented

old canopy
#

thanks

crude ivy
#

I’m home with the flu so I have nothing better to do

#

It’s either this or binge YouTube

old canopy
#

oh no :(

#

I got the flu vaccine and COVID vaccine at the same time and I rarely go outside. My mom and I still mask up when we leave the apartment because she's got lung problems.

#

flu sucks, hope you feel better

crude ivy
#

I did get the flu vaccine this winter

#

Anyway - the code

#

I think the thing is that you have to check each line for symbols potentially 3 times

#

Once as a previous line, once as the current line, once as the next line

#

There’s a lot of scope for repeated checks

old canopy
#

Yes

crude ivy
#

Which is why preprocessing would be more elegant

#

Also

#

If you find that you have to consider three lines at a time

#

Itertools is a good way to do that

old canopy
#

oh use a range you mean

#

hmm

crude ivy
#

Well not quite

#

!d more_itertools.windowed

lilac epochBOT
#

more_itertools.windowed(seq, n, fillvalue=None, step=1)```
Return a sliding window of width *n* over the given iterable.

```py
>>> all_windows = windowed([1, 2, 3, 4, 5], 3)
>>> list(all_windows)
[(1, 2, 3), (2, 3, 4), (3, 4, 5)]
```  When the window is larger than the iterable, *fillvalue* is used in place of missing values:

```py
>>> list(windowed([1, 2, 3], 4))
[(1, 2, 3, None)]
```...
crude ivy
#

This is third party but very useful

old canopy
#

oooh more_itertools

crude ivy
#

Windows of 2 are in std lib

#

(Kinda annoying that they didn’t make it generic)

#

But yeah - you can imagine using that over the input

#

To get a window of 3 lines at a time

old canopy
#

I think once I become more familiar with Python as a whole I'll be aware of everything I can use.

crude ivy
#

If you had no choice ofc

old canopy
#

Like I said it's been a long time.

crude ivy
#

Itertools is a must to get familiar with

old canopy
#

I like functools too.

#

Thanks for reviewing my code.

spiral stump
#

obligatory itertools.groupby shilling

#

i used that for this day in place of regex

crude ivy
#

I never use that one

#

I forget it exists and never really find a use case for it

spiral stump
old canopy
#

Oh that's neat @spiral stump

#

I am reading pydoc now

crude ivy
#

Warning - it be a little bit verbose

old canopy
#

I considered OOP

#

Java was the language the CS department at my university agreed on using (why) so my brain always goes to OOP first.

crude ivy
#

It’s debatable whether you call my code OOP

#

I didn’t really need a class there

old canopy
#

eh, there's a class :)

crude ivy
#

True

#

But the definition of OOP varies

#

Some people will say that any use of an object is OOP

#

In which case print('Hello World') is OOP in Python

#

Which - maybe it technically it is - but let’s face it, no one in reality considers that OOP

old canopy
#

oooh using timeit

#

good idea

crude ivy
#

For me there has to be composition and/or inheritance to be unambiguously OOP

#

Maybe I’ll call that uOOP

old canopy
#

That seems valid.

#

I like mixing things up because it makes things fun for me.

#

I like to have fun programming.

#

If I were doing this in Java I would've done this like you did here. My first inclination was to do both parts in one program.

#

I like your solution @crude ivy

#

I use sets in other problems.

crude ivy
old canopy
#

Your main function is really nice. I really like keeping main as direct and clear as possible. I should stop half-assing it haha.

crude ivy
#

I mean - it is AoC so that’s fine :)

#

And actually I have a lot more in main than one should have

#

It should probably be a single fn call

#

So that’s me also being lazy too

old canopy
#

I don't know there's a "should" for what belongs in main. You're right though, I bet you could probably just put all that in _str_ and print the object. I don't know if that's good practice though.

#

well okay Discord didn't format that the way I typed it

crude ivy
#

I think it should be declarative

#

So maybe not necessarily one function call

old canopy
#

sure

crude ivy
#

But just class/function calls

#

No imperative logic, I would say

old canopy
#

:O

crude ivy
#

That’s probably a good guide

old canopy
#

Would you say it's imperative that it's not imperative?

crude ivy
#

Haha 😂

#

Nice one :)

#

Also, did you check out my tests?

#

I have full test coverage

#

Actually I don’t have full test coverage

#

But I test a lot lol

old canopy
#

I saw a little bit of them. I like test-driven programming.

crude ivy
#

I’m not sure how purely TDD those were written

#

But by and large they were test first

old canopy
#

I have to run an errand but I'll be back later today.

#

It's been nice chatting with you.

#

Thanks again for reviewing my code.

crude ivy
#

No worries- enjoy your day! Hit me up either here or in #python-discussion whenever

storm leaf
#

would anyone mind taking a look at my code to help me determine why my part 1 is not giving the correct answer?

crude ivy
#

Although let me ask about the main bug people had in their scripts for this day

#

Does your script parse numbers at the very end of a line?

storm leaf
#

yes

crude ivy
#

Ok. Then feel free to send your code over :)

storm leaf
#

just to preface, i just started learning in the fall, so its probably going to be overcomplicated

crude ivy
#

That’s fine

storm leaf
#
symbols = {"!", "@", "#", "$", "%", "^", "&", "*", "/", "+", "=", "-"}
symbol_pos = []
num_pos = {}
parts = []


def check_up():
    if s[0] == n[0] and s[1] == n[1] + 1:  # checks up
        parts.append(int(num_pos[n]))
    elif s[1] == n[1] + 1 and n[0] in range(s[0] - len(num_pos[n]), s[0]):  # checks upper left
        parts.append(int(num_pos[n]))
    elif s[1] == n[1] + 1 and s[0] + 1 == n[0]:  # checks upper right
        parts.append(int(num_pos[n]))


def check_down():
    if s[0] == n[0] and s[1] == n[1] - 1:  # checks up
        parts.append(int(num_pos[n]))
    elif s[1] == n[1] - 1 and n[0] in range(s[0] - len(num_pos[n]), s[0]):  # checks bottom left
        parts.append(int(num_pos[n]))
    elif s[1] == n[1] - 1 and s[0] + 1 == n[0]:  # checks bottom right
        parts.append(int(num_pos[n]))


def check_left_right():
    if s[1] == n[1] and n[0] in range(s[0] - len(num_pos[n]), s[0]):  # checks left of symbol
        parts.append(int(num_pos[n]))
    elif s[1] == n[1] and s[0] == n[0] - 1:  # checks right of symbol
        parts.append(int(num_pos[n]))


with open('aoc3.txt', 'r') as file:
    # parses input and places the x, y coordinates of all symbols in the list symbol_pos
    # all numbers are placed in the dictionary num_pos with the x,y values as the key and the number itself as a value
    for y, line in enumerate(file.readlines()):
        tmp_string = ""
        for x, char in enumerate(line):
            if char in symbols:
                symbol_pos.append((x, y))
            if char.isnumeric():
                tmp_string += char
            if char in symbols or char == ".":
                if tmp_string.isnumeric():
                    num_pos[(x - len(tmp_string), y)] = tmp_string
                    tmp_string = ""
for n in num_pos:
    for s in symbol_pos:
        check_up()
        check_left_right()
        check_down()

print(sum(parts))
print(parts)```
crude ivy
#

!paste

storm leaf
#

ive tested against random input and everything seems to check out, but there must be errors happening somewhere

#

because im not getting the correct sum

crude ivy
#

did you test against the test input for day 3?

storm leaf
#

yes and it came back with the correct sum

#

ive also tested against random collections of lines from the actual input and haven't discovered the problem yet

crude ivy
#
with open('aoc3.txt', 'r') as file:
    # parses input and places the x, y coordinates of all symbols in the list symbol_pos
    # all numbers are placed in the dictionary num_pos with the x,y values as the key and the number itself as a value
    for y, line in enumerate(file.readlines()):
        tmp_string = ""
        for x, char in enumerate(line):
            if char in symbols:
                symbol_pos.append((x, y))
            if char.isnumeric():
                tmp_string += char
            if char in symbols or char == ".":
                if tmp_string.isnumeric():
                    num_pos[(x - len(tmp_string), y)] = tmp_string
                    tmp_string = ""

I'm not convinced that this code is finding numbers at the very end of a line

#

what part of this would save down a number if it were right at the very edge?

#

specifically, right at the very right edge

storm leaf
#

you are right, i thought those numbers were accounted for. I can fix that by checking at the first for loop to see if anyhting exists in the tmp_string, and add it to the dictionary

#
    for y, line in enumerate(file.readlines()):
        if tmp_string != "":
            num_pos[(x - len(tmp_string), y)] = tmp_string
        tmp_string = ""```
crude ivy
#

You can indeed - but also take care about numbers at the bottom right corner

storm leaf
#

bottom right of a symbol?

crude ivy
#

No - bottom right corner of the input

#

Think about it

storm leaf
#

theres no numbers at the bottom right, but i see what you mean

crude ivy
#

In which case you are lucky but you could have had one there

#

And created a bug that others would not easily detect even if they ran your script against their input

#

Would you like recommendations on a better way to parse out numbers?

storm leaf
#

sure

crude ivy
#

Regex parsing would be better

#

\d+ will match numbers

storm leaf
#

is regex an included module?

crude ivy
#

re is

#

And re.finditer will even give you the locations in the str for each match

storm leaf
#

yea that much better than what im doing lol

crude ivy
#

Getting good at regex is a useful skill

#

Especially for AOC parsing

storm leaf
#

these puzzles have taught me more about parsing than an entire semester in university lol

#

granted we didnt spend much time on it

crude ivy
#

Oh trust me there’s way more about parsing to learn

storm leaf
#

I can only imagine

crude ivy
#

None of the puzzles are particularly hard to parse

#

Certain grammars require tools that even regex cannot parse properly

storm leaf
#

for me they are lol, but yea I imagine it gets way more complicated

crude ivy
#

Also with AOC the data is well formed too

#

You don’t have to worry about crap data

#

And what you do if you have that

storm leaf
#

im doing c++ this semester, I don't even want to think about doing these puzzles in that language

#

although once I feel more comfortable parsing in python im sure ill give it a go

#

Well I appreciate your input on this, it's very helpful. I will look into regex and start learning.

Unfortunately I am still not getting the right sum, so I'm gonna have to tear this code down and decide if I want to start over.

crude ivy
crude ivy
storm leaf
#

Yea, im getting the right sum on the test input

crude ivy
#

One thing you could consider is using someone else’s input to debug your own

#

Sorry

storm leaf
#

so there must be some condition where a number isnt getting added to the dictionary, or potentially a number is not being parsed properly

crude ivy
#

Someone else’s code, I mean

storm leaf
#

Yea, I can compare outputs for specific input and look for differences. That could lead me to whats going wrong in my code.

crude ivy
#

Exactly

#

And debugging is an invaluable skill to learn

#

Here’s a perfect opportunity

storm leaf
#

Indeed. Thanks again, im gonna take a break and come back to this later. I'll update you once I have it fixed.

crude ivy
#

Best of luck!

storm leaf
#

its fixed, can't say what it was exactly but I think something may have been wrong with the input file. on to part 2

storm leaf
#

Digging deeper I found the causes.

  1. Numbers at the end of lines were not being parsed properly. I fixed that but my fix created the second issue.
  2. When numbers were parsed their "grid" coordinates were also taken, but my fix caused numbers that were being recorded at the end of the line to have their vertical coordinate increased by 1, so when they were later being checked for adjacency, they were being left out in some cases.
#

I was able to correct it by implementing an appropriate new solution for problem 1

crude ivy
storm leaf
#
    for y, line in enumerate(file.readlines()):
        tmp_string = ""
        for x, char in enumerate(line):
            if char in symbols:
                symbol_pos.append((x, y))
            if char.isnumeric():
                tmp_string += char
            if char in symbols or char == ".":
                if tmp_string.isnumeric():
                    num_pos[(x - len(tmp_string), y)] = tmp_string
                    tmp_string = ""
            if char == line[-1]:
                if tmp_string != "":
                    num_pos[(x - len(tmp_string), y)] = tmp_string```
#

I moved the fix to a new conditional statement at the bottom of the loop

#

basically just checking if the character being parsed was the last of the string

#

im now realizing that fix could also have unintended consequences

crude ivy
storm leaf
#

exactly, i could compare the "x" coord of that character against the lenght of the string

#

that would work properly in all cases i think

crude ivy
#

I think so

#

But yeah manually iterating through the string like this is dodgy

#

Lots of people made the errors you made

storm leaf
#
            if x == len(line) - 1 and tmp_string != "":
                num_pos[(x - len(tmp_string), y)] = tmp_string```
crude ivy