From 483b19ea16e8846874e04e455cc3bc31a2d62f32 Mon Sep 17 00:00:00 2001 From: Chris Leishman Date: Fri, 10 Jul 2026 15:07:55 -0400 Subject: [PATCH 1/2] Generalize to_string and formatters; add format-spec rendering in hertz Replace the six per-type to_string overloads and six std::formatter specializations with one generic template each, covering every frequency instantiation. An empty format spec keeps the exact-count form ("433kHz"); any floating-point spec renders the value in hertz: std::format("{:.1f}", millihertz(1500)) == "1.5Hz". Output strings are unchanged for all existing typedefs; specs were previously compile errors, so the spec path is purely additive. --- README.md | 6 ++ include/frequency/frequency.hpp | 158 ++++++++++++++++++++------------ tests/frequency_test.cpp | 24 +++++ 3 files changed, 130 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 83f6670..2eedaa2 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,12 @@ if (kilohertz{1} == hertz{1000}) { // String conversion std::string s = to_string(kilohertz(80)); // "80kHz" + +// Formatting: a floating-point format spec renders in hertz, while the +// default renders the exact stored value. +millihertz reading{1500}; +std::string disp = std::format("{:.1f}", reading); // "1.5Hz" +std::string raw = std::format("{}", reading); // "1500mHz" (exact stored value) ``` ## Frequency Types diff --git a/include/frequency/frequency.hpp b/include/frequency/frequency.hpp index 54d433a..1b4362c 100644 --- a/include/frequency/frequency.hpp +++ b/include/frequency/frequency.hpp @@ -1099,28 +1099,75 @@ using terahertz = frequency; /** @} */ // end of FrequencyTypes group -inline std::string to_string(millihertz f) { - return std::to_string(f.count()) + "mHz"; -} - -inline std::string to_string(hertz f) { - return std::to_string(f.count()) + "Hz"; -} - -inline std::string to_string(kilohertz f) { - return std::to_string(f.count()) + "kHz"; -} - -inline std::string to_string(megahertz f) { - return std::to_string(f.count()) + "MHz"; -} - -inline std::string to_string(gigahertz f) { - return std::to_string(f.count()) + "GHz"; +/** @cond INTERNAL */ +// SI prefix for a hertz-per-count ratio; nullptr when unmapped. +template +struct _si_prefix { + static constexpr const char* value = nullptr; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "f"; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "p"; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "n"; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "ยต"; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "m"; +}; +template<> +struct _si_prefix> { + static constexpr const char* value = ""; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "k"; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "M"; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "G"; +}; +template<> +struct _si_prefix { + static constexpr const char* value = "T"; +}; +template +inline constexpr const char* _si_prefix_v = _si_prefix::value; + +// Appends a NUL-terminated string to a format output iterator. +template +constexpr OutputIt _format_append(OutputIt out, const char* s) { + for (; *s != '\0'; ++s) { + *out++ = *s; + } + return out; } +/** @endcond */ -inline std::string to_string(terahertz f) { - return std::to_string(f.count()) + "THz"; +/** + * @brief Renders a frequency as its exact count with a precision-qualified + * unit, e.g. `to_string(kilohertz(433)) == "433kHz"`. + */ +template + requires(!treat_as_inexact_v) +std::string to_string(const frequency& f) { + using precision = typename frequency::precision; + static_assert(_si_prefix_v != nullptr, "frequency: precision has no SI prefix"); + return std::to_string(f.count()) + _si_prefix_v + "Hz"; } } // namespace freq @@ -1139,46 +1186,41 @@ struct common_type, freq::frequency -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(freq::millihertz f, format_context& ctx) const { return std::format_to(ctx.out(), "{}mHz", f.count()); } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(freq::hertz f, format_context& ctx) const { return std::format_to(ctx.out(), "{}Hz", f.count()); } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(freq::kilohertz f, format_context& ctx) const { return std::format_to(ctx.out(), "{}kHz", f.count()); } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(freq::megahertz f, format_context& ctx) const { return std::format_to(ctx.out(), "{}MHz", f.count()); } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(freq::gigahertz f, format_context& ctx) const { return std::format_to(ctx.out(), "{}GHz", f.count()); } -}; +// "{}" prints the exact stored count with a precision-qualified unit +// (kilohertz(433) -> "433kHz"). A non-empty spec is applied to the value in +// hertz (as double): std::format("{:.1f}", millihertz(1500)) == "1.5Hz". +template +struct formatter> { +private: + using _precision = typename freq::frequency::precision; + static constexpr const char* _prefix = freq::_si_prefix_v<_precision>; + std::formatter _num; + bool _has_spec = false; -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } +public: + constexpr auto parse(format_parse_context& ctx) { + auto it = ctx.begin(); + if (it == ctx.end() || *it == '}') { + if (_prefix == nullptr) { + throw format_error("frequency: precision has no SI prefix; use an explicit format spec"); + } + return it; + } + _has_spec = true; + return _num.parse(ctx); + } - auto format(freq::terahertz f, format_context& ctx) const { return std::format_to(ctx.out(), "{}THz", f.count()); } + template + auto format(const freq::frequency& f, FormatContext& ctx) const { + if (_has_spec) { + double hz = static_cast(f.count()) * _precision::num / _precision::den; + auto out = _num.format(hz, ctx); + return freq::_format_append(out, "Hz"); + } + auto out = std::format_to(ctx.out(), "{}", f.count()); + out = freq::_format_append(out, _prefix); + return freq::_format_append(out, "Hz"); + } }; #endif diff --git a/tests/frequency_test.cpp b/tests/frequency_test.cpp index 02e1829..dec89d0 100644 --- a/tests/frequency_test.cpp +++ b/tests/frequency_test.cpp @@ -248,8 +248,32 @@ TEST_CASE("to_string", "[frequency][string]") { REQUIRE(to_string(kilohertz(80)) == "80kHz"); REQUIRE(to_string(megahertz(80)) == "80MHz"); REQUIRE(to_string(gigahertz(2)) == "2GHz"); + REQUIRE(to_string(terahertz(5)) == "5THz"); } +#if CONFIG_FREQUENCY_STD_FORMAT +TEST_CASE("std::format frequency", "[frequency][string]") { + // Empty spec prints the exact stored count with a precision-qualified unit. + REQUIRE(std::format("{}", millihertz(1500)) == "1500mHz"); + REQUIRE(std::format("{}", hertz(440)) == "440Hz"); + REQUIRE(std::format("{}", kilohertz(433)) == "433kHz"); + REQUIRE(std::format("{}", megahertz(868)) == "868MHz"); + REQUIRE(std::format("{}", gigahertz(2)) == "2GHz"); + REQUIRE(std::format("{}", terahertz(5)) == "5THz"); +} + +TEST_CASE("std::format with spec renders in hertz", "[frequency][string]") { + // A floating-point format spec renders the value in hertz. + REQUIRE(std::format("{:.1f}", millihertz(1500)) == "1.5Hz"); + REQUIRE(std::format("{:.2f}", millihertz(1534)) == "1.53Hz"); + REQUIRE(std::format("{:.1f}", hertz(440)) == "440.0Hz"); + REQUIRE(std::format("{:g}", kilohertz(2)) == "2000Hz"); + + // Width and alignment pass through to the numeric part. + REQUIRE(std::format("{:7.1f}", millihertz(1500)) == " 1.5Hz"); +} +#endif + TEST_CASE("frequency period", "[frequency][period]") { using namespace std::chrono; From 139cf24d360b8059bbc7d1483df84b541d942699 Mon Sep 17 00:00:00 2001 From: Chris Leishman Date: Fri, 10 Jul 2026 15:07:55 -0400 Subject: [PATCH 2/2] Bump version to 1.2.0 --- CMakeLists.txt | 2 +- docs/Doxyfile | 2 +- idf_component.yml | 2 +- vcpkg-port/vcpkg.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bddcc2..b926e28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ if(ESP_PLATFORM) endif() project(frequency - VERSION 1.1.2 + VERSION 1.2.0 DESCRIPTION "Type-safe frequency handling library modeled after std::chrono" HOMEPAGE_URL "https://github.com/cleishm/frequency-cpp" LANGUAGES CXX diff --git a/docs/Doxyfile b/docs/Doxyfile index e4f1903..cf11061 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -6,7 +6,7 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "frequency" -PROJECT_NUMBER = "1.0.0" +PROJECT_NUMBER = "1.2.0" PROJECT_BRIEF = "Type-safe frequency handling library modeled after std::chrono" PROJECT_LOGO = OUTPUT_DIRECTORY = . diff --git a/idf_component.yml b/idf_component.yml index 4d3c400..389133f 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.1.3" +version: "1.2.0" description: "Type-safe frequency handling library modeled after std::chrono" url: "https://github.com/cleishm/frequency-cpp" repository: "https://github.com/cleishm/frequency-cpp.git" diff --git a/vcpkg-port/vcpkg.json b/vcpkg-port/vcpkg.json index 022121a..319af6e 100644 --- a/vcpkg-port/vcpkg.json +++ b/vcpkg-port/vcpkg.json @@ -1,6 +1,6 @@ { "name": "cleishm-frequency-cpp", - "version": "1.0.0", + "version": "1.2.0", "description": "Type-safe frequency handling library modeled after std::chrono", "homepage": "https://github.com/cleishm/frequency-cpp", "license": "MIT",