From 49215cbe382e6b66b3c7939e6714d119cde9f13f Mon Sep 17 00:00:00 2001 From: ephphatha Date: Sun, 27 Aug 2023 20:43:38 +1000 Subject: [PATCH 1/2] Use dedicated error enum for parsing failures --- Source/data/file.cpp | 14 +++++++------- Source/data/file.hpp | 2 +- Source/data/iterators.hpp | 35 ++++++++++++++++++++++++++--------- Source/playerdat.cpp | 8 ++++---- test/data_file_test.cpp | 18 +++++++++++++----- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/Source/data/file.cpp b/Source/data/file.cpp index 242f0d920a7..98aeeea8876 100644 --- a/Source/data/file.cpp +++ b/Source/data/file.cpp @@ -47,24 +47,24 @@ void DataFile::reportFatalError(Error code, std::string_view fileName) } } -void DataFile::reportFatalFieldError(std::errc code, std::string_view fileName, std::string_view fieldName, const DataFileField &field) +void DataFile::reportFatalFieldError(DataFileField::Error code, std::string_view fileName, std::string_view fieldName, const DataFileField &field) { switch (code) { - case std::errc::invalid_argument: + case DataFileField::Error::NotANumber: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} */ "Non-numeric value {0} for {1} in {2} at row {3} and column {4}")), field.currentValue(), fieldName, fileName, field.row(), field.column())); - case std::errc::result_out_of_range: + case DataFileField::Error::OutOfRange: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} */ "Out of range value {0} for {1} in {2} at row {3} and column {4}")), field.currentValue(), fieldName, fileName, field.row(), field.column())); - default: + case DataFileField::Error::InvalidValue: app_fatal(fmt::format(fmt::runtime(_( - /* TRANSLATORS: Fallback error message when an error occurs while parsing a data file and we can't determine the cause. Arguments are {file name}, {row/record number}, {column/field number} */ - "Unexpected error while reading {0} at row {1} and column {2}")), - fileName, field.row(), field.column())); + /* TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} */ + "Invalid value {0} for {1} in {2} at row {3} and column {4}")), + field.currentValue(), fieldName, fileName, field.row(), field.column())); } } diff --git a/Source/data/file.hpp b/Source/data/file.hpp index c2b67ee8950..f8eb4e2321e 100644 --- a/Source/data/file.hpp +++ b/Source/data/file.hpp @@ -75,7 +75,7 @@ class DataFile { static tl::expected load(std::string_view path); static void reportFatalError(Error code, std::string_view fileName); - static void reportFatalFieldError(std::errc code, std::string_view fileName, std::string_view fieldName, const DataFileField &field); + static void reportFatalFieldError(DataFileField::Error code, std::string_view fileName, std::string_view fieldName, const DataFileField &field); void resetHeader() { diff --git a/Source/data/iterators.hpp b/Source/data/iterators.hpp index 4bbf35e7907..fc0857c37e3 100644 --- a/Source/data/iterators.hpp +++ b/Source/data/iterators.hpp @@ -16,6 +16,26 @@ class DataFileField { unsigned column_; public: + enum class Error { + NotANumber, + OutOfRange, + InvalidValue + }; + + static tl::expected mapError(std::errc ec) + { + switch (ec) { + case std::errc(): + return {}; + case std::errc::result_out_of_range: + return tl::unexpected { Error::OutOfRange }; + case std::errc::invalid_argument: + return tl::unexpected { Error::NotANumber }; + default: + return tl::unexpected { Error::InvalidValue }; + } + } + DataFileField(GetFieldResult *state, const char *end, unsigned row, unsigned column) : state_(state) , end_(end) @@ -56,10 +76,10 @@ class DataFileField { * use with operator* or repeated calls to parseInt (even with different types). * @tparam T an Integral type supported by std::from_chars * @param destination value to store the result of successful parsing - * @return the error code from std::from_chars + * @return an error code corresponding to the from_chars result if parsing failed */ template - [[nodiscard]] std::errc parseInt(T &destination) + [[nodiscard]] tl::expected parseInt(T &destination) { std::from_chars_result result {}; if (state_->status == GetFieldResult::Status::ReadyToRead) { @@ -74,18 +94,15 @@ class DataFileField { } else { result = std::from_chars(state_->value.data(), end_, destination); } - return result.ec; + + return mapError(result.ec); } template - [[nodiscard]] tl::expected asInt() + [[nodiscard]] tl::expected asInt() { T value = 0; - auto parseIntResult = parseInt(value); - if (parseIntResult == std::errc()) { - return value; - } - return tl::unexpected { parseIntResult }; + return parseInt(value).map([value]() { return value; }); } /** diff --git a/Source/playerdat.cpp b/Source/playerdat.cpp index 99b93b960ee..0458516efa3 100644 --- a/Source/playerdat.cpp +++ b/Source/playerdat.cpp @@ -120,11 +120,11 @@ void ReloadExperienceData() case ExperienceColumn::Level: { auto parseIntResult = field.parseInt(level); - if (parseIntResult != std::errc()) { + if (!parseIntResult.has_value()) { if (*field == "MaxLevel") { skipRecord = true; } else { - DataFile::reportFatalFieldError(parseIntResult, filename, "Level", field); + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Level", field); } } } break; @@ -132,8 +132,8 @@ void ReloadExperienceData() case ExperienceColumn::Experience: { auto parseIntResult = field.parseInt(experience); - if (parseIntResult != std::errc()) { - DataFile::reportFatalFieldError(parseIntResult, filename, "Experience", field); + if (!parseIntResult.has_value()) { + DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Experience", field); } } break; diff --git a/test/data_file_test.cpp b/test/data_file_test.cpp index 3571486933e..ae261c59f7d 100644 --- a/test/data_file_test.cpp +++ b/test/data_file_test.cpp @@ -214,7 +214,11 @@ TEST(DataFileTest, ParseInt) DataFileField field = *fieldIt; uint8_t shortVal = 5; auto parseIntResult = field.parseInt(shortVal); - EXPECT_EQ(parseIntResult, std::errc::invalid_argument) << "Strings are not uint8_t values"; + if (parseIntResult.has_value()) { + ADD_FAILURE() << "Parsing a string as an int should not succeed"; + } else { + EXPECT_EQ(parseIntResult.error(), DataFileField::Error::NotANumber) << "Strings are not uint8_t values"; + } EXPECT_EQ(shortVal, 5) << "Value is not modified when parsing as uint8_t fails due to non-numeric fields"; EXPECT_EQ(*field, "Sample") << "Should be able to access the field value as a string after failure"; ++fieldIt; @@ -225,7 +229,7 @@ TEST(DataFileTest, ParseInt) field = *fieldIt; shortVal = 5; parseIntResult = field.parseInt(shortVal); - EXPECT_EQ(parseIntResult, std::errc()) << "Expected " << field << "to fit into a uint8_t variable"; + EXPECT_TRUE(parseIntResult.has_value()) << "Expected " << field << " to fit into a uint8_t variable"; EXPECT_EQ(shortVal, 145) << "Parsing should give the expected base 10 value"; EXPECT_EQ(*field, "145") << "Should be able to access the field value as a string even after parsing as an int"; ++fieldIt; @@ -235,11 +239,15 @@ TEST(DataFileTest, ParseInt) // Third field is a number too large for a uint8_t but that fits into an int value field = *fieldIt; parseIntResult = field.parseInt(shortVal); - EXPECT_EQ(parseIntResult, std::errc::result_out_of_range) << "A value too large to fit into a uint8_t variable should report an error"; + if (parseIntResult.has_value()) { + ADD_FAILURE() << "Parsing an int into a short variable should not succeed"; + } else { + EXPECT_EQ(parseIntResult.error(), DataFileField::Error::OutOfRange) << "A value too large to fit into a uint8_t variable should report an error"; + } EXPECT_EQ(shortVal, 145) << "Value is not modified when parsing as uint8_t fails due to out of range value"; int longVal = 42; parseIntResult = field.parseInt(longVal); - EXPECT_EQ(parseIntResult, std::errc()) << "Expected " << field << "to fit into an int variable"; + EXPECT_TRUE(parseIntResult.has_value()) << "Expected " << field << " to fit into an int variable"; EXPECT_EQ(longVal, 70322) << "Value is expected to be parsed into a larger type after an out of range failure"; EXPECT_EQ(*field, "70322") << "Should be able to access the field value as a string after parsing as an int"; ++fieldIt; @@ -249,7 +257,7 @@ TEST(DataFileTest, ParseInt) // Fourth field is not an integer, but a value that starts with one or more digits that fit into an uint8_t value field = *fieldIt; parseIntResult = field.parseInt(shortVal); - EXPECT_EQ(parseIntResult, std::errc()) << "Expected " << field << "to fit into a uint8_t variable (even though it's not really an int)"; + EXPECT_TRUE(parseIntResult.has_value()) << "Expected " << field << " to fit into a uint8_t variable (even though it's not really an int)"; EXPECT_EQ(shortVal, 6) << "Value is loaded as expected until the first non-digit character"; EXPECT_EQ(*field, "6.34") << "Should be able to access the field value as a string after failure"; } From 62eff65528d2825f549cd179a65b6d52e9f8d85a Mon Sep 17 00:00:00 2001 From: ephphatha Date: Sun, 17 Sep 2023 12:44:04 +1000 Subject: [PATCH 2/2] Add support for parsing fixed point decimal strings --- Source/data/iterators.hpp | 54 +++++++++++ Source/utils/parse_int.hpp | 101 ++++++++++++++++++++- test/CMakeLists.txt | 1 + test/data_file_test.cpp | 31 ++++++- test/fixtures/txtdata/sample.tsv | 4 +- test/parse_int_test.cpp | 151 +++++++++++++++++++++++++++++++ 6 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 test/parse_int_test.cpp diff --git a/Source/data/iterators.hpp b/Source/data/iterators.hpp index fc0857c37e3..c81745d892b 100644 --- a/Source/data/iterators.hpp +++ b/Source/data/iterators.hpp @@ -6,6 +6,7 @@ #include #include "parser.hpp" +#include "utils/parse_int.hpp" namespace devilution { @@ -36,6 +37,18 @@ class DataFileField { } } + static tl::expected mapError(ParseIntError ec) + { + switch (ec) { + case ParseIntError::OutOfRange: + return tl::unexpected { Error::OutOfRange }; + case ParseIntError::ParseError: + return tl::unexpected { Error::NotANumber }; + default: + return tl::unexpected { Error::InvalidValue }; + } + } + DataFileField(GetFieldResult *state, const char *end, unsigned row, unsigned column) : state_(state) , end_(end) @@ -105,6 +118,47 @@ class DataFileField { return parseInt(value).map([value]() { return value; }); } + /** + * @brief Attempts to parse the current field as a fixed point value with 6 bits for the fraction + * + * You can freely interleave this method with calls to operator*. If this is the first value + * access since the last advance this will scan the current field and store it for later + * use with operator* or repeated calls to parseInt/Fixed6 (even with different types). + * @tparam T an Integral type supported by std::from_chars + * @param destination value to store the result of successful parsing + * @return an error code equivalent to what you'd get from from_chars if parsing failed + */ + template + [[nodiscard]] tl::expected parseFixed6(T &destination) + { + ParseIntResult parseResult; + if (state_->status == GetFieldResult::Status::ReadyToRead) { + const char *begin = state_->next; + // first read, consume digits + parseResult = ParseFixed6({ begin, static_cast(end_ - begin) }, &state_->next); + // then read the remainder of the field + *state_ = GetNextField(state_->next, end_); + // and prepend what was already parsed + state_->value = { begin, (state_->value.data() - begin) + state_->value.size() }; + } else { + parseResult = ParseFixed6(state_->value); + } + + if (parseResult.has_value()) { + destination = parseResult.value(); + return {}; + } else { + return mapError(parseResult.error()); + } + } + + template + [[nodiscard]] tl::expected asFixed6() + { + T value = 0; + return parseFixed6(value).map([value]() { return value; }); + } + /** * Returns the current row number */ diff --git a/Source/utils/parse_int.hpp b/Source/utils/parse_int.hpp index 5ccaa6d19b5..2dc728f253e 100644 --- a/Source/utils/parse_int.hpp +++ b/Source/utils/parse_int.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -19,10 +20,13 @@ using ParseIntResult = tl::expected; template ParseIntResult ParseInt( std::string_view str, IntT min = std::numeric_limits::min(), - IntT max = std::numeric_limits::max()) + IntT max = std::numeric_limits::max(), const char **endOfParse = nullptr) { 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) @@ -32,4 +36,99 @@ ParseIntResult ParseInt( return value; } +inline uint8_t ParseFixed6Fraction(std::string_view str, const char **endOfParse = nullptr) +{ + unsigned numDigits = 0; + uint32_t decimalFraction = 0; + + // Read at most 7 digits, at that threshold we're able to determine an exact rounding for 6 bit fixed point numbers + while (!str.empty() && numDigits < 7) { + if (str[0] < '0' || str[0] > '9') { + break; + } + decimalFraction = decimalFraction * 10 + str[0] - '0'; + ++numDigits; + str.remove_prefix(1); + } + if (endOfParse != nullptr) { + // to mimic the behaviour of std::from_chars consume all remaining digits in case the value was overly precise. + *endOfParse = std::find_if_not(str.data(), str.data() + str.size(), [](char character) { return character >= '0' && character <= '9'; }); + } + // to ensure rounding to nearest we normalise all values to 7 decimal places + while (numDigits < 7) { + decimalFraction *= 10; + ++numDigits; + } + // we add half the step between representable values to use integer truncation as a substitute for rounding to nearest. + return (decimalFraction + 78125) / 156250; +} + +template +ParseIntResult ParseFixed6(std::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::min() >> 6; + constexpr IntT maxIntegerValue = std::numeric_limits::max() >> 6; + + const char *currentChar; // will be set by the call to parseInt + ParseIntResult integerParseResult = ParseInt(str, minIntegerValue, maxIntegerValue, ¤tChar); + + bool isNegative = std::is_signed_v && str[0] == '-'; + bool haveDigits = integerParseResult.has_value() || integerParseResult.error() == ParseIntError::OutOfRange; + if (haveDigits) { + str.remove_prefix(static_cast(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 && integerPart <= minIntegerValue))) { + return tl::unexpected { ParseIntError::OutOfRange }; + } else { + IntT fixedValue = integerPart << 6; + if (isNegative) { + fixedValue -= fractionPart; + } else { + fixedValue += fractionPart; + } + return fixedValue; + } +} + } // namespace devilution diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 66cbcac0188..cf5266c0f04 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -32,6 +32,7 @@ set(tests missiles_test pack_test path_test + parse_int_test player_test quests_test random_test diff --git a/test/data_file_test.cpp b/test/data_file_test.cpp index ae261c59f7d..de3c2466700 100644 --- a/test/data_file_test.cpp +++ b/test/data_file_test.cpp @@ -232,6 +232,12 @@ TEST(DataFileTest, ParseInt) EXPECT_TRUE(parseIntResult.has_value()) << "Expected " << field << " to fit into a uint8_t variable"; EXPECT_EQ(shortVal, 145) << "Parsing should give the expected base 10 value"; EXPECT_EQ(*field, "145") << "Should be able to access the field value as a string even after parsing as an int"; + + int longVal = 1; + auto parseFixedResult = field.parseFixed6(longVal); + EXPECT_TRUE(parseFixedResult.has_value()) << "Expected " << field << " to be parsed as a fixed point integer wiith only the integer part"; + EXPECT_EQ(longVal, 145 << 6) << "Parsing should give the expected fixed point base 10 value"; + ++fieldIt; ASSERT_NE(fieldIt, end) << "sample.tsv must contain a third field to use as a test value for large ints"; @@ -245,7 +251,7 @@ TEST(DataFileTest, ParseInt) EXPECT_EQ(parseIntResult.error(), DataFileField::Error::OutOfRange) << "A value too large to fit into a uint8_t variable should report an error"; } EXPECT_EQ(shortVal, 145) << "Value is not modified when parsing as uint8_t fails due to out of range value"; - int longVal = 42; + longVal = 42; parseIntResult = field.parseInt(longVal); EXPECT_TRUE(parseIntResult.has_value()) << "Expected " << field << " to fit into an int variable"; EXPECT_EQ(longVal, 70322) << "Value is expected to be parsed into a larger type after an out of range failure"; @@ -259,7 +265,28 @@ TEST(DataFileTest, ParseInt) parseIntResult = field.parseInt(shortVal); EXPECT_TRUE(parseIntResult.has_value()) << "Expected " << field << " to fit into a uint8_t variable (even though it's not really an int)"; EXPECT_EQ(shortVal, 6) << "Value is loaded as expected until the first non-digit character"; - EXPECT_EQ(*field, "6.34") << "Should be able to access the field value as a string after failure"; + EXPECT_EQ(*field, "6.34") << "Should be able to access the field value as a string after parsing as an int"; + int fixedVal = 64; + parseFixedResult = field.parseFixed6(fixedVal); + EXPECT_TRUE(parseFixedResult.has_value()) << "Expected " << field << " to be parsed as a fixed point value"; + // 6.34 is parsed as 384 (6<<6) + 22 (0.34 rounds to 0.34375, 22/64) + EXPECT_EQ(fixedVal, 406) << "Value is loaded as a fixed point number"; + + uint8_t shortFixedVal = 32; + parseFixedResult = field.parseFixed6(shortFixedVal); + EXPECT_FALSE(parseFixedResult.has_value()) << "Expected " << field << " to fail to parse into a 2.6 fixed point variable"; + EXPECT_EQ(parseFixedResult.error(), DataFileField::Error::OutOfRange) << "A value too large to fit into a 2 bit integer part should report an error"; + EXPECT_EQ(shortFixedVal, 32) << "The variiable should not be modified when parsing fails"; + + ++fieldIt; + + ASSERT_NE(fieldIt, end) << "sample.tsv must contain a fifth field to use as a test value for fixed point overflow"; + + field = *fieldIt; + parseFixedResult = field.parseFixed6(shortFixedVal); + EXPECT_FALSE(parseFixedResult.has_value()) << "Expected " << field << " to fail to parse into a 2.6 fixed point variable"; + EXPECT_EQ(parseFixedResult.error(), DataFileField::Error::OutOfRange) << "A value that after rounding is too large to fit into a 2 bit integer part should report an error"; + EXPECT_EQ(shortFixedVal, 32) << "The variiable should not be modified when parsing fails"; } } diff --git a/test/fixtures/txtdata/sample.tsv b/test/fixtures/txtdata/sample.tsv index d2f308ef7b4..a56e0e15741 100644 --- a/test/fixtures/txtdata/sample.tsv +++ b/test/fixtures/txtdata/sample.tsv @@ -1,2 +1,2 @@ -String Byte Int Float -Sample 145 70322 6.34 +String Byte Int Float FloatOverflow +Sample 145 70322 6.34 3.999 diff --git a/test/parse_int_test.cpp b/test/parse_int_test.cpp new file mode 100644 index 00000000000..1a49b13c670 --- /dev/null +++ b/test/parse_int_test.cpp @@ -0,0 +1,151 @@ +#include + +#include "utils/parse_int.hpp" + +namespace devilution { +TEST(ParseIntTest, ParseInt) +{ + ParseIntResult result = ParseInt(""); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ParseIntError::ParseError); + + result = ParseInt("abcd"); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ParseIntError::ParseError); + + result = ParseInt("12"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), 12); + + result = ParseInt(("99999999"), -5, 100); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), ParseIntError::OutOfRange); + + ParseIntResult shortResult = ParseInt(("99999999")); + ASSERT_FALSE(shortResult.has_value()); + EXPECT_EQ(shortResult.error(), ParseIntError::OutOfRange); +} + +TEST(ParseIntTest, ParseFixed6Fraction) +{ + EXPECT_EQ(ParseFixed6Fraction(""), 0); + EXPECT_EQ(ParseFixed6Fraction("0"), 0); + EXPECT_EQ(ParseFixed6Fraction("00781249"), 0); + EXPECT_EQ(ParseFixed6Fraction("0078125"), 1); + EXPECT_EQ(ParseFixed6Fraction("015625"), 1); + EXPECT_EQ(ParseFixed6Fraction("03125"), 2); + EXPECT_EQ(ParseFixed6Fraction("046875"), 3); + EXPECT_EQ(ParseFixed6Fraction("0625"), 4); + EXPECT_EQ(ParseFixed6Fraction("078125"), 5); + EXPECT_EQ(ParseFixed6Fraction("09375"), 6); + EXPECT_EQ(ParseFixed6Fraction("109375"), 7); + EXPECT_EQ(ParseFixed6Fraction("125"), 8); + EXPECT_EQ(ParseFixed6Fraction("140625"), 9); + EXPECT_EQ(ParseFixed6Fraction("15625"), 10); + EXPECT_EQ(ParseFixed6Fraction("171875"), 11); + EXPECT_EQ(ParseFixed6Fraction("1875"), 12); + EXPECT_EQ(ParseFixed6Fraction("203125"), 13); + EXPECT_EQ(ParseFixed6Fraction("21875"), 14); + EXPECT_EQ(ParseFixed6Fraction("234375"), 15); + EXPECT_EQ(ParseFixed6Fraction("25"), 16); + EXPECT_EQ(ParseFixed6Fraction("265625"), 17); + EXPECT_EQ(ParseFixed6Fraction("28125"), 18); + EXPECT_EQ(ParseFixed6Fraction("296875"), 19); + EXPECT_EQ(ParseFixed6Fraction("3125"), 20); + EXPECT_EQ(ParseFixed6Fraction("328125"), 21); + EXPECT_EQ(ParseFixed6Fraction("34375"), 22); + EXPECT_EQ(ParseFixed6Fraction("359375"), 23); + EXPECT_EQ(ParseFixed6Fraction("375"), 24); + EXPECT_EQ(ParseFixed6Fraction("390625"), 25); + EXPECT_EQ(ParseFixed6Fraction("40625"), 26); + EXPECT_EQ(ParseFixed6Fraction("421875"), 27); + EXPECT_EQ(ParseFixed6Fraction("4375"), 28); + EXPECT_EQ(ParseFixed6Fraction("453125"), 29); + EXPECT_EQ(ParseFixed6Fraction("46875"), 30); + EXPECT_EQ(ParseFixed6Fraction("484375"), 31); + EXPECT_EQ(ParseFixed6Fraction("5"), 32); + EXPECT_EQ(ParseFixed6Fraction("515625"), 33); + EXPECT_EQ(ParseFixed6Fraction("53125"), 34); + EXPECT_EQ(ParseFixed6Fraction("546875"), 35); + EXPECT_EQ(ParseFixed6Fraction("5625"), 36); + EXPECT_EQ(ParseFixed6Fraction("578125"), 37); + EXPECT_EQ(ParseFixed6Fraction("59375"), 38); + EXPECT_EQ(ParseFixed6Fraction("609375"), 39); + EXPECT_EQ(ParseFixed6Fraction("625"), 40); + EXPECT_EQ(ParseFixed6Fraction("640625"), 41); + EXPECT_EQ(ParseFixed6Fraction("65625"), 42); + EXPECT_EQ(ParseFixed6Fraction("671875"), 43); + EXPECT_EQ(ParseFixed6Fraction("6875"), 44); + EXPECT_EQ(ParseFixed6Fraction("703125"), 45); + EXPECT_EQ(ParseFixed6Fraction("71875"), 46); + EXPECT_EQ(ParseFixed6Fraction("734375"), 47); + EXPECT_EQ(ParseFixed6Fraction("75"), 48); + EXPECT_EQ(ParseFixed6Fraction("765625"), 49); + EXPECT_EQ(ParseFixed6Fraction("78125"), 50); + EXPECT_EQ(ParseFixed6Fraction("796875"), 51); + EXPECT_EQ(ParseFixed6Fraction("8125"), 52); + EXPECT_EQ(ParseFixed6Fraction("828125"), 53); + EXPECT_EQ(ParseFixed6Fraction("84375"), 54); + EXPECT_EQ(ParseFixed6Fraction("859375"), 55); + EXPECT_EQ(ParseFixed6Fraction("875"), 56); + EXPECT_EQ(ParseFixed6Fraction("890625"), 57); + EXPECT_EQ(ParseFixed6Fraction("90625"), 58); + EXPECT_EQ(ParseFixed6Fraction("921875"), 59); + EXPECT_EQ(ParseFixed6Fraction("9375"), 60); + EXPECT_EQ(ParseFixed6Fraction("953125"), 61); + EXPECT_EQ(ParseFixed6Fraction("96875"), 62); + EXPECT_EQ(ParseFixed6Fraction("984375"), 63); + EXPECT_EQ(ParseFixed6Fraction("99218749"), 63); + EXPECT_EQ(ParseFixed6Fraction("9921875"), 64); +} + +TEST(ParseInt, ParseFixed6) +{ + ParseIntResult result = ParseFixed6(""); + ASSERT_FALSE(result.has_value()) << "Empty strings are not valid fixed point values."; + EXPECT_EQ(result.error(), ParseIntError::ParseError) << "ParseFixed6 should give a ParseError code when parsing an empty string."; + + result = ParseFixed6("abcd"); + ASSERT_FALSE(result.has_value()) << "Non-numeric strings should not be parsed as a fixed-point value."; + EXPECT_EQ(result.error(), ParseIntError::ParseError) << "ParseFixed6 should give a ParseError code when parsing a non-numeric string."; + + result = ParseFixed6("."); + ASSERT_FALSE(result.has_value()) << "To match std::from_chars ParseFixed6 should fail to parse a decimal string with no digits."; + EXPECT_EQ(result.error(), ParseIntError::ParseError) << "Decimal strings with no digits are reported as ParseError codes."; + + result = ParseFixed6("1."); + ASSERT_TRUE(result.has_value()) << "A trailing decimal point is permitted for fixed point values"; + EXPECT_EQ(result.value(), 1 << 6); + + result = ParseFixed6(".5"); + ASSERT_TRUE(result.has_value()) << "A fixed point value with no integer part is accepted"; + EXPECT_EQ(result.value(), 32); + + std::string_view badString { "-." }; + const char *endOfParse = nullptr; + result = ParseFixed6(badString, &endOfParse); + ASSERT_FALSE(result.has_value()) << "To match std::from_chars ParseFixed6 should fail to parse a decimal string with no digits, even if it starts with a minus sign."; + EXPECT_EQ(result.error(), ParseIntError::ParseError) << "Decimal strings with no digits are reported as ParseError codes."; + EXPECT_EQ(endOfParse, badString.data()) << "Failed fixed point parsing should set the end pointer to match the start of the string even though it read multiple characters"; + + result = ParseFixed6("-1."); + ASSERT_TRUE(result.has_value()) << "negative fixed point values are handled when reading into signed types"; + EXPECT_EQ(result.value(), -1 << 6); + + result = ParseFixed6("-1.25"); + ASSERT_TRUE(result.has_value()) << "negative fixed point values are handled when reading into signed types"; + EXPECT_EQ(result.value(), -((1 << 6) + 16)) << "and the fraction part is combined with the integer part respecting the sign"; + + result = ParseFixed6("-.25"); + ASSERT_TRUE(result.has_value()) << "negative fixed point values with no integer digits are handled when reading into signed types"; + EXPECT_EQ(result.value(), -16) << "and the fraction part is used respecting the sign"; + + result = ParseFixed6("-0.25"); + ASSERT_TRUE(result.has_value()) << "negative fixed point values with an explicit -0 integer part are handled when reading into signed types"; + EXPECT_EQ(result.value(), -16) << "and the fraction part is used respecting the sign"; + + ParseIntResult unsignedResult = ParseFixed6("-1."); + ASSERT_FALSE(unsignedResult.has_value()) << "negative fixed point values are not permitted when reading into unsigned types"; + EXPECT_EQ(unsignedResult.error(), ParseIntError::ParseError) << "Attempting to parse a negative value into an unsigned type is a ParseError, not an OutOfRange value"; +} +} // namespace devilution