Advent of Code 2023

I'm kinda wary of code golf competitions because they tend to be used in bad faith by folks who want to criticize the language. But at the same time they're quite fun. This is as good as I could do for day 1 part 2 but it's a long way off the leaderboard. Maybe someone can take it and do better (I didn't submit it).

let w=["one","two","three","four","five","six","seven","eight","nine"]
extension Collection<Character>{func f(_ w:[Self])->Int{indices.compactMap{i in self[i].wholeNumberValue ?? zip(1...,w).first{self[i...].starts(with: $1)}?.0}.first!}}
print(sequence(state:()){_ in readLine()}.reduce(0){$0+$1.f(w)*10+$1.reversed().f(w.map{$0.reversed()})})

(interestingly the counting of tokens as 1 works in swift's slightly verbose std lib naming's favor, but I feel like naming your functions and variables longer than a letter in a code golf exercise goes against the grain :)

THANK YOU for sharing that. I came back here to reread mayoff's so I could finally really learn how to memoize, but your approach resonated so nicely with the way I was thinking about it that I decided to looking into DFA's instead!

Here's my homage to your code. (It's logically the same, I've thrown in a quite bit of cruft for my comprehension.)

I really appreciated seeing a different take. I owe you my part 2 star!

1 Like

Worst, IMO, they emphasise an attribute that's often undesirable in real-world applications. And while the participants might think they're compartmentalising, they probably aren't completely - that 'itch' to be excessively 'clever' will bleed into their work.

I'd be much more interested in a variant of this competition which ranks implementations by their conceptual complexity or 'readability' or some similar, useful and positive attribute. Alas those qualities are harder to quantify than trivial things like number of characters or tokens.

1 Like

How 'bout a 'happiness' score? As in: how happy would you be if you inherited this code (from someone who left) at work? Do you feel confident that you could change it? Does it learn you anything new?

Day 18 part 2 hints are full of references to the Shoelace Formula this morning. For whatever reason, I like to try to solve these without googling for algorithms, and I'd never heard of the shoelace formula, so once again, it seems like maybe I solved this in a weird way.

Imagine a small series of instructions like R 100, D 100, L 20, U 30, L 80, U 70. Figure out coordinates for each of the points of this polygon:
coords

Notice that one way you can calculate the interior area is by subdividing this polygon into rectangular areas. Because all of our lines are always going to be at 90 degree angles, this is always going to work. One way to subdivide the interior is made up of the rectangles colored here:
rects

So here's the algorithm:

  1. Figure out the points of all the vertices of your instructions

  2. Find all unique Xs and Ys from these points and sort them (xs=(0,80,100) ys=(0,70,100)).

  3. Map these values to new X' and Y' where each coordinate is the index in the xs or ys array *2 so that all even X' and Y' are our interesting X and Y values, and all odd X' and Y' are the spaces in between.

  4. Draw the grid using X' and Y' similar to how you presumably did in part 1 of the problem, which produces a totally reasonable small grid. Notice how the interior 5 .s correspond to the 5 colored rectangles:
    grid

  5. Flood fill as you presumably did in part 1.

  6. Walk your new smaller grid summing areas. If you have an X' coordinate n the width in that index is either 1 if n is even, or if it is odd the width is xs[n/2+1] - xs[n/2] - 1 (e.g. the space in between the interesting coordinate values). Similar for height and Y' and ys. The five interior .s in the grid here have areas of 79x69, 1x69, 19x69, 19x1, 19x29.

In this case I'm sure this is quite a bit slower than the Shoelace Formula way (both to write and in runtime), but I thought the novelty might be interesting.

1 Like

It’s always nice to see a novel approach. Also, this subdivision algorithm might be usable for other use cases as well.

1 Like

It's a variation on the same theme. @cache makes some choices: it uses a thread safe cache, which means you don't need to worry about the sendability concerns I mention, but at the same time, you pay a price if you don't need thread safety. You might also want to use a function body macro in Swift instead of a higher-order function, as my approach requires that you name your function _possibilities but then you call the memoized possibilities function.

1 Like

Thanks!

If I were brave, I would try implementing this as a test case ... but that might have to wait until next year. It feels like a natural extension that might be handy in general once function body macros are around.

Hi everyone,

I'm working on day 19, where I'm using Regexes to parse the input. However, I'm having trouble with the Regex Literal syntax in Xcode / Swift 5.9.

This Regex(1) works fine:
let regex = try! Regex("foo")

But I wanted to use the Regex literal syntax:
let regex = /foo/

However, this throws a compiler error:

(1) these are not the actual regular expressions I'm using for the puzzle, but they are the simplest example I can give to demonstrate the behavior.

Did the Regex literal syntax change?

Hi Maartene, you need to enable

BareSlashRegexLiterals

If you are making use of the AoC template, add it to package.swift

3 Likes

Ah OK. Thank you.

Didn't know this was an 'opt in' feature. Especially since this feature was introduced in Swift 5.7.

Confusingly, it's enabled by default in Xcode but not in SPM or the Swift compiler directly.

2 Likes

Yes, that is indeed very confusing. But good to know.

Nope, nope, nope... needs a little more work first!

I'm just getting started... Day 1 ok... Day 2.... I got this... Day 3... what are you asking me to do? :joy:

(edit: marked the code as spoiler, just in case)

OK, I'm stuck in day 24 - part 1. Yes, the one that's supposed to be easy.

My function to check for intersecting hailstones works for the example input, however, not for the actual input. This is how I determine the intersection point:

struct HailStone {
   let position: DVector3
   let velocity: DVector3

    // initializers ...

   func intersectsXY(_ other: HailStone, searchArea: ClosedRange<Float80>) -> DVector2D? {
        
        // check for hailstones that are on the same line
        // paralel lines on same segment have a dot product of -1 or 1
        let dot_velocities = DVector2D.dot(velocity.xy.normalized, other.velocity.xy.normalized)
        if dot_velocities == -1 || dot_velocities == 1 {
            // same direction, but are they the same?
            let line_from_one_to_the_other = (other.position.xy - position.xy).normalized
            let dot_lineVelocity = DVector2D.dot(line_from_one_to_the_other, velocity.xy.normalized)
            // again, the line from this hailstones position to the other hailstones position should be either the velocity or the inverse of the velocity. I.e. dot product should be -1 or 1.
            if dot_lineVelocity == -1 || dot_lineVelocity == 1 {
                // as the lines are the same, it doesn't really matter which position to return.
                return position.xy
            }
        }
        
        // Calculate intersection using function on https://en.wikipedia.org/wiki/Line–line_intersection
        // Points on the first line segment (this hailstone)
        let p1 = position.xy
        let p2 = position.xy + velocity.xy * 1_000_000
        
        // Points on the second line segment (other hailstone)
        let p3 = other.position.xy
        let p4 = other.position.xy + other.velocity.xy * 1_000_000
    
        // Determinants of the line segments
        let det_l1 = DVector2D.det(p1, p2)
        let det_l2 = DVector2D.det(p3, p4)
        
        let den = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x)
        
        // X coordinate
        let px_nom = det_l1 * (p3.x - p4.x) - (p1.x - p2.x) * det_l2
        let px = px_nom / den
        
        // Y coordinate
        let py_nom = det_l1 * (p3.y - p4.y) - (p1.y - p2.y) * det_l2
        let py = py_nom / den
        
        let p_intersect = DVector2D(x: px, y: py)
        
        // checks
        // inside search area?
        guard searchArea.contains(px) && searchArea.contains(py) else {
            print("Found intersection (\(px),\(py)), but outside of search range")
            return nil
        }
        
        // in the future for this hailstone? (dot product > 0)
        let line1 = (p_intersect - position.xy).normalized
        guard DVector2D.dot(line1, velocity.xy.normalized) >= 0 else {
            print("Found intersection (\(px),\(py)), but it's in the past for this hailstone")
            return nil
        }
        
        // in the future for the other hailstone? (dot product > 0)
        let line2 = (p_intersect - other.position.xy).normalized
        guard DVector2D.dot(line2, velocity.xy.normalized) >= 0 else {
            print("Found intersection (\(px),\(py)), but it's in the past for the other hailstone")
            return nil
        }
        
        return DVector2D(x: px, y: py)        
    }
}

DVector3 and DVector2 are vector structs based on Float80s (that should be precise enough right?). They have some utility functions to add/substract/multiply/..., normalize, calculate dot product, determinant.

This code should be able to determine the intersections for:

  • hailstones on different velocities and different start positions (if the intersection is in the search space)
  • hailstones that are on the same line - both when going in the same directions as well as in opposite directions.

For the example, this code returns the correct conclusions. However, for the actual input, it returns too few intersections. So I guess I'm missing a case to check for.

I do hope I'm just missing a case. Not that floating point precision is causing issues because that's not something I could easily solve.

Any ideas?

(full code here: AoC2023/day24 at main · maartene/AoC2023 · GitHub )

 let line2 = (p_intersect - other.position.xy).normalized
-guard DVector2D.dot(line2, velocity.xy.normalized) >= 0 else {
+guard DVector2D.dot(line2, other.velocity.xy.normalized) >= 0 else {

Seems a typo, but not sure it will give you the answer still.

1 Like

Thanks @paiv, this would have taken me hours to find myself! :sweat_smile:

Wow, so that was something else...

I used SageMath on CoCalc.com to solve the 9 equations for part 2.

But, I'm interested: did you find a solution using (pure) Swift to solve part 2 of day 24?

If anyone is curious about cross-language comparisons, I did a few days in both Swift and Haskell, and I was a bit surprised that both versions are usually equally fast (when optimized, the unoptimized ones are all over).

Not sure if this is a "duh! obviously" or a "hmm" fact for you; for me it was more of the latter. I wasn't really expecting one way or the other when I did the same day in both languages (I wasn't even doing this to compare performance, but rather to compare the code structure), but I was a bit surprised they are equal-ish in performance. These are very different languages with different histories and different foci, so I think it speaks to the strength of both GHC and swiftc that they're able to optimize code written in (on the surface level) different paradigms to run the same fast way when actually executed on the machine.

e.g. this is the Swift solution for day 17, while this is the Haskell one. The Swift solution is slightly longer, at 109 lines, compared to 62 lines of Haskell, but the Swift solution is arguably much more readable too especially for people who're not versed in either of the two languages (The Haskell solution has a beauty that takes knowing Haskell to appreciate, otherwise it seems like Perl-ish line noise).

Both solutions take 0.5 seconds on the full input for both parts.

2 Likes