DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
In this post I'll walk through my solution to AOC 2025 Day 2 in C#.
Utilities for Result and Option. I decided I wanted a few utilities to code more like I wanted so reached for OneOf and built a simple version of each.
I also realized I wanted some EnumerableExtensions in the process of solving these:
using OneOf;
public record Success<T>(T Value);
public record Failure<T>(T Error);
public class Result<TSuccess, TError> : OneOfBase<Success<TSuccess>, Failure<TError>>
{
private Result(OneOf<Success<TSuccess>, Failure<TError>> input) : base(input) { }
public static implicit operator Result<TSuccess, TError>(Success<TSuccess> success) => new(success);
public static implicit operator Result<TSuccess, TError>(Failure<TError> failure) => new(failure);
}
public record Some<T>(T Value);
public record None;
public class Option<T> : OneOfBase<Some<T>, None>
{
private Option(OneOf<Some<T>, None> input) : base(input) { }
public static implicit operator Option<T>(Some<T> some) => new(some);
public static implicit operator Option<T>(None none) => new(none);
}
public static class EnumerableExtensions
{
public static IEnumerable<long> LongRange(long start, long count)
{
for (long i = 0; i < count; i++)
yield return start + i;
}
public static IEnumerable<int> RangeStep(int start, int end, int step)
{
for (int i = start; i <= end; i += step)
yield return i;
}
}
The logic and my notes:
Context:
* Get a list of ranges that may contain bad product ids
* START-END
* bad product ids can be identified by going through the range and finding numbers that are repeated 55, 6464
Goal:
* Find the sum of all the bad numbers
Sub problems:
* A: Identify bad numbers
* 1: Turn int to string, use two pointers to see if same
* if odd length -> no
* if even try
* two pointers across
* B: How to iterate ranges
* 1: Brute force
* Turn start, end to ints
* Iterate from start to end
* See if numbers are bad or not
Solutions:
* A: Iterate ranges, check for bad - O(ranges * range * check)
* Iterate ranges - O(ranges)
* go through range - O(range)
* check number
* if bad, add to accumulator - O(check)
Code:
public static long Day2Part1(string[] lines)
{
return lines
.Select(l => l.Split("-").Select(n => long.Parse(n)).ToList())
.Select(numbers => new NumberRange(Start: numbers[0], End: numbers[1]))
.Aggregate(0L, (acc, range) =>
acc + EnumerableExtensions.LongRange(range.Start, range.End - range.Start + 1)
.Select(n => ParseBadNumber(n))
.Select(option => option.Match(some => some.Value, none => 0))
.Sum()
);
}
public static Option<long> ParseBadNumber(long inputNumber)
{
var numberString = inputNumber.ToString();
if(numberString.Length % 2 != 0)
{
return new None();
}
var midIndex = numberString.Length / 2;
for(var i = 0; i < midIndex; i++)
{
var firstNumber = numberString[i];
var secondNumber = numberString[i + midIndex];
if(firstNumber != secondNumber)
{
return new None();
}
}
return new Some<long>(inputNumber);
}
My notes:
Part 2:
* Bad numbers are now any number where they are created from a sequence of digits repeated at least twice
* So parseBadNumber makes sense BUT it's not always split in half - it may be more
Subproblems:
* A: Is this a bad number
* 0: Brute force matching
* Foreach substring - check if it matches the other substrings
* length 1 to length n / 2 - grab substring
* See if the rest matches it
* Optimization - only do this if the length is divisible by this substring size - else can't work
* 1: Loop over numbers the number is divisible by, check substrings against each other
Code:
public static long Day2Part2(string[] lines)
{
return lines
.Select(l => l.Split("-").Select(n => long.Parse(n)).ToList())
.Select(numbers => new NumberRange(Start: numbers[0], End: numbers[1]))
.Aggregate(0L, (acc, range) =>
acc + EnumerableExtensions.LongRange(range.Start, range.End - range.Start + 1)
.Select(n => ParseBadNumber2(n))
.Select(option => option.Match(some => some.Value, none => 0))
.Sum()
);
}
public static Option<long> ParseBadNumber2(long inputNumber)
{
var numberString = inputNumber.ToString();
var length = numberString.Length;
if(length <= 1) return new None();
var divisibleLengths = Enumerable.Range(1, length-1)
.Where(n => length % n == 0 && n < length)
.ToList();
var isBad = divisibleLengths.Any(divisibleLength =>
EnumerableExtensions.RangeStep(0, length - 1, divisibleLength)
.Select(start => numberString.Substring(start, divisibleLength))
.Distinct()
.Count() == 1
);
return isBad
? new Some<long>(inputNumber)
: new None();
}
C# wasn't so bad. It was a little clunky and I had to build out some core primitives to code in the way I want but this code really isn't that different than what I might do in F#.
If you liked this post you might also like :