conv

conv converts between numbers and text. Parsing takes a []const u8 (a string or a literal works) and returns T or conv.ParseError; formatting writes into a buffer you provide and returns how many bytes it wrote.

tape
import conv from "conv";

It never allocates and never touches the OS, so it works in every profile, t0 kernel code included.

Floats are exact. parse_f64 returns the double nearest to the decimal text, however many digits it has. fmt_f64 with a negative precision writes the shortest text that reads back as the very same double, so a value survives a trip through text bit for bit. parse_f32 and fmt_f32 do the same for f32:

tape
var buf: [32]u8;
let n: i64 = conv.fmt_f64(buf[..], 0.1 + 0.2, -1);
io.println(buf[0..n]);                       // 0.30000000000000004
let back: f64 = conv.parse_f64(buf[0..n]) or { return 1; };
// back == 0.1 + 0.2, exactly

Errors

tape
pub enum ParseError : u8 {
    Empty = 1: "empty input";
    InvalidDigit = 2: "invalid digit";
    Overflow = 3: "value out of range";
    InvalidFormat = 4: "invalid format";
}
ErrorWhen
EmptyThe text is empty.
InvalidDigitAn integer contains a byte that isn’t a digit (a space, _, ., a second sign, a letter beyond the radix).
OverflowThe number doesn’t fit: outside i64/u64, or a float too large for f64 (f32 for parse_f32).
InvalidFormatA sign with no digits, a malformed float, or anything but true/false for parse_bool.

If text has both a bad byte and a value out of range, you get InvalidDigit.

tape
let n: i64 = conv.parse_i64("12x") or {
    io.println(err.desc());                  // invalid digit
    return 1;
};

The parsers accept exactly one number and nothing else: no spaces around it, no _ separators, no 0x prefix. Trim first if your input may have spaces.

Parsing

parse_i64(s: []const u8) -> i64 or ParseError

A decimal integer: an optional + or -, then digits.

tape
let a: i64 = conv.parse_i64("-42") or { return 1; };          // -42
let b: i64 = conv.parse_i64("+007") or { return 1; };         // 7
let c: i64 = conv.parse_i64("9223372036854775808") or {       // Overflow
    return 2;
};

parse_u64(s: []const u8) -> u64 or ParseError

A decimal unsigned integer up to 18446744073709551615. A - sign is an InvalidDigit, even in -0.

tape
let max: u64 = conv.parse_u64("18446744073709551615") or { return 1; };

parse_i64_radix(s: []const u8, radix: i64) -> i64 or ParseError

An integer in base 2 to 36, with an optional sign. Digits above 9 are the letters a to z, in either case. There is no prefix: strip a 0x or 0b yourself.

tape
let h: i64 = conv.parse_i64_radix("ff", 16) or { return 1; };     // 255
let b: i64 = conv.parse_i64_radix("-101", 2) or { return 1; };    // -5
let z: i64 = conv.parse_i64_radix("Zz", 36) or { return 1; };     // 1295

A radix outside 2 to 36 stops the program (trap): it’s a bug in the caller, not bad input.

parse_f64(s: []const u8) -> f64 or ParseError

A decimal floating-point number:

  • an optional sign, digits, optionally a . followed by more digits, and optionally an exponent: e or E, an optional sign and digits: 3, -2.5, 1e10, 6.02214076E+23;
  • or inf, +inf, -inf, nan (lowercase).

There must be a digit on both sides of a .: .5 and 5. are InvalidFormat. The result is correctly rounded (ties to even), for any number of digits. Numbers too small for a double become 0 (or -0); numbers too large are Overflow.

tape
let x: f64 = conv.parse_f64("0.1") or { return 1; };
let y: f64 = conv.parse_f64("9007199254740993") or { return 1; };  // 9007199254740992: the tie rounds to even
let z: f64 = conv.parse_f64("-1e-400") or { return 1; };           // -0

parse_f32(s: []const u8) -> f32 or ParseError

The same syntax as parse_f64, rounded straight to the nearest f32 (ties to even). It doesn’t go through f64, which would round twice and can pick the wrong neighbour. Numbers above about 3.4028235e38 are Overflow.

tape
let x: f32 = conv.parse_f32("0.1") or { return 1; };
let y: f32 = conv.parse_f32("1e39") or { return 2; };      // Overflow

parse_bool(s: []const u8) -> bool or ParseError

Exactly true or false.

tape
let verbose: bool = conv.parse_bool("true") or { return 1; };

Formatting

The formatters write into dst from index 0 and return the number of bytes written; slice the buffer with that length (buf[0..n]). If the output doesn’t fit in dst the program traps, so size buffers with these constants:

ConstantValueFor
FMT_I64_MAX_LEN20fmt_i64
FMT_U64_RADIX_MAX_LEN64fmt_u64_radix (base 2)
FMT_F64_SHORTEST_MAX_LEN25fmt_f64 with precision < 0
FMT_F64_FIXED_MAX_LEN311fmt_f64 with precision p >= 0 needs this + p
FMT_F32_SHORTEST_MAX_LEN22fmt_f32 with precision < 0
FMT_F32_FIXED_MAX_LEN41fmt_f32 with precision p >= 0 needs this + p

fmt_i64(dst: []u8, v: i64) -> i64

Decimal, with - for negative numbers.

tape
var buf: [20]u8;                              // conv.FMT_I64_MAX_LEN
let n: i64 = conv.fmt_i64(buf[..], -1234);
io.println(buf[0..n]);                        // -1234

fmt_u64_radix(dst: []u8, v: u64, radix: i64) -> i64

Base 2 to 36, digits 0-9 then lowercase a-z, no prefix.

tape
var buf: [64]u8;                              // conv.FMT_U64_RADIX_MAX_LEN
let n: i64 = conv.fmt_u64_radix(buf[..], 255, 16);
io.println(buf[0..n]);                        // ff
let m: i64 = conv.fmt_u64_radix(buf[..], 5, 2);
io.println(buf[0..m]);                        // 101

A radix outside 2 to 36 traps.

fmt_f64(dst: []u8, v: f64, precision: i64) -> i64

With precision < 0: the shortest text that parse_f64 reads back as exactly v. Numbers from 0.000001 up to (not including) 1e21 are written plainly, without a trailing .0; others in scientific notation:

ValueText
1.01
0.10.1
-2.25-2.25
1e20100000000000000000000
1e211e21
0.0000010.000001
0.00000011e-7
5e-3245e-324
-0.0-0

With precision >= 0: exactly that many digits after the point (no point for 0), rounded from the exact value of v, ties to even. Never scientific.

CallText
fmt_f64(buf[..], 3.14159, 2)3.14
fmt_f64(buf[..], 2.5, 0)2 (a tie: rounds to even)
fmt_f64(buf[..], 0.125, 2)0.12 (0.125 is exact, a tie)
fmt_f64(buf[..], 1.005, 2)1.00 (the double is slightly below 1.005)
fmt_f64(buf[..], -0.001, 2)-0.00

Infinities are inf and -inf, and every NaN is nan, at any precision.

tape
var buf: [25]u8;                              // conv.FMT_F64_SHORTEST_MAX_LEN
let n: i64 = conv.fmt_f64(buf[..], 1.0 / 3.0, -1);
io.println(buf[0..n]);                        // 0.3333333333333333
var fixed: [316]u8;                           // conv.FMT_F64_FIXED_MAX_LEN + 5
let m: i64 = conv.fmt_f64(fixed[..], 1.0 / 3.0, 5);
io.println(fixed[0..m]);                      // 0.33333

fmt_f32(dst: []u8, v: f32, precision: i64) -> i64

fmt_f64 for f32. With precision < 0 it writes the shortest text that parse_f32 reads back as exactly v, which is often shorter than the text of the same value widened to f64:

tape
let x: f32 = 0.1;
var buf: [22]u8;                              // conv.FMT_F32_SHORTEST_MAX_LEN
let n: i64 = conv.fmt_f32(buf[..], x, -1);
io.println(buf[0..n]);                        // 0.1
let m: i64 = conv.fmt_f64(buf[..], x as f64, -1);
io.println(buf[0..m]);                        // 0.10000000149011612

With precision >= 0 it writes the same text as fmt_f64 of the widened value. String interpolation ("{x}") formats an f64 with fmt_f64 and an f32 with fmt_f32, both with precision -1.

Last modified: