Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Mojo reaches for the number. Mojo reaches for the number.

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. 20 stays one number instead of a 2 and a 0.
  • extract_ints() raises because Int(text) can raise on bad input.
  • Return the newly constructed List with ^ to transfer ownership of its data to the caller.
  • String(text[byte=i]) reads a single byte. Using text[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 a List[StringSpan], each slice a view into the original text. Convert a slice to String when 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.