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.
436 lines
13 KiB
436 lines
13 KiB
#pragma once |
|
|
|
#include <charconv> |
|
#include <ostream> |
|
|
|
#include <expected.hpp> |
|
|
|
#include "parser.hpp" |
|
#include "utils/parse_int.hpp" |
|
|
|
namespace devilution { |
|
|
|
class DataFileField { |
|
GetFieldResult *state_; |
|
const char *end_; |
|
unsigned row_; |
|
unsigned column_; |
|
|
|
public: |
|
enum class Error { |
|
NotANumber, |
|
OutOfRange, |
|
InvalidValue |
|
}; |
|
|
|
static tl::expected<void, Error> 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 }; |
|
} |
|
} |
|
|
|
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) |
|
, row_(row) |
|
, column_(column) |
|
{ |
|
} |
|
|
|
/** |
|
* @brief Returns a view of the current field |
|
* |
|
* This method scans the current field if this is the first value access since the last |
|
* advance. If you expect the field to contain a numeric value then calling parseInt first |
|
* is more efficient, but calling the methods in either order is supported. |
|
* @return The current field value (may be an empty string) or a zero length string_view |
|
*/ |
|
[[nodiscard]] std::string_view value() |
|
{ |
|
if (state_->status == GetFieldResult::Status::ReadyToRead) { |
|
*state_ = GetNextField(state_->next, end_); |
|
} |
|
return state_->value; |
|
} |
|
|
|
/** |
|
* Convenience function to let DataFileField instances be used like other single-value STL containers |
|
*/ |
|
[[nodiscard]] std::string_view operator*() |
|
{ |
|
return this->value(); |
|
} |
|
|
|
/** |
|
* @brief Attempts to parse the current field as a numeric value using std::from_chars |
|
* |
|
* 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 (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 corresponding to the from_chars result if parsing failed |
|
*/ |
|
template <typename T> |
|
[[nodiscard]] tl::expected<void, Error> parseInt(T &destination) |
|
{ |
|
std::from_chars_result result {}; |
|
if (state_->status == GetFieldResult::Status::ReadyToRead) { |
|
const char *begin = state_->next; |
|
result = std::from_chars(begin, end_, destination); |
|
if (result.ec != std::errc::invalid_argument) { |
|
// from_chars was able to consume at least one character, consume the rest of the field |
|
*state_ = GetNextField(result.ptr, end_); |
|
// and prepend what was already parsed |
|
state_->value = { begin, (state_->value.data() - begin) + state_->value.size() }; |
|
} |
|
} else { |
|
result = std::from_chars(state_->value.data(), end_, destination); |
|
} |
|
|
|
return mapError(result.ec); |
|
} |
|
|
|
template <typename T> |
|
[[nodiscard]] tl::expected<T, Error> asInt() |
|
{ |
|
T value = 0; |
|
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) |
|
{ |
|
ParseIntResult<T> parseResult; |
|
if (state_->status == GetFieldResult::Status::ReadyToRead) { |
|
const char *begin = state_->next; |
|
// first read, consume digits |
|
parseResult = ParseFixed6<T>({ begin, static_cast<size_t>(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<T>(state_->value); |
|
} |
|
|
|
if (parseResult.has_value()) { |
|
destination = parseResult.value(); |
|
return {}; |
|
} else { |
|
return mapError(parseResult.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 |
|
*/ |
|
[[nodiscard]] unsigned row() const |
|
{ |
|
return row_; |
|
} |
|
|
|
/** |
|
* Returns the current column/field number (from the start of the row/record) |
|
*/ |
|
[[nodiscard]] unsigned column() const |
|
{ |
|
return column_; |
|
} |
|
|
|
/** |
|
* Allows accessing the value of this field in a const context |
|
* |
|
* This requires an actual non-const value access to happen first before it returns |
|
* any useful results, intended for use in error reporting (or test output). |
|
*/ |
|
[[nodiscard]] std::string_view currentValue() const |
|
{ |
|
return state_->value; |
|
} |
|
}; |
|
|
|
/** |
|
* @brief Show the field value along with the row/column number (mainly used in test failure messages) |
|
* @param stream output stream, expected to have overloads for unsigned, std::string_view, and char* |
|
* @param field Object to display |
|
* @return the stream, to allow chaining |
|
*/ |
|
inline std::ostream &operator<<(std::ostream &stream, const DataFileField &field) |
|
{ |
|
return stream << "\"" << field.currentValue() << "\" (at row " << field.row() << ", column " << field.column() << ")"; |
|
} |
|
|
|
class FieldIterator { |
|
GetFieldResult *state_; |
|
const char *const end_; |
|
const unsigned row_; |
|
unsigned column_ = 0; |
|
|
|
public: |
|
using iterator_category = std::input_iterator_tag; |
|
using value_type = DataFileField; |
|
|
|
FieldIterator() |
|
: state_(nullptr) |
|
, end_(nullptr) |
|
, row_(0) |
|
{ |
|
} |
|
|
|
FieldIterator(GetFieldResult *state, const char *end, unsigned row) |
|
: state_(state) |
|
, end_(end) |
|
, row_(row) |
|
{ |
|
state_->status = GetFieldResult::Status::ReadyToRead; |
|
} |
|
|
|
[[nodiscard]] bool operator==(const FieldIterator &rhs) const |
|
{ |
|
if (state_ == nullptr && rhs.state_ == nullptr) |
|
return true; |
|
|
|
return state_ != nullptr && rhs.state_ != nullptr && state_->next == rhs.state_->next; |
|
} |
|
|
|
[[nodiscard]] bool operator!=(const FieldIterator &rhs) const |
|
{ |
|
return !(*this == rhs); |
|
} |
|
|
|
/** |
|
* Advances to the next field in the current record |
|
*/ |
|
FieldIterator &operator++() |
|
{ |
|
return *this += 1; |
|
} |
|
|
|
/** |
|
* @brief Advances by the specified number of fields |
|
* |
|
* if a non-zero increment is provided and advancing the iterator causes it to reach the end |
|
* of the record the iterator is invalidated. It will compare equal to an end iterator and |
|
* cannot be used for value access or any further parsing |
|
* @param increment how many fields to advance (can be 0) |
|
* @return self-reference |
|
*/ |
|
FieldIterator &operator+=(unsigned increment) |
|
{ |
|
if (increment == 0) |
|
return *this; |
|
|
|
if (state_->status == GetFieldResult::Status::ReadyToRead) { |
|
// We never read the value and no longer need it, discard it so that we end up |
|
// advancing past the field delimiter (as if a value access had happened) |
|
*state_ = DiscardField(state_->next, end_); |
|
} |
|
|
|
if (state_->endOfRecord()) { |
|
state_ = nullptr; |
|
} else { |
|
unsigned fieldsSkipped = 0; |
|
// By this point we've already advanced past the end of this field (either because the |
|
// last value access found the end of the field by necessity or we discarded it a few |
|
// lines up), so we only need to advance further if an increment greater than 1 was |
|
// provided. |
|
*state_ = DiscardMultipleFields(state_->next, end_, increment - 1, &fieldsSkipped); |
|
// As we've consumed the current field by this point we need to increment the internal |
|
// column counter one extra time so we have an accurate value. |
|
column_ += fieldsSkipped + 1; |
|
// We use Status::ReadyToRead as a marker so we only read the next value on the next |
|
// value access, this allows consumers to choose the most efficient method (e.g. if |
|
// they want the value as an int) or even repeated advances without using a value. |
|
state_->status = GetFieldResult::Status::ReadyToRead; |
|
} |
|
return *this; |
|
} |
|
|
|
/** |
|
* @brief Returns a view of the current field |
|
* |
|
* The returned value is a thin wrapper over the current state of this iterator (or last |
|
* successful read if incrementing this iterator would result in it reaching the end state). |
|
*/ |
|
[[nodiscard]] value_type operator*() |
|
{ |
|
return { state_, end_, row_, column_ }; |
|
} |
|
|
|
/** |
|
* @brief Returns the current row number |
|
*/ |
|
[[nodiscard]] unsigned row() const |
|
{ |
|
return row_; |
|
} |
|
/** |
|
* @brief Returns the current column/field number (from the start of the row/record) |
|
*/ |
|
[[nodiscard]] unsigned column() const |
|
{ |
|
return column_; |
|
} |
|
}; |
|
|
|
class DataFileRecord { |
|
GetFieldResult *state_; |
|
const char *const end_; |
|
const unsigned row_; |
|
|
|
public: |
|
DataFileRecord(GetFieldResult *state, const char *end, unsigned row) |
|
: state_(state) |
|
, end_(end) |
|
, row_(row) |
|
{ |
|
} |
|
|
|
[[nodiscard]] FieldIterator begin() |
|
{ |
|
return { state_, end_, row_ }; |
|
} |
|
|
|
[[nodiscard]] FieldIterator end() const |
|
{ |
|
return {}; |
|
} |
|
|
|
[[nodiscard]] unsigned row() const |
|
{ |
|
return row_; |
|
} |
|
}; |
|
|
|
class RecordIterator { |
|
GetFieldResult state_; |
|
const char *const end_; |
|
unsigned row_ = 0; |
|
|
|
public: |
|
using iterator_category = std::forward_iterator_tag; |
|
using value_type = DataFileRecord; |
|
|
|
RecordIterator() |
|
: state_(nullptr, GetFieldResult::Status::EndOfFile) |
|
, end_(nullptr) |
|
{ |
|
} |
|
|
|
RecordIterator(const char *begin, const char *end, bool skippedHeader) |
|
: state_(begin) |
|
, end_(end) |
|
, row_(skippedHeader ? 1 : 0) |
|
{ |
|
} |
|
|
|
[[nodiscard]] bool operator==(const RecordIterator &rhs) const |
|
{ |
|
return state_.next == rhs.state_.next; |
|
} |
|
|
|
[[nodiscard]] bool operator!=(const RecordIterator &rhs) const |
|
{ |
|
return !(*this == rhs); |
|
} |
|
|
|
RecordIterator &operator++() |
|
{ |
|
return *this += 1; |
|
} |
|
|
|
RecordIterator &operator+=(unsigned increment) |
|
{ |
|
if (increment == 0) |
|
return *this; |
|
|
|
if (!state_.endOfRecord()) { |
|
// The field iterator either hasn't been used or hasn't consumed the entire record |
|
state_ = DiscardRemainingFields(state_.next, end_); |
|
} |
|
|
|
if (state_.endOfFile()) { |
|
state_.next = nullptr; |
|
} else { |
|
unsigned recordsSkipped = 0; |
|
// By this point we've already advanced past the end of this record (either because the |
|
// last value access found the end of the record by necessity or we discarded any |
|
// leftovers a few lines up), so we only need to advance further if an increment |
|
// greater than 1 was provided. |
|
state_ = DiscardMultipleRecords(state_.next, end_, increment - 1, &recordsSkipped); |
|
// As we've consumed the current record by this point we need to increment the internal |
|
// row counter one extra time so we have an accurate value. |
|
row_ += recordsSkipped + 1; |
|
// We use Status::ReadyToRead as a marker in case the DataFileField iterator is never |
|
// used, so the next call to operator+= will advance past the current record |
|
state_.status = GetFieldResult::Status::ReadyToRead; |
|
} |
|
return *this; |
|
} |
|
|
|
[[nodiscard]] DataFileRecord operator*() |
|
{ |
|
return { &state_, end_, row_ }; |
|
} |
|
|
|
/** |
|
* @brief Exposes the current location of this input iterator. |
|
* |
|
* This is only expected to be used internally so the DataFile instance knows where the header |
|
* ends and the body begins. You probably don't want to use this directly. |
|
*/ |
|
[[nodiscard]] const char *data() const |
|
{ |
|
return state_.next; |
|
} |
|
|
|
/** |
|
* @brief Returns the current row/record number (from the start of the file) |
|
* |
|
* The header row is always considered row 0, however if you've called DataFile.parseHeader() |
|
* before calling DataFile.begin() then you'll get row 1 as the first record of the range. |
|
*/ |
|
[[nodiscard]] unsigned row() const |
|
{ |
|
return row_; |
|
} |
|
}; |
|
} // namespace devilution
|
|
|