Text · Lesson 14.12

Parse

Source
Read a number from text with ParseInt32, which returns int32 ! ParseError and says where malformed or overflowing text went wrong.

Parsing is formatting in reverse: text in, value out. Unlike formatting, it fails all the time, because the text usually comes from a person or a file and may not spell a number at all — "seven", "12abc", or nothing. So every parser in the Format package returns a fallible, and the failure says what was wrong and where:

ParseInt32(text) -> int32 ! ParseError

What can go wrong

ParseError is a variant, and every case but the first carries the byte position where the trouble was found:

CaseMeans
Emptythere was no text at all
InvalidCharacter(at)the byte at at is not part of a number
Overflow(at)a well-formed number too large for the type, usually at its end
OutOfMemory(at)only the very widest number types can hit this one

Describe names each case. A match on a variant must be exhaustive, so even the case an int32 can never hit gets an arm:

func Describe(error: ParseError) {
    match error {
        .Empty => PrintLine("empty text"),
        .InvalidCharacter(at) => PrintLine("not a digit at byte {}", at),
        .Overflow(at) => PrintLine("too large for an int32, at byte {}", at),
        .OutOfMemory(_) => PrintLine("out of memory")
    }
}

Read then opens the outcome with .Success and .Failure, as in Outcome:

func Read(text: char8[..]) {
    match ParseInt32(text) {
        .Success(value) => PrintLine("[{}]  {}", text, value),
        .Failure(error) => {
            Print("[{}]  refused, ", text);
            Describe(error);
        }
    }
}

What counts as a number

A sign is understood, and so is a base prefix of the kind {:#x} writes, so "0x1F" reads as 31. Everything else must be digits. The parser does not skip spaces or stop at the first non-digit: the whole text must be a number, or the answer is a failure saying which byte is wrong.

TextResultWhy
"42"42
"-17"-17a sign
"0x1F"31a base prefix, as {:#x} writes
""Emptynothing to read
" 42"InvalidCharacter(0)a space is not a digit, even at the start
"12abc"InvalidCharacter(2)a is the first byte that is not a digit
"2147483647"2147483647the largest int32
"2147483648"Overflow(10)one more does not fit
"-2147483648"-2147483648the negative side holds one more
flowchart LR
    t(["`text`"]) --> e{"empty?"}
    e -- "yes" --> E["`Empty`"]
    e -- "no" --> d{"sign, prefix and digits<br/>— and nothing else?"}
    d -- "no" --> I["`InvalidCharacter(at)`"]
    d -- "yes" --> f{"fits an `int32`?"}
    f -- "no" --> O["`Overflow(at)`"]
    f -- "yes" --> S["`Success(value)`"]

An int32 holds −2147483648 to 2147483647. The negative side holds one more than the positive, so -2147483648 parses even though 2147483648 does not.

A fallback in one expression

Often a failure just means "use a default". With catch, as in Catch fallback, that takes one expression:

let fallback = ParseInt32("seven") catch { else => 0 };

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Parsing is formatting in reverse: text in, value out. Unlike formatting, it fails all the time,
// because the text usually comes from a person or a file and may not spell a number at all.
// So every parser in the Format package returns a fallible:
//
//     ParseInt32(text) -> int32 ! ParseError
//
// `ParseError` is a variant, and every case says where in the text the trouble was found:
//
//     Empty                  there was no text at all
//     InvalidCharacter(at)   the byte at `at` is not part of a number
//     Overflow(at)           a well-formed number too large for an `int32`, usually at its end
//     OutOfMemory(at)        only the very widest number types can hit this one
import Format::{ ParseError, ParseInt32 };
import Io::{ Print, PrintLine };

func Describe(error: ParseError) {
    match error {
        .Empty => PrintLine("empty text"),
        .InvalidCharacter(at) => PrintLine("not a digit at byte {}", at),
        .Overflow(at) => PrintLine("too large for an int32, at byte {}", at),
        .OutOfMemory(_) => PrintLine("out of memory")
    }
}

func Read(text: char8[..]) {
    match ParseInt32(text) {
        .Success(value) => PrintLine("[{}]  {}", text, value),
        .Failure(error) => {
            Print("[{}]  refused, ", text);
            Describe(error);
        }
    }
}

func Main() -> int {
    // A sign, and a base prefix of the kind `{:#x}` writes, are both understood.
    Read("42");
    Read("-17");
    Read("0x1F");

    // Malformed text. The parser does not skip spaces or stop at the first non-digit: the
    // whole text must be a number, or the answer is a failure saying which byte is wrong.
    Read("");
    Read(" 42");
    Read("12abc");

    // Well-formed but too large. An `int32` holds -2147483648 to 2147483647, and the
    // negative side holds one more than the positive.
    Read("2147483647");
    Read("2147483648");
    Read("-2147483648");

    // With `catch`, a failure can become a fallback value in one expression.
    let fallback = ParseInt32("seven") catch { else => 0 };
    PrintLine("[seven] with a fallback  {}", fallback);
    return 0;
}

Besides Io, its Rux.toml lists Format under [Dependencies].

Run it

cd Examples/Text/Parse
rux run
[42]  42
[-17]  -17
[0x1F]  31
[]  refused, empty text
[ 42]  refused, not a digit at byte 0
[12abc]  refused, not a digit at byte 2
[2147483647]  2147483647
[2147483648]  refused, too large for an int32, at byte 10
[-2147483648]  -2147483648
[seven] with a fallback  0

Common mistakes

Try it yourself

  1. Parse "0b101" and "0o17". Which other prefixes does the parser understand?
  2. Parse "300" and "-1" with ParseUint8. What does each report?
  3. Trim " 42\n" with StringView::FromValidated(…).Trim() and parse the view. You will need Text in [Dependencies] and import Text::StringView;.
  4. Parse "3.75" with ParseFloat64, then try "3,75".

Learn more