You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
5.0 KiB
149 lines
5.0 KiB
#pragma once |
|
|
|
#if __cplusplus >= 201703L |
|
#include <charconv> |
|
#include <system_error> |
|
#endif |
|
|
|
#include <cstdint> |
|
|
|
#include <expected.hpp> |
|
|
|
#include "utils/stdcompat/string_view.hpp" |
|
|
|
namespace devilution { |
|
|
|
enum class ParseIntError { |
|
ParseError = 1, |
|
OutOfRange |
|
}; |
|
|
|
template <typename IntT> |
|
using ParseIntResult = tl::expected<IntT, ParseIntError>; |
|
|
|
template <typename IntT> |
|
ParseIntResult<IntT> ParseInt( |
|
string_view str, IntT min = std::numeric_limits<IntT>::min(), |
|
IntT max = std::numeric_limits<IntT>::max(), const char **endOfParse = nullptr) |
|
{ |
|
#if __cplusplus >= 201703L |
|
IntT value; |
|
const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); |
|
if (endOfParse != nullptr) { |
|
*endOfParse = result.ptr; |
|
} |
|
if (result.ec == std::errc::invalid_argument) |
|
return tl::unexpected(ParseIntError::ParseError); |
|
if (result.ec == std::errc::result_out_of_range || value < min || value > max) |
|
return tl::unexpected(ParseIntError::OutOfRange); |
|
if (result.ec != std::errc()) |
|
return tl::unexpected(ParseIntError::ParseError); |
|
return value; |
|
#else |
|
if (str.empty()) { |
|
return tl::unexpected { ParseIntError::ParseError }; |
|
} |
|
long long result = 0; |
|
bool negative = false; |
|
if (str[0] == '-') { |
|
negative = true; |
|
str.remove_prefix(1); |
|
} |
|
if (str.empty()) { |
|
return tl::unexpected { ParseIntError::ParseError }; |
|
} |
|
// Fall back to a simple implementation: |
|
while (!str.empty()) { |
|
if (str[0] < '0' || str[0] > '9') { |
|
return tl::unexpected { ParseIntError::ParseError }; |
|
} |
|
result = result * 10 + (str[0] - '0'); |
|
str.remove_prefix(1); |
|
} |
|
if (negative) { |
|
result = -result; |
|
} |
|
if (result > max || result < min) { |
|
return tl::unexpected { ParseIntError::OutOfRange }; |
|
} |
|
return result; |
|
#endif |
|
} |
|
|
|
/** |
|
* @brief Parses a sequence of decimal characters into a 6 bit fixed point number in the range [0, 1.0] |
|
* @param str a potentially empty string of base 10 digits, optionally followed by non-digit characters |
|
* @param[out] endOfParse equivalent to std::from_chars_result::ptr, used to tell where parsing stopped |
|
* @return a value in the range [0, 64], representing a 2.6 fixed value in the range [0, 1.0] |
|
*/ |
|
uint8_t ParseFixed6Fraction(string_view str, const char **endOfParse = nullptr); |
|
|
|
template <typename IntT> |
|
ParseIntResult<IntT> ParseFixed6(string_view str, const char **endOfParse = nullptr) |
|
{ |
|
if (endOfParse != nullptr) { |
|
// To allow for early returns we set the end pointer to the start of the string, which is the common case for errors. |
|
*endOfParse = str.data(); |
|
} |
|
|
|
if (str.empty()) { |
|
return tl::unexpected { ParseIntError::ParseError }; |
|
} |
|
|
|
constexpr IntT minIntegerValue = std::numeric_limits<IntT>::min() >> 6; |
|
constexpr IntT maxIntegerValue = std::numeric_limits<IntT>::max() >> 6; |
|
|
|
const char *currentChar; // will be set by the call to parseInt |
|
ParseIntResult<IntT> integerParseResult = ParseInt(str, minIntegerValue, maxIntegerValue, ¤tChar); |
|
|
|
bool isNegative = std::is_signed_v<IntT> && str[0] == '-'; |
|
bool haveDigits = integerParseResult.has_value() || integerParseResult.error() == ParseIntError::OutOfRange; |
|
if (haveDigits) { |
|
str.remove_prefix(static_cast<size_t>(std::distance(str.data(), currentChar))); |
|
} else if (isNegative) { |
|
str.remove_prefix(1); |
|
} |
|
|
|
// if the string has no leading digits we still need to try parse the fraction part |
|
uint8_t fractionPart = 0; |
|
if (!str.empty() && str[0] == '.') { |
|
// got a fractional part to read too |
|
str.remove_prefix(1); // skip past the decimal point |
|
|
|
fractionPart = ParseFixed6Fraction(str, ¤tChar); |
|
haveDigits = haveDigits || str.data() != currentChar; |
|
} |
|
|
|
if (!haveDigits) { |
|
// early return in case we got a string like "-.abc", don't want to set the end pointer in this case |
|
return tl::unexpected { ParseIntError::ParseError }; |
|
} |
|
|
|
if (endOfParse != nullptr) { |
|
*endOfParse = currentChar; |
|
} |
|
|
|
if (!integerParseResult.has_value() && integerParseResult.error() == ParseIntError::OutOfRange) { |
|
// if the integer parsing gave us an out of range value then we've done a bit of unnecessary |
|
// work parsing the fraction part, but it saves duplicating code. |
|
return integerParseResult; |
|
} |
|
// integerParseResult could be a ParseError at this point because of a string like ".123" or "-.1" |
|
// so we need to default to 0 (and use the result of the minus sign check when it's relevant) |
|
IntT integerPart = integerParseResult.value_or(0); |
|
|
|
// rounding could give us a value of 64 for the fraction part (e.g. 0.993 rounds to 1.0) so we need to ensure this doesn't overflow |
|
if (fractionPart >= 64 && (integerPart >= maxIntegerValue || (std::is_signed_v<IntT> && integerPart <= minIntegerValue))) { |
|
return tl::unexpected { ParseIntError::OutOfRange }; |
|
} else { |
|
IntT fixedValue = integerPart << 6; |
|
if (isNegative) { |
|
fixedValue -= fractionPart; |
|
} else { |
|
fixedValue += fractionPart; |
|
} |
|
return fixedValue; |
|
} |
|
} |
|
|
|
} // namespace devilution
|
|
|