dasmig-core 1.2.0
Shared random engine, distributions and data tiers for the dasmig generator libraries
Loading...
Searching...
No Matches
tsv.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_CORE_TSV_HPP
5#define DASMIG_CORE_TSV_HPP
6
7#include "rng.hpp"
8#include "utf8.hpp"
9
10#include <algorithm>
11#include <array>
12#include <cfloat>
13#include <charconv>
14#include <cmath>
15#include <concepts>
16#include <cstddef>
17#include <cstdint>
18#include <filesystem>
19#include <fstream>
20#include <functional>
21#include <iterator>
22#include <limits>
23#include <locale>
24#include <map>
25#include <memory>
26#include <optional>
27#include <sstream>
28#include <stdexcept>
29#include <string>
30#include <string_view>
31#include <system_error>
32#include <utility>
33#include <vector>
34
35/// @file tsv.hpp
36/// @brief Tab-separated data files with typed access and located errors.
37/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
38///
39/// The format every library's `resources/` and every extension layer uses:
40///
41/// - UTF-8, with or without a byte order mark; LF or CRLF line ends.
42/// - The first line names the columns, unless the columns are given to
43/// parse() (for headerless word lists).
44/// - Cells are separated by one tab and are not quoted. A row may have fewer
45/// cells than columns (the missing ones are empty), not more.
46/// - Empty lines and lines starting with `#` are skipped.
47///
48/// Numbers are parsed exactly (correctly rounded), so the same file gives the
49/// same doubles on every platform.
50
51namespace dasmig::core
52{
53
54/// @brief A data error with its location.
55///
56/// `what()` reads `source:line: column 'name': message`, leaving out the
57/// parts that do not apply.
58class tsv_error : public std::runtime_error
59{
60 public:
61 /// @brief Construct.
62 /// @param source File name or other description of the data.
63 /// @param line 1-based line number, or 0 for the whole source.
64 /// @param column Column name, or empty for the whole line.
65 /// @param message What is wrong.
66 tsv_error(std::string source, std::size_t line, std::string column,
67 const std::string& message)
68 : std::runtime_error{format(source, line, column, message)},
69 _source{std::move(source)}, _line{line}, _column{std::move(column)}
70 {
71 }
72
73 /// @brief The data source.
74 /// @return File name or description.
75 [[nodiscard]] const std::string& source() const noexcept { return _source; }
76
77 /// @brief The line.
78 /// @return 1-based line, or 0 for the whole source.
79 [[nodiscard]] std::size_t line() const noexcept { return _line; }
80
81 /// @brief The column.
82 /// @return Column name, or empty for the whole line.
83 [[nodiscard]] const std::string& column() const noexcept { return _column; }
84
85 private:
86 static std::string format(const std::string& source, std::size_t line,
87 const std::string& column, const std::string& message) // NOLINT(bugprone-easily-swappable-parameters)
88 {
89 std::string out = source;
90 if (line > 0) out += ":" + std::to_string(line);
91 out += ": ";
92 if (!column.empty()) out += "column '" + column + "': ";
93 return out + message;
94 }
95
96 std::string _source;
97 std::size_t _line;
98 std::string _column;
99};
100
101/// @cond INTERNAL
102namespace detail
103{
104
105// The significand of a plain decimal: its significant digits as an integer,
106// the power of ten that scales them, and where it ends in the text.
107struct decimal_significand
108{
109 std::uint64_t mantissa{0};
110 int exponent{0};
111 std::size_t end{0};
112};
113
114// Reads digits[.digits] (either part may be empty, not both) from text[i].
115// nullopt without digits or past 19 significant digits, the most that always
116// fit in 64 bits.
117[[nodiscard]] inline std::optional<decimal_significand> scan_significand(std::string_view text, std::size_t i)
118{
119 constexpr int max_digits = 19;
120 decimal_significand out;
121 int digits = 0;
122 bool any = false;
123 bool point = false;
124 for (; i < text.size(); ++i)
125 {
126 const char c = text[i];
127 if (c == '.' && !point)
128 {
129 point = true;
130 continue;
131 }
132 if (c < '0' || c > '9') break;
133 any = true;
134 if (point) --out.exponent;
135 if (out.mantissa == 0 && c == '0') continue;
136 if (++digits > max_digits) return std::nullopt;
137 out.mantissa = out.mantissa * 10 + static_cast<std::uint64_t>(c - '0');
138 }
139 if (!any) return std::nullopt;
140 out.end = i;
141 return out;
142}
143
144// Reads the rest of the text from text[i] as an exponent, e[+-]digits with at
145// most 4 digits, or nothing (0). nullopt for anything else.
146[[nodiscard]] inline std::optional<int> scan_exponent(std::string_view text, std::size_t i)
147{
148 if (i == text.size()) return 0;
149 if (text[i] != 'e' && text[i] != 'E') return std::nullopt;
150 ++i;
151 bool negative = false;
152 if (i < text.size() && (text[i] == '+' || text[i] == '-')) negative = text[i++] == '-';
153 if (i == text.size() || text.size() - i > 4) return std::nullopt;
154 int value = 0;
155 for (; i < text.size(); ++i)
156 {
157 if (text[i] < '0' || text[i] > '9') return std::nullopt;
158 value = value * 10 + (text[i] - '0');
159 }
160 return negative ? -value : value;
161}
162
163// Clinger's fast path for a plain decimal ([-]digits[.digits][e[+-]digits]):
164// when its significant digits form an integer m <= 2^53 and its power of ten
165// e is within [-22, 22], both m and 10^|e| are exact doubles, so the value is
166// one IEEE multiplication or division and correctly rounded, the same double
167// a correctly rounded parser gives. Returns nullopt for anything else, which
168// goes to the general parser. Data files are mostly such decimals, and the
169// general parser is slow where it emulates wider arithmetic (Emscripten's
170// strtod works in software 128-bit long double).
171[[nodiscard]] inline std::optional<double> parse_decimal_fast(std::string_view text)
172{
173#if FLT_EVAL_METHOD == 0
174 static constexpr std::array<double, 23> pow10{1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7,
175 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
176 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
177 constexpr std::uint64_t max_exact = 9007199254740992; // 2^53
178 constexpr int max_power = 22;
179
180 const bool negative = text.starts_with('-');
181 const auto significand = scan_significand(text, negative ? 1 : 0);
182 if (!significand) return std::nullopt;
183 const auto exponent = scan_exponent(text, significand->end);
184 if (!exponent) return std::nullopt;
185
186 double value = 0.0;
187 if (significand->mantissa != 0)
188 {
189 const int power = significand->exponent + *exponent;
190 if (significand->mantissa > max_exact || power < -max_power || power > max_power) return std::nullopt;
191 const auto m = static_cast<double>(significand->mantissa);
192 const double scale = pow10[static_cast<std::size_t>(power < 0 ? -power : power)]; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index): power is in [-22, 22]
193 value = power < 0 ? m / scale : m * scale;
194 }
195 return negative ? -value : value;
196#else
197 // Excess precision (x87) would round twice.
198 (void)text;
199 return std::nullopt;
200#endif
201}
202
203// Whole-cell number parsing; an optional leading '+' is accepted.
204template <typename T>
205[[nodiscard]] std::optional<T> parse_number(std::string_view text)
206{
207 if (text.size() > 1 && text.front() == '+' && text[1] != '-') text.remove_prefix(1);
208 if (text.empty()) return std::nullopt;
209 T value{};
210 if constexpr (std::floating_point<T>)
211 {
212 if constexpr (std::same_as<T, double>)
213 {
214 if (const auto fast = parse_decimal_fast(text)) return *fast;
215 }
216#if defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L
217 const auto res = std::from_chars(std::to_address(text.begin()), std::to_address(text.end()), value);
218 if (res.ec != std::errc{} || res.ptr != std::to_address(text.end())) return std::nullopt;
219#else
220 // Standard libraries without floating-point from_chars (older libc++):
221 // the classic locale's strtod is correctly rounded on those platforms.
222 std::istringstream in{std::string{text}};
223 in.imbue(std::locale::classic());
224 in >> value;
225 if (in.fail() || in.peek() != std::char_traits<char>::eof()) return std::nullopt;
226#endif
227 if (!std::isfinite(value)) return std::nullopt;
228 }
229 else
230 {
231 const auto res = std::from_chars(std::to_address(text.begin()), std::to_address(text.end()), value);
232 if (res.ec != std::errc{} || res.ptr != std::to_address(text.end())) return std::nullopt;
233 }
234 return value;
235}
236
237// A whole stream in one allocation and one read; the byte-at-a-time
238// istreambuf_iterator costs about 5 ns a byte.
239inline std::string read_all(std::ifstream& in)
240{
241 in.seekg(0, std::ios::end);
242 const auto size = in.tellg();
243 if (size < 0) return std::string{std::istreambuf_iterator<char>{in}, std::istreambuf_iterator<char>{}};
244 std::string text(static_cast<std::size_t>(size), '\0');
245 in.seekg(0, std::ios::beg);
246 in.read(text.data(), static_cast<std::streamsize>(text.size()));
247 text.resize(static_cast<std::size_t>(in.gcount()));
248 return text;
249}
250
251// The parsed content of a document, shared by its copies and its rows.
252struct tsv_data
253{
254 std::string text;
255 std::string source;
256 std::vector<std::string> columns;
257 std::map<std::string, std::size_t, std::less<>> column_index;
258 // A cell is where it lies in text: 8 bytes instead of a string_view's 16.
259 struct cell_span
260 {
261 std::uint32_t offset{0};
262 std::uint32_t length{0};
263 };
264 std::vector<cell_span> cells; // rows * columns
265 std::vector<std::uint32_t> row_lines; // 1-based
266
267 [[nodiscard]] std::string_view cell_at(std::size_t i) const noexcept
268 {
269 const cell_span c = cells[i];
270 return std::string_view{text}.substr(c.offset, c.length);
271 }
272 std::uint64_t hash{0};
273
274 [[nodiscard]] std::optional<std::size_t> find(std::string_view name) const
275 {
276 const auto it = column_index.find(name);
277 if (it == column_index.end()) return std::nullopt;
278 return it->second;
279 }
280};
281
282} // namespace detail
283/// @endcond
284
285class tsv_document;
286
287/// @brief One row of a tsv_document. A light view: copy it freely while the
288/// document, or any copy of it, is alive.
290{
291 public:
292 /// @brief The cell in a named column.
293 /// @param column Column name.
294 /// @return The cell text (empty for a missing trailing cell).
295 /// @throws tsv_error If the document has no such column.
296 [[nodiscard]] std::string_view operator[](std::string_view column) const;
297
298 /// @brief The cell at a column index.
299 /// @param index 0-based column index.
300 /// @return The cell text.
301 /// @throws std::out_of_range If @p index is not a column.
302 [[nodiscard]] std::string_view cell(std::size_t index) const;
303
304 /// @brief Whether the document has a column and this row's cell in it is
305 /// not empty.
306 /// @param column Column name.
307 /// @return True when a value is present.
308 [[nodiscard]] bool has(std::string_view column) const;
309
310 /// @brief A typed value.
311 ///
312 /// Integers are base 10; doubles are decimal or scientific and must be
313 /// finite; bool accepts `1`, `0`, `true` and `false`; strings are
314 /// returned as they are.
315 ///
316 /// @tparam T Arithmetic type, bool, `std::string` or `std::string_view`.
317 /// @param column Column name.
318 /// @return The value.
319 /// @throws tsv_error If the column is missing, the cell is empty, or the
320 /// text is not a valid T.
321 template <typename T>
322 [[nodiscard]] T get(std::string_view column) const
323 {
324 const std::string_view text = (*this)[column];
325 if (text.empty())
326 {
327 throw error(column, "missing value");
328 }
329 return convert<T>(column, text);
330 }
331
332 /// @brief A typed value, or a fallback when the column is missing or the
333 /// cell is empty.
334 /// @tparam T As get().
335 /// @param column Column name.
336 /// @param fallback Value for a missing or empty cell.
337 /// @return The value.
338 /// @throws tsv_error If the text is not a valid T.
339 template <typename T>
340 [[nodiscard]] T get_or(std::string_view column, T fallback) const
341 {
342 if (!has(column)) return fallback;
343 return convert<T>(column, (*this)[column]);
344 }
345
346 /// @brief Split a list cell.
347 /// @param column Column name.
348 /// @param separator List separator (`;` in the shipped data).
349 /// @return The items; empty for an empty cell. Items are not trimmed.
350 /// @throws tsv_error If the column is missing.
351 [[nodiscard]] std::vector<std::string_view> list(std::string_view column,
352 char separator = ';') const
353 {
354 std::vector<std::string_view> items;
355 std::string_view text = (*this)[column];
356 if (text.empty()) return items;
357 for (;;)
358 {
359 const auto pos = text.find(separator);
360 items.push_back(text.substr(0, pos));
361 if (pos == std::string_view::npos) break;
362 text.remove_prefix(pos + 1);
363 }
364 return items;
365 }
366
367 /// @brief The 1-based line of this row in its source.
368 /// @return The line number.
369 [[nodiscard]] std::size_t line() const noexcept;
370
371 /// @brief The source of this row's document.
372 /// @return File name or description.
373 [[nodiscard]] const std::string& source() const noexcept;
374
375 /// @brief An error located at this row.
376 /// @param column Column name, or empty for the whole row.
377 /// @param message What is wrong.
378 /// @return The error, for throwing.
379 [[nodiscard]] tsv_error error(std::string_view column, const std::string& message) const
380 {
381 return tsv_error{source(), line(), std::string{column}, message};
382 }
383
384 private:
385 friend class tsv_document;
386
387 tsv_row(const detail::tsv_data* data, std::size_t index) noexcept
388 : _data{data}, _index{index} {}
389
390 template <typename T>
391 [[nodiscard]] T convert(std::string_view column, std::string_view text) const
392 {
393 if constexpr (std::same_as<T, std::string_view>)
394 {
395 return text;
396 }
397 else if constexpr (std::same_as<T, std::string>)
398 {
399 return std::string{text};
400 }
401 else if constexpr (std::same_as<T, bool>)
402 {
403 if (text == "1" || text == "true") return true;
404 if (text == "0" || text == "false") return false;
405 throw error(column, "expected 0, 1, true or false, got '" + std::string{text} + "'");
406 }
407 else
408 {
409 static_assert(std::is_arithmetic_v<T>, "tsv_row::get: unsupported type");
410 if (const auto v = detail::parse_number<T>(text)) return *v;
411 throw error(column, "expected a number, got '" + std::string{text} + "'");
412 }
413 }
414
415 const detail::tsv_data* _data;
416 std::size_t _index;
417};
418
419/// @brief A parsed tab-separated file.
420///
421/// Copies share the text, so rows taken from one copy stay valid while any
422/// copy is alive.
424{
425 public:
426 /// @brief Parse text whose first line names the columns.
427 /// @param text UTF-8 text.
428 /// @param source Name used in errors (a file name, or a description such
429 /// as "layer from host").
430 /// @return The document.
431 /// @throws tsv_error On invalid UTF-8, no header, an empty or repeated
432 /// column name, or a row with more cells than columns.
433 [[nodiscard]] static tsv_document parse(std::string_view text, std::string source)
434 {
435 return tsv_document{std::string{text}, std::move(source), {}};
436 }
437
438 /// @brief Parse headerless text with the given column names.
439 /// @param text UTF-8 text.
440 /// @param source Name used in errors.
441 /// @param columns Column names.
442 /// @return The document.
443 /// @throws tsv_error As the other overload.
444 [[nodiscard]] static tsv_document parse(std::string_view text, std::string source,
445 std::vector<std::string> columns)
446 {
447 if (columns.empty())
448 {
449 throw tsv_error{std::move(source), 0, "", "no columns given"};
450 }
451 return tsv_document{std::string{text}, std::move(source), std::move(columns)};
452 }
453
454 /// @brief Read and parse a file.
455 /// @param path File to read; it is also the source name in errors.
456 /// @param columns Column names for a headerless file; empty when the
457 /// first line names them.
458 /// @return The document.
459 /// @throws tsv_error If the file cannot be read, or as parse().
460 [[nodiscard]] static tsv_document load(const std::filesystem::path& path,
461 std::vector<std::string> columns = {})
462 {
463 std::ifstream in{path, std::ios::binary};
464 if (!in)
465 {
466 throw tsv_error{path.generic_string(), 0, "", "cannot open file"};
467 }
468 std::string text = detail::read_all(in);
469 if (columns.empty())
470 {
471 return tsv_document{std::move(text), path.generic_string(), {}};
472 }
473 return tsv_document{std::move(text), path.generic_string(), std::move(columns)};
474 }
475
476 /// @brief The column names, in file order.
477 /// @return The columns.
478 [[nodiscard]] const std::vector<std::string>& columns() const noexcept
479 {
480 return _data->columns;
481 }
482
483 /// @brief The index of a column.
484 /// @param name Column name.
485 /// @return The 0-based index, or empty when there is no such column.
486 [[nodiscard]] std::optional<std::size_t> column_index(std::string_view name) const
487 {
488 return _data->find(name);
489 }
490
491 /// @brief Whether a column exists.
492 /// @param name Column name.
493 /// @return True when present.
494 [[nodiscard]] bool has_column(std::string_view name) const
495 {
496 return column_index(name).has_value();
497 }
498
499 /// @brief Number of data rows.
500 /// @return The row count.
501 [[nodiscard]] std::size_t size() const noexcept { return _data->row_lines.size(); }
502
503 /// @brief Whether there are no data rows.
504 /// @return True when empty.
505 [[nodiscard]] bool empty() const noexcept { return size() == 0; }
506
507 /// @brief A row.
508 /// @param i 0-based row index.
509 /// @return The row.
510 /// @throws std::out_of_range If @p i >= size().
511 [[nodiscard]] tsv_row row(std::size_t i) const
512 {
513 if (i >= size()) throw std::out_of_range("tsv_document::row: index out of range");
514 return tsv_row{_data.get(), i};
515 }
516
517 /// @brief All rows.
518 /// @return The rows, in file order.
519 [[nodiscard]] std::vector<tsv_row> rows() const
520 {
521 std::vector<tsv_row> out;
522 out.reserve(size());
523 for (std::size_t i = 0; i < size(); ++i) out.push_back(tsv_row{_data.get(), i});
524 return out;
525 }
526
527 /// @brief The source name used in errors.
528 /// @return File name or description.
529 [[nodiscard]] const std::string& source() const noexcept { return _data->source; }
530
531 /// @brief Hash of the parsed content: columns and cells, without
532 /// comments, blank lines, line-end style or byte order mark.
533 ///
534 /// The same data checked out with LF or CRLF line ends hashes the same.
535 ///
536 /// @return 64-bit FNV-1a of the canonical content.
537 [[nodiscard]] std::uint64_t content_hash() const noexcept { return _data->hash; }
538
539 private:
540 tsv_document(std::string text, std::string source, std::vector<std::string> columns)
541 : _data{std::make_shared<detail::tsv_data>()}
542 {
543 auto& d = *_data;
544 d.text = std::move(text);
545 d.source = std::move(source);
546 if (const auto bad = utf8_invalid_offset(d.text); bad != std::string_view::npos)
547 {
548 throw tsv_error{d.source, line_of(d.text, bad), "", "invalid UTF-8"};
549 }
550 // Cells and lines are held as 32-bit offsets.
551 if (d.text.size() >= std::numeric_limits<std::uint32_t>::max())
552 {
553 throw tsv_error{d.source, 0, "", "file is too large (4 GiB or more)"};
554 }
555 std::string_view rest = d.text;
556 if (rest.starts_with("\xEF\xBB\xBF")) rest.remove_prefix(3);
557
558 const bool header = columns.empty();
559 d.columns = std::move(columns);
560 // One reservation for the rows, from the line count (an upper bound
561 // when there are comments and blank lines).
562 const auto line_count = static_cast<std::size_t>(std::ranges::count(rest, '\n')) + 1;
563 std::size_t line = 0;
564 bool have_header = !header;
565 if (!header) index_columns(0);
566
567 while (!rest.empty())
568 {
569 ++line;
570 const auto end = rest.find('\n');
571 std::string_view text_line = rest.substr(0, end);
572 rest.remove_prefix(end == std::string_view::npos ? rest.size() : end + 1);
573 if (text_line.ends_with('\r')) text_line.remove_suffix(1);
574 if (text_line.empty() || text_line.front() == '#') continue;
575
576 if (!have_header)
577 {
578 for (const auto cell : split(text_line)) d.columns.emplace_back(cell);
579 index_columns(line);
580 have_header = true;
581 continue;
582 }
583 const auto width = d.columns.size();
584 const auto count = static_cast<std::size_t>(std::ranges::count(text_line, '\t')) + 1;
585 if (count > width)
586 {
587 throw tsv_error{d.source, line, "",
588 "row has " + std::to_string(count) + " cells but there are " +
589 std::to_string(width) + " columns"};
590 }
591 if (d.row_lines.empty())
592 {
593 d.cells.reserve(line_count * width);
594 d.row_lines.reserve(line_count);
595 }
596 std::string_view remaining = text_line;
597 for (;;)
598 {
599 const auto pos = remaining.find('\t');
600 const auto cell = remaining.substr(0, pos);
601 d.cells.push_back({static_cast<std::uint32_t>(cell.data() - d.text.data()),
602 static_cast<std::uint32_t>(cell.size())});
603 if (pos == std::string_view::npos) break;
604 remaining.remove_prefix(pos + 1);
605 }
606 d.cells.resize(d.cells.size() + (width - count));
607 d.row_lines.push_back(static_cast<std::uint32_t>(line));
608 }
609 if (!have_header)
610 {
611 throw tsv_error{d.source, 0, "", "no header line"};
612 }
613 d.hash = canonical_hash();
614 }
615
616 static std::vector<std::string_view> split(std::string_view line)
617 {
618 std::vector<std::string_view> cells;
619 for (;;)
620 {
621 const auto pos = line.find('\t');
622 cells.push_back(line.substr(0, pos));
623 if (pos == std::string_view::npos) break;
624 line.remove_prefix(pos + 1);
625 }
626 return cells;
627 }
628
629 static std::size_t line_of(std::string_view text, std::size_t offset)
630 {
631 std::size_t line = 1;
632 for (std::size_t i = 0; i < offset && i < text.size(); ++i)
633 {
634 if (text[i] == '\n') ++line;
635 }
636 return line;
637 }
638
639 void index_columns(std::size_t line)
640 {
641 auto& d = *_data;
642 for (std::size_t i = 0; i < d.columns.size(); ++i)
643 {
644 if (d.columns[i].empty())
645 {
646 throw tsv_error{d.source, line, "", "column " + std::to_string(i + 1) + " has no name"};
647 }
648 if (!d.column_index.emplace(d.columns[i], i).second)
649 {
650 throw tsv_error{d.source, line, d.columns[i], "column name repeated"};
651 }
652 }
653 }
654
655 [[nodiscard]] std::uint64_t canonical_hash() const
656 {
657 const auto& d = *_data;
658 std::uint64_t h = 0xcbf29ce484222325ULL;
659 auto feed = [&h](std::string_view s) {
660 for (const char c : s)
661 {
662 h ^= static_cast<std::uint8_t>(c);
663 h *= 0x100000001b3ULL;
664 }
665 };
666 auto feed_row = [&](auto first, auto last) {
667 for (auto it = first; it != last; ++it)
668 {
669 if (it != first) feed("\t");
670 feed(*it);
671 }
672 feed("\n");
673 };
674 feed_row(d.columns.begin(), d.columns.end());
675 const auto width = static_cast<std::ptrdiff_t>(d.columns.size());
676 for (std::size_t r = 0; r < d.row_lines.size(); ++r)
677 {
678 for (std::ptrdiff_t c = 0; c < width; ++c)
679 {
680 if (c > 0) feed("\t");
681 feed(d.cell_at((r * static_cast<std::size_t>(width)) + static_cast<std::size_t>(c)));
682 }
683 feed("\n");
684 }
685 return h;
686 }
687
688 std::shared_ptr<detail::tsv_data> _data;
689};
690
691inline std::string_view tsv_row::operator[](std::string_view column) const
692{
693 const auto index = _data->find(column);
694 if (!index)
695 {
696 throw error(column, "no such column");
697 }
698 return cell(*index);
699}
700
701inline std::string_view tsv_row::cell(std::size_t index) const
702{
703 const auto width = _data->columns.size();
704 if (index >= width) throw std::out_of_range("tsv_row::cell: index out of range");
705 return _data->cell_at((_index * width) + index);
706}
707
708inline bool tsv_row::has(std::string_view column) const
709{
710 const auto index = _data->find(column);
711 return index && !cell(*index).empty();
712}
713
714inline std::size_t tsv_row::line() const noexcept { return _data->row_lines[_index]; }
715
716inline const std::string& tsv_row::source() const noexcept { return _data->source; }
717
718} // namespace dasmig::core
719
720#endif // DASMIG_CORE_TSV_HPP
A parsed tab-separated file.
Definition tsv.hpp:424
static tsv_document load(const std::filesystem::path &path, std::vector< std::string > columns={})
Read and parse a file.
Definition tsv.hpp:460
static tsv_document parse(std::string_view text, std::string source, std::vector< std::string > columns)
Parse headerless text with the given column names.
Definition tsv.hpp:444
std::size_t size() const noexcept
Number of data rows.
Definition tsv.hpp:501
static tsv_document parse(std::string_view text, std::string source)
Parse text whose first line names the columns.
Definition tsv.hpp:433
bool has_column(std::string_view name) const
Whether a column exists.
Definition tsv.hpp:494
bool empty() const noexcept
Whether there are no data rows.
Definition tsv.hpp:505
const std::vector< std::string > & columns() const noexcept
The column names, in file order.
Definition tsv.hpp:478
const std::string & source() const noexcept
The source name used in errors.
Definition tsv.hpp:529
std::uint64_t content_hash() const noexcept
Hash of the parsed content: columns and cells, without comments, blank lines, line-end style or byte ...
Definition tsv.hpp:537
std::optional< std::size_t > column_index(std::string_view name) const
The index of a column.
Definition tsv.hpp:486
std::vector< tsv_row > rows() const
All rows.
Definition tsv.hpp:519
tsv_row row(std::size_t i) const
A row.
Definition tsv.hpp:511
A data error with its location.
Definition tsv.hpp:59
const std::string & source() const noexcept
The data source.
Definition tsv.hpp:75
const std::string & column() const noexcept
The column.
Definition tsv.hpp:83
tsv_error(std::string source, std::size_t line, std::string column, const std::string &message)
Construct.
Definition tsv.hpp:66
std::size_t line() const noexcept
The line.
Definition tsv.hpp:79
One row of a tsv_document. A light view: copy it freely while the document, or any copy of it,...
Definition tsv.hpp:290
T get_or(std::string_view column, T fallback) const
A typed value, or a fallback when the column is missing or the cell is empty.
Definition tsv.hpp:340
const std::string & source() const noexcept
The source of this row's document.
Definition tsv.hpp:716
std::string_view cell(std::size_t index) const
The cell at a column index.
Definition tsv.hpp:701
tsv_error error(std::string_view column, const std::string &message) const
An error located at this row.
Definition tsv.hpp:379
T get(std::string_view column) const
A typed value.
Definition tsv.hpp:322
std::vector< std::string_view > list(std::string_view column, char separator=';') const
Split a list cell.
Definition tsv.hpp:351
std::size_t line() const noexcept
The 1-based line of this row in its source.
Definition tsv.hpp:714
bool has(std::string_view column) const
Whether the document has a column and this row's cell in it is not empty.
Definition tsv.hpp:708
std::string_view operator[](std::string_view column) const
The cell in a named column.
Definition tsv.hpp:691
The random engine every generator library uses: xoshiro256** seeded through SplitMix64,...
Conversion between UTF-8 and std::wstring.
constexpr std::size_t utf8_invalid_offset(std::string_view text) noexcept
Whether a byte string is well-formed UTF-8.
Definition utf8.hpp:32