#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fesa { namespace { constexpr std::string_view instance_column{"Part Instance Name"}; constexpr std::string_view node_column{"Node Label"}; constexpr std::string_view element_column{"Element Label"}; using PositionKey = std::tuple>; using ColumnIndices = std::map>; std::size_t column_index( const ColumnIndices& column_indices, const std::string_view name) { return column_indices.find(name)->second; } std::string_view trim(const std::string_view value) { constexpr std::string_view whitespace{" \t\f\v\r\n"}; const std::size_t first = value.find_first_not_of(whitespace); if (first == std::string_view::npos) { return {}; } const std::size_t last = value.find_last_not_of(whitespace); return value.substr(first, last - first + 1U); } std::vector split_fields(const std::string_view line) { std::vector fields; std::size_t first = 0U; while (true) { const std::size_t comma = line.find(',', first); const std::string_view field = comma == std::string_view::npos ? line.substr(first) : line.substr(first, comma - first); fields.emplace_back(trim(field)); if (comma == std::string_view::npos) { break; } first = comma + 1U; } return fields; } void remove_trailing_empty_fields(std::vector& fields) { while (!fields.empty() && fields.back().empty()) { fields.pop_back(); } } ReferenceCsvReadResult failure( const DiagnosticStage stage, std::string code, std::string message, const std::filesystem::path& path, const std::size_t line) { std::vector diagnostics; diagnostics.push_back({ stage, Severity::error, std::move(code), std::move(message), SourceLocation{path, line, line == 0U ? 0U : 1U}, }); return {{}, std::move(diagnostics)}; } ReferenceCsvReadResult validation_failure( std::string code, std::string message, const std::filesystem::path& path, const std::size_t line) { return failure( DiagnosticStage::validation, std::move(code), std::move(message), path, line); } bool is_valid_utf8(const std::string_view text) { std::size_t index = 0U; while (index < text.size()) { const auto first = static_cast(text[index]); if (first <= 0x7FU) { ++index; continue; } std::size_t continuation_count = 0U; std::uint32_t code_point = 0U; std::uint32_t minimum = 0U; if (first >= 0xC2U && first <= 0xDFU) { continuation_count = 1U; code_point = first & 0x1FU; minimum = 0x80U; } else if (first >= 0xE0U && first <= 0xEFU) { continuation_count = 2U; code_point = first & 0x0FU; minimum = 0x800U; } else if (first >= 0xF0U && first <= 0xF4U) { continuation_count = 3U; code_point = first & 0x07U; minimum = 0x10000U; } else { return false; } if (index + continuation_count >= text.size()) { return false; } for (std::size_t offset = 1U; offset <= continuation_count; ++offset) { const auto continuation = static_cast(text[index + offset]); if ((continuation & 0xC0U) != 0x80U) { return false; } code_point = (code_point << 6U) | (continuation & 0x3FU); } if (code_point < minimum || code_point > 0x10FFFFU || (code_point >= 0xD800U && code_point <= 0xDFFFU)) { return false; } index += continuation_count + 1U; } return true; } std::size_t line_at_offset( const std::string_view text, const std::size_t offset) { return 1U + static_cast(std::ranges::count( text.substr(0U, offset), '\n')); } std::vector required_columns( const ReferenceQuantity quantity) { switch (quantity) { case ReferenceQuantity::displacement: return { node_column, "U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3", }; case ReferenceQuantity::reaction: return { node_column, "RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3", }; case ReferenceQuantity::internal_force: return { element_column, node_column, "SF-SF1", "SF-SF2", "SF-SF3", "SM-SM1", "SM-SM2", "SM-SM3", }; case ReferenceQuantity::centroid_stress: return {element_column, node_column, "Sxx"}; } return {}; } std::vector value_columns( const ReferenceQuantity quantity) { switch (quantity) { case ReferenceQuantity::displacement: return {"U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"}; case ReferenceQuantity::reaction: return {"RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"}; case ReferenceQuantity::internal_force: return {"SF-SF1", "SF-SF3", "SF-SF2", "SM-SM3", "SM-SM1", "SM-SM2"}; case ReferenceQuantity::centroid_stress: return {"Sxx"}; } return {}; } bool parse_positive_label( std::string_view text, std::int64_t& value) { if (text.starts_with('+')) { text.remove_prefix(1U); } const auto parsed = std::from_chars( text.data(), text.data() + text.size(), value); return !text.empty() && parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() && value > 0; } bool parse_finite_double(std::string_view text, double& value) { if (text.starts_with('+')) { text.remove_prefix(1U); } const auto parsed = std::from_chars( text.data(), text.data() + text.size(), value, std::chars_format::general); return !text.empty() && parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() && std::isfinite(value); } } // namespace ReferenceCsvReadResult read_reference_csv( const ReferenceQuantity quantity, const std::filesystem::path& path, const std::string_view single_instance_name) { std::ifstream file{path, std::ios::binary}; if (!file) { return failure( DiagnosticStage::io, "validation.reference_csv_open_failed", "Unable to open reference CSV file.", path, 0U); } std::string bytes{ std::istreambuf_iterator{file}, std::istreambuf_iterator{}, }; if (file.bad()) { return failure( DiagnosticStage::io, "validation.reference_csv_read_failed", "Failed while reading reference CSV file.", path, 0U); } constexpr std::string_view bom{"\xEF\xBB\xBF"}; if (bytes.starts_with(bom)) { bytes.erase(0U, bom.size()); } const std::size_t misplaced_bom = bytes.find(bom); if (misplaced_bom != std::string::npos || !is_valid_utf8(bytes)) { return validation_failure( "validation.reference_csv_invalid_encoding", "Reference CSV must be UTF-8 with an optional BOM only at the file start.", path, misplaced_bom == std::string::npos ? 1U : line_at_offset(bytes, misplaced_bom)); } std::istringstream input{bytes}; std::string line; if (!std::getline(input, line)) { return validation_failure( "validation.reference_csv_missing_header", "Reference CSV is missing its header row.", path, 1U); } std::vector headers = split_fields(line); remove_trailing_empty_fields(headers); ColumnIndices column_indices; for (std::size_t index = 0U; index < headers.size(); ++index) { if (headers[index].empty() || !column_indices.emplace(headers[index], index).second) { return validation_failure( "validation.reference_csv_duplicate_column", "Reference CSV contains an empty or duplicate column name.", path, 1U); } } const std::vector required = required_columns(quantity); for (const std::string_view name : required) { if (!column_indices.contains(name)) { return validation_failure( "validation.reference_csv_missing_column", "Reference CSV is missing required column '" + std::string{name} + "'.", path, 1U); } } const bool has_instance = column_indices.contains(instance_column); if (!has_instance && single_instance_name.empty()) { return validation_failure( "validation.reference_csv_missing_column", "Reference CSV omits 'Part Instance Name' without a single-Instance name.", path, 1U); } std::set> allowed_columns; allowed_columns.emplace(instance_column); for (const std::string_view name : required) { allowed_columns.emplace(name); } for (const std::string& header : headers) { if (!allowed_columns.contains(header)) { return validation_failure( "validation.reference_csv_unsupported_column", "Reference CSV contains unsupported column '" + header + "'.", path, 1U); } } const std::vector components = value_columns(quantity); std::vector rows; std::set positions; std::size_t line_number = 1U; while (std::getline(input, line)) { ++line_number; if (trim(line).empty()) { continue; } std::vector fields = split_fields(line); remove_trailing_empty_fields(fields); if (fields.size() != headers.size()) { return validation_failure( "validation.reference_csv_invalid_row", "Reference CSV row field count does not match the header.", path, line_number); } std::string instance_name = has_instance ? fields[column_index( column_indices, instance_column)] : std::string{single_instance_name}; if (instance_name.empty()) { return validation_failure( "validation.reference_csv_invalid_row", "Reference CSV row has an empty Instance name.", path, line_number); } std::int64_t entity_label = 0; const std::string_view entity_column = quantity == ReferenceQuantity::displacement || quantity == ReferenceQuantity::reaction ? node_column : element_column; if (!parse_positive_label( fields[column_index(column_indices, entity_column)], entity_label)) { return validation_failure( "validation.reference_csv_invalid_number", "Reference CSV entity label must be a positive integer.", path, line_number); } std::optional end_node_label; if (quantity == ReferenceQuantity::internal_force || quantity == ReferenceQuantity::centroid_stress) { std::int64_t parsed_end_node = 0; if (!parse_positive_label( fields[column_index(column_indices, node_column)], parsed_end_node)) { return validation_failure( "validation.reference_csv_invalid_number", "Reference CSV end-node label must be a positive integer.", path, line_number); } end_node_label = parsed_end_node; } std::vector values; values.reserve(components.size()); for (const std::string_view component : components) { double value = 0.0; if (!parse_finite_double( fields[column_index(column_indices, component)], value)) { return validation_failure( "validation.reference_csv_invalid_number", "Reference CSV component '" + std::string{component} + "' must be a finite number.", path, line_number); } values.push_back(value); } PositionKey key{instance_name, entity_label, end_node_label}; if (!positions.insert(key).second) { return validation_failure( "validation.reference_csv_duplicate_row", "Reference CSV contains a duplicate result position.", path, line_number); } rows.push_back({ quantity, {std::move(instance_name), entity_label, end_node_label}, std::move(values), }); } if (input.bad()) { return failure( DiagnosticStage::io, "validation.reference_csv_read_failed", "Failed while reading reference CSV file.", path, line_number); } if (rows.empty()) { return validation_failure( "validation.reference_csv_missing_rows", "Reference CSV contains no result rows.", path, line_number); } return {std::move(rows), {}}; } } // namespace fesa