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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

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: "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"
Expand Down
67 changes: 67 additions & 0 deletions include/thermo/thermo.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <limits>
#include <ratio>
#include <string>
#include <version> // pulls in __cpp_lib_format so the detection below works in non-IDF builds
#ifndef CONFIG_THERMO_STD_FORMAT
#if __has_include(<format>) && defined(__cpp_lib_format)
#define CONFIG_THERMO_STD_FORMAT 1
Expand Down Expand Up @@ -826,6 +827,21 @@ using decifahrenheit = temperature<fahrenheit_scale, delta<int64_t, std::ratio<5
/** @brief Fahrenheit with 0.001 degree precision. */
using millifahrenheit = temperature<fahrenheit_scale, delta<int64_t, std::ratio<5, 900>>>;

/**
* @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<celsius_real>(millicelsius(22500))` — for display or
* further floating-point computation.
*/
using celsius_real = temperature<celsius_scale, delta<double>>;
/** @brief Real-valued (double precision) Kelvin. @see celsius_real */
using kelvin_real = temperature<kelvin_scale, delta<double>>;
/** @brief Real-valued (double precision) Fahrenheit. @see celsius_real */
using fahrenheit_real = temperature<fahrenheit_scale, delta<double, std::ratio<5, 9>>>;

inline std::string to_string(celsius t) {
return std::to_string(t.count()) + "°C";
}
Expand Down Expand Up @@ -960,6 +976,57 @@ struct formatter<thermo::millifahrenheit> {
}
};

// 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<thermo::celsius_real> {
std::formatter<double> _num;

constexpr auto parse(format_parse_context& ctx) { return _num.parse(ctx); }

template<typename FormatContext>
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<thermo::kelvin_real> {
std::formatter<double> _num;

constexpr auto parse(format_parse_context& ctx) { return _num.parse(ctx); }

template<typename FormatContext>
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<thermo::fahrenheit_real> {
std::formatter<double> _num;

constexpr auto parse(format_parse_context& ctx) { return _num.parse(ctx); }

template<typename FormatContext>
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<thermo::delta<int64_t>> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
Expand Down
23 changes: 23 additions & 0 deletions tests/thermo_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<celsius_real>(millicelsius(22500))) == "22.5°C");
REQUIRE(std::format("{:.1f}", temperature_cast<fahrenheit_real>(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<celsius_real>(millicelsius(22500)).count() == 22.5);
REQUIRE(temperature_cast<celsius_real>(decicelsius(225)).count() == 22.5);
}

// =============================================================================
// Min/max values
// =============================================================================
Expand Down
Loading