Too busy for blogging

The Fall semester was again another hectic run through the courses, without much breaks. Three courses with two full time and two part time teachers helping, having nearly 900 students. No wonder…

Regarding my latest posts and this situation, I did manage to solve Advent of Code up to part 1 of day 10. Part 2 is still WIP, and I do have some ideas about that. But don’t know if and when I return to that, if ever.

Much of the teaching work at Fall was conducted not in prepared teaching sessions, since for some reason, many students nowadays do not seem to want or need to come to lectures or exercise sessions. Though we teachers sat though them all! Well we did cancel some sessions at the very end since nobody was coming, but anyways.

We even had to arrange three classroom sessions for the Fall, since last year (2024, fully online teaching) students demanded at least some face-to-face teaching for 1st year students. They even went to the Dean of the Faculty to make sure this happens.

Almost nobody came. Less than 20 students (out of ~900) from those three courses went to the classroom sessions, if I remember correctly. Not very effective use of teaching resources, IMO. Hopefully those who came found them valuable!

Well, we have video lectures and other support (Web based FAQ tool in Moodle, email), and major part of the teaching happens via these. Maybe we should be happy that the course materials and the support we do provide is enough for those students that decide to continue in the course. Some enrolled to my DSA (Data Structures and Algorithms) course do not, since they never provide the URL to their project repository or quit after first 1-2 programming assignments.

In the FAQ tool in DSA, I published 47 new FAQs in addition to the 33 old FAQs from the previous course implementations. Many of the solutions in the FAQs could have been avoided if carefully reading the course materials, watching the lectures and following the instructions in the programming assignments. Anyways, maybe the FAQs also contained explanations and points that somehow provided additional help to the students.

I also moved DSA course exams to controlled exam environment. Because of AI slop and cheating is on the rise. Some small hiccups there I need to fix, but seems to work overall.

To make preparing exams a bit more efficient, I implemented several console based tools (in Swift) that generate random Exam questions, also with image attachments. I have tools to generate questions about binary search algorithms, hash table and hashing using linear probing, as well as tools that generate questions testing if students understand how nodes are added to Binary Search Trees. And how to conduct breadth-first-search and depth-first-search to graphs.

Whenever I like, I can then generate new questions, to make sure every student has their own unique set of questions for their exams.

Today I refactored a part of my code analysis tool. The tool and the refactored feature helps me to check if the 300-something students in DSA have used solutions in their code that must be avoided or are forbidden (e.g. calling Java hashCode() when they should have implemented their own, or calculating the hash from wrong things). Or to make sure they have used elements that must or should be used (e.g. calling a function from itself, when recursive code is required). Something teachers could do manually, but since we do not have those teachers in the numbers required for manual work, automation comes to help.

The analysis code used Antrl to parse the students’ Java code to search for things that are not OK and must be OK. While it worked nicely in my Swift app, Antlr was very slow in debug builds. It was a bit faster when the app was build in release configuration, but still slow enough to be annoying.

So instead, the new implementation just runs a Java console app in a Subprocess, using the Swift Subprocess library. I implemented the same code analysis tool as a Java console app and gave that to the students, using com.github.javaparser for the source code parsing.

Using the tool, students can check, before deadlines‚ that they have not done anything stupid. Avoiding to get failed grade after the last deadline, as they can use the tool to check and fix any obvious wrong solutions during the course. Preferably before deadlines, as they will get sanctions on using solutions that are not accepted.

I needed to modify the Java app a bit so that the Swift app I use can launch it and use the output to record results to my Swift app’s database:

  • modified the command line parameters to include output format, either “text” or “json”. Default is text, so students can see more understandable output from the tool. Given the “json” parameter from my Swift app to the Java app explicitly, produces JSON formatted output my Swift tool reads from Subprocess’ stdout.
  • Make sure the Java app output to stdout contain only the JSON, containing the issues found in the code, when “json” format was selected. So the Swift app can then rely on the received output to be valid JSON.
  • Fix some issues in the code analysis output at the same time, that lead to invalid JSON. Like having unescaped quotes in the analysis output, or line endings like \r which messed the output not just in JSON but also in text format.

This change makes also sure that the Java app the students use is behaves exactly as my tool. No surprises in what the tool reports and does not report to students, compared to the results the teacher sees.

Java app executed in the subprocess executes definitely faster than the Antlr implementation in Swift. Additionally, I managed to strip down tens of lines of code, the Antlr generated Java parser removed, plus the dependency to the Antlr library from the Swift tool project is now gone.

Advent of Code 2025

Advent of Code (AoC) for this year started yesterday. I’ve been too busy with work to write here how my puzzle solving has been going. Or anything else, for that matter. Probably will be for the rest of this year.

Why busy? I’ve been moving Data structures and algorithms course exams from Moodle to Exam. Exam is a controlled environment as Moodle was not (at all). There is a tool to import Moodle exam questions to Exam but that is so limited that I decided to hand write new exam questions for this year.

This has taken time, also learning how the Exam works since I have never used it before. Also I’ve been preparing new demonstrations and visualizations for the course to aid learning. There’s still one visualization demonstration to do, namely how the Dijkstra’s shortest path algorithm works. If I do not find the time for that, I just have to search for existing demonstrations from the internet. Would like to use the familiar graph examples from the lectures and other demo materials though.

Anyhows, I have solved both Day 1 and Day 2 puzzles in AoC, using Swift as last year. If you wish to see how the actual puzzles look like, just head on to the AoC website.

Yesterday’s part 2 was a bit difficult, mainly due to some basic mistakes I made in the beginning, and not starting from scratch when I saw the mistakes I had “fixed” with unnecessarily complex code. Sauna break in the evening helped, teaching and other work in between less so 😅. And starting from scratch was a good idea.

Day 1 implementations were fast, part 1 took 0.000684708 seconds to execute, part 2 took 0.000698 seconds (on MacBook Pro M2, Swift implementation, release build, obviously). Lines of code I needed for the part 1 solution is 18, for part 2 line count is 32.

Day 2 implementation’s Part 1 (17 lines of code) took 0.150482667 seconds to execute, while part 2 (27 lines of code) took 0.437480584 seconds. Here I was using Swift’s .split, .map, stride (a for loop kind of a thing) and chunks(ofCount:) from Swift Algorithms package. Much more straightforward (for me) than yesterday’s part 2.

Now to lunch, then to prepare for a MSc thesis supervision session, then to teach (remotely, as usual) for the rest of the afternoon.

Slowly out of Corona virus

I got the corona virus again, the second time for me. Previous infection was in August 2022, and then it was really bad. I had a terrible headache and very, very sore throat for two weeks. Couldn’t event swallow saliva without much pain, even though I ate the max amount of over-the-counter pain killers.

This time was not as bad but still quite bad. Partly the same symptoms but now it was more like a really bad flu with almost as bad headache and sore throat as last time. It’s almost three weeks since I got the first symptoms and it is still not over.

Anyways, I did have the energy to solve Advent of Code days 10 and 11 during this time, when I wasn’t yet feeling too bad. Just didn’t have the energy to write anything about the solutions here. Also managed to solve part 1 of day 12. Since then, nothing.

I still have cough, sneezing every now and then, a feeling of having a flu and am also very tired. The whole Xmas break, as well as new year, and all the days in-between and after was spent just being sick. Didn’t see any family or the kids and their families during the break, to avoid infecting them.

We had great plans to rest and recuperate after the busy work period from September to December; just rest mentally, go skiing and running, eat good food, go sauna, meet the family, etc. So that we’d be well rested when teaching begins next week.

Instead, we are just tired, consumed by this fucking virus. Hopefully no long covid comes out of this, that’d be too much.

Let’s see if I bother to write about the solved AoC puzzles and/or continue to solve the remaining puzzles later on. Because it seems that I have more student projects to supervise than last year, with larger groups. That seems to be a trend nowadays, each year more students, less resources (teachers and hours) to do the work, actually teaching students stuff. This is so bullshit but hey, what can you do.

The student projects start next week, when I am supposed to contact them all and kick them off. Also, next week I need to start grading the Data structures and algorithms course projects, as well as prepare the final exam for that course. That’s already a lot of work to do, in this shape I currently am. Going to take it easy…

Advent of Code quick update

Solved Day 9 part 1 yesterday, but since then I’ve been caught with much work and haven’t had the opportunity to continue in spare/free time. So, I haven’t had the time nor energy to finish part 2.

This is the final teaching week in two programming courses, when students are getting quite active in making sure their work is either ready and acceptable, or they are too late in the schedule and are desperately asking for help to be able to proceed to meet the deadlines. It’s been quite busy, in Zoom breakout rooms, email and our internal support Q&A system…

So, this is probably going to be the AoC week when I’m going to fall behind. I’ll post updates when something gets done, but otherwise it’s radio silence until there is time for this “project”.

Advent of Code Day 8 – Resonant Collinearity

This was surprisingly easy! The part 1 of today’s task was correct on the first run, which is the first time this has happened to me! Usually I make stupid mistakes, not reading the puzzle carefully enough, and rushing to coding with too little planning ahead.

The puzzle part 1 was to find out positions on a map to place antinodes to prevent antennas to emit a signal that makes people 0.1% more likely to buy Easter Bunny brand Imitation Mediocre Chocolate as a Christmas gift.

Calculate the impact of the signal. How many unique locations within the bounds of the map contain an antinode?

A map looks like this:

............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............

Where 0 and A are antennas that are sending on a same frequency, A antennas on their own and 0 antennas theirs, respectively. To prevent their signals, the antinodes ( #)must be placed in line of the antennas, after the first and last pair of antennas, like this:

......#....#
...#....0...
....#0....#.
..#....0....
....0....#..
.#....A.....
...#........
#......#....
........A...
.........A..
..........#.
..........#.

To solve the problem, I implented a Dictionary with key-value -pairs (the [Character: [Point]] below), where the key is an antenna with a specific frequency and value is an array of (x, y) positions on the map where that kind of antennas are placed.

var antinodes: Set<Point> = []
let matrix: Matrix<Character> = parse(data: data)
let antennaMap: [Character: [Point]] = parseAntennaMap(matrix)

After parsing the antennas and their positions into a Matrix, and extracting the antennas and their positions into the dictionary, I could print out the dictionary keys (antennas) with the values (a list of positions for the antennas with the same frequency ):

Antenna A
5:6
8:8
9:9
Antenna 0
1:8
2:5
3:7
4:4

So I could verify that everything is in order for the next step of the puzzle.

I already had a Point structure, having the x and y coordinates, as well as operations to calculate the difference of two points in the coordinate (the distance between them) as well as add together two coordinate points to extrapolate the line between two antennas to position the antinodes inline with the antennas.

So what to do with these? First I needed to handle all the antennas with the same frequency in pairs, to extrapolate the line between them to both directions to add the antinodes. The permutations(ofCount:) algorithm did that. For each pair of antennas, I determined which comes first (point is upper on the map compared to the other) and vice versa.

After that I counted the difference between the first and last point, because the antinode must be place an equal distance away from the antennas. Then I added a point above the first point (first - difference) and below the last point (last + difference).

Then I just added these two antinodes into a Set to get a number of antinodes placed on the map:

for (_, points) in antennaMap {
  let permutations = points.permutations(ofCount: 2)
  for permutation in permutations {
     let first =
     permutation.first! < permutation.last! ? permutation.first! : permutation.last!
    let last = first == permutation.first! ? permutation.last! : permutation.first!
    let difference = last - first
    antinodes.insert(first - difference)
    antinodes.insert(last + difference)
  }
}
antinodes = antinodes.filter({ matrix.contains(y: $0.y, x: $0.x) })
return antinodes.count

Finally I made sure no antinodes were placed outside the map coordinates (using filter to include only those points in the area of the matrix coordinates) and finally returning the count of antinodes placed (the puzzle answer).

Part 2 was just an addition to part 1. Instead of placing one antinode before and after the line of two antennas, place them so that the antinodes are repeated with equal spacing along the line formed by a pair of antennas, until to the edge of the map.

What I first missed in the puzzle instructions was that the positions of the antennas also count in the final answer, not just the antinodes. I did get that the unique positions only are counted (that is why the Set<Point> is used for antinode positions). As I realized this, I just formed a union of antinode and antenna positions, and count of those was the correct answer!

In total, this didn’t take long to solve, thanks to the existing data structures and algorithms, both from Swift, Swift libraries (Swift Algorithms) and my own utilities from previous years. Performance is also OK:

$ swift run -c release AdventOfCode 8 --benchmark
Building for production...
Build of product 'AdventOfCode' complete! (0.28s)
Executing Advent of Code challenge 8...
Part 1: 259
Part 2: 927
Part 1 took 0.000727958 seconds, part 2 took 0.000798333 seconds.

Advent of Code Day 7 – Bridge Repair

The goal for the day was to find out if summing or multiplication would produce the result of an equation, evaluating from left to right (not as usual in maths, evaluating multiplication first). For example:

190: 10 19

Here the result is 190, so it is obvious that multiplication is needed, 10 x 19 is 190. Things get a bit more complicated when there are more numbers:

3267: 81 40 27

Here it is possible to solve the correct result by 81 + 40 * 27 and 81 * 40 + 27 since they both equal 3267. Basically this is a problem where you want to try out different combinations, also called as permutations, of how to use summing and multiplication to produce the correct end result. The input contains some formulas that do not produce the correct result with any permutation, so those are ignored when producing the end sum of the game.

One way to solve this is using recursion – trying different combinations based on the previous evaluation step result with the current number and looking if, after evaluating all the numbers, this produces the correct result.

Simplified, the recursive part looks like the code below. Parameters are:

  • result: The expected end result of the evaluation.
  • intermediate: The intermediate result of the previous + and * operations on the previous numbers, and
  • numbers: the numbers not yet evaluated.

The return value is the result of the evaluation, either the correct result or zero if evaluation didn’t result in the correct result.

func evaluate(_ result: Int, _ intermediate: Int, _ numbers: [Int]) -> Int {
  if numbers.count == 1 {
   if intermediate + numbers.first! == result {
    return result
   } else if intermediate * numbers.first! == result {
    return result
   }
   return 0
  }
  
  let nextNumber = numbers.first!
  
  let summed = intermediate + nextNumber
  let multiplied = intermediate * nextNumber
  
  let evaluateSummed = evaluate(result, summed, Array(numbers.suffix(numbers.count - 1)), useConcatOperator: useConcatOperator)
  if evaluateSummed == result {
   return result
  }
  let evaluateMultiplied = evaluate(result, multiplied, Array(numbers.suffix(numbers.count - 1)), useConcatOperator: useConcatOperator)
  if evaluateMultiplied == result {
   return result
  }
  return 0
}

Recursion must always stop, otherwise it would continue forever – or actually, until stack memory available to the program runs out, producing a stack overflow.

Ending the recursion is handled first; if there is only one number left (the rightmost) we check if the intermediate result of the previous calculations together with the last number produce the expected result either by summing or multiplying. If not, return zero to indicate that this permutation of operations does not work correctly .

If there was more than one number left, both the sum and multiplication is calculated from the previous intermediate result, together with the current number. Then both intermediate values are used — recursively calling evaluate twice with sum and multiplied intermediate — with the rest of the numbers, checking if this produced the result we want. If not, code continues to the end of the function and returns zero — cannot do the thing with this permutation of operations.

I could have optimized the result, finishing the recursion earlier if the produced intermediary sum is greater than the end result. In that case, it is not useful to continue handing the numbers that are still left. Maybe I’ll add that later.

Second part of the puzzle added a third operator, concatenation. In addition to summing or multiplying, two numbers could be also concatenated, e.g. numbers 12 and 34 would result in number 1234.

This was easily added to the code above, just add another else if branch in the beginning and in the end part of the algorithm above, and execute those conditionally (if the function was called from part 2 solution).

Since the non-optimized version was already fast enough, haven’t done any optimization:

$ swift run -c release AdventOfCode 7 --benchmark
Building for production...
Build of product 'AdventOfCode' complete! (0.17s)
Executing Advent of Code challenge 7...
Part 1: 12940396350192
Part 2: 106016735664498
Part 1 took 0.023434208 seconds, part 2 took 0.590302166 seconds.

Now lunch, then some socializing, maybe continuing to Day 8 puzzles today or tomorrow, let’s see…

Advent of Code Day 6 – Guard Gallivant

First part of today’s puzzle was to find out the path of the guard on a map shown below, and count how many distinct positions will the guard visit before leaving the mapped area.

....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...

Guard starts walking upwards from the ^ and the # ‘s are blocks on the path, and when the guard meets one, she always turns right in front of the block.

The part 1 was quite straightforward to implement and didn’t take too long.

In part 2 the goal was to find out in how many places a new obstacle (#) can be placed, so that it causes the guard to walk in circles.

I was working on different solutions, considering using a graph again to see if the graph contains a cycle (there are algorithms for that). Since yesterday was quite a busy day otherwise, I couldn’t focus on solving the puzzle, hence I am one day behind.

Today morning I continued with the part 2 with the ideas I had been thinking about, not needing a Graph. One can detect a cycle when the same turning point is met the second time. So I use a Set<Turn> structure, containing the point and the direction where the turn was taken. Set doesn’t allow you to add a new item that is already in the set, and if this happens, it indicates a cycle in the path.

So I just started to add new obstacles in the Matrix in every possible point that is not a starting point, not already containing an obstacle, and then see if this leads to a cycle in the path, counting those places.

The solution worked with the tests, but when executed with my input data, it always produced too small a number. And I couldn’t find out why! Finally, I searched the internetz and found a comment in Reddit, someone having the same issue and explaining the reason to it:

I was changing direction and going to the next position if there was a blockade in the same iteration, instead of changing direction or moving to the next position. This worked for part 1, but for part 2, this gave me the correct answer for sample input, but for the actual input, the answer was too low, and it took me 4 hours to figure out the issue.

— ajzone007

I was having exactly the same issue! This was a one line fix, not advancing to the next position when direction changes, until the next roll of the loop! This is relatively fast:

$ swift run -c release AdventOfCode 6 --benchmark
Building for production...
Build of product 'AdventOfCode' complete! (0.16s)
Executing Advent of Code challenge 6...
Part 1: 4819
Part 2: 1796
Part 1 took 0.001846167 seconds, part 2 took 0.56900375 seconds.

Here’s the path finding algorithm, used both in part 1 and part 2, throwing an exception when a cycle is met, used in part 2 solution:

private func findPath(
    from start: Point,
    in matrix: Matrix<Character>
) throws -> (path: [Point], turns: Set<Turn>) {
    var direction = Direction.from(matrix[start.y, start.x]!)!
    var currentPoint = start
    var path: [Point] = []
    var turns: Set<Turn> = []
    path.append(start)
    repeat {
      let nextPoint = currentPoint.movedTo(direction)
      if !matrix.contains(y: nextPoint.y, x: nextPoint.x) {
        break
      }
      if matrix[nextPoint.y, nextPoint.x] == "#" {
        direction = direction.turnRight()
        let (inserted, _) = turns.insert(Turn(point: currentPoint, direction: direction))
        if !inserted {
          throw Day06Error.hasCycles
        }
      }
      else {
        currentPoint = nextPoint
      }
      path.append(currentPoint)
    } while true
    return (path, turns)
}

The Set return value is not actually used in the calling code, so that could be taken out.

Then to the Day 7…

Advent of Code Day 5 – Print Queue

Yesterday was a busy day at work and also after, shopping for today: the Independence Day of Finland. Soon I’ll go to see the cannons fire at Linnansaari (Castle Island) with the kids’ families so just a short update on yesterday’s puzzle.

Part 1 was easy and fast to implement. I used last year’s Graph<T> algorithms I implemented to find out which book pages should precede other pages in a list of page numbers. Graph contains the list of pages (vertices) and for each page, those pages that should follow that page (edges).

Second puzzle then… I started with it way too complicated. When failed, I decided to go brute force. The goal was to fix broken page orders, so I thought that I’d just create all the permutations of page orders for a certain print job and try which of those would be correct.

Well, when the input data has page sequences like…:

74,56,86,81,84,44,53,92,12,36,15,66,95,26,71

…and you realize that this sequence of 15 elements alone has 15! permutations, that is 1 307 674 368 000, and the data set has 200+ such sequences, you’ll know that this will take way too much time.

So I left this puzzle alone yesterday and continued this morning, when I woke up after 5 am and couldn’t get any sleep, waiting for the trip to see the cannons fire.

Instead of permutations, the working (and fast) algorithm just goes through the sequence and checks if a page number following the previous is in the edge list of the previous page. If not, the pages are swapped. This is done in a loop that checks if the sequence is still not correct, the checking and swapping continues until the morale improves:

private func fix(_ pages: [Int], using graph: Graph<Int>) -> [Int] {
    var modPages = pages
    repeat {
      for index in 0..<modPages.count - 1 {
        let preceding = modPages[index]
        let following = modPages[index + 1]
        let edges = graph.edges(for: graph.vertex(for: preceding)!)?.map ( {$0.destination.item } )
        if edges != nil && !edges!.isEmpty {
          if !edges!.contains(following) {
            modPages.swapAt(index, index + 1)
          }
        } else {
          modPages.swapAt(index, index + 1)
        }
      }
    } while !isUpdateValid(modPages, graph)
    return modPages
}

This did the trick, and is fast, and the puzzle is solved. Let’s see how today’s puzzle is when I return from the morning trip.

$ swift run -c release AdventOfCode 5 --benchmark
Building for production...
[5/5] Linking AdventOfCode
Build of product 'AdventOfCode' complete! (2.73s)
Executing Advent of Code challenge 5...
Part 1: 4689
Part 2: 6336
Part 1 took 0.006513333 seconds, part 2 took 0.020255375 seconds.

Advent of Code Day 4 – Ceres Search

Today I got to reuse the Matrix<T> generic data structure I implemented last year. Input being a grid of characters like:

MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX

The goal in part 1 of the puzzle was to find out how many times the word “XMAS” appears in the grid, whether it is written left-to-right, right-to-left or downwards or upwards. And diagonally, from left to right and down, or from right to left and up, as well as from left to right and up, or from right to left and down.

The Matrix implementation has an algorithm to rotate the matrix in steps of 90 degrees. So instead of traversing the grid in all those directions, I rotated it 90 degrees to find out the left-right-up-down and back occurrences of XMAS. After each rotation, I just searched let to right, covering the other directions using rotation

The diagonals are handled in the same way, but in each rotation, I only handled the upper right half of the grid, diagonally where the lines were longer than three characters. I first made the mistake of handling the corner-to-corner line in all rotated diagonals, having too large count of occurrences. When I got to see this mistake, I handled that corner-to-corner diagonal only in one of the rotations.

In all of those cases I ended up with a line, string of characters. What I needed to do then was to find out how many times the line (and the line in reversed order) contains the XMAS.

Part 2 was much more simpler than part 1, so that was quickly solved. The goal was to count how many crosses like this…:

M.S
.A.
M.S

…the matrix contains.

This required just two loops within, starting from the first line and column from the edge, finishing on second-to-last row and column, check if the cell contains an A and then check if the opposing corners are different and contain M and S.

This was quick on my work laptop (MacBook Pro M2, 2022):

$ swift run -c release AdventOfCode 4 --benchmark
Building for production...
[1/1] Write swift-version--1AB21518FC5DEDBE.txt
Build of product 'AdventOfCode' complete! (0.16s)
Executing Advent of Code challenge 4...
Part 1: 2549
Part 2: 2003
Part 1 took 0.014584666 seconds, part 2 took 0.002027958 seconds.

The matrix rotations take time, obviously, so part 1 was slower. I considered counting the results of rotated matrices in parallel, but decided not to. That may or may not produce any speed advantage, don’t know… Also could’ve tried to do not rotating in part 1, and just go all in to the matrix from all directions and back to find the words. Well’ it works so I’ll let it be.

Advent of Code Day 3 revisited

So, originally I did this the old school style, parsing the input string character by character, managing the state of processing while discovering the elements from the input string.

Tonight I reimplemented part 1 using #Swift RegexBuilder. I’ve never been comfortable in using regular expressions (regex) but wanted to give a try.

Swift RegexBuilder code snippet showing how to pick up the mul instructions with parameters from the day's puzzle input.
Snippet of RegexBuilder code showing how to specify what elements to pick from the input string.

Got it working nicely after some hiccups, as usual – wondered why the sum of mul operations was smaller than expected, and it was because I had Repeat(1...2) instead of Repeat(1...3) in the code. Didn’t notice that some mul parameters were three numbers, since in puzzle examples there were only two numbers. So the regex was only picking first two numbers at most from the input :facepalm:

Learned something new. Maybe regexes are not so terrifying things after all…

An illustration depicting a muscular figure reminiscent of a Greek warrior, labeled "PROGRAMMERS," with an arrow lodged in its leg labeled "REGEX." The style is simplistic and humorous, emphasizing the struggles programmers face with regular expressions.