Hey guys! Ready for another coding challenge? Today, we're diving into Advent of Code 2023, Day 1, Part 2. If you tackled Part 1, you're already warmed up. If not, no worries, we’ll get you up to speed. This challenge involves a bit of string manipulation and pattern recognition, perfect for honing those coding skills. So, grab your favorite text editor or IDE, and let’s get started!

    Understanding the Challenge

    In this Advent of Code challenge, the task is to extract calibration values from a series of lines. Each line contains a mix of digits and spelled-out numbers (e.g., "one", "two", "three"). The goal is to find the first and last digit (or number word) in each line and combine them to form a two-digit number. The sum of these two-digit numbers across all lines is the final answer we need to find. The tricky part? We need to consider both numerical digits (1, 2, 3, etc.) and their text representations.

    For example, if a line reads "two1nine", the first digit is "2" (from "two") and the last digit is "9" (from "nine"). Combining these gives us 29. Similarly, for a line like "abcone2threexyz", the first digit is "1" (from "one") and the last digit is "3" (from "three"), resulting in 13. The challenge is to automate this process for a large set of input lines and calculate the final sum.

    To break it down further, consider these examples:

    • "eightwothree" should be interpreted as 83 (eight and three).
    • "abcone2threexyz" gives us 13 (one and three).
    • "xtwone3four" results in 24 (two and four – note the overlapping "two" and "one").
    • "4nineeightseven2" leads to 42 (4 and 2).
    • "7pqrstsixteen" becomes 76 (7 and six).

    The key here is to correctly identify the first and last numerical values, whether they are represented as digits or spelled out, and to handle potential overlaps or tricky combinations within the input strings.

    Breaking Down the Solution

    Let's outline a plan to tackle this problem step by step. We'll break the solution into manageable parts, making the coding process smoother and easier to understand. Here’s what we’ll do:

    1. Read the Input: First, we need to read the input data, which consists of multiple lines of text. Each line will be processed individually to extract the calibration value.
    2. Identify Digits and Number Words: For each line, we need to find both numerical digits (0-9) and spelled-out numbers ("one", "two", ..., "nine"). We'll create a mechanism to recognize these patterns within the text.
    3. Find the First and Last Occurrences: Once we can identify digits and number words, we need to find the first and last occurrences of these patterns in each line. This involves scanning the line from the beginning and end to locate the relevant values.
    4. Convert to Numerical Values: After identifying the first and last occurrences, we'll convert them to numerical values. For example, "one" becomes 1, "two" becomes 2, and so on. Numerical digits will remain as they are.
    5. Combine and Calculate: Finally, we'll combine the first and last numerical values to form a two-digit number. We'll sum these two-digit numbers across all lines to obtain the final calibration value.

    Step-by-Step Implementation

    Let's dive into each step with some code examples (using Python, but you can adapt it to your preferred language):

    1. Read the Input:

    We start by reading the input data. Assuming the input is stored in a file named input.txt, we can read it line by line:

    with open('input.txt', 'r') as file:
        lines = file.readlines()
    

    This code reads each line from the file and stores it in a list called lines. Each element of the list represents a line from the input.

    2. Identify Digits and Number Words:

    Next, we need to identify both numerical digits and spelled-out numbers. We'll create a dictionary to map number words to their numerical values:

    number_map = {
        'one': '1',
        'two': '2',
        'three': '3',
        'four': '4',
        'five': '5',
        'six': '6',
        'seven': '7',
        'eight': '8',
        'nine': '9'
    }
    

    This dictionary will help us convert number words to their corresponding numerical digits.

    3. Find the First and Last Occurrences:

    Now, we need to find the first and last occurrences of digits and number words in each line. We can iterate through the line and check for matches:

    def find_first_and_last(line, number_map):
        first_digit = None
        last_digit = None
        first_index = len(line)
        last_index = -1
    
        for word, digit in number_map.items():
            # Find first occurrence
            if word in line:
                first_occurrence = line.find(word)
                if first_occurrence != -1 and first_occurrence < first_index:
                    first_index = first_occurrence
                    first_digit = digit
    
                # Find last occurrence
                last_occurrence = line.rfind(word)
                if last_occurrence != -1 and last_occurrence > last_index:
                    last_index = last_occurrence
                    last_digit = digit
    
        # Check for numerical digits
        for i, char in enumerate(line):
            if char.isdigit():
                if i < first_index:
                    first_index = i
                    first_digit = char
                if i > last_index:
                    last_index = i
                    last_digit = char
    
        return first_digit, last_digit
    

    This function iterates through each number word and numerical digit, checking for its presence in the line. It keeps track of the first and last occurrences based on their indices.

    4. Convert to Numerical Values:

    This step is already integrated into the find_first_and_last function, as we use the number_map to directly obtain the numerical values of number words.

    5. Combine and Calculate:

    Finally, we combine the first and last numerical values to form a two-digit number and sum these numbers across all lines:

    def calculate_calibration_value(lines, number_map):
        total_sum = 0
        for line in lines:
            first_digit, last_digit = find_first_and_last(line, number_map)
            if first_digit and last_digit:
                calibration_value = int(first_digit + last_digit)
                total_sum += calibration_value
        return total_sum
    
    # Calculate the total sum
    total_sum = calculate_calibration_value(lines, number_map)
    print("Total Calibration Value:", total_sum)
    

    This code iterates through each line, calls the find_first_and_last function to extract the digits, combines them to form a two-digit number, and adds it to the total sum.

    Putting It All Together

    Here’s the complete code:

    number_map = {
        'one': '1',
        'two': '2',
        'three': '3',
        'four': '4',
        'five': '5',
        'six': '6',
        'seven': '7',
        'eight': '8',
        'nine': '9'
    }
    
    def find_first_and_last(line, number_map):
        first_digit = None
        last_digit = None
        first_index = len(line)
        last_index = -1
    
        for word, digit in number_map.items():
            # Find first occurrence
            if word in line:
                first_occurrence = line.find(word)
                if first_occurrence != -1 and first_occurrence < first_index:
                    first_index = first_occurrence
                    first_digit = digit
    
                # Find last occurrence
                last_occurrence = line.rfind(word)
                if last_occurrence != -1 and last_occurrence > last_index:
                    last_index = last_occurrence
                    last_digit = digit
    
        # Check for numerical digits
        for i, char in enumerate(line):
            if char.isdigit():
                if i < first_index:
                    first_index = i
                    first_digit = char
                if i > last_index:
                    last_index = i
                    last_digit = char
    
        return first_digit, last_digit
    
    def calculate_calibration_value(lines, number_map):
        total_sum = 0
        for line in lines:
            first_digit, last_digit = find_first_and_last(line, number_map)
            if first_digit and last_digit:
                calibration_value = int(first_digit + last_digit)
                total_sum += calibration_value
        return total_sum
    
    with open('input.txt', 'r') as file:
        lines = file.readlines()
    
    total_sum = calculate_calibration_value(lines, number_map)
    print("Total Calibration Value:", total_sum)
    

    Save this code as a .py file (e.g., advent_code_day1_part2.py) and run it. Make sure your input.txt file is in the same directory.

    Optimizing the Solution

    To make our solution even better, we can consider a few optimizations:

    • Regular Expressions: Instead of using find and rfind, we could use regular expressions to find all occurrences of digits and number words. This might simplify the code and potentially improve performance.
    • Precompile Number Map: For large inputs, precompiling the number_map can save time. This involves converting the dictionary into a more efficient data structure for lookups.
    • Handle Edge Cases: Ensure the code handles edge cases gracefully. For example, what happens if a line contains no digits or number words? Adding checks for these scenarios can make the solution more robust.

    Common Pitfalls and How to Avoid Them

    While working on this problem, you might encounter a few common pitfalls. Here’s how to avoid them:

    • Overlapping Number Words: Be careful with overlapping number words like "twone" (21) or "eightwo" (82). Ensure your code correctly identifies both numbers.
    • Incorrect Indices: Pay close attention to the indices when finding the first and last occurrences. An off-by-one error can lead to incorrect results.
    • Case Sensitivity: Ensure your code is case-insensitive when matching number words, or convert the input to a consistent case before processing.
    • Empty Lines: Handle empty lines in the input gracefully. They should not cause the code to crash or produce incorrect results.

    Conclusion

    And there you have it! We’ve successfully tackled Advent of Code 2023 Day 1 Part 2. By breaking the problem into smaller steps and carefully handling edge cases, we’ve created a robust and efficient solution. Keep practicing, and you’ll become a coding master in no time! Happy coding, and see you in the next challenge!