From 0873647c7a190f4b8ffadaa4fe85fd237fc21925 Mon Sep 17 00:00:00 2001 From: Chris Leishman Date: Sun, 5 Jul 2026 19:40:56 -0400 Subject: [PATCH] Add real-valued (double) temperature types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add celsius_real, kelvin_real, and fahrenheit_real: double-precision temperatures whose count() reads directly in scale degrees. Their std::formatter specializations render the degree value with the scale suffix (e.g. "22.5°C") and honor the standard floating-point format spec ("{:.1f}" -> "22.5°C"). Convert an exact reading with a cast, e.g. temperature_cast(millicelsius(22500)). Include before the std::format feature-test check so the formatter block is enabled in standalone (non-IDF) builds, where nothing pulls in __cpp_lib_format beforehand. --- README.md | 18 ++++++++++- idf_component.yml | 2 +- include/thermo/thermo.hpp | 67 +++++++++++++++++++++++++++++++++++++++ tests/thermo_test.cpp | 23 ++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 700c316..70d7e1d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A type-safe, header-only C++23 library for temperature handling, modeled after ` ## Features - **Type-safe temperatures** with distinct types for Celsius, Kelvin, and Fahrenheit -- **Configurable precision** (degree, decidegree, millidegree) +- **Configurable precision** (degree, decidegree, millidegree) plus real-valued (`double`) types for display and computation - **Automatic conversions** between scales and precisions - **Lossless implicit conversions** (lossy conversions require explicit casts) - **Temperature deltas** distinct from absolute temperatures @@ -91,6 +91,12 @@ 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). +millicelsius reading{22500}; +celsius_real shown = reading; // 22.5 (implicit) +std::string disp = std::format("{:.1f}", shown); // "22.5°C" ``` ## Temperature Types @@ -108,6 +114,16 @@ std::string s = to_string(room_temp); // "20°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`). ### Temperature Deltas diff --git a/idf_component.yml b/idf_component.yml index 2f9daff..1f9ecde 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.2.2" +version: "1.3.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 82cf0dc..c87b8fa 100644 --- a/include/thermo/thermo.hpp +++ b/include/thermo/thermo.hpp @@ -11,6 +11,7 @@ #include #include #include +#include // pulls in __cpp_lib_format so the detection below works in non-IDF builds #ifndef CONFIG_THERMO_STD_FORMAT #if __has_include() && defined(__cpp_lib_format) #define CONFIG_THERMO_STD_FORMAT 1 @@ -826,6 +827,21 @@ using decifahrenheit = 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. + */ +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"; } @@ -960,6 +976,57 @@ struct formatter { } }; +// 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 { + std::formatter _num; + + 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; + } + return out; + } +}; + +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; + } + return out; + } +}; + +template<> +struct formatter { + std::formatter _num; + + 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; + } + return out; + } +}; + template<> struct formatter> { constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } diff --git a/tests/thermo_test.cpp b/tests/thermo_test.cpp index df27e94..5d55e3b 100644 --- a/tests/thermo_test.cpp +++ b/tests/thermo_test.cpp @@ -336,8 +336,31 @@ TEST_CASE("std::format delta", "[thermo][string][delta]") { REQUIRE(std::format("{}", delta_decifahrenheit(360)) == "360Δd°F"); 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"); + + // 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"); +} #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); +} + // ============================================================================= // Min/max values // =============================================================================