Advent of Code 2022

Well the time has finally come… It’s another first of December and Advent of Code has begun again. I hope some of you are also taking part. I will post my discussions here, as I have in past years, and any of you is of course welcome to do the same, or to comment as you follow along. Most of all, learn, practice and have fun coding.

1 Like

It’s nice to start with a fairly simple one. Took me 46 minutes, mostly typing.

I’m using a very nice Common Lisp idiom called do. It’s something I use a lot.

Otherwise this is all pretty simple. Total up each elf’s calories and return the max.

I refactored the calorie-totals function in part 1 to just return the totals so that I could re-use the same function in part two.

;;;; Day01.lisp
;;;; 2022 AOC Day 01 solution
;;;; Leo Laporte, 1 Dec 2022

(ql:quickload '(:fiveam))

(defpackage :day01
  (:use #:cl
	#:fiveam))      ; for inline testing

(in-package :day01)

(setf fiveam:*run-test-when-defined* t) ; test when compiling test code (for quick iteration)
(declaim (optimize (debug 3)))          ; max debugging info

(defparameter *data-file* "~/cl/AOC/2022/day01/input.txt")  ; supplied data from AoC

#|
--- Part One ---

Find the Elf carrying the most Calories. How many total Calories is that Elf carrying?

NOTES: As is often the case the only challenge here is how to parse the data. FILE-READ-LINES reads
the file into a list of integer strings separated by an empty string (that's how it interprets
the blank line). The obvious solution is nested for loops - one to total the groups, one to
stuff the totals into a list. DO loop to the rescue.

This is a uniquely lispy idiom I use all the time. The DO has three clauses, the first sets the
local variables (and advances them each time through the loop), the second is the termination
case with a return value, the third is the body of the loop.
|#

(defun calorie-totals (list-of-cals)
  "given a list of calories carried by each elf, separated by an empty string,
return the list of total calories carried by each elf sorted from greatest to lowest"
  (do ((loc list-of-cals (rest loc))      ; the input data, a list of calorie strings
       (cals-per-elf 0)                   ; total cals per elf
       (totals nil))                      ; list of totals for each elf

      ;; termination condition (empty loc)
      ((equal loc nil) (sort totals #'>)) ; done? return the sorted list of totals

    ;; body of DO loop
    (if (equal (first loc) "")            ; inter-elf separator reached
	(progn
	  (push cals-per-elf totals)      ; save the total to the totals list
	  (setf cals-per-elf 0))          ; and clear the tote for the next elf
	;; else
	(incf cals-per-elf (parse-integer (first loc)))))) ; add this calorie count to elf's total

(defparameter *test-data* '("1000" "2000" "3000" ""
			    "4000" ""
			    "5000" "6000" ""
			    "7000" "8000" "9000" ""
			    "10000"))

(test calorie-totals-test
  (is (= 24000 (first (calorie-totals *test-data*)))))

(defun day01-1 (f)
  "given a data file containing a list of integer string groups separated by empty strings return
the highest group total"
  (first (calorie-totals (uiop:read-file-lines f)))) ; read input file, return maximum calorie total

#|
--- Part Two ---

To avoid this unacceptable situation, the Elves would instead like to know the total
Calories carried by the top three Elves carrying the most Calories. That way, even
if one of those Elves runs out of snacks, they still have two backups.

|#

(defun day01-2 (f)
  "returns the three highest calorie totals from a list of calorie counts"
  (apply #'+ (subseq (calorie-totals (uiop:read-file-lines f)) 0 3))) ; add the top three

(time (format t "The answer to AOC 2022 Day 01 Part 1 is ~a" (day01-1 *data-file*)))
(time (format t "The answer to AOC 2022 Day 01 Part 2 is ~a" (day01-2 *data-file*)))

;; The answer to AOC 2022 Day 01 Part 1 is 72017
;; Evaluation took:
;; 0.000 seconds of real time
;; 0.000396 seconds of total run time (0.000330 user, 0.000066 system)
;; 100.00% CPU
;; 130,544 bytes consed

;; The answer to AOC 2022 Day 01 Part 2 is 212520
;; Evaluation took:
;; 0.000 seconds of real time
;; 0.000398 seconds of total run time (0.000345 user, 0.000053 system)
;; 100.00% CPU
;; 196,080 bytes consed


;; --------Part 1--------   --------Part 2--------
;; Day       Time   Rank  Score       Time   Rank  Score
;; 1   00:36:07  10562      0   00:46:09  10629      0


So usually Day 1 is not much of a challenge. This year is no exception. It basically boils down to being able to handle/parse the input. The actual name of the algorithm you need is called Control Break. Basically you need to process the inputs one at a time accumulating a sum, and when you see a blank line, you need to do save the sum (put it in a list or an array) and then reset the sum for the next input.

I am using Java again this year, and my code is structured the same as last year. I create a Main class and a class specific to the day, Day01 in this case. In past years I have gone through more effort to make the day class not be static, but for this year I am only going to do that when I know I will need multiple instances, which is unlikely to be needed unless I encounter a problem that suits it.

I supply my input with the recently new Java feature to have multi-line strings, such as this code from my test case for the sample input. I added an extra blank line and a -1 input to signify the end of input and to make handling the input easier. (More specifically, I am using the Java String split() function on blank lines and it will ignore blank lines at the end, so adding the -1 makes a fast and easy workaround to needing the input to end on a blank line.)

 final static String SAMPLE_INPUT =
        """
1000
2000
3000

4000

5000
6000

7000
8000
9000

10000

-1
        """;
  final static String[] DAY01_INPUT_LINES = DAY01_INPUT.split("\n");

  public static void main(final String[] args)
  {
    final Long[] howMuchEachElfCarries = Day01.getHowMuchEachElfIsCarrying(DAY01_INPUT_LINES);
    final long   maxElf = Day01.getMaxElfAmount();
    
    System.out.println("Part 1: " + maxElf);
    
    System.out.println("Part 2: " + Day01.getNameChangedToAvoidSpoilingPart2());

So the part one code looks like this:

final class Day01
{
  static private long maxCarry = -1;
  static Long[] howMuchEachElfCarries = new Long[0];
  
  public static Long[] getHowMuchEachElfIsCarrying(final String[] day01InputLines)
  {
    final List<Long> elfCarryAmounts = new ArrayList<>();
    
    long carryAmount = 0;
    for (final String line: day01InputLines)
    {
      if (line.length() == 0)
      {
        elfCarryAmounts.add(carryAmount);
        if (carryAmount > maxCarry) maxCarry = carryAmount;
        carryAmount = 0;
      }
      else
      {
        final long elfItem = Long.parseLong(line);
        if (elfItem < 0) break;
        carryAmount += elfItem;
      }
    }
    howMuchEachElfCarries = elfCarryAmounts.toArray(new Long[0]);
    return howMuchEachElfCarries.clone();
  }

  public static long getMaxElfAmount()
  {
    return maxCarry;
  }

There isn’t really any trick here, except that I am calculating the max as I go along, rather than making a second pass. I also keep the array around as a static so I don’t have to pass it back in for part 2.

Part 2 is a straight forward sorting of the array saved from part 1 and then just grabbing the final three elements for a sum.

Here’s the function for part 2:

  public static long getTopThreeElves()
  {
    Arrays.sort(howMuchEachElfCarries);
    final long topThreeElves =
        howMuchEachElfCarries[howMuchEachElfCarries.length-1] +
        howMuchEachElfCarries[howMuchEachElfCarries.length-2] +
        howMuchEachElfCarries[howMuchEachElfCarries.length-3];
    return topThreeElves;
  }

Most of my time before I tackled the code was attempt to use Typescript, get odd errors that basically meant I have to learn too much, and just reuse the base code from last year.

The base code for each day are two functions, part1(d) and part2(d), with d being the raw data (after trimming) for the day.

Explanation of my code

As is the norm at the start, I basically make lambda chains, like I’m treating JavaScript as a pure functional language.

  • First, I split d on the blank lines. At this point in the chain, I have an array of strings.
  • Second, the map function I use creates a sub chain, which first splits on new lines, converts each element to a number, then sums them up with the reduce function. So now I have an array with the sum for elf.
  • For part 1, I just return the max at this point. Since the template code is
    const data = chain
    return data
    
    I just put the max function on the return line.
  • Part 2 continues the chain by sorting the array in reverse order (As I need to write the sorter function for numbers in JavaScript, it sorts in reverse to begin with
  • Then I use the slice function to get the first three elements
  • Then I use the reduce function to sum up everything, and finally the chain for part 2 is done.

Because of the length of Part 2’s chain, I added line breaks this time!

My Code
/**
 * @param {string} d 
 */
export const part1 = async d => {
	const data = d.split('\n\n')
		.map(e => e.split('\n')
			.map(e => parseInt(e, 10))
			.reduce((p, c) => p + c, 0)
		);
	return Math.max(...data);
};

/**
 * @param {string} d 
 */
export const part2 = async d => {
	const data = d.split('\n\n')
		.map(
			e => e.split('\n')
				.map(e => parseInt(e, 10))
				.reduce((p, c) => p + c, 0)
		)
		.sort((a,b)=> b - a)
		.slice(0, 3)
		.reduce((p, v) => p + v, 0);
	return data;
};

I’m using Perl again.
Comparing my solutions this year and last year, I’ve notice on big difference.
My solutions are less memory intensive. Lots of solutions seem to load up memory structures then apply a sort or other processing. I tend to just deal w/ one input line at time and keep running sums.
My day1 part 1 solution:

 21    if (open my $F, '<', $input) {
 22       while (my $calories = <$F>) {
 23          chomp $calories;
 24          if ($calories eq '') {
 25             if ($sum > $max_sum) {
 26                $max_sum = $sum;
 27             }
 28             print "sum=$sum  max=$max_sum\n";
 29             $sum = 0;
 30          }
 31          else {
 32             $sum += $calories;
 33          }
 34       }
 35       # Handle last elf
 36       if ($sum > $max_sum) {
 37          $max_sum = $sum;
 38       }
 39       &Log("Max calories sum  = $max_sum\n");
 40    }

chomping calories is somehow so right!

2 Likes

Day 2 and we’re playing Rock, Paper, Scissors with a strategy guide to decode.

The parsing for the input is pretty straight forward, nothing really tricky there. The best way to decode the required math appeared to be a pair of nested case statements, although you could have just as easily put all cases into a hashmap and looked up the constant result.

Here’s what I did for part 1:

final class Day02
{
  public static long part1(String[] day02InputLines)
  {
    long totalScore = 0;
    for (final String gameLine : day02InputLines)
    {
      final String[] moves = gameLine.split(" ");
      if (moves.length != 2) throw new IllegalStateException("Bad moves: " + day02InputLines);
      totalScore += playGame(moves[0], moves[1]);
    }
    return totalScore;
  }

  private static int playGame(final String move1, final String move2)
  {
    switch (move1)
    {
      case "A":  // Rock
      {
        switch (move2)
        {
          case "X":  return 1+3; // Rock
          case "Y":  return 2+6; // Paper
          case "Z":  return 3+0; // Scissors
          default: throw new IllegalStateException();
        }
      }
      
      case "B":  // Paper
      {
        switch (move2)
        {
          case "X":  return 1+0; // Rock
          case "Y":  return 2+3; // Paper
          case "Z":  return 3+6; // Scissors
          default: throw new IllegalStateException();
        }
      }

      case "C":  // Scissors
      {
        switch (move2)
        {
          case "X":  return 1+6; // Rock
          case "Y":  return 2+0; // Paper
          case "Z":  return 3+3; // Scissors
          default: throw new IllegalStateException();
        }
      }
      
      default: throw new IllegalStateException();
    }
  }

And part two was mostly just a reformulating of part 1 with different math:

  public static long part2(String[] day02InputLines)
  {
    long totalScore = 0;
    for (final String gameLine : day02InputLines)
    {
      final String[] moves = gameLine.split(" ");
      if (moves.length != 2) throw new IllegalStateException("Bad moves: " + day02InputLines);
      totalScore += playGame2(moves[0], moves[1]);
    }
    return totalScore;
  }

  private static int playGame2(final String move1, final String move2)
  {
    switch (move1)
    {
      case "A":  // Rock
      {
        switch (move2)
        {
          case "X":  return 3+0; // Need to lose, so scissors
          case "Y":  return 1+3; // Need to tie, so rock
          case "Z":  return 2+6; // Need to win, so paper
          default: throw new IllegalStateException();
        }
      }
      
      case "B":  // Paper
      {
        switch (move2)
        {
          case "X":  return 1+0; // Need to lose, so rock
          case "Y":  return 2+3; // Need to tie, so paper
          case "Z":  return 3+6; // Need to win, so scissors
          default: throw new IllegalStateException();
        }
      }

      case "C":  // Scissors
      {
        switch (move2)
        {
          case "X":  return 2+0; // Need to lose, so paper
          case "Y":  return 3+3; // Need to tie, so scissors
          case "Z":  return 1+6; // Need to win, so rock
          default: throw new IllegalStateException();
        }
      }
      
      default: throw new IllegalStateException();
    }
  }

Using Perl again. And a hash. Day2 part1:

 17    my %scores  = (
 18                   'A X' => 4,  # 1 + 3
 19                   'A Y' => 8,  # 2 + 6
 20                   'A Z' => 3,  # 3 + 0
 21                   'B X' => 1,  # 1 + 0
 22                   'B Y' => 5,  # 2 + 3
 23                   'B Z' => 9,  # 3 + 6
 24                   'C X' => 7,  # 1 + 6
 25                   'C Y' => 2,  # 2 + 0
 26                   'C Z' => 6,  # 3 + 3
 27                   );
 28    my $sum = 0;
 29    if (open my $F, '<', $input) {
 30       while (my $choices = <$F>) {
 31          chomp $choices;
 32          if (!exists $scores{$choices}) {
 33             print "ERROR $choices not in hash\n";
 34          }
 35          $sum += $scores{$choices};
 36          sleep 0;
 37       }
 38       &Log("sum = $sum\n");

I thought about a big case statement, or a hash, but I ended up indexing into a two dimensional array for the scoring. Part 2 just required the addition of another 2D array to chose the right move.

Not so complicated, but it took me a couple of hours.

;;;; Day02.lisp
;;;; 2022 AOC Day 02 solution

;;;; Leo Laporte, Dec 2022

(ql:quickload '(:fiveam :cl-ppcre :alexandria))

(defpackage :day02
  (:use #:cl
	#:fiveam       ; for inline testing
	#:cl-ppcre     ; regex
	#:alexandria)) ; lil utilities

(in-package :day02)

(setf fiveam:*run-test-when-defined* t) ; test when compiling test code (for quick iteration)
(declaim (optimize (debug 3)))          ; max debugging info

(defparameter *data-file* "~/cl/AOC/2022/day02/input.txt")  ; supplied data from AoC

#|
--- Day 2: Rock Paper Scissors ---

--- Part One ---

What would your total score be if everything goes exactly according to your strategy guide?

NOTES: Well this might be inelegant but I'm going to create a two-dimensional
array of possible outcomes in rock paper scissors.
|#

(defun play (move-list)
  "given a list of moves, calculate the total score after all moves are played"
  (let ((score 0))            ; this will be the total score
    (dolist (m move-list)     ; work through the list of moves one by one
      (incf score (move m)))  ; score() does the work here, add it to the total
    score))                   ; return the final tally

(defun move (the-move)
  "returns the score after opp and me are played"
  (let* ((m (convert the-move))
	 ;; this is the array I was talking about - did this by hand
	 (scoring (make-array '(3 3) :initial-contents '((3 6 0)
							 (0 3 6)
							 (6 0 3)))))

    (+ (1+ (cdr m))                       ; the points for the move I played
       (aref scoring (car m) (cdr m)))))  ; and addressing into the array for the score

(test move-test
  (is (= 8 (move "A Y")))
  (is (= 1 (move "B X")))
  (is (= 6 (move "C Z"))))

(defun convert (move)
  "converts letter moves into a cons of naturals for accessing the score array"
  (let ((opp (subseq move 0 1))
	(me (subseq move 2 3)))
    (cons
     (cond ((equalp opp "A") 0)
	   ((equalp opp "B") 1)
	   ((equalp opp "C") 2))
     (cond ((equalp me "X") 0)
	   ((equalp me "Y") 1)
	   ((equalp me "Z") 2)))))

(test convert-test
  (is (equal (cons 0 0) (convert "A X")))
  (is (equal (cons 0 1) (convert "A Y")))
  (is (equal (cons 1 0 ) (convert "B X"))))

(test play-test
  (is (= 15 (play '("A Y" "B X" "C Z")))))

(defun day02-1 (f)
  (play (uiop:read-file-lines f)))

#|
--- Part Two ---

The Elf finishes helping with the tent and sneaks back over to you. "Anyway, the second column says how the round needs to end: X means you need to lose, Y means you need to end the round in a draw, and Z means you need to win. Good luck!"

NOTES: I just have to add a step before scoring: convert the instruction into the
appropriate move. Looks like I'll be making another array, this time of moves I
should play according to the instructions. I can use the rest of the code above
for the actual scoring.

|#

(defun play2 (move-list)
  "given a list of moves, calculate the total score after all moves are played (this time adjusting for the second instruction being an outcome instead of a move)"
  (let ((score 0))
    (dolist (m move-list)
      (incf score (move (choose-move m))))  ; choose move based on desired outcome
    score))

(defun choose-move (the-move)
  "given a move in the form of a string with two letters, return the move that I must
play to achieve the desired outcome"
  (let ((mv (convert the-move))        ; turn the letters into array indices
	(opp (subseq the-move 0 1))    ; I'll still need the letter for my opp's move

	;; and now another array with the moves to be made for the desired result
	(which-move (make-array '(3 3)
				:initial-contents '(("Z" "X" "Y")       ; rock: lose/draw/win
						    ("X" "Y" "Z")       ; paper: l/d/w
						    ("Y" "Z" "X")))))   ;scissors: l/d/w
    ;; rebuild the move with with my actual response
    (concatenate 'string opp " " (aref which-move (car mv) (cdr mv)))))

(test choose-move-test
  (is (equalp "A X" (choose-move "A Y")))
  (is (equalp "B X" (choose-move "B X")))
  (is (equalp "C X" (choose-move "C Z"))))

(defun day02-2 (f)
  (play2 (uiop:read-file-lines f)))

(time (format t "The answer to AOC 2022 Day 02 Part 1 is ~a" (day02-1 *data-file*)))
(time (format t "The answer to AOC 2022 Day 02 Part 2 is ~a" (day02-2 *data-file*)))

;; The answer to AOC 2022 Day 02 Part 1 is 14264
;; Evaluation took:
;; 0.000 seconds of real time
;; 0.000812 seconds of total run time (0.000642 user, 0.000170 system)
;; 100.00% CPU
;; 850,800 bytes consed

;; The answer to AOC 2022 Day 02 Part 2 is 12382
;; Evaluation took:
;; 0.001 seconds of real time
;; 0.001315 seconds of total run time (0.001092 user, 0.000223 system)
;; 100.00% CPU
;; 1,570,992 bytes consed

;; --------Part 1--------   --------Part 2--------
;; Day       Time   Rank  Score       Time   Rank  Score
;; 2   01:25:57  19891      0   01:57:08  20821      0
;; 1   00:36:07  10562      0   00:46:09  10629      0

I did a hash map for day 2. My function chain was the same for each part as a result. I wonder what day will cause me to abandon the function chain this year?

'use strict';
/**
 * @param {string} d 
 */
export const part1 = async d => {
	const winData = {
		'A X': 1 + 3,
		'A Y': 2 + 6,
		'A Z': 3 + 0,
		'B X': 1 + 0,
		'B Y': 2 + 3,
		'B Z': 3 + 6,
		'C X': 1 + 6,
		'C Y': 2 + 0,
		'C Z': 3 + 3,
	};
	const data = d.split('\n')
		.map(e => winData[e])
		.reduce((p, c) => p + c, 0);
	return data;
};

/**
 * @param {string} d 
 */
export const part2 = async d => {
	const winData = {
		'A X': 3 + 0,
		'A Y': 1 + 3,
		'A Z': 2 + 6,
		'B X': 1 + 0,
		'B Y': 2 + 3,
		'B Z': 3 + 6,
		'C X': 2 + 0,
		'C Y': 3 + 3,
		'C Z': 1 + 6,
	};
	const data = d.split('\n')
		.map(e => winData[e])
		.reduce((p, c) => p + c, 0);
	return data;
};

So I couldn’t rest. I decided to do Day 2 as a formula instead of lookup. Not faster. But here are the Common Lisp one-liners…


(defun part1 (f)
  (apply #'+                    ; add scores up
	 (mapcar #'(lambda (x)  ; covert moves into a list of scores
		     (1+ (+ (cdr x) (* (mod (1+ (- (cdr x) (car x))) 3) 3))))
		 (mapcar #'(lambda (s) ; convert strings (e.g. "A X") into cons (e.g. (0 . 0))
			     (cons
			      (- (char-code (char s 0)) (char-code #\A))    ; CAR = opponents move
			      (- (char-code (char s 2)) (char-code #\X))))  ; CDR = my move
			 (uiop:read-file-lines f)))))  ; get list of moves as strings (e.g. "X Y")


(defun part2 (f)
  (apply #'+                    ; add scores up
	 (mapcar #'(lambda (x)  ; covert moves into a list of scores
		     (+ (* (cdr x) 3) (1+ (mod (+ (car x) (cdr x) 2) 3))))
		 (mapcar #'(lambda (s) ; convert strings (e.g. "A X") into cons (e.g. (0 . 0))
			     (cons
			      (- (char-code (char s 0)) (char-code #\A))    ; opponents move
			      (- (char-code (char s 2)) (char-code #\X))))  ; my move
			 (uiop:read-file-lines f)))))  ; get list of moves as strings (e.g. "X Y")

Day 3 is about finding a letter in common with sets of letters, and then mapping that letter to a priority, with lowercase being 1-26 and uppercase being 27-52.

For part 1 you need to split the strings in half, which should be pretty easy in most languages. I suppose technically you could not do the split in some languages like C. In any case the general approach is to run over all the letters in the first part and look for that same letter in the second part. Since you are told there is only one letter that appears in both, you can stop as soon as you find that one letter.

Here’s my code for part 1:

final class Day03
{
  public static long part1(String[] day03InputLines)
  {
    long sum=0;
    for (final String items : day03InputLines)
    {
      final String item1 = items.substring(0, items.length()/2);
      final String item2 = items.substring(items.length()/2);
      if (item1.length() != item2.length())  throw new IllegalStateException("Items aren't the same length: " + item1 + " " + item2);
      final char charInCommon = findCharInCommon(item1, item2);
      final int priority;
      if (charInCommon > 96)
      {
        priority = (charInCommon - 'a') + 1;
      }
      else
      {
        priority = (charInCommon - 'A') + 27;
      }
      sum += priority;
    }

    return sum;
  }

  private static char findCharInCommon(final String item1, final String item2)
  {
    for (final char c: item1.toCharArray())
    {
      if (item2.contains("" + c))
      {
        return c;
      }
    }
    throw new IllegalStateException("Common char not found: " + item1 +", " + item2);
  }

For part 2, you now need to process the input in a different way, taking groups of three. If you have all the input in one big array, this is very straightforward, you just need a for loop that goes up by threes. Finding the letter in common among all three is basically the same as in part 1, but now you need to look at two other items to see if the letter is present, and as soon as you find one, you’re again done.

Here’s my code for part 2:

  public static long part2(String[] day03InputLines)
  {
    long sum=0;
    for (int i=0; i<day03InputLines.length; i+=3)
    {
      final char charInCommon = findCharInCommon2(day03InputLines[i], day03InputLines[i+1], day03InputLines[i+2]);
      final int priority;
      if (charInCommon > 96)
      {
        priority = (charInCommon - 'a') + 1;
      }
      else
      {
        priority = (charInCommon - 'A') + 27;
      }
      sum += priority;
    }
    return sum;
  }

  private static char findCharInCommon2(final String item1, final String item2, final String item3)
  {
    for (final char c: item1.toCharArray())
    {
      if (item2.contains("" + c) && item3.contains("" + c))
      {
        return c;
      }
    }
    throw new IllegalStateException("Common char not found: " + item1 +", " + item2 + ", " + item3);
  }

Today’s seemed a little easier than yesterday. At least the first half was. Maybe it was not having to parse the instructions for rock/paper/scissors!

As usual my code is on Github

1 Like

I felt like I spent to much time reading the opening paragraphs of Day3 part1. In post analysis (and one Manhattan later) I feel I could probably just start coding by start reading at the example data. eg:

For example, suppose you have the following list of contents from six rucksacks:

vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw
The first rucksack contains the items vJrwpWtwJgWrhcsFMMfFFhFp, which means its first compartment contains the items vJrwpWtwJgWr, while the second compartment contains the items hcsFMMfFFhFp. The only item type that appears in both compartments is lowercase p.
The second rucksack's compartments contain jqHRNqRjqzjGDLGL and rsFMfFZSrLrFZsSL. The only item type that appears in both compartments is uppercase L.
The third rucksack's compartments contain PmmdzqPrV and vPwwTWBwg; the only common item type is uppercase P.
The fourth rucksack's compartments only share item type v.
The fifth rucksack's compartments only share item type t.
The sixth rucksack's compartments only share item type s.
To help prioritize item rearrangement, every item type can be converted to a priority:

Lowercase item types a through z have priorities 1 through 26.
Uppercase item types A through Z have priorities 27 through 52.
In the above example, the priority of the item type that appears in both compartments of each rucksack is 16 (p), 38 (L), 42 (P), 22 (v), 20 (t), and 19 (s); the sum of these is 157.

Find the item type that appears in both compartments of each rucksack. What is the sum of the priorities of those item types?

Yeah I scan ahead. In this case, you would miss that you need to split the lines in half, I had to read back to find that.

Day3 Part1:

15    my @a2Z = ('!','a'..'z','A'..'Z');
 16    my $sum = 0;
 17    if (open my $F, '<', $input) {
 18       while (my $line = <$F>) {
 19          my %front;
 20          my %back;
 21          chomp $line;
 22          my @chars = split(//, $line);
 23          my $later_index = scalar @chars /2;
 24          for (my $i=0; $i<$later_index; $i++) {
 25             $front{$chars[$i]}++;
 26          }
 27          ABL: for (my $i=$later_index; $i<($later_index *2); $i++) {
 28             my $char = $chars[$i];
 29             if (exists $front{$char}) {
 30                # found matching char.
 31                for (my $j=1; $j<53; $j++) {
 32                   if ($char eq $a2Z[$j]) {
 33                      my $priority = $j;
 34                      $sum += $priority;
 35                      last ABL;
 36                   }
 37                }
 38             }
 39          }

Day 3 was a day I shouldn’t have tried to force using chain functions. Part 1 is VERY ugly as a result.

And since I did it last night (spent around 40 minutes), I ended up adding comments! Mainly for those who want to figure out what each bit does because chaining functions is not easy to follow if you didn’t write it. At least not the way I wrote the code.

GitHub Linky

Reading here, I missed the line about only one item being the same in the groups… I may have been able to write cleaner code if I caught that.

It is interesting that if you read the source of the AoC webpage you see:

If you're curious about how Advent of Code works, it's running on some custom
Perl code.

Since I’m coding my solutions via Perl, I’m reviewing all the Perl string functions…

Day 4 is two ranges of numbers and you need to detect overlapping.

After you split and parse the input into numeric ranges, there are different ways to proceed. I chose to make a simple structure with start and end for each range and pass two of those in to a function to decide if they overlapped in the necessary way.

Here’s my part 1 code:

final class Day04
{
  record ElfSections(int start, int end) {};
  
  public static long part1(final String[] day04InputLines)
  {
    int count = 0;
    for (final String s : day04InputLines)
    {
      final String[] parts = s.split(",");
      final ElfSections elf1 = getElfSections(parts[0]);
      final ElfSections elf2 = getElfSections(parts[1]);
      
      if (oneElfSectionFullyContainsAnother(elf1, elf2))
      {
        count++;
      }
    }
    return count;
  }

  private static final Pattern elfSectionPattern = Pattern.compile("([0-9]+)[-]([0-9]+)");
  private static ElfSections getElfSections(final String sectionSpec)
  {
    final Matcher m = elfSectionPattern.matcher(sectionSpec);
    if (!m.matches())  throw new IllegalStateException("Matcher fails: " + sectionSpec);
    final int start = Integer.parseInt(m.group(1));
    final int end   = Integer.parseInt(m.group(2));
    return new ElfSections(start, end);
  }

  private static boolean oneElfSectionFullyContainsAnother(final ElfSections elf1, final ElfSections elf2)
  {
    if ((elf1.start <= elf2.start) && (elf1.end >= elf2.end)) return true;
    if ((elf2.start <= elf1.start) && (elf2.end >= elf1.end)) return true;
    return false;
  }

For part 2, you need to check for overlap differently. For my approach, I figured a set was easiest. Fill the set with the numbers in the first range, check if any of the numbers in the second range are in the set.

Here’s my code for part 2:

  public static long part2(final String[] day04InputLines)
  {
    int count = 0;
    for (final String s : day04InputLines)
    {
      final String[] parts = s.split(",");
      final ElfSections elf1 = getElfSections(parts[0]);
      final ElfSections elf2 = getElfSections(parts[1]);
      
      if (oneElfSectionOverlapsAnother(elf1, elf2))
      {
        count++;
      }
    }
    return count;
  }

  private static boolean oneElfSectionOverlapsAnother(final ElfSections elf1, final ElfSections elf2)
  {
    final Set<Integer> sectionNumbers = new HashSet<>();

    for (int i=elf1.start; i<= elf1.end; i++)
      sectionNumbers.add(i);

    for (int i=elf2.start; i<= elf2.end; i++)
      if (sectionNumbers.contains(i))  return true;

    return false;
  }

Again with the Lisp. I feel like this is much easier so far than last year. Last year we were already solving Bingo in Day 4. Not sure if that bodes well for day 5.

All this required was a simple regular expression and sets. Only point of note is the omnibus regex function register-groups-bind from the cl-ppcre library. It assigns the regex grouping to variables and then processes them all in one command.

I’m starting to think of functional coding as building little machines, black boxes, that spit out a result. register-groups-bind is a perfect example.