feat(linear-static-3d-euler-beam): step 11 - inp-syntax-parser

This commit is contained in:
KOKO\Mimi
2026-08-09 15:19:29 +09:00
parent 3b56fc906b
commit b73f6cd823
8 changed files with 550 additions and 0 deletions
+1
View File
@@ -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
+216
View File
@@ -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