Another day where the dumb way would have so much quicker and easier, but I'm not competing for time.
I decided to solve it numerically without regex or using to_string(), which was more taxing for the ol' grey matter but is perhaps fairly optimal (if I bothered to pre-compute all those pow() calls, anyway).
Part 2 runs in 35ms (on my AMD Ryzen 7 9800X3D), whereas the to_string() version runs in 40ms. So... not really worth it, and it's less readable.
Rust
use std::fs;
use color_eyre::eyre::{Result, bail};
type InvalidChecker = fn(usize) -> bool;
fn sum_invalids(input: &str, checkfn: InvalidChecker) -> Result<usize> {
let total = input
.trim()
.split(',')
.map(|idrange| {
if let Some((start, end)) = idrange.split_once('-') {
let mut sum = 0;
for n in start.parse::<usize>()?..=end.parse::<usize>()? {
if checkfn(n) {
sum += n;
}
}
Ok(sum)
} else {
bail!("Couldn't parse {idrange}")
}
})
.sum::<Result<usize, _>>()?;
Ok(total)
}
fn is_invalid_p1(n: usize) -> bool {
let len = n.ilog10() + 1;
// odd-length numbers can't repeat
if len % 2 == 1 {
return false;
}
let lhs = n / 10_usize.pow(len / 2);
let rhs = n - (lhs * 10_usize.pow(len / 2));
lhs == rhs
}
const SPANS: &[&[u32]] = &[
&[], // i = 0
&[], // i = 1
&[1], // i = 2
&[1], // i = 3
&[1, 2], // i = 4
&[1], // i = 5
&[1, 2, 3], // i = 6
&[1], // i = 7
&[1, 2, 4], // i = 8
&[1, 3], // i = 9
&[1, 2, 5], // i = 10
&[1], // i = 11
&[1, 2, 3, 4, 6], // i = 12
];
fn is_invalid_p2(n: usize) -> bool {
let len = n.ilog10() + 1;
// 1-length numbers can't repeat
if len == 1 {
return false;
}
SPANS[len as usize].iter().any(|&span| {
let lhs = n / 10_usize.pow(len - span);
let mut remainder = n;
let mut rhs = lhs;
(2..=(len / span)).all(|i| {
remainder -= rhs * 10_usize.pow(len - (i - 1) * span);
rhs = remainder / 10_usize.pow(len - i * span);
lhs == rhs
})
})
}
fn part1(filepath: &str) -> Result<usize> {
let input = fs::read_to_string(filepath)?;
let res = sum_invalids(&input, is_invalid_p1)?;
Ok(res)
}
fn part2(filepath: &str) -> Result<usize> {
let input = fs::read_to_string(filepath)?;
let res = sum_invalids(&input, is_invalid_p2)?;
Ok(res)
}
to_string version:
fn is_invalid_p2(n: usize) -> bool {
let s = n.to_string();
let len = s.len();
// 1-length numbers can't repeat
if len == 1 {
return false;
}
SPANS[len].iter().any(|&span| {
let span = span as usize;
let lhs = &s[0..span].as_bytes();
s.as_bytes().chunks(span).all(|rhs| *lhs == rhs)
})
}

Sounds amazing, I'll have to keep an eye out for rhubarb amaro. Growing up, we had rhubarb growing in the garden, but I almost never taste it nowadays.