I'm working on day 1 of advent of code 2023 and it requires me to loop through a string, find the first and last numbers characters of the string, and combine them.
So for example if I had the string "x1dsdf425dk79dd" the result should be 19. The code I have below works, but it's pretty hacky. I was wondering if there's a better way to loop through a string. I also don't like how I did char = Result.withDefault (Str.fromUtf8 [byte]) ""
to get the character, but it didn't seem like there was any other way to do it.
firstAndLastDigits = \s ->
{first, last} = Str.walkUtf8 s { first: "", last: "" } \state, byte ->
char = Result.withDefault (Str.fromUtf8 [byte]) ""
isNumber = Result.isOk(Str.toU8 char)
if Bool.and (Str.isEmpty state.first) isNumber then
{ first: char, last: char }
else if isNumber then
{ state & last: char }
else
state
Str.concat first last |> Str.toU64 |> Result.withDefault 0
For advent of code, I would generally advise using List U8
as your default data type instead of string
For anything pure ascii with lots of character based manipulation (like advent of code) that is generally better.
Also, there is #Advent of Code > 2023 Day 1 with discussion on this problem and multiple solutions
Also for advent of code, where you really don't care to handle errors, I prefer to write an unwrap helper function:
unwrap = \res ->
when res is
Ok v -> v
Err e -> crash e
Sweet, thanks for the quick reply! I'll take a look at that thread.
Oh, actually, unwrap should be:
unwrap = \res ->
when res is
Ok v -> v
Err e -> crash (Inspect.toStr e)
I would encourage using AoC problems as a good way to learn proper error handling in Roc. It takes a bit of a mind shift, but once you wrap your head around it, it's really nice!
Thanks for all the help! This is much cleaner!
I avoided crashing for the extra challenge.
module []
import "example1.txt" as example1 : Str
import "example2.txt" as example2 : Str
import "real.txt" as real : Str
isDigit = \b ->
b == '0' ||
b == '1' ||
b == '2' ||
b == '3' ||
b == '4' ||
b == '5' ||
b == '6' ||
b == '7' ||
b == '8' ||
b == '9'
firstAndLastDigits = \s ->
numbers = s
|> Str.toUtf8
|> List.keepIf isDigit
|> List.keepOks (\x -> [x] |> Str.fromUtf8)
first = List.first numbers
last = List.last numbers
when (first, last) is
(Ok f, Ok l) -> Str.toU64 "$(f)$(l)"
_ ->
dbg "\"$(s)\" has no numbers???"
Err NoNumberChars
solution1 = \fileContents ->
fileContents
|> Str.split "\n"
|> List.keepOks firstAndLastDigits
|> List.sum
solution2 = \fileContents ->
fileContents
|> Str.split "\n"
|> List.map (\x -> Str.replaceEach x "one" "one1one")
|> List.map (\x -> Str.replaceEach x "two" "two2two")
|> List.map (\x -> Str.replaceEach x "three" "three3three")
|> List.map (\x -> Str.replaceEach x "four" "four4our")
|> List.map (\x -> Str.replaceEach x "five" "five5five")
|> List.map (\x -> Str.replaceEach x "six" "six6six")
|> List.map (\x -> Str.replaceEach x "seven" "seven7seven")
|> List.map (\x -> Str.replaceEach x "eight" "eight8eight")
|> List.map (\x -> Str.replaceEach x "nine" "nine9nine")
|> List.keepOks firstAndLastDigits
|> List.sum
expect solution1 example1 == 142
expect solution1 real == 54605
expect solution2 example2 == 281
expect solution2 real == 55429
Optionally, your isDigit
could be made a bit less error prone like so:
isDigit = \n -> n >= '0' && n <= '9'
Quick plug for my package roc-ascii which might be useful for AoC :)
@Hannes
What's the difference between Ascii and String in Roc?
Glancing at the package, it is a string type that assumes fixed-width (ascii) characters, whereas Roc builtin strings are utf8 encoded, which is a variable-length encoding. The big difference is that you can index into an ascii string (like in most of the languages) and can expect to get a valid ascii character, while the same can't be said to an utf8 string, cause you can index into the middle of a "character that actually spreads multiple bytes and shouldn't be called a character". Like emojies.
If you are sure you will not need to handle utf8 strings, using the ascii package will give you more functions on (ascii) strings than what is available in the builtin String module, since ascii doesn't have the limitation of variable-length encoding.
Couldn't have said it better myself Norbert! I might borrow some of that explanation for roc-ascii's readme!
Glad to help! Sure, do so!
Last updated: Jul 06 2025 at 12:14 UTC