diff --git a/Source/data/iterators.hpp b/Source/data/iterators.hpp index fc0857c37e3..ce86e8ff045 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,69 @@ 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) + { + constexpr T minIntegerValue = std::numeric_limits::min() >> 6; + constexpr T maxIntegerValue = std::numeric_limits::max() >> 6; + + const char *begin = state_->status == GetFieldResult::Status::ReadyToRead ? state_->next : state_->value.data(); + const char *currentChar; // will be set by the call to parseInt + ParseIntResult integerParseResult = ParseInt({ begin, end_ }, minIntegerValue, maxIntegerValue, ¤tChar); + + if (integerParseResult.has_value()) { + uint8_t fractionPart = 0; + T integerPart = integerParseResult.value(); + + if (currentChar != end_ && *(currentChar) == '.') { + // got a fractional part to read too + ++currentChar; + + fractionPart = ParseFixed6Fraction({ currentChar, end_ }, ¤tChar); + } + + // 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))) { + integerParseResult = tl::unexpected { ParseIntError::OutOfRange }; + } else { + destination = integerPart << 6; + if (destination < 0) { + destination -= fractionPart; + } else { + destination += fractionPart; + } + } + } + if (state_->status == GetFieldResult::Status::ReadyToRead) { + *state_ = GetNextField(currentChar, end_); + // and prepend what was already parsed + state_->value = { begin, (state_->value.data() - begin) + state_->value.size() }; + } + + if (integerParseResult.has_value()) { + return {}; + } else { + return mapError(integerParseResult.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..c71370a37dd 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,30 @@ 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 + if (numDigits < 7) { + decimalFraction *= std::pow(10U, 7U - numDigits); + } + // we add half the step between representable values to use integer truncation as a substitute for rounding to nearest. + return (decimalFraction + 78125) / 156250; +} + } // 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..699d1975c4e 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..eb6c61072bb --- /dev/null +++ b/test/parse_int_test.cpp @@ -0,0 +1,101 @@ +#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); +} +} // namespace devilution