diff --git a/CMakeLists.txt b/CMakeLists.txt index 23786cb..ff93226 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ if(ESP_PLATFORM) endif() project(thermo - VERSION 1.3.0 + VERSION 2.0.0 DESCRIPTION "Type-safe temperature handling library modeled after std::chrono" HOMEPAGE_URL "https://github.com/cleishm/thermo-cpp" LANGUAGES CXX diff --git a/README.md b/README.md index 70d7e1d..6fadb79 100644 --- a/README.md +++ b/README.md @@ -92,11 +92,11 @@ if (millicelsius{20000} == celsius{20}) { // String conversion std::string s = to_string(room_temp); // "20°C" -// Real-valued display: cast an exact reading to a floating-point degree type, -// whose formatter reads in degrees (rather than raw ticks). +// Formatting: a floating-point format spec renders in scale degrees, +// while the default renders the exact stored value. millicelsius reading{22500}; -celsius_real shown = reading; // 22.5 (implicit) -std::string disp = std::format("{:.1f}", shown); // "22.5°C" +std::string disp = std::format("{:.1f}", reading); // "22.5°C" +std::string raw = std::format("{}", reading); // "22500m°C" (exact stored value) ``` ## Temperature Types @@ -114,16 +114,11 @@ std::string disp = std::format("{:.1f}", shown); // "22.5°C" | `fahrenheit` | Fahrenheit | 1°F | | `decifahrenheit` | Fahrenheit | 0.1°F | | `millifahrenheit` | Fahrenheit | 0.001°F | -| `celsius_real` | Celsius | real-valued (`double`) | -| `kelvin_real` | Kelvin | real-valued (`double`) | -| `fahrenheit_real` | Fahrenheit | real-valued (`double`) | - -The `*_real` types hold a floating-point degree value, so `count()` reads -directly in scale degrees (e.g. `22.5`) and `std::format` renders them as -`22.5°C`, honoring the standard float format spec (`{:.1f}` → `22.5°C`). Obtain -one from an exact reading with a cast — see [Usage](#usage). The integer types -format as their exact stored value with a precision-qualified unit (e.g. -`millicelsius{22500}` → `22500m°C`). + +With `std::format`, `{}` renders a temperature as its exact stored value with a +precision-qualified unit (e.g. `millicelsius{22500}` → `22500m°C`), while any +floating-point format spec renders the value in scale degrees — no cast needed +(`{:.1f}` of `millicelsius{22534}` → `22.5°C`). ### Temperature Deltas diff --git a/docs/Doxyfile b/docs/Doxyfile index 649368e..4ed8ac3 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -6,7 +6,7 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "thermo" -PROJECT_NUMBER = "1.3.0" +PROJECT_NUMBER = "2.0.0" PROJECT_BRIEF = "Type-safe temperature handling library modeled after std::chrono" PROJECT_LOGO = OUTPUT_DIRECTORY = . diff --git a/idf_component.yml b/idf_component.yml index 1f9ecde..b89fd84 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.3.0" +version: "2.0.0" description: "Type-safe temperature handling library modeled after std::chrono" url: "https://github.com/cleishm/thermo-cpp" repository: "https://github.com/cleishm/thermo-cpp.git" diff --git a/include/thermo/thermo.hpp b/include/thermo/thermo.hpp index c87b8fa..59fd68d 100644 --- a/include/thermo/thermo.hpp +++ b/include/thermo/thermo.hpp @@ -476,30 +476,76 @@ using delta_fahrenheit = delta>; /** @brief Delta with 0.1°F precision. */ using delta_decifahrenheit = delta>; /** @brief Delta with 0.001°F precision. */ -using delta_millifahrenheit = delta>; +using delta_millifahrenheit = delta>; -inline std::string to_string(delta d) { - return std::to_string(d.count()) + "Δ°C"; -} - -inline std::string to_string(delta d) { - return std::to_string(d.count()) + "Δd°C"; -} - -inline std::string to_string(delta d) { - return std::to_string(d.count()) + "Δm°C"; -} - -inline std::string to_string(delta_fahrenheit d) { - return std::to_string(d.count()) + "Δ°F"; -} +/** @cond INTERNAL */ +// SI prefix for a degrees-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 = ""; +}; +template<> +struct _si_prefix> { + static constexpr const char* value = "d"; +}; +template<> +struct _si_prefix> { + static constexpr const char* value = "m"; +}; +template +inline constexpr const char* _si_prefix_v = _si_prefix::value; + +// Display family (degree size + unit suffix) for a delta precision, keyed on +// NORMALIZED ratios. Undefined primary template: unmapped precisions have no +// determinable unit and are not formattable. +template +struct _delta_unit; +struct _celsius_delta_unit { + using degree = std::ratio<1>; + static constexpr const char* suffix = "°C"; +}; +struct _fahrenheit_delta_unit { + using degree = std::ratio<5, 9>; + static constexpr const char* suffix = "°F"; +}; +template<> +struct _delta_unit> : _celsius_delta_unit {}; +template<> +struct _delta_unit> : _celsius_delta_unit {}; +template<> +struct _delta_unit> : _celsius_delta_unit {}; +template<> +struct _delta_unit> : _fahrenheit_delta_unit {}; +template<> +struct _delta_unit> : _fahrenheit_delta_unit {}; // 5/90 normalized +template<> +struct _delta_unit> : _fahrenheit_delta_unit {}; // 5/9000 normalized -inline std::string to_string(delta_decifahrenheit d) { - return std::to_string(d.count()) + "Δd°F"; +// 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(delta_millifahrenheit d) { - return std::to_string(d.count()) + "Δm°F"; +/** + * @brief Renders a delta as its exact count with a precision-qualified unit, + * e.g. `to_string(delta_millicelsius(1500)) == "1500Δm°C"`. + */ +template + requires(!treat_as_inexact_v) +std::string to_string(const delta& d) { + using unit = _delta_unit::precision>; + using dpc = std::ratio_divide::precision, typename unit::degree>; + static_assert(_si_prefix_v != nullptr, "thermo: precision has no SI prefix"); + return std::to_string(d.count()) + "Δ" + _si_prefix_v + unit::suffix; } // ============================================================================ @@ -514,6 +560,7 @@ inline std::string to_string(delta_millifahrenheit d) { */ struct celsius_scale { using offset = std::ratio<27315, 100>; + using degree = std::ratio<1>; static constexpr const char* suffix = "°C"; }; @@ -524,6 +571,7 @@ struct celsius_scale { */ struct kelvin_scale { using offset = std::ratio<0>; + using degree = std::ratio<1>; static constexpr const char* suffix = "K"; }; @@ -534,6 +582,7 @@ struct kelvin_scale { */ struct fahrenheit_scale { using offset = std::ratio<45967, 180>; + using degree = std::ratio<5, 9>; static constexpr const char* suffix = "°F"; }; /** @endcond */ @@ -825,57 +874,18 @@ using fahrenheit = temperature /** @brief Fahrenheit with 0.1 degree precision. */ using decifahrenheit = temperature>>; /** @brief Fahrenheit with 0.001 degree precision. */ -using millifahrenheit = temperature>>; +using millifahrenheit = temperature>>; /** - * @brief Real-valued (double precision) Celsius. - * - * Unlike the integer-precision typedefs above, a real-valued temperature holds - * a floating-point degree value, so `count()` reads directly in scale degrees - * (e.g. 22.5) and formats as `22.5°C`. Obtain one from an exact reading with a - * cast — `temperature_cast(millicelsius(22500))` — for display or - * further floating-point computation. + * @brief Renders a temperature as its exact count with a precision-qualified + * unit, e.g. `to_string(millicelsius(22500)) == "22500m°C"`. */ -using celsius_real = temperature>; -/** @brief Real-valued (double precision) Kelvin. @see celsius_real */ -using kelvin_real = temperature>; -/** @brief Real-valued (double precision) Fahrenheit. @see celsius_real */ -using fahrenheit_real = temperature>>; - -inline std::string to_string(celsius t) { - return std::to_string(t.count()) + "°C"; -} - -inline std::string to_string(decicelsius t) { - return std::to_string(t.count()) + "d°C"; -} - -inline std::string to_string(millicelsius t) { - return std::to_string(t.count()) + "m°C"; -} - -inline std::string to_string(kelvin t) { - return std::to_string(t.count()) + "K"; -} - -inline std::string to_string(decikelvin t) { - return std::to_string(t.count()) + "dK"; -} - -inline std::string to_string(millikelvin t) { - return std::to_string(t.count()) + "mK"; -} - -inline std::string to_string(fahrenheit t) { - return std::to_string(t.count()) + "°F"; -} - -inline std::string to_string(decifahrenheit t) { - return std::to_string(t.count()) + "d°F"; -} - -inline std::string to_string(millifahrenheit t) { - return std::to_string(t.count()) + "m°F"; +template + requires(!treat_as_inexact_v) +std::string to_string(const temperature>& t) { + using dpc = std::ratio_divide::precision, typename Scale::degree>; + static_assert(_si_prefix_v != nullptr, "thermo: precision has no SI prefix"); + return std::to_string(t.count()) + _si_prefix_v + Scale::suffix; } } // namespace thermo @@ -899,185 +909,84 @@ struct common_type, thermo::temperature -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::celsius t, format_context& ctx) const { return std::format_to(ctx.out(), "{}°C", t.count()); } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::decicelsius t, format_context& ctx) const { - return std::format_to(ctx.out(), "{}d°C", t.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::millicelsius t, format_context& ctx) const { - return std::format_to(ctx.out(), "{}m°C", t.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::kelvin t, format_context& ctx) const { return std::format_to(ctx.out(), "{}K", t.count()); } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::decikelvin t, format_context& ctx) const { - return std::format_to(ctx.out(), "{}dK", t.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::millikelvin t, format_context& ctx) const { - return std::format_to(ctx.out(), "{}mK", t.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::fahrenheit t, format_context& ctx) const { - return std::format_to(ctx.out(), "{}°F", t.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::decifahrenheit t, format_context& ctx) const { - return std::format_to(ctx.out(), "{}d°F", t.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::millifahrenheit t, format_context& ctx) const { - return std::format_to(ctx.out(), "{}m°F", t.count()); - } -}; - -// Real-valued temperatures format their degree value directly (count() is the -// value in scale degrees) and honor the standard floating-point format spec, so -// std::format("{:.1f}", celsius_real(22.53)) == "22.5°C". -template<> -struct formatter { +// "{}" prints the exact stored count with a precision-qualified unit +// (millicelsius(22500) -> "22500m°C"). A non-empty spec is applied to the +// value in scale degrees (as double), honoring the standard floating-point +// format spec: std::format("{:.1f}", millicelsius(22534)) == "22.5°C". +template +struct formatter>> { +private: + using _delta_t = thermo::delta; + using _dpc = typename ratio_divide::type; + static constexpr const char* _prefix = thermo::_si_prefix_v<_dpc>; std::formatter _num; + bool _has_spec = false; - constexpr auto parse(format_parse_context& ctx) { return _num.parse(ctx); } - - template - auto format(thermo::celsius_real t, FormatContext& ctx) const { - auto out = _num.format(t.count(), ctx); - for (const char* p = "°C"; *p != '\0'; ++p) { - *out++ = *p; +public: + constexpr auto parse(format_parse_context& ctx) { + auto it = ctx.begin(); + if (it == ctx.end() || *it == '}') { + if (_prefix == nullptr) { + throw format_error("thermo: precision has no SI prefix; use an explicit format spec"); + } + return it; } - return out; + _has_spec = true; + return _num.parse(ctx); } -}; - -template<> -struct formatter { - std::formatter _num; - - constexpr auto parse(format_parse_context& ctx) { return _num.parse(ctx); } template - auto format(thermo::kelvin_real t, FormatContext& ctx) const { - auto out = _num.format(t.count(), ctx); - for (const char* p = "K"; *p != '\0'; ++p) { - *out++ = *p; + auto format(const thermo::temperature& t, FormatContext& ctx) const { + if (_has_spec) { + double degrees = static_cast(t.count()) * _dpc::num / _dpc::den; + auto out = _num.format(degrees, ctx); + return thermo::_format_append(out, Scale::suffix); } - return out; + auto out = std::format_to(ctx.out(), "{}", t.count()); + out = thermo::_format_append(out, _prefix); + return thermo::_format_append(out, Scale::suffix); } }; -template<> -struct formatter { +// "{}" prints the exact stored count with a precision-qualified unit +// (delta_millicelsius(1500) -> "1500Δm°C"). A non-empty spec is applied to +// the value in scale degrees (as double): +// std::format("{:.1f}", delta_millicelsius(1500)) == "1.5Δ°C". +template +struct formatter> { +private: + using _delta_t = thermo::delta; + using _unit = thermo::_delta_unit; // hard error if unmapped + using _dpc = typename ratio_divide::type; + static constexpr const char* _prefix = thermo::_si_prefix_v<_dpc>; std::formatter _num; + bool _has_spec = false; - constexpr auto parse(format_parse_context& ctx) { return _num.parse(ctx); } - - template - auto format(thermo::fahrenheit_real t, FormatContext& ctx) const { - auto out = _num.format(t.count(), ctx); - for (const char* p = "°F"; *p != '\0'; ++p) { - *out++ = *p; +public: + constexpr auto parse(format_parse_context& ctx) { + auto it = ctx.begin(); + if (it == ctx.end() || *it == '}') { + if (_prefix == nullptr) { + throw format_error("thermo: precision has no SI prefix; use an explicit format spec"); + } + return it; } - return out; - } -}; - -template<> -struct formatter> { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::delta d, format_context& ctx) const { - return std::format_to(ctx.out(), "{}Δ°C", d.count()); - } -}; - -template<> -struct formatter> { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::delta d, format_context& ctx) const { - return std::format_to(ctx.out(), "{}Δd°C", d.count()); - } -}; - -template<> -struct formatter> { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::delta d, format_context& ctx) const { - return std::format_to(ctx.out(), "{}Δm°C", d.count()); + _has_spec = true; + return _num.parse(ctx); } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - auto format(thermo::delta_fahrenheit d, format_context& ctx) const { - return std::format_to(ctx.out(), "{}Δ°F", d.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::delta_decifahrenheit d, format_context& ctx) const { - return std::format_to(ctx.out(), "{}Δd°F", d.count()); - } -}; - -template<> -struct formatter { - constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } - - auto format(thermo::delta_millifahrenheit d, format_context& ctx) const { - return std::format_to(ctx.out(), "{}Δm°F", d.count()); + template + auto format(const _delta_t& d, FormatContext& ctx) const { + if (_has_spec) { + double degrees = static_cast(d.count()) * _dpc::num / _dpc::den; + auto out = _num.format(degrees, ctx); + out = thermo::_format_append(out, "Δ"); + return thermo::_format_append(out, _unit::suffix); + } + auto out = std::format_to(ctx.out(), "{}", d.count()); + out = thermo::_format_append(out, "Δ"); + out = thermo::_format_append(out, _prefix); + return thermo::_format_append(out, _unit::suffix); } }; #endif diff --git a/tests/thermo_test.cpp b/tests/thermo_test.cpp index 5d55e3b..dfdf49a 100644 --- a/tests/thermo_test.cpp +++ b/tests/thermo_test.cpp @@ -194,6 +194,25 @@ TEST_CASE("fahrenheit to celsius conversion", "[thermo][temperature]") { REQUIRE(cm40.count() == -40); } +TEST_CASE("millifahrenheit conversion", "[thermo][temperature]") { + // 0.001°F precision: 1°F = 1000 counts + REQUIRE(temperature_cast(fahrenheit(1)).count() == 1000); + millifahrenheit mf = fahrenheit(1); // finer precision, implicit + REQUIRE(mf.count() == 1000); + REQUIRE(temperature_cast(fahrenheit(72)).count() == 72000); + + // Cross-scale: 0°C = 32°F = 32000 millifahrenheit + REQUIRE(temperature_cast(celsius(0)).count() == 32000); + + // 72.5°F in decifahrenheit -> millifahrenheit + REQUIRE(millifahrenheit(decifahrenheit(725)).count() == 72500); + + // Deltas + REQUIRE(delta_millifahrenheit(delta_fahrenheit(1)).count() == 1000); + REQUIRE(delta_millifahrenheit(9000) == delta_celsius(5)); + REQUIRE(delta_millifahrenheit(9000) == delta_millicelsius(5000)); +} + // ============================================================================= // Temperature arithmetic // ============================================================================= @@ -325,9 +344,15 @@ TEST_CASE("to_string", "[thermo][string]") { REQUIRE(to_string(fahrenheit(32)) == "32°F"); REQUIRE(to_string(millicelsius(1000)) == "1000m°C"); REQUIRE(to_string(millikelvin(273150)) == "273150mK"); + REQUIRE(to_string(decicelsius(200)) == "200d°C"); + REQUIRE(to_string(decikelvin(2955)) == "2955dK"); + REQUIRE(to_string(decifahrenheit(725)) == "725d°F"); + REQUIRE(to_string(millifahrenheit(72500)) == "72500m°F"); + REQUIRE(to_string(delta_celsius(5)) == "5Δ°C"); + REQUIRE(to_string(delta_millifahrenheit(36000)) == "36000Δm°F"); } -#if __has_include() && defined(__cpp_lib_format) +#if CONFIG_THERMO_STD_FORMAT TEST_CASE("std::format delta", "[thermo][string][delta]") { REQUIRE(std::format("{}", delta_celsius(20)) == "20Δ°C"); REQUIRE(std::format("{}", delta_decicelsius(200)) == "200Δd°C"); @@ -337,28 +362,47 @@ TEST_CASE("std::format delta", "[thermo][string][delta]") { REQUIRE(std::format("{}", delta_millifahrenheit(36000)) == "36000Δm°F"); } -TEST_CASE("std::format real temperature", "[thermo][string][real]") { - // count() holds the degree value, so it formats directly (no precision suffix). - REQUIRE(std::format("{}", celsius_real(22.5)) == "22.5°C"); - REQUIRE(std::format("{}", celsius_real(20.0)) == "20°C"); - REQUIRE(std::format("{}", kelvin_real(295.65)) == "295.65K"); - REQUIRE(std::format("{}", fahrenheit_real(72.5)) == "72.5°F"); - - // The standard floating-point format spec is honored. - REQUIRE(std::format("{:.2f}", celsius_real(22.5)) == "22.50°C"); - REQUIRE(std::format("{:.1f}", celsius_real(22.53)) == "22.5°C"); +TEST_CASE("std::format temperature", "[thermo][string][temperature]") { + // Empty spec prints the exact stored count with a precision-qualified unit. + REQUIRE(std::format("{}", celsius(20)) == "20°C"); + REQUIRE(std::format("{}", decicelsius(200)) == "200d°C"); + REQUIRE(std::format("{}", millicelsius(22500)) == "22500m°C"); + REQUIRE(std::format("{}", kelvin(273)) == "273K"); + REQUIRE(std::format("{}", decikelvin(2955)) == "2955dK"); + REQUIRE(std::format("{}", millikelvin(273150)) == "273150mK"); + REQUIRE(std::format("{}", fahrenheit(72)) == "72°F"); + REQUIRE(std::format("{}", decifahrenheit(725)) == "725d°F"); + REQUIRE(std::format("{}", millifahrenheit(72500)) == "72500m°F"); +} - // Intended use: cast an exact integer reading to a real-valued type for display. - REQUIRE(std::format("{:.1f}", temperature_cast(millicelsius(22500))) == "22.5°C"); - REQUIRE(std::format("{:.1f}", temperature_cast(millicelsius(22500))) == "72.5°F"); +TEST_CASE("std::format with spec renders in scale degrees", "[thermo][string]") { + // A floating-point format spec renders the value in scale degrees. + REQUIRE(std::format("{:.1f}", millicelsius(22534)) == "22.5°C"); + REQUIRE(std::format("{:g}", millicelsius(22500)) == "22.5°C"); + REQUIRE(std::format("{:.2f}", decicelsius(225)) == "22.50°C"); + REQUIRE(std::format("{:.2f}", millikelvin(295650)) == "295.65K"); + REQUIRE(std::format("{:.1f}", fahrenheit(72)) == "72.0°F"); + REQUIRE(std::format("{:.1f}", decifahrenheit(725)) == "72.5°F"); + REQUIRE(std::format("{:.2f}", millifahrenheit(72530)) == "72.53°F"); + + // Width and alignment pass through to the numeric part. + REQUIRE(std::format("{:7.1f}", millicelsius(22500)) == " 22.5°C"); + + // Deltas render the same way, keeping the Δ marker. + REQUIRE(std::format("{:.1f}", delta_millicelsius(1500)) == "1.5Δ°C"); + REQUIRE(std::format("{:.1f}", delta_millifahrenheit(9000)) == "9.0Δ°F"); + REQUIRE(std::format("{:.1f}", delta_fahrenheit(36)) == "36.0Δ°F"); } + #endif -TEST_CASE("real-valued temperature conversion", "[thermo][real]") { - // count() reads directly in scale degrees; same-scale casts are exact here. - REQUIRE(celsius_real(millicelsius(22500)).count() == 22.5); // implicit - REQUIRE(temperature_cast(millicelsius(22500)).count() == 22.5); - REQUIRE(temperature_cast(decicelsius(225)).count() == 22.5); +TEST_CASE("floating-point representation conversion", "[thermo][real]") { + // Inexact reps accept lossy conversions implicitly; count() reads in + // precision steps, so with the default precision that is scale degrees. + using celsius_d = temperature>; + REQUIRE(celsius_d(millicelsius(22500)).count() == 22.5); // implicit + REQUIRE(temperature_cast(millicelsius(22500)).count() == 22.5); + REQUIRE(temperature_cast(decicelsius(225)).count() == 22.5); } // ============================================================================= diff --git a/vcpkg-port/vcpkg.json b/vcpkg-port/vcpkg.json index 1b736eb..0a2f0ce 100644 --- a/vcpkg-port/vcpkg.json +++ b/vcpkg-port/vcpkg.json @@ -1,6 +1,6 @@ { "name": "cleishm-thermo-cpp", - "version": "1.3.0", + "version": "2.0.0", "description": "Type-safe temperature handling library modeled after std::chrono", "homepage": "https://github.com/cleishm/thermo-cpp", "license": "MIT",