plugify 1.0.0.0
Loading...
Searching...
No Matches
version.hpp
1#pragma once
2
3#include <cstddef>
4#include <cstdint>
5#include <iosfwd>
6#include <limits>
7#include <optional>
8#include <string>
9#include <string_view>
10#include <charconv>
11#include <compare>
12
13#ifndef PLUGIFY_STRING_NO_STD_FORMAT
14# include "compat_format.hpp"
15#endif
16
17#include "macro.hpp"
18
19// from https://github.com/Neargye/semver
20namespace plg {
21 enum struct prerelease : uint8_t {
22 alpha = 0,
23 beta = 1,
24 rc = 2,
25 none = 3
26 };
27
28 struct from_chars_result : std::from_chars_result {
29 constexpr operator bool() const noexcept { return ec == std::errc{}; }
30 };
31
32 struct to_chars_result : std::to_chars_result {
33 constexpr operator bool() const noexcept { return ec == std::errc{}; }
34 };
35
36 // Max version string length = 5(<major>) + 1(.) + 5(<minor>) + 1(.) + 5(<patch>) + 1(-) + 5(<prerelease>) + 1(.) + 5(<prereleaseversion>) = 29.
37 inline constexpr auto max_version_string_length = std::size_t{29};
38
39 namespace detail {
40 inline constexpr auto alpha = std::string_view{"alpha", 5};
41 inline constexpr auto beta = std::string_view{"beta", 4};
42 inline constexpr auto rc = std::string_view{"rc", 2};
43
44 // Min version string length = 1(<major>) + 1(.) + 1(<minor>) + 1(.) + 1(<patch>) = 5.
45 inline constexpr auto min_version_string_length = 5;
46
47 constexpr char to_lower(char c) noexcept {
48 return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + ('a' - 'A')) : c;
49 }
50
51 constexpr bool is_digit(char c) noexcept {
52 return c >= '0' && c <= '9';
53 }
54
55 constexpr bool is_space(char c) noexcept {
56 return c == ' ';
57 }
58
59 constexpr bool is_operator(char c) noexcept {
60 return c == '<' || c == '>' || c == '=';
61 }
62
63 constexpr bool is_dot(char c) noexcept {
64 return c == '.';
65 }
66
67 constexpr bool is_logical_or(char c) noexcept {
68 return c == '|';
69 }
70
71 constexpr bool is_hyphen(char c) noexcept {
72 return c == '-';
73 }
74
75 constexpr bool is_letter(char c) noexcept {
76 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
77 }
78
79 constexpr uint16_t to_digit(char c) noexcept {
80 return static_cast<uint16_t>(c - '0');
81 }
82
83 constexpr uint8_t length(uint16_t x) noexcept {
84 if (x < 10) {
85 return 1;
86 }
87 if (x < 100) {
88 return 2;
89 }
90 if (x < 1000) {
91 return 3;
92 }
93 if (x < 10000) {
94 return 4;
95 }
96 return 5;
97 }
98
99 constexpr uint8_t length(prerelease t) noexcept {
100 if (t == prerelease::alpha) {
101 return static_cast<uint8_t>(alpha.length());
102 } else if (t == prerelease::beta) {
103 return static_cast<uint8_t>(beta.length());
104 } else if (t == prerelease::rc) {
105 return static_cast<uint8_t>(rc.length());
106 }
107
108 return 0;
109 }
110
111 constexpr bool equals(const char* first, const char* last, std::string_view str) noexcept {
112 for (std::size_t i = 0; first != last && i < str.length(); ++i, ++first) {
113 if (to_lower(*first) != to_lower(str[i])) {
114 return false;
115 }
116 }
117
118 return true;
119 }
120
121 constexpr char* to_chars(char* str, uint16_t x, bool dot = true) noexcept {
122 do {
123 *(--str) = static_cast<char>('0' + (x % 10));
124 x /= 10;
125 } while (x != 0);
126
127 if (dot) {
128 *(--str) = '.';
129 }
130
131 return str;
132 }
133
134 constexpr char* to_chars(char* str, prerelease t) noexcept {
135 const auto p = t == prerelease::alpha
136 ? alpha
137 : t == prerelease::beta
138 ? beta
139 : t == prerelease::rc
140 ? rc
141 : std::string_view{};
142
143 if (!p.empty()) {
144 for (auto it = p.rbegin(); it != p.rend(); ++it) {
145 *(--str) = *it;
146 }
147 *(--str) = '-';
148 }
149
150 return str;
151 }
152
153 constexpr const char* from_chars(const char* first, const char* last, uint16_t& d) noexcept {
154 if (first != last && is_digit(*first)) {
155 int32_t t = 0;
156 for (; first != last && is_digit(*first); ++first) {
157 t = t * 10 + to_digit(*first);
158 }
159 if (t <= (std::numeric_limits<uint16_t>::max)()) {
160 d = static_cast<uint16_t>(t);
161 return first;
162 }
163 }
164
165 return nullptr;
166 }
167
168 constexpr const char* from_chars(const char* first, const char* last, std::optional<uint16_t>& d) noexcept {
169 if (first != last && is_digit(*first)) {
170 int32_t t = 0;
171 for (; first != last && is_digit(*first); ++first) {
172 t = t * 10 + to_digit(*first);
173 }
174 if (t <= (std::numeric_limits<uint16_t>::max)()) {
175 d = static_cast<uint16_t>(t);
176 return first;
177 }
178 }
179
180 return nullptr;
181 }
182
183 constexpr const char* from_chars(const char* first, const char* last, prerelease& p) noexcept {
184 if (is_hyphen(*first)) {
185 ++first;
186 }
187
188 if (equals(first, last, alpha)) {
189 p = prerelease::alpha;
190 return first + alpha.length();
191 } else if (equals(first, last, beta)) {
192 p = prerelease::beta;
193 return first + beta.length();
194 } else if (equals(first, last, rc)) {
195 p = prerelease::rc;
196 return first + rc.length();
197 }
198
199 return nullptr;
200 }
201
202 constexpr bool check_delimiter(const char* first, const char* last, char d) noexcept {
203 return first != last && first != nullptr && *first == d;
204 }
205
206 template <typename T, typename = void>
208 static auto resize(T& str, std::size_t size) -> std::void_t<decltype(str.resize(size))> {
209 str.resize(size);
210 }
211 };
212
213 template <typename T>
214 struct resize_uninitialized<T, std::void_t<decltype(std::declval<T>().__resize_default_init(42))>> {
215 static void resize(T& str, std::size_t size) {
216 str.__resize_default_init(size);
217 }
218 };
219
220 } // namespace plg::detail
221
222 struct version {
223 uint16_t major = 0;
224 uint16_t minor = 1;
225 uint16_t patch = 0;
226 prerelease prerelease_type = prerelease::none;
227 std::optional<uint16_t> prerelease_number = std::nullopt;
228
229 constexpr version(uint16_t mj,
230 uint16_t mn,
231 uint16_t pt,
232 prerelease prt = prerelease::none,
233 std::optional<uint16_t> prn = std::nullopt) noexcept
234 : major{mj},
235 minor{mn},
236 patch{pt},
237 prerelease_type{prt},
238 prerelease_number{prt == prerelease::none ? std::nullopt : prn} {
239 }
240
241 constexpr version(uint16_t mj,
242 uint16_t mn,
243 uint16_t pt,
244 prerelease prt,
246 : major{mj},
247 minor{mn},
248 patch{pt},
249 prerelease_type{prt},
250 prerelease_number{prt == prerelease::none ? std::nullopt : std::make_optional<uint16_t>(prn)} {
251 }
252
253 explicit constexpr version(std::string_view str) : version(0, 0, 0, prerelease::none, std::nullopt) {
254 from_string(str);
255 }
256
257 constexpr version() = default; // https://semver.org/#how-should-i-deal-with-revisions-in-the-0yz-initial-development-phase
258
259 constexpr version(const version&) = default;
260
261 constexpr version(version&&) = default;
262
263 ~version() = default;
264
265 version& operator=(const version&) = default;
266
267 version& operator=(version&&) = default;
268
269 constexpr from_chars_result from_chars(const char* first, const char* last) noexcept {
270 if (first == nullptr || last == nullptr || (last - first) < detail::min_version_string_length) {
271 return {first, std::errc::invalid_argument};
272 }
273
274 auto next = first;
275 if (next = detail::from_chars(next, last, major); detail::check_delimiter(next, last, '.')) {
276 if (next = detail::from_chars(++next, last, minor); detail::check_delimiter(next, last, '.')) {
277 if (next = detail::from_chars(++next, last, patch); next == last) {
278 prerelease_type = prerelease::none;
279 prerelease_number = {};
280 return {next, std::errc{}};
281 } else if (detail::check_delimiter(next, last, '-')) {
282 if (next = detail::from_chars(next, last, prerelease_type); next == last) {
283 prerelease_number = {};
284 return {next, std::errc{}};
285 } else if (detail::check_delimiter(next, last, '.')) {
286 if (next = detail::from_chars(++next, last, prerelease_number); next == last) {
287 return {next, std::errc{}};
288 }
289 }
290 }
291 }
292 }
293
294 return {first, std::errc::invalid_argument};
295 }
296
297 constexpr to_chars_result to_chars(char* first, char* last) const noexcept {
298 const auto length = string_length();
299 if (first == nullptr || last == nullptr || (last - first) < length) {
300 return {last, std::errc::value_too_large};
301 }
302
303 auto next = first + length;
304 if (prerelease_type != prerelease::none) {
305 if (prerelease_number.has_value()) {
306 next = detail::to_chars(next, prerelease_number.value());
307 }
308 next = detail::to_chars(next, prerelease_type);
309 }
310 next = detail::to_chars(next, patch);
311 next = detail::to_chars(next, minor);
312 next = detail::to_chars(next, major, false);
313
314 return {first + length, std::errc{}};
315 }
316
317 constexpr bool from_string_noexcept(std::string_view str) noexcept {
318 return from_chars(str.data(), str.data() + str.length());
319 }
320
321 constexpr version& from_string(std::string_view str) {
322 PLUGIFY_ASSERT(from_string_noexcept(str), "plg::version::from_string invalid version.", std::invalid_argument);
323 return *this;
324 }
325
326 std::string to_string_noexcept() const noexcept {
327 std::string str{};
329 if (to_chars(str.data(), str.data() + str.length())) {
330 return str;
331 }
332 return {};
333 }
334
335 std::string to_string() const {
336 std::string str{};
338 PLUGIFY_ASSERT(to_chars(str.data(), str.data() + str.length()), "plg::version::to_string invalid version.", std::invalid_argument);
339 return str;
340 }
341
342 constexpr uint8_t string_length() const noexcept {
343 // (<major>) + 1(.) + (<minor>) + 1(.) + (<patch>)
344 auto length = detail::length(major) + detail::length(minor) + detail::length(patch) + 2;
345 if (prerelease_type != prerelease::none) {
346 // + 1(-) + (<prerelease>)
347 length += detail::length(prerelease_type) + 1;
348 if (prerelease_number.has_value()) {
349 // + 1(.) + (<prereleaseversion>)
350 length += detail::length(prerelease_number.value()) + 1;
351 }
352 }
353
354 return static_cast<uint8_t>(length);
355 }
356
357 constexpr int compare(const version& other) const noexcept {
358 if (major != other.major) {
359 return major - other.major;
360 }
361
362 if (minor != other.minor) {
363 return minor - other.minor;
364 }
365
366 if (patch != other.patch) {
367 return patch - other.patch;
368 }
369
370 if (prerelease_type != other.prerelease_type) {
371 return static_cast<uint8_t>(prerelease_type) - static_cast<uint8_t>(other.prerelease_type);
372 }
373
374 if (prerelease_number.has_value()) {
375 if (other.prerelease_number.has_value()) {
376 return prerelease_number.value() - other.prerelease_number.value();
377 }
378 return 1;
379 } else if (other.prerelease_number.has_value()) {
380 return -1;
381 }
382
383 return 0;
384 }
385 };
386
387 constexpr bool operator==(const version& lhs, const version& rhs) noexcept {
388 return lhs.compare(rhs) == 0;
389 }
390
391 constexpr bool operator!=(const version& lhs, const version& rhs) noexcept {
392 return lhs.compare(rhs) != 0;
393 }
394
395 constexpr bool operator>(const version& lhs, const version& rhs) noexcept {
396 return lhs.compare(rhs) > 0;
397 }
398
399 constexpr bool operator>=(const version& lhs, const version& rhs) noexcept {
400 return lhs.compare(rhs) >= 0;
401 }
402
403 constexpr bool operator<(const version& lhs, const version& rhs) noexcept {
404 return lhs.compare(rhs) < 0;
405 }
406
407 constexpr bool operator<=(const version& lhs, const version& rhs) noexcept {
408 return lhs.compare(rhs) <= 0;
409 }
410
411#if __cpp_impl_three_way_comparison
412 constexpr std::strong_ordering operator<=>(const version& lhs, const version& rhs) {
413 int compare = lhs.compare(rhs);
414 if (compare == 0)
415 return std::strong_ordering::equal;
416 if (compare > 0)
417 return std::strong_ordering::greater;
418 return std::strong_ordering::less;
419 }
420#endif // __cpp_impl_three_way_comparison
421
422 constexpr version operator""_version(const char* str, std::size_t length) {
423 return version{std::string_view{str, length}};
424 }
425
426 constexpr bool valid(std::string_view str) noexcept {
427 return version{}.from_string_noexcept(str);
428 }
429
430 constexpr from_chars_result from_chars(const char* first, const char* last, version& v) noexcept {
431 return v.from_chars(first, last);
432 }
433
434 constexpr to_chars_result to_chars(char* first, char* last, const version& v) noexcept {
435 return v.to_chars(first, last);
436 }
437
438 constexpr std::optional<version> from_string_noexcept(std::string_view str) noexcept {
439 if (version v{}; v.from_string_noexcept(str)) {
440 return v;
441 }
442
443 return std::nullopt;
444 }
445
446 constexpr version from_string(std::string_view str) {
447 return version{str};
448 }
449
450 inline std::string to_string(const version& v) {
451 return v.to_string();
452 }
453
454 template <typename Char, typename Traits>
455 inline std::basic_ostream<Char, Traits>& operator<<(std::basic_ostream<Char, Traits>& os, const version& v) {
456 for (const auto c : v.to_string()) {
457 os.put(c);
458 }
459
460 return os;
461 }
462
463 inline namespace comparators {
464 enum struct comparators_option : uint8_t {
465 exclude_prerelease,
466 include_prerelease
467 };
468
469 constexpr int compare(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept {
470 if (option == comparators_option::exclude_prerelease) {
471 return version{lhs.major, lhs.minor, lhs.patch}.compare(version{rhs.major, rhs.minor, rhs.patch});
472 }
473 return lhs.compare(rhs);
474 }
475
476 constexpr bool equal_to(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept {
477 return compare(lhs, rhs, option) == 0;
478 }
479
480 constexpr bool not_equal_to(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept {
481 return compare(lhs, rhs, option) != 0;
482 }
483
484 constexpr bool greater(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept {
485 return compare(lhs, rhs, option) > 0;
486 }
487
488 constexpr bool greater_equal(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept {
489 return compare(lhs, rhs, option) >= 0;
490 }
491
492 constexpr bool less(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept {
493 return compare(lhs, rhs, option) < 0;
494 }
495
496 constexpr bool less_equal(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept {
497 return compare(lhs, rhs, option) <= 0;
498 }
499 } // namespace plg::comparators
500
501 namespace range {
502 namespace detail {
503 using namespace plg::detail;
504
505 class range {
506 public:
507 constexpr explicit range(std::string_view str) noexcept : parser{str} {}
508
509 constexpr bool satisfies(const version& ver, bool include_prerelease) {
510 const bool has_prerelease = ver.prerelease_type != prerelease::none;
511
512 do {
513 if (is_logical_or_token()) {
514 parser.advance_token(range_token_type::logical_or);
515 }
516
517 bool contains = true;
518 bool allow_compare = include_prerelease;
519
520 while (is_operator_token() || is_number_token()) {
521 const auto range_comparison = parser.parse_range();
522 const bool equal_without_tags = equal_to(range_comparison.ver, ver, comparators_option::exclude_prerelease);
523
525 allow_compare = true;
526 }
527
528 if (!range_comparison.satisfies(ver)) {
529 contains = false;
530 break;
531 }
532 }
533
534 if (has_prerelease) {
535 if (allow_compare && contains) {
536 return true;
537 }
538 } else if (contains) {
539 return true;
540 }
541
542 } while (is_logical_or_token());
543
544 return false;
545 }
546
547 private:
548 enum struct range_operator : uint8_t {
549 less,
550 less_or_equal,
551 greater,
552 greater_or_equal,
553 equal
554 };
555
556 struct range_comparator {
557 range_operator op;
558 version ver;
559
560 constexpr bool satisfies(const version& version) const {
561 switch (op) {
562 case range_operator::equal:
563 return version == ver;
564 case range_operator::greater:
565 return version > ver;
566 case range_operator::greater_or_equal:
567 return version >= ver;
568 case range_operator::less:
569 return version < ver;
570 case range_operator::less_or_equal:
571 return version <= ver;
572 default:
573 PLUGIFY_ASSERT(false, "plg::range unexpected operator.", std::invalid_argument);
574 }
575 }
576 };
577
578 enum struct range_token_type : uint8_t {
579 none,
580 number,
581 range_operator,
582 dot,
583 logical_or,
584 hyphen,
585 prerelease,
586 end_of_line
587 };
588
589 struct range_token {
590 range_token_type type = range_token_type::none;
591 uint16_t number = 0;
592 range_operator op = range_operator::equal;
593 prerelease prerelease_type = prerelease::none;
594 };
595
596 struct range_lexer {
597 std::string_view text;
598 std::size_t pos;
599
600 constexpr explicit range_lexer(std::string_view _text) noexcept : text{_text}, pos{0} {}
601
602 constexpr range_token get_next_token() noexcept {
603 while (!end_of_line()) {
604
605 if (is_space(text[pos])) {
606 advance(1);
607 continue;
608 }
609
610 if (is_logical_or(text[pos])) {
611 advance(2);
612 return {range_token_type::logical_or};
613 }
614
615 if (is_operator(text[pos])) {
616 const auto op = get_operator();
617 return {range_token_type::range_operator, 0, op};
618 }
619
620 if (is_digit(text[pos])) {
621 const auto number = get_number();
622 return {range_token_type::number, number};
623 }
624
625 if (is_dot(text[pos])) {
626 advance(1);
627 return {range_token_type::dot};
628 }
629
630 if (is_hyphen(text[pos])) {
631 advance(1);
632 return {range_token_type::hyphen};
633 }
634
635 if (is_letter(text[pos])) {
636 const auto prerelease = get_prerelease();
637 return {range_token_type::prerelease, 0, range_operator::equal, prerelease};
638 }
639 }
640
641 return {range_token_type::end_of_line};
642 }
643
644 constexpr bool end_of_line() const noexcept { return pos >= text.length(); }
645
646 constexpr void advance(std::size_t i) noexcept {
647 pos += i;
648 }
649
650 constexpr range_operator get_operator() noexcept {
651 if (text[pos] == '<') {
652 advance(1);
653 if (text[pos] == '=') {
654 advance(1);
655 return range_operator::less_or_equal;
656 }
657 return range_operator::less;
658 } else if (text[pos] == '>') {
659 advance(1);
660 if (text[pos] == '=') {
661 advance(1);
662 return range_operator::greater_or_equal;
663 }
664 return range_operator::greater;
665 } else if (text[pos] == '=') {
666 advance(1);
667 return range_operator::equal;
668 }
669
670 return range_operator::equal;
671 }
672
673 constexpr uint16_t get_number() noexcept {
674 const auto first = text.data() + pos;
675 const auto last = text.data() + text.length();
676 if (uint16_t n{}; from_chars(first, last, n) != nullptr) {
677 advance(length(n));
678 return n;
679 }
680
681 return 0;
682 }
683
684 constexpr prerelease get_prerelease() noexcept {
685 const auto first = text.data() + pos;
686 const auto last = text.data() + text.length();
687 if (first > last) {
688 advance(1);
689 return prerelease::none;
690 }
691
692 if (prerelease p{}; from_chars(first, last, p) != nullptr) {
693 advance(length(p));
694 return p;
695 }
696
697 advance(1);
698
699 return prerelease::none;
700 }
701 };
702
703 struct range_parser {
704 range_lexer lexer;
705 range_token current_token;
706
707 constexpr explicit range_parser(std::string_view str) : lexer{str}, current_token{range_token_type::none} {
708 advance_token(range_token_type::none);
709 }
710
711 constexpr void advance_token(range_token_type token_type) {
712 PLUGIFY_ASSERT(current_token.type == token_type, "plg::range unexpected token.", std::invalid_argument);
713 current_token = lexer.get_next_token();
714 }
715
716 constexpr range_comparator parse_range() {
717 if (current_token.type == range_token_type::number) {
718 const auto version = parse_version();
719 return {range_operator::equal, version};
720 } else if (current_token.type == range_token_type::range_operator) {
721 const auto range_operator = current_token.op;
722 advance_token(range_token_type::range_operator);
723 const auto version = parse_version();
724 return {range_operator, version};
725 }
726
727 return {range_operator::equal, version{}};
728 }
729
730 constexpr version parse_version() {
731 const auto major = parse_number();
732
733 advance_token(range_token_type::dot);
734 const auto minor = parse_number();
735
736 advance_token(range_token_type::dot);
737 const auto patch = parse_number();
738
739 prerelease prerelease = prerelease::none;
740 std::optional<uint16_t> prerelease_number = std::nullopt;
741
742 if (current_token.type == range_token_type::hyphen) {
743 advance_token(range_token_type::hyphen);
744 prerelease = parse_prerelease();
745 if (current_token.type == range_token_type::dot) {
746 advance_token(range_token_type::dot);
747 prerelease_number = parse_number();
748 }
749 }
750
751 return {major, minor, patch, prerelease, prerelease_number};
752 }
753
754 constexpr uint16_t parse_number() {
755 const auto token = current_token;
756 advance_token(range_token_type::number);
757
758 return token.number;
759 }
760
761 constexpr prerelease parse_prerelease() {
762 const auto token = current_token;
763 advance_token(range_token_type::prerelease);
764
765 return token.prerelease_type;
766 }
767 };
768
769 constexpr bool is_logical_or_token() const noexcept {
770 return parser.current_token.type == range_token_type::logical_or;
771 }
772
773 constexpr bool is_operator_token() const noexcept {
774 return parser.current_token.type == range_token_type::range_operator;
775 }
776
777 constexpr bool is_number_token() const noexcept {
778 return parser.current_token.type == range_token_type::number;
779 }
780
781 range_parser parser;
782 };
783
784 } // namespace plg::range::detail
785
786 enum struct satisfies_option : uint8_t {
787 exclude_prerelease,
788 include_prerelease
789 };
790
791 constexpr bool satisfies(const version& ver, std::string_view str, satisfies_option option = satisfies_option::exclude_prerelease) {
792 switch (option) {
793 case satisfies_option::exclude_prerelease:
794 return detail::range{str}.satisfies(ver, false);
795 case satisfies_option::include_prerelease:
796 return detail::range{str}.satisfies(ver, true);
797 default:
798 PLUGIFY_ASSERT(false, "plg::range unexpected satisfies_option.", std::invalid_argument);
799 }
800 }
801 } // namespace plg::range
802} // namespace plg
803
804#ifndef PLUGIFY_STRING_NO_STD_FORMAT
805// format support
806#ifdef FMT_HEADER_ONLY
807namespace fmt {
808#else
809namespace std {
810#endif
811 template<>
812 struct formatter<plg::version> {
813 constexpr auto parse(std::format_parse_context& ctx) {
814 return ctx.begin();
815 }
816
817 template<class FormatContext>
818 auto format(const plg::version& ver, FormatContext& ctx) const {
819 return std::format_to(ctx.out(), "{}", ver.to_string_noexcept());
820 }
821 };
822}// namespace std
823#endif // PLUGIFY_STRING_NO_STD_FORMAT
Represents the absence of a value.
Definition any.hpp:10