diff options
Diffstat (limited to 'nihil.core/tabulate.ccm')
| -rw-r--r-- | nihil.core/tabulate.ccm | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/nihil.core/tabulate.ccm b/nihil.core/tabulate.ccm new file mode 100644 index 0000000..98a957c --- /dev/null +++ b/nihil.core/tabulate.ccm @@ -0,0 +1,298 @@ +// This source code is released into the public domain. +export module nihil.core:tabulate; + +import nihil.std; + +import :ctype; +import :error; + +namespace nihil { + +// tabulate: format the given range in an ASCII table and write the output +// to the given output iterator. The range's values will be converted to +// strings as if by std::format. +// +// tabulate is implemented by copying the range; this allows it to work on +// input/forward ranges at the cost of slightly increased memory use. +// +// The table spec is a string consisting of zero or more field formats, +// formatted as {flags:fieldname}; both flags and fieldname are optional. +// If there are fewer field formats than fields, the remaining fields +// are formatted as if by {:}. +// +// The following flags are supported: +// +// < left-align this column (default) +// > right-align this column + +// Exception thrown when a table spec is invalid. +export struct table_spec_error : error { + explicit table_spec_error(std::string_view what) + : error(what) + { + } +}; + +// The specification for a single field. +template<typename Char> +struct field_spec { + enum align_t { left, right }; + + // Get the name of this field. + auto name(this field_spec const &self) + -> std::basic_string_view<Char> + { + return self.m_name; + } + + // Set the name of this field. + auto name(this field_spec &self, + std::basic_string_view<Char> new_name) + -> void + { + self.m_name = new_name; + } + + // Set this field's alignment. + auto align(this field_spec &self, align_t new_align) -> void + { + self.m_align = new_align; + } + + // Ensure the length of this field is at least the given width. + auto ensure_width(this field_spec &self, std::size_t newwidth) + -> void + { + self.m_width = std::max(self.m_width, newwidth); + } + + // Format an object to a string based on our field spec. + [[nodiscard]] auto format(this field_spec const &, auto &&obj) + -> std::basic_string<Char> + { + auto format_string = std::basic_string<Char>{'{', '}'}; + return std::format(std::runtime_format(format_string), obj); + } + + // Print a column value to an output iterator according to our field + // spec. If is_last is true, this is the last field on the line, so + // we won't output any trailling padding. + auto print(this field_spec const &self, + std::basic_string_view<Char> value, + std::output_iterator<Char> auto &out, + bool is_last) + -> void + { + auto padding = self.m_width - value.size(); + + if (self.m_align == right) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + + std::ranges::copy(value, out); + + if (!is_last && self.m_align == left) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + } + +private: + std::basic_string_view<Char> m_name; + std::size_t m_width = 0; + align_t m_align = left; +}; + +/* + * The specification for an entire table. + */ +template<typename Char> +struct table_spec { + // Add a new field spec to this table. + auto add(this table_spec &self, field_spec<Char> field) -> void + { + self.m_fields.emplace_back(std::move(field)); + } + + // Return the field spec for a given field. If the field doesn't + // exist, this field and any intermediate fields will be created. + [[nodiscard]] auto field(this table_spec &self, std::size_t fieldnr) + -> field_spec<Char> & + { + if (fieldnr >= self.m_fields.size()) + self.m_fields.resize(fieldnr + 1); + return self.m_fields.at(fieldnr); + } + + // The number of columns in this table. + [[nodiscard]] auto columns(this table_spec const &self) -> std::size_t + { + return self.m_fields.size(); + } + + // Return all the fields in this table. + [[nodiscard]] auto fields(this table_spec const &self) + -> std::vector<field_spec<Char>> const & + { + return self.m_fields; + } + +private: + std::vector<field_spec<Char>> m_fields; +}; + +// Parse the field flags, e.g. '<'. +template<typename Char, + std::input_iterator Iterator, std::sentinel_for<Iterator> Sentinel> +auto parse_field_flags(field_spec<Char> &field, Iterator &pos, Sentinel end) + -> void +{ + while (pos < end) { + switch (*pos) { + case '<': + field.align(field_spec<Char>::left); + break; + case '>': + field.align(field_spec<Char>::right); + break; + case ':': + ++pos; + /*FALLTHROUGH*/ + case '}': + return; + default: + throw table_spec_error("Invalid table spec: " + "unknown flag character"); + } + + if (++pos == end) + throw table_spec_error("Invalid table spec: " + "unterminated field"); + } +} + +// Parse a complete field spec, e.g. "{<:NAME}". +template<typename Char, + std::input_iterator Iterator, std::sentinel_for<Iterator> Sentinel> +[[nodiscard]] auto parse_field(Iterator &pos, Sentinel end) + -> field_spec<Char> +{ + auto field = field_spec<Char>{}; + + if (pos == end) + throw table_spec_error("Invalid table spec: empty field"); + + // The field spec should start with a '{'. + if (*pos != '{') + throw table_spec_error("Invalid table spec: expected '{'"); + + if (++pos == end) + throw table_spec_error("Invalid table spec: unterminated field"); + + // This consumes 'pos' up to and including the ':'. + parse_field_flags(field, pos, end); + + auto brace = std::ranges::find(pos, end, '}'); + if (brace == end) + throw table_spec_error("Invalid table spec: expected '}'"); + + field.name(std::basic_string_view<Char>(pos, brace)); + pos = std::next(brace); + + // The field must be at least as wide as its header. + field.ensure_width(field.name().size()); + + return field; +} + +template<typename Char> +[[nodiscard]] auto parse_table_spec(std::basic_string_view<Char> spec) + -> table_spec<Char> +{ + auto table = table_spec<Char>(); + + auto pos = std::ranges::begin(spec); + auto end = std::ranges::end(spec); + + for (;;) { + // Skip leading whitespace + while (pos < end && is_c_space(*pos)) + ++pos; + + if (pos == end) + break; + + table.add(parse_field<Char>(pos, end)); + } + + return table; +} + +export template<typename Char, + std::ranges::range Range, + std::output_iterator<Char> Iterator> +auto basic_tabulate(std::basic_string_view<Char> table_spec, + Range &&range, + Iterator &&out) + -> void +{ + // Parse the table spec. + auto table = parse_table_spec(table_spec); + + // Create our copy of the input data. + auto data = std::vector<std::vector<std::basic_string<Char>>>(); + // Reserve the first row for the header. + data.resize(1); + + // Find the required length of each field. + for (auto &&row : range) { + // LLVM doesn't have std::enumerate_view yet + auto i = std::size_t{0}; + auto &this_row = data.emplace_back(); + + for (auto &&column : row) { + auto &field = table.field(i); + auto &str = this_row.emplace_back(field.format(column)); + field.ensure_width(str.size()); + ++i; + } + } + + // Add the header row. + for (auto &&field : table.fields()) + data.at(0).emplace_back(std::from_range, field.name()); + + // Print the values. + for (auto &&row : data) { + for (std::size_t i = 0; i < row.size(); ++i) { + auto &field = table.field(i); + bool is_last = (i == row.size() - 1); + + field.print(row[i], out, is_last); + + if (!is_last) + *out++ = ' '; + } + + *out++ = '\n'; + } +} + +export auto tabulate(std::string_view table_spec, + std::ranges::range auto &&range, + std::output_iterator<char> auto &&out) +{ + return basic_tabulate<char>(table_spec, + std::forward<decltype(range)>(range), + std::forward<decltype(out)>(out)); +} + +export auto wtabulate(std::wstring_view table_spec, + std::ranges::range auto &&range, + std::output_iterator<wchar_t> auto &&out) +{ + return basic_tabulate<wchar_t>(table_spec, + std::forward<decltype(range)>(range), + std::forward<decltype(out)>(out)); +} + +} // namespace nihil |
