Constexpr `fmt::formatter::parse` for C++20 with `from_chars` (#3944)

This commit is contained in:
Antoine Prouvost 2025-05-20 13:40:42 +02:00 committed by GitHub
parent aaae25ff04
commit e807e690ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 176 additions and 39 deletions

View File

@ -15,6 +15,7 @@
#include <fmt/format.h>
#include "mamba/specs/error.hpp"
#include "mamba/util/charconv.hpp"
namespace mamba::specs
{
@ -262,9 +263,43 @@ struct fmt::formatter<mamba::specs::Version>
std::optional<std::size_t> m_level;
FormatType m_type = FormatType::Normal;
auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator
{
const auto end = ctx.end();
const auto start = ctx.begin();
auto format(const ::mamba::specs::Version v, format_context& ctx) const -> decltype(ctx.out());
// Make sure that range is not empty
if (start == end || *start == '}')
{
return start;
}
// Check for restricted number of segments at beginning
std::size_t val = 0;
auto [ptr, ec] = mamba::util::constexpr_from_chars(start, end, val);
if (ec == std::errc())
{
m_level = val;
}
// Check for end of format spec
if (ptr == end || *ptr == '}')
{
return ptr;
}
// Check the custom format type
if (*ptr == 'g')
{
m_type = FormatType::Glob;
++ptr;
}
return ptr;
}
auto format(const ::mamba::specs::Version v, format_context& ctx) const
-> format_context::iterator;
};
#endif

View File

@ -0,0 +1,63 @@
// Copyright (c) 2025, Cppreference.com
//
// Distributed under the terms of the Copyright/CC-BY-SA License.
//
// The full license can be found at the address
// https://en.cppreference.com/w/Cppreference:Copyright/CC-BY-SA
/**
* Backport of C++23 ``std::from_chars`` function as constexpr.
*/
#ifndef MAMBA_UTIL_CHARCONV_HPP
#define MAMBA_UTIL_CHARCONV_HPP
#include <charconv>
#include <limits>
#include <type_traits>
#include "mamba/util/deprecation.hpp"
namespace mamba::util
{
template <typename Int>
MAMBA_DEPRECATED_CXX23 constexpr auto
constexpr_from_chars(const char* first, const char* last, Int& value) -> std::from_chars_result
{
static_assert(
std::is_integral_v<Int> && std::is_unsigned_v<Int>,
"Only unsigned integers supported"
);
constexpr auto is_digit = [](char c) -> bool { return c >= '0' && c <= '9'; };
if (first == last)
{
return { first, std::errc::invalid_argument };
}
value = 0;
auto it = first;
while (it != last && is_digit(*it))
{
Int digit = static_cast<Int>(static_cast<unsigned char>(*it) - '0');
if (value > (std::numeric_limits<Int>::max() - digit) / 10)
{
return { it, std::errc::result_out_of_range };
}
value = value * 10 + digit;
++it;
}
if (it == first)
{
return { first, std::errc::invalid_argument };
}
return { it, std::errc{} };
}
}
#endif

View File

@ -864,45 +864,9 @@ namespace mamba::specs
}
}
auto
fmt::formatter<mamba::specs::Version>::parse(format_parse_context& ctx) -> decltype(ctx.begin())
{
const auto end = ctx.end();
const auto start = ctx.begin();
// Make sure that range is not empty
if (start == end || *start == '}')
{
return start;
}
// Check for restricted number of segments at beginning
std::size_t val = 0;
auto [ptr, ec] = std::from_chars(start, end, val);
if (ec == std::errc())
{
m_level = val;
}
// Check for end of format spec
if (ptr == end || *ptr == '}')
{
return ptr;
}
// Check the custom format type
if (*ptr == 'g')
{
m_type = FormatType::Glob;
++ptr;
}
return ptr;
}
auto
fmt::formatter<mamba::specs::Version>::format(const ::mamba::specs::Version v, format_context& ctx) const
-> decltype(ctx.out())
-> format_context::iterator
{
auto out = ctx.out();
if (v.epoch() != 0)

View File

@ -19,6 +19,7 @@ set(
# Utility library
src/util/test_cast.cpp
src/util/test_compare.cpp
src/util/test_charconv.cpp
src/util/test_cryptography.cpp
src/util/test_encoding.cpp
src/util/test_environment.cpp

View File

@ -0,0 +1,74 @@
// Copyright (c) 2025, Cppreference.com
//
// Distributed under the terms of the Copyright/CC-BY-SA License.
//
// The full license can be found at the address
// https://en.cppreference.com/w/Cppreference:Copyright/CC-BY-SA
#include <catch2/catch_all.hpp>
#include "mamba/util/charconv.hpp"
using namespace mamba::util;
namespace
{
TEST_CASE("constexpr_from_chars works for valid input", "[mamba::util]")
{
SECTION("Basic parsing")
{
constexpr const char* input = "12345";
unsigned value = 0;
auto res = constexpr_from_chars(input, input + 5u, value);
REQUIRE(res.ec == std::errc{});
REQUIRE(res.ptr == input + 5);
REQUIRE(value == 12345u);
}
SECTION("Empty input")
{
constexpr const char* input = "";
std::size_t value = 0;
auto res = constexpr_from_chars(input, input, value);
REQUIRE(res.ec == std::errc::invalid_argument);
REQUIRE(res.ptr == input);
}
SECTION("Non-digit character")
{
constexpr const char* input = "123a";
unsigned value = 0;
auto res = constexpr_from_chars(input, input + 4, value);
REQUIRE(res.ec == std::errc{});
REQUIRE(res.ptr == input + 3);
REQUIRE(value == 123u);
}
SECTION("No digits at all")
{
constexpr const char* input = "abc";
unsigned value = 0;
auto res = constexpr_from_chars(input, input + 3, value);
REQUIRE(res.ec == std::errc::invalid_argument);
REQUIRE(res.ptr == input);
}
SECTION("Overflow")
{
constexpr const char* input = "99999999999999999999";
std::size_t value = 0;
auto res = constexpr_from_chars(input, input + 20, value);
REQUIRE(res.ec == std::errc::result_out_of_range);
}
SECTION("Leading zeroes")
{
constexpr const char* input = "00042";
unsigned value = 0;
auto res = constexpr_from_chars(input, input + 5, value);
REQUIRE(res.ec == std::errc{});
REQUIRE(res.ptr == input + 5);
REQUIRE(value == 42u);
}
}
}