aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.util/tabulate.ccm
blob: 5998b24e70e828526a80d8b85a1fc1c10e67ce35 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/*
 * This source code is released into the public domain.
 */

module;

#include <algorithm>
#include <cstdlib>
#include <format>
#include <ranges>
#include <iterator>
#include <vector>

export module nihil.util:tabulate;

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 {
	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