MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
DelimitedTextReader.cpp
Go to the documentation of this file.
2
4
5#include <charconv>
6#include <cerrno>
7#include <cstdlib>
8
9namespace MayaFlux::IO {
10
11namespace {
12
13 std::string extension_of(const std::string& filepath)
14 {
15 auto extension = std::filesystem::path(filepath).extension().string();
16
17 std::ranges::transform(extension, extension.begin(),
18 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
19
20 return extension;
21 }
22
23 std::string_view trim_numeric(std::string_view value)
24 {
25 while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front())))
26 value.remove_prefix(1);
27
28 while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back())))
29 value.remove_suffix(1);
30
31 return value;
32 }
33
34 template <typename T>
35 bool parse_number(std::string_view text, T& value)
36 {
37 text = trim_numeric(text);
38 if (text.empty())
39 return false;
40
41 if constexpr (std::same_as<T, bool>) {
42 if (text == "true" || text == "1") {
43 value = true;
44 return true;
45 }
46 if (text == "false" || text == "0") {
47 value = false;
48 return true;
49 }
50
51 return false;
52 } else if constexpr (std::integral<T>) {
53 using Parsed = std::conditional_t<std::is_signed_v<T>, long long, unsigned long long>;
54 Parsed parsed {};
55
56 const auto [end, error] = std::from_chars(
57 text.data(), text.data() + text.size(), parsed);
58
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()))
62 return false;
63
64 value = static_cast<T>(parsed);
65 return true;
66 } else if constexpr (std::same_as<T, long double>) {
67 std::string value_copy(text);
68 char* end = nullptr;
69 errno = 0;
70 const auto parsed = std::strtold(value_copy.c_str(), &end);
71
72 if (errno == ERANGE || end != value_copy.c_str() + value_copy.size()
73 || !std::isfinite(parsed))
74 return false;
75
76 value = static_cast<long double>(parsed);
77 return true;
78 } else {
79 const auto [end, error] = std::from_chars(
80 text.data(), text.data() + text.size(), value);
81
82 return error == std::errc {} && end == text.data() + text.size()
83 && std::isfinite(value);
84 }
85 }
86
87 template <typename T>
88 bool set_numeric(Kakshya::CompositeInsertion& insertion, size_t row,
89 std::string_view name, std::string_view text)
90 {
91 T value {};
92
93 return parse_number(text, value) && insertion.set(row, name, value);
94 }
95
96 bool supported_field(const Kakshya::CompositeField& field)
97 {
98 const auto type = field.type;
99
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);
116 }
117
118 bool set_field(Kakshya::CompositeInsertion& insertion, size_t row,
119 const Kakshya::CompositeField& field, std::string_view text,
120 bool quoted)
121 {
122 const auto name = std::string_view(field.name);
123 const auto type = field.type;
124
125 if (type == typeid(std::string)) {
126 if (text.empty() && !quoted)
127 return true;
128
129 return insertion.set_text(row, name, text);
130 }
131
132 if (trim_numeric(text).empty())
133 return true;
134
135 if (type == typeid(bool))
136 return set_numeric<bool>(insertion, row, name, text);
137
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);
144
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);
149
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);
154
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);
159
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);
164
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);
171
172 return false;
173 }
174
175}
176
178
180{
181 if (is_open())
182 return false;
183
184 m_requested_layout = std::move(layout);
185 return true;
186}
187
188bool DelimitedTextReader::set_has_header(bool enabled) noexcept
189{
190 if (is_open())
191 return false;
192
193 m_has_header = enabled;
194 return true;
195}
196
197bool DelimitedTextReader::set_delimiter(char delimiter) noexcept
198{
199 if (is_open() || delimiter == '\0' || delimiter == '"'
200 || delimiter == '\r' || delimiter == '\n')
201 return false;
202
203 m_delimiter = delimiter;
204 return true;
205}
206
207bool DelimitedTextReader::can_read(const std::string& filepath) const
208{
209 const auto extension = extension_of(filepath);
210 return extension == ".csv" || extension == ".tsv";
211}
212
214 const std::string& filepath, FileReadOptions)
215{
216 close();
217 m_last_error.clear();
218 if (!can_read(filepath)) {
219 m_last_error = "Expected a .csv or .tsv file";
220 return false;
221 }
222
224 m_last_error = "Headerless input requires a supplied CompositeLayout";
225 return false;
226 }
227
228 m_filepath = resolve_path(filepath);
231 : (extension_of(filepath) == ".tsv" ? '\t' : ',');
232
233 m_file.open(m_filepath, std::ios::binary);
234 if (!m_file) {
235 m_last_error = "Cannot open delimited text file";
236 return false;
237 }
238
239 std::vector<ParsedField> names;
240 bool at_end = false;
241 if (m_has_header) {
242 if (!read_record(names, at_end) || at_end) {
243 if (at_end)
244 m_last_error = "Delimited text file has no header";
245 close();
246 return false;
247 }
248
249 if (!names.empty() && names.front().value.starts_with("\xEF\xBB\xBF"))
250 names.front().value.erase(0, 3);
251 }
252
253 if (m_requested_layout) {
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";
258 close();
259 return false;
260 }
261
262 for (size_t i = 0; i < fields.size(); ++i) {
263 if (!supported_field(fields[i])
264 || (m_has_header && fields[i].name != names[i].value)) {
265 m_last_error = "Unsupported or mismatched Composite field: " + fields[i].name;
266 close();
267 return false;
268 }
269 }
270 } else {
272 for (auto& name : names) {
273 if (!layout.add_field<std::string>(std::move(name.value))) {
274 m_last_error = "Header field names must be nonempty and unique";
275 close();
276 return false;
277 }
278 }
279
280 m_layout = std::move(layout);
281 }
282
284 m_layout = empty.layout();
285
286 m_data_start = m_file.tellg();
287 if (m_data_start == std::streampos(-1)) {
288 m_file.clear();
289 m_file.seekg(0, std::ios::end);
290 m_data_start = m_file.tellg();
291 }
292
293 m_file.clear();
294 m_file.seekg(m_data_start);
295
296 m_row_position = 0;
297 m_at_end = false;
298 return true;
299}
300
302{
303 if (m_file.is_open())
304 m_file.close();
305
306 m_layout.reset();
307 m_filepath.clear();
308 m_row_position = 0;
309 m_at_end = false;
310}
311
312std::optional<FileMetadata> DelimitedTextReader::get_metadata() const
313{
314 if (!is_open() || !m_layout)
315 return std::nullopt;
316
318 metadata.format = m_active_delimiter == '\t' ? "tsv" : "csv";
319 metadata.mime_type = m_active_delimiter == '\t'
320 ? "text/tab-separated-values"
321 : "text/csv";
322
323 std::error_code error;
324 metadata.file_size = std::filesystem::file_size(m_filepath, error);
325 if (error)
326 metadata.file_size = 0;
327
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);
332
333 metadata.attributes.emplace("fields", std::move(names));
334 return metadata;
335}
336
337std::optional<Kakshya::CompositeArray>
339{
340 return read_next(std::numeric_limits<size_t>::max());
341}
342
343std::optional<Kakshya::CompositeArray>
345{
346 if (!is_open() || !m_layout) {
347 m_last_error = "No delimited text file is open";
348 return std::nullopt;
349 }
350
352 std::vector<ParsedField> fields;
353
354 while (result.size() < max_elements && !m_at_end) {
355 bool at_end = false;
356
357 if (!read_record(fields, at_end))
358 return std::nullopt;
359
360 if (at_end) {
361 m_at_end = true;
362 break;
363 }
364
365 if (!append_record(result, fields))
366 return std::nullopt;
367
369 }
370
371 m_last_error.clear();
372 return result;
373}
374
376 const std::vector<uint64_t>& position)
377{
378 if (!is_open() || position.size() != 1) {
379 m_last_error = "Seek requires one record position in an open file";
380 return false;
381 }
382 m_file.clear();
383 m_file.seekg(m_data_start);
384 m_row_position = 0;
385 m_at_end = false;
386
387 std::vector<ParsedField> fields;
388
389 while (m_row_position < position[0]) {
390 bool at_end = false;
391
392 if (!read_record(fields, at_end))
393 return false;
394
395 if (at_end) {
396 m_at_end = true;
397 m_last_error = "Seek position is past the end of the file";
398 return false;
399 }
400
402 }
403
404 m_last_error.clear();
405 return true;
406}
407
409 std::vector<ParsedField>& fields, bool& at_end)
410{
411 fields.clear();
412 at_end = false;
413
414 std::string value;
415 bool quoted = false;
416 bool closed_quote = false;
417 bool field_start = true;
418 bool field_quoted = false;
419 bool saw_input = false;
420
421 while (true) {
422 const int next = m_file.get();
423 if (next == std::char_traits<char>::eof()) {
424 if (m_file.bad()) {
425 m_last_error = "Failed to read delimited text file";
426 return false;
427 }
428
429 if (quoted) {
430 m_last_error = "Unterminated quoted field";
431 return false;
432 }
433
434 if (!saw_input) {
435 at_end = true;
436 return true;
437 }
438
439 fields.push_back({ .value = std::move(value), .quoted = field_quoted });
440 return true;
441 }
442
443 saw_input = true;
444 const char c = static_cast<char>(next);
445 if (quoted) {
446 if (c == '"') {
447 if (m_file.peek() == '"') {
448 m_file.get();
449 value.push_back('"');
450 } else {
451 quoted = false;
452 closed_quote = true;
453 }
454 } else {
455 value.push_back(c);
456 }
457
458 continue;
459 }
460
461 if (c == m_active_delimiter || c == '\n' || c == '\r') {
462 fields.push_back({ .value = std::move(value), .quoted = field_quoted });
463 value.clear();
464 field_start = true;
465 field_quoted = false;
466 closed_quote = false;
467 if (c == '\r' && m_file.peek() == '\n')
468 m_file.get();
469
470 if (c != m_active_delimiter)
471 return true;
472
473 continue;
474 }
475
476 if (c == '"' && field_start) {
477 quoted = true;
478 field_start = false;
479 field_quoted = true;
480 continue;
481 }
482
483 if (c == '"' || closed_quote) {
484 m_last_error = "Unexpected character after quoted field";
485 return false;
486 }
487
488 value.push_back(c);
489 field_start = false;
490 }
491}
492
495 const std::vector<ParsedField>& fields)
496{
497 const auto& layout_fields = array.layout().fields();
498 if (fields.size() > layout_fields.size()) {
499 m_last_error = "Record has more fields than the CompositeLayout";
500 return false;
501 }
502
503 Kakshya::CompositeInsertion insertion(array);
504 const auto row = insertion.append();
505
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
510 + "' at record " + std::to_string(m_row_position);
511 return false;
512 }
513 }
514
515 return true;
516}
517
518}
std::optional< glm::vec3 > position
Definition Decoder.cpp:93
std::unordered_map< std::string, std::string > metadata
glm::vec3 value
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
std::string name
Definition VKDevice.cpp:123
std::optional< Kakshya::CompositeLayout > m_requested_layout
std::optional< Kakshya::CompositeArray > read_composite() override
Consume all remaining records into one array.
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.
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.
Definition Composite.hpp:65
const std::vector< CompositeField > & fields() const noexcept
Declared fields in element order, with finalized byte offsets.
Definition Composite.hpp:82
Shared field description and byte layout for a CompositeArray.
Definition Composite.hpp:52
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.
std::string format
File format identifier (e.g., "wav", "mp3", "hdf5")
Generic metadata structure for any file type.