Browse Source

Add formatting of bytes into the best unit of measurement

Laytan Laats 1 year ago
parent
commit
735cfcd290
6 changed files with 130 additions and 0 deletions
  1. 3 0
      core/fmt/doc.odin
  2. 61 0
      core/fmt/fmt.odin
  3. 2 0
      core/mem/mem.odin
  4. 2 0
      core/runtime/core.odin
  5. 3 0
      tests/core/Makefile
  6. 59 0
      tests/core/fmt/test_core_fmt.odin

+ 3 - 0
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
 

+ 61 - 0
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 "+<value>" 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)

+ 2 - 0
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)

+ 2 - 0
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
 

+ 3 - 0
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

+ 59 - 0
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))
+}