Nifty, it worked. Brought the run time down from 30 secs for my naive brute force solution to 100 msecs for the efficient solution. I think there's room for improvement still, but it's super satisfying to find the efficient solution hidden in the rubble.
It makes sense. The manhattan distance function would just look like a 3-d inverted pyramid centered on that point (e.g. \/ ), and the sum of distances is just gonna be the sum of those functions, which in turn are just the sums of the 2-d cross-sections
(sorted-set)
ftw https://github.com/thegeez/clj-advent-of-code-2018/blob/master/src/advent2018/core.clj#L747
Good call!
sorted-set
is also useful in part 2 for keeping track of the in-progress tasks, sorted by time of completion.
Mine's not exactly optimally expressed - there's some duplicate calculations I'd like to weed out - but I think it reads nicely 🙂 https://github.com/armstnp/advent-of-code-2018/blob/master/clojure/src/advent_of_code_2018/day7.clj#L67
I can't quickly check for myself right now, but if you add something to a 'sorted-set' it might be added out of place? The documentation isn't clear, in Java it's a specific type of set which does keep order.
(conj (conj (conj (sorted-set) 1) 2) 0) => #{0 1 2}
Sorry, that's terribly formatted.
(-> (sorted-set)
(conj 1)
(conj 2)
(conj 0)) => #{0 1 2}
Much better
@gklijs I’m assuming it’s always natural sorting by default.
(same as the java set)
So letters will stay in alphabetic order
I tried passing around data this time and having each function recieve and transmit the same data structure. It could have been just as easily done with an atom. I have never tried something like this before and it was super painful. What do other people do? https://gitlab.com/randall.mason/advent-of-code/blob/master/src/advent_of_code_2018/day07.clj Also, gitlab may be having struggs?
I have finally gotten the update assoc distinction, so at least I'm not learning that! 🙂
I like the idea of ancillary functions taking a large map of all the data, then only operating on the subsets the function needs. It's very "inversion of control" style.
I think you could improve your usage of the style by having your ancillary functions return the whole map, after performing some update-in
calls to update the keys that pertain to that function.
Then where you are calling the ancillary functions you can use a threading macro:
(->> the-data
do-thing-1
do-thing-2
do-thing-3)
Ah, I see it's basically what you're doing but instead of calling update-in
and letting the other values come along for the ride by default, you are manually reconstituting the map including all the keys that don't pertain to the function.
Here are a couple examples; https://gist.github.com/bgrabow/0306387873ec31e053091eff73c50626
so is it just me or is day8 part 1 really unclear how you're supposed to split up child nodes?
It's clear to me
hm
ok i think i got it
just tries in the repl (def test-set (sorted-set "d" "c" "r" "m" "a"))
gives #{"a" "c" "d" "m" "r"}
and (conj "n" test-set)
evaluates as #{"a" "c" "d" "m" "n" "r"}
so it works
I just woke up, want to be the first of the team for once
My day 7: https://github.com/jreighley/advent-of-cljc/blob/master/src/aoc/y2018/d07/jreighley.cljc I would love some code review — I am a lonely self taught hack. 😉
Yeah, that's a whole ton more readable. The way I was doing it I was losing some keys when I tried that. Do you not have to enumerate ll the keys, or just the ones you use?
All the data will be preserved if you bind the whole map in your arg list (the :as the-data
part).
And then instead of returning a map from scratch, you return the result of calling (update the-data k f)
(let [m {:a 1 :b 2 :c 3}]
(update m :b inc)) ; => {:a 1, :b 3, :c 3}
@clashthebunny I think the second example in my gist was a little off. If you have a brand new value for a key, you should use assoc
. If you want to apply a function on the old value to get the new value, you should use update
. Hope that helps.
I’m really curious to see other people’s approaches to stream processing today.
I'm done with part 1, my solution is in my own repo though
There is a very sneaky thing in part 2
yeah
You might even say the business requirements aren't clear
mm, I'd say they're off by a small amount
Today's was relatively easy, compared to yesterday's 😃
My favorite part of day 7 was my fn to parse the line:
(defn parse-line [line]
(let [[[lhs] [rhs]] (->> (str/split line #"tep ")
(drop 1))]
[lhs rhs]))
Day 8 was definitely easier, day 7 was the hardest so far for me
i've never worked with zippers, and therefore I am having troubles wrapping my head about parsing day 8 😞
I didn't use zippers, just regular split by space
but I did use trampoline for the first time to avoid a stack overflow
I didn't end up with a deep enough tree to have to worry about stack overflows. Maybe I should test it against other inputs.
No stack overflow for me, raw recursion worked fine.
I think I had similar pains as you @helios, my solution of having a functional parser required using both stack-recursion and loops for state magenement which is unfamiliar for me to piece together. soln: https://github.com/rymndhng/advent-of-clojure/blob/master/src/advent_2018/08.clj
My solution for Day 8: https://github.com/benfle/advent-of-code-2018/blob/master/day8.clj
It's always painful to write a parser in a functional style so I did it with local state 🙂
My day 8: https://github.com/pesterhazy/advent2018/blob/master/src/advent/puzzle08.clj#L37 - I took the functional route
- bad: I was stuck for half an hour because I didn't realized that children are 1-indexed (why??) - good: In the process I rediscovered clojure.tools.trace (which I'm sure will be useful later on)
I'm trying to use clojure.core.reducer/fold
with @.mfikes's day 5 solution.
https://gist.github.com/namenu/21e5d99989fddb1447ba446ca53e40a6
But I don't know why the merging fn is not called. 😞 Any advice?
Started a journal to remember the problems I'm running into and maybe come up with some conclusions: https://github.com/pesterhazy/advent2018/blob/master/journal.md#L1
I’ve actually never seen/used volatile! before. Looks like a new 1.10 thing? Time to learn something new 🙂
they've been there since 1.7 https://github.com/clojure/clojure/blob/master/changes.md#21-transducers
not sure if they're documented except for the (tautological?) docstrings
I found this, https://stackoverflow.com/questions/31288608/what-is-clojure-volatile#comment50623688_31288608 - looks like a good summary
@namenu By default, only a vector and a hashmap are foldable https://github.com/clojure/clojure/blob/c4dc8815272382725ca3d3d06f195e57c3228109/src/clj/clojure/core/reducers.clj#L320-L334
It's pretty clear in the text to ignore the 0, and use the 1's as zero. Your lucky with using clojure since sometimes the index is higher then the number of children. With clojure they must are ignored, which not always is a good thing.
@athos Oh, that's the point! thanks 😄
I'm talking about the "meta-data`, which (in my test data) includes no 0s
I agree that it's clear from the text (I was hasty once more)
I wonder is there a rule of thumb when volatiles are safe to use
and when atoms are required instead
Day 8: https://github.com/mfikes/advent-of-code/blob/master/src/advent_2018/day_08.cljc
@mfikes almost identical to mine!
https://github.com/pesterhazy/advent2018/blob/master/src/advent/puzzle08.clj#L37
Wow!
differences - reduce vs loop (I think I prefer loop here) - different tree-seq params (not sure why mine works)
just discovered the private leaderboard
I'm getting a spec error on my local machine from time to time on the advent-of-cljc tests. Does anybody know what I'm doing wrong? This compiled fine last night...
=== Running clojure test aoc.y2018.d07.clashthebunny
Running tests in #{"src"}
Testing user
Ran 0 tests containing 0 assertions.
0 failures, 0 errors.
=== Running cljs test aoc.y2018.d07.clashthebunny
#error {
:cause Library name must be specified as a symbol in :require / :require-macros; offending spec: [] at line 1 /Users/ranmason/code/advent-of-cljc/cljs-test-runner-out/gen/cljs_test_runner/gen.cljs
:data {:file #object[java.io.File 0x70c69586 /Users/ranmason/code/advent-of-cljc/cljs-test-runner-out/gen/cljs_test_runner/gen.cljs], :line 1, :column 1, :tag :cljs/analysis-error}
:via
[{:type clojure.lang.ExceptionInfo
:message Library name must be specified as a symbol in :require / :require-macros; offending spec: [] at line 1 /Users/ranmason/code/advent-of-cljc/cljs-test-runner-out/gen/cljs_test_runner/gen.cljs
:data {:file #object[java.io.File 0x70c69586 /Users/ranmason/code/advent-of-cljc/cljs-test-runner-out/gen/cljs_test_runner/gen.cljs], :line 1, :column 1, :tag :cljs/analysis-error}
:at [cljs.analyzer$error invokeStatic analyzer.cljc 718]}]
That file is generated with the empty brakets at the top:
cat /Users/ranmason/code/advent-of-cljc/cljs-test-runner-out/gen/cljs_test_runner/gen.cljs
(ns cljs-test-runner.gen
(:require [doo.runner :refer-macros [doo-tests]] []))
I've done a git clean -fdx
, so I am pretty sure I'm not being bitten by cache.
@pesterhazy Yeah, both bother me a little: loop
directly says what you are doing, albeit at a very low level, and it feels like I'm shoehorning reduce
by driving it with range
and essentially ignoring the range
values.
What you really want to do in words is "iterate over this n times". I've updated my solution to try to be closer to that:
https://github.com/mfikes/advent-of-code/commit/7c60c2b1468e1863a30ba5ec9573f9a12d12736e
As a beginner I’d like to ask why you prefer loop over reduce here?
yeah iterate
seems like the most descriptive choice (I actually considered but discarded it - not sure why)
essentially because the 2nd argument in the reduce-fn is ignored
I just saw you mentioned it in the main thread. Thanks tons!
@clashthebunny What is that empty vector doing at the end?
(Of your ns
form.)
It's an autogenerated file from cljs_test_runner.
So I assume I made either made an egregious error in the clojure side of things or I have a bug in the versions of things I use...
That empty vector would normally have the test namespaces listed.
Mine looks like:
(ns cljs-test-runner.gen
(:require [doo.runner :refer-macros [doo-tests]] [aoc.y2018.d08.mfikes]))
Perhaps you deleted the deftest
?
No... that doesn't provoke that kind of failure.
Damn! I figured it out.
It's the whole uppercase wasn't allowed in the username and then I ran it again with lowercase.
Ahh. Damn!
Thanks for the help!
So script/test-one aoc.y2018.d07.ClashTheBunny
works for both cljs and clj, but script/test-one aoc.y2018.d07.clashthebunny
only works for clj.
Is this something that would be expected? Should cljs downcase the namespace?
Or should clj not be downcasing it?
No... this isn't really a case-specific problem. It happens if you ask it to run tests for which there is no namespace.
Or maybe you are onto something... hrm.
I don't know where the problem lies, either Michiel's script, or upstream in one of the test runners. Perhaps file a ticket against advent-of-cljc
.
My rule of thumb is to use volatile unless I have concurrency. There is also transient for IEditableCollection
. https://clojure.org/reference/transients
basically keep them contained in a single fn right?
There are two aspects: - for correctness: ensure the volatiles/transients are not accessed by multiple threads. They don't ensure atomicity. - for code organization: If I use state I prefer to keep it as local as possible so in this case inside the function
For some reasons it always bugs me when the functional style forces me to keep all the state around and change my function signatures.
good points both
Yeah, having to thread the remaining sequence through is a bit of a pain for this one.
damn, I’m close, but I’m having a stackoverflow. I might have to throw the towel due to time constraints 😢
funny, I used naive recursion and didn't run into any stack overflows
Yeah, I think the depth of the tree is smaller than typical stack limits.
yeah, my solution is probably wrong
it works for the test input though
After a few rounds of yakshaving, I ended up with a script to create the next puzzleNN.clj, usable as clojure -m advent.main
: https://github.com/pesterhazy/advent2018/blob/master/src/advent/main.clj#L3
Even better, scripts/dev
automatically starts rebel-readline in the latest puzzle namespace
cool. there’s also a script like this in advent-of-cljc
I think I should stop now 🙂
If you want you could also make it to get your problem
My input data has a depth of 6
As I was doing this I worried there would be some degenerate case where we’d need to save the score for a node to be efficient, like maybe metadata [1 1 1 …} on 5 or 6 nested nodes…
I took this into account and multiplied the node-value by it's frequency, and it is quite a bit faster, but as my full runtime is dominated by parsing the tree, it doesn't make much of a difference.
I've been thinking about this, and I really do think it's possible to do both parts as a single, linear pass that doesn't require recursion...
Yeah, it's basically just turning the recursion stack into an explicit one. But it would certainly make the parts that consume the stream easier to handle, I suspect, by breaking apart the functions that handle different read states (reading header, reading body, reading metadata)
It's possible because I'm doing it, but it's much easier with mutable data, but you can have that as well in clojure.
I had the same. The test input isn’t stacked deeply. Do you need another test input? 2 1 3 3 0 1 2 0 3 7 8 9 1 1 0 2 6 12 1 4 1 2 1 3 0 1 17 1 4 4 2
That one broke my first implementation as did the real data.
I still enjoy the lisp-y way of pushing environments onto an explicit stack. That said, I'm probably making life hard for myself 🙂
I can see how to do that with part 1 but with part 2 I don’t think I stand a chance to to something without a second pass. Unless I don’t understand what you mean with “second pass” :thinking_face:
thanks, I’ll try
If you need the numbers for part 1 or 2, just tell me.
(They’re comfortable enough to do by hand)
can you tell me the number for the test input you just gave me?
Part 1: 80
Do you want Part 2 as well?
I get 80 as well
Accumulating your total with a single walk through the tree's data, beginning to end, no back-tracking
Do we have a private leaderboard on AoC ?
sweet
but the real file still doesn’t work? 😮
Urgh. Nope. I couldn’t do that. But I will now attempt to use tree-seq for walking the tree after I built it… I also want to change how I built the tree because it’s s l o w.
Yes, the invite code is in the little bit of text next to the pin icon and at https://github.com/adventofcode-clojurians/adventofcode-clojurians#leaderboard
Thanks. And OMG I am not last
yeah
I’m sorry that the additional test input didn’t help 😞
no problem
Have you checked your iterations?
Hang on.
Have you considered that there is a newline at the end of the test file?
(That one kicked my bucket a few times already and I guess that’s not it. But as a tester I have to ask 😉 )
got it now
\o/
what was it?
I’m almost certain that this will turn into manually managed frames.
my solutions: https://gist.github.com/dmitrygusev/316119976f77368eb9356aa82a0d0b17
they’re raw and not refactored after I’ve got correct answer, which should stress the pain I had while solving them 🙂
I’m new to clojure and lisp
man….
I’ve gone back and forth between reifying data and doing things algorithmically during AoC
I feel like the results are all over!
Today’s is so much better if you just build the tree.
well… I suppose I’m conflating “reify data” with “use sequence fns”
so reifying the data is probably always worth
sequence fns — not so much. Often too slow for the task when it comes to AoC
Uhm… I just learned a new word. Reify. I am not sure yet if I understand it completely though I consulted a lexicon
:shocked_face_with_exploding_head: I just learned that a test that runs in 1s with vectors can run 40s if I use lists. I guess it’s worth deciding the right way!
In this case: There’s an implied data structure in the sequence of numbers for Day 8. Reifying it means to make it into clojure maps, lists, and vectors instead of requiring pre-understood knowledge.
Structuring it? Like building a tree?
Text is hard 😄
Yepp. Especially when you’re not a native speaker 😄
I’m still finding it difficult to estimate computational complexity of an algorithm in Clojure 😕
I’m far from it, too 😄
But I am learning tons by AoC. First I try to solve it and now I am trying to optimize and to try things out.
It is a tree, but it’s represented as a list of numbers. Better is to make the relationships explicit.
So instead of
[2 0 0 3 10 11 12]
Do
{:metadata []
:children [{:metadata [10 11 12]}]}
How do you walk the first? Well… You the developer must be told what each number means.
How do you walk the second? The relationships are explicit in the structure. It’s immediately obvious.
There are some information about the complexity of the Clojure data structures here: https://clojure.org/reference/data_structures#Collections
@dmitrygusev Do you have an example?
pick any from the gist I’ve posted, i.e. day1 pt 2
the problem is there are plenty of built-in functions whose complexity is unknown
I have four Revisions in this gist and especially the difference between Rev 3 and Rev 4 are mind blowing https://gist.github.com/MeikeMertsch/f1f3a1bfc157c3ed3a6cfcb1dec0ff57
> their specific behavior is slightly different for different types of collections this ^
@dmitrygusev I’m not sure what you mean by that statement. The complexity is pretty well stated for most operations on most structures.
But it is sometimes polymorphic. So you must be aware of the type of the underlying structure.
@meikemertsch Is there a particular change you’re interested in hearing about. Instead of “this whole diff,” it’s easier to talk about, “This change from concat
to conj
” or whatever.
(map-invert
(frequencies
(flatten
(vals
ids-by-minutes))))
i.e. how can you estimate complexity for the above?O(n) (3*n I think)
sry 2*n
but O(n) anyways
flatten is lazy O(n), frequencies is eager O(n), map-invert is eager O(n). The lazy will get rolled into the first eager O(n) so it doesn’t contribute.
Language is hard… what’s “O(n)“?
Once you fundamentally get your algorithm sorted, you can often tweak things to ensure optimizations like chunked sequences or directly reducible collections help.
oh. Just check the diff from rev 3 to 4. it’s just two lines. And really just a conversion
But it’s obviously not polynomial or more. It’s going over some data a few times.
yeah, I meant the C in front of O(n) is difficult to estimate
@meikemertsch The last revision changes behavior. It’s not an isolated optimization.
knowing something is O(n) helps, for sure
Right, gotcha. So is it the laziness that’s difficult?
I noticed. 39 seconds worth of a behavioral change
y
Yeah, that takes a while to get used too, I agree.
@meikemertsch One of those should return an incorrect result, I would presume.
So it might be faster because you’ve dropped some data. idk the context.
no. Both rune fine. Before trying that out I changed all peeks, pops and conjs, so they would work the same for vecs and lists
that’s why the vector version is so slow already. With peeks, pops, and conjs I gain almost half a second speed (see version 2)
ah I’m sorry, I misread this
Obviously I access data WAY more often from the end than from the front. And lists aren’t built for that, right??
Okay, so you’re running into the same lazy evaluation issues @dmitrygusev was talking about
calling vec
goes over all the data immediately
so you’re potentially adding a lot of extra processing
depending on the length of the list
using vec made me 39seconds (!) faster (:shocked_face_with_exploding_head: )
vec vs list is like array with direct access vs linked list, get element from vec is O(1), from list it’s O(n)
So I think I’m misunderstanding the question 🙂 It looks like you took out calls to vec (I presume to make it faster)
But, in general, there’s a lot going on in the solution, so it’s difficult to characterize where it’s spending its time.
haha. Sorry for the confusion. My tests ran 500msec and I wanted them faster so I checked my collection operations and spotted that I did a whole lot of conversions to vectors. So I refactored them out. With the result that everything was s l o w e r
😄 😄 😄
so let’s talk about butlast
the docstring on it directly says it’s a linear scan
pop
is constant time
I’m not certain that’s your issue, but that’s a common mistake
I am quite impressed with what you can read out of those docs. Most of the internals are just “blahb” to me
I can read the words but I don’t understand what they mean.
At least I figured out the tree-seq again. I am fairly happy with that
my day 8 https://github.com/borkdude/advent-of-cljc/blob/master/src/aoc/y2018/d08/borkdude.cljc 😅
Nice. Very similar to Paulus' and mine. 🙂
it took me too long…
Did you start with the tree approach? Or something optimized for part 1 that was then converted to tree?
no, I didn’t start with the tree
I had another solution that only gave me the meta values
Yeah, I suspect if you go down that path, then part 2 becomes exceedingly difficult.
CLJ:
Testing aoc.y2018.d08.borkdude
part-2 took 7.98 msecs
part-1 took 2.81 msecs
Testing aoc.y2018.d08.iamdrowsy
part-2 took 10.25 msecs
part-1 took 14.21 msecs
Testing aoc.y2018.d08.mfikes
part-2 took 18.30 msecs
part-1 took 21.29 msecs
CLJS:
Testing aoc.y2018.d08.borkdude
part-1 took 20.00 msecs
part-2 took 2.00 msecs
Testing aoc.y2018.d08.iamdrowsy
part-1 took 58.00 msecs
part-2 took 38.00 msecs
Testing aoc.y2018.d08.mfikes
part-1 took 120.00 msecs
part-2 took 76.00 msecs
Nice, a rare appearance of trampoline
🙂
I noticed that too 🙂
Lots of solutions involving the expression
[{:children children, :metadata metadata} the-rest-of-the-seq]
🙂I'm not even sure if is correctly placed
mine too
If we had a state monad that we were all used to using maybe we could avoid that. Othewise this aspect was troublesome.
I had [n-children n-meta & nums]
I used an integer position instead of the-rest-of-the-seq, and left the input vector untouched.
you have an outline of how state monad would have helped? i dont see it
https://github.com/bloat/aoc2018/blob/master/src/net/slothrop/aoc2018/day8.clj
@borkdude Yeah, you had
[{:meta (vec meta)
:children children} rest]
just a poorly formatted tree afaict
I could probably optimize that by not going through a seq at all, but staying in a vec
I had no time yesterday to read, but how did you approach part2 of day 7?
The way I see it, you have this extra appendage you have to thread through all of your calls, which is state you need to pass around. Perhaps a monad of some sort would help hide a bit of that complexity.
that’s what the state monad is for yes
uh, I could def be wrong, I don’t think it helps in recursive situations
Otherwise lots of [what-i-want extra-crap]
destructuring, along with passing extra-crap
around.
https://github.com/glguy/advent2018/blob/master/execs/Day08.hs (solution with State Monad)
Yeah, I don't have enough experience with state monads to know if they work in recursive situations. Well, I have nearly zero experience.
I worked through the http://HaskellBook.com this year. I have a little bit of experience with the State monad, and StateT monad transformer.. but I’m not using that a lot so just a matter of time before I forget again
well aren’t we just a bunch of dynalang apes 😄 :troll:
:lol:
I saw that. And I appreciated it while it lasted 😛
Haskell: banging on the type system until it works. Clojure: banging on the REPL until it works.
you saw what?
your original posting re: haskell
just before you deleted it
does anyone know any other interesting users coming from other language to follow? The postgres guy is super interesting https://github.com/xocolatl/advent-of-code/tree/master/2018
oh yeah 😉
(this was mentioned here a few days ago, thanks for that)
no problem 😉
I was on a private leaderboard with him last year and one PureScript user (so three different langs). it was fun to see the different solutions
oh wow, impressive @lucaspolymeris
chapeau @lucaspolymeris!
There are some missing stuff, and others very slow . So if you know Prolog glad to accept PRs
This list looks great: https://github.com/Bogdanp/awesome-advent-of-code
I don’t think it’s doing anything, because parse-data
never returns a function
These Scala solutions look more compact that I would have expected. (Perhaps the author is golfing or that's fairly idiomatic?) https://github.com/FlorianCassayre/AdventOfCode-2018
Yes that looks pretty good, better than most of the haskell/ocaml solution I've looked at
Yeah, I'm leaning towards the idea that the author is pretty good.
I think I’ve convinced myself that solving day 8 w/o recursion would require some gnarly stack juggling
yes
you’d have to basically re-implement call stack frames 😄
“here’s all the state we had at this frame, and where we left off when we were here”
(Just for part 2. I did stack-based in part 1 for funsies.)
I feel like day 8 is foreshadowing a problem where direct recursion won’t be feasible 😰
last year (2017) I saw someone write here: in 2016 the puzzles were harder, now they’re sometimes too easy. I haven’t heard that this year yet?
I feel like first week of 2018 is slightly more challenging than first week of 2017
I think untill now, hardness has been equivalent to 2017
by 2030 AoC will have exhausted every problem but P=NP
Nice F# solution - also has to thread through the "remainingTree"
https://github.com/CameronAavik/AdventOfCode/blob/master/Program.fs#L762
Probably once you've done them for one year it also becomes easier? It's a pretty specific kind of problems.
Yeah it is interesting;
let meta, remainingTree = List.splitAt metadata tree'
and
val (metadata, other) = right.splitAt(nMetadata)
and
(let [[meta rest] (split-at n-meta nums)]
all look very similar(We are all roughly employing the same algorithm, even using the language's built-in "split at" functions.) That's F#, Scala, Clojure
here’s a day 8 stack-based solution https://www.reddit.com/r/adventofcode/comments/a47ubw/2018_day_8_solutions/ebc99t8/
I think there's something about this particular problem that forces you down a similar path (whereas other problems admit a richer variety of approaches.) I found James Henderson's bit of code here https://github.com/jarohen/advent-of-code/blob/master/2018/src/aoc2018/day8.clj#L9-L20 way too similar to mine here https://github.com/mfikes/advent-of-code/blob/b89aff099c2dd42928e038f3d70344b685f7d889/src/advent_2018/day_08.cljc#L9-L18
he used reduce where you used iterate right?
I only later changed mine to iterate
after the conversation with Paulus
reduce
over range
was my first approach
both make sense to me. reduce with a counter could also have been used, but that starts to look like a normal loop
I think I learned the iterate
/ nth
pattern from Bruce from last year