From 735cfcd29011ea9188b008e3389aa236529f0e5f Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Fri, 1 Sep 2023 16:21:43 +0200 Subject: [PATCH] Add formatting of bytes into the best unit of measurement --- core/fmt/doc.odin | 3 ++ core/fmt/fmt.odin | 61 +++++++++++++++++++++++++++++++ core/mem/mem.odin | 2 + core/runtime/core.odin | 2 + tests/core/Makefile | 3 ++ tests/core/fmt/test_core_fmt.odin | 59 ++++++++++++++++++++++++++++++ 6 files changed, 130 insertions(+) create mode 100644 tests/core/fmt/test_core_fmt.odin diff --git a/core/fmt/doc.odin b/core/fmt/doc.odin index 991058661..e758f9638 100644 --- a/core/fmt/doc.odin +++ b/core/fmt/doc.odin @@ -35,6 +35,8 @@ Floating-point, complex numbers, and quaternions: %F synonym for %f %h hexadecimal (lower-case) representation with 0h prefix (0h01234abcd) %H hexadecimal (upper-case) representation with 0H prefix (0h01234ABCD) + %m number of bytes in the best unit of measurement, e.g. 123.45mb + %M number of bytes in the best unit of measurement, e.g. 123.45MB String and slice of bytes %s the uninterpreted bytes of the string or slice %q a double-quoted string safely escaped with Odin syntax @@ -85,6 +87,7 @@ Other flags: add leading 0z for dozenal (%#z) add leading 0x or 0X for hexadecimal (%#x or %#X) remove leading 0x for %p (%#p) + add a space between bytes and the unit of measurement (%#m or %#M) ' ' (space) leave a space for elided sign in numbers (% d) 0 pad with leading zeros rather than spaces diff --git a/core/fmt/fmt.odin b/core/fmt/fmt.odin index b3b8067e8..fad2548c8 100644 --- a/core/fmt/fmt.odin +++ b/core/fmt/fmt.odin @@ -1048,6 +1048,65 @@ _fmt_int_128 :: proc(fi: ^Info, u: u128, base: int, is_signed: bool, bit_size: i fi.zero = false _pad(fi, s) } +// Units of measurements: +__MEMORY_LOWER := " b kb mb gb tb pb eb" +__MEMORY_UPPER := " B KB MB GB TB PB EB" +// Formats an integer value as bytes with the best representation. +// +// Inputs: +// - fi: A pointer to an Info structure +// - u: The integer value to format +// - is_signed: A boolean indicating if the integer is signed +// - bit_size: The bit size of the integer +// - digits: A string containing the digits for formatting +// +_fmt_memory :: proc(fi: ^Info, u: u64, is_signed: bool, bit_size: int, units: string) { + abs, neg := strconv.is_integer_negative(u, is_signed, bit_size) + + // Default to a precision of 2, but if less than a kb, 0 + prec := fi.prec if (fi.prec_set || abs < mem.Kilobyte) else 2 + + div, off, unit_len := 1, 0, 1 + for n := abs; n >= mem.Kilobyte; n /= mem.Kilobyte { + div *= mem.Kilobyte + off += 3 + + // First iteration is slightly different because you go from + // units of length 1 to units of length 2. + if unit_len == 1 { + off = 2 + unit_len = 2 + } + } + + // If hash, we add a space between the value and the suffix. + if fi.hash { + unit_len += 1 + } else { + off += 1 + } + + amt := f64(abs) / f64(div) + if neg { + amt = -amt + } + + buf: [256]byte + str := strconv.append_float(buf[:], amt, 'f', prec, 64) + + // Add the unit at the end. + copy(buf[len(str):], units[off:off+unit_len]) + str = string(buf[:len(str)+unit_len]) + + if !fi.plus { + // Strip sign from "+" but not "+Inf". + if str[0] == '+' && str[1] != 'I' { + str = str[1:] + } + } + + _pad(fi, str) +} // Hex Values: __DIGITS_LOWER := "0123456789abcdefx" __DIGITS_UPPER := "0123456789ABCDEFX" @@ -1096,6 +1155,8 @@ fmt_int :: proc(fi: ^Info, u: u64, is_signed: bool, bit_size: int, verb: rune) { io.write_string(fi.writer, "U+", &fi.n) _fmt_int(fi, u, 16, false, bit_size, __DIGITS_UPPER) } + case 'm': _fmt_memory(fi, u, is_signed, bit_size, __MEMORY_LOWER) + case 'M': _fmt_memory(fi, u, is_signed, bit_size, __MEMORY_UPPER) case: fmt_bad_verb(fi, verb) diff --git a/core/mem/mem.odin b/core/mem/mem.odin index a06579d71..dd985d5dd 100644 --- a/core/mem/mem.odin +++ b/core/mem/mem.odin @@ -8,6 +8,8 @@ Kilobyte :: runtime.Kilobyte Megabyte :: runtime.Megabyte Gigabyte :: runtime.Gigabyte Terabyte :: runtime.Terabyte +Petabyte :: runtime.Petabyte +Exabyte :: runtime.Exabyte set :: proc "contextless" (data: rawptr, value: byte, len: int) -> rawptr { return runtime.memset(data, i32(value), len) diff --git a/core/runtime/core.odin b/core/runtime/core.odin index 4f85cf50e..0634f573a 100644 --- a/core/runtime/core.odin +++ b/core/runtime/core.odin @@ -337,6 +337,8 @@ Kilobyte :: 1024 * Byte Megabyte :: 1024 * Kilobyte Gigabyte :: 1024 * Megabyte Terabyte :: 1024 * Gigabyte +Petabyte :: 1024 * Terabyte +Exabyte :: 1024 * Petabyte // Logging stuff diff --git a/tests/core/Makefile b/tests/core/Makefile index 77d4b85a0..81bcdeeb8 100644 --- a/tests/core/Makefile +++ b/tests/core/Makefile @@ -57,3 +57,6 @@ c_libc_test: net_test: $(ODIN) run net -out:test_core_net + +fmt_test: + $(ODIN) run fmt -out:test_core_fmt diff --git a/tests/core/fmt/test_core_fmt.odin b/tests/core/fmt/test_core_fmt.odin new file mode 100644 index 000000000..e9acfa3a5 --- /dev/null +++ b/tests/core/fmt/test_core_fmt.odin @@ -0,0 +1,59 @@ +package test_core_fmt + +import "core:fmt" +import "core:os" +import "core:testing" +import "core:mem" + +TEST_count := 0 +TEST_fail := 0 + +when ODIN_TEST { + expect :: testing.expect + log :: testing.log +} else { + expect :: proc(t: ^testing.T, condition: bool, message: string, loc := #caller_location) { + TEST_count += 1 + if !condition { + TEST_fail += 1 + fmt.printf("[%v] %v\n", loc, message) + return + } + } + log :: proc(t: ^testing.T, v: any, loc := #caller_location) { + fmt.printf("[%v] ", loc) + fmt.printf("log: %v\n", v) + } +} + +main :: proc() { + t := testing.T{} + test_fmt_memory(&t) + + fmt.printf("%v/%v tests successful.\n", TEST_count - TEST_fail, TEST_count) + if TEST_fail > 0 { + os.exit(1) + } +} + +test_fmt_memory :: proc(t: ^testing.T) { + check :: proc(t: ^testing.T, exp: string, format: string, args: ..any, loc := #caller_location) { + got := fmt.tprintf(format, ..args) + expect(t, got == exp, fmt.tprintf("(%q, %v): %q != %q", format, args, got, exp), loc) + } + + check(t, "5b", "%m", 5) + check(t, "5B", "%M", 5) + check(t, "-5B", "%M", -5) + check(t, "3.00kb", "%m", mem.Kilobyte * 3) + check(t, "3kb", "%.0m", mem.Kilobyte * 3) + check(t, "3KB", "%.0M", mem.Kilobyte * 3) + check(t, "3.000 mb", "%#.3m", mem.Megabyte * 3) + check(t, "3.50 gb", "%#m", u32(mem.Gigabyte * 3.5)) + check(t, "001tb", "%5.0m", mem.Terabyte) + check(t, "-001tb", "%5.0m", -mem.Terabyte) + check(t, "2.50 pb", "%#5.m", uint(mem.Petabyte * 2.5)) + check(t, "1.00 EB", "%#M", mem.Exabyte) + check(t, "255 B", "%#M", u8(255)) + check(t, "0b", "%m", u8(0)) +}