Lector v1.0.0
C++ library for parsing command line arguments.
text.hpp
1// Copyright © 2026, Alexandre Coderre-Chabot.
2
3// This file is part of Lector (https://github.com/acodcha/lector), a C++ library for parsing
4// command line arguments. Lector is licensed under the MIT License (https://mit-license.org).
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7// associated documentation files (the "Software"), to deal in the Software without restriction,
8// including without limitation the rights to use, copy, modify, merge, publish, distribute,
9// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11// - The above copyright notice and this permission notice shall be included in all copies or
12// substantial portions of the Software.
13// - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
14// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM
17// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18
19#ifndef LECTOR_TEXT_HPP
20#define LECTOR_TEXT_HPP
21
22#include <algorithm>
23#include <cctype>
24#include <cstddef>
25#include <string>
26#include <string_view>
27#include <vector>
28
29/// @brief The Lector library's namespace.
30namespace lector {
31
32/// @brief Counts and returns the number of UTF-8 code points in a string of text. The number of
33/// UTF-8 code points is a useful approximation of the number of graphemes in the string.
34/// @param[in] text The string of text whose UTF-8 code points are to be counted.
35/// @return The number of UTF-8 code points in the string of text.
36[[nodiscard]] inline ::std::size_t code_points(const ::std::string_view text) {
37 // This function uses an optimized trick for counting UTF-8 code points. It works because of how
38 // UTF-8 is designed at the binary level:
39 // - The ASCII characters all use 1 byte and all start with 0. They are of the form 0xxxxxxx.
40 // - The first byte of a UTF-8 multi-byte character is the lead byte. Lead bytes all start with
41 // the binary patterns 110xxxxx, 1110xxxx, or 11110xxx; these binary patterns indicate that the
42 // UTF-8 character is two, three, or four bytes long, respectively.
43 // - The second, third, and fourth bytes of a UTF-8 multi-byte character are continuation bytes.
44 // These bytes always start with the binary pattern 10xxxxxx, which ranges from 0x80 to 0xBF in
45 // hexadecimal notation.
46 // Therefore, instead of trying to parse the lead byte and then predicting the number of bytes in
47 // the character, this function simply iterates through the string and ignores any byte that
48 // starts with the binary pattern 10xxxxxx, which indicates a continuation byte, leaving only the
49 // lead bytesto be counted. By only counting the lead bytes, this function perfectly counts the
50 // number of valid code points, treating ASCII characters and UTF-8 multi-byte characters as
51 // exactly one unit of length, which is a good approximation of the number of graphemes in the
52 // string.
53 ::std::size_t count{0UL};
54 for (const char character : text) {
55 // Cast to an unsigned character to avoid undefined behavior with bitwise operations on signed
56 // characters. 0xC0 is 11000000 in binary. 0x80 is 10000000 in binary.
57 if ((static_cast<unsigned char>(character) & 0xC0) != 0x80) {
58 ++count;
59 }
60 }
61 return count;
62}
63
64/// @brief Joins a vector of strings where each string corresponds to a line of text into a single
65/// string of text, with newline characters inserted between the lines, and the lines left-aligned.
66/// @param[in] lines Vector of strings to be joined and left-aligned.
67/// @return The joined and left-aligned string of text.
68[[nodiscard]] inline ::std::string join_and_left_align(const ::std::vector<::std::string>& lines) {
69 // Handle the empty case immediately to prevent underflow later.
70 if (lines.empty()) {
71 return ::std::string{};
72 }
73 // Calculate the exact required capacity.
74 ::std::size_t total_length{0UL};
75 for (const ::std::string& line : lines) {
76 total_length += line.length();
77 }
78 // Add space for the newline separators (one less than the total number of lines).
79 total_length += lines.size() - static_cast<::std::size_t>(1UL);
80 // Create and allocate the result.
81 ::std::string result;
82 result.reserve(total_length);
83 // Append the first line.
84 result.append(lines.front());
85 // Append subsequent lines prefixed by a newline.
86 for (::std::size_t line_index{1UL}; line_index < lines.size(); ++line_index) {
87 result.push_back('\n');
88 result.append(lines.at(line_index));
89 }
90 return result;
91}
92
93/// @brief Computes and returns the length of the longest word in a string of text. The length of a
94/// word is measured by its number of UTF-8 code points.
95/// @param[in] text The string of text whose longest word length is to be computed.
96/// @return The length of the longest word in the string of text.
97[[nodiscard]] inline ::std::size_t longest_word_length(const ::std::string_view text) {
98 ::std::size_t longest_word_length{0UL};
99 ::std::size_t index{0UL};
100 while (index < text.length()) {
101 // Skip over any whitespaces.
102 while (index < text.length() && ::std::isspace(static_cast<unsigned char>(text[index])) != 0) {
103 ++index;
104 }
105 // Return if the end of the string has been reached after skipping whitespaces.
106 if (index >= text.length()) {
107 break;
108 }
109 // The index now points to the start of the current word.
110 const ::std::size_t current_word_start{index};
111 // Find the end of the current word.
112 while (index < text.length() && ::std::isspace(static_cast<unsigned char>(text[index])) == 0) {
113 ++index;
114 }
115 // Obtain the current word.
116 const ::std::string_view current_word{
117 text.substr(current_word_start, index - current_word_start)};
118 // Compute the length of the current word.
119 const ::std::size_t current_word_length{::lector::code_points(current_word)};
120 // Update the longest word length.
121 longest_word_length = ::std::max(longest_word_length, current_word_length);
122 }
123 return longest_word_length;
124}
125
126/// @brief Wraps a string of text to a line length and returns the result as a vector of strings
127/// that contains one string per line.
128/// @param[in] text The string of text to wrap.
129/// @param[in] line_length The desired line length to use when wrapping. The actual line length may
130/// be longer if the string of text contains very long words whose lengths exceed the desired line
131/// length.
132/// @return The resulting vector of strings that contains one string per line.
133[[nodiscard]] inline ::std::vector<::std::string> wrap(
134 const ::std::string_view text, const ::std::size_t line_length) {
135 // Adjust the specified maximum line length if needed, increasing it to the longest word length if
136 // it is greater than the specified maximum line length. A specified maximum line length of 0
137 // indicates that the line length is unlimited.
138 const ::std::size_t maximum_line_length{
139 line_length == static_cast<::std::size_t>(0UL) ?
140 ::std::string::npos :
141 ::std::max(line_length, ::lector::longest_word_length(text))};
142 // Pre-allocate memory to avoid multiple reallocations. Assume an average word and space
143 // distribution.
144 ::std::vector<::std::string> result;
145 result.reserve((text.length() / maximum_line_length) + static_cast<::std::size_t>(1UL));
146 // Iterate through the string.
147 ::std::size_t current_line_length{0UL};
148 ::std::size_t index{0UL};
149 bool is_first_word{true};
150 while (index < text.length()) {
151 // Skip over any whitespaces. This effectively treats consecutive spaces, tabs, and newlines as
152 // one delimiter. Cast to an unsigned character to avoid undefined behavior with negative
153 // character values in std::isspace.
154 while (index < text.length() && ::std::isspace(static_cast<unsigned char>(text[index])) != 0) {
155 ++index;
156 }
157 // Return if the end of the string has been reached after skipping whitespaces.
158 if (index >= text.length()) {
159 break;
160 }
161 // The index now points to the start of the current word.
162 const ::std::size_t word_start_index{index};
163 // Find the end of the current word.
164 while (index < text.length() && ::std::isspace(static_cast<unsigned char>(text[index])) == 0) {
165 ++index;
166 }
167 // Obtain the current word.
168 const ::std::string_view current_word{text.substr(word_start_index, index - word_start_index)};
169 const ::std::size_t current_word_length{::lector::code_points(current_word)};
170 // Place the word in the result vector.
171 if (is_first_word) {
172 // In this case, this is the first word in the result string, so it can be safely added
173 // without a leading space.
174 result.emplace_back(current_word);
175 current_line_length = current_word_length;
176 is_first_word = false;
177 } else {
178 // In this case, this is not the first word in the result string, so a leading space must be
179 // added. Check if adding a space plus the word would exceed the line limit.
180 if (current_line_length + 1 + current_word_length <= maximum_line_length) {
181 // In this case, the word can be safely added to the current line. Access the current line
182 // via result.back() and append to it.
183 result.back().push_back(' ');
184 result.back().append(current_word);
185 current_line_length += static_cast<::std::size_t>(1UL) + current_word_length;
186 } else {
187 // In this case, the word doesn't fit on this line, so push a new line into the vector.
188 result.emplace_back(current_word);
189 current_line_length = current_word_length;
190 }
191 }
192 }
193 return result;
194}
195
196/// @brief Left-aligns and wraps a string of text to a line length.
197/// @param[in] text The string of text to left-align and wrap.
198/// @param[in] line_length The desired line length to use when wrapping. The actual line length may
199/// be longer if the string of text contains very long words whose lengths exceed the desired line
200/// length.
201/// @return The resulting left-aligned and wrapped text.
202[[nodiscard]] inline ::std::string wrap_and_left_align(
203 const ::std::string_view text, const ::std::size_t line_length) {
204 // Adjust the specified maximum line length if needed, increasing it to the longest word length if
205 // it is greater than the specified maximum line length. A specified maximum line length of 0
206 // indicates that the line length is unlimited.
207 const ::std::size_t maximum_line_length{
208 line_length == static_cast<::std::size_t>(0UL) ?
209 ::std::string::npos :
210 ::std::max(line_length, ::lector::longest_word_length(text))};
211 // Pre-allocate memory to avoid multiple reallocations. Assume an average word and space
212 // distribution.
213 ::std::string result;
214 result.reserve(
215 text.length() + (text.length() / maximum_line_length) + static_cast<::std::size_t>(1UL));
216 // Iterate through the string.
217 ::std::size_t current_line_length{0UL};
218 ::std::size_t index{0UL};
219 bool is_first_word{true};
220 while (index < text.length()) {
221 // Skip over any whitespaces. This effectively treats consecutive spaces, tabs, and newlines as
222 // one delimiter. Cast to an unsigned character to avoid undefined behavior with negative
223 // character values in std::isspace.
224 while (index < text.length() && ::std::isspace(static_cast<unsigned char>(text[index])) != 0) {
225 ++index;
226 }
227 // Return if the end of the string has been reached after skipping whitespaces.
228 if (index >= text.length()) {
229 break;
230 }
231 // The index now points to the start of the current word.
232 const ::std::size_t word_start_index{index};
233 // Find the end of the current word.
234 while (index < text.length() && ::std::isspace(static_cast<unsigned char>(text[index])) == 0) {
235 ++index;
236 }
237 // Obtain the current word.
238 const ::std::string_view current_word{text.substr(word_start_index, index - word_start_index)};
239 const ::std::size_t current_word_length{::lector::code_points(current_word)};
240 // Place the word in the result string.
241 if (is_first_word) {
242 // In this case, this is the first word in the result string, so it can be safely added
243 // without a leading space.
244 result.append(current_word);
245 current_line_length = current_word_length;
246 is_first_word = false;
247 } else {
248 // In this case, this is not the first word in the result string, so a leading space must be
249 // added. Check if adding a space plus the word would exceed the line limit.
250 if (current_line_length + 1 + current_word_length <= maximum_line_length) {
251 // In this case, the word can be safely added to the current line.
252 result.push_back(' ');
253 result.append(current_word);
254 current_line_length += static_cast<::std::size_t>(1UL) + current_word_length;
255 } else {
256 // In this case, the word doesn't fit on this line, so drop down to the next line.
257 result.push_back('\n');
258 result.append(current_word);
259 current_line_length = current_word_length;
260 }
261 }
262 }
263 return result;
264}
265
266/// @brief Combines two strings of text, each representing a column, into a single vector of strings
267/// that contains one line per string of text, with the lines formatted such that the two columns
268/// are left-aligned and spaced a short distance apart.
269/// @param[in] first_column_text The string of text for the first column.
270/// @param[in] first_column_width The desired width of the first column. The actual width may be
271/// larger if the string of text contains very long words whose lengths exceed the desired column
272/// width.
273/// @param[in] second_column_text The string of text for the second column.
274/// @param[in] second_column_width The desired width of the second column. The actual width may be
275/// larger if the string of text contains very long words whose lengths exceed the desired column
276/// width.
277/// @return The vector of strings that contains the combined text.
278[[nodiscard]] inline ::std::string combine_and_left_align(
279 const ::std::string_view first_column_text, const ::std::size_t first_column_width,
280 const ::std::string_view second_column_text, const ::std::size_t second_column_width) {
281 // Use a minimum gutter width of two spaces.
282 constexpr ::std::size_t gutter_width{2UL};
283 // Wrap and split both columns.
284 const ::std::vector<::std::string> first_column{
285 ::lector::wrap(first_column_text, first_column_width)};
286 const ::std::vector<::std::string> second_column{
287 ::lector::wrap(second_column_text, second_column_width)};
288 // Find the length of the longest line in the first column.
289 ::std::size_t first_column_longest_line_length{0UL};
290 for (const ::std::string& line : first_column) {
291 first_column_longest_line_length =
292 ::std::max(first_column_longest_line_length, ::lector::code_points(line));
293 }
294 // Determine the actual width of the first column. This automatically handles the case where the
295 // desired first column width is zero because in this case, the first column longest line length
296 // is necessarily greater than or equal to zero.
297 const ::std::size_t first_column_actual_width{
298 ::std::max(first_column_width, first_column_longest_line_length)};
299 // Determine the total number of rows required.
300 const ::std::size_t rows{::std::max(first_column.size(), second_column.size())};
301 // Pre-allocate memory for the result. A safe and highly efficient upper bound is the byte size of
302 // both original input strings, plus the maximum possible padding spaces and newlines per row.
303 ::std::string result;
304 result.reserve(
305 first_column_text.length() + second_column_text.length()
306 + (rows * (first_column_actual_width + gutter_width + static_cast<::std::size_t>(1UL))));
307 // Combine the rows line by line.
308 for (::std::size_t row_index{0UL}; row_index < rows; ++row_index) {
309 // Append a newline character for every row after the first to separate them without leaving a
310 // trailing newline at the very end of the string.
311 if (row_index > static_cast<::std::size_t>(0UL)) {
312 result.push_back('\n');
313 }
314 // Grab the string for column 1 if it exists on this row; otherwise, use an empty string.
315 const ::std::string_view first_cell{
316 row_index < first_column.size() ? ::std::string_view{first_column.at(row_index)} :
317 ::std::string_view{}};
318 result.append(first_cell);
319 // If column 2 has text on this row, pad column 1 and append column 2. Otherwise, if column 2 is
320 // exhausted, skip this padding to avoid unnecessary trailing whitespace.
321 if (row_index < second_column.size()) {
322 const ::std::size_t first_cell_length{::lector::code_points(first_cell)};
323 const ::std::size_t padding{first_column_actual_width - first_cell_length + gutter_width};
324 result.append(padding, ' ');
325 result.append(second_column.at(row_index));
326 }
327 }
328 return result;
329}
330
331} // namespace lector
332
333#endif // LECTOR_TEXT_HPP
The Lector library's namespace.
Definition arguments.hpp:45
::std::string wrap_and_left_align(const ::std::string_view text, const ::std::size_t line_length)
Left-aligns and wraps a string of text to a line length.
Definition text.hpp:202
::std::size_t code_points(const ::std::string_view text)
Counts and returns the number of UTF-8 code points in a string of text. The number of UTF-8 code poin...
Definition text.hpp:36
::std::string combine_and_left_align(const ::std::string_view first_column_text, const ::std::size_t first_column_width, const ::std::string_view second_column_text, const ::std::size_t second_column_width)
Combines two strings of text, each representing a column, into a single vector of strings that contai...
Definition text.hpp:278
::std::string join_and_left_align(const ::std::vector<::std::string > &lines)
Joins a vector of strings where each string corresponds to a line of text into a single string of tex...
Definition text.hpp:68
::std::size_t longest_word_length(const ::std::string_view text)
Computes and returns the length of the longest word in a string of text. The length of a word is meas...
Definition text.hpp:97
::std::vector<::std::string > wrap(const ::std::string_view text, const ::std::size_t line_length)
Wraps a string of text to a line length and returns the result as a vector of strings that contains o...
Definition text.hpp:133