4#ifndef DASMIG_CORE_TSV_HPP
5#define DASMIG_CORE_TSV_HPP
31#include <system_error>
67 const std::string& message)
75 [[nodiscard]]
const std::string&
source() const noexcept {
return _source; }
79 [[nodiscard]] std::size_t
line() const noexcept {
return _line; }
83 [[nodiscard]]
const std::string&
column() const noexcept {
return _column; }
86 static std::string format(
const std::string&
source, std::size_t
line,
87 const std::string&
column,
const std::string& message)
90 if (
line > 0) out +=
":" + std::to_string(
line);
107struct decimal_significand
109 std::uint64_t mantissa{0};
117[[nodiscard]]
inline std::optional<decimal_significand> scan_significand(std::string_view text, std::size_t i)
119 constexpr int max_digits = 19;
120 decimal_significand out;
124 for (; i < text.size(); ++i)
126 const char c = text[i];
127 if (c ==
'.' && !point)
132 if (c <
'0' || c >
'9')
break;
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');
139 if (!any)
return std::nullopt;
146[[nodiscard]]
inline std::optional<int> scan_exponent(std::string_view text, std::size_t i)
148 if (i == text.size())
return 0;
149 if (text[i] !=
'e' && text[i] !=
'E')
return std::nullopt;
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;
155 for (; i < text.size(); ++i)
157 if (text[i] <
'0' || text[i] >
'9')
return std::nullopt;
158 value = value * 10 + (text[i] -
'0');
160 return negative ? -value : value;
171[[nodiscard]]
inline std::optional<double> parse_decimal_fast(std::string_view text)
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;
178 constexpr int max_power = 22;
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;
187 if (significand->mantissa != 0)
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)];
193 value = power < 0 ? m / scale : m * scale;
195 return negative ? -value : value;
205[[nodiscard]] std::optional<T> parse_number(std::string_view text)
207 if (text.size() > 1 && text.front() ==
'+' && text[1] !=
'-') text.remove_prefix(1);
208 if (text.empty())
return std::nullopt;
210 if constexpr (std::floating_point<T>)
212 if constexpr (std::same_as<T, double>)
214 if (
const auto fast = parse_decimal_fast(text))
return *fast;
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;
222 std::istringstream in{std::string{text}};
223 in.imbue(std::locale::classic());
225 if (in.fail() || in.peek() != std::char_traits<char>::eof())
return std::nullopt;
227 if (!std::isfinite(value))
return std::nullopt;
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;
239inline std::string read_all(std::ifstream& in)
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()));
256 std::vector<std::string> columns;
257 std::map<std::string, std::size_t, std::less<>> column_index;
261 std::uint32_t offset{0};
262 std::uint32_t length{0};
264 std::vector<cell_span> cells;
265 std::vector<std::uint32_t> row_lines;
267 [[nodiscard]] std::string_view cell_at(std::size_t i)
const noexcept
269 const cell_span c = cells[i];
270 return std::string_view{text}.substr(c.offset, c.length);
272 std::uint64_t hash{0};
274 [[nodiscard]] std::optional<std::size_t> find(std::string_view name)
const
276 const auto it = column_index.find(name);
277 if (it == column_index.end())
return std::nullopt;
296 [[nodiscard]] std::string_view
operator[](std::string_view column)
const;
302 [[nodiscard]] std::string_view
cell(std::size_t index)
const;
308 [[nodiscard]]
bool has(std::string_view column)
const;
321 template <
typename T>
322 [[nodiscard]] T
get(std::string_view column)
const
324 const std::string_view text = (*this)[column];
327 throw error(column,
"missing value");
329 return convert<T>(column, text);
339 template <
typename T>
340 [[nodiscard]] T
get_or(std::string_view column, T fallback)
const
342 if (!
has(column))
return fallback;
343 return convert<T>(column, (*
this)[column]);
351 [[nodiscard]] std::vector<std::string_view>
list(std::string_view column,
352 char separator =
';')
const
354 std::vector<std::string_view> items;
355 std::string_view text = (*this)[column];
356 if (text.empty())
return items;
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);
369 [[nodiscard]] std::size_t
line() const noexcept;
373 [[nodiscard]] const std::
string&
source() const noexcept;
379 [[nodiscard]]
tsv_error error(std::string_view column, const std::
string& message)
const
387 tsv_row(
const detail::tsv_data* data, std::size_t index) noexcept
388 : _data{data}, _index{index} {}
390 template <
typename T>
391 [[nodiscard]] T convert(std::string_view column, std::string_view text)
const
393 if constexpr (std::same_as<T, std::string_view>)
397 else if constexpr (std::same_as<T, std::string>)
399 return std::string{text};
401 else if constexpr (std::same_as<T, bool>)
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} +
"'");
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} +
"'");
415 const detail::tsv_data* _data;
445 std::vector<std::string>
columns)
461 std::vector<std::string>
columns = {})
463 std::ifstream in{path, std::ios::binary};
466 throw tsv_error{path.generic_string(), 0,
"",
"cannot open file"};
468 std::string text = detail::read_all(in);
471 return tsv_document{std::move(text), path.generic_string(), {}};
473 return tsv_document{std::move(text), path.generic_string(), std::move(
columns)};
478 [[nodiscard]]
const std::vector<std::string>&
columns() const noexcept
480 return _data->columns;
486 [[nodiscard]] std::optional<std::size_t>
column_index(std::string_view name)
const
488 return _data->find(name);
501 [[nodiscard]] std::size_t
size() const noexcept {
return _data->row_lines.size(); }
505 [[nodiscard]]
bool empty() const noexcept {
return size() == 0; }
513 if (i >=
size())
throw std::out_of_range(
"tsv_document::row: index out of range");
514 return tsv_row{_data.get(), i};
519 [[nodiscard]] std::vector<tsv_row>
rows()
const
521 std::vector<tsv_row> out;
523 for (std::size_t i = 0; i <
size(); ++i) out.push_back(
tsv_row{_data.get(), i});
529 [[nodiscard]]
const std::string&
source() const noexcept {
return _data->source; }
537 [[nodiscard]] std::uint64_t
content_hash() const noexcept {
return _data->hash; }
540 tsv_document(std::string text, std::string source, std::vector<std::string> columns)
541 : _data{std::make_shared<detail::tsv_data>()}
544 d.text = std::move(text);
545 d.source = std::move(source);
551 if (d.text.size() >= std::numeric_limits<std::uint32_t>::max())
553 throw tsv_error{d.source, 0,
"",
"file is too large (4 GiB or more)"};
555 std::string_view rest = d.text;
556 if (rest.starts_with(
"\xEF\xBB\xBF")) rest.remove_prefix(3);
558 const bool header = columns.empty();
559 d.columns = std::move(columns);
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);
567 while (!rest.empty())
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;
578 for (
const auto cell : split(text_line)) d.columns.emplace_back(cell);
583 const auto width = d.columns.size();
584 const auto count =
static_cast<std::size_t
>(std::ranges::count(text_line,
'\t')) + 1;
587 throw tsv_error{d.source, line,
"",
588 "row has " + std::to_string(count) +
" cells but there are " +
589 std::to_string(width) +
" columns"};
591 if (d.row_lines.empty())
593 d.cells.reserve(line_count * width);
594 d.row_lines.reserve(line_count);
596 std::string_view remaining = text_line;
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);
606 d.cells.resize(d.cells.size() + (width - count));
607 d.row_lines.push_back(
static_cast<std::uint32_t
>(line));
611 throw tsv_error{d.source, 0,
"",
"no header line"};
613 d.hash = canonical_hash();
616 static std::vector<std::string_view> split(std::string_view line)
618 std::vector<std::string_view> cells;
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);
629 static std::size_t line_of(std::string_view text, std::size_t offset)
631 std::size_t line = 1;
632 for (std::size_t i = 0; i < offset && i < text.size(); ++i)
634 if (text[i] ==
'\n') ++line;
639 void index_columns(std::size_t line)
642 for (std::size_t i = 0; i < d.columns.size(); ++i)
644 if (d.columns[i].empty())
646 throw tsv_error{d.source, line,
"",
"column " + std::to_string(i + 1) +
" has no name"};
648 if (!d.column_index.emplace(d.columns[i], i).second)
650 throw tsv_error{d.source, line, d.columns[i],
"column name repeated"};
655 [[nodiscard]] std::uint64_t canonical_hash()
const
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)
662 h ^=
static_cast<std::uint8_t
>(c);
663 h *= 0x100000001b3ULL;
666 auto feed_row = [&](
auto first,
auto last) {
667 for (
auto it = first; it != last; ++it)
669 if (it != first) feed(
"\t");
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)
678 for (std::ptrdiff_t c = 0; c < width; ++c)
680 if (c > 0) feed(
"\t");
681 feed(d.cell_at((r *
static_cast<std::size_t
>(width)) +
static_cast<std::size_t
>(c)));
688 std::shared_ptr<detail::tsv_data> _data;
691inline std::string_view tsv_row::operator[](std::string_view column)
const
693 const auto index = _data->find(column);
696 throw error(column,
"no such column");
701inline std::string_view tsv_row::cell(std::size_t index)
const
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);
708inline bool tsv_row::has(std::string_view column)
const
710 const auto index = _data->find(column);
711 return index && !cell(*index).empty();
714inline std::size_t tsv_row::line() const noexcept {
return _data->row_lines[_index]; }
716inline const std::string& tsv_row::source() const noexcept {
return _data->source; }
A parsed tab-separated file.
static tsv_document load(const std::filesystem::path &path, std::vector< std::string > columns={})
Read and parse a file.
static tsv_document parse(std::string_view text, std::string source, std::vector< std::string > columns)
Parse headerless text with the given column names.
std::size_t size() const noexcept
Number of data rows.
static tsv_document parse(std::string_view text, std::string source)
Parse text whose first line names the columns.
bool has_column(std::string_view name) const
Whether a column exists.
bool empty() const noexcept
Whether there are no data rows.
const std::vector< std::string > & columns() const noexcept
The column names, in file order.
const std::string & source() const noexcept
The source name used in errors.
std::uint64_t content_hash() const noexcept
Hash of the parsed content: columns and cells, without comments, blank lines, line-end style or byte ...
std::optional< std::size_t > column_index(std::string_view name) const
The index of a column.
std::vector< tsv_row > rows() const
All rows.
tsv_row row(std::size_t i) const
A row.
A data error with its location.
const std::string & source() const noexcept
The data source.
const std::string & column() const noexcept
The column.
tsv_error(std::string source, std::size_t line, std::string column, const std::string &message)
Construct.
std::size_t line() const noexcept
The line.
One row of a tsv_document. A light view: copy it freely while the document, or any copy of it,...
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.
const std::string & source() const noexcept
The source of this row's document.
std::string_view cell(std::size_t index) const
The cell at a column index.
tsv_error error(std::string_view column, const std::string &message) const
An error located at this row.
T get(std::string_view column) const
A typed value.
std::vector< std::string_view > list(std::string_view column, char separator=';') const
Split a list cell.
std::size_t line() const noexcept
The 1-based line of this row in its source.
bool has(std::string_view column) const
Whether the document has a column and this row's cell in it is not empty.
std::string_view operator[](std::string_view column) const
The cell in a named column.
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.