Skip to content

Commit 2808331

Browse files
committed
conversion: add explicit, locale-free type conversion utilities
- implement strict string_view to int/bool/float/enum conversions - provide expected-based error handling with detailed diagnostics - add ASCII-only parsing helpers and trimming utilities - expose generic parse<T> facade and to_string helpers - include comprehensive smoke tests for all conversions
1 parent 30a10db commit 2808331

16 files changed

Lines changed: 1448 additions & 0 deletions
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#pragma once
2+
3+
#include <type_traits>
4+
#include <utility>
5+
6+
#if __has_include(<expected>)
7+
#include <expected>
8+
#define VIX_CONVERSION_HAS_STD_EXPECTED 1
9+
#else
10+
#define VIX_CONVERSION_HAS_STD_EXPECTED 0
11+
#endif
12+
13+
namespace vix::conversion
14+
{
15+
16+
/**
17+
* @brief expected<T, E> alias used by the conversion module.
18+
*
19+
* Policy:
20+
* - Prefer std::expected when available.
21+
* - Provide a small fallback when std::expected is not available.
22+
*
23+
* This type is used to avoid exceptions in conversion code.
24+
*/
25+
#if VIX_CONVERSION_HAS_STD_EXPECTED
26+
27+
template <typename T, typename E>
28+
using expected = std::expected<T, E>;
29+
30+
template <typename E>
31+
using unexpected = std::unexpected<E>;
32+
33+
#else
34+
35+
template <typename E>
36+
class unexpected
37+
{
38+
public:
39+
using error_type = E;
40+
41+
constexpr explicit unexpected(const E &e) : error_(e) {}
42+
constexpr explicit unexpected(E &&e) noexcept(std::is_nothrow_move_constructible_v<E>)
43+
: error_(std::move(e)) {}
44+
45+
[[nodiscard]] constexpr const E &error() const & noexcept { return error_; }
46+
[[nodiscard]] constexpr E &error() & noexcept { return error_; }
47+
[[nodiscard]] constexpr E &&error() && noexcept { return std::move(error_); }
48+
49+
private:
50+
E error_;
51+
};
52+
53+
template <typename T, typename E>
54+
class expected
55+
{
56+
public:
57+
using value_type = T;
58+
using error_type = E;
59+
60+
// Constructors
61+
constexpr expected(const T &v) : has_(true) { new (&storage_.value) T(v); }
62+
constexpr expected(T &&v) noexcept(std::is_nothrow_move_constructible_v<T>)
63+
: has_(true) { new (&storage_.value) T(std::move(v)); }
64+
65+
constexpr expected(const unexpected<E> &u) : has_(false) { new (&storage_.error) E(u.error()); }
66+
constexpr expected(unexpected<E> &&u) noexcept(std::is_nothrow_move_constructible_v<E>)
67+
: has_(false) { new (&storage_.error) E(std::move(u).error()); }
68+
69+
constexpr expected(const expected &o)
70+
: has_(o.has_)
71+
{
72+
if (has_)
73+
new (&storage_.value) T(o.storage_.value);
74+
else
75+
new (&storage_.error) E(o.storage_.error);
76+
}
77+
78+
constexpr expected(expected &&o) noexcept(
79+
std::is_nothrow_move_constructible_v<T> &&
80+
std::is_nothrow_move_constructible_v<E>)
81+
: has_(o.has_)
82+
{
83+
if (has_)
84+
new (&storage_.value) T(std::move(o.storage_.value));
85+
else
86+
new (&storage_.error) E(std::move(o.storage_.error));
87+
}
88+
89+
constexpr expected &operator=(const expected &o)
90+
{
91+
if (this == &o)
92+
return *this;
93+
destroy_();
94+
has_ = o.has_;
95+
if (has_)
96+
new (&storage_.value) T(o.storage_.value);
97+
else
98+
new (&storage_.error) E(o.storage_.error);
99+
return *this;
100+
}
101+
102+
constexpr expected &operator=(expected &&o) noexcept(
103+
std::is_nothrow_move_constructible_v<T> &&
104+
std::is_nothrow_move_constructible_v<E>)
105+
{
106+
if (this == &o)
107+
return *this;
108+
destroy_();
109+
has_ = o.has_;
110+
if (has_)
111+
new (&storage_.value) T(std::move(o.storage_.value));
112+
else
113+
new (&storage_.error) E(std::move(o.storage_.error));
114+
return *this;
115+
}
116+
117+
~expected() { destroy_(); }
118+
119+
// Observers
120+
[[nodiscard]] constexpr bool has_value() const noexcept { return has_; }
121+
[[nodiscard]] constexpr explicit operator bool() const noexcept { return has_; }
122+
123+
[[nodiscard]] constexpr T &value() & { return storage_.value; }
124+
[[nodiscard]] constexpr const T &value() const & { return storage_.value; }
125+
[[nodiscard]] constexpr T &&value() && { return std::move(storage_.value); }
126+
127+
[[nodiscard]] constexpr E &error() & { return storage_.error; }
128+
[[nodiscard]] constexpr const E &error() const & { return storage_.error; }
129+
[[nodiscard]] constexpr E &&error() && { return std::move(storage_.error); }
130+
131+
// Convenience
132+
[[nodiscard]] constexpr T value_or(T fallback) const
133+
{
134+
return has_ ? storage_.value : fallback;
135+
}
136+
137+
private:
138+
constexpr void destroy_() noexcept
139+
{
140+
if (has_)
141+
storage_.value.~T();
142+
else
143+
storage_.error.~E();
144+
}
145+
146+
union Storage
147+
{
148+
constexpr Storage() {}
149+
~Storage() {}
150+
151+
T value;
152+
E error;
153+
} storage_{};
154+
155+
bool has_{false};
156+
};
157+
158+
#endif // VIX_CONVERSION_HAS_STD_EXPECTED
159+
160+
/**
161+
* @brief Helper to build an unexpected error in a consistent namespace.
162+
*/
163+
template <typename E>
164+
[[nodiscard]] constexpr unexpected<E> make_unexpected(E err)
165+
{
166+
return unexpected<E>(std::move(err));
167+
}
168+
169+
} // namespace vix::conversion

include/vix/conversion/Parse.hpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#pragma once
2+
3+
#include <string_view>
4+
#include <type_traits>
5+
6+
#include <vix/conversion/ConversionError.hpp>
7+
#include <vix/conversion/Expected.hpp>
8+
#include <vix/conversion/ToBool.hpp>
9+
#include <vix/conversion/ToFloat.hpp>
10+
#include <vix/conversion/ToInt.hpp>
11+
#include <vix/conversion/ToEnum.hpp>
12+
13+
namespace vix::conversion
14+
{
15+
16+
/**
17+
* @brief Generic parse<T> for common scalar types.
18+
*
19+
* Supported:
20+
* - Integral types
21+
* - Floating point types
22+
* - bool
23+
*
24+
* For enums:
25+
* - use parse_enum<T>(input, mapping) or to_enum<T>(...)
26+
*/
27+
template <typename T>
28+
[[nodiscard]] inline expected<T, ConversionError>
29+
parse(std::string_view input) noexcept
30+
{
31+
if constexpr (std::is_same_v<T, bool>)
32+
{
33+
return to_bool(input);
34+
}
35+
else if constexpr (std::is_integral_v<T>)
36+
{
37+
return to_int<T>(input);
38+
}
39+
else if constexpr (std::is_floating_point_v<T>)
40+
{
41+
return to_float<T>(input);
42+
}
43+
else
44+
{
45+
static_assert(!std::is_same_v<T, T>,
46+
"vix::conversion::parse<T>: unsupported type. "
47+
"Supported: integral, floating point, bool. "
48+
"For enums, use parse_enum<T>(input, mapping).");
49+
}
50+
}
51+
52+
/**
53+
* @brief Parse an enum using an explicit mapping table.
54+
*/
55+
template <typename Enum>
56+
[[nodiscard]] inline expected<Enum, ConversionError>
57+
parse_enum(
58+
std::string_view input,
59+
const EnumEntry<Enum> *entries,
60+
std::size_t count,
61+
bool case_insensitive = true) noexcept
62+
{
63+
return to_enum<Enum>(input, entries, count, case_insensitive);
64+
}
65+
66+
/**
67+
* @brief Helper overload for static arrays.
68+
*/
69+
template <typename Enum, std::size_t N>
70+
[[nodiscard]] inline expected<Enum, ConversionError>
71+
parse_enum(
72+
std::string_view input,
73+
const EnumEntry<Enum> (&entries)[N],
74+
bool case_insensitive = true) noexcept
75+
{
76+
return to_enum<Enum>(input, entries, N, case_insensitive);
77+
}
78+
79+
} // namespace vix::conversion

include/vix/conversion/ToBool.hpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#pragma once
2+
3+
#include <string_view>
4+
5+
#include <vix/conversion/ConversionError.hpp>
6+
#include <vix/conversion/Expected.hpp>
7+
#include <vix/conversion/detail/Ascii.hpp>
8+
#include <vix/conversion/detail/Trim.hpp>
9+
10+
namespace vix::conversion
11+
{
12+
13+
namespace detail
14+
{
15+
16+
/**
17+
* @brief Case-insensitive ASCII equals for string_view.
18+
*/
19+
[[nodiscard]] constexpr bool iequals(std::string_view a, std::string_view b) noexcept
20+
{
21+
if (a.size() != b.size())
22+
{
23+
return false;
24+
}
25+
26+
for (std::size_t i = 0; i < a.size(); ++i)
27+
{
28+
if (vix::conversion::detail::to_lower(a[i]) != vix::conversion::detail::to_lower(b[i]))
29+
{
30+
return false;
31+
}
32+
}
33+
34+
return true;
35+
}
36+
37+
} // namespace detail
38+
39+
/**
40+
* @brief Convert a string_view to bool (strict).
41+
*
42+
* Accepted values (after trim, case-insensitive):
43+
* - true / false
44+
* - 1 / 0
45+
* - yes / no
46+
* - on / off
47+
*/
48+
[[nodiscard]] constexpr expected<bool, ConversionError>
49+
to_bool(std::string_view input) noexcept
50+
{
51+
const std::string_view s = vix::conversion::detail::trim(input);
52+
53+
if (s.empty())
54+
{
55+
return make_unexpected(ConversionError{
56+
ConversionErrorCode::EmptyInput, input});
57+
}
58+
59+
if (s == "1" || detail::iequals(s, "true") || detail::iequals(s, "yes") || detail::iequals(s, "on"))
60+
{
61+
return true;
62+
}
63+
64+
if (s == "0" || detail::iequals(s, "false") || detail::iequals(s, "no") || detail::iequals(s, "off"))
65+
{
66+
return false;
67+
}
68+
69+
return make_unexpected(ConversionError{
70+
ConversionErrorCode::InvalidBoolean, input});
71+
}
72+
73+
} // namespace vix::conversion

0 commit comments

Comments
 (0)