// This source code is released into the public domain. export module nihil.util:tabulate; import nihil.std; import nihil.error; import :ctype; 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 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 { return self.m_name; } // Set the name of this field. auto name(this field_spec &self, std::basic_string_view 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 { auto format_string = std::basic_string{'{', '}'}; 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 value, std::output_iterator 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 m_name; std::size_t m_width = 0; align_t m_align = left; }; /* * The specification for an entire table. */ template struct table_spec { // Add a new field spec to this table. auto add(this table_spec &self, field_spec 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 & { 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> const & { return self.m_fields; } private: std::vector> m_fields; }; // Parse the field flags, e.g. '<'. template Sentinel> auto parse_field_flags(field_spec &field, Iterator &pos, Sentinel end) -> void { while (pos < end) { switch (*pos) { case '<': field.align(field_spec::left); break; case '>': field.align(field_spec::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 Sentinel> [[nodiscard]] auto parse_field(Iterator &pos, Sentinel end) -> field_spec { auto field = field_spec{}; 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(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 [[nodiscard]] auto parse_table_spec(std::basic_string_view spec) -> table_spec { auto table = table_spec(); 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(pos, end)); } return table; } export template Iterator> auto basic_tabulate(std::basic_string_view 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>>(); // 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 auto &&out) { return basic_tabulate(table_spec, std::forward(range), std::forward(out)); } export auto wtabulate(std::wstring_view table_spec, std::ranges::range auto &&range, std::output_iterator auto &&out) { return basic_tabulate(table_spec, std::forward(range), std::forward(out)); } } // namespace nihil