dimval 0.2.0
Modern C++23 header-only library of dimensional values (units, measures, ranges)
Loading...
Searching...
No Matches
format.hpp
Go to the documentation of this file.
1#pragma once
2
20
21#include <dimval/core.hpp>
22#include <dimval/descriptor.hpp>
23#include <dimval/measure.hpp>
24#include <dimval/range.hpp>
25#include <dimval/traits.hpp>
26#include <dimval/unit.hpp>
27
28#include <cstddef>
29#include <cstdint>
30#include <format>
31#include <string>
32#include <string_view>
33
34namespace dimval::detail {
35
36enum class FormatStyle : std::uint8_t {
37 Default,
38 Short,
39 Full,
40 Json,
41};
42
43struct ValueFormatSpec {
44 FormatStyle style{FormatStyle::Default};
45 int precision{-1}; // -1 = descriptor default
46};
47
48template <typename It>
49constexpr It parse_value_format_spec(It it, It end, ValueFormatSpec& spec) {
50 // Parse style token first (optional).
51 auto remaining_size = static_cast<std::size_t>(end - it);
52 std::string_view rest{it, remaining_size};
53
54 auto consume = [&](const std::string_view tok, const FormatStyle s) {
55 if (rest.starts_with(tok)) {
56 spec.style = s;
57 it += static_cast<std::ptrdiff_t>(tok.size());
58 rest.remove_prefix(tok.size());
59 return true;
60 }
61 return false;
62 };
63 consume("default", FormatStyle::Default) || consume("short", FormatStyle::Short) ||
64 consume("full", FormatStyle::Full) || consume("json", FormatStyle::Json);
65 // Optional precision: ".N"
66 if (it != end && *it == '.') {
67 ++it;
68 int p = 0;
69 bool any = false;
70 while (it != end && *it >= '0' && *it <= '9') {
71 p = p * 10 + (*it - '0');
72 any = true;
73 ++it;
74 }
75 if (any) {
76 spec.precision = p;
77 }
78 }
79 return it;
80}
81
82template <typename T>
83[[nodiscard]] std::string format_numeric(T v, int precision) {
84 if (precision >= 0) {
85 return std::format("{:.{}f}", static_cast<double>(v), precision);
86 }
87 return std::format("{}", v);
88}
89
90[[nodiscard]] inline std::string render_unit_value(const double v,
91 const int precision,
92 const UnitDescriptor& d,
93 const FormatStyle style) {
94 const int eff_precision = precision >= 0 ? precision : d.default_precision;
95 std::string num = format_numeric<double>(v, eff_precision);
96 switch (style) {
97 case FormatStyle::Json:
98 return std::format(R"({{"unit":"{}","value":{}}})", d.id, num);
99 case FormatStyle::Full:
100 return std::format("{} {}", num, d.long_name);
101 case FormatStyle::Short: {
102 const std::string_view sym = d.short_name.empty() ? d.symbol : d.short_name;
103 if (sym.empty()) {
104 return num;
105 }
106 return std::format("{}{}", num, sym);
107 }
108 case FormatStyle::Default:
109 default: {
110 if (d.symbol.empty()) {
111 return num;
112 }
113 if (d.no_space_before_symbol) {
114 return std::format("{}{}", num, d.symbol);
115 }
116 return std::format("{} {}", num, d.symbol);
117 }
118 }
119}
121[[nodiscard]] inline std::string render_measure_value(const double v,
122 const int precision,
123 const MeasureDescriptor& md,
124 const UnitDescriptor& ud,
125 const FormatStyle style) {
126 int eff_precision = precision;
127 if (eff_precision < 0)
128 eff_precision = md.default_precision;
129 if (eff_precision < 0)
130 eff_precision = ud.default_precision;
131 std::string num = format_numeric<double>(v, eff_precision);
132 switch (style) {
133 case FormatStyle::Json:
134 return std::format(R"({{"measure":"{}","unit":"{}","value":{}}})", md.id, ud.id, num);
135 case FormatStyle::Full:
136 return std::format("{} {} ({})", num, md.name, ud.long_name);
137 case FormatStyle::Short: {
138 const std::string_view sym = ud.short_name.empty() ? ud.symbol : ud.short_name;
139 if (sym.empty()) {
140 return num;
141 }
142 return std::format("{}{}", num, sym);
143 }
144 case FormatStyle::Default:
145 default: {
146 if (ud.symbol.empty()) {
147 return num;
148 }
149 if (ud.no_space_before_symbol) {
150 return std::format("{}{}", num, ud.symbol);
151 }
152 return std::format("{} {}", num, ud.symbol);
153 }
154 }
155}
156
157[[nodiscard]] inline char open_bracket(const Bound b) noexcept {
158 return b == Bound::Inclusive ? '[' : '(';
159}
160
161[[nodiscard]] inline char close_bracket(const Bound b) noexcept {
162 return b == Bound::Inclusive ? ']' : ')';
163}
164
165} // namespace dimval::detail
166
167template <dimval::UnitLike U, dimval::NumericValue T>
168struct std::formatter<dimval::UnitValue<U, T>> {
169 dimval::detail::ValueFormatSpec spec_{};
170
171 constexpr auto parse(const std::format_parse_context& ctx) {
172 const auto* it = dimval::detail::parse_value_format_spec(ctx.begin(), ctx.end(), spec_);
173 if (it != ctx.end() && *it != '}') {
174 throw std::format_error{"dimval::UnitValue: invalid format spec"};
175 }
176 return it;
177 }
178
179 template <typename FormatContext>
180 auto format(const dimval::UnitValue<U, T>& v, FormatContext& ctx) const {
181 const auto d = U::descriptor();
182 const auto s = dimval::detail::render_unit_value(
183 static_cast<double>(v.v), spec_.precision, d, spec_.style);
184 return std::ranges::copy(s, ctx.out()).out;
185 }
186};
187
188template <dimval::MeasureLike M, dimval::NumericValue T>
189struct std::formatter<dimval::MeasureValue<M, T>> {
190 dimval::detail::ValueFormatSpec spec_{};
191
192 constexpr auto parse(const std::format_parse_context& ctx) {
193 const auto* it = dimval::detail::parse_value_format_spec(ctx.begin(), ctx.end(), spec_);
194 if (it != ctx.end() && *it != '}') {
195 throw std::format_error{"dimval::MeasureValue: invalid format spec"};
196 }
197 return it;
198 }
199
200 template <typename FormatContext>
201 auto format(const dimval::MeasureValue<M, T>& v, FormatContext& ctx) const {
202 const auto md = M::descriptor();
203 using unit_t = M::base_unit_t;
204 const auto ud = unit_t::descriptor();
205 const auto s = dimval::detail::render_measure_value(
206 static_cast<double>(v.v), spec_.precision, md, ud, spec_.style);
207 return std::ranges::copy(s, ctx.out()).out;
208 }
209};
210
211template <dimval::UnitLike U, dimval::NumericValue T>
212struct std::formatter<dimval::UnitRangeValue<U, T>> {
213 dimval::detail::ValueFormatSpec spec_{};
214
215 constexpr auto parse(const std::format_parse_context& ctx) {
216 const auto* it = dimval::detail::parse_value_format_spec(ctx.begin(), ctx.end(), spec_);
217 if (it != ctx.end() && *it != '}') {
218 throw std::format_error{"dimval::UnitRangeValue: invalid format spec"};
219 }
220 return it;
221 }
222
223 template <typename FormatContext>
224 auto format(const dimval::UnitRangeValue<U, T>& r, FormatContext& ctx) const {
225 const auto d = U::descriptor();
226 if (spec_.style == dimval::detail::FormatStyle::Json) {
227 const auto out = std::format(
228 R"({{"unit":"{}","min":{},"max":{},"min_inclusive":{},"max_inclusive":{}}})",
229 d.id,
230 dimval::detail::format_numeric<double>(static_cast<double>(r.min().v),
231 spec_.precision),
232 dimval::detail::format_numeric<double>(static_cast<double>(r.max().v),
233 spec_.precision),
234 r.inclusion().lower == dimval::Bound::Inclusive ? "true" : "false",
235 r.inclusion().upper == dimval::Bound::Inclusive ? "true" : "false");
236 return std::ranges::copy(out, ctx.out()).out;
237 }
238 const auto lo = dimval::detail::render_unit_value(
239 static_cast<double>(r.min().v), spec_.precision, d, spec_.style);
240 const auto hi = dimval::detail::render_unit_value(
241 static_cast<double>(r.max().v), spec_.precision, d, spec_.style);
242 const auto out = std::format("{}{}, {}{}",
243 dimval::detail::open_bracket(r.inclusion().lower),
244 lo,
245 hi,
246 dimval::detail::close_bracket(r.inclusion().upper));
247 return std::ranges::copy(out, ctx.out()).out;
248 }
249};
250
251template <dimval::MeasureLike M, dimval::NumericValue T>
252struct std::formatter<dimval::MeasureRangeValue<M, T>> {
253 dimval::detail::ValueFormatSpec spec_{};
254
255 constexpr auto parse(const std::format_parse_context& ctx) {
256 const auto* it = dimval::detail::parse_value_format_spec(ctx.begin(), ctx.end(), spec_);
257 if (it != ctx.end() && *it != '}') {
258 throw std::format_error{"dimval::MeasureRangeValue: invalid format spec"};
259 }
260 return it;
261 }
262
263 template <typename FormatContext>
264 auto format(const dimval::MeasureRangeValue<M, T>& r, FormatContext& ctx) const {
265 const auto md = M::descriptor();
266 using unit_t = M::base_unit_t;
267 const auto ud = unit_t::descriptor();
268 if (spec_.style == dimval::detail::FormatStyle::Json) {
269 const auto out =
270 std::format(R"({{"measure":"{}","unit":"{}","min":{},"max":{},)"
271 R"("min_inclusive":{},"max_inclusive":{}}})",
272 md.id,
273 ud.id,
274 dimval::detail::format_numeric<double>(static_cast<double>(r.min().v),
275 spec_.precision),
276 dimval::detail::format_numeric<double>(static_cast<double>(r.max().v),
277 spec_.precision),
278 r.inclusion().lower == dimval::Bound::Inclusive ? "true" : "false",
279 r.inclusion().upper == dimval::Bound::Inclusive ? "true" : "false");
280 return std::ranges::copy(out, ctx.out()).out;
281 }
282 const auto lo = dimval::detail::render_measure_value(
283 static_cast<double>(r.min().v), spec_.precision, md, ud, spec_.style);
284 const auto hi = dimval::detail::render_measure_value(
285 static_cast<double>(r.max().v), spec_.precision, md, ud, spec_.style);
286 const auto out = std::format("{}{}, {}{}",
287 dimval::detail::open_bracket(r.inclusion().lower),
288 lo,
289 hi,
290 dimval::detail::close_bracket(r.inclusion().upper));
291 return std::ranges::copy(out, ctx.out()).out;
292 }
293};
294
295template <>
296struct std::formatter<dimval::UnitDescriptor> {
297 static constexpr auto parse(const std::format_parse_context& ctx) {
298 const auto* it = ctx.begin();
299 if (it != ctx.end() && *it != '}') {
300 throw std::format_error{"dimval::UnitDescriptor: invalid format spec"};
301 }
302 return it;
303 }
304 template <typename FormatContext>
305 static auto format(const dimval::UnitDescriptor& d, FormatContext& ctx) {
306 const auto out =
307 std::format("UnitDescriptor{{id={}, symbol={}, kind={}, factor={}, offset={}}}",
308 d.id,
309 d.symbol,
310 d.kind,
311 d.factor,
312 d.offset);
313 return std::ranges::copy(out, ctx.out()).out;
314 }
315};
316
317template <>
318struct std::formatter<dimval::MeasureDescriptor> {
319 static constexpr auto parse(const std::format_parse_context& ctx) {
320 const auto* it = ctx.begin();
321 if (it != ctx.end() && *it != '}') {
322 throw std::format_error{"dimval::MeasureDescriptor: invalid format spec"};
323 }
324 return it;
325 }
326 template <typename FormatContext>
327 static auto format(const dimval::MeasureDescriptor& d, FormatContext& ctx) {
328 const auto out = std::format(
329 "MeasureDescriptor{{id={}, base_unit={}, name={}}}", d.id, d.base_unit_id, d.name);
330 return std::ranges::copy(out, ctx.out()).out;
331 }
332};
333
334// Out-of-line implementations of the IUnitValue / IMeasureValue overrides that
335// need to call std::format. Defining them here breaks the otherwise circular
336// dependency between unit.hpp / measure.hpp and the formatter specs above.
337namespace dimval {
338
339template <UnitLike U, NumericValue T>
340inline std::string UnitValue<U, T>::to_string() const {
341 return std::format("{}", *this);
342}
343
344template <UnitLike U, NumericValue T>
345inline std::string UnitValue<U, T>::to_formatted_string() const {
346 return std::format("{}", *this);
347}
348
349template <MeasureLike M, NumericValue T>
350inline std::string MeasureValue<M, T>::to_string() const {
351 return std::format("{}", *this);
352}
353
354template <MeasureLike M, NumericValue T>
355inline std::string MeasureValue<M, T>::to_formatted_string() const {
356 return std::format("{}", *this);
357}
358
359template <UnitLike U, NumericValue T>
360inline std::string UnitRangeValue<U, T>::to_string() const {
361 return std::format("{}", *this);
362}
363
364template <UnitLike U, NumericValue T>
365inline std::string UnitRangeValue<U, T>::to_formatted_string() const {
366 return std::format("{}", *this);
367}
368
369template <MeasureLike M, NumericValue T>
370inline std::string MeasureRangeValue<M, T>::to_string() const {
371 return std::format("{}", *this);
372}
373
374template <MeasureLike M, NumericValue T>
375inline std::string MeasureRangeValue<M, T>::to_formatted_string() const {
376 return std::format("{}", *this);
377}
378
379} // namespace dimval
Closed/open interval over a MeasureValue<M,T>.
Definition range.hpp:196
Closed/open interval over a UnitValue<U,T>.
Definition range.hpp:45
Common concepts, error types, and small utilities used across dimval.
Bound
Inclusivity of a single range bound.
Definition core.hpp:50
Runtime metadata structs for units and measures.
MeasureValue<M,T>: a UnitValue tagged with an additional semantic measure.
UnitRangeValue and MeasureRangeValue — closed/open intervals of dimensional values.
Runtime metadata for a measure (a semantic specialization of a unit).
Definition descriptor.hpp:54
std::string_view id
Stable identifier (e.g. "distance"). Unique per registry.
Definition descriptor.hpp:55
std::string_view name
Human-readable name (e.g. "Distance").
Definition descriptor.hpp:57
int default_precision
Empty falls back to the base unit's precision.
Definition descriptor.hpp:60
A value carrying both a measure tag and a unit tag.
Definition measure.hpp:31
Runtime metadata for a unit.
Definition descriptor.hpp:29
std::string_view short_name
Short human-readable name (e.g. "m").
Definition descriptor.hpp:32
int default_precision
Default numeric precision; -1 = unset.
Definition descriptor.hpp:39
std::string_view symbol
Display symbol (e.g. "m"). Often equal to id.
Definition descriptor.hpp:31
bool no_space_before_symbol
Render flush, e.g. "100%" instead of "100 %".
Definition descriptor.hpp:40
std::string_view long_name
Full human-readable name (e.g. "meter").
Definition descriptor.hpp:33
std::string_view id
Stable identifier (e.g. "m"). Unique per registry.
Definition descriptor.hpp:30
A value paired at the type level with a unit tag.
Definition unit.hpp:36
std::string to_string() const override
Render <value> <symbol> (or whatever the descriptor's formatter dictates).
Definition format.hpp:340
std::string to_formatted_string() const override
Render with the descriptor's default precision applied.
Definition format.hpp:345
Concepts and helper accessors that drive the static side of dimval.
UnitValue<U,T>: a strongly-typed value carrying a compile-time unit tag.