Skip to content

Commit

Permalink
Add support for parsing fixed point decimal strings
Browse files Browse the repository at this point in the history
  • Loading branch information
ephphatha committed Sep 17, 2023
1 parent e96ed87 commit 8fee34e
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 5 deletions.
76 changes: 76 additions & 0 deletions Source/data/iterators.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <expected.hpp>

#include "parser.hpp"
#include "utils/parse_int.hpp"

namespace devilution {

Expand Down Expand Up @@ -36,6 +37,18 @@ class DataFileField {
}
}

static tl::expected<void, Error> 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)
Expand Down Expand Up @@ -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 <typename T>
[[nodiscard]] tl::expected<void, Error> parseFixed6(T &destination)
{
constexpr T minIntegerValue = std::numeric_limits<T>::min() >> 6;
constexpr T maxIntegerValue = std::numeric_limits<T>::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<T> integerParseResult = ParseInt({ begin, end_ }, minIntegerValue, maxIntegerValue, &currentChar);

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_ }, &currentChar);
}

// 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<T> && 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 <typename T>
[[nodiscard]] tl::expected<T, Error> asFixed6()
{
T value = 0;
return parseFixed6(value).map([value]() { return value; });
}

/**
* Returns the current row number
*/
Expand Down
32 changes: 31 additions & 1 deletion Source/utils/parse_int.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <algorithm>
#include <charconv>
#include <string_view>
#include <system_error>
Expand All @@ -19,10 +20,13 @@ using ParseIntResult = tl::expected<IntT, ParseIntError>;
template <typename IntT>
ParseIntResult<IntT> ParseInt(
std::string_view str, IntT min = std::numeric_limits<IntT>::min(),
IntT max = std::numeric_limits<IntT>::max())
IntT max = std::numeric_limits<IntT>::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)
Expand All @@ -32,4 +36,30 @@ ParseIntResult<IntT> 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
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ set(tests
missiles_test
pack_test
path_test
parse_int_test
player_test
quests_test
random_test
Expand Down
31 changes: 29 additions & 2 deletions test/data_file_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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";
}
}

Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/txtdata/sample.tsv
Original file line number Diff line number Diff line change
@@ -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
101 changes: 101 additions & 0 deletions test/parse_int_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <gtest/gtest.h>

#include "utils/parse_int.hpp"

namespace devilution {
TEST(ParseIntTest, ParseInt)
{
ParseIntResult<int> result = ParseInt<int>("");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ParseIntError::ParseError);

result = ParseInt<int>("abcd");
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ParseIntError::ParseError);

result = ParseInt<int>("12");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), 12);

result = ParseInt<int>(("99999999"), -5, 100);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), ParseIntError::OutOfRange);

ParseIntResult<int8_t> shortResult = ParseInt<int8_t>(("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

0 comments on commit 8fee34e

Please sign in to comment.