Pull numbers from text 🔥
Advent of Code puzzles often hide numbers inside text. Sometimes the input has a regular structure you can split into fields. Other times, you need to scan through the text, find the numbers yourself, and decide which ones actually matter to the puzzle.
In this chapter, you’ll return to the sensor log from Work with files and pull numbers directly from its text. You’ll scan for runs of digits, select values based on the shape of the input, and check your results against something you already know should be true. Then you’ll throw in a signed decimal and discover where a simple digit scan stops being the right tool for the job.
Scan for the numbers
Create pull_numbers_from_text.mojo. You can collect digit runs by walking
text a byte at a time. Add this function above main():
def extract_ints(text: String) raises -> List[Int]:
var nums = List[Int]()
var cur = String("")
for i in range(text.byte_length()):
var c = String(text[byte=i])
if c >= "0" and c <= "9": # an ASCII digit
cur += c # extend the current run
elif cur: # a run just ended
nums.append(Int(cur))
cur = String("")
if cur: # a run at the very end
nums.append(Int(cur))
return nums^
Takeaways
- The scan groups adjacent digits into runs.
20stays one number instead of a2and a0. extract_ints()raises becauseInt(text)can raise on bad input.- Return the newly constructed
Listwith^to transfer ownership of its data to the caller. String(text[byte=i])reads a single byte. Usingtext[i]is an error. The string constructor converts the byte back to a string. In ASCII, the bytes from 48 through 57 represent the digits 0 through 9.
Read the log and run the scan over the whole thing:
var log = Path("src/downloads/temp_log.txt")
var text = log.read_text()
var lines = text.splitlines()
print(text)
var found = extract_ints(text)
print(t"found: {found}")
# [1, 20, 5, 2, 22, 3, 3, 19, 8, 4, 25, 1, 5, 44, 5, 6, 27, 7, 7, 42, 3,
# 8, 28, 2, 9, 23, 0, 10, 36, 5]
Every day, temperature, and condition got flattened into one long run of integers, with no idea which numbers belong together.
Which numbers do you want?
You know the shape of each line: day number, whole-degree part, tenths digit, three numbers per line. Take every third value to pull out just the day numbers:
# Every line contributes three numbers: the day, the whole-degree part
# of the temperature, and the tenths digit. Take every 3rd value.
var day_numbers = List[Int]()
for i in range(0, len(found), 3):
day_numbers.append(found[i])
print(t"day numbers: {day_numbers}") # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Takeaways
range(0, len(found), 3)walks every third index. Extraction finds candidates; you pick the ones that matter based on the input’s shape.
Check your work
A log with ten entries should number its days 1 through 10, in order, with nothing missing or duplicated. Build the sequence you expect and compare:
var expected = [i for i in range(1, len(lines) + 1)]
print(t"complete, in order: {day_numbers == expected}") # True
Takeaways
- An invariant you can derive from the input, like a complete day count, gives you a free correctness check. Reach for it before you trust a parse.
- This is the same move on puzzle input: parse, then confirm against something the shape of the input already promises you.
Push it: a reading with a decimal
Digit-run scanning is effective for whole numbers, but not for values carrying a sign or a decimal point:
print(t"scanned: {extract_ints(String(lines[0]))}") # [1, 20, 5]
-20.5 comes back as [1, 20, 5]. The minus sign vanishes and the dot
ends the run early, splitting one reading into two numbers.
For a real reading, switch techniques: split the line into fields and parse the one you want.
var entry = lines[0].strip() # 'Day 1: -20.5C, Partly Cloudy'
var temp_field = entry.split(": ")[1].split(", ")[0] # '-20.5C'
var reading = Float64(temp_field.removesuffix("C"))
print(t"reading: {reading}") # -20.5
Because you know the temperature field is a signed decimal followed by
"C", it’s safe to remove the unit before parsing.
Takeaways
- Match the tool to the data. A digit scan extracts whole numbers from prose. A field split isolates a value in a known position, sign and all.
split(sep)returns aList[StringSpan], each slice a view into the original text. Convert a slice toStringwhen you need to create or modify owned text.Float64(text)raises on anything that isn’t a number.
Final code
Your complete pull_numbers_from_text.mojo:
from std.pathlib import Path
def extract_ints(text: String) raises -> List[Int]:
var nums = List[Int]()
var cur = String("")
for i in range(text.byte_length()):
var c = String(text[byte=i])
if c >= "0" and c <= "9": # an ASCII digit
cur += c # extend the current run
elif cur: # a run just ended
nums.append(Int(cur))
cur = String("")
if cur: # a run at the very end
nums.append(Int(cur))
return nums^
def main() raises:
var log = Path("src/downloads/temp_log.txt")
var text = log.read_text()
var lines = text.splitlines()
print(text)
var found = extract_ints(text)
print(t"found: {found}")
# [1, 20, 5, 2, 22, 3, 3, 19, 8, 4, 25, 1, 5, 44, 5, 6, 27, 7, 7, 42, 3,
# 8, 28, 2, 9, 23, 0, 10, 36, 5]
# Every line contributes three numbers: the day, the whole-degree part
# of the temperature, and the tenths digit. Take every 3rd value.
var day_numbers = List[Int]()
for i in range(0, len(found), 3):
day_numbers.append(found[i])
print(t"day numbers: {day_numbers}") # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var expected = [i for i in range(1, len(lines) + 1)]
print(t"complete, in order: {day_numbers == expected}") # True
print(t"scanned: {extract_ints(String(lines[0]))}") # [1, 20, 5]
var entry = lines[0].strip() # 'Day 1: -20.5C, Partly Cloudy'
var temp_field = entry.split(": ")[1].split(", ")[0] # '-20.5C'
var reading = Float64(temp_field.removesuffix("C"))
print(t"reading: {reading}") # -20.5
Topics Covered
Byte-by-byte string scanning, digit runs, Int and Float64 parsing,
raises propagation, returning a List with ^, stride-based filtering,
validating a parse against an invariant the input’s shape promises, and
splitting into fields.