Advent of Code 2023 - Trebuchet?! (Day 1, Part 2)

Advent of Code 2023 - Trebuchet?! (Day 1, Part 2)

Having successfully solved Part 1 of Day 1 in the Advent of Code, I was greeted with an intriguing, albeit not overly complex, twist in Part 2. This new element in the puzzle presented a unique challenge: the calibration document's digits were now occasionally spelt out in words such as 'one', 'two', 'three', and so on. While this twist didn't drastically increase the difficulty level, it certainly introduced a variety of edge cases that demanded careful consideration and a nuanced approach.

The challenge now was to enhance my existing Ruby solution to handle these textual digits. This adaptation wasn't just about addressing a straightforward change but diving into the subtleties and edge cases that such a modification entailed. It required a detailed re-examination of the problem, a keen understanding of potential pitfalls, and thoughtful tweaks to the existing code.

In this post, I'll take you through my process of adapting to this new scenario. I'll discuss my modifications to my original solution, explain how I navigated through the various edge cases, and share the insights I gained from this experience. This part of the Advent of Code journey offers a glimpse into the nuanced programming world, where sometimes the smallest changes can lead to interesting new challenges.

Problem Description

Part 2 of Day 1's challenge introduces us to a clever linguistic twist to the existing problem. The calibration document, previously thought to contain only numerical digits, now had some of these digits spelt out as words. This development necessitated a more nuanced approach to parsing the document.

Understanding the New Requirement

  • Digits as Words: The task was no longer just about identifying numerical characters. Words like 'one', 'two', 'three', and so on, scattered throughout the text, were also valid digits. For instance, a line like 'two1nine' should now be interpreted as containing the digits '2', '1', and '9'.
  • The Real First and Last Digit: The challenge was to accurately find the real first and last digit on each line, considering both numerical digits and spelt-out numbers, and then combining them to form a calibration value.

To understand the challenge better, consider these example lines:

  1. two1nine – The calibration value is 29.
  2. eightwothree – The calibration value is 83.
  3. abcone2threexyz – The calibration value is 13.

The Goal

The ultimate goal remained the same as in Part 1: to sum all the calibration values obtained from each line of the document. However, the added complexity of deciphering digits from words significantly altered the approach needed for the task.

Revised Solution Strategy

The introduction of spelt-out numbers required a significant adaptation of my initial solution. This new problem dimension called for a blend of string parsing and number conversion techniques.

Key Aspects of the Modified Approach

  • Dual Identification Process: The solution needed to account for both numerical digits and numbers spelt out as words. This dual identification process involved scanning each line for both types of digits and accurately extracting their numerical equivalents.
  • Word-to-Number Conversion: A crucial part of the strategy was to develop a method for converting words like 'one', 'two', 'three', etc., into their corresponding numerical values. This conversion required a mapping mechanism to transform these words into digits.
  • Robust Parsing Mechanism: The solution had to robustly parse each line to identify the first and last digit, whether they appeared as numerals or words. This parsing mechanism had to handle various combinations and positions of digits within the text.

Implementing the Strategy

  • Handling Edge Cases: Special attention was given to edge cases, such as lines where the first or last digit was a word, or lines with a mixture of numeric digits and words. The solution had to be flexible enough to handle these scenarios without errors.
  • Efficient Processing: Despite the added complexity, it was important to maintain efficiency in processing each line of the document. The solution aimed to minimize unnecessary computations and string manipulations.

Leveraging Ruby's Capabilities

  • Utilizing Ruby's Array and String Methods: The revised strategy extensively used Ruby's powerful array and string processing methods. These methods facilitated efficient parsing and transformation of the text lines.
  • Custom Methods for Digit Extraction: Custom methods were developed to extract the first and last digits (numeric or spelt out) from each line, demonstrating Ruby's versatility in handling diverse data manipulation tasks.

Code Modifications and Walkthrough

In addressing the unique requirements of Part 2, I modified my code to handle both numerical digits and their spelt-out counterparts. Let's break down the key components of this adapted solution.

The Ruby Solution Explained

NUMBERS=%w(one two three four five six seven eight nine ten)

def get_left_number(line)
    line_copy = line.dup
    while line_copy.length > 0
        return line_copy[0].to_i if line_copy[0].to_i > 0

        found_number = NUMBERS.find { |number| line_copy.start_with?(number) }
        return NUMBERS.index(found_number) + 1 if found_number

        line_copy = line_copy[1..-1]
    end
    return nil
end

def get_right_number(line)
    line_copy = line.dup
    while line_copy.length > 0
        return line_copy[-1].to_i if line_copy[-1].to_i > 0

        found_number = NUMBERS.find { |number| line_copy.end_with?(number) }
        return NUMBERS.index(found_number) + 1 if found_number

        line_copy = line_copy[0..-2]
    end
    return nil
end

result = File.open("../input.txt").readlines.map(&:chomp).inject(0) do |sum, line|
    left_number = get_left_number(line)
    right_number = get_right_number(line)
    sum += "#{left_number}#{right_number}".to_i
end

# Print the sum of all the sums
puts result
  • Defining Textual Digits:
    • NUMBERS=%w(one two three four five six seven eight nine ten): This constant holds the textual representations of digits, facilitating the conversion of words to numbers.
  • Extracting the First (Left) Number:
    • get_left_number(line): This method iterates over each character of the line from left to right. It checks for numerical digits and matches substrings against the NUMBERS array. Once it identifies a valid number (digit or word), it returns its numerical value.
  • Extracting the Last (Right) Number:
    • get_right_number(line): A mirror of get_left_number, but it processes the line from right to left. It identifies the last digit or spelled-out number and returns its numerical equivalent.
  • Parsing and Summing the Calibration Values:
    • In the main loop, each line is processed to find the first and last numbers. These are then concatenated into a two-digit number and added to the cumulative sum.

Breakdown of Key Methods

  • get_left_number and get_right_number:
    • These methods demonstrate an effective use of Ruby's string manipulation capabilities. By iteratively checking each part of the string and comparing it with the known textual digits, they can accurately extract the needed numbers.
  • Efficiency in Processing:
    • The code efficiently handles each line, ensuring it correctly identifies numbers regardless of format. The use of Ruby's array and string methods allows this process to be concise and clear.

Handling Edge Cases

  • The solution was carefully crafted to handle various edge cases, such as lines with only one digit (numerical or spelt out) or lines with a mix of both formats. This robustness ensures the accuracy of the sum calculation, regardless of the input's complexity.

Example Execution Flow

Let's correct the execution flow to accurately reflect how the adapted code handles the new inputs, considering the limitation that only single-digit words (one through nine) are recognized.

Sample Inputs and Their Processing

  • Processing Line 1: two1nine
    • get_left_number identifies 'two' as 2.
    • get_right_number finds 'nine' as 9.
    • The calibration value is 29.
  • Processing Line 2: eightwothree
    • Identifies 'eight' as 8 and 'three' as 3.
    • The calibration value is 83.
  • Processing Line 3: abcone2threexyz
    • Finds 'one' as 1 and 'three' as 3.
    • The calibration value is 13.
  • Processing Line 4: xtwone3four
    • Identifies 'two' as 2 and 'four' as 4.
    • The calibration value is 24.
  • Processing Line 5: 4nineeightseven2
    • Finds 4 and 2 as the digits.
    • The calibration value is 42.
  • Processing Line 6: zoneight234
    • Identifies 'one' as 1 and 4.
    • The calibration value is 14.
  • Processing Line 7: 7pqrstsixteen
    • Identifies 7 and 'six' as 6 (not 'sixteen').
    • The calibration value is 76.

Calculating the Final Sum

  • The sum of the calibration values from these lines is 29 + 83 + 13 + 24 + 42 + 14 + 76 = 281.

Observations on the Execution Flow

The corrected execution flow demonstrates how the code handles the combination of numerical digits and spelt-out single-digit words in the calibration document. While this approach does not capture multi-digit words like 'sixteen' (which is fine because it was not a requirement), it efficiently processes the given inputs and calculates the total sum based on the recognized patterns.

Reflection on the Solution

Having navigated through the nuanced challenge of Part 2, it's valuable to reflect on the experience and the lessons learned from adapting the solution to meet the new requirements.

Adapting to New Challenges

  • Complexity in Simplicity: While the twist in Part 2 wasn't overly complex, it introduced a layer of detail that required careful consideration. This experience underlines the importance of paying attention to subtleties in programming challenges.
  • Tackling Edge Cases: The limitation in recognizing only single-digit words and not multi-digit words like 'sixteen' highlighted the importance of considering and handling edge cases in coding solutions.

Insights Gained

  • Flexibility in Problem-Solving: The task reinforced the value of being adaptable in problem-solving. Modifying an existing solution to accommodate new rules is a crucial skill in software development.
  • Understanding Language Limitations: This challenge also sheds light on understanding and working within the limitations of the programming language and the chosen approach.

The Importance of Robust Testing

  • Testing for Accuracy: The process underscored the importance of thorough testing, especially when adapting solutions to new scenarios. Testing is key to ensuring that the solution works correctly for various inputs, including edge cases.

Conclusion

As I wrap up the second part of Day 1's challenge in the Advent of Code, it's clear that each puzzle is not just a test of programming skills but a journey of learning and adaptation. This foray into the world of coding challenges has been as much about thinking creatively and adapting swiftly as it has been about applying technical knowledge.

The transition from part 1 to part 2 was a poignant reminder of the fluid nature of problem-solving in software development. Adapting the solution to accommodate spelt-out numbers reinforced the importance of flexibility in programming. It was a compelling exercise in refining code and expanding my approach to address unexpected nuances of a problem.