summaryrefslogtreecommitdiff
path: root/asl/formatting
diff options
context:
space:
mode:
authorSteven Le Rouzic <steven.lerouzic@gmail.com>2025-02-17 00:21:48 +0100
committerSteven Le Rouzic <steven.lerouzic@gmail.com>2025-02-17 22:29:50 +0100
commita141c401f78467bc15f62882fca5d55a007cacbb (patch)
tree908ac71a8640f78f45d22c6808c5fa6e373000fa /asl/formatting
parentcb77cbe9ce4cddad6a460aa190ff70f0c13e4703 (diff)
Reorganize everything
Diffstat (limited to 'asl/formatting')
-rw-r--r--asl/formatting/BUILD.bazel32
-rw-r--r--asl/formatting/format.cpp201
-rw-r--r--asl/formatting/format.hpp109
-rw-r--r--asl/formatting/format_float.cpp98
-rw-r--r--asl/formatting/format_tests.cpp110
5 files changed, 550 insertions, 0 deletions
diff --git a/asl/formatting/BUILD.bazel b/asl/formatting/BUILD.bazel
new file mode 100644
index 0000000..cab293e
--- /dev/null
+++ b/asl/formatting/BUILD.bazel
@@ -0,0 +1,32 @@
+cc_library(
+ name = "formatting",
+ hdrs = [
+ "format.hpp",
+ ],
+ srcs = [
+ "format.cpp",
+ "format_float.cpp",
+ ],
+ deps = [
+ "//asl/base",
+ "//asl/memory",
+ "//asl/strings:string_view",
+ "//asl/types:span",
+ "//asl/io:writer",
+ "//vendor/dragonbox",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+cc_test(
+ name = "tests",
+ srcs = [
+ "format_tests.cpp",
+ ],
+ deps = [
+ ":formatting",
+ "//asl/tests:utils",
+ "//asl/testing",
+ "//asl/strings:string_builder",
+ ],
+)
diff --git a/asl/formatting/format.cpp b/asl/formatting/format.cpp
new file mode 100644
index 0000000..0c52c1b
--- /dev/null
+++ b/asl/formatting/format.cpp
@@ -0,0 +1,201 @@
+#include "asl/formatting/format.hpp"
+#include "asl/base/utility.hpp"
+#include "asl/base/assert.hpp"
+#include "asl/memory/memory.hpp"
+
+void asl::format_internals::format(
+ Writer* writer,
+ string_view fmt,
+ span<const type_erased_arg> args)
+{
+ Formatter f(writer);
+
+ auto arg_it = args.begin();
+ auto arg_end = args.end();
+
+ isize_t i = 0;
+ while (i < fmt.size())
+ {
+ if (fmt[i] == '{')
+ {
+ if (i + 1 < fmt.size())
+ {
+ if (fmt[i + 1] == '}')
+ {
+ f.write(fmt.substr(0, i));
+ fmt = fmt.substr(i + 2);
+ i = 0;
+
+ if (arg_it == arg_end)
+ {
+ f.write("<ERROR>");
+ }
+ else
+ {
+ arg_it->fn(f, arg_it->data);
+ arg_it++;
+ }
+
+ continue;
+ }
+
+ if (fmt[i + 1] == '{')
+ {
+ f.write(fmt.substr(0, i + 1));
+ fmt = fmt.substr(i + 2);
+ i = 0;
+
+ continue;
+ }
+ }
+
+ f.write(fmt.substr(0, i));
+ fmt = fmt.substr(i + 1);
+ i = 0;
+
+ f.write("<ERROR>");
+ }
+ else if (i + 1 < fmt.size() && fmt[i] == '}' && fmt[i + 1] == '}')
+ {
+ f.write(fmt.substr(0, i + 1));
+ fmt = fmt.substr(i + 2);
+ i = 0;
+ }
+ else
+ {
+ i += 1;
+ }
+ }
+
+ f.write(fmt);
+}
+
+void asl::AslFormat(Formatter& f, const char* str)
+{
+ f.write({str, asl::strlen(str)});
+}
+
+void asl::AslFormat(Formatter& f, bool v)
+{
+ if (v)
+ {
+ f.write("true");
+ }
+ else
+ {
+ f.write("false");
+ }
+}
+
+void asl::AslFormat(Formatter& f, uint8_t v)
+{
+ AslFormat(f, static_cast<uint64_t>(v));
+}
+
+void asl::AslFormat(Formatter& f, uint16_t v)
+{
+ AslFormat(f, static_cast<uint64_t>(v));
+}
+
+void asl::AslFormat(Formatter& f, uint32_t v)
+{
+ AslFormat(f, static_cast<uint64_t>(v));
+}
+
+static constexpr int32_t kMaxUint64Digits = 20;
+
+asl::string_view asl::format_uint64(uint64_t v, asl::span<char, kMaxUint64Digits> buffer)
+{
+ static constexpr char s_pairs_storage[] = {
+ '0', '0', '0', '1', '0', '2', '0', '3', '0', '4',
+ '0', '5', '0', '6', '0', '7', '0', '8', '0', '9',
+ '1', '0', '1', '1', '1', '2', '1', '3', '1', '4',
+ '1', '5', '1', '6', '1', '7', '1', '8', '1', '9',
+ '2', '0', '2', '1', '2', '2', '2', '3', '2', '4',
+ '2', '5', '2', '6', '2', '7', '2', '8', '2', '9',
+ '3', '0', '3', '1', '3', '2', '3', '3', '3', '4',
+ '3', '5', '3', '6', '3', '7', '3', '8', '3', '9',
+ '4', '0', '4', '1', '4', '2', '4', '3', '4', '4',
+ '4', '5', '4', '6', '4', '7', '4', '8', '4', '9',
+ '5', '0', '5', '1', '5', '2', '5', '3', '5', '4',
+ '5', '5', '5', '6', '5', '7', '5', '8', '5', '9',
+ '6', '0', '6', '1', '6', '2', '6', '3', '6', '4',
+ '6', '5', '6', '6', '6', '7', '6', '8', '6', '9',
+ '7', '0', '7', '1', '7', '2', '7', '3', '7', '4',
+ '7', '5', '7', '6', '7', '7', '7', '8', '7', '9',
+ '8', '0', '8', '1', '8', '2', '8', '3', '8', '4',
+ '8', '5', '8', '6', '8', '7', '8', '8', '8', '9',
+ '9', '0', '9', '1', '9', '2', '9', '3', '9', '4',
+ '9', '5', '9', '6', '9', '7', '9', '8', '9', '9',
+ };
+
+ static constexpr span s_pairs = s_pairs_storage;
+ int32_t cursor = kMaxUint64Digits;
+
+ auto write_two = [&buffer, &cursor](span<const char, 2> str)
+ {
+ ASL_ASSERT(cursor >= 2);
+ buffer[--cursor] = str[1];
+ buffer[--cursor] = str[0];
+ };
+
+ auto write_one = [&buffer, &cursor](char c)
+ {
+ ASL_ASSERT(cursor >= 1);
+ buffer[--cursor] = c;
+ };
+
+ while (v >= 100)
+ {
+ uint64_t x = v % 100;
+ v /= 100;
+ write_two(s_pairs.subspan(static_cast<isize_t>(x * 2)).first<2>());
+ }
+
+ if (v >= 10)
+ {
+ write_two(s_pairs.subspan(static_cast<isize_t>(v * 2)).first<2>());
+ }
+ else if (v > 0 || cursor == kMaxUint64Digits)
+ {
+ ASL_ASSERT(v < 10);
+ write_one(static_cast<char>('0' + v));
+ }
+
+ return string_view(buffer.data(), kMaxUint64Digits).substr(cursor);
+}
+
+void asl::AslFormat(Formatter& f, uint64_t v)
+{
+ char buffer[kMaxUint64Digits];
+ f.write(format_uint64(v, buffer));
+}
+
+void asl::AslFormat(Formatter& f, int8_t v)
+{
+ AslFormat(f, static_cast<int64_t>(v));
+}
+
+void asl::AslFormat(Formatter& f, int16_t v)
+{
+ AslFormat(f, static_cast<int64_t>(v));
+}
+
+void asl::AslFormat(Formatter& f, int32_t v)
+{
+ AslFormat(f, static_cast<int64_t>(v));
+}
+
+void asl::AslFormat(Formatter& f, int64_t v)
+{
+ if (v < 0)
+ {
+ f.write("-");
+ uint64_t absolute_value = ~(bit_cast<uint64_t>(v) - 1);
+ AslFormat(f, absolute_value);
+ }
+ else
+ {
+ AslFormat(f, static_cast<uint64_t>(v));
+ }
+}
diff --git a/asl/formatting/format.hpp b/asl/formatting/format.hpp
new file mode 100644
index 0000000..02f43eb
--- /dev/null
+++ b/asl/formatting/format.hpp
@@ -0,0 +1,109 @@
+#pragma once
+
+#include "asl/base/integers.hpp"
+#include "asl/base/meta.hpp"
+#include "asl/io/writer.hpp"
+#include "asl/types/span.hpp"
+#include "asl/strings/string_view.hpp"
+
+namespace asl
+{
+
+class Formatter;
+
+template<typename T>
+concept formattable = requires (Formatter& f, const T& value)
+{
+ AslFormat(f, value);
+};
+
+namespace format_internals
+{
+
+struct type_erased_arg
+{
+ const void* data;
+ void (*fn)(Formatter&, const void*);
+
+ template<formattable T>
+ static constexpr void erased_fn(Formatter& f, const void* data)
+ {
+ AslFormat(f, *reinterpret_cast<const T*>(data));
+ }
+
+ template<formattable T>
+ explicit constexpr type_erased_arg(const T& arg)
+ : data{&arg}
+ , fn{erased_fn<T>}
+ {}
+};
+
+void format(Writer*, string_view fmt, span<const type_erased_arg> args);
+
+} // namespace internals
+
+class Formatter
+{
+ Writer* m_writer;
+
+public:
+ explicit constexpr Formatter(Writer* writer)
+ : m_writer{writer}
+ {}
+
+ constexpr void write(string_view s)
+ {
+ m_writer->write(as_bytes(s.as_span()));
+ }
+
+ constexpr Writer* writer() const { return m_writer; }
+};
+
+template<formattable... Args>
+void format(Writer* w, string_view fmt, const Args&... args)
+{
+ if constexpr (types_count<Args...> > 0)
+ {
+ format_internals::type_erased_arg type_erased_args[] = {
+ format_internals::type_erased_arg(args)...
+ };
+
+ format_internals::format(w, fmt, type_erased_args);
+ }
+ else
+ {
+ format_internals::format(w, fmt, {});
+ }
+}
+
+template<isize_t N>
+void AslFormat(Formatter& f, const char (&str)[N])
+{
+ f.write(string_view(str, N - 1));
+}
+
+void AslFormat(Formatter& f, const char* str);
+
+inline void AslFormat(Formatter& f, string_view sv)
+{
+ f.write(sv);
+}
+
+void AslFormat(Formatter& f, float);
+void AslFormat(Formatter& f, double);
+
+void AslFormat(Formatter& f, bool);
+
+void AslFormat(Formatter& f, uint8_t);
+void AslFormat(Formatter& f, uint16_t);
+void AslFormat(Formatter& f, uint32_t);
+void AslFormat(Formatter& f, uint64_t);
+
+void AslFormat(Formatter& f, int8_t);
+void AslFormat(Formatter& f, int16_t);
+void AslFormat(Formatter& f, int32_t);
+void AslFormat(Formatter& f, int64_t);
+
+string_view format_uint64(uint64_t value, span<char, 20> buffer);
+
+} // namespace asl
diff --git a/asl/formatting/format_float.cpp b/asl/formatting/format_float.cpp
new file mode 100644
index 0000000..eca1d92
--- /dev/null
+++ b/asl/formatting/format_float.cpp
@@ -0,0 +1,98 @@
+#include "asl/formatting/format.hpp"
+#include "asl/base/float.hpp"
+
+#define JKJ_STD_REPLACEMENT_NAMESPACE_DEFINED 0
+#define JKJ_STATIC_DATA_SECTION_DEFINED 0
+#include <dragonbox.h>
+
+static constexpr isize_t kZeroCount = 100;
+static constexpr char kZeros[kZeroCount] = {
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+ '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
+};
+
+template<asl::is_floating_point T>
+static void format_float(asl::Formatter& f, T value)
+{
+ if (asl::is_infinity(value))
+ {
+ if (value > 0)
+ {
+ f.write("Infinity"_sv);
+ }
+ else
+ {
+ f.write("-Infinity"_sv);
+ }
+ return;
+ }
+
+ if (value == static_cast<T>(0))
+ {
+ f.write("0"_sv);
+ return;
+ }
+
+ if (asl::is_nan(value))
+ {
+ f.write("NaN"_sv);
+ return;
+ }
+
+ auto decimal = jkj::dragonbox::to_decimal(value);
+
+ if (decimal.is_negative) { f.write("-"); }
+
+ char buffer[20];
+ asl::string_view digits = asl::format_uint64(decimal.significand, buffer);
+
+ if (decimal.exponent >= 0)
+ {
+ f.write(digits);
+ while (decimal.exponent > 0)
+ {
+ isize_t to_write = asl::min(static_cast<isize_t>(decimal.exponent), kZeroCount);
+ f.write(asl::string_view(kZeros, to_write));
+ decimal.exponent -= to_write;
+ }
+ }
+ else
+ {
+ if (digits.size() <= -decimal.exponent)
+ {
+ f.write("0.");
+ decimal.exponent = -decimal.exponent - static_cast<int>(digits.size());
+ while (decimal.exponent > 0)
+ {
+ isize_t to_write = asl::min(static_cast<isize_t>(decimal.exponent), kZeroCount);
+ f.write(asl::string_view(kZeros, to_write));
+ decimal.exponent -= to_write;
+ }
+ f.write(digits);
+ }
+ else
+ {
+ f.write(digits.first(digits.size() + decimal.exponent));
+ f.write(".");
+ f.write(digits.last(-decimal.exponent));
+ }
+ }
+}
+
+void asl::AslFormat(Formatter& f, float value)
+{
+ format_float(f, value);
+}
+
+void asl::AslFormat(Formatter& f, double value)
+{
+ format_float(f, value);
+}
diff --git a/asl/formatting/format_tests.cpp b/asl/formatting/format_tests.cpp
new file mode 100644
index 0000000..96ed7d0
--- /dev/null
+++ b/asl/formatting/format_tests.cpp
@@ -0,0 +1,110 @@
+#include "asl/formatting/format.hpp"
+#include "asl/testing/testing.hpp"
+#include "asl/base/float.hpp"
+#include "asl/strings/string_builder.hpp"
+
+static_assert(asl::formattable<decltype("Hello")>);
+
+ASL_TEST(format_args)
+{
+ // @Todo Introduce ASL_TEST_EXPECT_EQ, or ASL_TEST_EXPECT_STREQ
+
+ auto s = asl::format_to_string("Hello, world!");
+ ASL_TEST_EXPECT(s == "Hello, world!"_sv);
+
+ s = asl::format_to_string("");
+ ASL_TEST_EXPECT(s == ""_sv);
+
+ s = asl::format_to_string("Hello, {}!", "world");
+ ASL_TEST_EXPECT(s == "Hello, world!"_sv);
+
+ s = asl::format_to_string("Hello, {}! {}", "world");
+ ASL_TEST_EXPECT(s == "Hello, world! <ERROR>"_sv);
+
+ s = asl::format_to_string("Hello, pup!", "world");
+ ASL_TEST_EXPECT(s == "Hello, pup!"_sv);
+
+ s = asl::format_to_string("{}", "CHEESE");
+ ASL_TEST_EXPECT(s == "CHEESE"_sv);
+
+ s = asl::format_to_string("{ ", "CHEESE");
+ ASL_TEST_EXPECT(s == "<ERROR> "_sv);
+
+ s = asl::format_to_string("{", "CHEESE");
+ ASL_TEST_EXPECT(s == "<ERROR>"_sv);
+
+ s = asl::format_to_string("a{{b");
+ ASL_TEST_EXPECT(s == "a{b"_sv);
+
+ s = asl::format_to_string("{{{}}} }", "CHEESE");
+ ASL_TEST_EXPECT(s == "{CHEESE} }"_sv);
+}
+
+ASL_TEST(format_integers)
+{
+ auto s = asl::format_to_string("{} {} {}", 0, 1, 2);
+ ASL_TEST_EXPECT(s == "0 1 2"_sv);
+
+ s = asl::format_to_string("{} {} {}", 10, 11, 12);
+ ASL_TEST_EXPECT(s == "10 11 12"_sv);
+
+ s = asl::format_to_string("{} {} {}", 100, 101, 102);
+ ASL_TEST_EXPECT(s == "100 101 102"_sv);
+
+ s = asl::format_to_string("{} {} {}", 1000, 1001, 1002);
+ ASL_TEST_EXPECT(s == "1000 1001 1002"_sv);
+
+ s = asl::format_to_string("{} {} {} {}", -1, -23, -456, -7890);
+ ASL_TEST_EXPECT(s == "-1 -23 -456 -7890"_sv);
+}
+
+ASL_TEST(format_floats)
+{
+ auto s = asl::format_to_string("{} {} {}", 0.0F, 1.0, 2.0F);
+ ASL_TEST_EXPECT(s == "0 1 2"_sv);
+
+ s = asl::format_to_string("{} {} {}", 0.1F, 0.001F, 0.123F);
+ ASL_TEST_EXPECT(s == "0.1 0.001 0.123"_sv);
+
+ s = asl::format_to_string("{} {}", 1.25F, -22.3);
+ ASL_TEST_EXPECT(s == "1.25 -22.3"_sv);
+
+ s = asl::format_to_string("{}", 1e32);
+ ASL_TEST_EXPECT(s == "100000000000000000000000000000000"_sv);
+
+ s = asl::format_to_string("{}", 123e-8);
+ ASL_TEST_EXPECT(s == "0.00000123"_sv);
+
+ s = asl::format_to_string("{} {}", asl::infinity<float>(), -asl::infinity<double>());
+ ASL_TEST_EXPECT(s == "Infinity -Infinity"_sv);
+
+ s = asl::format_to_string("{}", asl::nan<float>());
+ ASL_TEST_EXPECT(s == "NaN"_sv);
+}
+
+ASL_TEST(format_boolean)
+{
+ auto s = asl::format_to_string("{} {}", true, false);
+ ASL_TEST_EXPECT(s == "true false"_sv);
+}
+
+struct CustomFormat
+{
+ int x;
+ friend void AslFormat(asl::Formatter&, const CustomFormat&);
+};
+
+void AslFormat(asl::Formatter& f, const CustomFormat& c)
+{
+ f.write("("_sv);
+ AslFormat(f, c.x);
+ f.write(")"_sv);
+}
+
+static_assert(asl::formattable<CustomFormat>);
+
+ASL_TEST(format_custom)
+{
+ auto s = asl::format_to_string("{}", CustomFormat{37});
+ ASL_TEST_EXPECT(s == "(37)"_sv);
+}