commons 0.1.5
Header-only C++23 library of common/shared types for the C++ libraries
Loading...
Searching...
No Matches
semver.hpp
Go to the documentation of this file.
1#pragma once
2
38
39#include <commons/types.hpp>
40
41#include <algorithm>
42#include <array>
43#include <compare>
44#include <format>
45#include <functional>
46#include <optional>
47#include <ostream>
48#include <string>
49#include <string_view>
50
51namespace comms {
52
54struct SemVer {
55 comms::u32 major = 0;
56 comms::u32 minor = 0;
57 comms::u32 patch = 0;
58 std::string prerelease;
59 std::string build;
60
61 // -- parsing ------------------------------------------------------------
62
67 [[nodiscard]] static std::optional<SemVer> parse(std::string_view s) {
68 if (!s.empty() && (s.front() == 'v' || s.front() == 'V')) {
69 s.remove_prefix(1);
70 }
71 if (s.empty()) {
72 return std::nullopt;
73 }
74
75 // Split off `+build` at the first '+', then `-prerelease` at the first
76 // '-' in what remains; the build may itself contain '-', which is why
77 // it is peeled off first.
78 std::string_view build;
79 bool has_build = false;
80 if (const auto plus = s.find('+'); plus != std::string_view::npos) {
81 has_build = true;
82 build = s.substr(plus + 1);
83 s = s.substr(0, plus);
84 }
85
86 std::string_view prerelease;
87 bool has_prerelease = false;
88 if (const auto hyphen = s.find('-'); hyphen != std::string_view::npos) {
89 has_prerelease = true;
90 prerelease = s.substr(hyphen + 1);
91 s = s.substr(0, hyphen);
92 }
93
94 // Dotted numeric core: 1-3 components, missing ones default to 0.
95 if (s.empty()) {
96 return std::nullopt;
97 }
98 std::array<comms::u32, 3> parts{0, 0, 0};
99 usize index = 0;
100 std::string_view core = s;
101 while (true) {
102 if (index == 3) {
103 return std::nullopt; // more than three numeric components
104 }
105 const auto dot = core.find('.');
106 const std::string_view token =
107 (dot == std::string_view::npos) ? core : core.substr(0, dot);
108 const auto value = parse_uint(token);
109 if (!value) {
110 return std::nullopt;
111 }
112 parts[index++] = *value;
113 if (dot == std::string_view::npos) {
114 break;
115 }
116 core = core.substr(dot + 1);
117 }
118
119 if (has_prerelease && !valid_identifiers(prerelease, /*numeric_no_leading_zero=*/true)) {
120 return std::nullopt;
121 }
122 if (has_build && !valid_identifiers(build, /*numeric_no_leading_zero=*/false)) {
123 return std::nullopt;
124 }
125
126 SemVer v;
127 v.major = parts[0];
128 v.minor = parts[1];
129 v.patch = parts[2];
130 v.prerelease = std::string{prerelease};
131 v.build = std::string{build};
132 return v;
133 }
134
135 // -- text ---------------------------------------------------------------
136
139 [[nodiscard]] std::string to_string() const {
140 std::string s =
141 std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch);
142 if (!prerelease.empty()) {
143 s += "-" + prerelease;
144 }
145 if (!build.empty()) {
146 s += "+" + build;
147 }
148 return s;
149 }
150
151 // -- ordering -----------------------------------------------------------
152
155 [[nodiscard]] std::strong_ordering operator<=>(const SemVer& o) const {
156 if (const auto c = major <=> o.major; c != 0) {
157 return c;
158 }
159 if (const auto c = minor <=> o.minor; c != 0) {
160 return c;
161 }
162 if (const auto c = patch <=> o.patch; c != 0) {
163 return c;
164 }
165 return compare_prerelease(prerelease, o.prerelease);
166 }
167
170 [[nodiscard]] bool operator==(const SemVer& o) const {
171 return std::is_eq(*this <=> o);
172 }
173
174private:
178 [[nodiscard]] static std::optional<comms::u32> parse_uint(const std::string_view s) {
179 if (s.empty()) {
180 return std::nullopt;
181 }
182 constexpr comms::u64 u32_max = ~static_cast<comms::u32>(0);
183 comms::u64 result = 0; // accumulate wide so overflow is detectable
184 for (const char c : s) {
185 if (c < '0' || c > '9') {
186 return std::nullopt;
187 }
188 result = result * 10 + static_cast<comms::u64>(c - '0');
189 if (result > u32_max) {
190 return std::nullopt;
191 }
192 }
193 return static_cast<comms::u32>(result);
194 }
195
197 [[nodiscard]] static bool is_numeric(std::string_view s) {
198 return !s.empty() &&
199 std::ranges::all_of(s, [](const char c) { return c >= '0' && c <= '9'; });
200 }
201
205 [[nodiscard]] static bool valid_identifiers(const std::string_view s,
206 const bool numeric_no_leading_zero) {
207 usize start = 0;
208 while (true) {
209 const auto dot = s.find('.', start);
210 const std::string_view id =
211 (dot == std::string_view::npos) ? s.substr(start) : s.substr(start, dot - start);
212 if (id.empty()) {
213 return false;
214 }
215 bool all_digits = true;
216 for (const char c : id) {
217 const bool ok = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
218 (c >= 'a' && c <= 'z') || c == '-';
219 if (!ok) {
220 return false;
221 }
222 if (c < '0' || c > '9') {
223 all_digits = false;
224 }
225 }
226 if (numeric_no_leading_zero && all_digits && id.size() > 1 && id.front() == '0') {
227 return false;
228 }
229 if (dot == std::string_view::npos) {
230 return true;
231 }
232 start = dot + 1;
233 }
234 }
235
238 [[nodiscard]] static std::strong_ordering compare_prerelease(const std::string_view a,
239 const std::string_view b) {
240 if (a.empty() && b.empty()) {
241 return std::strong_ordering::equal;
242 }
243 if (a.empty()) {
244 return std::strong_ordering::greater; // no prerelease outranks one
245 }
246 if (b.empty()) {
247 return std::strong_ordering::less;
248 }
249
250 usize ai = 0;
251 usize bi = 0;
252 while (ai < a.size() && bi < b.size()) {
253 const auto ad = a.find('.', ai);
254 const auto bd = b.find('.', bi);
255 const std::string_view ida =
256 (ad == std::string_view::npos) ? a.substr(ai) : a.substr(ai, ad - ai);
257 const std::string_view idb =
258 (bd == std::string_view::npos) ? b.substr(bi) : b.substr(bi, bd - bi);
259
260 const bool a_num = is_numeric(ida);
261 if (const bool b_num = is_numeric(idb); a_num && b_num) {
262 // No leading zeros, so the shorter string is the smaller number;
263 // equal length falls back to a lexical (== numeric) compare.
264 if (ida.size() != idb.size()) {
265 return ida.size() <=> idb.size();
266 }
267 if (const auto c = ida <=> idb; c != 0) {
268 return c;
269 }
270 } else if (a_num != b_num) {
271 return a_num ? std::strong_ordering::less : std::strong_ordering::greater;
272 } else {
273 if (const auto c = ida <=> idb; c != 0) {
274 return c;
275 }
276 }
277
278 ai = (ad == std::string_view::npos) ? a.size() : ad + 1;
279 bi = (bd == std::string_view::npos) ? b.size() : bd + 1;
280 }
281
282 // All shared identifiers equal: the longer list has higher precedence.
283 const bool a_more = ai < a.size();
284 if (const bool b_more = bi < b.size(); a_more == b_more) {
285 return std::strong_ordering::equal;
286 }
287 return a_more ? std::strong_ordering::greater : std::strong_ordering::less;
288 }
289};
290
291// ---------------------------------------------------------------------------
292// Text output: to_string + std::ostream insertion. (std::format support is the
293// std::formatter specialization below, outside namespace comms.)
294// ---------------------------------------------------------------------------
295
297[[nodiscard]] inline std::string to_string(const SemVer& v) {
298 return v.to_string();
299}
300
301inline std::ostream& operator<<(std::ostream& os, const SemVer& v) {
302 return os << v.to_string();
303}
304
305} // namespace comms
306
307// ---------------------------------------------------------------------------
308// std::format and std::hash support. The specializations live in namespace std
309// (the primary templates are visible), like nlohmann's adl_serializer route in
310// json.hpp.
311// ---------------------------------------------------------------------------
312
313// This spec-less formatter reads no member state, but `std::formatter` requires
314// `parse`/`format` to be non-static members — so silence the convert-to-static
315// suggestion here.
316// NOLINTBEGIN(readability-convert-member-functions-to-static)
317
319template <>
320struct std::formatter<comms::SemVer> {
321 constexpr auto parse(const std::format_parse_context& ctx) {
322 const auto* it = ctx.begin();
323 if (it != ctx.end() && *it != '}') {
324 throw std::format_error("commons: SemVer takes no format spec");
325 }
326 return it;
327 }
328
329 auto format(const comms::SemVer& v, std::format_context& ctx) const {
330 return std::format_to(ctx.out(), "{}", comms::to_string(v));
331 }
332};
333
334// NOLINTEND(readability-convert-member-functions-to-static)
335
338template <>
339struct std::hash<comms::SemVer> {
340 [[nodiscard]] std::size_t operator()(const comms::SemVer& v) const noexcept {
341 std::size_t seed = 0;
342 const auto mix = [&seed](const std::size_t h) {
343 seed ^= h + 0x9e3779b9 + (seed << 6) + (seed >> 2);
344 };
345 mix(std::hash<comms::u32>{}(v.major));
346 mix(std::hash<comms::u32>{}(v.minor));
347 mix(std::hash<comms::u32>{}(v.patch));
348 mix(std::hash<std::string>{}(v.prerelease));
349 return seed;
350 }
351};
std::string to_string(const Color &c)
Color as its canonical hex string (#RRGGBB, or #RRGGBBAA when not opaque).
Definition color.hpp:1388
A semantic version: major.minor.patch with optional prerelease/build.
Definition semver.hpp:54
std::string prerelease
Part after '-', before '+' (no leading '-').
Definition semver.hpp:58
std::strong_ordering operator<=>(const SemVer &o) const
Total order over major, minor, patch, then prerelease per §11.
Definition semver.hpp:155
static std::optional< SemVer > parse(std::string_view s)
Non-throwing parse.
Definition semver.hpp:67
std::string to_string() const
The canonical string: major.minor.patch, then -prerelease if set, then +build if set.
Definition semver.hpp:139
bool operator==(const SemVer &o) const
Equality consistent with <=> (so it likewise ignores build metadata).
Definition semver.hpp:170
std::string build
Part after '+' (no leading '+').
Definition semver.hpp:59
Fixed-width numeric aliases shared across the C++ libraries.
std::uint32_t u32
Unsigned 32-bit integer.
Definition types.hpp:24
std::size_t usize
Unsigned size type (std::size_t).
Definition types.hpp:43
std::uint64_t u64
Unsigned 64-bit integer.
Definition types.hpp:25