aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.core
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-07-02 04:00:06 +0100
committerLexi Winter <lexi@le-fay.org>2025-07-02 04:00:06 +0100
commit5adeb648f74c1771164c0686d6e0fc584cf36d9e (patch)
tree060cd918d3dd9e931a1541a43c9edff1a404ff47 /nihil.core
parent06fafff8e9e9c096cc39bde0306caa53ad3a2351 (diff)
downloadnihil-5adeb648f74c1771164c0686d6e0fc584cf36d9e.tar.gz
nihil-5adeb648f74c1771164c0686d6e0fc584cf36d9e.tar.bz2
move everything from util to core
Diffstat (limited to 'nihil.core')
-rw-r--r--nihil.core/CMakeLists.txt16
-rw-r--r--nihil.core/capture_stream.ccm55
-rw-r--r--nihil.core/capture_stream.test.cc44
-rw-r--r--nihil.core/construct.ccm22
-rw-r--r--nihil.core/ctype.ccm72
-rw-r--r--nihil.core/ctype.test.cc376
-rw-r--r--nihil.core/error.ccm321
-rw-r--r--nihil.core/error.test.cc273
-rw-r--r--nihil.core/flagset.ccm200
-rw-r--r--nihil.core/flagset.test.cc144
-rw-r--r--nihil.core/guard.ccm41
-rw-r--r--nihil.core/guard.test.cc16
-rw-r--r--nihil.core/match.ccm18
-rw-r--r--nihil.core/match.test.cc32
-rw-r--r--nihil.core/monad.ccm282
-rw-r--r--nihil.core/monad.test.cc65
-rw-r--r--nihil.core/next_word.ccm35
-rw-r--r--nihil.core/next_word.test.cc65
-rw-r--r--nihil.core/nihil.core.ccm15
-rw-r--r--nihil.core/parse_size.ccm90
-rw-r--r--nihil.core/parse_size.test.cc165
-rw-r--r--nihil.core/save_errno.ccm35
-rw-r--r--nihil.core/skipws.ccm26
-rw-r--r--nihil.core/skipws.test.cc48
-rw-r--r--nihil.core/sys_error.ccm18
-rw-r--r--nihil.core/tabulate.ccm298
-rw-r--r--nihil.core/tabulate.test.cc72
-rw-r--r--nihil.core/uuid.ccm768
-rw-r--r--nihil.core/uuid.test.cc923
29 files changed, 4535 insertions, 0 deletions
diff --git a/nihil.core/CMakeLists.txt b/nihil.core/CMakeLists.txt
index 637b825..e9998a3 100644
--- a/nihil.core/CMakeLists.txt
+++ b/nihil.core/CMakeLists.txt
@@ -6,6 +6,22 @@ target_include_directories(nihil.core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_sources(nihil.core
PUBLIC FILE_SET modules TYPE CXX_MODULES FILES
nihil.core.ccm
+
+ capture_stream.ccm
+ construct.ccm
+ ctype.ccm
errc.ccm
+ error.ccm
features.ccm
+ flagset.ccm
+ guard.ccm
+ match.ccm
+ monad.ccm
+ next_word.ccm
+ parse_size.ccm
+ save_errno.ccm
+ skipws.ccm
+ sys_error.ccm
+ tabulate.ccm
+ uuid.ccm
)
diff --git a/nihil.core/capture_stream.ccm b/nihil.core/capture_stream.ccm
new file mode 100644
index 0000000..5ab4f2d
--- /dev/null
+++ b/nihil.core/capture_stream.ccm
@@ -0,0 +1,55 @@
+// This source code is released into the public domain.
+export module nihil.core:capture_stream;
+
+import nihil.std;
+
+namespace nihil {
+
+// Capture output written to a stream and redirect it to an internal string
+// buffer. Call .str() to get the data written. Call .release() to stop
+// capturing (or simply delete the capture_stream object).
+export template<typename Char, typename Traits>
+struct capture_stream {
+ capture_stream(std::basic_ostream<Char, Traits> &stream)
+ : m_stream(&stream)
+ {
+ m_old_streambuf = m_stream->rdbuf();
+ m_stream->rdbuf(m_buffer.rdbuf());
+ }
+
+ ~capture_stream()
+ {
+ if (m_old_streambuf == nullptr)
+ return;
+ m_stream->rdbuf(m_old_streambuf);
+ }
+
+ /*
+ * Release this capture, returning the stream to its previous state.
+ */
+ auto release(this capture_stream &self) -> void
+ {
+ if (self.m_old_streambuf == nullptr)
+ throw std::logic_error(
+ "release() called on empty capture_stream");
+
+ self.m_stream->rdbuf(self.m_old_streambuf);
+ self.m_old_streambuf = nullptr;
+ }
+
+ /*
+ * Get the data which has been written to the stream.
+ */
+ [[nodiscard]] auto str(this capture_stream const &self)
+ -> std::basic_string_view<Char, Traits>
+ {
+ return self.m_buffer.view();
+ }
+
+private:
+ std::basic_ostringstream<Char, Traits> m_buffer;
+ std::basic_ostream<Char, Traits> *m_stream;
+ std::streambuf *m_old_streambuf;
+};
+
+} // namespace nihil
diff --git a/nihil.core/capture_stream.test.cc b/nihil.core/capture_stream.test.cc
new file mode 100644
index 0000000..4dd7db6
--- /dev/null
+++ b/nihil.core/capture_stream.test.cc
@@ -0,0 +1,44 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+TEST_CASE("nihil.util: capture", "[nihil][nihil.util]")
+{
+ SECTION("std::cout with release()") {
+ auto cap = nihil::capture_stream(std::cout);
+
+ std::cout << 1 << '+' << 1 << '=' << (1 + 1) << '\n';
+ REQUIRE(cap.str() == "1+1=2\n");
+
+ cap.release();
+ REQUIRE(cap.str() == "1+1=2\n");
+ }
+
+ SECTION("std::cout with dtor") {
+ auto cap = nihil::capture_stream(std::cout);
+ std::cout << 1 << '+' << 1 << '=' << (1 + 1) << '\n';
+ REQUIRE(cap.str() == "1+1=2\n");
+ }
+
+ SECTION("std::cerr with release()") {
+ auto cap = nihil::capture_stream(std::cerr);
+
+ std::cerr << 1 << '+' << 1 << '=' << (1 + 1) << '\n';
+ REQUIRE(cap.str() == "1+1=2\n");
+
+ cap.release();
+ REQUIRE(cap.str() == "1+1=2\n");
+ }
+
+ SECTION("std::cerr with dtor") {
+ auto cap = nihil::capture_stream(std::cerr);
+ std::cerr << 1 << '+' << 1 << '=' << (1 + 1) << '\n';
+ REQUIRE(cap.str() == "1+1=2\n");
+ }
+}
+
+} // anonymous namespace
diff --git a/nihil.core/construct.ccm b/nihil.core/construct.ccm
new file mode 100644
index 0000000..5dd4422
--- /dev/null
+++ b/nihil.core/construct.ccm
@@ -0,0 +1,22 @@
+// This source code is released into the public domain.
+export module nihil.core:construct;
+
+import nihil.std;
+
+namespace nihil {
+
+// A functor that constructs objects of an arbitrary type.
+// Useful for std::views::transform.
+template <typename T>
+struct construct_fn final
+{
+ [[nodiscard]] auto operator()(this construct_fn const &, auto &&...args) -> T
+ {
+ return T(std::forward<decltype(args)>(args)...);
+ }
+};
+
+export template <typename T>
+inline constexpr auto construct = construct_fn<T>{};
+
+} // namespace nihil
diff --git a/nihil.core/ctype.ccm b/nihil.core/ctype.ccm
new file mode 100644
index 0000000..365edc0
--- /dev/null
+++ b/nihil.core/ctype.ccm
@@ -0,0 +1,72 @@
+// This source code is released into the public domain.
+export module nihil.core:ctype;
+
+import nihil.std;
+
+namespace nihil {
+
+/*
+ * ctype_is: wrap std::ctype<T>::is() in a form suitable for use as an algorithm
+ * predicate, i.e., ctype_is(m) will return a functor object that takes any char
+ * type as an argument and returns bool.
+ *
+ * If the locale is not specified, the current global locale is used by default.
+ *
+ * ctype_is copies the locale, so passing a temporary is fine.
+ */
+
+export struct ctype_is final
+{
+ explicit ctype_is(std::ctype_base::mask mask_,
+ std::locale const &locale_ = std::locale()) noexcept
+ : m_mask(mask_)
+ , m_locale(locale_)
+ {
+ }
+
+ [[nodiscard]] auto operator()(this ctype_is const &self, std::integral auto c)
+ {
+ using ctype = std::ctype<decltype(c)>;
+ auto &facet = std::use_facet<ctype>(self.m_locale);
+ return facet.is(self.m_mask, c);
+ }
+
+private:
+ std::ctype_base::mask m_mask;
+ std::locale m_locale;
+};
+
+// Predefined tests for the current global locale.
+
+export inline auto is_space = ctype_is(std::ctype_base::space);
+export inline auto is_print = ctype_is(std::ctype_base::print);
+export inline auto is_cntrl = ctype_is(std::ctype_base::cntrl);
+export inline auto is_upper = ctype_is(std::ctype_base::upper);
+export inline auto is_lower = ctype_is(std::ctype_base::lower);
+export inline auto is_alpha = ctype_is(std::ctype_base::alpha);
+export inline auto is_digit = ctype_is(std::ctype_base::digit);
+export inline auto is_punct = ctype_is(std::ctype_base::punct);
+export inline auto is_xdigit = ctype_is(std::ctype_base::xdigit);
+export inline auto is_blank = ctype_is(std::ctype_base::blank);
+export inline auto is_alnum = ctype_is(std::ctype_base::alnum);
+export inline auto is_graph = ctype_is(std::ctype_base::graph);
+
+// Predefined tests for the C locale. The C locale is guaranteed to always be
+// available, so this doesn't create lifetime issues.
+
+//NOLINTBEGIN: Technically, std::locale::classic() can throw. Assume it doesn't.
+export inline auto is_c_space = ctype_is(std::ctype_base::space, std::locale::classic());
+export inline auto is_c_print = ctype_is(std::ctype_base::print, std::locale::classic());
+export inline auto is_c_cntrl = ctype_is(std::ctype_base::cntrl, std::locale::classic());
+export inline auto is_c_upper = ctype_is(std::ctype_base::upper, std::locale::classic());
+export inline auto is_c_lower = ctype_is(std::ctype_base::lower, std::locale::classic());
+export inline auto is_c_alpha = ctype_is(std::ctype_base::alpha, std::locale::classic());
+export inline auto is_c_digit = ctype_is(std::ctype_base::digit, std::locale::classic());
+export inline auto is_c_punct = ctype_is(std::ctype_base::punct, std::locale::classic());
+export inline auto is_c_xdigit = ctype_is(std::ctype_base::xdigit, std::locale::classic());
+export inline auto is_c_blank = ctype_is(std::ctype_base::blank, std::locale::classic());
+export inline auto is_c_alnum = ctype_is(std::ctype_base::alnum, std::locale::classic());
+export inline auto is_c_graph = ctype_is(std::ctype_base::graph, std::locale::classic());
+//NOLINTEND
+
+} // namespace nihil
diff --git a/nihil.core/ctype.test.cc b/nihil.core/ctype.test.cc
new file mode 100644
index 0000000..df73707
--- /dev/null
+++ b/nihil.core/ctype.test.cc
@@ -0,0 +1,376 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+
+TEST_CASE("ctype: space", "[ctype]") {
+ auto is_utf8_space =
+ nihil::ctype_is(std::ctype_base::space,
+ std::locale("C.UTF-8"));
+
+ // '\v' (vertical tab) is a space
+ REQUIRE(nihil::is_space('\v') == true);
+ REQUIRE(nihil::is_space(L'\v') == true);
+
+ REQUIRE(nihil::is_c_space('\v') == true);
+ REQUIRE(nihil::is_c_space(L'\v') == true);
+
+ REQUIRE(is_utf8_space('\v') == true);
+ REQUIRE(is_utf8_space(L'\v') == true);
+
+ // 'x' is not a space
+ REQUIRE(nihil::is_space('x') == false);
+ REQUIRE(nihil::is_space(L'x') == false);
+
+ REQUIRE(nihil::is_c_space('x') == false);
+ REQUIRE(nihil::is_c_space(L'x') == false);
+
+ REQUIRE(is_utf8_space('x') == false);
+ REQUIRE(is_utf8_space(L'x') == false);
+
+ // U+2003 EM SPACE is a space
+ REQUIRE(nihil::is_space(L'\u2003') == false);
+ REQUIRE(nihil::is_c_space(L'\u2003') == false);
+ REQUIRE(is_utf8_space(L'\u2003') == true);
+}
+
+TEST_CASE("ctype: print", "[ctype]") {
+ auto is_utf8_print =
+ nihil::ctype_is(std::ctype_base::print,
+ std::locale("C.UTF-8"));
+
+ // 'x' is printable
+ REQUIRE(nihil::is_print('x') == true);
+ REQUIRE(nihil::is_print(L'x') == true);
+
+ REQUIRE(nihil::is_c_print('x') == true);
+ REQUIRE(nihil::is_c_print(L'x') == true);
+
+ REQUIRE(is_utf8_print('x') == true);
+ REQUIRE(is_utf8_print(L'x') == true);
+
+ // '\003' is not printable
+ REQUIRE(nihil::is_print('\003') == false);
+ REQUIRE(nihil::is_print(L'\003') == false);
+
+ REQUIRE(nihil::is_c_print('\003') == false);
+ REQUIRE(nihil::is_c_print(L'\003') == false);
+
+ REQUIRE(is_utf8_print('\003') == false);
+ REQUIRE(is_utf8_print(L'\003') == false);
+
+ // U+0410 CYRILLIC CAPITAL LETTER A is printable
+ REQUIRE(nihil::is_print(L'\u0410') == false);
+ REQUIRE(nihil::is_c_print(L'\u0410') == false);
+ REQUIRE(is_utf8_print(L'\u0410') == true);
+}
+
+TEST_CASE("ctype: cntrl", "[ctype]") {
+ auto is_utf8_cntrl =
+ nihil::ctype_is(std::ctype_base::cntrl,
+ std::locale("C.UTF-8"));
+
+ // '\003' is a control character
+ REQUIRE(nihil::is_cntrl('\003') == true);
+ REQUIRE(nihil::is_cntrl(L'\003') == true);
+
+ REQUIRE(nihil::is_c_cntrl('\003') == true);
+ REQUIRE(nihil::is_c_cntrl(L'\003') == true);
+
+ REQUIRE(is_utf8_cntrl('\003') == true);
+ REQUIRE(is_utf8_cntrl(L'\003') == true);
+
+
+ // 'x' is not a control character
+ REQUIRE(nihil::is_cntrl('x') == false);
+ REQUIRE(nihil::is_cntrl(L'x') == false);
+
+ REQUIRE(nihil::is_c_cntrl('x') == false);
+ REQUIRE(nihil::is_c_cntrl(L'x') == false);
+
+ REQUIRE(is_utf8_cntrl('x') == false);
+ REQUIRE(is_utf8_cntrl(L'x') == false);
+
+ // U+00AD SOFT HYPHEN is a control character.
+ REQUIRE(nihil::is_cntrl(L'\u00ad') == false);
+ REQUIRE(nihil::is_c_cntrl(L'\u00ad') == false);
+ REQUIRE(is_utf8_cntrl(L'\u00ad') == true);
+}
+
+TEST_CASE("ctype: upper", "[ctype]") {
+ auto is_utf8_upper =
+ nihil::ctype_is(std::ctype_base::upper,
+ std::locale("C.UTF-8"));
+
+ // 'A' is upper case
+ REQUIRE(nihil::is_upper('A') == true);
+ REQUIRE(nihil::is_upper(L'A') == true);
+
+ REQUIRE(nihil::is_c_upper('A') == true);
+ REQUIRE(nihil::is_c_upper(L'A') == true);
+
+ REQUIRE(is_utf8_upper('A') == true);
+ REQUIRE(is_utf8_upper(L'A') == true);
+
+ // 'a' is not upper case
+ REQUIRE(nihil::is_upper('a') == false);
+ REQUIRE(nihil::is_upper(L'a') == false);
+
+ REQUIRE(nihil::is_c_upper('a') == false);
+ REQUIRE(nihil::is_c_upper(L'a') == false);
+
+ REQUIRE(is_utf8_upper('a') == false);
+ REQUIRE(is_utf8_upper(L'a') == false);
+
+ // U+0410 CYRILLIC CAPITAL LETTER A is upper case
+ REQUIRE(nihil::is_upper(L'\u0410') == false);
+ REQUIRE(nihil::is_c_upper(L'\u0410') == false);
+ REQUIRE(is_utf8_upper(L'\u0410') == true);
+}
+
+TEST_CASE("ctype: lower", "[ctype]") {
+ auto is_utf8_lower =
+ nihil::ctype_is(std::ctype_base::lower,
+ std::locale("C.UTF-8"));
+
+ // 'a' is lower case
+ REQUIRE(nihil::is_lower('a') == true);
+ REQUIRE(nihil::is_lower(L'a') == true);
+
+ REQUIRE(nihil::is_c_lower('a') == true);
+ REQUIRE(nihil::is_c_lower(L'a') == true);
+
+ REQUIRE(is_utf8_lower('a') == true);
+ REQUIRE(is_utf8_lower(L'a') == true);
+
+ // 'A' is not lower case
+ REQUIRE(nihil::is_lower('A') == false);
+ REQUIRE(nihil::is_lower(L'A') == false);
+
+ REQUIRE(nihil::is_c_lower('A') == false);
+ REQUIRE(nihil::is_c_lower(L'A') == false);
+
+ REQUIRE(is_utf8_lower('A') == false);
+ REQUIRE(is_utf8_lower(L'A') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_lower(L'\u0430') == false);
+ REQUIRE(nihil::is_c_lower(L'\u0430') == false);
+ REQUIRE(is_utf8_lower(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: alpha", "[ctype]") {
+ auto is_utf8_alpha =
+ nihil::ctype_is(std::ctype_base::alpha,
+ std::locale("C.UTF-8"));
+
+ // 'a' is alphabetical
+ REQUIRE(nihil::is_alpha('a') == true);
+ REQUIRE(nihil::is_alpha(L'a') == true);
+
+ REQUIRE(nihil::is_c_alpha('a') == true);
+ REQUIRE(nihil::is_c_alpha(L'a') == true);
+
+ REQUIRE(is_utf8_alpha('a') == true);
+ REQUIRE(is_utf8_alpha(L'a') == true);
+
+ // '1' is not alphabetical
+ REQUIRE(nihil::is_alpha('1') == false);
+ REQUIRE(nihil::is_alpha(L'1') == false);
+
+ REQUIRE(nihil::is_c_alpha('1') == false);
+ REQUIRE(nihil::is_c_alpha(L'1') == false);
+
+ REQUIRE(is_utf8_alpha('1') == false);
+ REQUIRE(is_utf8_alpha(L'1') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_alpha(L'\u0430') == false);
+ REQUIRE(nihil::is_c_alpha(L'\u0430') == false);
+ REQUIRE(is_utf8_alpha(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: digit", "[ctype]") {
+ auto is_utf8_digit =
+ nihil::ctype_is(std::ctype_base::digit,
+ std::locale("C.UTF-8"));
+
+ // '1' is a digit
+ REQUIRE(nihil::is_digit('1') == true);
+ REQUIRE(nihil::is_digit(L'1') == true);
+
+ REQUIRE(nihil::is_c_digit('1') == true);
+ REQUIRE(nihil::is_c_digit(L'1') == true);
+
+ REQUIRE(is_utf8_digit('1') == true);
+ REQUIRE(is_utf8_digit(L'1') == true);
+
+ // 'a' is not a digit
+ REQUIRE(nihil::is_digit('a') == false);
+ REQUIRE(nihil::is_digit(L'a') == false);
+
+ REQUIRE(nihil::is_c_digit('a') == false);
+ REQUIRE(nihil::is_c_digit(L'a') == false);
+
+ REQUIRE(is_utf8_digit('a') == false);
+ REQUIRE(is_utf8_digit(L'a') == false);
+
+ // U+0660 ARABIC-INDIC DIGIT ZERO
+ REQUIRE(nihil::is_digit(L'\u0660') == false);
+ REQUIRE(nihil::is_c_digit(L'\u0660') == false);
+ REQUIRE(is_utf8_digit(L'\u0660') == true);
+}
+
+TEST_CASE("ctype: punct", "[ctype]") {
+ auto is_utf8_punct =
+ nihil::ctype_is(std::ctype_base::punct,
+ std::locale("C.UTF-8"));
+
+ // ';' is punctuation
+ REQUIRE(nihil::is_punct(';') == true);
+ REQUIRE(nihil::is_punct(L';') == true);
+
+ REQUIRE(nihil::is_c_punct(';') == true);
+ REQUIRE(nihil::is_c_punct(L';') == true);
+
+ REQUIRE(is_utf8_punct(';') == true);
+ REQUIRE(is_utf8_punct(L';') == true);
+
+ // 'a' is not punctuation
+ REQUIRE(nihil::is_punct('a') == false);
+ REQUIRE(nihil::is_punct(L'a') == false);
+
+ REQUIRE(nihil::is_c_punct('a') == false);
+ REQUIRE(nihil::is_c_punct(L'a') == false);
+
+ REQUIRE(is_utf8_punct('a') == false);
+ REQUIRE(is_utf8_punct(L'a') == false);
+
+ // U+00A1 INVERTED EXCLAMATION MARK
+ REQUIRE(nihil::is_punct(L'\u00A1') == false);
+ REQUIRE(nihil::is_c_punct(L'\u00A1') == false);
+ REQUIRE(is_utf8_punct(L'\u00A1') == true);
+}
+
+TEST_CASE("ctype: xdigit", "[ctype]") {
+ auto is_utf8_xdigit =
+ nihil::ctype_is(std::ctype_base::xdigit,
+ std::locale("C.UTF-8"));
+
+ // 'f' is an xdigit
+ REQUIRE(nihil::is_xdigit('f') == true);
+ REQUIRE(nihil::is_xdigit(L'f') == true);
+
+ REQUIRE(nihil::is_c_xdigit('f') == true);
+ REQUIRE(nihil::is_c_xdigit(L'f') == true);
+
+ REQUIRE(is_utf8_xdigit('f') == true);
+ REQUIRE(is_utf8_xdigit(L'f') == true);
+
+ // 'g' is not an xdigit
+ REQUIRE(nihil::is_xdigit('g') == false);
+ REQUIRE(nihil::is_xdigit(L'g') == false);
+
+ REQUIRE(nihil::is_c_xdigit('g') == false);
+ REQUIRE(nihil::is_c_xdigit(L'g') == false);
+
+ REQUIRE(is_utf8_xdigit('g') == false);
+ REQUIRE(is_utf8_xdigit(L'g') == false);
+}
+
+TEST_CASE("ctype: blank", "[ctype]") {
+ auto is_utf8_blank =
+ nihil::ctype_is(std::ctype_base::blank,
+ std::locale("C.UTF-8"));
+
+ // '\t' is a blank
+ REQUIRE(nihil::is_blank('\t') == true);
+ REQUIRE(nihil::is_blank(L'\t') == true);
+
+ REQUIRE(nihil::is_c_blank('\t') == true);
+ REQUIRE(nihil::is_c_blank(L'\t') == true);
+
+ REQUIRE(is_utf8_blank('\t') == true);
+ REQUIRE(is_utf8_blank(L'\t') == true);
+
+ // '\v' is not a blank
+ REQUIRE(nihil::is_blank('\v') == false);
+ REQUIRE(nihil::is_blank(L'\v') == false);
+
+ REQUIRE(nihil::is_c_blank('\v') == false);
+ REQUIRE(nihil::is_c_blank(L'\v') == false);
+
+ REQUIRE(is_utf8_blank('\v') == false);
+ REQUIRE(is_utf8_blank(L'\v') == false);
+
+ // There don't seem to be any UTF-8 blank characters, at least
+ // in FreeBSD libc.
+}
+
+TEST_CASE("ctype: alnum", "[ctype]") {
+ auto is_utf8_alnum =
+ nihil::ctype_is(std::ctype_base::alnum,
+ std::locale("C.UTF-8"));
+
+ // 'a' is alphanumeric
+ REQUIRE(nihil::is_alnum('a') == true);
+ REQUIRE(nihil::is_alnum(L'a') == true);
+
+ REQUIRE(nihil::is_c_alnum('a') == true);
+ REQUIRE(nihil::is_c_alnum(L'a') == true);
+
+ REQUIRE(is_utf8_alnum('a') == true);
+ REQUIRE(is_utf8_alnum(L'a') == true);
+
+ // '\t' is not a alnum
+ REQUIRE(nihil::is_alnum('\t') == false);
+ REQUIRE(nihil::is_alnum(L'\t') == false);
+
+ REQUIRE(nihil::is_c_alnum('\t') == false);
+ REQUIRE(nihil::is_c_alnum(L'\t') == false);
+
+ REQUIRE(is_utf8_alnum('\t') == false);
+ REQUIRE(is_utf8_alnum(L'\t') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_alnum(L'\u0430') == false);
+ REQUIRE(nihil::is_c_alnum(L'\u0430') == false);
+ REQUIRE(is_utf8_alnum(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: graph", "[ctype]") {
+ auto is_utf8_graph =
+ nihil::ctype_is(std::ctype_base::graph,
+ std::locale("C.UTF-8"));
+
+ // 'a' is graphical
+ REQUIRE(nihil::is_graph('a') == true);
+ REQUIRE(nihil::is_graph(L'a') == true);
+
+ REQUIRE(nihil::is_c_graph('a') == true);
+ REQUIRE(nihil::is_c_graph(L'a') == true);
+
+ REQUIRE(is_utf8_graph('a') == true);
+ REQUIRE(is_utf8_graph(L'a') == true);
+
+ // '\t' is not graphical
+ REQUIRE(nihil::is_graph('\t') == false);
+ REQUIRE(nihil::is_graph(L'\t') == false);
+
+ REQUIRE(nihil::is_c_graph('\t') == false);
+ REQUIRE(nihil::is_c_graph(L'\t') == false);
+
+ REQUIRE(is_utf8_graph('\t') == false);
+ REQUIRE(is_utf8_graph(L'\t') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_graph(L'\u0430') == false);
+ REQUIRE(nihil::is_c_graph(L'\u0430') == false);
+ REQUIRE(is_utf8_graph(L'\u0430') == true);
+}
+
+} // anonymous namespace
diff --git a/nihil.core/error.ccm b/nihil.core/error.ccm
new file mode 100644
index 0000000..5aa23f5
--- /dev/null
+++ b/nihil.core/error.ccm
@@ -0,0 +1,321 @@
+// This source code is released into the public domain.
+export module nihil.core:error;
+
+// error: a type representing an error.
+//
+// An error consists of an immediate cause, which may be a string or
+// std:error_code, and an optional proximate cause, which is another error
+// object. Any number of error objects may be stacked.
+//
+// For example, a failure to open a file might be a stack of two errors:
+//
+// - string, "failed to open /etc/somefile",
+// - std::error_code, "No such file or directory".
+//
+// Calling .str() will format the entire stack starting at that error,
+// for example: "failed to open /etc/somefile: No such file or directory".
+//
+// Errors may be moved and (relatively) cheaply copied, since the cause
+// chain is refcounted.
+//
+// error derives from std::exception, so it may be thrown and caught and
+// provides a useful what(). When throwing errors, creating a derived
+// error will make it easier to distinguish errors when catching them.
+
+import nihil.std;
+
+import :match;
+
+namespace nihil {
+
+// Things which can be errors.
+using error_t = std::variant<std::monostate, std::string, std::error_code, std::error_condition>;
+
+// A proxy class used when constructing errors. This has implicit constructors from various types,
+// which means we don't have to handle every possible combination of types in error itself.
+export struct error_proxy
+{
+ // Construct from...
+
+ // ... a string_view
+ error_proxy(std::string_view const what) // NOLINT
+ : m_error(std::string(what))
+ {
+ }
+
+ // ... an std::string; so we can move the string into place if it's an rvalue.
+ error_proxy(auto &&what) // NOLINT
+ requires(std::same_as<std::remove_cvref_t<decltype(what)>, std::string>)
+ : m_error(std::forward<decltype(what)>(what))
+ {
+ }
+
+ // ... a C string
+ error_proxy(char const *what) // NOLINT
+ : m_error(std::string(what))
+ {
+ }
+
+ // ... an std::error_code
+ error_proxy(std::error_code const what) // NOLINT
+ : m_error(what)
+ {
+ }
+
+ // ... an std::error_condition
+ error_proxy(std::error_condition const what) // NOLINT
+ : m_error(what)
+ {
+ }
+
+ // ... an error code enum
+ template <typename T>
+ requires(std::is_error_code_enum<T>::value)
+ error_proxy(T what) // NOLINT
+ : m_error(make_error_code(what))
+ {
+ }
+
+ // ... an error condition enum
+ template <typename T>
+ requires(std::is_error_condition_enum<T>::value)
+ error_proxy(T what) // NOLINT
+ : m_error(make_error_condition(what))
+ {
+ }
+
+ // Not copyable.
+ error_proxy(error_proxy const &) = delete;
+ auto operator=(error_proxy const &) -> error_proxy & = delete;
+
+ // Not movable.
+ error_proxy(error_proxy &&) = delete;
+ auto operator=(error_proxy &&) -> error_proxy & = delete;
+
+ ~error_proxy() = default;
+
+ // Let error extract the error_t.
+ [[nodiscard]] auto error() && -> error_t &&
+ {
+ return std::move(m_error);
+ }
+
+private:
+ // The error.
+ error_t m_error;
+};
+
+// The error class.
+export struct error : std::exception
+{
+ // Create an empty error, representing success.
+ error() = default;
+
+ // Destroy an error.
+ ~error() override = default;
+
+ // Create an error from an error proxy.
+ explicit error(error_proxy &&proxy)
+ : m_error(std::move(std::move(proxy).error()))
+ {
+ }
+
+ // Create an error from an error proxy and an error cause.
+ error(error_proxy &&proxy, error cause)
+ : m_error(std::move(std::move(proxy).error()))
+ , m_cause(std::make_shared<error>(std::move(cause)))
+ {
+ }
+
+ // Create an error from an error proxy and an error_proxy cause.
+ // For example, error("cannot open file", std::errc::permission_denied).
+ error(error_proxy &&proxy, error_proxy &&cause)
+ : m_error(std::move(std::move(proxy).error()))
+ , m_cause(std::make_shared<error>(std::move(cause)))
+ {
+ }
+
+ // Copyable.
+ error(error const &) = default;
+ auto operator=(error const &) -> error & = default;
+
+ // Movable.
+ error(error &&) noexcept = default;
+ auto operator=(error &&) noexcept -> error & = default;
+
+ // Return the cause of this error.
+ [[nodiscard]] auto cause(this error const &self) -> std::shared_ptr<error> const &
+ {
+ return self.m_cause;
+ }
+
+ // Return the root cause of this error, which may be this object.
+ // For errors caused by an OS error, this will typically be the
+ // error_code error.
+ [[nodiscard]] auto root_cause(this error const &self) -> error const &
+ {
+ auto const *cause = &self;
+ while (cause->m_cause)
+ cause = cause->m_cause.get();
+ return *cause;
+ }
+
+ // Format this error without its cause as a string.
+ [[nodiscard]] auto this_str(this error const &self) -> std::string
+ {
+ return self.m_error | match {
+ [] (std::monostate) -> std::string {
+ return "No error";
+ },
+ [] (std::error_code const &m) {
+ return m.message();
+ },
+ [] (std::error_condition const &m) {
+ return m.message();
+ },
+ [] (std::string const &m) {
+ return m;
+ }
+ };
+ }
+
+ // Format this error and its cause as a string.
+ [[nodiscard]] auto full_str(this error const &self) -> std::string
+ {
+ auto str = self.this_str();
+
+ auto cause = self.cause();
+ while (cause) {
+ str += ": " + cause->this_str();
+ cause = cause->cause();
+ }
+
+ return str;
+ }
+
+ // Return this error's error_code, if any.
+ [[nodiscard]] auto code(this error const &self) -> std::optional<std::error_code>
+ {
+ auto const *code = std::get_if<std::error_code>(&self.m_error);
+ if (code != nullptr)
+ return {*code};
+ return {};
+ }
+
+ // Return this error's error_condition, if any.
+ [[nodiscard]] auto condition(this error const &self) -> std::optional<std::error_condition>
+ {
+ auto const *condition = std::get_if<std::error_condition>(&self.m_error);
+ if (condition != nullptr)
+ return {*condition};
+ return {};
+ }
+
+ // Format this error and its cause as a C string and return it. This is for
+ // compatibility with std::exception. The lifetime of the returned string
+ // is the same as the error object.
+ [[nodiscard]] auto what() const noexcept -> char const * final
+ {
+ if (!m_what)
+ m_what = this->full_str();
+ return m_what->c_str();
+ }
+
+ // Allow error to be implicitly converted to std::expected and std::unexpected, to make
+ // using it with std::expected easier.
+
+ template <typename T>
+ operator std::expected<T, error> () && // NOLINT
+ {
+ return std::unexpected{std::move(*this)};
+ }
+
+ template<typename T>
+ operator std::expected<T, error> () const // NOLINT
+ {
+ return std::unexpected{*this};
+ }
+
+ operator std::unexpected<error> () && // NOLINT
+ {
+ return std::unexpected{std::move(*this)};
+ }
+
+ operator std::unexpected<error> () const // NOLINT
+ {
+ return std::unexpected{*this};
+ }
+
+private:
+ // This error.
+ error_t m_error = make_error_code(std::errc());
+
+ // The cause of this error, if any.
+ std::shared_ptr<error> m_cause;
+
+ // For std::exception::what(), we need to keep the string valid
+ // until we're destroyed.
+ mutable std::optional<std::string> m_what;
+
+ // Equality comparison.
+ [[nodiscard]] friend auto operator==(error const &lhs, error const &rhs) -> bool
+ {
+ return lhs.m_error == rhs.m_error;
+ }
+
+ [[nodiscard]] friend auto operator<=>(error const &lhs, error const &rhs) -> std::strong_ordering
+ {
+ return lhs.m_error <=> rhs.m_error;
+ }
+
+ // Compare an error with an std::error_code.
+ [[nodiscard]] friend auto operator==(error const &lhs, std::error_code const &rhs) -> bool
+ {
+ return lhs.code() == rhs;
+ }
+
+ // Compare an error to an std::error_condition.
+ [[nodiscard]] friend auto operator==(error const &lhs, std::error_condition const &rhs) -> bool
+ {
+ return lhs.condition() == rhs;
+ }
+
+ // Compare an error to an std::error_code enum.
+ [[nodiscard]] friend auto operator==(error const &lhs, auto rhs) -> bool
+ requires(std::is_error_code_enum<decltype(rhs)>::value)
+ {
+ return lhs.code() == rhs;
+ }
+
+ // Compare an error to an std::error_condition enum.
+ [[nodiscard]] friend auto operator==(error const &lhs, auto rhs) -> bool
+ requires(std::is_error_condition_enum<decltype(rhs)>::value)
+ {
+ return lhs.condition() == rhs;
+ }
+
+ // Print an error to a stream.
+ friend auto operator<<(std::ostream &strm, error const &e) -> std::ostream &
+ {
+ return strm << e.full_str();
+ }
+};
+
+} // namespace nihil
+
+// Make error formattable.
+export template <>
+struct std::formatter<nihil::error, char>
+{
+ template <typename ParseContext>
+ constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator
+ {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ auto format(nihil::error const &e, FormatContext &ctx) const -> FormatContext::iterator
+ {
+ return std::ranges::copy(e.full_str(), ctx.out()).out;
+ }
+};
diff --git a/nihil.core/error.test.cc b/nihil.core/error.test.cc
new file mode 100644
index 0000000..f4ec1ee
--- /dev/null
+++ b/nihil.core/error.test.cc
@@ -0,0 +1,273 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+import nihil.util;
+
+namespace {
+inline constexpr auto *test_tags = "[nihil][nihil.error]";
+
+TEST_CASE("error: invariants", test_tags)
+{
+ static_assert(std::destructible<nihil::error>);
+ static_assert(std::default_initializable<nihil::error>);
+ static_assert(std::move_constructible<nihil::error>);
+ static_assert(std::copy_constructible<nihil::error>);
+ static_assert(std::equality_comparable<nihil::error>);
+ static_assert(std::totally_ordered<nihil::error>);
+ static_assert(std::swappable<nihil::error>);
+ static_assert(std::regular<nihil::error>);
+}
+
+SCENARIO("A nihil::error can be constructed from a C string", test_tags)
+{
+ GIVEN ("An error object constructed from a string") {
+ auto e = nihil::error("an error");
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "an error");
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed from an std::string lvalue", test_tags)
+{
+ GIVEN ("An error object constructed from an std::string lvalue") {
+ auto s = std::string("an error");
+ auto e = nihil::error(s);
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "an error");
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed from an std::string rvalue", test_tags)
+{
+ GIVEN ("An error object constructed from an std::string rvalue") {
+ auto e = nihil::error(std::string("an error"));
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "an error");
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed from an std::string_view lvalue", test_tags)
+{
+ GIVEN ("An error object constructed from an std::string_view lvalue") {
+ auto s = std::string_view("an error");
+ auto e = nihil::error(s);
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "an error");
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed from an std::string_view rvalue", test_tags)
+{
+ GIVEN ("An error object constructed from an std::string_view rvalue") {
+ auto e = nihil::error(std::string_view("an error"));
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "an error");
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed from an std::error_condition", test_tags)
+{
+ GIVEN ("An error object constructed from std::errc::invalid_argument") {
+ auto e = nihil::error(std::error_condition(std::errc::invalid_argument));
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "Invalid argument");
+ }
+
+ AND_THEN ("condition() should return the error code") {
+ REQUIRE(e.condition().has_value());
+ REQUIRE(*e.condition() == std::errc::invalid_argument);
+ }
+
+ AND_THEN ("The error should be comparable to the error code") {
+ REQUIRE(e == std::errc::invalid_argument);
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed from an std::errc", test_tags)
+{
+ GIVEN ("An error object constructed from std::errc::invalid_argument") {
+ auto e = nihil::error(std::errc::invalid_argument);
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "Invalid argument");
+ }
+
+ AND_THEN ("condition() should return the error code") {
+ REQUIRE(e.condition().has_value());
+ REQUIRE(*e.condition() == std::errc::invalid_argument);
+ }
+
+ AND_THEN ("The error should be comparable to the error code") {
+ REQUIRE(e == std::errc::invalid_argument);
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed from a nihil::errc", test_tags)
+{
+ GIVEN ("An error object constructed from std::errc::invalid_argument") {
+ auto e = nihil::error(nihil::errc::incomplete_command);
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "Incomplete command");
+ }
+
+ AND_THEN ("condition() should return the error code") {
+ REQUIRE(e.condition().has_value());
+ REQUIRE(*e.condition() == nihil::errc::incomplete_command);
+ }
+
+ AND_THEN ("The error should be comparable to the error code") {
+ REQUIRE(e == nihil::errc::incomplete_command);
+ }
+ }
+}
+
+SCENARIO("A nihil::error can be constructed with a cause", test_tags)
+{
+ GIVEN ("An error object constructed with a cause") {
+ auto e = nihil::error("an error", std::errc::invalid_argument);
+
+ THEN ("full_str() should return the string") {
+ REQUIRE(e.full_str() == "an error: Invalid argument");
+ }
+
+ AND_THEN ("cause() should return the cause") {
+ REQUIRE(e.cause());
+ REQUIRE(*e.cause() == std::errc::invalid_argument);
+ }
+ }
+}
+
+SCENARIO("std::format with a nihil::error", test_tags)
+{
+ GIVEN ("A nihil::error with no cause") {
+ auto e = nihil::error("an error");
+
+ THEN ("std::format should return the string") {
+ REQUIRE(std::format("{}", e) == "an error");
+ }
+
+ AND_THEN ("std::format should return the same as full_str()") {
+ REQUIRE(std::format("{}", e) == e.full_str());
+ }
+ }
+
+ GIVEN ("A nihil::error with a cause") {
+ auto e = nihil::error("an error", std::errc::invalid_argument);
+
+ THEN ("std::format should return the string") {
+ REQUIRE(std::format("{}", e) == "an error: Invalid argument");
+ }
+
+ AND_THEN ("std::format should return the same as full_str()") {
+ REQUIRE(std::format("{}", e) == e.full_str());
+ }
+ }
+}
+
+SCENARIO("Print a nihil::error to an std::ostream", test_tags)
+{
+ GIVEN ("A nihil::error with no cause") {
+ auto e = nihil::error("an error");
+
+ THEN ("The error should be printed to the stream") {
+ auto ss = std::stringstream();
+ ss << e;
+ REQUIRE(ss.str() == "an error");
+ }
+ }
+
+ GIVEN ("A nihil::error with a cause") {
+ auto e = nihil::error("an error", std::errc::invalid_argument);
+
+ THEN ("The error should be printed to the stream") {
+ auto ss = std::stringstream();
+ ss << e;
+ REQUIRE(ss.str() == "an error: Invalid argument");
+ }
+ }
+}
+
+SCENARIO("Comparison of nihil::error with operator==", test_tags)
+{
+ GIVEN ("Two nihil::error objects constructed from the same string") {
+ auto e1 = nihil::error("an error");
+ auto e2 = nihil::error("an error");
+
+ THEN ("The two objects should be equal") {
+ REQUIRE(e1 == e2);
+ }
+ }
+
+ GIVEN ("Two nihil::error objects constructed from different strings") {
+ auto e1 = nihil::error("an error");
+ auto e2 = nihil::error("another error");
+
+ THEN ("The two objects should not be equal") {
+ REQUIRE(e1 != e2);
+ }
+ }
+
+ GIVEN ("Two nihil::error objects constructed from the same error code") {
+ auto e1 = nihil::error(std::errc::invalid_argument);
+ auto e2 = nihil::error(std::errc::invalid_argument);
+
+ THEN ("The two objects should be equal") {
+ REQUIRE(e1 == e2);
+ }
+ }
+
+ GIVEN ("Two nihil::error objects constructed from different error codes") {
+ auto e1 = nihil::error(std::errc::invalid_argument);
+ auto e2 = nihil::error(std::errc::permission_denied);
+
+ THEN ("The two objects should not be equal") {
+ REQUIRE(e1 != e2);
+ }
+ }
+}
+
+SCENARIO("Comparison of nihil::error with operator<", test_tags)
+{
+ GIVEN ("Two nihil::error objects constructed from the same string") {
+ auto e1 = nihil::error("aaa");
+ auto e2 = nihil::error("zzz");
+
+ THEN ("aaa should be less than zzz") {
+ REQUIRE(e1 < e2);
+ }
+ }
+}
+
+SCENARIO("Throwing and catching a nihil::error object", test_tags)
+{
+ GIVEN ("A nihil::error object") {
+ THEN ("We should be able to throw and catch the error") {
+ REQUIRE_THROWS_AS(throw nihil::error("an error"), nihil::error);
+
+ try {
+ throw nihil::error("an error");
+ } catch (nihil::error const &e) {
+ REQUIRE(e.full_str() == "an error");
+ };
+ }
+ }
+}
+
+} // anonymous namespace
diff --git a/nihil.core/flagset.ccm b/nihil.core/flagset.ccm
new file mode 100644
index 0000000..cea9889
--- /dev/null
+++ b/nihil.core/flagset.ccm
@@ -0,0 +1,200 @@
+// This source code is released into the public domain.
+export module nihil.core:flagset;
+
+/*
+ * flagset: a type-safe flags type.
+ */
+
+import nihil.std;
+
+namespace nihil {
+
+export template <std::integral base_type, typename Tag>
+struct flagset final
+{
+ using underlying_type = base_type;
+
+ /*
+ * Create an empty flags.
+ */
+ flagset() noexcept = default;
+
+ /*
+ * Copyable.
+ */
+ flagset(flagset const &other) noexcept
+ : m_value(other.m_value)
+ {
+ }
+
+ /*
+ * Create flags from an integer mask.
+ */
+ template <base_type flag>
+ [[nodiscard]] static constexpr auto mask() noexcept -> flagset
+ {
+ return flagset(flag);
+ }
+
+ /*
+ * Create flags for a specific bit.
+ */
+ template <unsigned bitnr>
+ [[nodiscard]] static constexpr auto bit() noexcept -> flagset
+ {
+ static_assert(bitnr < std::numeric_limits<base_type>::digits);
+ return flagset(static_cast<base_type>(1) << bitnr);
+ }
+
+ /*
+ * Create flags from a runtime value.
+ */
+ [[nodiscard]] static auto from_int(base_type value) noexcept -> flagset
+ {
+ return flagset(value);
+ }
+
+ /*
+ * Assign this flagset.
+ */
+ auto operator=(this flagset &lhs, flagset rhs) noexcept -> flagset &
+ {
+ if (&lhs != &rhs)
+ lhs.m_value = rhs.m_value;
+ return lhs;
+ }
+
+ /*
+ * The integer value of this flagset.
+ */
+ [[nodiscard]] constexpr auto value(this flagset self) noexcept -> base_type
+ {
+ return self.m_value;
+ }
+
+ /*
+ * True if this flagset has any bits set.
+ */
+ [[nodiscard]] explicit constexpr operator bool(this flagset self) noexcept
+ {
+ return self.m_value != 0;
+ }
+
+ /*
+ * Set bits.
+ */
+ constexpr auto operator|=(this flagset &lhs, flagset rhs) noexcept -> flagset &
+ {
+ lhs.m_value |= rhs.value();
+ return lhs;
+ }
+
+ /*
+ * Mask bits.
+ */
+ constexpr auto operator&=(this flagset &lhs, flagset rhs) noexcept -> flagset &
+ {
+ lhs.m_value &= rhs.value();
+ return lhs;
+ }
+
+ /*
+ * Invert bits.
+ */
+ [[nodiscard]] constexpr auto operator~(this flagset self) noexcept -> flagset
+ {
+ return flagset(~self.m_value);
+ }
+
+ /*
+ * xor bits.
+ */
+ constexpr auto operator^=(this flagset &lhs, flagset rhs) noexcept -> flagset
+ {
+ lhs.m_value ^= rhs.value();
+ return lhs;
+ }
+
+private:
+ base_type m_value = 0;
+
+ explicit constexpr flagset(base_type mask) noexcept
+ : m_value(mask)
+ {
+ }
+
+ [[nodiscard]] friend auto operator|(flagset lhs, flagset rhs) noexcept -> flagset
+ {
+ return (lhs |= rhs);
+ }
+
+ [[nodiscard]] friend auto operator&(flagset lhs, flagset rhs) noexcept -> flagset
+ {
+ return (lhs &= rhs);
+ }
+
+ [[nodiscard]] friend auto operator^(flagset lhs, flagset rhs) noexcept -> flagset
+ {
+ return (lhs ^= rhs);
+ }
+
+ [[nodiscard]] friend auto
+ operator==(flagset lhs, flagset rhs) noexcept -> bool
+ {
+ return lhs.value() == rhs.value();
+ }
+
+ friend auto operator<<(std::ostream &strm, flagset<base_type, Tag> flags) -> std::ostream &
+ {
+ std::print(strm, "{}", flags);
+ return strm;
+ }
+};
+
+} // namespace nihil
+
+/*
+ * Formatting for flagset.
+ */
+export template <std::integral base_type, typename Tag, typename Char>
+struct std::formatter<nihil::flagset<base_type, Tag>, Char>
+{
+ using flags_t = nihil::flagset<base_type, Tag>;
+
+ template <typename ParseContext>
+ constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator
+ {
+ return ctx.begin();
+ }
+
+ template <typename FmtContext>
+ auto format(flags_t flags, FmtContext &ctx) const -> FmtContext::iterator
+ {
+ auto constexpr digits = std::numeric_limits<base_type>::digits;
+ auto value = flags.value();
+ auto it = ctx.out();
+ *it++ = Char{'<'};
+
+ auto printed_any = false;
+
+ for (unsigned i = 0; i < digits; ++i) {
+ unsigned bit = (digits - 1) - i;
+
+ auto this_bit = static_cast<base_type>(1) << bit;
+ if ((value & this_bit) == 0)
+ continue;
+
+ if (printed_any)
+ *it++ = Char{','};
+
+ if (bit > 10)
+ *it++ = Char{'0'} + (bit / 10);
+ *it++ = Char{'0'} + (bit % 10);
+
+ printed_any = true;
+ }
+
+ *it++ = Char{'>'};
+ return it;
+ }
+};
diff --git a/nihil.core/flagset.test.cc b/nihil.core/flagset.test.cc
new file mode 100644
index 0000000..353b638
--- /dev/null
+++ b/nihil.core/flagset.test.cc
@@ -0,0 +1,144 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+struct test_tag
+{
+};
+using testflags = nihil::flagset<unsigned, test_tag>;
+
+constexpr auto zero = testflags::bit<0>();
+constexpr auto one = testflags::bit<1>();
+constexpr auto two = testflags::bit<2>();
+constexpr auto twelve = testflags::bit<12>();
+
+TEST_CASE("flagset: invariant", "[nihil]")
+{
+ static_assert(std::regular<testflags>);
+ static_assert(sizeof(testflags) == sizeof(testflags::underlying_type));
+}
+
+TEST_CASE("flagset: bit<>", "[nihil]")
+{
+ REQUIRE(zero.value() == 0x1);
+ REQUIRE(one.value() == 0x2);
+}
+
+TEST_CASE("flagset: mask<>", "[nihil]")
+{
+ auto zero_ = testflags::mask<0x1>();
+ auto one_ = testflags::mask<0x2>();
+
+ REQUIRE(zero_ == zero);
+ REQUIRE(one_ == one);
+}
+
+TEST_CASE("flagset: constructor", "[nihil]")
+{
+ SECTION ("default construct") {
+ auto flags = testflags();
+ REQUIRE(flags.value() == 0);
+ }
+
+ SECTION ("construct from int") {
+ auto flags = testflags::from_int(one.value() | zero.value());
+ REQUIRE(flags == (one | zero));
+ }
+
+ SECTION ("copy construct") {
+ auto flags = one;
+ auto flags2(flags);
+ REQUIRE(flags == flags2);
+ }
+}
+
+TEST_CASE("flagset: operators", "[nihil]")
+{
+ SECTION ("operator|") {
+ REQUIRE((zero | one).value() == 0x3);
+ }
+
+ SECTION ("operator|=") {
+ auto flags = zero;
+ flags |= one;
+ REQUIRE(flags.value() == 0x3);
+ }
+
+ SECTION ("operator&") {
+ auto flags = zero | one;
+
+ REQUIRE((flags & zero) == zero);
+ }
+
+ SECTION ("operator&=") {
+ auto flags = zero | one | two;
+ REQUIRE(flags.value() == 0x7);
+ flags &= (zero | one);
+ REQUIRE(flags.value() == 0x3);
+ }
+
+ SECTION ("operator^") {
+ auto flags = zero | one;
+ REQUIRE((flags ^ (one | two)) == (zero | two));
+ }
+
+ SECTION ("operator^=") {
+ auto flags = zero | one;
+ flags ^= (one | two);
+ REQUIRE(flags == (zero | two));
+ }
+
+ SECTION ("operator~") {
+ auto flags = ~zero;
+ REQUIRE(flags.value() == ~static_cast<unsigned>(1));
+ }
+
+ SECTION ("operator==") {
+ auto flags = zero;
+ REQUIRE(flags == zero);
+ REQUIRE(flags != one);
+ }
+}
+
+TEST_CASE("flagset: assignment", "[nihil]")
+{
+ auto flags = zero;
+ REQUIRE(flags == zero);
+
+ flags = one;
+ REQUIRE(flags == one);
+ REQUIRE(flags != zero);
+}
+
+TEST_CASE("flagset: format", "[nihil]")
+{
+ REQUIRE(std::format("{}", testflags()) == "<>");
+ REQUIRE(std::format("{}", zero) == "<0>");
+ REQUIRE(std::format("{}", one) == "<1>");
+ REQUIRE(std::format("{}", zero | one) == "<1,0>");
+
+ REQUIRE(std::format("{}", twelve) == "<12>");
+ REQUIRE(std::format("{}", twelve | one) == "<12,1>");
+}
+
+TEST_CASE("flagset: ostream operator<<", "[nihil]")
+{
+ auto write = [](testflags flags) -> std::string {
+ auto strm = std::ostringstream();
+ strm << flags;
+ return strm.str();
+ };
+
+ REQUIRE(write(testflags()) == "<>");
+ REQUIRE(write(zero) == "<0>");
+ REQUIRE(write(one) == "<1>");
+ REQUIRE(write(zero | one) == "<1,0>");
+
+ REQUIRE(write(twelve) == "<12>");
+ REQUIRE(write(twelve | one) == "<12,1>");
+}
+} // anonymous namespace
diff --git a/nihil.core/guard.ccm b/nihil.core/guard.ccm
new file mode 100644
index 0000000..0576042
--- /dev/null
+++ b/nihil.core/guard.ccm
@@ -0,0 +1,41 @@
+// This source code is released into the public domain.
+export module nihil.core:guard;
+
+import nihil.std;
+
+namespace nihil {
+
+// guard: invoke a callable when this object is destroyed; this is similar to
+// scope_exit from the library fundamentals TS, which LLVM doesn't implement.
+export template<std::invocable F>
+struct guard final {
+ // Initialise the guard with a callable we will invoke later.
+ explicit guard(F func) : m_func(std::move(func)) {}
+
+ // We are being destroyed, so call the callable.
+ // If the callable throws, std::terminate() will be called.
+ ~guard()
+ {
+ if (m_func)
+ std::invoke(*m_func);
+ }
+
+ // Release the guard. This turns the destructor into a no-op.
+ auto release(this guard &self) noexcept -> void
+ {
+ self.m_func.reset();
+ }
+
+ // Not default-constructible, movable or copyable.
+ guard() = delete;
+ guard(guard const &) = delete;
+ guard(guard &&) noexcept = delete;
+ auto operator=(this guard &, guard const &) -> guard & = delete;
+ auto operator=(this guard &, guard &&) noexcept -> guard & = delete;
+
+private:
+ // The callable to be invoked when we are destroyed.
+ std::optional<F> m_func;
+};
+
+} // namespace nihil
diff --git a/nihil.core/guard.test.cc b/nihil.core/guard.test.cc
new file mode 100644
index 0000000..43076f5
--- /dev/null
+++ b/nihil.core/guard.test.cc
@@ -0,0 +1,16 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.core;
+
+TEST_CASE("guard: basic", "[guard]") {
+ int n = 0;
+
+ {
+ auto guard = nihil::guard([&] { n = 1; });
+ REQUIRE(n == 0);
+ }
+
+ REQUIRE(n == 1);
+}
diff --git a/nihil.core/match.ccm b/nihil.core/match.ccm
new file mode 100644
index 0000000..e1c894c
--- /dev/null
+++ b/nihil.core/match.ccm
@@ -0,0 +1,18 @@
+// This source code is released into the public domain.
+export module nihil.core:match;
+
+import nihil.std;
+
+namespace nihil {
+
+export template<class... Ts>
+struct match : Ts... { using Ts::operator()...; };
+
+export template<typename... Ts, typename... Fs>
+[[nodiscard]] constexpr decltype(auto) operator|
+ (std::variant<Ts...> const &v, match<Fs...> const &match)
+{
+ return std::visit(match, v);
+}
+
+} // namespace nihil
diff --git a/nihil.core/match.test.cc b/nihil.core/match.test.cc
new file mode 100644
index 0000000..2394651
--- /dev/null
+++ b/nihil.core/match.test.cc
@@ -0,0 +1,32 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+TEST_CASE("match", "[nihil]")
+{
+ using namespace nihil;
+ using namespace std::literals;
+
+ auto v = std::variant<int, std::string>(42);
+
+ auto s = v | match {
+ [](int) { return "int"s; },
+ [](std::string const &) { return "string"s; }
+ };
+
+ REQUIRE(s == "int");
+
+ v = "test"s;
+
+ s = v | match {
+ [](int) { return "int"s; },
+ [](std::string const &) { return "string"s; }
+ };
+
+ REQUIRE(s == "string");
+}
+} // anonymous namespace
diff --git a/nihil.core/monad.ccm b/nihil.core/monad.ccm
new file mode 100644
index 0000000..ae26416
--- /dev/null
+++ b/nihil.core/monad.ccm
@@ -0,0 +1,282 @@
+/*
+ * From https://github.com/toby-allsopp/coroutine_monad
+ *
+ * Copyright (c) 2017 Toby Allsopp
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+export module nihil.core:monad;
+
+import nihil.std;
+
+namespace nihil {
+
+/**********************************************************************
+ * return_object_holder
+ */
+
+// An object that starts out unitialized. Initialized by a call to emplace.
+export template <typename T>
+using deferred = std::optional<T>;
+
+export template <typename T>
+struct return_object_holder {
+ // The staging object that is returned (by copy/move) to the caller of
+ // the coroutine.
+ deferred<T> stage;
+ return_object_holder*& p;
+
+ // When constructed, we assign a pointer to ourselves to the supplied
+ // reference to pointer.
+ return_object_holder(return_object_holder*& p)
+ : stage{}
+ , p(p)
+ {
+ p = this;
+ }
+
+ // Copying doesn't make any sense (which copy should the pointer refer
+ // to?).
+ return_object_holder(return_object_holder const&) = delete;
+
+ // To move, we just update the pointer to point at the new object.
+ return_object_holder(return_object_holder&& other)
+ : stage(std::move(other.stage))
+ , p(other.p)
+ {
+ p = this;
+ }
+
+ // Assignment doesn't make sense.
+ void operator=(return_object_holder const&) = delete;
+ void operator=(return_object_holder&&) = delete;
+
+ // A non-trivial destructor is required until
+ // https://bugs.llvm.org//show_bug.cgi?id=28593 is fixed.
+ ~return_object_holder() {}
+
+ // Construct the staging value; arguments are perfect forwarded to T's
+ // constructor.
+ template <typename... Args>
+ void emplace(Args&&... args)
+ {
+ stage.emplace(std::forward<Args>(args)...);
+ }
+
+ // We assume that we will be converted only once, so we can move from
+ // the staging object. We also assume that `emplace` has been called
+ // at least once.
+ operator T()
+ {
+ return std::move(*stage);
+ }
+};
+
+export template <typename T>
+auto make_return_object_holder(return_object_holder<T>*& p)
+{
+ return return_object_holder<T>{p};
+}
+
+/**********************************************************************
+ * std::optional
+ */
+
+template <typename T>
+struct optional_promise {
+ return_object_holder<std::optional<T>>* data;
+
+ auto get_return_object()
+ {
+ return make_return_object_holder(data);
+ }
+
+ auto initial_suspend() noexcept -> std::suspend_never
+ {
+ return {};
+ }
+
+ auto final_suspend() noexcept -> std::suspend_never
+ {
+ return {};
+ }
+
+ void return_value(T x)
+ {
+ data->emplace(std::move(x));
+ }
+
+ void unhandled_exception()
+ {
+ std::rethrow_exception(std::current_exception());
+ }
+};
+
+} // namespace nihil
+
+export template <typename T, typename... Args>
+struct std::coroutine_traits<std::optional<T>, Args...> {
+ using promise_type = nihil::optional_promise<T>;
+};
+
+namespace nihil {
+
+template <typename T>
+struct optional_awaitable {
+ std::optional<T> o;
+
+ auto await_ready()
+ {
+ return o.has_value();
+ }
+
+ auto await_resume()
+ {
+ return *o;
+ }
+
+ template <typename U>
+ void await_suspend(std::coroutine_handle<optional_promise<U>> h)
+ {
+ h.promise().data->emplace(std::nullopt);
+ h.destroy();
+ }
+};
+
+} // namespace nihil
+
+namespace std {
+
+export template <typename T>
+auto operator co_await(std::optional<T> o) {
+ return nihil::optional_awaitable<T>{std::move(o)};
+}
+
+} // namespace std
+
+/**********************************************************************
+ * std::expected
+ */
+
+namespace nihil {
+
+export template <typename T, typename E>
+struct expected_promise_base {
+ return_object_holder<std::expected<T, E>>* data;
+
+ auto get_return_object()
+ {
+ return make_return_object_holder(data);
+ }
+
+ auto initial_suspend() noexcept -> std::suspend_never
+ {
+ return {};
+ }
+
+ auto final_suspend() noexcept -> std::suspend_never
+ {
+ return {};
+ }
+
+ void unhandled_exception()
+ {
+ std::rethrow_exception(std::current_exception());
+ }
+};
+
+export template <typename T, typename E>
+struct expected_promise : expected_promise_base<T, E> {
+ void return_value(this expected_promise &self, std::unexpected<E> err)
+ {
+ self.data->emplace(std::move(err));
+ }
+
+ void return_value(this expected_promise &self, T o)
+ {
+ self.data->emplace(std::move(o));
+ }
+};
+
+export template <typename E>
+struct expected_promise<void, E> : expected_promise_base<void, E> {
+ void return_value(this expected_promise &self, std::unexpected<E> err)
+ {
+ self.data->emplace(std::move(err));
+ }
+
+ void return_value(this expected_promise &self, std::expected<void, E> o)
+ {
+ self.data->emplace(std::move(o));
+ }
+};
+
+} // namespace nihil
+
+export template <typename T, typename E, typename... Args>
+struct std::coroutine_traits<std::expected<T, E>, Args...> {
+ using promise_type = nihil::expected_promise<T, E>;
+};
+
+namespace nihil {
+
+export template<typename T, typename E>
+struct expected_awaitable_base {
+ std::expected<T, E> o;
+
+ auto await_ready()
+ {
+ return o.has_value();
+ }
+
+ template <typename P>
+ void await_suspend(std::coroutine_handle<P> h)
+ {
+ h.promise().data->emplace(std::unexpected(o.error()));
+ h.destroy();
+ }
+};
+
+export template <typename T, typename E>
+struct expected_awaitable : expected_awaitable_base<T, E> {
+ auto await_resume(this expected_awaitable &self)
+ {
+ return std::move(*self.o);
+ }
+};
+
+export template <typename E>
+struct expected_awaitable<void, E> : expected_awaitable_base<void, E> {
+ auto await_resume(this expected_awaitable &)
+ {
+ return std::expected<void, E>();
+ }
+};
+
+} // namespace nihil
+
+namespace std {
+
+export template <typename T, typename E>
+auto operator co_await(std::expected<T, E> o) {
+ return nihil::expected_awaitable<T, E>{std::move(o)};
+}
+
+} // namespace std
diff --git a/nihil.core/monad.test.cc b/nihil.core/monad.test.cc
new file mode 100644
index 0000000..e5003ac
--- /dev/null
+++ b/nihil.core/monad.test.cc
@@ -0,0 +1,65 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+TEST_CASE("monad: co_await std::optional<> with value", "[nihil]")
+{
+ auto get_value = [] -> std::optional<int> {
+ return 42;
+ };
+
+ auto try_get_value = [&get_value] -> std::optional<int> {
+ co_return co_await get_value();
+ };
+
+ auto o = try_get_value();
+ REQUIRE(o == 42);
+}
+
+TEST_CASE("monad: co_await std::optional<> without value", "[nihil]")
+{
+ auto get_value = [] -> std::optional<int> {
+ return {};
+ };
+
+ auto try_get_value = [&get_value] -> std::optional<int> {
+ co_return co_await get_value();
+ };
+
+ auto o = try_get_value();
+ REQUIRE(!o.has_value());
+}
+
+TEST_CASE("monad: co_await std::expected<> with value", "[nihil]")
+{
+ auto get_value = [] -> std::expected<int, std::string> {
+ return 42;
+ };
+
+ auto try_get_value = [&get_value] -> std::expected<int, std::string> {
+ co_return co_await get_value();
+ };
+
+ auto o = try_get_value();
+ REQUIRE(o == 42);
+}
+
+TEST_CASE("monad: co_await std::expected<> with error", "[nihil]")
+{
+ auto get_value = [] -> std::expected<int, std::string> {
+ return std::unexpected("error");
+ };
+
+ auto try_get_value = [&get_value] -> std::expected<int, std::string> {
+ co_return co_await get_value();
+ };
+
+ auto o = try_get_value();
+ REQUIRE(!o);
+ REQUIRE(o.error() == "error");
+}
+} // anonymous namespace
diff --git a/nihil.core/next_word.ccm b/nihil.core/next_word.ccm
new file mode 100644
index 0000000..7fcb2be
--- /dev/null
+++ b/nihil.core/next_word.ccm
@@ -0,0 +1,35 @@
+// This source code is released into the public domain.
+export module nihil.core:next_word;
+
+import nihil.std;
+
+import :ctype;
+import :skipws;
+
+namespace nihil {
+
+// Return the next word from a string_view. Skips leading whitespace, so
+// calling this repeatedly will return each word from the string.
+export template <typename Char>
+[[nodiscard]]
+auto next_word(std::basic_string_view<Char> text, std::locale const &locale = std::locale())
+ -> std::pair<std::basic_string_view<Char>, std::basic_string_view<Char>>
+{
+ text = skipws(text, locale);
+
+ auto is_space = ctype_is(std::ctype_base::space, locale);
+ auto split_pos = std::ranges::find_if(text, is_space);
+
+ return {{std::ranges::begin(text), split_pos}, {split_pos, std::ranges::end(text)}};
+}
+
+export template <typename Char>
+auto next_word(std::basic_string_view<Char> *text, std::locale const &locale = std::locale())
+ -> std::basic_string_view<Char>
+{
+ auto [word, rest] = next_word(*text, locale);
+ *text = rest;
+ return word;
+}
+
+} // namespace nihil
diff --git a/nihil.core/next_word.test.cc b/nihil.core/next_word.test.cc
new file mode 100644
index 0000000..2566ea6
--- /dev/null
+++ b/nihil.core/next_word.test.cc
@@ -0,0 +1,65 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+
+TEST_CASE("next_word: basic", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = "foo bar baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == "foo");
+ REQUIRE(words.second == " bar baz");
+
+ auto word = nihil::next_word(&s);
+ REQUIRE(word == "foo");
+ REQUIRE(s == " bar baz");
+}
+
+TEST_CASE("next_word: multiple spaces", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = "foo bar baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == "foo");
+ REQUIRE(words.second == " bar baz");
+
+ auto word = nihil::next_word(&s);
+ REQUIRE(word == "foo");
+ REQUIRE(s == " bar baz");
+}
+
+TEST_CASE("next_word: leading spaces", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = " \tfoo bar baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == "foo");
+ REQUIRE(words.second == " bar baz");
+
+ auto word = nihil::next_word(&s);
+ REQUIRE(word == "foo");
+ REQUIRE(s == " bar baz");
+}
+
+TEST_CASE("next_word: locale", "[next_word]")
+{
+ using namespace std::literals;
+ auto s = L"\u2003foo\u2003bar\u2003baz"sv;
+
+ auto words = nihil::next_word(s);
+ REQUIRE(words.first == s);
+
+ words = nihil::next_word(s, std::locale("C.UTF-8"));
+ REQUIRE(words.first == L"foo");
+ REQUIRE(words.second == L"\u2003bar\u2003baz");
+}
+
+} // anonymous namespace
diff --git a/nihil.core/nihil.core.ccm b/nihil.core/nihil.core.ccm
index 0aa5402..37ad032 100644
--- a/nihil.core/nihil.core.ccm
+++ b/nihil.core/nihil.core.ccm
@@ -1,5 +1,20 @@
// This source code is released into the public domain.
export module nihil.core;
+export import :capture_stream;
+export import :construct;
+export import :ctype;
export import :errc;
+export import :error;
export import :features;
+export import :flagset;
+export import :guard;
+export import :match;
+export import :monad;
+export import :next_word;
+export import :parse_size;
+export import :save_errno;
+export import :skipws;
+export import :sys_error;
+export import :tabulate;
+export import :uuid;
diff --git a/nihil.core/parse_size.ccm b/nihil.core/parse_size.ccm
new file mode 100644
index 0000000..5f80755
--- /dev/null
+++ b/nihil.core/parse_size.ccm
@@ -0,0 +1,90 @@
+// This source code is released into the public domain.
+export module nihil.core:parse_size;
+
+import nihil.std;
+
+import :ctype;
+import :errc;
+import :error;
+import :monad;
+
+namespace nihil {
+
+export template <typename Char>
+auto get_multiplier(Char c) -> std::expected<std::uint64_t, error>
+{
+ auto ret = std::uint64_t{1};
+
+ // clang-format off
+ switch (c) {
+ case 'p': case 'P': ret *= 1024; // NOLINT
+ case 't': case 'T': ret *= 1024; // NOLINT
+ case 'g': case 'G': ret *= 1024; // NOLINT
+ case 'm': case 'M': ret *= 1024; // NOLINT
+ case 'k': case 'K': ret *= 1024; // NOLINT
+ return ret;
+
+ default:
+ return error(errc::invalid_unit);
+ }
+ // clang-format on
+}
+
+// Parse a string containing a human-formatted size, such as "1024"
+// or "4g". Parsing is always done in the "C" locale and does not
+// recognise thousands separators or negative numbers.
+export template <typename T, typename Char>
+[[nodiscard]]
+auto parse_size(std::basic_string_view<Char> str) -> std::expected<T, error>
+{
+ // Extract the numeric part of the string.
+ auto it = std::ranges::find_if_not(str, is_c_digit);
+ auto num_str = std::basic_string_view<Char>(std::ranges::begin(str), it);
+
+ if (num_str.empty())
+ co_return error(errc::empty_string);
+
+ auto ret = T{0};
+
+ for (auto c : num_str) {
+ if (ret > (std::numeric_limits<T>::max() / 10))
+ co_return error(std::errc::result_out_of_range);
+ ret *= 10;
+
+ auto digit = static_cast<T>(c - '0');
+ if ((std::numeric_limits<T>::max() - digit) < ret)
+ co_return error(std::errc::result_out_of_range);
+ ret += digit;
+ }
+
+ if (it == str.end())
+ // No multiplier.
+ co_return ret;
+
+ auto mchar = *it++;
+
+ if (it != str.end())
+ // Multiplier is more than one character.
+ co_return error(errc::invalid_unit);
+
+ auto mult = co_await get_multiplier(mchar);
+
+ if (std::cmp_greater(ret, std::numeric_limits<T>::max() / mult))
+ co_return error(std::errc::result_out_of_range);
+
+ co_return ret *mult;
+}
+
+export template <typename T>
+[[nodiscard]] auto parse_size(char const *s)
+{
+ return parse_size<T>(std::string_view(s));
+}
+
+export template <typename T>
+[[nodiscard]] auto parse_size(wchar_t const *s)
+{
+ return parse_size<T>(std::wstring_view(s));
+}
+
+} // namespace nihil
diff --git a/nihil.core/parse_size.test.cc b/nihil.core/parse_size.test.cc
new file mode 100644
index 0000000..6676543
--- /dev/null
+++ b/nihil.core/parse_size.test.cc
@@ -0,0 +1,165 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+TEST_CASE("parse_size: empty value", "[nihil]")
+{
+ using namespace nihil;
+
+ auto n = parse_size<std::uint64_t>("");
+ REQUIRE(!n);
+ REQUIRE(n.error() == nihil::errc::empty_string);
+}
+
+TEST_CASE("parse_size: basic", "[nihil]")
+{
+ using namespace nihil;
+
+ SECTION ("bare number") {
+ auto n = parse_size<std::uint64_t>("1024").value();
+ REQUIRE(n == 1024);
+ }
+
+ SECTION ("max value, unsigned") {
+ auto n = parse_size<std::uint16_t>("65535").value();
+ REQUIRE(n == 65535);
+ }
+
+ SECTION ("max value, signed") {
+ auto n = parse_size<std::uint16_t>("32767").value();
+ REQUIRE(n == 32767);
+ }
+
+ SECTION ("overflow by 1, unsigned") {
+ auto n = parse_size<std::uint16_t>("65536");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION ("overflow by 1, signed") {
+ auto n = parse_size<std::int16_t>("32768");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION ("overflow by many, unsigned") {
+ auto n = parse_size<std::uint16_t>("100000");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION ("overflow by many, signed") {
+ auto n = parse_size<std::int16_t>("100000");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+}
+
+TEST_CASE("parse_size: invalid multiplier", "[nihil]")
+{
+ using namespace nihil;
+
+ auto n = parse_size<std::uint64_t>("4z");
+ REQUIRE(!n);
+ REQUIRE(n.error() == nihil::errc::invalid_unit);
+
+ n = parse_size<std::uint64_t>("4kz");
+ REQUIRE(!n);
+ REQUIRE(n.error() == nihil::errc::invalid_unit);
+}
+
+TEST_CASE("parse_size: multipliers", "[nihil]")
+{
+ using namespace nihil;
+
+ auto sf = static_cast<std::uint64_t>(4);
+
+ SECTION ("k") {
+ auto n = parse_size<std::uint64_t>("4k").value();
+ REQUIRE(n == sf * 1024);
+ }
+
+ SECTION ("m") {
+ auto n = parse_size<std::uint64_t>("4m").value();
+ REQUIRE(n == sf * 1024 * 1024);
+ }
+
+ SECTION ("g") {
+ auto n = parse_size<std::uint64_t>("4g").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024);
+ }
+
+ SECTION ("t") {
+ auto n = parse_size<std::uint64_t>("4t").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024);
+ }
+
+ SECTION ("p") {
+ auto n = parse_size<std::uint64_t>("4p").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024);
+ }
+}
+
+TEST_CASE("parse_size: multiplier overflow", "[nihil]")
+{
+ using namespace nihil;
+
+ SECTION ("signed") {
+ auto n = parse_size<std::uint16_t>("64k");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+
+ SECTION ("unsigned") {
+ auto n = parse_size<std::int16_t>("32k");
+ REQUIRE(!n);
+ REQUIRE(n.error() == std::errc::result_out_of_range);
+ }
+}
+
+TEST_CASE("parse_size: wide", "[nihil]")
+{
+ using namespace nihil;
+
+ SECTION ("bare number") {
+ auto n = parse_size<std::uint64_t>(L"1024").value();
+ REQUIRE(n == 1024);
+ }
+}
+
+TEST_CASE("parse_size: wide multipliers", "[nihil]")
+{
+ using namespace nihil;
+
+ auto sf = static_cast<std::uint64_t>(4);
+
+ SECTION ("k") {
+ auto n = parse_size<std::uint64_t>(L"4k").value();
+ REQUIRE(n == sf * 1024);
+ }
+
+ SECTION ("m") {
+ auto n = parse_size<std::uint64_t>(L"4m").value();
+ REQUIRE(n == sf * 1024 * 1024);
+ }
+
+ SECTION ("g") {
+ auto n = parse_size<std::uint64_t>(L"4g").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024);
+ }
+
+ SECTION ("t") {
+ auto n = parse_size<std::uint64_t>(L"4t").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024);
+ }
+
+ SECTION ("p") {
+ auto n = parse_size<std::uint64_t>(L"4p").value();
+ REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024);
+ }
+}
+} // anonymous namespace
diff --git a/nihil.core/save_errno.ccm b/nihil.core/save_errno.ccm
new file mode 100644
index 0000000..56eafa8
--- /dev/null
+++ b/nihil.core/save_errno.ccm
@@ -0,0 +1,35 @@
+// This source code is released into the public domain.
+module;
+
+#include <cerrno>
+
+export module nihil.core:save_errno;
+
+// save_errno: save the current value of errno and restore it when we're destroyed.
+// this allows wrappers around C functions that use errno to preserve the caller's
+// errno value.
+
+namespace nihil {
+
+export struct save_errno final
+{
+ save_errno() : m_errno(errno) {}
+
+ ~save_errno()
+ {
+ errno = m_errno;
+ }
+
+ // Not copyable
+ save_errno(const save_errno&) = delete;
+ auto operator=(const save_errno&) -> save_errno & = delete;
+
+ // Not movable
+ save_errno(save_errno&&) = delete;
+ auto operator=(save_errno&&) -> save_errno & = delete;
+
+private:
+ int m_errno;
+};
+
+} // namespace nihil
diff --git a/nihil.core/skipws.ccm b/nihil.core/skipws.ccm
new file mode 100644
index 0000000..3901120
--- /dev/null
+++ b/nihil.core/skipws.ccm
@@ -0,0 +1,26 @@
+// This source code is released into the public domain.
+export module nihil.core:skipws;
+
+import nihil.std;
+import :ctype;
+
+namespace nihil {
+
+// Remove leading whitespace from a string.
+export template <typename Char>
+[[nodiscard]]
+auto skipws(std::basic_string_view<Char> text, std::locale const &locale = std::locale())
+ -> std::basic_string_view<Char>
+{
+ auto is_space = ctype_is(std::ctype_base::space, locale);
+ auto nonws = std::ranges::find_if_not(text, is_space);
+ return {nonws, std::ranges::end(text)};
+}
+
+export template <typename Char>
+auto skipws(std::basic_string_view<Char> *text, std::locale const &locale = std::locale()) -> void
+{
+ *text = skipws(*text, locale);
+}
+
+} // namespace nihil
diff --git a/nihil.core/skipws.test.cc b/nihil.core/skipws.test.cc
new file mode 100644
index 0000000..c8163ae
--- /dev/null
+++ b/nihil.core/skipws.test.cc
@@ -0,0 +1,48 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+namespace {
+TEST_CASE("skipws: basic", "[skipws]")
+{
+ using namespace std::literals;
+
+ REQUIRE(nihil::skipws("foo"sv) == "foo");
+ REQUIRE(nihil::skipws(" foo"sv) == "foo");
+ REQUIRE(nihil::skipws("foo "sv) == "foo ");
+ REQUIRE(nihil::skipws("foo bar"sv) == "foo bar");
+}
+
+TEST_CASE("skipws: pointer", "[skipws]")
+{
+ using namespace std::literals;
+
+ auto s = "foo"sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo");
+
+ s = " foo"sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo");
+
+ s = "foo "sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo ");
+
+ s = "foo bar"sv;
+ nihil::skipws(&s);
+ REQUIRE(s == "foo bar");
+}
+
+TEST_CASE("skipws: locale", "[skipws]")
+{
+ using namespace std::literals;
+
+ // Assume the default locale is C.
+ REQUIRE(nihil::skipws(L"\u2003foo"sv) == L"\u2003foo");
+ REQUIRE(nihil::skipws(L"\u2003foo"sv, std::locale("C.UTF-8")) == L"foo");
+}
+} // anonymous namespace
diff --git a/nihil.core/sys_error.ccm b/nihil.core/sys_error.ccm
new file mode 100644
index 0000000..3e5911f
--- /dev/null
+++ b/nihil.core/sys_error.ccm
@@ -0,0 +1,18 @@
+// This source code is released into the public domain.
+module;
+
+#include <cerrno>
+
+export module nihil.core:sys_error;
+
+import nihil.std;
+
+namespace nihil {
+
+// Allow access to errno without having to include <cerrno>.
+export [[nodiscard]] auto sys_error() -> std::errc
+{
+ return static_cast<std::errc>(errno); // NOLINT
+}
+
+} // namespace nihil
diff --git a/nihil.core/tabulate.ccm b/nihil.core/tabulate.ccm
new file mode 100644
index 0000000..98a957c
--- /dev/null
+++ b/nihil.core/tabulate.ccm
@@ -0,0 +1,298 @@
+// This source code is released into the public domain.
+export module nihil.core:tabulate;
+
+import nihil.std;
+
+import :ctype;
+import :error;
+
+namespace nihil {
+
+// tabulate: format the given range in an ASCII table and write the output
+// to the given output iterator. The range's values will be converted to
+// strings as if by std::format.
+//
+// tabulate is implemented by copying the range; this allows it to work on
+// input/forward ranges at the cost of slightly increased memory use.
+//
+// The table spec is a string consisting of zero or more field formats,
+// formatted as {flags:fieldname}; both flags and fieldname are optional.
+// If there are fewer field formats than fields, the remaining fields
+// are formatted as if by {:}.
+//
+// The following flags are supported:
+//
+// < left-align this column (default)
+// > right-align this column
+
+// Exception thrown when a table spec is invalid.
+export struct table_spec_error : error {
+ explicit table_spec_error(std::string_view what)
+ : error(what)
+ {
+ }
+};
+
+// The specification for a single field.
+template<typename Char>
+struct field_spec {
+ enum align_t { left, right };
+
+ // Get the name of this field.
+ auto name(this field_spec const &self)
+ -> std::basic_string_view<Char>
+ {
+ return self.m_name;
+ }
+
+ // Set the name of this field.
+ auto name(this field_spec &self,
+ std::basic_string_view<Char> new_name)
+ -> void
+ {
+ self.m_name = new_name;
+ }
+
+ // Set this field's alignment.
+ auto align(this field_spec &self, align_t new_align) -> void
+ {
+ self.m_align = new_align;
+ }
+
+ // Ensure the length of this field is at least the given width.
+ auto ensure_width(this field_spec &self, std::size_t newwidth)
+ -> void
+ {
+ self.m_width = std::max(self.m_width, newwidth);
+ }
+
+ // Format an object to a string based on our field spec.
+ [[nodiscard]] auto format(this field_spec const &, auto &&obj)
+ -> std::basic_string<Char>
+ {
+ auto format_string = std::basic_string<Char>{'{', '}'};
+ return std::format(std::runtime_format(format_string), obj);
+ }
+
+ // Print a column value to an output iterator according to our field
+ // spec. If is_last is true, this is the last field on the line, so
+ // we won't output any trailling padding.
+ auto print(this field_spec const &self,
+ std::basic_string_view<Char> value,
+ std::output_iterator<Char> auto &out,
+ bool is_last)
+ -> void
+ {
+ auto padding = self.m_width - value.size();
+
+ if (self.m_align == right)
+ for (std::size_t i = 0; i < padding; ++i)
+ *out++ = ' ';
+
+ std::ranges::copy(value, out);
+
+ if (!is_last && self.m_align == left)
+ for (std::size_t i = 0; i < padding; ++i)
+ *out++ = ' ';
+ }
+
+private:
+ std::basic_string_view<Char> m_name;
+ std::size_t m_width = 0;
+ align_t m_align = left;
+};
+
+/*
+ * The specification for an entire table.
+ */
+template<typename Char>
+struct table_spec {
+ // Add a new field spec to this table.
+ auto add(this table_spec &self, field_spec<Char> field) -> void
+ {
+ self.m_fields.emplace_back(std::move(field));
+ }
+
+ // Return the field spec for a given field. If the field doesn't
+ // exist, this field and any intermediate fields will be created.
+ [[nodiscard]] auto field(this table_spec &self, std::size_t fieldnr)
+ -> field_spec<Char> &
+ {
+ if (fieldnr >= self.m_fields.size())
+ self.m_fields.resize(fieldnr + 1);
+ return self.m_fields.at(fieldnr);
+ }
+
+ // The number of columns in this table.
+ [[nodiscard]] auto columns(this table_spec const &self) -> std::size_t
+ {
+ return self.m_fields.size();
+ }
+
+ // Return all the fields in this table.
+ [[nodiscard]] auto fields(this table_spec const &self)
+ -> std::vector<field_spec<Char>> const &
+ {
+ return self.m_fields;
+ }
+
+private:
+ std::vector<field_spec<Char>> m_fields;
+};
+
+// Parse the field flags, e.g. '<'.
+template<typename Char,
+ std::input_iterator Iterator, std::sentinel_for<Iterator> Sentinel>
+auto parse_field_flags(field_spec<Char> &field, Iterator &pos, Sentinel end)
+ -> void
+{
+ while (pos < end) {
+ switch (*pos) {
+ case '<':
+ field.align(field_spec<Char>::left);
+ break;
+ case '>':
+ field.align(field_spec<Char>::right);
+ break;
+ case ':':
+ ++pos;
+ /*FALLTHROUGH*/
+ case '}':
+ return;
+ default:
+ throw table_spec_error("Invalid table spec: "
+ "unknown flag character");
+ }
+
+ if (++pos == end)
+ throw table_spec_error("Invalid table spec: "
+ "unterminated field");
+ }
+}
+
+// Parse a complete field spec, e.g. "{<:NAME}".
+template<typename Char,
+ std::input_iterator Iterator, std::sentinel_for<Iterator> Sentinel>
+[[nodiscard]] auto parse_field(Iterator &pos, Sentinel end)
+ -> field_spec<Char>
+{
+ auto field = field_spec<Char>{};
+
+ if (pos == end)
+ throw table_spec_error("Invalid table spec: empty field");
+
+ // The field spec should start with a '{'.
+ if (*pos != '{')
+ throw table_spec_error("Invalid table spec: expected '{'");
+
+ if (++pos == end)
+ throw table_spec_error("Invalid table spec: unterminated field");
+
+ // This consumes 'pos' up to and including the ':'.
+ parse_field_flags(field, pos, end);
+
+ auto brace = std::ranges::find(pos, end, '}');
+ if (brace == end)
+ throw table_spec_error("Invalid table spec: expected '}'");
+
+ field.name(std::basic_string_view<Char>(pos, brace));
+ pos = std::next(brace);
+
+ // The field must be at least as wide as its header.
+ field.ensure_width(field.name().size());
+
+ return field;
+}
+
+template<typename Char>
+[[nodiscard]] auto parse_table_spec(std::basic_string_view<Char> spec)
+ -> table_spec<Char>
+{
+ auto table = table_spec<Char>();
+
+ auto pos = std::ranges::begin(spec);
+ auto end = std::ranges::end(spec);
+
+ for (;;) {
+ // Skip leading whitespace
+ while (pos < end && is_c_space(*pos))
+ ++pos;
+
+ if (pos == end)
+ break;
+
+ table.add(parse_field<Char>(pos, end));
+ }
+
+ return table;
+}
+
+export template<typename Char,
+ std::ranges::range Range,
+ std::output_iterator<Char> Iterator>
+auto basic_tabulate(std::basic_string_view<Char> table_spec,
+ Range &&range,
+ Iterator &&out)
+ -> void
+{
+ // Parse the table spec.
+ auto table = parse_table_spec(table_spec);
+
+ // Create our copy of the input data.
+ auto data = std::vector<std::vector<std::basic_string<Char>>>();
+ // Reserve the first row for the header.
+ data.resize(1);
+
+ // Find the required length of each field.
+ for (auto &&row : range) {
+ // LLVM doesn't have std::enumerate_view yet
+ auto i = std::size_t{0};
+ auto &this_row = data.emplace_back();
+
+ for (auto &&column : row) {
+ auto &field = table.field(i);
+ auto &str = this_row.emplace_back(field.format(column));
+ field.ensure_width(str.size());
+ ++i;
+ }
+ }
+
+ // Add the header row.
+ for (auto &&field : table.fields())
+ data.at(0).emplace_back(std::from_range, field.name());
+
+ // Print the values.
+ for (auto &&row : data) {
+ for (std::size_t i = 0; i < row.size(); ++i) {
+ auto &field = table.field(i);
+ bool is_last = (i == row.size() - 1);
+
+ field.print(row[i], out, is_last);
+
+ if (!is_last)
+ *out++ = ' ';
+ }
+
+ *out++ = '\n';
+ }
+}
+
+export auto tabulate(std::string_view table_spec,
+ std::ranges::range auto &&range,
+ std::output_iterator<char> auto &&out)
+{
+ return basic_tabulate<char>(table_spec,
+ std::forward<decltype(range)>(range),
+ std::forward<decltype(out)>(out));
+}
+
+export auto wtabulate(std::wstring_view table_spec,
+ std::ranges::range auto &&range,
+ std::output_iterator<wchar_t> auto &&out)
+{
+ return basic_tabulate<wchar_t>(table_spec,
+ std::forward<decltype(range)>(range),
+ std::forward<decltype(out)>(out));
+}
+
+} // namespace nihil
diff --git a/nihil.core/tabulate.test.cc b/nihil.core/tabulate.test.cc
new file mode 100644
index 0000000..e1ea32f
--- /dev/null
+++ b/nihil.core/tabulate.test.cc
@@ -0,0 +1,72 @@
+// This source code is released into the public domain.
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+using namespace std::literals;
+using namespace nihil;
+
+namespace {
+TEST_CASE("tabulate: basic", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "foo", "b"},
+ std::vector{"bar", "c", "baz"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {:2} {:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a foo b\n"
+"bar c baz\n");
+}
+
+TEST_CASE("tabulate: basic wide", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{L"a", L"foo", L"b"},
+ std::vector{L"bar", L"c", L"baz"},
+ };
+
+ auto result = std::wstring();
+ wtabulate(L"{:1} {:2} {:3}", input, std::back_inserter(result));
+
+ REQUIRE(result ==
+L"1 2 3\n"
+"a foo b\n"
+"bar c baz\n");
+}
+
+TEST_CASE("tabulate: jagged", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "foo", "b"},
+ std::vector{"bar", "baz"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {:2} {:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a foo b\n"
+"bar baz\n");
+}
+
+TEST_CASE("tabulate: align", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "longvalue", "s"},
+ std::vector{"a", "s", "longvalue"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {<:2} {>:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a longvalue s\n"
+"a s longvalue\n");
+}
+} // anonymous namespace
diff --git a/nihil.core/uuid.ccm b/nihil.core/uuid.ccm
new file mode 100644
index 0000000..b1b5d5f
--- /dev/null
+++ b/nihil.core/uuid.ccm
@@ -0,0 +1,768 @@
+// From https://github.com/mariusbancila/stduuid
+//
+// Copyright (c) 2017
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+export module nihil.core:uuid;
+
+import nihil.std;
+
+namespace nihil {
+
+template <typename TChar>
+[[nodiscard]] constexpr auto hex2char(TChar const ch) noexcept -> unsigned char
+{
+ if (ch >= static_cast<TChar>('0') && ch <= static_cast<TChar>('9'))
+ return static_cast<unsigned char>(ch - static_cast<TChar>('0'));
+
+ if (ch >= static_cast<TChar>('a') && ch <= static_cast<TChar>('f'))
+ return static_cast<unsigned char>(10 + ch - static_cast<TChar>('a'));
+
+ if (ch >= static_cast<TChar>('A') && ch <= static_cast<TChar>('F'))
+ return static_cast<unsigned char>(10 + ch - static_cast<TChar>('A'));
+
+ return 0;
+}
+
+template <typename TChar>
+[[nodiscard]] constexpr auto is_hex(TChar const ch) noexcept -> bool
+{
+ return (ch >= static_cast<TChar>('0') && ch <= static_cast<TChar>('9')) ||
+ (ch >= static_cast<TChar>('a') && ch <= static_cast<TChar>('f')) ||
+ (ch >= static_cast<TChar>('A') && ch <= static_cast<TChar>('F'));
+}
+
+template <typename TChar>
+[[nodiscard]] constexpr auto
+to_string_view(TChar const *str) noexcept -> std::basic_string_view<TChar>
+{
+ if (str)
+ return str;
+ return {};
+}
+
+template <typename StringType>
+[[nodiscard]] constexpr auto to_string_view(StringType const &str) noexcept
+ -> std::basic_string_view<typename StringType::value_type, typename StringType::traits_type>
+{
+ return str;
+}
+
+struct sha1
+{
+ using digest32_t = std::array<std::uint32_t, 5>;
+ using digest8_t = std::array<std::uint8_t, 20>;
+
+ static constexpr unsigned int block_bytes = 64;
+
+ sha1()
+ {
+ reset();
+ }
+
+ [[nodiscard]] static auto
+ left_rotate(std::uint32_t value, std::size_t const count) noexcept -> std::uint32_t
+ {
+ return (value << count) ^ (value >> (32 - count));
+ }
+
+ auto reset(this sha1 &self) noexcept -> void
+ {
+ self.m_digest[0] = 0x67452301;
+ self.m_digest[1] = 0xEFCDAB89;
+ self.m_digest[2] = 0x98BADCFE;
+ self.m_digest[3] = 0x10325476;
+ self.m_digest[4] = 0xC3D2E1F0;
+ self.m_blockByteIndex = 0;
+ self.m_byteCount = 0;
+ }
+
+ auto process_byte(this sha1 &self, std::uint8_t octet) -> void
+ {
+ self.m_block.at(self.m_blockByteIndex++) = octet;
+ ++self.m_byteCount;
+
+ if (self.m_blockByteIndex == block_bytes) {
+ self.m_blockByteIndex = 0;
+ self.process_block();
+ }
+ }
+
+ auto process_block(this sha1 &self, void const *const start, void const *const end) -> void
+ {
+ auto const *first = static_cast<std::uint8_t const *>(start);
+ auto const *last = static_cast<std::uint8_t const *>(end);
+
+ while (first != last) {
+ self.process_byte(*first);
+ first++;
+ }
+ }
+
+ auto process_bytes(this sha1 &self, void const *const data, std::size_t const len) -> void
+ {
+ auto *block = static_cast<std::uint8_t const *>(data);
+ self.process_block(block, block + len);
+ }
+
+ auto get_digest(this sha1 &self) -> digest32_t
+ {
+ auto const bit_count = self.m_byteCount * 8;
+
+ self.process_byte(0x80);
+ if (self.m_blockByteIndex > 56) {
+ while (self.m_blockByteIndex != 0)
+ self.process_byte(0);
+
+ while (self.m_blockByteIndex < 56)
+ self.process_byte(0);
+ } else {
+ while (self.m_blockByteIndex < 56)
+ self.process_byte(0);
+ }
+
+ self.process_byte(0);
+ self.process_byte(0);
+ self.process_byte(0);
+ self.process_byte(0);
+ self.process_byte(static_cast<unsigned char>((bit_count >> 24U) & 0xFFU));
+ self.process_byte(static_cast<unsigned char>((bit_count >> 16U) & 0xFFU));
+ self.process_byte(static_cast<unsigned char>((bit_count >> 8U) & 0xFFU));
+ self.process_byte(static_cast<unsigned char>((bit_count) & 0xFFU));
+
+ return self.m_digest;
+ }
+
+ auto get_digest_bytes(this sha1 &self) -> digest8_t
+ {
+ auto d32 = self.get_digest();
+
+ return {
+ static_cast<std::uint8_t>(d32[0] >> 24U),
+ static_cast<std::uint8_t>(d32[0] >> 16U),
+ static_cast<std::uint8_t>(d32[0] >> 8U),
+ static_cast<std::uint8_t>(d32[0] >> 0U),
+
+ static_cast<std::uint8_t>(d32[1] >> 24U),
+ static_cast<std::uint8_t>(d32[1] >> 16U),
+ static_cast<std::uint8_t>(d32[1] >> 8U),
+ static_cast<std::uint8_t>(d32[1] >> 0U),
+
+ static_cast<std::uint8_t>(d32[2] >> 24U),
+ static_cast<std::uint8_t>(d32[2] >> 16U),
+ static_cast<std::uint8_t>(d32[2] >> 8U),
+ static_cast<std::uint8_t>(d32[2] >> 0U),
+
+ static_cast<std::uint8_t>(d32[3] >> 24U),
+ static_cast<std::uint8_t>(d32[3] >> 16U),
+ static_cast<std::uint8_t>(d32[3] >> 8U),
+ static_cast<std::uint8_t>(d32[3] >> 0U),
+
+ static_cast<std::uint8_t>(d32[4] >> 24U),
+ static_cast<std::uint8_t>(d32[4] >> 16U),
+ static_cast<std::uint8_t>(d32[4] >> 8U),
+ static_cast<std::uint8_t>(d32[4] >> 0U),
+ };
+ }
+
+private:
+ auto process_block(this sha1 &self) -> void
+ {
+ auto w = std::array<std::uint32_t, 80>{};
+
+ for (std::size_t i = 0; i < 16; i++) {
+ w.at(i) = static_cast<std::uint32_t>(self.m_block.at((i * 4) + 0)) << 24U;
+ w.at(i) |= static_cast<std::uint32_t>(self.m_block.at((i * 4) + 1)) << 16U;
+ w.at(i) |= static_cast<std::uint32_t>(self.m_block.at((i * 4) + 2)) << 8U;
+ w.at(i) |= static_cast<std::uint32_t>(self.m_block.at((i * 4) + 3));
+ }
+
+ for (std::size_t i = 16; i < 80; i++) {
+ w.at(i) = left_rotate(
+ (w.at(i - 3) ^ w.at(i - 8) ^ w.at(i - 14) ^ w.at(i - 16)), 1);
+ }
+
+ auto a = self.m_digest[0];
+ auto b = self.m_digest[1];
+ auto c = self.m_digest[2];
+ auto d = self.m_digest[3];
+ auto e = self.m_digest[4];
+
+ for (std::size_t i = 0; i < 80; ++i) {
+ auto f = std::uint32_t{0};
+ auto k = std::uint32_t{0};
+
+ if (i < 20) {
+ f = (b & c) | (~b & d);
+ k = 0x5A827999;
+ } else if (i < 40) {
+ f = b ^ c ^ d;
+ k = 0x6ED9EBA1;
+ } else if (i < 60) {
+ f = (b & c) | (b & d) | (c & d);
+ k = 0x8F1BBCDC;
+ } else {
+ f = b ^ c ^ d;
+ k = 0xCA62C1D6;
+ }
+
+ auto temp = std::uint32_t{left_rotate(a, 5) + f + e + k + w.at(i)};
+ e = d;
+ d = c;
+ c = left_rotate(b, 30);
+ b = a;
+ a = temp;
+ }
+
+ self.m_digest[0] += a;
+ self.m_digest[1] += b;
+ self.m_digest[2] += c;
+ self.m_digest[3] += d;
+ self.m_digest[4] += e;
+ }
+
+ digest32_t m_digest{};
+ std::array<std::uint8_t, 64> m_block{};
+ std::size_t m_blockByteIndex{};
+ std::size_t m_byteCount{};
+};
+
+template <typename CharT>
+inline constexpr std::string_view empty_guid = "00000000-0000-0000-0000-000000000000";
+
+template <>
+inline constexpr std::wstring_view empty_guid<wchar_t> = L"00000000-0000-0000-0000-000000000000";
+
+template <typename CharT>
+inline constexpr std::string_view guid_encoder = "0123456789abcdef";
+
+template <>
+inline constexpr std::wstring_view guid_encoder<wchar_t> = L"0123456789abcdef";
+
+// ---------------------------------------------------------------------
+// UUID format https://tools.ietf.org/html/rfc4122
+// ---------------------------------------------------------------------
+
+// ---------------------------------------------------------------------
+// Field NDR Data Type Octet # Note
+// Note
+// ---------------------------------------------------------------------
+// time_low unsigned long 0 - 3
+// The low field of the timestamp.
+// time_mid unsigned short 4 - 5
+// The middle field of the timestamp.
+// time_hi_and_version unsigned short 6 - 7
+// The high field of the timestamp multiplexed with the version number.
+// clock_seq_hi_and_reserved unsigned small 8
+// The high field of the clock sequence multiplexed with the variant.
+// clock_seq_low unsigned small 9
+// The low field of the clock sequence.
+// node character 10 - 15
+// The spatially unique node identifier.
+// ---------------------------------------------------------------------
+// 0 1 2 3
+// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// | time_low |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// | time_mid | time_hi_and_version |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |clk_seq_hi_res | clk_seq_low | node (0-1) |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// | node (2-5) |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+// ---------------------------------------------------------------------
+// enumerations
+// ---------------------------------------------------------------------
+
+// indicated by a bit pattern in octet 8, marked with N in
+// xxxxxxxx-xxxx-xxxx-Nxxx-xxxxxxxxxxxx
+export enum struct uuid_variant : std::uint8_t {
+ // NCS backward compatibility (with the obsolete Apollo Network
+ // Computing System 1.5 UUID format).
+ // N bit pattern: 0xxx
+ // > the first 6 octets of the UUID are a 48-bit timestamp (the number
+ // of 4 microsecond units of time since 1 Jan 1980 UTC);
+ // > the next 2 octets are reserved;
+ // > the next octet is the "address family";
+ // > the final 7 octets are a 56-bit host ID in the form specified by
+ // the address family
+ ncs,
+
+ // RFC 4122/DCE 1.1
+ // N bit pattern: 10xx
+ // > big-endian byte order
+ rfc,
+
+ // Microsoft Corporation backward compatibility
+ // N bit pattern: 110x
+ // > little endian byte order
+ // > formely used in the Component Object Model (COM) library
+ microsoft,
+
+ // reserved for possible future definition
+ // N bit pattern: 111x
+ reserved
+};
+
+// indicated by a bit pattern in octet 6, marked with M in
+// xxxxxxxx-xxxx-Mxxx-xxxx-xxxxxxxxxxxx
+export enum struct uuid_version : std::uint8_t {
+ // only possible for nil or invalid uuids
+ none = 0,
+ // The time-based version specified in RFC 4122
+ time_based = 1,
+ // DCE Security version, with embedded POSIX UIDs.
+ dce_security = 2,
+ // The name-based version specified in RFS 4122 with MD5 hashing
+ name_based_md5 = 3,
+ // The randomly or pseudo-randomly generated version specified in RFS 4122
+ random_number_based = 4,
+ // The name-based version specified in RFS 4122 with SHA1 hashing
+ name_based_sha1 = 5
+};
+
+// Forward declare uuid and to_string so that we can declare to_string as a
+// friend later.
+export struct uuid;
+export template <typename CharT = char, typename Traits = std::char_traits<CharT>,
+ typename Allocator = std::allocator<CharT>>
+auto to_string(uuid const &id) -> std::basic_string<CharT, Traits, Allocator>;
+
+// --------------------------------------------------------------------------------------------------------------------------
+// uuid class
+// --------------------------------------------------------------------------------------------------------------------------
+export struct uuid
+{
+ using value_type = std::uint8_t;
+
+ constexpr uuid() noexcept = default;
+
+ uuid(value_type (&arr)[16]) noexcept // NOLINT
+ {
+ std::ranges::copy(arr, std::ranges::begin(data));
+ }
+
+ explicit constexpr uuid(std::array<value_type, 16> const &arr) noexcept
+ : data{arr}
+ {
+ }
+
+ explicit uuid(std::span<value_type, 16> bytes)
+ {
+ std::ranges::copy(bytes, std::ranges::begin(data));
+ }
+
+ explicit uuid(std::span<value_type> bytes)
+ {
+ if (bytes.size() != 16)
+ throw std::logic_error("wrong size for uuid");
+ std::ranges::copy(bytes, std::ranges::begin(data));
+ }
+
+ template <typename ForwardIterator>
+ explicit uuid(ForwardIterator first, ForwardIterator last)
+ {
+ if (std::distance(first, last) != 16)
+ throw std::logic_error("wrong size for uuid");
+
+ std::copy(first, last, std::begin(data));
+ }
+
+ [[nodiscard]] constexpr auto variant() const noexcept -> uuid_variant
+ {
+ if ((data[8] & 0x80U) == 0x00U)
+ return uuid_variant::ncs;
+ else if ((data[8] & 0xC0U) == 0x80U)
+ return uuid_variant::rfc;
+ else if ((data[8] & 0xE0U) == 0xC0U)
+ return uuid_variant::microsoft;
+ else
+ return uuid_variant::reserved;
+ }
+
+ [[nodiscard]] constexpr auto version() const noexcept -> uuid_version
+ {
+ if ((data[6] & 0xF0U) == 0x10U)
+ return uuid_version::time_based;
+ else if ((data[6] & 0xF0U) == 0x20U)
+ return uuid_version::dce_security;
+ else if ((data[6] & 0xF0U) == 0x30U)
+ return uuid_version::name_based_md5;
+ else if ((data[6] & 0xF0U) == 0x40U)
+ return uuid_version::random_number_based;
+ else if ((data[6] & 0xF0U) == 0x50U)
+ return uuid_version::name_based_sha1;
+ else
+ return uuid_version::none;
+ }
+
+ [[nodiscard]] constexpr auto is_nil() const noexcept -> bool
+ {
+ return std::ranges::all_of(data, [](auto i) { return i == 0; });
+ }
+
+ auto swap(uuid &other) noexcept -> void
+ {
+ data.swap(other.data);
+ }
+
+ [[nodiscard]] auto as_bytes() const -> std::span<std::byte const, 16>
+ {
+ return std::span<std::byte const, 16>(
+ reinterpret_cast<std::byte const *>(data.data()), 16);
+ }
+
+ template <typename StringType>
+ [[nodiscard]] constexpr static auto is_valid_uuid(StringType const &in_str) noexcept -> bool
+ {
+ auto str = to_string_view(in_str);
+ auto firstDigit = true;
+ auto hasBraces = std::size_t{0};
+ auto index = std::size_t{0};
+
+ if (str.empty())
+ return false;
+
+ if (str.front() == '{')
+ hasBraces = 1;
+
+ if (hasBraces && str.back() != '}')
+ return false;
+
+ for (std::size_t i = hasBraces; i < str.size() - hasBraces; ++i) {
+ if (str[i] == '-')
+ continue;
+
+ if (index >= 16 || !is_hex(str[i]))
+ return false;
+
+ if (firstDigit) {
+ firstDigit = false;
+ } else {
+ index++;
+ firstDigit = true;
+ }
+ }
+
+ if (index < 16)
+ return false;
+
+ return true;
+ }
+
+ template <typename StringType>
+ [[nodiscard]] constexpr static auto
+ from_string(StringType const &in_str) -> std::optional<uuid>
+ {
+ auto str = to_string_view(in_str);
+ bool firstDigit = true;
+ auto hasBraces = std::size_t{0};
+ auto index = std::size_t{0};
+
+ auto data = std::array<std::uint8_t, 16>{};
+
+ if (str.empty())
+ return {};
+
+ if (str.front() == '{')
+ hasBraces = 1;
+ if (hasBraces && str.back() != '}')
+ return {};
+
+ for (std::size_t i = hasBraces; i < str.size() - hasBraces; ++i) {
+ if (str[i] == '-')
+ continue;
+
+ if (index >= 16 || !is_hex(str[i])) {
+ return {};
+ }
+
+ if (firstDigit) {
+ data.at(index) = static_cast<std::uint8_t>(hex2char(str[i]) << 4);
+ firstDigit = false;
+ } else {
+ data.at(index) =
+ static_cast<std::uint8_t>(data.at(index) | hex2char(str[i]));
+ index++;
+ firstDigit = true;
+ }
+ }
+
+ if (index < 16) {
+ return {};
+ }
+
+ return uuid{data};
+ }
+
+private:
+ std::array<value_type, 16> data{{0}};
+
+ friend auto operator==(uuid const &, uuid const &) noexcept -> bool;
+ friend auto operator<(uuid const &, uuid const &) noexcept -> bool;
+
+ template <class Elem, class Traits>
+ friend auto operator<<(std::basic_ostream<Elem, Traits> &s, uuid const &id)
+ -> std::basic_ostream<Elem, Traits> &;
+
+ template <class CharT, class Traits, class Allocator>
+ friend auto to_string(uuid const &id) -> std::basic_string<CharT, Traits, Allocator>;
+
+ friend std::hash<uuid>;
+};
+
+// --------------------------------------------------------------------------------------------------------------------------
+// operators and non-member functions
+// --------------------------------------------------------------------------------------------------------------------------
+
+export [[nodiscard]]
+auto operator==(uuid const &lhs, uuid const &rhs) noexcept -> bool
+{
+ return lhs.data == rhs.data;
+}
+
+export [[nodiscard]]
+auto operator!=(uuid const &lhs, uuid const &rhs) noexcept -> bool
+{
+ return !(lhs == rhs);
+}
+
+export [[nodiscard]]
+auto operator<(uuid const &lhs, uuid const &rhs) noexcept -> bool
+{
+ return lhs.data < rhs.data;
+}
+
+export template <typename CharT, typename Traits, typename Allocator>
+[[nodiscard]] auto to_string(uuid const &id) -> std::basic_string<CharT, Traits, Allocator>
+{
+ auto uustr =
+ std::basic_string<CharT, Traits, Allocator>(std::from_range, empty_guid<CharT>);
+
+ for (std::size_t i = 0, index = 0; i < 36; ++i) {
+ if (i == 8 || i == 13 || i == 18 || i == 23)
+ continue;
+
+ uustr[i] = guid_encoder<CharT>[id.data.at(index) >> 4U & 0x0FU];
+ uustr[++i] = guid_encoder<CharT>[id.data.at(index) & 0x0FU];
+ index++;
+ }
+
+ return uustr;
+}
+
+export template <class Elem, class Traits>
+auto operator<<(std::basic_ostream<Elem, Traits> &s, uuid const &id)
+ -> std::basic_ostream<Elem, Traits> &
+{
+ return s << to_string(id);
+}
+
+export auto swap(uuid &lhs, uuid &rhs) noexcept -> void
+{
+ lhs.swap(rhs);
+}
+
+/***********************************************************************
+ * namespace IDs that could be used for generating name-based uuids
+ */
+
+// Name string is a fully-qualified domain name
+export uuid uuid_namespace_dns{
+ {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30,
+ 0xc8}
+};
+
+// Name string is a URL
+export uuid uuid_namespace_url{
+ {0x6b, 0xa7, 0xb8, 0x11, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30,
+ 0xc8}
+};
+
+// Name string is an ISO OID (See https://oidref.com/,
+// https://en.wikipedia.org/wiki/Object_identifier)
+export uuid uuid_namespace_oid{
+ {0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30,
+ 0xc8}
+};
+
+// Name string is an X.500 DN, in DER or a text output format (See
+// https://en.wikipedia.org/wiki/X.500,
+// https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One)
+export uuid uuid_namespace_x500{
+ {0x6b, 0xa7, 0xb8, 0x14, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30,
+ 0xc8}
+};
+
+/***********************************************************************
+ * uuid generators
+ */
+
+export template <typename UniformRandomNumberGenerator>
+struct basic_uuid_random_generator
+{
+ using engine_type = UniformRandomNumberGenerator;
+
+ explicit basic_uuid_random_generator(engine_type &gen)
+ : generator(&gen, [](auto) {})
+ {
+ }
+
+ explicit basic_uuid_random_generator(engine_type *gen)
+ : generator(gen, [](auto) {})
+ {
+ }
+
+ [[nodiscard]] auto operator()() -> uuid
+ {
+ auto bytes = std::array<std::uint8_t, 16>{};
+ std::ranges::generate(bytes, [&] { return distribution(*generator); });
+
+ // variant must be 10xxxxxx
+ bytes[8] &= 0xBFU;
+ bytes[8] |= 0x80U;
+
+ // version must be 0100xxxx
+ bytes[6] &= 0x4FU;
+ bytes[6] |= 0x40U;
+
+ return uuid{std::begin(bytes), std::end(bytes)};
+ }
+
+private:
+ std::uniform_int_distribution<std::uint32_t> distribution;
+ std::shared_ptr<UniformRandomNumberGenerator> generator;
+};
+
+export using uuid_random_generator = basic_uuid_random_generator<std::mt19937>;
+
+export struct uuid_name_generator
+{
+ explicit uuid_name_generator(uuid const &namespace_uuid) noexcept
+ : nsuuid(namespace_uuid)
+ {
+ }
+
+ template <typename StringType>
+ [[nodiscard]] auto operator()(StringType const &name) -> uuid
+ {
+ reset();
+ process_characters(to_string_view(name));
+ return make_uuid();
+ }
+
+private:
+ auto reset() -> void
+ {
+ hasher.reset();
+
+ auto nsbytes = nsuuid.as_bytes();
+
+ auto bytes = std::array<std::byte, 16>();
+ std::ranges::copy(nsbytes, std::ranges::begin(bytes));
+
+ hasher.process_bytes(bytes.data(), bytes.size());
+ }
+
+ template <typename CharT, typename Traits>
+ auto process_characters(std::basic_string_view<CharT, Traits> const str) -> void
+ {
+ for (std::uint32_t c : str) {
+ hasher.process_byte(static_cast<std::uint8_t>(c & 0xFFU));
+ if constexpr (!std::is_same_v<CharT, char>) {
+ hasher.process_byte(static_cast<std::uint8_t>((c >> 8U) & 0xFFU));
+ hasher.process_byte(static_cast<std::uint8_t>((c >> 16U) & 0xFFU));
+ hasher.process_byte(static_cast<std::uint8_t>((c >> 24U) & 0xFFU));
+ }
+ }
+ }
+
+ [[nodiscard]] auto make_uuid() -> uuid
+ {
+ auto digest = hasher.get_digest_bytes();
+
+ // variant must be 0b10xxxxxx
+ digest[8] &= 0xBFU;
+ digest[8] |= 0x80U;
+
+ // version must be 0b0101xxxx
+ digest[6] &= 0x5FU;
+ digest[6] |= 0x50U;
+
+ return uuid(std::span(digest).subspan(0, 16));
+ }
+
+ uuid nsuuid;
+ sha1 hasher;
+};
+
+/*
+ * Create a random UUID.
+ */
+export auto random_uuid() -> uuid
+{
+ auto rd = std::random_device();
+ auto seed_data = std::array<int, std::mt19937::state_size>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+
+ auto seq = std::seed_seq(std::ranges::begin(seed_data), std::ranges::end(seed_data));
+ auto generator = std::mt19937(seq);
+ auto gen = uuid_random_generator{generator};
+
+ return gen();
+}
+
+} // namespace nihil
+
+namespace std {
+
+export template <>
+struct hash<nihil::uuid>
+{
+ using argument_type = nihil::uuid;
+ using result_type = std::size_t;
+
+ [[nodiscard]] auto operator()(argument_type const &uuid) const noexcept -> result_type
+ {
+ auto const l = static_cast<uint64_t>(uuid.data[0]) << 56U |
+ static_cast<uint64_t>(uuid.data[1]) << 48U |
+ static_cast<uint64_t>(uuid.data[2]) << 40U |
+ static_cast<uint64_t>(uuid.data[3]) << 32U |
+ static_cast<uint64_t>(uuid.data[4]) << 24U |
+ static_cast<uint64_t>(uuid.data[5]) << 16U |
+ static_cast<uint64_t>(uuid.data[6]) << 8U |
+ static_cast<uint64_t>(uuid.data[7]);
+
+ auto const h = static_cast<uint64_t>(uuid.data[8]) << 56U |
+ static_cast<uint64_t>(uuid.data[9]) << 48U |
+ static_cast<uint64_t>(uuid.data[10]) << 40U |
+ static_cast<uint64_t>(uuid.data[11]) << 32U |
+ static_cast<uint64_t>(uuid.data[12]) << 24U |
+ static_cast<uint64_t>(uuid.data[13]) << 16U |
+ static_cast<uint64_t>(uuid.data[14]) << 8U |
+ static_cast<uint64_t>(uuid.data[15]);
+
+ return std::hash<std::uint64_t>{}(l ^ h);
+ }
+};
+
+} // namespace std
diff --git a/nihil.core/uuid.test.cc b/nihil.core/uuid.test.cc
new file mode 100644
index 0000000..407f4e1
--- /dev/null
+++ b/nihil.core/uuid.test.cc
@@ -0,0 +1,923 @@
+/*
+ * From https://github.com/mariusbancila/stduuid
+ *
+ * Copyright (c) 2017
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil.std;
+import nihil.core;
+
+// NOLINTBEGIN(bugprone-unchecked-optional-access)
+
+namespace {
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0205r0.html
+template <typename EngineT, std::size_t StateSize = EngineT::state_size>
+void seed_rng(EngineT &engine)
+{
+ using engine_type = typename EngineT::result_type;
+ using device_type = std::random_device::result_type;
+ using seedseq_type = std::seed_seq::result_type;
+
+ constexpr auto bytes_needed = StateSize * sizeof(engine_type);
+ constexpr auto numbers_needed = (sizeof(device_type) < sizeof(seedseq_type))
+ ? (bytes_needed / sizeof(device_type))
+ : (bytes_needed / sizeof(seedseq_type));
+
+ auto numbers = std::array<device_type, numbers_needed>{};
+ auto rnddev = std::random_device{};
+ std::ranges::generate(numbers, std::ref(rnddev));
+
+ auto seedseq = std::seed_seq(std::cbegin(numbers), std::cend(numbers));
+ engine.seed(seedseq);
+}
+
+using namespace nihil;
+
+TEST_CASE("uuid: Test multiple default generators", "[uuid]")
+{
+ uuid id1;
+ uuid id2;
+
+ {
+ std::random_device rd;
+ auto seed_data = std::array<int, std::mt19937::state_size>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ std::mt19937 generator(seq);
+
+ id1 = uuid_random_generator{generator}();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::random_number_based);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+ }
+
+ {
+ std::random_device rd;
+ auto seed_data = std::array<int, std::mt19937::state_size>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ std::mt19937 generator(seq);
+
+ id2 = uuid_random_generator{generator}();
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::random_number_based);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+ }
+
+ REQUIRE(id1 != id2);
+}
+
+TEST_CASE("uuid: Test default generator", "[uuid]")
+{
+ std::random_device rd;
+ auto seed_data = std::array<int, std::mt19937::state_size>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ std::mt19937 generator(seq);
+
+ uuid const guid = uuid_random_generator{generator}();
+ REQUIRE(!guid.is_nil());
+ REQUIRE(guid.version() == uuid_version::random_number_based);
+ REQUIRE(guid.variant() == uuid_variant::rfc);
+}
+
+TEST_CASE("uuid: Test random generator (conversion ctor w/ smart ptr)", "[uuid]")
+{
+ std::random_device rd;
+ auto seed_data = std::array<int, std::mt19937::state_size>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ std::mt19937 generator(seq);
+
+ uuid_random_generator dgen(&generator);
+ auto id1 = dgen();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::random_number_based);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen();
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::random_number_based);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+}
+
+TEST_CASE("uuid: Test random generator (conversion ctor w/ ptr)", "[uuid]")
+{
+ std::random_device rd;
+ auto seed_data = std::array<int, std::mt19937::state_size>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ auto generator = std::make_unique<std::mt19937>(seq);
+
+ uuid_random_generator dgen(generator.get());
+ auto id1 = dgen();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::random_number_based);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id2.version() == uuid_version::random_number_based);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+}
+
+TEST_CASE("uuid: Test random generator (conversion ctor w/ ref)", "[uuid]")
+{
+ std::random_device rd;
+ auto seed_data = std::array<int, std::mt19937::state_size>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ std::mt19937 generator(seq);
+
+ uuid_random_generator dgen(generator);
+ auto id1 = dgen();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::random_number_based);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen();
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::random_number_based);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+}
+
+TEST_CASE("uuid: Test basic random generator (conversion ctor w/ ptr) "
+ "w/ ranlux48_base",
+ "[uuid]")
+{
+ std::random_device rd;
+ auto seed_data = std::array<int, 6>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ std::ranlux48_base generator(seq);
+
+ basic_uuid_random_generator<std::ranlux48_base> dgen(&generator);
+ auto id1 = dgen();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::random_number_based);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen();
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::random_number_based);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+}
+
+TEST_CASE("uuid: Test basic random generator (conversion ctor w/ smart ptr) "
+ "w/ ranlux48_base",
+ "[uuid]")
+{
+ std::random_device rd;
+ auto seed_data = std::array<int, 6>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ auto generator = std::make_unique<std::ranlux48_base>(seq);
+
+ basic_uuid_random_generator<std::ranlux48_base> dgen(generator.get());
+ auto id1 = dgen();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::random_number_based);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen();
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::random_number_based);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+}
+
+TEST_CASE("uuid: Test basic random generator (conversion ctor w/ ref) "
+ "w/ ranlux48_base",
+ "[uuid]")
+{
+ std::random_device rd;
+ auto seed_data = std::array<int, 6>{};
+ std::ranges::generate(seed_data, std::ref(rd));
+ std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
+ std::ranlux48_base generator(seq);
+
+ basic_uuid_random_generator<std::ranlux48_base> dgen(generator);
+ auto id1 = dgen();
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::random_number_based);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen();
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::random_number_based);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+}
+
+TEST_CASE("uuid: Test namespaces", "[uuid]")
+{
+ REQUIRE(uuid_namespace_dns == uuid::from_string("6ba7b810-9dad-11d1-80b4-00c04fd430c8"));
+ REQUIRE(uuid_namespace_url == uuid::from_string("6ba7b811-9dad-11d1-80b4-00c04fd430c8"));
+ REQUIRE(uuid_namespace_oid == uuid::from_string("6ba7b812-9dad-11d1-80b4-00c04fd430c8"));
+ REQUIRE(uuid_namespace_x500 == uuid::from_string("6ba7b814-9dad-11d1-80b4-00c04fd430c8"));
+}
+
+TEST_CASE("uuid: Test name generator (char*)", "[uuid]")
+{
+ uuid_name_generator dgen(uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value());
+
+ auto id1 = dgen("john");
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::name_based_sha1);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen("jane");
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::name_based_sha1);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ auto id3 = dgen("jane");
+ REQUIRE(!id3.is_nil());
+ REQUIRE(id3.version() == uuid_version::name_based_sha1);
+ REQUIRE(id3.variant() == uuid_variant::rfc);
+
+ auto id4 = dgen(L"jane");
+ REQUIRE(!id4.is_nil());
+ REQUIRE(id4.version() == uuid_version::name_based_sha1);
+ REQUIRE(id4.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+ REQUIRE(id2 == id3);
+ REQUIRE(id3 != id4);
+}
+
+TEST_CASE("uuid: Test name generator (std::string)", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ uuid_name_generator dgen(uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value());
+ auto id1 = dgen("john"s);
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::name_based_sha1);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen("jane"s);
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::name_based_sha1);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ auto id3 = dgen("jane"s);
+ REQUIRE(!id3.is_nil());
+ REQUIRE(id3.version() == uuid_version::name_based_sha1);
+ REQUIRE(id3.variant() == uuid_variant::rfc);
+
+ auto id4 = dgen(L"jane"s);
+ REQUIRE(!id4.is_nil());
+ REQUIRE(id4.version() == uuid_version::name_based_sha1);
+ REQUIRE(id4.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+ REQUIRE(id2 == id3);
+ REQUIRE(id3 != id4);
+}
+
+TEST_CASE("uuid: Test name generator (std::string_view)", "[uuid]")
+{
+ using namespace std::string_view_literals;
+
+ uuid_name_generator dgen(uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value());
+ auto id1 = dgen("john"sv);
+ REQUIRE(!id1.is_nil());
+ REQUIRE(id1.version() == uuid_version::name_based_sha1);
+ REQUIRE(id1.variant() == uuid_variant::rfc);
+
+ auto id2 = dgen("jane"sv);
+ REQUIRE(!id2.is_nil());
+ REQUIRE(id2.version() == uuid_version::name_based_sha1);
+ REQUIRE(id2.variant() == uuid_variant::rfc);
+
+ auto id3 = dgen("jane"sv);
+ REQUIRE(!id3.is_nil());
+ REQUIRE(id3.version() == uuid_version::name_based_sha1);
+ REQUIRE(id3.variant() == uuid_variant::rfc);
+
+ auto id4 = dgen(L"jane"sv);
+ REQUIRE(!id4.is_nil());
+ REQUIRE(id4.version() == uuid_version::name_based_sha1);
+ REQUIRE(id4.variant() == uuid_variant::rfc);
+
+ REQUIRE(id1 != id2);
+ REQUIRE(id2 == id3);
+ REQUIRE(id3 != id4);
+}
+
+TEST_CASE("uuid: Test name generator equality (char const*, std::string, "
+ "std::string_view)",
+ "[uuid]")
+{
+ using namespace std::literals;
+
+ auto dgen = uuid_name_generator(
+ uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value());
+ auto id1 = dgen("john");
+ auto id2 = dgen("john"s);
+ auto id3 = dgen("john"sv);
+
+ REQUIRE(id1 == id2);
+ REQUIRE(id2 == id3);
+}
+
+TEST_CASE("uuid: Test default constructor", "[uuid]")
+{
+ auto empty = uuid();
+ REQUIRE(empty.is_nil());
+}
+
+TEST_CASE("uuid: Test string conversion", "[uuid]")
+{
+ auto empty = uuid();
+ REQUIRE(to_string(empty) == "00000000-0000-0000-0000-000000000000");
+ REQUIRE(to_string<wchar_t>(empty) == L"00000000-0000-0000-0000-000000000000");
+}
+
+TEST_CASE("uuid: Test is_valid_uuid(char*)", "[uuid]")
+{
+ REQUIRE(uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43"));
+ REQUIRE(uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43}"));
+ REQUIRE(uuid::is_valid_uuid(L"47183823-2574-4bfd-b411-99ed177d3e43"));
+ REQUIRE(uuid::is_valid_uuid(L"{47183823-2574-4bfd-b411-99ed177d3e43}"));
+ REQUIRE(uuid::is_valid_uuid("00000000-0000-0000-0000-000000000000"));
+ REQUIRE(uuid::is_valid_uuid("{00000000-0000-0000-0000-000000000000}"));
+ REQUIRE(uuid::is_valid_uuid(L"00000000-0000-0000-0000-000000000000"));
+ REQUIRE(uuid::is_valid_uuid(L"{00000000-0000-0000-0000-000000000000}"));
+}
+
+TEST_CASE("uuid: Test is_valid_uuid(basic_string)", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e43"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = L"47183823-2574-4bfd-b411-99ed177d3e43"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = L"{47183823-2574-4bfd-b411-99ed177d3e43}"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "00000000-0000-0000-0000-000000000000"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "{00000000-0000-0000-0000-000000000000}"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = L"00000000-0000-0000-0000-000000000000"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = L"{00000000-0000-0000-0000-000000000000}"s;
+ REQUIRE(uuid::is_valid_uuid(str));
+ }
+}
+
+TEST_CASE("uuid: Test is_valid_uuid(basic_string_view)", "[uuid]")
+{
+ using namespace std::string_view_literals;
+
+ REQUIRE(uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43"sv));
+ REQUIRE(uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43}"sv));
+ REQUIRE(uuid::is_valid_uuid(L"47183823-2574-4bfd-b411-99ed177d3e43"sv));
+ REQUIRE(uuid::is_valid_uuid(L"{47183823-2574-4bfd-b411-99ed177d3e43}"sv));
+ REQUIRE(uuid::is_valid_uuid("00000000-0000-0000-0000-000000000000"sv));
+ REQUIRE(uuid::is_valid_uuid("{00000000-0000-0000-0000-000000000000}"sv));
+ REQUIRE(uuid::is_valid_uuid(L"00000000-0000-0000-0000-000000000000"sv));
+ REQUIRE(uuid::is_valid_uuid(L"{00000000-0000-0000-0000-000000000000}"sv));
+}
+
+TEST_CASE("uuid: Test is_valid_uuid(char*) invalid format", "[uuid]")
+{
+ REQUIRE(!uuid::is_valid_uuid(""));
+ REQUIRE(!uuid::is_valid_uuid("{}"));
+ REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e4"));
+ REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e430"));
+ REQUIRE(!uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43"));
+ REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43}"));
+}
+
+TEST_CASE("uuid: Test is_valid_uuid(basic_string) invalid format", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ {
+ auto str = ""s;
+ REQUIRE(!uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "{}"s;
+ REQUIRE(!uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e4"s;
+ REQUIRE(!uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e430"s;
+ REQUIRE(!uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "{47183823-2574-4bfd-b411-99ed177d3e43"s;
+ REQUIRE(!uuid::is_valid_uuid(str));
+ }
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e43}"s;
+ REQUIRE(!uuid::is_valid_uuid(str));
+ }
+}
+
+TEST_CASE("uuid: Test is_valid_uuid(basic_string_view) invalid format", "[uuid]")
+{
+ using namespace std::string_view_literals;
+
+ REQUIRE(!uuid::is_valid_uuid(""sv));
+ REQUIRE(!uuid::is_valid_uuid("{}"sv));
+ REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e4"sv));
+ REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e430"sv));
+ REQUIRE(!uuid::is_valid_uuid("{47183823-2574-4bfd-b411-99ed177d3e43"sv));
+ REQUIRE(!uuid::is_valid_uuid("47183823-2574-4bfd-b411-99ed177d3e43}"sv));
+}
+
+TEST_CASE("uuid: Test from_string(char*)", "[uuid]")
+{
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e43";
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string(guid) == str);
+ }
+
+ {
+ auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}";
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43");
+ }
+
+ {
+ auto guid = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value();
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43");
+ REQUIRE(to_string<wchar_t>(guid) == L"47183823-2574-4bfd-b411-99ed177d3e43");
+ }
+
+ {
+ auto str = L"47183823-2574-4bfd-b411-99ed177d3e43";
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string<wchar_t>(guid) == str);
+ }
+
+ {
+ auto str = "4718382325744bfdb41199ed177d3e43";
+ REQUIRE_NOTHROW(uuid::from_string(str));
+ REQUIRE(uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "00000000-0000-0000-0000-000000000000";
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = "{00000000-0000-0000-0000-000000000000}";
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = L"00000000-0000-0000-0000-000000000000";
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = L"{00000000-0000-0000-0000-000000000000}";
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+}
+
+TEST_CASE("uuid: Test from_string(basic_string)", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e43"s;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string(guid) == str);
+ }
+
+ {
+ auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}"s;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43");
+ }
+
+ {
+ auto guid = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43"s).value();
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43");
+ REQUIRE(to_string<wchar_t>(guid) == L"47183823-2574-4bfd-b411-99ed177d3e43");
+ }
+
+ {
+ auto str = L"47183823-2574-4bfd-b411-99ed177d3e43"s;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string<wchar_t>(guid) == str);
+ }
+
+ {
+ auto str = "4718382325744bfdb41199ed177d3e43"s;
+ REQUIRE_NOTHROW(uuid::from_string(str));
+ REQUIRE(uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "00000000-0000-0000-0000-000000000000"s;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = "{00000000-0000-0000-0000-000000000000}"s;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = L"00000000-0000-0000-0000-000000000000"s;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = L"{00000000-0000-0000-0000-000000000000}"s;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+}
+
+TEST_CASE("uuid: Test from_string(basic_string_view)", "[uuid]")
+{
+ using namespace std::string_view_literals;
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e43"sv;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string(guid) == str);
+ }
+
+ {
+ auto str = "{47183823-2574-4bfd-b411-99ed177d3e43}"sv;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43");
+ }
+
+ {
+ auto guid = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43"sv).value();
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43");
+ REQUIRE(to_string<wchar_t>(guid) == L"47183823-2574-4bfd-b411-99ed177d3e43");
+ }
+
+ {
+ auto str = L"47183823-2574-4bfd-b411-99ed177d3e43"sv;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(to_string<wchar_t>(guid) == str);
+ }
+
+ {
+ auto str = "4718382325744bfdb41199ed177d3e43"sv;
+ REQUIRE_NOTHROW(uuid::from_string(str));
+ REQUIRE(uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "00000000-0000-0000-0000-000000000000"sv;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = "{00000000-0000-0000-0000-000000000000}"sv;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = L"00000000-0000-0000-0000-000000000000"sv;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+
+ {
+ auto str = L"{00000000-0000-0000-0000-000000000000}"sv;
+ auto guid = uuid::from_string(str).value();
+ REQUIRE(guid.is_nil());
+ }
+}
+
+TEST_CASE("uuid: Test constexpr from_string", "[uuid]")
+{
+ constexpr uuid value = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value();
+ static_assert(!value.is_nil());
+ static_assert(value.variant() == uuid_variant::rfc);
+ static_assert(value.version() != uuid_version::none);
+}
+
+TEST_CASE("uuid: Test from_string(char*) invalid format", "[uuid]")
+{
+ REQUIRE(!uuid::from_string("").has_value());
+ REQUIRE(!uuid::from_string("{}").has_value());
+ REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e4").has_value());
+ REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e430").has_value());
+ REQUIRE(!uuid::from_string("{47183823-2574-4bfd-b411-99ed177d3e43").has_value());
+ REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43}").has_value());
+}
+
+TEST_CASE("uuid: Test from_string(basic_string) invalid format", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ {
+ auto str = ""s;
+ REQUIRE(!uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "{}"s;
+ REQUIRE(!uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e4"s;
+ REQUIRE(!uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e430"s;
+ REQUIRE(!uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "{47183823-2574-4bfd-b411-99ed177d3e43"s;
+ REQUIRE(!uuid::from_string(str).has_value());
+ }
+
+ {
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e43}"s;
+ REQUIRE(!uuid::from_string(str).has_value());
+ }
+}
+
+TEST_CASE("uuid: Test from_string(basic_string_view) invalid format", "[uuid]")
+{
+ using namespace std::string_view_literals;
+
+ REQUIRE(!uuid::from_string(""sv).has_value());
+ REQUIRE(!uuid::from_string("{}"sv).has_value());
+ REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e4"sv).has_value());
+ REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e430"sv).has_value());
+ REQUIRE(!uuid::from_string("{47183823-2574-4bfd-b411-99ed177d3e43"sv).has_value());
+ REQUIRE(!uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43}"sv).has_value());
+}
+
+TEST_CASE("uuid: Test iterators constructor", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ {
+ std::array<uuid::value_type, 16> arr{
+ {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed,
+ 0x17, 0x7d, 0x3e, 0x43}
+ };
+
+ auto const guid = uuid(std::begin(arr), std::end(arr));
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s);
+ }
+
+ {
+ uuid::value_type arr[16] = {// NOLINT
+ 0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd,
+ 0xb4, 0x11, 0x99, 0xed, 0x17, 0x7d, 0x3e, 0x43};
+
+ auto const guid = uuid(std::begin(arr), std::end(arr));
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s);
+ }
+}
+
+TEST_CASE("uuid: Test array constructors", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ {
+ auto const guid = uuid {
+ {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed,
+ 0x17, 0x7d, 0x3e, 0x43}
+ };
+
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s);
+ }
+
+ {
+ auto arr = std::array<uuid::value_type, 16>{
+ {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed,
+ 0x17, 0x7d, 0x3e, 0x43}
+ };
+
+ auto const guid = uuid(arr);
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s);
+ }
+
+ {
+ uuid::value_type arr[16]{// NOLINT
+ 0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd,
+ 0xb4, 0x11, 0x99, 0xed, 0x17, 0x7d, 0x3e, 0x43};
+
+ auto const guid = uuid(arr);
+ REQUIRE(to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43"s);
+ }
+}
+
+TEST_CASE("uuid: Test equality", "[uuid]")
+{
+ uuid empty;
+
+ auto engine = uuid_random_generator::engine_type{};
+ seed_rng(engine);
+ uuid guid = uuid_random_generator{engine}();
+
+ REQUIRE(empty == empty);
+ REQUIRE(guid == guid);
+ REQUIRE(empty != guid);
+}
+
+TEST_CASE("Test comparison", "[uuid]")
+{
+ auto empty = uuid{};
+
+ auto engine = uuid_random_generator::engine_type{};
+ seed_rng(engine);
+
+ auto gen = uuid_random_generator{engine};
+ auto id = gen();
+
+ REQUIRE(empty < id);
+
+ auto ids = std::set{uuid{}, gen(), gen(), gen(), gen()};
+
+ REQUIRE(ids.size() == 5);
+ REQUIRE(ids.contains(uuid{}) == true);
+}
+
+TEST_CASE("uuid: Test hashing", "[uuid]")
+{
+ using namespace std::string_literals;
+
+ auto str = "47183823-2574-4bfd-b411-99ed177d3e43"s;
+ auto guid = uuid::from_string(str).value();
+
+ auto h1 = std::hash<std::string>{};
+ auto h2 = std::hash<uuid>{};
+ REQUIRE(h1(str) != h2(guid));
+
+ auto engine = uuid_random_generator::engine_type{};
+ seed_rng(engine);
+ uuid_random_generator gen{engine};
+
+ std::unordered_set<uuid> ids{uuid{}, gen(), gen(), gen(), gen()};
+
+ REQUIRE(ids.size() == 5);
+ REQUIRE(ids.find(uuid{}) != ids.end());
+}
+
+TEST_CASE("uuid: Test swap", "[uuid]")
+{
+ uuid empty;
+
+ auto engine = uuid_random_generator::engine_type{};
+ seed_rng(engine);
+ uuid guid = uuid_random_generator{engine}();
+
+ REQUIRE(empty.is_nil());
+ REQUIRE(!guid.is_nil());
+
+ std::swap(empty, guid);
+
+ REQUIRE(!empty.is_nil());
+ REQUIRE(guid.is_nil());
+
+ empty.swap(guid);
+
+ REQUIRE(empty.is_nil());
+ REQUIRE(!guid.is_nil());
+}
+
+TEST_CASE("uuid: Test constexpr", "[uuid]")
+{
+ constexpr uuid empty;
+ static_assert(empty.is_nil());
+ static_assert(empty.variant() == uuid_variant::ncs);
+ static_assert(empty.version() == uuid_version::none);
+}
+
+TEST_CASE("uuid: Test size", "[uuid]")
+{
+ REQUIRE(sizeof(uuid) == 16);
+}
+
+TEST_CASE("uuid: Test assignment", "[uuid]")
+{
+ auto id1 = uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value();
+ auto id2 = id1;
+ REQUIRE(id1 == id2);
+
+ id1 = uuid::from_string("{fea43102-064f-4444-adc2-02cec42623f8}").value();
+ REQUIRE(id1 != id2);
+
+ auto id3 = std::move(id2);
+ REQUIRE(to_string(id3) == "47183823-2574-4bfd-b411-99ed177d3e43");
+}
+
+TEST_CASE("uuid: Test trivial", "[uuid]")
+{
+ REQUIRE(std::is_trivially_copyable_v<uuid>);
+}
+
+TEST_CASE("uuid: Test as_bytes", "[uuid]")
+{
+ std::array<uuid::value_type, 16> arr{
+ {0x47, 0x18, 0x38, 0x23, 0x25, 0x74, 0x4b, 0xfd, 0xb4, 0x11, 0x99, 0xed, 0x17, 0x7d,
+ 0x3e, 0x43}
+ };
+
+ {
+ uuid id{arr};
+ REQUIRE(!id.is_nil());
+
+ auto view = id.as_bytes();
+ REQUIRE(memcmp(view.data(), arr.data(), arr.size()) == 0);
+ }
+
+ {
+ const uuid id{arr};
+ REQUIRE(!id.is_nil());
+
+ auto view = id.as_bytes();
+ REQUIRE(memcmp(view.data(), arr.data(), arr.size()) == 0);
+ }
+}
+} // anonymous namespace
+
+// NOLINTEND(bugprone-unchecked-optional-access)