userver: userver/utils/from_string.hpp Source File
Loading...
Searching...
No Matches
from_string.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/from_string.hpp
4/// @brief @copybrief utils::FromString
5/// @ingroup userver_universal
6
7#include <algorithm>
8#include <cctype>
9#include <cerrno>
10#include <charconv>
11#include <cstdint>
12#include <cstdlib>
13#include <string>
14#include <string_view>
15#include <type_traits>
16#include <typeinfo>
17
18#include <userver/utils/expected.hpp>
19#include <userver/utils/zstring_view.hpp>
20
21USERVER_NAMESPACE_BEGIN
22
23namespace utils {
24
25/// @brief Conversion error code.
27 /// @brief String contains leading whitespace characters.
29
30 /// @brief String contains invalid (non-digit) characters at the end.
32
33 /// @brief String does not contain a number.
35
36 /// @brief Conversion result is out of the valid range of the specified type.
38};
39
40/// @brief Converts @a code to string representation.
41constexpr inline std::string_view ToString(FromStringErrorCode code) noexcept {
42 switch (code) {
44 return "leading spaces are not allowed";
46 return "extra junk at the end of the string is not allowed";
48 return "no number found";
50 return "overflow";
51 default:
52 return "unknown";
53 }
54}
55
56/// @brief Function `utils::FromString` exception type.
57class FromStringException : public std::runtime_error {
58public:
59 /// @brief Creates exception for @a code .
60 FromStringException(FromStringErrorCode code, const std::string& what);
61
62 /// @brief Returns conversion error code.
63 FromStringErrorCode GetCode() const noexcept { return code_; }
64
65private:
67};
68
69namespace impl {
70
71template <typename T>
72concept IsFromCharsConvertible =
73 requires(T& v) { std::from_chars(std::declval<const char*>(), std::declval<const char*>(), v); } &&
74#if defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 13
75 // libstdc++ before 13.1 parse long double incorrectly
76 !std::same_as<T, long double>
77#else
78 true
79#endif
80 ;
81
82[[noreturn]] void ThrowFromStringException(
84 std::string_view input,
85 const std::type_info& result_type
86);
87
88template <typename T>
89requires(std::is_floating_point_v<T> && !IsFromCharsConvertible<T>)
90expected<T, FromStringErrorCode> FromString(utils::zstring_view str) noexcept {
91 static_assert(!std::is_const_v<T> && !std::is_volatile_v<T>);
92 static_assert(!std::is_reference_v<T>);
93
94 if (str.empty()) {
95 return unexpected{FromStringErrorCode::kNoNumber};
96 }
97 if (std::isspace(str.front())) {
98 return unexpected{FromStringErrorCode::kLeadingSpaces};
99 }
100 if (str.size() > 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
101 return unexpected{FromStringErrorCode::kTrailingJunk};
102 }
103
104 errno = 0;
105 char* end = nullptr;
106
107 const auto result = [&] {
108 if constexpr (std::is_same_v<T, float>) {
109 return std::strtof(str.c_str(), &end);
110 } else if constexpr (std::is_same_v<T, double>) {
111 return std::strtod(str.c_str(), &end);
112 } else if constexpr (std::is_same_v<T, long double>) {
113 return std::strtold(str.c_str(), &end);
114 }
115 }();
116
117 if (errno == ERANGE && !(result < 1 && result > 0.0)) {
118 return unexpected{FromStringErrorCode::kOverflow};
119 }
120
121 if (end == str.c_str()) {
122 return unexpected{FromStringErrorCode::kNoNumber};
123 }
124
125 if (end != str.data() + str.size()) {
126 return unexpected{FromStringErrorCode::kTrailingJunk};
127 }
128
129 return result;
130}
131
132template <typename T>
133requires(std::is_floating_point_v<T> && !IsFromCharsConvertible<T>)
134expected<T, FromStringErrorCode> FromString(const std::string& str) noexcept {
135 return impl::FromString<T>(utils::zstring_view{str});
136}
137
138template <typename T>
139requires(std::is_floating_point_v<T> && !IsFromCharsConvertible<T>)
140expected<T, FromStringErrorCode> FromString(const char* str) noexcept {
141 return impl::FromString<T>(utils::zstring_view{str});
142}
143
144template <typename T>
145requires(std::is_floating_point_v<T> && !IsFromCharsConvertible<T>)
146expected<T, FromStringErrorCode> FromString(std::string_view str) noexcept {
147 static constexpr std::size_t kSmallBufferSize = 32;
148
149 if (str.size() >= kSmallBufferSize) {
150 const std::string buffer{str};
151 return impl::FromString<T>(utils::zstring_view{buffer});
152 }
153
154 char buffer[kSmallBufferSize];
155 std::ranges::copy(str, buffer);
156 buffer[str.size()] = '\0';
157
158 return impl::FromString<T>(utils::zstring_view::UnsafeMake(buffer, str.size()));
159}
160
161template <IsFromCharsConvertible T>
162expected<T, FromStringErrorCode> FromString(std::string_view str) noexcept {
163 static_assert(!std::is_const_v<T> && !std::is_volatile_v<T>);
164 static_assert(!std::is_reference_v<T>);
165
166 if (str.empty()) {
167 return unexpected{FromStringErrorCode::kNoNumber};
168 }
169 if (std::isspace(str[0])) {
170 return unexpected{FromStringErrorCode::kLeadingSpaces};
171 }
172
173 std::size_t offset = 0;
174
175 // to allow leading plus
176 if (str.size() > 1 && str[0] == '+' && str[1] == '-') {
177 return unexpected{FromStringErrorCode::kNoNumber};
178 }
179 if (str[0] == '+') {
180 offset = 1;
181 }
182
183 // to process '-0' correctly
184 if (std::is_unsigned_v<T> && str[0] == '-') {
185 offset = 1;
186 }
187
188 T result{};
189 const auto [end, error_code] = std::from_chars(str.data() + offset, str.data() + str.size(), result);
190
191 if (error_code == std::errc::result_out_of_range) {
192 return unexpected{FromStringErrorCode::kOverflow};
193 }
194 if (error_code == std::errc::invalid_argument) {
195 return unexpected{FromStringErrorCode::kNoNumber};
196 }
197
198 if (std::is_unsigned_v<T> && str[0] == '-' && result != 0) {
199 return unexpected{FromStringErrorCode::kOverflow};
200 }
201
202 if (end != str.data() + str.size()) {
203 return unexpected{FromStringErrorCode::kTrailingJunk};
204 }
205
206 return result;
207}
208
209} // namespace impl
210
211/// @brief Extract the number contained in the string. No space characters or
212/// other extra characters allowed. Supported types:
213///
214/// - Integer types. Leading plus or minus is allowed. The number is always
215/// base-10.
216/// - Floating-point types. The accepted number format is identical to
217/// `std::strtod`.
218///
219/// @tparam T The type of the number to be parsed
220/// @param str The string that contains the number
221/// @return The extracted number
222/// @throw FromStringException if the string does not contain an integer or
223/// floating-point number in the specified format, or the string contains extra
224/// junk, or the number does not fit into the provided type
225template <typename T, typename StringType>
226requires std::is_convertible_v<StringType, std::string_view>
227T FromString(const StringType& str) {
228 const auto result = impl::FromString<T>(str);
229
230 if (result) {
231 return result.value();
232 } else {
233 impl::ThrowFromStringException(result.error(), str, typeid(T));
234 }
235}
236
237/// @brief Extract the number contained in the string. No space characters or
238/// other extra characters allowed. Supported types:
239///
240/// - Integer types. Leading plus or minus is allowed. The number is always
241/// base-10.
242/// - Floating-point types. The accepted number format is identical to
243/// `std::strtod`.
244///
245/// @tparam T The type of the number to be parsed
246/// @param str The string that contains the number
247/// @return `utils::expected` with the conversion result or error code
248template <typename T, typename StringType>
249requires std::is_convertible_v<StringType, std::string_view>
250expected<T, FromStringErrorCode> FromStringNoThrow(const StringType& str) noexcept {
251 return impl::FromString<T>(str);
252}
253
254std::int64_t FromHexString(std::string_view str);
255
256} // namespace utils
257
258USERVER_NAMESPACE_END