feat(linear-static-3d-euler-beam): step 11 - inp-syntax-parser
This commit is contained in:
@@ -4,6 +4,7 @@ add_library(
|
||||
build_info.cpp
|
||||
core/diagnostic.cpp
|
||||
core/status.cpp
|
||||
io/abaqus/input_reader.cpp
|
||||
math/matrix.cpp
|
||||
math/vector.cpp
|
||||
model/domain.cpp
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "fesa/io/abaqus/input_reader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path normalizedPath(const std::filesystem::path& path) {
|
||||
std::error_code error;
|
||||
const auto absolute = std::filesystem::absolute(path, error);
|
||||
return (error ? path : absolute).lexically_normal();
|
||||
}
|
||||
|
||||
bool isAsciiWhitespace(char value) noexcept {
|
||||
return value == ' ' || value == '\t' || value == '\r' ||
|
||||
value == '\n' || value == '\f' || value == '\v';
|
||||
}
|
||||
|
||||
std::string trim(std::string_view text) {
|
||||
while (!text.empty() && isAsciiWhitespace(text.front())) {
|
||||
text.remove_prefix(1U);
|
||||
}
|
||||
while (!text.empty() && isAsciiWhitespace(text.back())) {
|
||||
text.remove_suffix(1U);
|
||||
}
|
||||
return std::string{text};
|
||||
}
|
||||
|
||||
std::string uppercaseAscii(std::string value) {
|
||||
std::transform(
|
||||
value.begin(), value.end(), value.begin(), [](char character) {
|
||||
if (character >= 'a' && character <= 'z') {
|
||||
return static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
return character;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::string> splitFields(std::string_view line) {
|
||||
std::vector<std::string> fields;
|
||||
std::size_t fieldStart = 0U;
|
||||
while (true) {
|
||||
const std::size_t separator = line.find(',', fieldStart);
|
||||
if (separator == std::string_view::npos) {
|
||||
fields.push_back(trim(line.substr(fieldStart)));
|
||||
break;
|
||||
}
|
||||
fields.push_back(trim(line.substr(fieldStart, separator - fieldStart)));
|
||||
fieldStart = separator + 1U;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
std::string contentIdentity(const std::string& bytes) {
|
||||
constexpr std::uint64_t offsetBasis = 14695981039346656037ULL;
|
||||
constexpr std::uint64_t prime = 1099511628211ULL;
|
||||
std::uint64_t hash = offsetBasis;
|
||||
// Hash the binary input before CRLF handling so provenance follows the
|
||||
// exact file bytes rather than a normalized text representation.
|
||||
for (const unsigned char byte : bytes) {
|
||||
hash ^= static_cast<std::uint64_t>(byte);
|
||||
hash *= prime;
|
||||
}
|
||||
|
||||
std::ostringstream formatted;
|
||||
formatted << "fnv1a64:" << std::hex << std::setfill('0')
|
||||
<< std::setw(16) << hash;
|
||||
return formatted.str();
|
||||
}
|
||||
|
||||
Result<ParsedInput> failure(
|
||||
const std::filesystem::path& sourcePath,
|
||||
std::size_t line,
|
||||
std::string code,
|
||||
std::string keyword,
|
||||
std::string message) {
|
||||
Diagnostic diagnostic{
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
{sourcePath, line},
|
||||
std::move(keyword),
|
||||
"",
|
||||
std::move(message)};
|
||||
return Result<ParsedInput>::failure(Status::failure(
|
||||
FailureCategory::input, {std::move(diagnostic)}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<ParsedInput> AbaqusInputReader::read(
|
||||
const std::filesystem::path& inputPath) const {
|
||||
const auto sourcePath = normalizedPath(inputPath);
|
||||
std::ifstream stream{sourcePath, std::ios::binary};
|
||||
if (!stream) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
0U,
|
||||
"input-file-unreadable",
|
||||
"",
|
||||
"The Abaqus input file could not be opened for reading.");
|
||||
}
|
||||
|
||||
const std::string bytes{
|
||||
std::istreambuf_iterator<char>{stream},
|
||||
std::istreambuf_iterator<char>{}};
|
||||
if (stream.bad()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
0U,
|
||||
"input-file-unreadable",
|
||||
"",
|
||||
"The Abaqus input file could not be read completely.");
|
||||
}
|
||||
|
||||
ParsedInput parsed{sourcePath, contentIdentity(bytes), {}};
|
||||
std::size_t lineStart = 0U;
|
||||
std::size_t lineNumber = 1U;
|
||||
while (lineStart < bytes.size()) {
|
||||
const std::size_t newline = bytes.find('\n', lineStart);
|
||||
const std::size_t lineEnd =
|
||||
newline == std::string::npos ? bytes.size() : newline;
|
||||
std::string originalLine = bytes.substr(lineStart, lineEnd - lineStart);
|
||||
if (!originalLine.empty() && originalLine.back() == '\r') {
|
||||
originalLine.pop_back();
|
||||
}
|
||||
|
||||
const std::string trimmedLine = trim(originalLine);
|
||||
if (!trimmedLine.empty() && trimmedLine.rfind("**", 0U) != 0U) {
|
||||
if (trimmedLine.front() == '*') {
|
||||
const auto fields = splitFields(trimmedLine);
|
||||
const std::string keywordText =
|
||||
fields.empty() ? std::string{} : trim(
|
||||
std::string_view{fields[0]}.substr(1U));
|
||||
if (keywordText.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"malformed-keyword",
|
||||
trimmedLine,
|
||||
"A keyword line requires a non-empty keyword name.");
|
||||
}
|
||||
|
||||
KeywordBlock block{
|
||||
uppercaseAscii(keywordText),
|
||||
originalLine,
|
||||
{},
|
||||
{},
|
||||
{sourcePath, lineNumber}};
|
||||
for (std::size_t index = 1U; index < fields.size(); ++index) {
|
||||
const std::string& field = fields[index];
|
||||
if (field.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"malformed-keyword",
|
||||
block.canonicalName,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
|
||||
const std::size_t equals = field.find('=');
|
||||
const std::string parameterName = trim(std::string_view{field}.substr(
|
||||
0U, equals));
|
||||
if (parameterName.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"malformed-keyword",
|
||||
block.canonicalName,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
|
||||
KeywordParameter parameter{
|
||||
uppercaseAscii(parameterName), std::nullopt};
|
||||
if (equals != std::string::npos) {
|
||||
parameter.value = trim(
|
||||
std::string_view{field}.substr(equals + 1U));
|
||||
}
|
||||
block.parameters.push_back(std::move(parameter));
|
||||
}
|
||||
parsed.blocks.push_back(std::move(block));
|
||||
} else {
|
||||
if (parsed.blocks.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"orphan-data-line",
|
||||
"",
|
||||
"A data line must follow a keyword line.");
|
||||
}
|
||||
parsed.blocks.back().data.push_back(
|
||||
{splitFields(originalLine), {sourcePath, lineNumber}});
|
||||
}
|
||||
}
|
||||
|
||||
if (newline == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
lineStart = newline + 1U;
|
||||
++lineNumber;
|
||||
}
|
||||
|
||||
return Result<ParsedInput>::success(std::move(parsed));
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
Reference in New Issue
Block a user