Many people on reddit reporting they were hit by one edge case that's not covered in the examples. But my implementation passed these edge cases too. I was hit by another edge case. So there are at least two edge-cases (which are in the actual data) that aren't covered in the examples or the description.
It was a little frustrating that the main edge case that caught everyone didn't blow up my implementation, and going row by row of the 1000 line input it wasn't immediately obvious where it was going wrong (I'm not sure how far down I would've had to go to find the first time it hits this edge case, I just ended up checking the reddit for hints).
I really do think things like this should at least be hinted in the text to save a little frustration; I'm not trying to be the best on the leaderboard, I'm just doing these in the morning before work for a little fun.
I only found my edge case (and dumb implementation mistake, I guess) by forking a "working solution" from someone else, outputting their solution for each line, then diffing that with a similar output from mine. It took me a lot of debugging and troubleshooting.
Once I diffed it against a working output it was clear and solved within minutes.
Warning: post contains spoilers, HN doesn't support spoiler text so continue reading at your peril.
These edge cases are triggered by fundamentally approaching the problem incorrectly.
In some ways it's excellent to bring that up early in a way that's relatively easy to debug and diagnose.
Like many others I started with the wrong solution, and I was hit by the same problematic cases, but what I found interesting was that some developers went further down a rabbit hole of trying to force replacement to work (e.g. replacing "one" with "one1one", etc) rather than taking a step back and re-thinking the approach entirely and thinking about the problem as a searching problem.
Further spoiler: I replaced "one" with "o1e", "two" with "t2o", etc. which is hacky as hell, but works for throwaway contest code. (Replacing as suggested above also works, but is more typing.)
That let me keep the problem in the filter/map/first-last/reduce space, which was the shape of my solution to part 1.
That’s exactly what I pivoted to once I realized my naive replace wouldn’t work. That sort of moment is what makes AoC feel so rewarding in my opinion.
Here's a snippet of a first attempt, which does not work:
|> Array.map (fun s -> Regex(@"(one|two|three|four|five|six|seven|eight|nine|\d)").Matches(s))
|> Array.map (fun m -> m |> Seq.map (fun g -> g.Value) |> Seq.toList)
And here's the correct code:
|> Array.map (fun s -> Regex(@"(?=(one|two|three|four|five|six|seven|eight|nine|\d))").Matches(s))
|> Array.map (fun m -> m |> Seq.map (fun g -> g.Groups.[1].Value) |> Seq.toList)
There's no "fundamental" difference between the two, rather, one uses a regex with positive lookahead and the other doesn't. The first approach was not operating under the assumption that there would be strings like "oneighth" at the end of a line. That's a detail and it only applies to one part of a functional pipeline!
I agree that one should take a step back at that point but how exactly is this the fundamentally wrong approach to the problem?
Re-using the existing code where possible is the fundamentally right approach to the problem. It just so happens that in this case, there were edge cases that prevented this, so another approach had to be taken.
> Re-using the existing code where possible is the fundamentally right approach to the problem.
Reusing code does not make something "fundamentally right approach". An implementation that actually does what the problem asks is a "fundamentally right approach". If you can reuse existing code in that implementation then that's a bonus.
The problem is very clearly specified, so if you choose to implement something else hoping that it's "close enough" then that's on you...
In my experience they are very much "actual business problems"-alike situations.
There's never been a customer that asked me "I have some elves that need to make snow and here's a trebuchet", sure. But they ask me stuff, I intepret that as well as I can. Go back for questions. Apply DDD, event-storming or whatever if I'm lucky.
But there always is an interpretation issue somewhere, that makes that some "bug" is really "but I spend 5 hours implementing that exception and now you tell me it's a bug?".
Yeah, part two was really something. I got part one in 2:40, and then spent half an hour trying to figure out what the hell was going wrong with part two, lol.
Maybe it was actually done on purpose to foil ChatGPT, because when I get desperate and tried to let GPT4 solve it, it also couldn't figure it out.
I found the problems straightforward to code in Haskell. The first part ran correctly right after I got it to compile. The second initially gave bogus results. Turns out I needed to invert a test and then it ran correctly too.
15 lines of code including 3 imports and 3 type declarations;
1 line to comment out to do the easier problem.
I just created a list of string-value pairs and found first index of and last index of for each of them keeping track of the best first and best last. C# is pretty freaking amazing like that.
My part 2 was 4-line addition to part 1 (see code below, if inappropriate let me know and I'll remove it), and it worked first try. On the other hand, I made a mistake in part one and got it right only on a second attempt...
(definition of nums[] omitted)
for (j = 1; j < 10; j++)
if (!strncmp(&buf[i], nums[j], strlen(nums[j])))
buf[i] = j + '0';
The most obvious edge-case was mentioned by other siblings.
My "edge case" is probably not really even an edge-case though.
SPOILER ALERT ---
My implementation stopped looking when it found a number. So "one2one" or "1twone" and so on, would find the numbers "1,2" and not "1,2,1". There were no examples where a number appeared multiple times in a line AND this affected the first-last pair. So e.g. abc1ninexyz841 would result in 14 in my case but should've been 11.
Again: it's rather implied and quite obvious if you interpret the description as human, but I worked at it from TDD, and the example missed this situation and the actual input had only a relative few of them, so debugging was hard.
Thanks! I thought I had the most straight-forward code there is and there were no edge cases that I hit. But I didn't use regex or something, just manual pattern matching like gp.
Imho regexes are overused for such stuff, precisely because they might do a lot of things you don't think about and are usually a lot slower than manually implementing what you want.
If they have functions that for example reads from forward then backward or vice versa while changing the structure of the row, one test will pass but the other won't. It's a lot of rows in the input and it took a lot of time for me personally to debug to understand what went wrong
The potential for overlapping numbers was the thing that tripped up many developers. But a simple “find the first number searching from each end, just like the puzzle instructions asked” implementation just worked.
The lesson is to read the puzzle instructions carefully and avoid solving more general problems.
I did not use regexes, so as said, the most common edge case did not hit me.
What hit me was stupid, but also not covered in the example. It was rather implied and obvious from the example, though.
SPOILER ALERT
In my mistaken implementation `one2three1` would find "1, 2, 3" but not the second case of 1. Now, while the description never explicitly mentioned this, it's still obvious that it should be "11" and not `13`. Though my example, derived by TDD-ing from the example, gave `11`.
Only after I diffed my output with that of a known working solution did I find a few lines (there were several of them, though not that much) that made my issue clear: I missed the second case of a number appearing. So "one1one1one" in my solution would only find the first one.
The real world is ugly and full of edge-cases and noise and ever-changing-requirements.
So encoding that, and keeping a solution elegant and "beautiful" is what I strive for.
Finding elegance in mathematically pure setups, is, I guess, very rewarding too for many. But not for me. I find pleasure in abstractions, algorithms and architecture that embraces the ugliness and inconsistency of "The Real World" to make it workable (over decades). I hardly ever manage in this though. Most code somehow still ends up in deeply nested if-elses-foreaches and whatnots.
For part 2 there is an elegant and very simple solution that doesn't use regex and doesn't need to work around edge cases... you just have to find it ;)
Many people on reddit reporting they were hit by one edge case that's not covered in the examples. But my implementation passed these edge cases too. I was hit by another edge case. So there are at least two edge-cases (which are in the actual data) that aren't covered in the examples or the description.