diff --git a/CMakeLists.txt b/CMakeLists.txt index c2ff19a..3ba4140 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ if(ESP_PLATFORM) endif() project(electro - VERSION 0.2.0 + VERSION 0.3.0 DESCRIPTION "Type-safe electrical units library modeled after std::chrono" HOMEPAGE_URL "https://github.com/cleishm/electro-cpp" LANGUAGES CXX diff --git a/README.md b/README.md index 4ec3e68..8839076 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,14 @@ if (kilovolts{1} == volts{1000}) { // String conversion std::string s = to_string(kiloohms(10)); // "10kΩ" + +// Formatting: a floating-point format spec renders in display units +// (base unit, or the conventional unit for Ah/Wh precisions), while the +// default renders the exact stored value. +millivolts reading{3300}; +std::string disp = std::format("{:.1f}", reading); // "3.3V" +std::string raw = std::format("{}", reading); // "3300mV" (exact stored value) +std::format("{:.1f}", milliampere_hours(200)); // "0.2Ah" ``` ### Cross-Quantity Arithmetic diff --git a/docs/Doxyfile b/docs/Doxyfile index 29328de..8d27dd9 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -6,7 +6,7 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "electro" -PROJECT_NUMBER = "0.1.0" +PROJECT_NUMBER = "0.3.0" PROJECT_BRIEF = "Type-safe electrical units library modeled after std::chrono" PROJECT_LOGO = OUTPUT_DIRECTORY = . diff --git a/idf_component.yml b/idf_component.yml index 040ae52..2d53c38 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,4 +1,4 @@ -version: "0.2.0" +version: "0.3.0" description: "Type-safe electrical units library modeled after std::chrono" url: "https://github.com/cleishm/electro-cpp" repository: "https://github.com/cleishm/electro-cpp.git" diff --git a/include/electro/decibel.hpp b/include/electro/decibel.hpp index 779d37e..6f54124 100644 --- a/include/electro/decibel.hpp +++ b/include/electro/decibel.hpp @@ -681,24 +681,65 @@ struct common_type, electro::level requires electro::_is_decimal_precision struct formatter, char> { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } +private: + std::formatter _num; + bool _has_spec = false; + +public: + constexpr auto parse(format_parse_context& ctx) { + auto it = ctx.begin(); + if (it == ctx.end() || *it == '}') { + return it; + } + _has_spec = true; + return _num.parse(ctx); + } - auto format(const electro::quantity& g, format_context& ctx) const { + template + auto format(const electro::quantity& g, FormatContext& ctx) const { + if (_has_spec) { + double value = static_cast(g.count()) * Precision::num / Precision::den; + auto out = _num.format(value, ctx); + return electro::_format_append(out, electro::decibel_unit::symbol); + } return std::format_to( ctx.out(), "{}{}", electro::_format_db(g.count()), electro::decibel_unit::symbol ); } }; +// See the gain formatter above; the level's reference symbol is appended +// ("13.01dBm", "{:.1f}" -> "13.0dBm"). template requires electro::_is_decimal_precision struct formatter, char> { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } +private: + std::formatter _num; + bool _has_spec = false; + +public: + constexpr auto parse(format_parse_context& ctx) { + auto it = ctx.begin(); + if (it == ctx.end() || *it == '}') { + return it; + } + _has_spec = true; + return _num.parse(ctx); + } - auto format(const electro::level& l, format_context& ctx) const { + template + auto format(const electro::level& l, FormatContext& ctx) const { + if (_has_spec) { + double value = static_cast(l.count()) * Precision::num / Precision::den; + auto out = _num.format(value, ctx); + return electro::_format_append(out, Reference::symbol); + } return std::format_to( ctx.out(), "{}{}", electro::_format_db(l.count()), Reference::symbol ); diff --git a/include/electro/electro.hpp b/include/electro/electro.hpp index 99d391d..97acfe0 100644 --- a/include/electro/electro.hpp +++ b/include/electro/electro.hpp @@ -1199,6 +1199,47 @@ struct quantity_suffix> { template concept _has_suffix = requires { quantity_suffix::value(); }; + +// Display family for a (unit, precision) pair when a format spec is given: +// the value is rendered in `scale`-sized units labeled `suffix`. SI-prefixed +// precisions render in the base unit ("3.3V"); conventional non-decade +// precisions render in their conventional unit ("0.2Ah", never "720.0C"). +template +struct _display_unit { + using scale = std::ratio<1>; + static constexpr const char* suffix = Unit::symbol; +}; + +struct _ampere_hour_display_unit { + using scale = std::ratio<3600>; + static constexpr const char* suffix = "Ah"; +}; + +struct _watt_hour_display_unit { + using scale = std::ratio<3600>; + static constexpr const char* suffix = "Wh"; +}; + +template<> +struct _display_unit> : _ampere_hour_display_unit {}; + +template<> +struct _display_unit> : _ampere_hour_display_unit {}; + +template<> +struct _display_unit> : _watt_hour_display_unit {}; + +template<> +struct _display_unit> : _watt_hour_display_unit {}; + +// 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 */ /** @@ -1235,12 +1276,38 @@ struct common_type, electro::quantity< }; #if CONFIG_ELECTRO_STD_FORMAT +// "{}" prints the exact stored count with a precision-qualified unit +// (millivolts(3300) -> "3300mV"). A non-empty spec is applied to the value in +// display units (as double): std::format("{:.1f}", millivolts(3300)) == "3.3V". +// SI-prefixed precisions display in the base unit; conventional non-decade +// precisions display in their conventional unit ("0.2Ah"). template requires electro::_has_suffix struct formatter, char> { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } +private: + using _display = electro::_display_unit; + // display units per count + using _upc = typename ratio_divide::type; + std::formatter _num; + bool _has_spec = false; + +public: + constexpr auto parse(format_parse_context& ctx) { + auto it = ctx.begin(); + if (it == ctx.end() || *it == '}') { + return it; + } + _has_spec = true; + return _num.parse(ctx); + } - auto format(const electro::quantity& q, format_context& ctx) const { + template + auto format(const electro::quantity& q, FormatContext& ctx) const { + if (_has_spec) { + double value = static_cast(q.count()) * _upc::num / _upc::den; + auto out = _num.format(value, ctx); + return electro::_format_append(out, _display::suffix); + } return std::format_to( ctx.out(), "{}{}", q.count(), electro::quantity_suffix::value() ); diff --git a/tests/decibel_test.cpp b/tests/decibel_test.cpp index 598caee..d763fdc 100644 --- a/tests/decibel_test.cpp +++ b/tests/decibel_test.cpp @@ -291,6 +291,17 @@ TEST_CASE("std::format support for levels and gains", "[format]") { REQUIRE(std::format("{}", 1050_cdB) == "10.50dB"); REQUIRE(std::format("rx {} with {} margin", -66_dBm, 34_dB) == "rx -66dBm with 34dB margin"); } + +TEST_CASE("std::format with spec renders gains and levels in decibels", "[format]") { + // A floating-point format spec renders the value in decibels. + REQUIRE(std::format("{:.1f}", 1050_cdB) == "10.5dB"); + REQUIRE(std::format("{:.0f}", 20_dB) == "20dB"); + REQUIRE(std::format("{:.1f}", 1301_cdBm) == "13.0dBm"); + REQUIRE(std::format("{:.2f}", -73_dBm) == "-73.00dBm"); + + // Width and alignment pass through to the numeric part. + REQUIRE(std::format("{:6.1f}", 1050_cdB) == " 10.5dB"); +} #endif TEST_CASE("link budget example", "[integration]") { diff --git a/tests/electro_test.cpp b/tests/electro_test.cpp index 338851f..f01b0e0 100644 --- a/tests/electro_test.cpp +++ b/tests/electro_test.cpp @@ -345,6 +345,24 @@ TEST_CASE("std::format support", "[format]") { std::format("battery: {} at {}", milliampere_hours(2000), millivolts(3700)) == "battery: 2000mAh at 3700mV" ); } + +TEST_CASE("std::format with spec renders in display units", "[format]") { + // A floating-point format spec renders the value in the base unit. + REQUIRE(std::format("{:.1f}", millivolts(3300)) == "3.3V"); + REQUIRE(std::format("{:.2f}", millivolts(3312)) == "3.31V"); + REQUIRE(std::format("{:.1f}", kilovolts(5)) == "5000.0V"); + REQUIRE(std::format("{:g}", microamperes(1500)) == "0.0015A"); + REQUIRE(std::format("{:.1f}", milliohms(4700)) == "4.7Ω"); + + // Width and alignment pass through to the numeric part. + REQUIRE(std::format("{:7.1f}", millivolts(3300)) == " 3.3V"); + + // Conventional non-decade precisions render in their conventional unit. + REQUIRE(std::format("{:.1f}", milliampere_hours(200)) == "0.2Ah"); + REQUIRE(std::format("{:.1f}", ampere_hours(2)) == "2.0Ah"); + REQUIRE(std::format("{:.1f}", watt_hours(500)) == "500.0Wh"); + REQUIRE(std::format("{:.0f}", kilowatt_hours(2)) == "2000Wh"); +} #endif TEST_CASE("battery runtime example", "[integration]") { diff --git a/vcpkg-port/vcpkg.json b/vcpkg-port/vcpkg.json index dd8e215..2891607 100644 --- a/vcpkg-port/vcpkg.json +++ b/vcpkg-port/vcpkg.json @@ -1,6 +1,6 @@ { "name": "cleishm-electro-cpp", - "version": "0.2.0", + "version": "0.3.0", "description": "Type-safe electrical units library modeled after std::chrono", "homepage": "https://github.com/cleishm/electro-cpp", "license": "MIT",