From 8effea6343594e5a4b62dae6906ce860851bf290 Mon Sep 17 00:00:00 2001 From: Mikhail Tavarez Date: Wed, 24 Apr 2024 14:47:03 -0500 Subject: [PATCH] Feature/fmt upgrades (#38) * update fmt to include some more options * update fmt --- gojo/fmt/__init__.mojo | 2 +- gojo/fmt/fmt.mojo | 136 +++++++++++++++++++++++++++++++++-------- tests/test_fmt.mojo | 6 ++ 3 files changed, 116 insertions(+), 28 deletions(-) diff --git a/gojo/fmt/__init__.mojo b/gojo/fmt/__init__.mojo index fe5652b..a4b04e3 100644 --- a/gojo/fmt/__init__.mojo +++ b/gojo/fmt/__init__.mojo @@ -1 +1 @@ -from .fmt import sprintf, printf +from .fmt import sprintf, printf, sprintf_str diff --git a/gojo/fmt/fmt.mojo b/gojo/fmt/fmt.mojo index fe25b17..a44cdf3 100644 --- a/gojo/fmt/fmt.mojo +++ b/gojo/fmt/fmt.mojo @@ -8,35 +8,41 @@ Boolean Integer %d base 10 +%q a single-quoted character literal. +%x base 16, with lower-case letters for a-f +%X base 16, with upper-case letters for A-F Floating-point and complex constituents: %f decimal point but no exponent, e.g. 123.456 String and slice of bytes (treated equivalently with these verbs): %s the uninterpreted bytes of the string or slice +%q a double-quoted string TODO: - Add support for more formatting options - Switch to buffered writing to avoid multiple string concatenations - Add support for width and precision formatting options +- Handle escaping for String's %q """ from utils.variant import Variant +from math import floor +from ..builtins import Byte - -alias Args = Variant[String, Int, Float64, Bool] +alias Args = Variant[String, Int, Float64, Bool, List[Byte]] fn replace_first(s: String, old: String, new: String) -> String: """Replace the first occurrence of a substring in a string. - Parameters: - s (str): The original string - old (str): The substring to be replaced - new (str): The new substring + Args: + s: The original string + old: The substring to be replaced + new: The new substring Returns: - String: The string with the first occurrence of the old substring replaced by the new one. + The string with the first occurrence of the old substring replaced by the new one. """ # Find the first occurrence of the old substring var index = s.find(old) @@ -49,49 +55,122 @@ fn replace_first(s: String, old: String, new: String) -> String: return s -fn format_string(s: String, arg: String) -> String: - return replace_first(s, String("%s"), arg) +fn find_first_verb(s: String, verbs: List[String]) -> String: + """Find the first occurrence of a verb in a string. + + Args: + s: The original string + verbs: The list of verbs to search for. + + Returns: + The verb to replace. + """ + var index = -1 + var verb: String = "" + + for v in verbs: + var i = s.find(v[]) + if i != -1 and (index == -1 or i < index): + index = i + verb = v[] + + return verb + + +alias BASE10_TO_BASE16 = List[String]("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f") + + +fn convert_base10_to_base16(value: Int) -> String: + """Converts a base 10 number to base 16. + + Args: + value: Base 10 number. + + Returns: + Base 16 number as a String. + """ + + var val: Float64 = 0.0 + var result: Float64 = value + var base16: String = "" + while result > 1: + var temp = result / 16 + var floor_result = floor(temp) + var remainder = temp - floor_result + result = floor_result + val = 16 * remainder + + base16 = BASE10_TO_BASE16[int(val)] + base16 + + return base16 -fn format_integer(s: String, arg: Int) -> String: - return replace_first(s, String("%d"), arg) +fn format_string(format: String, arg: String) -> String: + var verb = find_first_verb(format, List[String]("%s", "%q")) + var arg_to_place = arg + if verb == "%q": + arg_to_place = '"' + arg + '"' + return replace_first(format, String("%s"), arg) -fn format_float(s: String, arg: Float64) -> String: - return replace_first(s, String("%f"), arg) +fn format_bytes(format: String, arg: List[Byte]) -> String: + var argument = arg + if argument[-1] != 0: + argument.append(0) -fn format_boolean(s: String, arg: Bool) -> String: - var value: String = "" + return format_string(format, argument) + + +fn format_integer(format: String, arg: Int) -> String: + var verb = find_first_verb(format, List[String]("%x", "%X", "%d", "%q")) + var arg_to_place = String(arg) + if verb == "%x": + arg_to_place = String(convert_base10_to_base16(arg)).lower() + elif verb == "%X": + arg_to_place = String(convert_base10_to_base16(arg)).upper() + elif verb == "%q": + arg_to_place = "'" + String(arg) + "'" + + return replace_first(format, verb, arg_to_place) + + +fn format_float(format: String, arg: Float64) -> String: + return replace_first(format, String("%f"), arg) + + +fn format_boolean(format: String, arg: Bool) -> String: + var value: String = "False" if arg: value = "True" - else: - value = "False" - return replace_first(s, String("%t"), value) + return replace_first(format, String("%t"), value) + + +# If the number of arguments does not match the number of format specifiers +alias BadArgCount = "(BAD ARG COUNT)" -fn sprintf(formatting: String, *args: Args) raises -> String: +fn sprintf(formatting: String, *args: Args) -> String: var text = formatting - var formatter_count = formatting.count("%") + var raw_percent_count = formatting.count("%%") * 2 + var formatter_count = formatting.count("%") - raw_percent_count - if formatter_count > len(args): - raise Error("Not enough arguments for format string") - elif formatter_count < len(args): - raise Error("Too many arguments for format string") + if formatter_count != len(args): + return BadArgCount for i in range(len(args)): var argument = args[i] if argument.isa[String](): text = format_string(text, argument.get[String]()[]) + elif argument.isa[List[Byte]](): + text = format_bytes(text, argument.get[List[Byte]]()[]) elif argument.isa[Int](): text = format_integer(text, argument.get[Int]()[]) elif argument.isa[Float64](): text = format_float(text, argument.get[Float64]()[]) elif argument.isa[Bool](): text = format_boolean(text, argument.get[Bool]()[]) - else: - raise Error("Unknown for argument #" + String(i)) return text @@ -114,7 +193,8 @@ fn sprintf_str(formatting: String, args: List[String]) raises -> String: fn printf(formatting: String, *args: Args) raises: var text = formatting - var formatter_count = formatting.count("%") + var raw_percent_count = formatting.count("%%") * 2 + var formatter_count = formatting.count("%") - raw_percent_count if formatter_count > len(args): raise Error("Not enough arguments for format string") @@ -125,6 +205,8 @@ fn printf(formatting: String, *args: Args) raises: var argument = args[i] if argument.isa[String](): text = format_string(text, argument.get[String]()[]) + elif argument.isa[List[Byte]](): + text = format_bytes(text, argument.get[List[Byte]]()[]) elif argument.isa[Int](): text = format_integer(text, argument.get[Int]()[]) elif argument.isa[Float64](): diff --git a/tests/test_fmt.mojo b/tests/test_fmt.mojo index 5d41f57..a7eaa21 100644 --- a/tests/test_fmt.mojo +++ b/tests/test_fmt.mojo @@ -16,6 +16,12 @@ fn test_sprintf() raises: "Hello, world. I am 29 years old. More precisely, I am 29.5 years old. It is True that I like Mojo!", ) + s = sprintf("This is a number: %d. In base 16: %x. In base 16 upper: %X.", 42, 42, 42) + test.assert_equal(s, "This is a number: 42. In base 16: 2a. In base 16 upper: 2A.") + + s = sprintf("Hello %s", String("world").as_bytes()) + test.assert_equal(s, "Hello world") + fn test_printf() raises: var test = MojoTest("Testing printf")