Skip to content

Commit 50da754

Browse files
committed
feat(core): introduce format API and pure C++ test suite
- add vix::format with {} placeholder support - support automatic and explicit indexing - handle escaped braces {{ and }} - introduce format_append and format_to helpers - integrate format module under include/vix/format - add root header include/vix/format.hpp - improve print behavior (Python-like string rendering) - remove quotes for string output in print - add pure C++ test suite for format (no external deps) - register format_test in CMake tests this establishes a minimal, fast, and dependency-free formatting layer for Vix
1 parent 0c44117 commit 50da754

5 files changed

Lines changed: 855 additions & 1 deletion

File tree

include/vix/format.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
*
3+
* @file format.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*
13+
*/
14+
15+
#ifndef VIX_FORMAT_HPP
16+
#define VIX_FORMAT_HPP
17+
18+
#include <vix/format/Format.hpp>
19+
20+
#endif // VIX_FORMAT_HPP

include/vix/format/Format.hpp

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
/**
2+
*
3+
* @file Format.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*
13+
* Lightweight formatting utilities for Vix.
14+
* This module provides a simple placeholder-based formatting API inspired by
15+
* Python and modern formatting libraries, while keeping a small surface area
16+
* and minimal complexity.
17+
*
18+
* Supported placeholders:
19+
* - {} : automatic argument indexing
20+
* - {0} : explicit positional indexing
21+
* - {{ : escaped opening brace
22+
* - }} : escaped closing brace
23+
*
24+
* Unsupported on purpose:
25+
* - format specifiers such as {:>10}, {:.2f}, etc.
26+
*
27+
* Example:
28+
*
29+
* std::string s1 = vix::format("Hello, {}", "world");
30+
* std::string s2 = vix::format("Value = {0}, name = {1}", 42, "Ada");
31+
* std::string s3 = vix::format("{{ config }} = {}", "ready");
32+
*
33+
*/
34+
35+
#ifndef VIX_FORMAT_FORMAT_HPP
36+
#define VIX_FORMAT_FORMAT_HPP
37+
38+
#include <array>
39+
#include <cctype>
40+
#include <cstddef>
41+
#include <stdexcept>
42+
#include <string>
43+
#include <string_view>
44+
#include <type_traits>
45+
#include <utility>
46+
#include <sstream>
47+
48+
#include <vix/print.hpp>
49+
50+
namespace vix
51+
{
52+
/**
53+
* @brief Exception type thrown on invalid format strings or invalid argument access.
54+
*/
55+
class format_error : public std::runtime_error
56+
{
57+
public:
58+
/**
59+
* @brief Construct a formatting error with a message.
60+
* @param message Human-readable error message.
61+
*/
62+
explicit format_error(const std::string &message)
63+
: std::runtime_error(message)
64+
{
65+
}
66+
67+
/**
68+
* @brief Construct a formatting error with a C-string message.
69+
* @param message Human-readable error message.
70+
*/
71+
explicit format_error(const char *message)
72+
: std::runtime_error(message)
73+
{
74+
}
75+
};
76+
77+
namespace detail
78+
{
79+
/**
80+
* @brief Convert a value to string using the Vix rendering pipeline.
81+
* @tparam T Value type.
82+
* @param value Value to convert.
83+
* @return String representation of the value.
84+
*/
85+
template <typename T>
86+
[[nodiscard]] inline std::string format_arg_to_string(const T &value)
87+
{
88+
std::ostringstream oss;
89+
90+
auto &cfg = vix::default_config();
91+
const bool old_raw_strings = cfg.raw_strings;
92+
cfg.raw_strings = false;
93+
94+
try
95+
{
96+
vix::detail::write(oss, value);
97+
}
98+
catch (...)
99+
{
100+
cfg.raw_strings = old_raw_strings;
101+
throw;
102+
}
103+
104+
cfg.raw_strings = old_raw_strings;
105+
return oss.str();
106+
}
107+
108+
/**
109+
* @brief Small non-owning view over a pre-rendered argument list.
110+
*/
111+
class rendered_arg_list
112+
{
113+
public:
114+
/**
115+
* @brief Construct from a fixed-size array of rendered arguments.
116+
* @tparam N Number of arguments.
117+
* @param values Array of argument strings.
118+
*/
119+
template <std::size_t N>
120+
explicit rendered_arg_list(const std::array<std::string, N> &values) noexcept
121+
: data_(values.data()), size_(N)
122+
{
123+
}
124+
125+
/**
126+
* @brief Return the number of stored arguments.
127+
* @return Argument count.
128+
*/
129+
[[nodiscard]] std::size_t size() const noexcept
130+
{
131+
return size_;
132+
}
133+
134+
/**
135+
* @brief Get a rendered argument by index.
136+
* @param index Zero-based argument index.
137+
* @return Reference to the stored string.
138+
* @throws vix::format_error If index is out of range.
139+
*/
140+
[[nodiscard]] const std::string &at(std::size_t index) const
141+
{
142+
if (index >= size_)
143+
{
144+
throw format_error("format argument index out of range");
145+
}
146+
return data_[index];
147+
}
148+
149+
private:
150+
const std::string *data_ = nullptr;
151+
std::size_t size_ = 0;
152+
};
153+
154+
/**
155+
* @brief Parse an unsigned decimal integer from a placeholder body.
156+
* @param text Placeholder content without braces.
157+
* @return Parsed index.
158+
* @throws vix::format_error If the text is empty or contains non-digits.
159+
*/
160+
[[nodiscard]] inline std::size_t parse_index(std::string_view text)
161+
{
162+
if (text.empty())
163+
{
164+
throw format_error("empty explicit format index");
165+
}
166+
167+
std::size_t value = 0;
168+
for (char ch : text)
169+
{
170+
if (!std::isdigit(static_cast<unsigned char>(ch)))
171+
{
172+
throw format_error("invalid explicit format index");
173+
}
174+
175+
value = (value * 10u) + static_cast<std::size_t>(ch - '0');
176+
}
177+
178+
return value;
179+
}
180+
181+
/**
182+
* @brief Render a format string into the destination string.
183+
*
184+
* Rules:
185+
* - {} inserts the next automatic argument.
186+
* - {N} inserts argument N.
187+
* - {{ inserts '{'
188+
* - }} inserts '}'
189+
*
190+
* Mixing automatic indexing ({}) with explicit indexing ({0}) is rejected.
191+
*
192+
* @param out Destination string.
193+
* @param fmt Format string.
194+
* @param args Pre-rendered argument list.
195+
* @throws vix::format_error On malformed input.
196+
*/
197+
inline void render_format_string(std::string &out,
198+
std::string_view fmt,
199+
const rendered_arg_list &args)
200+
{
201+
std::size_t i = 0;
202+
std::size_t next_auto_index = 0;
203+
bool used_auto_index = false;
204+
bool used_explicit_index = false;
205+
206+
while (i < fmt.size())
207+
{
208+
const char ch = fmt[i];
209+
210+
if (ch == '{')
211+
{
212+
if ((i + 1u) < fmt.size() && fmt[i + 1u] == '{')
213+
{
214+
out.push_back('{');
215+
i += 2u;
216+
continue;
217+
}
218+
219+
const std::size_t close = fmt.find('}', i + 1u);
220+
if (close == std::string_view::npos)
221+
{
222+
throw format_error("unmatched '{' in format string");
223+
}
224+
225+
const std::string_view token = fmt.substr(i + 1u, close - (i + 1u));
226+
227+
if (token.empty())
228+
{
229+
if (used_explicit_index)
230+
{
231+
throw format_error("cannot mix automatic and explicit argument indexing");
232+
}
233+
234+
used_auto_index = true;
235+
out += args.at(next_auto_index++);
236+
}
237+
else
238+
{
239+
if (used_auto_index)
240+
{
241+
throw format_error("cannot mix automatic and explicit argument indexing");
242+
}
243+
244+
used_explicit_index = true;
245+
246+
if (token.find(':') != std::string_view::npos)
247+
{
248+
throw format_error("format specifiers are not supported");
249+
}
250+
251+
const std::size_t index = parse_index(token);
252+
out += args.at(index);
253+
}
254+
255+
i = close + 1u;
256+
continue;
257+
}
258+
259+
if (ch == '}')
260+
{
261+
if ((i + 1u) < fmt.size() && fmt[i + 1u] == '}')
262+
{
263+
out.push_back('}');
264+
i += 2u;
265+
continue;
266+
}
267+
268+
throw format_error("single '}' encountered in format string");
269+
}
270+
271+
out.push_back(ch);
272+
++i;
273+
}
274+
}
275+
276+
/**
277+
* @brief Estimate a useful output capacity before rendering.
278+
* @param fmt Format string.
279+
* @param args Pre-rendered argument list.
280+
* @return Estimated output size.
281+
*/
282+
[[nodiscard]] inline std::size_t estimate_output_size(std::string_view fmt,
283+
const rendered_arg_list &args) noexcept
284+
{
285+
std::size_t total = fmt.size();
286+
for (std::size_t i = 0; i < args.size(); ++i)
287+
{
288+
total += args.at(i).size();
289+
}
290+
return total;
291+
}
292+
293+
} // namespace detail
294+
295+
/**
296+
* @brief Format values into a new string using Vix placeholder syntax.
297+
*
298+
* Supported placeholders:
299+
* - {} automatic indexing
300+
* - {0} explicit indexing
301+
* - {{ escaped '{'
302+
* - }} escaped '}'
303+
*
304+
* @tparam Args Argument types.
305+
* @param fmt Format string.
306+
* @param args Values to inject.
307+
* @return Newly formatted string.
308+
*
309+
* @throws vix::format_error If the format string is malformed or references
310+
* a missing argument.
311+
*
312+
* @example
313+
* auto s = vix::format("Hello, {}", "world");
314+
* auto t = vix::format("{0} + {0} = {1}", 2, 4);
315+
*/
316+
template <typename... Args>
317+
[[nodiscard]] std::string format(std::string_view fmt, const Args &...args)
318+
{
319+
std::array<std::string, sizeof...(Args)> rendered_args{
320+
detail::format_arg_to_string(args)...};
321+
322+
detail::rendered_arg_list rendered{rendered_args};
323+
324+
std::string out;
325+
out.reserve(detail::estimate_output_size(fmt, rendered));
326+
detail::render_format_string(out, fmt, rendered);
327+
return out;
328+
}
329+
330+
/**
331+
* @brief Append formatted output to an existing string.
332+
*
333+
* This avoids replacing the content of the destination and is useful for
334+
* incremental string building.
335+
*
336+
* @tparam Args Argument types.
337+
* @param out Destination string.
338+
* @param fmt Format string.
339+
* @param args Values to inject.
340+
*
341+
* @throws vix::format_error If the format string is malformed or references
342+
* a missing argument.
343+
*/
344+
template <typename... Args>
345+
void format_append(std::string &out, std::string_view fmt, const Args &...args)
346+
{
347+
std::array<std::string, sizeof...(Args)> rendered_args{
348+
detail::format_arg_to_string(args)...};
349+
350+
detail::rendered_arg_list rendered{rendered_args};
351+
out.reserve(out.size() + detail::estimate_output_size(fmt, rendered));
352+
detail::render_format_string(out, fmt, rendered);
353+
}
354+
355+
/**
356+
* @brief Replace the destination string with formatted output.
357+
*
358+
* @tparam Args Argument types.
359+
* @param out Destination string.
360+
* @param fmt Format string.
361+
* @param args Values to inject.
362+
*
363+
* @throws vix::format_error If the format string is malformed or references
364+
* a missing argument.
365+
*/
366+
template <typename... Args>
367+
void format_to(std::string &out, std::string_view fmt, const Args &...args)
368+
{
369+
out.clear();
370+
format_append(out, fmt, args...);
371+
}
372+
373+
} // namespace vix
374+
375+
#endif // VIX_FORMAT_FORMAT_HPP

0 commit comments

Comments
 (0)