refactor: 💥 ascii -> portable, unicode -> utf8, 'A' -> 'P'

This commit is contained in:
Mateusz Pusz
2024-10-10 00:02:08 +02:00
parent cb424a79c0
commit 4eb63227e2
13 changed files with 158 additions and 150 deletions

View File

@ -164,11 +164,11 @@ code.
please let us know in the associated [GitHub Issue](https://github.com/mpusz/mp-units/issues/93).
## Why Unicode quantity symbols are used by default instead of ASCII-only characters?
## Why UTF-8 quantity symbols are used by default instead of portable characters?
Both C++ and [ISO 80000](../appendix/references.md#ISO80000) are standardized by the ISO.
[ISO 80000](../appendix/references.md#ISO80000) and the [SI](../appendix/references.md#SIBrochure)
standards specify Unicode symbols as the official unit names for some quantities
standards specify UTF-8 symbols as the official unit names for some quantities
(e.g. `Ω` symbol for the resistance quantity).
As the **mp-units** library will be proposed for standardization as a part of the C++ Standard Library
we have to obey the rules and be consistent with ISO specifications.
@ -176,7 +176,7 @@ we have to obey the rules and be consistent with ISO specifications.
!!! note
We do understand engineering reality and the constraints of some environments. This is why the library
has the option of [ASCII-only Quantity Symbols](../users_guide/framework_basics/text_output.md#unit_symbol_formatting).
has the option of [Portable Quantity Symbols](../users_guide/framework_basics/text_output.md#unit_symbol_formatting).
## Why don't we have CMake options to disable the building of tests and examples?

View File

@ -240,18 +240,18 @@ are opt-in. A user has to explicitly "import" them from a dedicated `unit_symbol
quantity q2 = 42 * km / h;
```
We also provide alternative object identifiers using Unicode characters in their names for most
unit symbols. The code using Unicode looks nicer, but it is harder to type on the keyboard.
We also provide alternative object identifiers using UTF-8 characters in their names for most
unit symbols. The code using UTF-8 looks nicer, but it is harder to type on the keyboard.
This is why we provide both versions of identifiers for such units.
=== "ASCII only"
=== "Portable"
```cpp
quantity resistance = 60 * kohm;
quantity capacitance = 100 * uF;
```
=== "With Unicode glyphs"
=== "With UTF-8 glyphs"
```cpp
quantity resistance = 60 * kΩ;

View File

@ -114,18 +114,18 @@ and units of derived quantities.
### `text_encoding`
[ISQ](../../appendix/glossary.md#isq) and [SI](../../appendix/glossary.md#si) standards always
specify symbols using Unicode encoding. This is why it is a default and primary target for
text output. However, in some applications or environments, a standard ASCII-like text output
specify symbols using UTF-8 encoding. This is why it is a default and primary target for
text output. However, in some applications or environments, a standard portable text output
using only the characters from the [basic literal character set](https://en.cppreference.com/w/cpp/language/charset)
can be preferred by users.
This is why the library provides an option to change the default encoding to the ASCII one with:
This is why the library provides an option to change the default encoding to the portable one with:
```cpp
enum class text_encoding : std::int8_t {
unicode, // µs; m³; L²MT⁻³
ascii, // us; m^3; L^2MT^-3
default_encoding = unicode
utf8, // µs; m³; L²MT⁻³
portable, // us; m^3; L^2MT^-3
default_encoding = utf8
};
```
@ -154,7 +154,7 @@ template<dimension_symbol_formatting fmt = dimension_symbol_formatting{}, typena
For example:
```cpp
static_assert(dimension_symbol<{.encoding = text_encoding::ascii}>(isq::power.dimension) == "L^2MT^-3");
static_assert(dimension_symbol<{.encoding = text_encoding::portable}>(isq::power.dimension) == "L^2MT^-3");
```
!!! note
@ -175,7 +175,7 @@ For example:
```cpp
std::string txt;
dimension_symbol_to(std::back_inserter(txt), isq::power.dimension, {.encoding = text_encoding::ascii});
dimension_symbol_to(std::back_inserter(txt), isq::power.dimension, {.encoding = text_encoding::portable});
std::cout << txt << "\n";
```
@ -203,7 +203,7 @@ enum class unit_symbol_solidus : std::int8_t {
enum class unit_symbol_separator : std::int8_t {
space, // kg m²/s²
half_high_dot, // kg⋅m²/s² (valid only for unicode encoding)
half_high_dot, // kg⋅m²/s² (valid only for utf8 encoding)
default_separator = space
};
@ -455,7 +455,7 @@ as text and, thus, are aligned to the left by default.
```ebnf
dimension-format-spec = [fill-and-align], [width], [dimension-spec];
dimension-spec = [text-encoding];
text-encoding = 'U' | 'A';
text-encoding = 'U' | 'P';
```
In the above grammar:
@ -463,8 +463,8 @@ In the above grammar:
- `fill-and-align` and `width` tokens are defined in the [format.string.std](https://wg21.link/format.string.std)
chapter of the C++ standard specification,
- `text-encoding` token specifies the symbol text encoding:
- `U` (default) uses the **Unicode** symbols defined by [@ISO80000] (e.g., `LT⁻²`),
- `A` forces non-standard **ASCII**-only output (e.g., `LT^-2`).
- `U` (default) uses the **UTF-8** symbols defined by [@ISO80000] (e.g., `LT⁻²`),
- `P` forces non-standard **portable** output (e.g., `LT^-2`).
Dimension symbols of some quantities are specified to use Unicode signs by the
[ISQ](../../appendix/glossary.md#isq) (e.g., `Θ` symbol for the _thermodynamic temperature_
@ -475,9 +475,9 @@ symbol can be forced to be printed using such characters thanks to `text-encodin
```cpp
std::println("{}", isq::dim_thermodynamic_temperature); // Θ
std::println("{:A}", isq::dim_thermodynamic_temperature); // O
std::println("{:P}", isq::dim_thermodynamic_temperature); // O
std::println("{}", isq::power.dimension); // L²MT⁻³
std::println("{:A}", isq::power.dimension); // L^2MT^-3
std::println("{:P}", isq::power.dimension); // L^2MT^-3
```
### Unit formatting
@ -506,7 +506,7 @@ In the above grammar:
(e.g., `m s⁻¹`, `kg m⁻¹ s⁻¹`)
- `unit-symbol-separator` token specifies how multiplied unit symbols should be separated:
- 's' (default) uses **space** as a separator (e.g., `kg m²/s²`)
- 'd' uses half-high **dot** (``) as a separator (e.g., `kg⋅m²/s²`) (requires the Unicode encoding)
- 'd' uses half-high **dot** (``) as a separator (e.g., `kg⋅m²/s²`) (requires the UTF-8 encoding)
- 'L' is reserved for possible future localization use in case the C++ standard library gets access to
the ICU-like database.
@ -525,11 +525,11 @@ In such a case, the unit symbol can be forced to be printed using such character
```cpp
std::println("{}", si::ohm); // Ω
std::println("{:A}", si::ohm); // ohm
std::println("{:P}", si::ohm); // ohm
std::println("{}", us); // µs
std::println("{:A}", us); // us
std::println("{:P}", us); // us
std::println("{}", m / s2); // m/s²
std::println("{:A}", m / s2); // m/s^2
std::println("{:P}", m / s2); // m/s^2
```
Additionally, both ISO 80000 and [SI](../../appendix/glossary.md#si) leave some freedom on how to
@ -576,7 +576,7 @@ std::println("{:d}", kg * m2 / s2); // kg⋅m²/s²
!!! note
'd' requires the Unicode encoding to be set.
'd' requires the UTF-8 encoding to be set.
### Quantity formatting

View File

@ -77,7 +77,7 @@ template<Unit auto From, Unit auto To>
#else
[[nodiscard]] std::string_view to_string_view(Unit auto u) { return u.symbol.ascii().c_str(); }
[[nodiscard]] std::string_view to_string_view(Unit auto u) { return u.symbol.portable().c_str(); }
template<Unit auto From, Unit auto To>
[[nodiscard]] double exchange_rate(std::chrono::sys_seconds timestamp)

View File

@ -98,19 +98,19 @@ template<std::intmax_t Value>
template<typename CharT, std::size_t N, std::size_t M, std::output_iterator<CharT> Out>
constexpr Out copy(const symbol_text<N, M>& txt, text_encoding encoding, Out out)
{
if (encoding == text_encoding::unicode) {
if (encoding == text_encoding::utf8) {
if constexpr (is_same_v<CharT, char8_t>)
return ::mp_units::detail::copy(txt.unicode().begin(), txt.unicode().end(), out);
return ::mp_units::detail::copy(txt.utf8().begin(), txt.utf8().end(), out);
else if constexpr (is_same_v<CharT, char>) {
for (const char8_t ch : txt.unicode()) *out++ = static_cast<char>(ch);
for (const char8_t ch : txt.utf8()) *out++ = static_cast<char>(ch);
return out;
} else
MP_UNITS_THROW(std::invalid_argument("Unicode text can't be copied to CharT output"));
MP_UNITS_THROW(std::invalid_argument("UTF-8 text can't be copied to CharT output"));
} else {
if constexpr (is_same_v<CharT, char>)
return ::mp_units::detail::copy(txt.ascii().begin(), txt.ascii().end(), out);
return ::mp_units::detail::copy(txt.portable().begin(), txt.portable().end(), out);
else
MP_UNITS_THROW(std::invalid_argument("ASCII text can't be copied to CharT output"));
MP_UNITS_THROW(std::invalid_argument("Portable text can't be copied to CharT output"));
}
}

View File

@ -112,7 +112,7 @@ MP_UNITS_EXPORT_END
//
// dimension-format-spec = [fill-and-align], [width], [dimension-spec];
// dimension-spec = [text-encoding];
// text-encoding = 'U' | 'A';
// text-encoding = 'U' | 'P';
//
template<mp_units::Dimension D, typename Char>
class MP_UNITS_STD_FMT::formatter<D, Char> {
@ -126,15 +126,16 @@ class MP_UNITS_STD_FMT::formatter<D, Char> {
auto it = begin;
if (it == end || *it == '}') return begin;
constexpr auto valid_modifiers = std::string_view{"UA"};
constexpr auto valid_modifiers = std::string_view{"UP"};
for (; it != end && *it != '}'; ++it) {
if (valid_modifiers.find(*it) == std::string_view::npos)
throw MP_UNITS_STD_FMT::format_error("invalid dimension modifier specified");
}
end = it;
if (it = mp_units::detail::at_most_one_of(begin, end, "UA"); it != end)
specs_.encoding = (*it == 'U') ? mp_units::text_encoding::unicode : mp_units::text_encoding::ascii;
if (it = mp_units::detail::at_most_one_of(begin, end, "UAP"); it != end)
// TODO 'A' stands for an old and deprecated ASCII encoding
specs_.encoding = (*it == 'U') ? mp_units::text_encoding::utf8 : mp_units::text_encoding::portable;
return end;
}
@ -198,15 +199,16 @@ class MP_UNITS_STD_FMT::formatter<U, Char> {
auto it = begin;
if (it == end || *it == '}') return begin;
constexpr auto valid_modifiers = std::string_view{"UA1ansd"};
constexpr auto valid_modifiers = std::string_view{"UAP1ansd"};
for (; it != end && *it != '}'; ++it) {
if (valid_modifiers.find(*it) == std::string_view::npos)
throw MP_UNITS_STD_FMT::format_error("invalid unit modifier specified");
}
end = it;
if (it = mp_units::detail::at_most_one_of(begin, end, "UA"); it != end)
specs_.encoding = (*it == 'U') ? mp_units::text_encoding::unicode : mp_units::text_encoding::ascii;
if (it = mp_units::detail::at_most_one_of(begin, end, "UAP"); it != end)
// TODO 'A' stands for an old and deprecated ASCII encoding
specs_.encoding = (*it == 'U') ? mp_units::text_encoding::utf8 : mp_units::text_encoding::portable;
if (it = mp_units::detail::at_most_one_of(begin, end, "1an"); it != end) {
switch (*it) {
case '1':
@ -221,8 +223,8 @@ class MP_UNITS_STD_FMT::formatter<U, Char> {
}
}
if (it = mp_units::detail::at_most_one_of(begin, end, "sd"); it != end) {
if (*it == 'd' && specs_.encoding == mp_units::text_encoding::ascii)
throw MP_UNITS_STD_FMT::format_error("half_high_dot unit separator allowed only for Unicode encoding");
if (*it == 'd' && specs_.encoding == mp_units::text_encoding::portable)
throw MP_UNITS_STD_FMT::format_error("half_high_dot unit separator allowed only for UTF-8 encoding");
specs_.separator =
(*it == 's') ? mp_units::unit_symbol_separator::space : mp_units::unit_symbol_separator::half_high_dot;
}

View File

@ -310,9 +310,9 @@ template<typename CharT, std::output_iterator<CharT> Out>
constexpr Out print_separator(Out out, const unit_symbol_formatting& fmt)
{
if (fmt.separator == unit_symbol_separator::half_high_dot) {
if (fmt.encoding != text_encoding::unicode)
if (fmt.encoding != text_encoding::utf8)
MP_UNITS_THROW(
std::invalid_argument("'unit_symbol_separator::half_high_dot' can be only used with 'text_encoding::unicode'"));
std::invalid_argument("'unit_symbol_separator::half_high_dot' can be only used with 'text_encoding::utf8'"));
const std::string_view dot = "";
out = detail::copy(dot.begin(), dot.end(), out);
} else {

View File

@ -56,9 +56,11 @@ namespace mp_units {
// NOLINTNEXTLINE(readability-enum-initial-value)
MP_UNITS_EXPORT enum class text_encoding : std::int8_t {
unicode, // µs; m³; L²MT⁻³
ascii, // us; m^3; L^2MT^-3
default_encoding = unicode
utf8, // µs; m³; L²MT⁻³
unicode [[deprecated("Use `utf8` instead")]] = utf8,
portable, // us; m^3; L^2MT^-3
ascii [[deprecated("Use `portable` instead")]] = portable,
default_encoding = utf8
};
namespace detail {
@ -89,65 +91,67 @@ constexpr fixed_u8string<N> to_u8string(fixed_string<N> txt)
*
* This class template is responsible for definition and handling of a symbol text
* representation. In the libary it is used to define symbols of units and prefixes.
* Each symbol can have two versions: Unicode and ASCI-only.
* Each symbol can have two versions: UTF-8 and portable.
*
* @tparam N The size of a Unicode symbol
* @tparam M The size of the ASCII-only symbol
* @tparam N The size of a UTF-8 symbol
* @tparam M The size of the portable symbol
*/
MP_UNITS_EXPORT template<std::size_t N, std::size_t M>
class symbol_text {
public:
fixed_u8string<N> unicode_;
fixed_string<M> ascii_;
fixed_u8string<N> utf8_;
fixed_string<M> portable_;
// NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
constexpr explicit(false) symbol_text(char ch) : unicode_(static_cast<char8_t>(ch)), ascii_(ch)
constexpr explicit(false) symbol_text(char ch) : utf8_(static_cast<char8_t>(ch)), portable_(ch)
{
MP_UNITS_EXPECTS(detail::is_basic_literal_character_set_char(ch));
}
// NOLINTNEXTLINE(*-avoid-c-arrays, google-explicit-constructor, hicpp-explicit-conversions)
consteval explicit(false) symbol_text(const char (&txt)[N + 1]) :
unicode_(detail::to_u8string(basic_fixed_string{txt})), ascii_(txt)
utf8_(detail::to_u8string(basic_fixed_string{txt})), portable_(txt)
{
MP_UNITS_EXPECTS(txt[N] == char{});
MP_UNITS_EXPECTS(detail::is_basic_literal_character_set(txt));
}
// NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
constexpr explicit(false) symbol_text(const fixed_string<N>& txt) : unicode_(detail::to_u8string(txt)), ascii_(txt)
constexpr explicit(false) symbol_text(const fixed_string<N>& txt) : utf8_(detail::to_u8string(txt)), portable_(txt)
{
MP_UNITS_EXPECTS(detail::is_basic_literal_character_set(txt.data_));
}
// NOLINTNEXTLINE(*-avoid-c-arrays)
consteval symbol_text(const char8_t (&u)[N + 1], const char (&a)[M + 1]) : unicode_(u), ascii_(a)
consteval symbol_text(const char8_t (&u)[N + 1], const char (&a)[M + 1]) : utf8_(u), portable_(a)
{
MP_UNITS_EXPECTS(u[N] == char8_t{});
MP_UNITS_EXPECTS(a[M] == char{});
MP_UNITS_EXPECTS(detail::is_basic_literal_character_set(a));
}
constexpr symbol_text(const fixed_u8string<N>& unicode, const fixed_string<M>& ascii) :
unicode_(unicode), ascii_(ascii)
constexpr symbol_text(const fixed_u8string<N>& utf8, const fixed_string<M>& portable) :
utf8_(utf8), portable_(portable)
{
MP_UNITS_EXPECTS(detail::is_basic_literal_character_set(ascii.data_));
MP_UNITS_EXPECTS(detail::is_basic_literal_character_set(portable.data_));
}
[[nodiscard]] constexpr const auto& unicode() const { return unicode_; }
[[nodiscard]] constexpr const auto& ascii() const { return ascii_; }
[[nodiscard]] constexpr const auto& utf8() const { return utf8_; }
[[nodiscard]] constexpr const auto& portable() const { return portable_; }
[[deprecated("Use `utf8()` instead")]] constexpr const auto& unicode() const { return utf8(); }
[[deprecated("Use `portable()` instead")]] constexpr const auto& ascii() const { return portable(); }
[[nodiscard]] constexpr bool empty() const
{
MP_UNITS_ASSERT_DEBUG(unicode().empty() == ascii().empty());
return unicode().empty();
MP_UNITS_ASSERT_DEBUG(utf8().empty() == portable().empty());
return utf8().empty();
}
template<std::size_t N2, std::size_t M2>
[[nodiscard]] constexpr friend symbol_text<N + N2, M + M2> operator+(const symbol_text& lhs,
const symbol_text<N2, M2>& rhs)
{
return symbol_text<N + N2, M + M2>(lhs.unicode() + rhs.unicode(), lhs.ascii() + rhs.ascii());
return symbol_text<N + N2, M + M2>(lhs.utf8() + rhs.utf8(), lhs.portable() + rhs.portable());
}
template<std::size_t N2, std::size_t M2>
@ -155,15 +159,15 @@ public:
{
MP_UNITS_DIAGNOSTIC_PUSH
MP_UNITS_DIAGNOSTIC_IGNORE_ZERO_AS_NULLPOINTER_CONSTANT
if (const auto cmp = lhs.unicode() <=> rhs.unicode(); cmp != 0) return cmp;
if (const auto cmp = lhs.utf8() <=> rhs.utf8(); cmp != 0) return cmp;
MP_UNITS_DIAGNOSTIC_POP
return lhs.ascii() <=> rhs.ascii();
return lhs.portable() <=> rhs.portable();
}
template<std::size_t N2, std::size_t M2>
[[nodiscard]] friend constexpr bool operator==(const symbol_text& lhs, const symbol_text<N2, M2>& rhs) noexcept
{
return lhs.unicode() == rhs.unicode() && lhs.ascii() == rhs.ascii();
return lhs.utf8() == rhs.utf8() && lhs.portable() == rhs.portable();
}
};

View File

@ -48,7 +48,7 @@ enum class unit_symbol_solidus : std::int8_t {
// NOLINTNEXTLINE(readability-enum-initial-value)
enum class unit_symbol_separator : std::int8_t {
space, // kg m²/s²
half_high_dot, // kg⋅m²/s² (valid only for unicode encoding)
half_high_dot, // kg⋅m²/s² (valid only for utf8 encoding)
default_separator = space
};

View File

@ -443,7 +443,7 @@ TEST_CASE("Unit formatting should use proper text encoding")
CHECK(MP_UNITS_STD_FMT::format("{}", m / s2) == "m/s²");
}
SECTION("ASCII text output")
SECTION("Portable text output")
{
CHECK(MP_UNITS_STD_FMT::format("{:A}", km / h) == "km/h");
CHECK(MP_UNITS_STD_FMT::format("{:A}", si::kilo<si::ohm>) == "kohm");
@ -618,7 +618,7 @@ TEST_CASE("more then one modifier of the same kind should throw", "[text][fmt][e
}
}
TEST_CASE("half_high_dot separator requested for ASCII encoding should throw", "[text][fmt][exception]")
TEST_CASE("half_high_dot separator requested for portable encoding should throw", "[text][fmt][exception]")
{
REQUIRE_THROWS_MATCHES(MP_UNITS_STD_FMT::vformat("{:dAa}", MP_UNITS_STD_FMT::make_format_args(m)),
MP_UNITS_STD_FMT::format_error,
@ -1037,9 +1037,9 @@ TEST_CASE("unit_symbol", "[text]")
CHECK(os.str() == "m/s²");
}
SECTION("ASCII mode")
SECTION("Portable mode")
{
os << unit_symbol<unit_symbol_formatting{.encoding = ascii}>(m / s2);
os << unit_symbol<unit_symbol_formatting{.encoding = portable}>(m / s2);
CHECK(os.str() == "m/s^2");
}
@ -1068,9 +1068,9 @@ TEST_CASE("dimension_symbol", "[text]")
CHECK(os.str() == "L²MT⁻³");
}
SECTION("ASCII mode")
SECTION("Portable mode")
{
os << dimension_symbol<dimension_symbol_formatting{.encoding = ascii}>(isq::power.dimension);
os << dimension_symbol<dimension_symbol_formatting{.encoding = portable}>(isq::power.dimension);
CHECK(os.str() == "L^2MT^-3");
}
}

View File

@ -39,14 +39,14 @@ static_assert(dimension_symbol(dimension_one) == "1");
// base dimensions
static_assert(dimension_symbol(isq::dim_length) == "L");
static_assert(dimension_symbol(isq::dim_thermodynamic_temperature) == "Θ");
static_assert(dimension_symbol<dimension_symbol_formatting{.encoding = ascii}>(isq::dim_thermodynamic_temperature) ==
static_assert(dimension_symbol<dimension_symbol_formatting{.encoding = portable}>(isq::dim_thermodynamic_temperature) ==
"O");
// derived dimensions
static_assert(dimension_symbol(isq::speed.dimension) == "LT⁻¹");
static_assert(dimension_symbol<dimension_symbol_formatting{.encoding = ascii}>(isq::speed.dimension) == "LT^-1");
static_assert(dimension_symbol<dimension_symbol_formatting{.encoding = portable}>(isq::speed.dimension) == "LT^-1");
static_assert(dimension_symbol(isq::power.dimension) == "L²MT⁻³");
static_assert(dimension_symbol<dimension_symbol_formatting{.encoding = ascii}>(isq::power.dimension) == "L^2MT^-3");
static_assert(dimension_symbol<dimension_symbol_formatting{.encoding = portable}>(isq::power.dimension) == "L^2MT^-3");
static_assert(dimension_symbol(pow<123>(isq::dim_length)) == "L¹²³");
static_assert(dimension_symbol(pow<1, 2>(isq::dim_length)) == "L^(1/2)");

View File

@ -37,26 +37,26 @@ static_assert(sym1 <= 'b');
static_assert(sym1 <= 'c');
static_assert(sym1 >= 'b');
static_assert(sym1 >= 'a');
static_assert(sym1.unicode() == u8"b");
static_assert(sym1.ascii() == "b");
static_assert(sym1.utf8() == u8"b");
static_assert(sym1.portable() == "b");
constexpr symbol_text sym3("ab");
static_assert(sym3.unicode() == u8"ab");
static_assert(sym3.ascii() == "ab");
static_assert(sym3.utf8() == u8"ab");
static_assert(sym3.portable() == "ab");
constexpr basic_fixed_string txt1("bc");
constexpr symbol_text sym4(txt1);
static_assert(sym4.unicode() == u8"bc");
static_assert(sym4.ascii() == "bc");
static_assert(sym4.utf8() == u8"bc");
static_assert(sym4.portable() == "bc");
constexpr symbol_text sym5(u8"bc", "de");
static_assert(sym5.unicode() == u8"bc");
static_assert(sym5.ascii() == "de");
static_assert(sym5.utf8() == u8"bc");
static_assert(sym5.portable() == "de");
constexpr basic_fixed_string txt2("de");
constexpr symbol_text sym6(sym4.unicode(), txt2);
static_assert(sym6.unicode() == u8"bc");
static_assert(sym6.ascii() == "de");
constexpr symbol_text sym6(sym4.utf8(), txt2);
static_assert(sym6.utf8() == u8"bc");
static_assert(sym6.portable() == "de");
static_assert(sym6 == symbol_text(u8"bc", "de"));
static_assert(sym6 != symbol_text(u8"fg", "hi"));

View File

@ -48,59 +48,59 @@ static_assert(unit_symbol(metre) == "m");
static_assert(unit_symbol(second) == "s");
static_assert(unit_symbol(joule) == "J");
static_assert(unit_symbol(degree_Celsius) == "\u2103");
static_assert(unit_symbol<usf{.encoding = ascii}>(degree_Celsius) == "`C");
static_assert(unit_symbol<usf{.encoding = portable}>(degree_Celsius) == "`C");
static_assert(unit_symbol(kilogram) == "kg");
static_assert(unit_symbol(hour) == "h");
// prefixed units
static_assert(unit_symbol(quecto<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(quecto<ohm>) == "qohm");
static_assert(unit_symbol<usf{.encoding = portable}>(quecto<ohm>) == "qohm");
static_assert(unit_symbol(ronto<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(ronto<ohm>) == "rohm");
static_assert(unit_symbol<usf{.encoding = portable}>(ronto<ohm>) == "rohm");
static_assert(unit_symbol(yocto<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(yocto<ohm>) == "yohm");
static_assert(unit_symbol<usf{.encoding = portable}>(yocto<ohm>) == "yohm");
static_assert(unit_symbol(zepto<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(zepto<ohm>) == "zohm");
static_assert(unit_symbol<usf{.encoding = portable}>(zepto<ohm>) == "zohm");
static_assert(unit_symbol(atto<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(atto<ohm>) == "aohm");
static_assert(unit_symbol<usf{.encoding = portable}>(atto<ohm>) == "aohm");
static_assert(unit_symbol(femto<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(femto<ohm>) == "fohm");
static_assert(unit_symbol<usf{.encoding = portable}>(femto<ohm>) == "fohm");
static_assert(unit_symbol(pico<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(pico<ohm>) == "pohm");
static_assert(unit_symbol<usf{.encoding = portable}>(pico<ohm>) == "pohm");
static_assert(unit_symbol(nano<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(nano<ohm>) == "nohm");
static_assert(unit_symbol<usf{.encoding = portable}>(nano<ohm>) == "nohm");
static_assert(unit_symbol(micro<ohm>) == "µΩ");
static_assert(unit_symbol<usf{.encoding = ascii}>(micro<ohm>) == "uohm");
static_assert(unit_symbol<usf{.encoding = portable}>(micro<ohm>) == "uohm");
static_assert(unit_symbol(milli<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(milli<ohm>) == "mohm");
static_assert(unit_symbol<usf{.encoding = portable}>(milli<ohm>) == "mohm");
static_assert(unit_symbol(centi<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(centi<ohm>) == "cohm");
static_assert(unit_symbol<usf{.encoding = portable}>(centi<ohm>) == "cohm");
static_assert(unit_symbol(deci<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(deci<ohm>) == "dohm");
static_assert(unit_symbol<usf{.encoding = portable}>(deci<ohm>) == "dohm");
static_assert(unit_symbol(deca<ohm>) == "daΩ");
static_assert(unit_symbol<usf{.encoding = ascii}>(deca<ohm>) == "daohm");
static_assert(unit_symbol<usf{.encoding = portable}>(deca<ohm>) == "daohm");
static_assert(unit_symbol(hecto<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(hecto<ohm>) == "hohm");
static_assert(unit_symbol<usf{.encoding = portable}>(hecto<ohm>) == "hohm");
static_assert(unit_symbol(kilo<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(kilo<ohm>) == "kohm");
static_assert(unit_symbol<usf{.encoding = portable}>(kilo<ohm>) == "kohm");
static_assert(unit_symbol(mega<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(mega<ohm>) == "Mohm");
static_assert(unit_symbol<usf{.encoding = portable}>(mega<ohm>) == "Mohm");
static_assert(unit_symbol(giga<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(giga<ohm>) == "Gohm");
static_assert(unit_symbol<usf{.encoding = portable}>(giga<ohm>) == "Gohm");
static_assert(unit_symbol(tera<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(tera<ohm>) == "Tohm");
static_assert(unit_symbol<usf{.encoding = portable}>(tera<ohm>) == "Tohm");
static_assert(unit_symbol(peta<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(peta<ohm>) == "Pohm");
static_assert(unit_symbol<usf{.encoding = portable}>(peta<ohm>) == "Pohm");
static_assert(unit_symbol(exa<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(exa<ohm>) == "Eohm");
static_assert(unit_symbol<usf{.encoding = portable}>(exa<ohm>) == "Eohm");
static_assert(unit_symbol(zetta<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(zetta<ohm>) == "Zohm");
static_assert(unit_symbol<usf{.encoding = portable}>(zetta<ohm>) == "Zohm");
static_assert(unit_symbol(yotta<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(yotta<ohm>) == "Yohm");
static_assert(unit_symbol<usf{.encoding = portable}>(yotta<ohm>) == "Yohm");
static_assert(unit_symbol(ronna<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(ronna<ohm>) == "Rohm");
static_assert(unit_symbol<usf{.encoding = portable}>(ronna<ohm>) == "Rohm");
static_assert(unit_symbol(quetta<ohm>) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(quetta<ohm>) == "Qohm");
static_assert(unit_symbol<usf{.encoding = portable}>(quetta<ohm>) == "Qohm");
static_assert(unit_symbol(kibi<bit>) == "Kibit");
static_assert(unit_symbol(mebi<bit>) == "Mibit");
@ -113,13 +113,13 @@ static_assert(unit_symbol(yobi<bit>) == "Yibit");
// scaled units
static_assert(unit_symbol(mag<100> * metre) == "[100 m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<100> * metre) == "[100 m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<100> * metre) == "[100 m]");
static_assert(unit_symbol(mag<1000> * metre) == "[10³ m]");
static_assert(unit_symbol(mag_power<10, 3> * metre) == "[10³ m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<1000> * metre) == "[10^3 m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<1000> * metre) == "[10^3 m]");
static_assert(unit_symbol(mag<6000> * metre) == "[6 × 10³ m]");
static_assert(unit_symbol(mag<6> * mag_power<10, 3> * metre) == "[6 × 10³ m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<6000> * metre) == "[6 x 10^3 m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<6000> * metre) == "[6 x 10^3 m]");
static_assert(unit_symbol(mag<10600> * metre) == "[10600 m]");
static_assert(unit_symbol(mag<60> * second) == "[60 s]");
static_assert(unit_symbol(mag_ratio<1, 18> * metre / second) == "[1/18 m]/s");
@ -128,18 +128,18 @@ static_assert(unit_symbol(mag_ratio<1, 1800> * metre / second) == "[1/1800 m]/s"
static_assert(unit_symbol(mag_ratio<1, 1800> * (metre / second)) == "[1/1800 m/s]");
static_assert(unit_symbol(mag_ratio<1, 18000> * metre / second) == "[1/18 × 10⁻³ m]/s");
static_assert(unit_symbol(mag_ratio<1, 18000> * (metre / second)) == "[1/18 × 10⁻³ m/s]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag_ratio<1, 18000> * metre / second) == "[1/18 x 10^-3 m]/s");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag_ratio<1, 18000> * (metre / second)) == "[1/18 x 10^-3 m/s]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag_ratio<1, 18000> * metre / second) == "[1/18 x 10^-3 m]/s");
static_assert(unit_symbol<usf{.encoding = portable}>(mag_ratio<1, 18000> * (metre / second)) == "[1/18 x 10^-3 m/s]");
// TODO implement all the below
// static_assert(unit_symbol(mag_power<2, 1, 2> * one) == "[2^(1/2)]");
// static_assert(unit_symbol<usf{.encoding = ascii}>(mag_power<2, 1, 2> * one) == "[2^(1/2)]");
// static_assert(unit_symbol<usf{.encoding = portable}>(mag_power<2, 1, 2> * one) == "[2^(1/2)]");
// static_assert(unit_symbol(mag_power<2, 1, 2> * m) == "[2^(1/2) m]");
// static_assert(unit_symbol<usf{.encoding = ascii}>(mag_power<2, 1, 2> * m) == "[2^(1/2) m]");
// static_assert(unit_symbol<usf{.encoding = portable}>(mag_power<2, 1, 2> * m) == "[2^(1/2) m]");
// static_assert(unit_symbol(mag<1> / mag_power<2, 1, 2> * one) == "[1/2^(1/2)]");
// static_assert(unit_symbol<usf{.encoding = ascii}>(mag<1> / mag_power<2, 1, 2> * one) == "[1/2^(1/2)]");
// static_assert(unit_symbol<usf{.encoding = portable}>(mag<1> / mag_power<2, 1, 2> * one) == "[1/2^(1/2)]");
// static_assert(unit_symbol(mag<1> / mag_power<2, 1, 2> * m) == "[1/2^(1/2) m]");
// static_assert(unit_symbol<usf{.encoding = ascii}>(mag<1> / mag_power<2, 1, 2> * m) == "[1/2^(1/2) m]");
// static_assert(unit_symbol<usf{.encoding = portable}>(mag<1> / mag_power<2, 1, 2> * m) == "[1/2^(1/2) m]");
// magnitude constants
#if defined MP_UNITS_COMP_CLANG || MP_UNITS_COMP_CLANG < 18
@ -151,47 +151,47 @@ inline constexpr struct e final : mag_constant<"e", std::numbers::e_v<long doubl
} e;
static_assert(unit_symbol(mag<pi> * one) == "[𝜋]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<pi> * one) == "[pi]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<pi> * one) == "[pi]");
static_assert(unit_symbol(mag<pi> * metre) == "[𝜋 m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<pi> * metre) == "[pi m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<pi> * metre) == "[pi m]");
static_assert(unit_symbol(mag<2> * mag<pi> * metre) == "[2 𝜋 m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<2> * mag<pi> * metre) == "[2 pi m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<2> * mag<pi> * metre) == "[2 pi m]");
static_assert(unit_symbol<usf{.separator = half_high_dot}>(mag<2> * mag<pi> * metre) == "[2⋅𝜋 m]");
static_assert(unit_symbol(mag<1> / mag<pi> * one) == "[1/𝜋]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<1> / mag<pi> * one) == "[1/pi]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<1> / mag<pi> * one) == "[1/pi]");
static_assert(unit_symbol<usf{.solidus = never}>(mag<1> / mag<pi> * one) == "[𝜋⁻¹]");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(mag<1> / mag<pi> * one) == "[pi^-1]");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(mag<1> / mag<pi> * one) == "[pi^-1]");
static_assert(unit_symbol(mag<1> / mag<pi> * metre) == "[1/𝜋 m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<1> / mag<pi> * metre) == "[1/pi m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<1> / mag<pi> * metre) == "[1/pi m]");
static_assert(unit_symbol<usf{.solidus = never}>(mag<1> / mag<pi> * metre) == "[𝜋⁻¹ m]");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(mag<1> / mag<pi> * metre) == "[pi^-1 m]");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(mag<1> / mag<pi> * metre) == "[pi^-1 m]");
static_assert(unit_symbol(mag<2> / mag<pi> * metre) == "[2/𝜋 m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<2> / mag<pi> * metre) == "[2/pi m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<2> / mag<pi> * metre) == "[2/pi m]");
static_assert(unit_symbol<usf{.solidus = never}>(mag<2> / mag<pi> * metre) == "[2 𝜋⁻¹ m]");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(mag<2> / mag<pi> * metre) == "[2 pi^-1 m]");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(mag<2> / mag<pi> * metre) == "[2 pi^-1 m]");
static_assert(unit_symbol<usf{.solidus = never, .separator = half_high_dot}>(mag<2> / mag<pi> * metre) == "[2⋅𝜋⁻¹ m]");
static_assert(unit_symbol(mag<1> / (mag<2> * mag<pi>)*metre) == "[2⁻¹ 𝜋⁻¹ m]");
static_assert(unit_symbol<usf{.solidus = always}>(mag<1> / (mag<2> * mag<pi>)*metre) == "[1/(2 𝜋) m]");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = always}>(mag<1> / (mag<2> * mag<pi>)*metre) ==
static_assert(unit_symbol<usf{.encoding = portable, .solidus = always}>(mag<1> / (mag<2> * mag<pi>)*metre) ==
"[1/(2 pi) m]");
static_assert(unit_symbol(mag_ratio<1, 2> / mag<pi> * metre) == "[2⁻¹ 𝜋⁻¹ m]");
static_assert(unit_symbol<usf{.solidus = always}>(mag_ratio<1, 2> / mag<pi> * metre) == "[1/(2 𝜋) m]");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = always}>(mag_ratio<1, 2> / mag<pi> * metre) ==
static_assert(unit_symbol<usf{.encoding = portable, .solidus = always}>(mag_ratio<1, 2> / mag<pi> * metre) ==
"[1/(2 pi) m]");
static_assert(unit_symbol(mag_ratio<1, 2> * mag<pi> * metre) == "[𝜋/2 m]");
static_assert(unit_symbol(mag_power<pi, 2> * one) == "[𝜋²]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag_power<pi, 2> * one) == "[pi^2]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag_power<pi, 2> * one) == "[pi^2]");
static_assert(unit_symbol(mag_power<pi, 1, 2> * metre) == "[𝜋^(1/2) m]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag_power<pi, 1, 2> * metre) == "[pi^(1/2) m]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag_power<pi, 1, 2> * metre) == "[pi^(1/2) m]");
static_assert(unit_symbol(mag<pi> * mag<e> * one) == "[e 𝜋]");
static_assert(unit_symbol(mag<e> * mag<pi> * one) == "[e 𝜋]");
static_assert(unit_symbol<usf{.encoding = ascii}>(mag<pi> * mag<e> * one) == "[e pi]");
static_assert(unit_symbol<usf{.encoding = portable}>(mag<pi> * mag<e> * one) == "[e pi]");
static_assert(unit_symbol(mag<pi> / mag<e> * one) == "[𝜋/e]");
static_assert(unit_symbol(mag<1> / mag<e> * mag<pi> * one) == "[𝜋/e]");
static_assert(unit_symbol<usf{.solidus = never}>(mag<pi> / mag<e> * one) == "[𝜋 e⁻¹]");
@ -216,49 +216,51 @@ static_assert(unit_symbol(get_common_unit(radian, degree)) == "EQUIV{[1/𝜋°],
static_assert(unit_symbol(one) == ""); // NOLINT(readability-container-size-empty)
static_assert(unit_symbol(percent) == "%");
static_assert(unit_symbol(per_mille) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(per_mille) == "%o");
static_assert(unit_symbol<usf{.encoding = portable}>(per_mille) == "%o");
static_assert(unit_symbol(parts_per_million) == "ppm");
static_assert(unit_symbol(square(metre)) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(square(metre)) == "m^2");
static_assert(unit_symbol<usf{.encoding = portable}>(square(metre)) == "m^2");
static_assert(unit_symbol(cubic(metre)) == "");
static_assert(unit_symbol<usf{.encoding = ascii}>(cubic(metre)) == "m^3");
static_assert(unit_symbol<usf{.encoding = portable}>(cubic(metre)) == "m^3");
static_assert(unit_symbol(kilo<metre> * metre) == "km m");
static_assert(unit_symbol<usf{.separator = half_high_dot}>(kilo<metre> * metre) == "km⋅m");
static_assert(unit_symbol(metre / metre) == ""); // NOLINT(readability-container-size-empty)
static_assert(unit_symbol(kilo<metre> / metre) == "km/m");
static_assert(unit_symbol<usf{.solidus = never}>(kilo<metre> / metre) == "km m⁻¹");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(kilo<metre> / metre) == "km m^-1");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(kilo<metre> / metre) == "km m^-1");
static_assert(unit_symbol(metre / second) == "m/s");
static_assert(unit_symbol<usf{.solidus = always}>(metre / second) == "m/s");
static_assert(unit_symbol<usf{.solidus = never}>(metre / second) == "m s⁻¹");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(metre / second) == "m s^-1");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(metre / second) == "m s^-1");
static_assert(unit_symbol<usf{.solidus = never, .separator = half_high_dot}>(metre / second) == "m⋅s⁻¹");
static_assert(unit_symbol(metre / square(second)) == "m/s²");
static_assert(unit_symbol<usf{.encoding = ascii}>(metre / square(second)) == "m/s^2");
static_assert(unit_symbol<usf{.encoding = portable}>(metre / square(second)) == "m/s^2");
static_assert(unit_symbol<usf{.solidus = always}>(metre / square(second)) == "m/s²");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = always}>(metre / square(second)) == "m/s^2");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = always}>(metre / square(second)) == "m/s^2");
static_assert(unit_symbol<usf{.solidus = never}>(metre / square(second)) == "m s⁻²");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(metre / square(second)) == "m s^-2");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(metre / square(second)) == "m s^-2");
static_assert(unit_symbol<usf{.solidus = never, .separator = half_high_dot}>(metre / square(second)) == "m⋅s⁻²");
static_assert(unit_symbol(kilogram * metre / square(second)) == "kg m/s²");
static_assert(unit_symbol<usf{.separator = half_high_dot}>(kilogram * metre / square(second)) == "kg⋅m/s²");
static_assert(unit_symbol<usf{.encoding = ascii}>(kilogram * metre / square(second)) == "kg m/s^2");
static_assert(unit_symbol<usf{.encoding = portable}>(kilogram * metre / square(second)) == "kg m/s^2");
static_assert(unit_symbol<usf{.solidus = always}>(kilogram * metre / square(second)) == "kg m/s²");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = always}>(kilogram * metre / square(second)) == "kg m/s^2");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = always}>(kilogram * metre / square(second)) ==
"kg m/s^2");
static_assert(unit_symbol<usf{.solidus = never}>(kilogram * metre / square(second)) == "kg m s⁻²");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(kilogram * metre / square(second)) == "kg m s^-2");
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(kilogram * metre / square(second)) ==
"kg m s^-2");
static_assert(unit_symbol<usf{.solidus = never, .separator = half_high_dot}>(kilogram * metre / square(second)) ==
"kg⋅m⋅s⁻²");
static_assert(unit_symbol(one / metre / square(second)) == "m⁻¹ s⁻²");
static_assert(unit_symbol<usf{.solidus = always}>(one / metre / square(second)) == "1/(m s²)");
static_assert(unit_symbol(kilogram / metre / square(second)) == "kg m⁻¹ s⁻²");
static_assert(unit_symbol<usf{.separator = half_high_dot}>(kilogram / metre / square(second)) == "kg⋅m⁻¹⋅s⁻²");
static_assert(unit_symbol<usf{.encoding = ascii}>(kilogram / metre / square(second)) == "kg m^-1 s^-2");
static_assert(unit_symbol<usf{.encoding = portable}>(kilogram / metre / square(second)) == "kg m^-1 s^-2");
static_assert(unit_symbol<usf{.solidus = always}>(kilogram / metre / square(second)) == "kg/(m s²)");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = always}>(kilogram / metre / square(second)) ==
static_assert(unit_symbol<usf{.encoding = portable, .solidus = always}>(kilogram / metre / square(second)) ==
"kg/(m s^2)");
static_assert(unit_symbol<usf{.solidus = never}>(kilogram / metre / square(second)) == "kg m⁻¹ s⁻²");
static_assert(unit_symbol<usf{.encoding = ascii, .solidus = never}>(kilogram / metre / square(second)) ==
static_assert(unit_symbol<usf{.encoding = portable, .solidus = never}>(kilogram / metre / square(second)) ==
"kg m^-1 s^-2");
static_assert(unit_symbol<usf{.solidus = never, .separator = half_high_dot}>(kilogram / metre / square(second)) ==
"kg⋅m⁻¹⋅s⁻²");