Kickoff Coding Challenge: Advent Of Code 2023 With C# Day 1 -- Part 1

Dive into the First Day of Advent of Code 2023: Part 1 Using C#

·

5 min read

Advent Of Code is a coding event that happens every December. From December 1st to December 25th, there's a Leetcode-style puzzle every day. Each puzzle has two parts, with the second part often being more challenging than the first. I'll share my thought process and solutions for each problem using C#, as if I'm answering a question in a job interview. I welcome advice or comments on how to make my solutions better!

Due to copyright reasons, I won't directly copy any of the question text or input from the website. However, I will explain the problem in my own words and use a simple example to illustrate the requirements and expected results.

Day 1 --Part 1

Question:

You are given a bunch of text with multiple lines, where each line is consisting of at least one digit and alphabets. For each line, we will have to combine the first-appearing digit, and the last-appearing digit to form a 2 digit number, and our goal is to find the sum of all of the numbers.

// Example Input
1osafdij23   ----> this should give us the number 13
o3ia4sjdf0n2 ----> this should give us the number 32
fasdf9l      ----> this should give us the number 99
oas3ij0df3a68psd4f  ----> this should give us the number 34

The expected outcome would be 13 + 32 + 99 + 34 = 178

Thought Process

  1. From the example, we notice that an edge case occurs when there's only one digit in the line. In such cases, we treat this single digit as both the tens and units place of the two-digit number we're calculating.

  2. For each line, we'll likely need to loop through each character, using a specific data structure to keep track of the first and last digit we encounter.

  3. After identifying the first and last number in each line, we'll multiply the first digit by 10 and add the last digit to get the line's result.

  4. The final result will be the sum of the results from each line, completing our calculation.

Solution

First, I would like to split the input string by the newline character, so we can then use a foreach loop for each of the line.

public void Main()
{
    string sampleInput = @"1osafdij23
o3ia4sjdf0n2
fasdf9l      
oas3ij0df3a68psd4f";

    string[] splitInput = sampleInput.Split(new string[] { Environment.NewLine },
        StringSplitOptions.None);
}

Then, I would create a method to calculate the result for each line. How will I identify the first and last digit? I plan to use two indices within a for loop.

There will be two indices starting from 0 and the end of the line. The tenthDigitIndex will move to the right, while the unitDigitIndex will move to the left. They will stop when they encounter a number. This method allows us to find both numbers quickly. Then, we will also need two flags to indicate when to stop moving through the line for each index.

public int CalculateLineResult(string lineInput)
{
        int result = 0;
        int tenthDigitIndex = 0;
        int unitDigitIndex = lineInput.Length - 1;
        bool stopFindingTenthDigit = false;
        bool stopFindingUnitDigit = false;

        while (!stopFindingTenthDigit)
        {
            char currentCharacter = lineInput[tenthDigitIndex];

            if (!char.IsDigit(currentCharacter))
                tenthDigitIndex++;
            else
            {
                var tenthDigit = int.Parse(currentCharacter.ToString());
                result += tenthDigit * 10;

                stopFindingTenthDigit = true;
            }
        }

        while (!stopFindingUnitDigit)
        {
            char currentCharacter = lineInput[unitDigitIndex];

            if (!char.IsDigit(currentCharacter))
                unitDigitIndex--;
            else
            {
                var unitDigit = int.Parse(currentCharacter.ToString());
                result += unitDigit;

                stopFindingUnitDigit = true;
            }
        }

        return result;
}

Finally at the main method, we can call the CalculateLineResult for each split result, and log the answer to the console.

public void Main()
{
    // previous code
    int finalResult = 0;

    foreach(var line in splitResult)
        finalResult += CalculateLineResult(line);

    Console.WriteLine(finalResult);
    Console.ReadKey();
}

Refactoring with LINQ

However, as you can see, the code in the CalculateLineResult method is a bit messy. It's long and not very easy to read, which isn't ideal. The good news is, we can try to refactor the method by using LINQ!

public int CalculateLineResultWithLinq(string lineInput)
{  
    // Tips :
    // If we are performing a function on each member of the collection, 
    // instead of doing lineInput.Where(character => char.IsDigit(character)),
    // we can simply pass in the function to the where clause! 
    var digitsInLine = lineInput.Where(char.IsDigit);

    int unitDigit = int.Parse(digitsInLine.Last().ToString());
    int tenthDigit = int.Parse(digitsInLine.First().ToString());

    return tenthDigit * 10 + unitDigit;
}
public void Main()
{
    // Previous code
    // Replace the foreach loop with this LINQ expression
    int result = splitInput.Select(CalculateLineResultWithLinq).Sum();

    Console.WriteLine(result);
    Console.ReadKey();
}

Performance

Using LINQ makes our method looks much cleaner! However, does that make our method equally as fast? Let's run some benchmark tests on the CalulateLineResult method to find out!

For the benchmark test, I used the short input mentioned earlier and the long input, which is the actual question input. For the short input, CalculateLineResult takes only 217.12 ns (1 nanosecond = 1 x 10^-9 second) on average, while CalculateLineResultWithLinq takes 825.94 ns on average, almost 3 times slower! With the long input, the gap widens: CalculateLineResult averages 96.91 us(1 microsecond = 1 x 10^-6 second), whereas CalculateLineResultWithLinq averages 287.92 us . This significant difference cannot be overlooked!

Why is this the case? In CalculateLineResult, we have the ability to decide when to stop looping through the string, which isn't possible with CalculateLineResultWithLinq. The only situation where CalculateLineResult must loop through every character is when there's only one digit, and this isn't always the case. That's why CalculateLineResult performs much better than CalculateLineResultWithLinq! This teaches us a crucial lesson: more readable code doesn't always mean better performance!

That's all for Day 1 Part 1. We will continue to talk about Day 1 Part 2 in the next article. Stay tuned!

Resources:

  1. Language Integrated Query (LINQ) in C# - C# | Microsoft Learn