diff --git a/include/etl/format.h b/include/etl/format.h index 75ac5952..30152dc9 100644 --- a/include/etl/format.h +++ b/include/etl/format.h @@ -91,44 +91,70 @@ namespace etl POINTER // const void* }; - // Map a type to its category. Decays and removes cv-qualifiers. + // Trait mapping a type (with references and cv-qualifiers removed) to its type_category. + // + // NOTE: signed char/unsigned char (typically int8_t/uint8_t) are NOT + // categorised as CHAR here - only plain 'char' gets character semantics. + // signed/unsigned char are formatted as integers, matching basic_format_arg + // storage and std::format's formatter/. This is + // enforced structurally: the INTEGER partial specialisation is constrained + // with is_integral && !is_same && !is_same, so + // signed/unsigned char (is_integral == true) fall into INTEGER while the + // dedicated bool/char full specialisations take priority for those exact types. + template + struct type_category_trait + { + static constexpr type_category value = type_category::NONE; // unknown type: custom formatter, be permissive + }; + + template <> + struct type_category_trait + { + static constexpr type_category value = type_category::BOOLEAN; + }; + + template <> + struct type_category_trait + { + static constexpr type_category value = type_category::CHAR; + }; + + template + struct type_category_trait< + T, typename etl::enable_if::value && !etl::is_same::value && !etl::is_same::value>::type> + { + static constexpr type_category value = type_category::INTEGER; + }; + + template + struct type_category_trait::value>::type> + { + static constexpr type_category value = type_category::FLOAT; + }; + + template + struct type_category_trait::value || etl::is_same::value + || etl::is_same::value || etl::is_base_of::value>::type> + { + static constexpr type_category value = type_category::STRING; + }; + + template + struct type_category_trait< + T, typename etl::enable_if::value && !etl::is_same::value && !etl::is_same::value>::type> + { + static constexpr type_category value = type_category::POINTER; + }; + + // Map a type to its category. Removes references and cv-qualifiers, then defers + // entirely to the type_category_trait specialisations above. template constexpr type_category get_type_category() { using U = typename etl::remove_cv::type>::type; - // Order matters: bool before integral, char before integral - if (etl::is_same::value) - return type_category::BOOLEAN; - if (etl::is_same::value) - return type_category::CHAR; - if (etl::is_same::value) - return type_category::CHAR; - if (etl::is_same::value) - return type_category::CHAR; - if (etl::is_integral::value) - return type_category::INTEGER; - if (etl::is_same::value) - return type_category::FLOAT; - if (etl::is_same::value) - return type_category::FLOAT; - if (etl::is_same::value) - return type_category::FLOAT; - if (etl::is_same::value) - return type_category::STRING; - if (etl::is_same::value) - return type_category::STRING; - if (etl::is_same::value) - return type_category::STRING; - if (etl::is_base_of::value) - return type_category::STRING; - if (etl::is_pointer::value) - return type_category::POINTER; - if (etl::is_same::value) - return type_category::POINTER; - if (etl::is_same::value) - return type_category::POINTER; - return type_category::NONE; // unknown type: custom formatter, be permissive + return type_category_trait::value; } // Check if a format type character is valid for a given type category. @@ -727,13 +753,16 @@ namespace etl { } + // int8_t / uint8_t are typically defined as signed char / unsigned char, but + // (unlike plain char) they are formatted as integers by default, matching + // std::format's formatter/formatter behaviour. basic_format_arg(const signed char v) - : data(static_cast(v)) + : data(static_cast(v)) { } basic_format_arg(const unsigned char v) - : data(static_cast(v)) + : data(static_cast(v)) { } diff --git a/test/test_format.cpp b/test/test_format.cpp index 6fd2cbf4..480345b5 100644 --- a/test/test_format.cpp +++ b/test/test_format.cpp @@ -218,10 +218,9 @@ namespace { etl::string<100> s; - // mapped to unsigned char - // CHECK_EQUAL("34", test_format(s, "{}", static_cast(34))); - // mapped to signed char - // CHECK_EQUAL("-14", test_format(s, "{}", static_cast(-14))); + // int8_t/uint8_t are formatted as integers (issue #1549), not as characters + CHECK_EQUAL("34", test_format(s, "{}", static_cast(34))); + CHECK_EQUAL("-14", test_format(s, "{}", static_cast(-14))); CHECK_EQUAL("6534", test_format(s, "{}", static_cast(6534))); CHECK_EQUAL("-9414", test_format(s, "{}", static_cast(-9414))); CHECK_EQUAL("236534", test_format(s, "{}", static_cast(236534))); @@ -619,45 +618,35 @@ namespace //************************************************************************* TEST(test_format_signed_char) { + // signed char (typically int8_t) is formatted as an integer by default + // (issue #1549), matching std::format. Only {:c} converts it to a character. + // '?' (debug-escape) is NOT a valid presentation type for signed char. etl::string<100> s; - CHECK_EQUAL("a s b", test_format(s, "a {} b", static_cast('s'))); + CHECK_EQUAL("a 115 b", test_format(s, "a {} b", static_cast('s'))); CHECK_EQUAL("a s b", test_format(s, "a {:c} b", static_cast('s'))); - CHECK_EQUAL("a 's' b", test_format(s, "a {:?} b", static_cast('s'))); - CHECK_EQUAL("a \t b", test_format(s, "a {} b", static_cast('\t'))); - CHECK_EQUAL("a '\\t' b", test_format(s, "a {:?} b", static_cast('\t'))); - CHECK_EQUAL("a '\\n' b", test_format(s, "a {:?} b", static_cast('\n'))); - CHECK_EQUAL("a '\\r' b", test_format(s, "a {:?} b", static_cast('\r'))); - CHECK_EQUAL("a '\\\"' b", test_format(s, "a {:?} b", static_cast('"'))); - CHECK_EQUAL("a '\\'' b", test_format(s, "a {:?} b", static_cast('\''))); - CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast('\\'))); - CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast('\\'))); CHECK_EQUAL("a 97 b", test_format(s, "a {:d} b", static_cast('a'))); CHECK_EQUAL("a 61 b", test_format(s, "a {:X} b", static_cast('a'))); CHECK_EQUAL("a 61 b", test_format(s, "a {:x} b", static_cast('a'))); CHECK_EQUAL("a 0x61 b", test_format(s, "a {:#x} b", static_cast('a'))); + CHECK_EQUAL("a -12 b", test_format(s, "a {} b", static_cast(-12))); } //************************************************************************* TEST(test_format_unsigned_char) { + // unsigned char (typically uint8_t) is formatted as an integer by default + // (issue #1549), matching std::format. Only {:c} converts it to a character. + // '?' (debug-escape) is NOT a valid presentation type for unsigned char. etl::string<100> s; - CHECK_EQUAL("a s b", test_format(s, "a {} b", static_cast('s'))); + CHECK_EQUAL("a 115 b", test_format(s, "a {} b", static_cast('s'))); CHECK_EQUAL("a s b", test_format(s, "a {:c} b", static_cast('s'))); - CHECK_EQUAL("a 's' b", test_format(s, "a {:?} b", static_cast('s'))); - CHECK_EQUAL("a \t b", test_format(s, "a {} b", static_cast('\t'))); - CHECK_EQUAL("a '\\t' b", test_format(s, "a {:?} b", static_cast('\t'))); - CHECK_EQUAL("a '\\n' b", test_format(s, "a {:?} b", static_cast('\n'))); - CHECK_EQUAL("a '\\r' b", test_format(s, "a {:?} b", static_cast('\r'))); - CHECK_EQUAL("a '\\\"' b", test_format(s, "a {:?} b", static_cast('"'))); - CHECK_EQUAL("a '\\'' b", test_format(s, "a {:?} b", static_cast('\''))); - CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast('\\'))); - CHECK_EQUAL("a '\\\\' b", test_format(s, "a {:?} b", static_cast('\\'))); CHECK_EQUAL("a 97 b", test_format(s, "a {:d} b", static_cast('a'))); CHECK_EQUAL("a 61 b", test_format(s, "a {:X} b", static_cast('a'))); CHECK_EQUAL("a 61 b", test_format(s, "a {:x} b", static_cast('a'))); CHECK_EQUAL("a 0x61 b", test_format(s, "a {:#x} b", static_cast('a'))); + CHECK_EQUAL("a 65 b", test_format(s, "a {} b", static_cast(65))); } //*************************************************************************