Skip Navigation

The official awful.systems Advent of Code 2023 thread

Rules: no spoilers.

The other rules are made up as we go along.

Share code by link to a forge, home page, pastebin (Eric Wastl has one here) or code section in a comment.

121
121 comments
  • Just when I think I'm out, awful systems pulls me right back in.

    C++, Horrifying C++ for day 1b check it out ;)

    Day 1: https://www.animeprincess.net/blog/?p=55

    One day is enough for me though, writing deliberately off-the-wall code is surprisingly mentally taxing, and normally I make sure to never program outside of work.

  • day 1

    part 1

    perl
    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    use 5.010;
    
    my $total = 0;
    
    for my $line (<>) {
        my @nums = ($line =~ /\d/g);
        $total += $nums[0] * 10 + $nums[-1];
    }
    
    say $total;
    

    part 2

    perl
    #!/usr/bin/env perl
    
    use strict;
    use warnings;
    use v5.010;
    
    my %nums = (one => 1, two => 2, three => 3, four => 4, five => 5, six => 6, seven => 7, eight => 8, nine => 9);
    $nums{$_} = $_ for 1..9;
    
    my $regex = join "|", keys %nums;
    
    my $total = 0;
    
    for my $line (<>) {
        $line =~ /($regex)/;
        my $first_num = $nums{$1};
    
        my $window = 1;
        my $sub = substr $line, -1;
        while ($sub !~ /($regex)/) {
            $window ++;
            $sub = substr $line, -$window;
        }
    
        $sub =~ /($regex)/;
        my $second_num = $nums{$1};
    
        $total += $first_num * 10 + $second_num;
    }
    
    say $total;
    

    Part 2 gave me a surprising amount of trouble. I resolved it by looking at longer and longer substrings from the end of the line in order to find the very last word even if it overlapped, which you can't do with normal regex split. I doubt this is the most efficient possible solution.

    Also Lemmy is eating my < characters inside code blocks, which seems wrong. Pretend the "&lt;>" part says "<>", lol

  • Day 14: Parabolic Reflector Dish

    I only managed part 1 today. My enthusiasm for index fiddling is waning rapidly.

  • Day 4: Scratchcards

    Late to the party and never done advents before, I liked how this problem reminded me that tree traversal is thing, almost as much as I don't that so much of my career involves powershell now.

    I'm putting everything up at https://github.com/SpaceAntelope/advent-of-code-2023 except the input files.

    Using command abbreviations like % and ? to keep the horizontal length friendly to lemmy post areas, they are expanded in git.

    Part 2 in Powershell
    function calculate([string]$data) {
      # code for parsing data and calculating matches from pt1 here, check the github link if you like banal regexps
      # returns objects with the relevant fields being the card index and the match count
    }
    
    function calculateAccumulatedCards($data) {
        $cards = calculate $data # do pt1 calculations
    
        $cards 
        | ? MatchCount -gt 0 # otherwise the losing card becomes its own child and the search cycles to overflow
        | % { 
            $children = ($_.Index + 1) .. ($_.Index + $_.MatchCount)  # range of numbers corresponding to indices of cards won
            | % { $cards[$_ - 1] } # map to the actual cards
            | ? { $null -ne $_ }  # filter out overflow when index exceeds input length
    
            $_ | Add-Member -NotePropertyName Children -NotePropertyValue $children # add cards gained as children property
        }
    
        # do depth first search on every card and its branching children while counting every node
        # the recursive function is inlined in the foreach block because it's simpler than referencing it 
        # from outside the parallel scope
        $cards | % -Parallel {
            function traverse($card) {
                $script:count++        
                foreach ($c in $card.Children) { traverse($c) }
            }
            
            $script:count = 0 # script: means it's basically globally scoped
            traverse $_ 
            $script:count # pass node count to pipeline     
        } 
        | measure -sum    
        | % sum
    }
    
  • Day 2: Cube Conundrum

    https://adventofcode.com/2023/day/2

    Parsing the puzzle (both instructions and input) was the hardest this time for me.

  • Have been mostly using jq for fun.

    Day 1

    Part 1
    #!/usr/bin/env jq -n -R -f
    
    # Get and reduce every "pretty" line
    reduce inputs as $line (
      0;
      # Add extracted number
      . + ( $line / "" | [ .[] | tonumber? ] | [first * 10 , last] | add )
    )
    

    First part was easy, and very suited to jq

    Part 2
    #!/usr/bin/env jq -n -R -f
    
    # Define string to num value map
    {
      "one":   1,  "1": 1,
      "two":   2,  "2": 2,
      "three": 3,  "3": 3,
      "four":  4,  "4": 4,
      "five":  5,  "5": 5,
      "six":   6,  "6": 6,
      "seven": 7,  "7": 7,
      "eight": 8,  "8": 8,
      "nine":  9,  "9": 9
    } as $to_num |
    
    # Get and reduce every "pretty" line
    reduce inputs as $line (
      0;
      . + (
        $line |
        # Try two capture two numbers
        capture("(^.*?(?(one|two|three|four|five|six|seven|eight|nine|[1-9])).*(?(one|two|three|four|five|six|seven|eight|nine|[1-9])).*?$)?") |
        # If no capture, get one number twice
        if .f == "" then $line | capture("^.*?(?(one|two|three|four|five|six|seven|eight|nine|[1-9]))") | .l = .f else . end |
        # Add extracted number
        $to_num[.f] * 10 + $to_num[.l]
      )
    )
    

    Second part was harder than expected, i had to resort to regex.

    Day 2

    Part 1
    #!/usr/bin/env jq -n -R -f
    
    # For each game: Is 12 red cubes, 13 green cubes, and 14 blue cubes possible ?
    # Line Format =
    # Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
    [
      # Splitting input game id and content
      inputs / ": " |
      # Saving id
      (.[0] / " " | .[1] | tonumber ) as $id |
      # Parsing game
      .[1] / "; " | [
        .[] / ", " | [ .[] / " " | {(.[1]): .[0] | tonumber} ] | add |
        # Is given sample possible ?
        .red &lt;= 12 and .green &lt;= 13 and .blue &lt;= 14
      ] |
      # If all samples possible, return id, else 0
      if all then $id else 0 end
    ] |
    
    # Return sum of all possible game ids
    add
    

    Not too much trickery in this example.

    Part 2
    #!/usr/bin/env jq -n -R -f
    
    # Line Format =
    # Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
    [
      # Splitting input game id and content
      inputs / ": " |
      # Parsing game
      .[1] / "; " |
        [ .[] / ", " | [ .[] / " " | {(.[1]): .[0] | tonumber} ] | add ] |
        # Getting minimum required mumber for each color,
        # and computing the power
        {
          r: ([.[].red]   | max),
          g: ([.[].green] | max),
          b: ([.[].blue]  | max)
        } | .r * .g * .b
    ] |
    
    # Return sum of all powers
    add
    

    Satisifyingly straightfoward edit form part one.

    Day 3

    Part 1
    #!/usr/bin/env jq -n -R -f
    
    # Getting input with padding, and padded width
    [ "." + inputs + "." ] as $inputs | ( $inputs[0] | length ) as $w |
    
    # Working with flattened string, convert all symbols to '#'
    [
      ([range($w) | "."]|join("")), # Padding
      $inputs[],
      ([range($w) | "."]|join(""))  # Padding
    ] | join("") | gsub("[^0-9.]";"#") as $inputs |
    
    reduce (
      # Get all indices for symbols, in box pattern around symbols
      $inputs | indices("#")[] |
      . - $w -1  , . - $w , . - $w + 1 ,
      . - 1      , empty  , . + 1      ,
      . + $w - 1 , . + $w , . + $w + 1
    ) as $i (
      # Numbers containes bounding indices,
      # of numbers bordering symbols
      {numbers: []};
    
      # Test if current index isn't included in any found number
      def new_number($i): [ .numbers[] | .[0] &lt;= $i and $i &lt;= .[1] ] | any | not ;
      # Make "number" as bounding indices, by extending left and right
      def make_number($i):
        {a: $i, b: ($i+1 )}
          | until( $inputs[.a:.b] | test("^[^0-9]"); .a -= 1 )
          | until( $inputs[.a:.b] | test("[^0-9]$"); .b += 1 )
          | [ .a +1 , .b -1 ]
      ;
    
      # Add numbers if bordering symbol and new
      if ($inputs[$i:$i+1] | test("[0-9]")) and new_number($i) then .numbers += [ make_number($i) ] else . end
    ) |
    
    # Output sum of all found numbers
    [ .numbers[] | $inputs[.[0]:.[1]] | tonumber ] | add
    

    Took More time than i expected, glad i had the idea early to search by the indices of the symbols and not the digits. Not super well suited to jq, unless I'm missing a better solution.

    Part 2
    #!/usr/bin/env jq -n -R -f
    
    # Getting input with padding, and padded width
    [ "." + inputs + "." ] as $inputs | ( $inputs[0] | length ) as $w |
    
    # Working with flattened string, only keep gear '*' symbols
    [
      ([range($w) | "."]|join("")), # Padding
      $inputs[],
      ([range($w) | "."]|join(""))  # Padding
    ] | join("") | gsub("[^0-9*]";".") as $inputs |
    
    # Iterate over index positions of all gears
    reduce ($inputs | indices("*")[]) as $i (
      0;
      # Re-use part-1 functions
      def new_number($i):
        [ .numbers[] | .[0] &lt;= $i and $i &lt;= .[1] ] | any | not
      ;
      def make_number($i):
        {a: $i, b: ($i+1 )}
          | until( $inputs[.a:.b] | test("^[^0-9]"); .a -= 1 )
          | until( $inputs[.a:.b] | test("[^0-9]$"); .b += 1 )
          | [ .a +1 , .b -1 ]
      ;
      # Reset and add numbers for each "box" ids
      def add_numbers($box_idx):
        reduce $box_idx[] as $i ({numbers:[]};
          if ($inputs[$i:$i+1] | test("[0-9]")) and new_number($i) then
            .numbers += [ make_number($i) ]
          else
            .
          end
        )
      ;
      add_numbers([
        $i - $w -1 , $i - $w , $i -$w + 1 ,
        $i - 1     , empty   , $i + 1     ,
        $i + $w - 1, $i + $w , $i + $w + 1
      ]).numbers as $numbers |
    
      if $numbers | length == 2 then
        # Add product if exactly two bordering numbers
        . += ( $numbers | map($inputs[.[0]:.[1]]|tonumber) | .[0] * .[1] )
      else
        .
      end
    )
    

    Not too far of an edit from part one.

  • Day 11: Cosmic Expansion

    https://adventofcode.com/2023/day/11

    discussion

    After yesterday' fiddle-fest we are back with a straight-forward puzzle. Today we get the return of Manhattan distance, an AoC fav, but this time not spelled out to fool the crafty LLMs.

    I made the initial decision not to "move" the galaxies in the initial map, but instead to store an offset that was increased whenever an empty row or column preceding the object was detected. This turned out to make part 2 really easy once I figured out the off-by-one error.

  • Let's do this.

    Day 1: Trebuchet?!

    https://adventofcode.com/2023/day/1

  • Back to a more straightfoward day, do they make them harder on the weekends?

    Day 4 Scratchcards

    Part 1
    #!/usr/bin/env jq -n -R -f
    [
      inputs
      # Split winning numbers | card
      | split(" | ")
      # Get numbers, remove game id
      | .[] |= [ match("\\d+"; "g").string | tonumber ] | .[0] |= .[1:]
      # Get score for each line
      | .[1] - (.[1] - .[0]) | length | select(. > 0) | pow(2; . - 1)
    ]
    
    # Output total score sum
    | add
    

    Very suited to JQ, extra trick learned using: [ match("\\d+"; "g").string | tonumber ] as a parse all ints in line.

    Part 2
    #!/usr/bin/env jq -n -R -f
    [
      inputs
      # Split winning numbers | card
      | split(" | ")
      # Get numbers, remove game id
      | .[] |= [ match("\\d+"; "g").string | tonumber ] | .[0] |= .[1:]
      # Set number of cards to 1, and further cards count
      | .[1] - (.[1] - .[0]) | [ 1, length ]
    ]
    
    | { cards: ., i: 0, l: length } | until (.i == .l;
      # Get number for current card
      .cards[.i][0] as $num
      # Increase range of futher cards, by current number
      | .cards[.i + range(.cards[.i][1]) + 1 ][0] += $num
      | .i += 1
    )
    
    # Output total sum of cards
    | [ .cards[][0] ] | add
    

    Not too much of an edit compared to part one, being able to easily do operations on range of indices is convenient.

  • Happy Holidays everyone. I’ve decided I am going to take a break from aoc to properly rest and recover from my mystery illness. Perhaps I will attempt solves again in the new year.

  • Day 5: If You Give A Seed A Fertilizer

    https://adventofcode.com/2023/day/5

    Leaderboard completion time: 26m37s, so it's the toughest problem this year so far.

  • Day 6: Wait For It

    https://adventofcode.com/2023/day/6

    Alternate spoiler name - for part 2

    Do you remember highschool algebra? Can you (or your compiler) remember highschool algebra fast enough to beat out a naïve implementation?

  • I have come up with a more horrifying way to solve Day 1 Part 1 involving C++ implementation defined behavior, but it requires launching two subprocesses for every test case so I'm not sure I have the motivation right now.

    Proof of concept: https://www.animeprincess.net/blog/?p=60

  • Day 7: Camel Cards

    https://adventofcode.com/2023/day/7

    So far, my favorite puzzle. Challenging but fair. Also plays to Perl's strengths.

    Leaderboard completion time: 16 minutes flat, so not a pushover.

  • Day 8: Haunted Wasteland

    https://adventofcode.com/2023/day/8

    Not so easy at least for part two.

    spoiler

    Do you remember high school math, like lowest common multiple, part 2 electric boogaloo.

  • Day 9: Mirage Maintenance

    My solution: https://github.com/gustafe/aoc2023/blob/main/d09-Mirage-Maintenance.pl

    discussion

    What can I say. Shockingly simple.

    I just literally followed the instructions, and got a solution in 20ms. This despite literally creating each intermediate array yet only using the ends. I'm sure I used way to much memory but you know? I'm using a $5/mo VPS for everything and unless I'm barking totally up the wrong tree I've never exceeded its memory limits.

    On the subreddit I see people discussing recursion and "dynamic programming" (which is an empty signifier imho) but I really don't see the need, unless you wanna be "elegant"

  • Day 20: Pulse Propagation

    It feels weird to kick one of these threads off, but hey, here we go.

    Code as always: https://github.com/Fluxward/aoc2023/blob/main/20.dart

    a,b

    A

    So following from yesterday where I was punished by not going full OO, I decided, hey, this is a problem in which I can do some OOP, so I did. This took very long to do but I regret nothing. If you look at my code, feel free to click your tongue and shake your head at every opportunity I missed to use a design pattern.

    Anyway, after a slight snafu with misunderstanding the FF logic and not spotting that some modules can be dummy modules, it all just worked, and I got my answer.

    B

    This was a bit of a headscratcher, but the answer was surprisingly simple.

    First, the answer. Here's how to do it:

    • Look for the "rx" module in your input.
    • If the module that outputs to rx is a conjunction, keep track of how many button presses it takes for each input of the conjunction to change. The answer is the LCM of all those numbers.
    • If the module is a FF, you can also work backwards to get it, but this was not my input so I didn't need to try this.

    Getting here was a bit weird. I hoped that I could just run the code from A and spit out the answer when rx went low, but as of time of writing I've been running it now on a separate machine for about an hour and still no result.

    My next instinct was to simply work it out from pen and paper. I thought it might be possible (it probably is) but decided to at least experimentally see if the states of the modules connected to rx were cyclic first. I did, and that was enough for me to get to the answer.

    My answer was about 230 trillion BPs, which, extrapolating on how long execution is taking on my other machine, might take just under 137 years to calculate naively. Fun!

  • 21 Step Counter

    Starting this thread having only solved a.

    A

    Pretty straightforward. Could probably be done in a few lines with the right syntactic sugar.

    B

    This is some game of life thing that I've never implemented or seen an implementation of, so I am altogether lost.

    My current code (https://github.com/Fluxward/aoc2023/blob/main/21.dart) has a memoisation based approach but my current ailments are preventing me from really thinking about this properly so I am bowing out until I have the wherewithal.

  • Day 12: Hot springs

    https://adventofcode.com/2023/day/12

    • Leaderboard completion time: 22:57
    • Personal completion time: ahahahahahahaha (at least i had fun)

    Where a curse the fact I decided to use JQ and not a "real" programming language.

    spoiler

    Had to resort to memoization, but sadly JQ isn't mega well suited to that. I had to refactor my part 1 function, to make including the "state" at every function call possible. I wish it were as easy as a @cache decorator, but i guess this way i had fun (for an arbitrary definition of "fun")

    Further cleaned up version: https://github.com/zogwarg/advent-of-code/blob/main/2023/jq/12-b.jq

    Also lost a fair amount of time not not noticing that the sequence should be joined with "?" not with "". (that'll teach me to always run on the example before the full input, when execution time is super long).

    Execution time: 17m10s (without memoization a single row was taking multiple minutes, and there's 1000 rows ^^...)

    EDIT: see massive improvement by running in parallel in reply.

  • Day 17: Clumsy Crucible

    Intimidating at first, so I went shopping for christmas presents first, which engaged my brain juices.

    [Language: jq]

    https://github.com/zogwarg/advent-of-code/blob/main/2023/jq/17-b.jq

    In hindsight this is is searching for the shortest path in a graph, making sure that each square is actually two nodes, for when approached vertically or horizontally. My shcool days are distant now, and i wonder how far from optimal my implementation is ^^.

    Part two was only a small edit from part one for me, and the code actually ran faster! from ~45s -> ~20s.

  • Day 18: Lavaduct Lagoon

    [Language: jq]

    https://github.com/zogwarg/advent-of-code/blob/main/2023/jq/18-b.jq

    Satisfyingly short (in lines, not in time writing) some of the longer part is hexadecimal parsing, that doesn't come natively in JQ, I started doing polygon math from part 1, and what took me the longest was properly handling the area contributed by the perimeter. (I toyed with trying very annoying things like computing the outmost vertex at each turn, which is complicated by the fact that you don't initially know which way the digger is turning, and needing previous and next point to disambiguate).

    #!/usr/bin/env jq -n -R -f
    
    reduce (
      # Produce stream of the vertices, for the position of the center
      foreach (
        # From hexadecimal representation
        # Get inputs as stream of directions = ["R", 5]
        inputs | scan("#(.+)\\)") | .[0] / ""
        | map(
            if tonumber? // false then tonumber
            else {"a":10,"b":11,"c":12,"d":13,"e":14,"f":15}[.] end
          )
        | [["R","D","L","U"][.[-1]], .[:-1]]
        | .[1] |= (
          # Convert base-16 array to numeric value.
          .[0] * pow(16;4) +
          .[1] * pow(16;3) +
          .[2] * pow(16;2) +
          .[3] * 16 +
          .[4]
        )
      ) as $dir ([0,0];
        if   $dir[0] == "R" then .[0] += $dir[1]
        elif $dir[0] == "D" then .[1] += $dir[1]
        elif $dir[0] == "L" then .[0] -= $dir[1]
        elif $dir[0] == "U" then .[1] -= $dir[1]
        end
      )
      # Add up total area enclosed by path of center
      # And up the are of the perimeter, perimeter * 1/2 + 1
    ) as [$x, $y] ( #
      {prev: [0,0], area: 0, perimeter_area: 1  };
    
      # Adds positve rectangles
      # Removes negative rectangles
      .area += ( $x - .prev[0] ) * $y |
    
      # Either Δx or Δy is 0, so this is safe
      .perimeter_area += (($x - .prev[0]) + ($y - .prev[1]) | abs) / 2 |
    
      # Keep current position for next vertex
      .prev = [$x, $y]
    )
    
    # Output total area
    | ( .area | abs ) + .perimeter_area
    

    Day 19: Aplenty

    [Language: jq]

    https://github.com/zogwarg/advent-of-code/blob/main/2023/jq/19-b.jq

    Satisfyingly very well suited to JQ once you are used to the stream, foreach(init; mod; extract) and recurse(exp) [where every output item of exp as a stream is fed back into recurse] operators. It's a different way of coding but has a certain elegance IMO. This was actually quick to implement, along with re-using the treating a range as a primitive approach of the seeds-to-soil day.

    #!/usr/bin/env jq -n -sR -f
    
    inputs / "\n\n"
    
    # Parse rules
    | .[0] / "\n"
    | .[] |= (
      scan("(.+){(.+)}")
      | .[1] |= (. / ",")
      | .[1][] |= capture("^((?<reg>.)(?<op>[^\\d]+)(?<num>\\d+):)?(?<to>[a-zA-Z]+)$")
      | ( .[1][].num | strings ) |= tonumber
      | {key: .[0], value: (.[1]) }
    ) | from_entries as $rules |
    
    # Split part ranges into new ranges
    def split_parts($part; $rule_seq):
      # For each rule in the sequence
      foreach $rule_seq[] as $r (
        # INIT = full range
        {f:$part};
    
        # OPERATE =
        # Adjust parts being sent forward to next rule
        if $r.reg == null then
          .out = [ .f , $r.to ]
        elif $r.op == "<" and .f[$r.reg][0] < $r.num then
          ([ .f[$r.reg][1], $r.num - 1] | min ) as $split |
          .out = [(.f | .[$r.reg][1] |= $split ), $r.to ] |
          .f[$r.reg][0] |= ($split + 1)
        elif $r.op == ">" and .f[$r.reg][1] > $r.num then
          ([ .f[$r.reg][0], $r.num + 1] | max ) as $split |
          .out = [(.f | .[$r.reg][0] |= $split), $r.to ]  |
          .f[$r.reg][1] |= ($split - 1)
        end;
    
        # EXTRACT = parts sent to other nodes
        # for recursion call
        .out | select(all(.[0][]; .[0] < .[1]))
      )
    ;
    
    [ # Start with full range of possible sings in input = "in"
      [ {x:[1,4000],m:[1,4000],a:[1,4000],s:[1,4000]} , "in" ] |
    
      # Recusively split musical parts, into new ranges objects
      recurse(
        if .[1] == "R" or .[1] == "A" then
          # Stop recursion if "Rejected" or "Accepted"
          empty
        else
          # Recursively split
          split_parts(.[0];$rules[.[1]])
        end
        # Keep only part ranges in "Accepted" state
      ) | select(.[1] == "A") | .[0]
    
      # Total number if parts in each object is the product of the ranges
      | ( 1 + .x[1] - .x[0] ) *
        ( 1 + .m[1] - .m[0] ) *
        ( 1 + .a[1] - .a[0] ) *
        ( 1 + .s[1] - .s[0] )
      # Sum total number of possibly accepted musical parts
    ] | add
    

    EDIT: Less-thans and greater-thans replaced by fullwidth version, because lemmy is a hungry little goblin.

121 comments