13 std::string extension_of(
const std::string& filepath)
15 auto extension = std::filesystem::path(filepath).extension().string();
17 std::ranges::transform(extension, extension.begin(),
18 [](
unsigned char c) { return static_cast<char>(std::tolower(c)); });
23 std::string_view trim_numeric(std::string_view
value)
25 while (!
value.empty() && std::isspace(
static_cast<unsigned char>(
value.front())))
26 value.remove_prefix(1);
28 while (!
value.empty() && std::isspace(
static_cast<unsigned char>(
value.back())))
29 value.remove_suffix(1);
35 bool parse_number(std::string_view text,
T&
value)
37 text = trim_numeric(text);
41 if constexpr (std::same_as<T, bool>) {
42 if (text ==
"true" || text ==
"1") {
46 if (text ==
"false" || text ==
"0") {
52 }
else if constexpr (std::integral<T>) {
53 using Parsed = std::conditional_t<std::is_signed_v<T>,
long long,
unsigned long long>;
56 const auto [end,
error] = std::from_chars(
57 text.data(), text.data() + text.size(), parsed);
59 if (error != std::errc {} || end != text.data() + text.size()
60 || parsed <
static_cast<Parsed
>(std::numeric_limits<T>::lowest())
61 || parsed >
static_cast<Parsed
>(std::numeric_limits<T>::max()))
64 value =
static_cast<T>(parsed);
66 }
else if constexpr (std::same_as<T, long double>) {
67 std::string value_copy(text);
70 const auto parsed = std::strtold(value_copy.c_str(), &end);
72 if (errno == ERANGE || end != value_copy.c_str() + value_copy.size()
73 || !std::isfinite(parsed))
76 value =
static_cast<long double>(parsed);
79 const auto [end,
error] = std::from_chars(
80 text.data(), text.data() + text.size(),
value);
82 return error == std::errc {} && end == text.data() + text.size()
83 && std::isfinite(
value);
88 bool set_numeric(Kakshya::CompositeInsertion& insertion,
size_t row,
89 std::string_view
name, std::string_view text)
93 return parse_number(text,
value) && insertion.set(row,
name,
value);
96 bool supported_field(
const Kakshya::CompositeField& field)
98 const auto type = field.type;
100 return type ==
typeid(std::string)
101 ||
type ==
typeid(
bool)
102 ||
type ==
typeid(char)
103 ||
type ==
typeid(
signed char)
104 ||
type ==
typeid(
unsigned char)
105 ||
type ==
typeid(
short)
106 ||
type ==
typeid(
unsigned short)
107 ||
type ==
typeid(
int)
108 ||
type ==
typeid(
unsigned int)
109 ||
type ==
typeid(
long)
110 ||
type ==
typeid(
unsigned long)
111 ||
type ==
typeid(
long long)
112 ||
type ==
typeid(
unsigned long long)
113 ||
type ==
typeid(
float)
114 ||
type ==
typeid(double)
115 ||
type ==
typeid(
long double);
118 bool set_field(Kakshya::CompositeInsertion& insertion,
size_t row,
119 const Kakshya::CompositeField& field, std::string_view text,
122 const auto name = std::string_view(field.name);
123 const auto type = field.type;
125 if (
type ==
typeid(std::string)) {
126 if (text.empty() && !quoted)
129 return insertion.set_text(row,
name, text);
132 if (trim_numeric(text).empty())
135 if (
type ==
typeid(
bool))
136 return set_numeric<bool>(insertion, row,
name, text);
138 if (
type ==
typeid(
char))
139 return set_numeric<char>(insertion, row,
name, text);
140 if (
type ==
typeid(
signed char))
141 return set_numeric<signed char>(insertion, row,
name, text);
142 if (
type ==
typeid(
unsigned char))
143 return set_numeric<unsigned char>(insertion, row,
name, text);
145 if (
type ==
typeid(
short))
146 return set_numeric<short>(insertion, row,
name, text);
147 if (
type ==
typeid(
unsigned short))
148 return set_numeric<unsigned short>(insertion, row,
name, text);
150 if (
type ==
typeid(
int))
151 return set_numeric<int>(insertion, row,
name, text);
152 if (
type ==
typeid(
unsigned int))
153 return set_numeric<unsigned int>(insertion, row,
name, text);
155 if (
type ==
typeid(
long))
156 return set_numeric<long>(insertion, row,
name, text);
157 if (
type ==
typeid(
unsigned long))
158 return set_numeric<unsigned long>(insertion, row,
name, text);
160 if (
type ==
typeid(
long long))
161 return set_numeric<long long>(insertion, row,
name, text);
162 if (
type ==
typeid(
unsigned long long))
163 return set_numeric<unsigned long long>(insertion, row,
name, text);
165 if (
type ==
typeid(
float))
166 return set_numeric<float>(insertion, row,
name, text);
167 if (
type ==
typeid(
double))
168 return set_numeric<double>(insertion, row,
name, text);
169 if (
type ==
typeid(
long double))
170 return set_numeric<long double>(insertion, row,
name, text);
193 m_has_header = enabled;
199 if (is_open() || delimiter ==
'\0' || delimiter ==
'"'
200 || delimiter ==
'\r' || delimiter ==
'\n')
203 m_delimiter = delimiter;
209 const auto extension = extension_of(filepath);
210 return extension ==
".csv" || extension ==
".tsv";
224 m_last_error =
"Headerless input requires a supplied CompositeLayout";
231 : (extension_of(filepath) ==
".tsv" ?
'\t' :
',');
239 std::vector<ParsedField> names;
249 if (!names.empty() && names.front().value.starts_with(
"\xEF\xBB\xBF"))
250 names.front().value.erase(0, 3);
255 const auto& fields =
m_layout->fields();
256 if (fields.empty() || (
m_has_header && fields.size() != names.size())) {
257 m_last_error =
"Header does not match the supplied CompositeLayout";
262 for (
size_t i = 0; i < fields.size(); ++i) {
263 if (!supported_field(fields[i])
265 m_last_error =
"Unsupported or mismatched Composite field: " + fields[i].name;
272 for (
auto&
name : names) {
274 m_last_error =
"Header field names must be nonempty and unique";
289 m_file.seekg(0, std::ios::end);
320 ?
"text/tab-separated-values"
323 std::error_code error;
328 std::vector<std::string> names;
329 names.reserve(
m_layout->fields().size());
330 for (
const auto& field :
m_layout->fields())
331 names.push_back(field.name);
333 metadata.attributes.emplace(
"fields", std::move(names));
337std::optional<Kakshya::CompositeArray>
340 return read_next(std::numeric_limits<size_t>::max());
343std::optional<Kakshya::CompositeArray>
352 std::vector<ParsedField> fields;
376 const std::vector<uint64_t>&
position)
379 m_last_error =
"Seek requires one record position in an open file";
387 std::vector<ParsedField> fields;
397 m_last_error =
"Seek position is past the end of the file";
409 std::vector<ParsedField>& fields,
bool& at_end)
416 bool closed_quote =
false;
417 bool field_start =
true;
418 bool field_quoted =
false;
419 bool saw_input =
false;
422 const int next =
m_file.get();
423 if (next == std::char_traits<char>::eof()) {
439 fields.push_back({ .value = std::move(
value), .quoted = field_quoted });
444 const char c =
static_cast<char>(next);
447 if (
m_file.peek() ==
'"') {
449 value.push_back(
'"');
462 fields.push_back({ .value = std::move(
value), .quoted = field_quoted });
465 field_quoted =
false;
466 closed_quote =
false;
467 if (c ==
'\r' &&
m_file.peek() ==
'\n')
476 if (c ==
'"' && field_start) {
483 if (c ==
'"' || closed_quote) {
484 m_last_error =
"Unexpected character after quoted field";
495 const std::vector<ParsedField>& fields)
498 if (fields.size() > layout_fields.size()) {
499 m_last_error =
"Record has more fields than the CompositeLayout";
504 const auto row = insertion.
append();
506 for (
size_t i = 0; i < fields.size(); ++i) {
507 if (!set_field(insertion, row, layout_fields[i],
508 fields[i].
value, fields[i].quoted)) {
509 m_last_error =
"Invalid value in field '" + layout_fields[i].name
std::optional< glm::vec3 > position
std::unordered_map< std::string, std::string > metadata
vk::PhysicalDeviceType type
std::optional< Kakshya::CompositeLayout > m_requested_layout
std::optional< Kakshya::CompositeArray > read_composite() override
Consume all remaining records into one array.
std::streampos m_data_start
bool open(const std::string &filepath, FileReadOptions options=FileReadOptions::ALL) override
Open a CSV or TSV file and establish its Composite layout.
bool set_delimiter(char delimiter) noexcept
Override the delimiter chosen from .csv or .tsv.
bool set_layout(Kakshya::CompositeLayout layout)
Set an exact field layout before opening a file.
std::optional< Kakshya::CompositeArray > read_next(size_t max_elements) override
Consume a bounded number of records.
bool read_record(std::vector< ParsedField > &fields, bool &at_end)
bool is_open() const override
Whether a file is currently open.
bool can_read(const std::string &filepath) const override
Check for the .csv or .tsv extension.
bool seek(const std::vector< uint64_t > &position) override
Seek to a zero-based data record by replaying from the first.
~DelimitedTextReader() override
bool set_has_header(bool enabled) noexcept
Select whether the first record supplies field names.
bool append_record(Kakshya::CompositeArray &array, const std::vector< ParsedField > &fields)
std::optional< Kakshya::CompositeLayout > m_layout
std::optional< FileMetadata > get_metadata() const override
File metadata and ordered field names, when open.
void close() override
Close the input and release its active layout.
static std::string resolve_path(const std::string &filepath)
Resolve a filepath against the project source root if not found as-is.
size_t size() const noexcept
Number of packed elements.
const CompositeLayout & layout() const noexcept
Finalized layout shared by all elements.
bool clear(size_t index, std::string_view field_name)
Mark a field absent without removing its stored bytes.
Owns a sequence of values that share a CompositeLayout.
size_t append()
Append an empty element.
Mutable insertion adapter for a CompositeArray.
bool add_field(std::string name)
Declare a named arithmetic, text, or binary field.
const std::vector< CompositeField > & fields() const noexcept
Declared fields in element order, with finalized byte offsets.
Shared field description and byte layout for a CompositeArray.
FileReadOptions
Generic options for file reading behavior.
void error(Component component, Context context, std::source_location location, std::string_view message)
Log an error message and optionally throw an exception.