Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 = .
Expand Down
2 changes: 1 addition & 1 deletion idf_component.yml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
49 changes: 45 additions & 4 deletions include/electro/decibel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -681,24 +681,65 @@ struct common_type<electro::level<Reference, Rep1, Precision1>, electro::level<R
};

#if CONFIG_ELECTRO_STD_FORMAT
// "{}" renders the exact value as a fixed-point decimal ("10.50dB"). A
// non-empty spec is applied to the value in decibels (as double):
// std::format("{:.1f}", 1050_cdB) == "10.5dB".
template<typename Rep, typename Precision>
requires electro::_is_decimal_precision<typename Precision::type>
struct formatter<electro::quantity<electro::decibel_unit, Rep, Precision>, char> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
private:
std::formatter<double> _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<electro::decibel_unit, Rep, Precision>& g, format_context& ctx) const {
template<typename FormatContext>
auto format(const electro::quantity<electro::decibel_unit, Rep, Precision>& g, FormatContext& ctx) const {
if (_has_spec) {
double value = static_cast<double>(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<typename Precision::type>(g.count()), electro::decibel_unit::symbol
);
}
};

// See the gain formatter above; the level's reference symbol is appended
// ("13.01dBm", "{:.1f}" -> "13.0dBm").
template<typename Reference, typename Rep, typename Precision>
requires electro::_is_decimal_precision<typename Precision::type>
struct formatter<electro::level<Reference, Rep, Precision>, char> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
private:
std::formatter<double> _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<Reference, Rep, Precision>& l, format_context& ctx) const {
template<typename FormatContext>
auto format(const electro::level<Reference, Rep, Precision>& l, FormatContext& ctx) const {
if (_has_spec) {
double value = static_cast<double>(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<typename Precision::type>(l.count()), Reference::symbol
);
Expand Down
71 changes: 69 additions & 2 deletions include/electro/electro.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,47 @@ struct quantity_suffix<joule_unit, std::ratio<3600000>> {

template<typename Unit, typename Precision>
concept _has_suffix = requires { quantity_suffix<Unit, Precision>::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<typename Unit, typename Precision>
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<coulomb_unit, std::ratio<18, 5>> : _ampere_hour_display_unit {};

template<>
struct _display_unit<coulomb_unit, std::ratio<3600>> : _ampere_hour_display_unit {};

template<>
struct _display_unit<joule_unit, std::ratio<3600>> : _watt_hour_display_unit {};

template<>
struct _display_unit<joule_unit, std::ratio<3600000>> : _watt_hour_display_unit {};

// Appends a NUL-terminated string to a format output iterator.
template<typename OutputIt>
constexpr OutputIt _format_append(OutputIt out, const char* s) {
for (; *s != '\0'; ++s) {
*out++ = *s;
}
return out;
}
/** @endcond */

/**
Expand Down Expand Up @@ -1235,12 +1276,38 @@ struct common_type<electro::quantity<Unit, Rep1, Precision1>, 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<typename Unit, typename Rep, typename Precision>
requires electro::_has_suffix<Unit, typename Precision::type>
struct formatter<electro::quantity<Unit, Rep, Precision>, char> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
private:
using _display = electro::_display_unit<Unit, typename Precision::type>;
// display units per count
using _upc = typename ratio_divide<typename Precision::type, typename _display::scale>::type;
std::formatter<double> _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<Unit, Rep, Precision>& q, format_context& ctx) const {
template<typename FormatContext>
auto format(const electro::quantity<Unit, Rep, Precision>& q, FormatContext& ctx) const {
if (_has_spec) {
double value = static_cast<double>(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<Unit, typename Precision::type>::value()
);
Expand Down
11 changes: 11 additions & 0 deletions tests/decibel_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]") {
Expand Down
18 changes: 18 additions & 0 deletions tests/electro_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]") {
Expand Down
2 changes: 1 addition & 1 deletion vcpkg-port/vcpkg.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading