This is following up on this question
In this part, I wanted to be able to choose arguments by index and have a placeholder for the format of the argument. As usually happens, my initial implementation was pretty much straightforward. Unfortunately, things get a bit ugly from here…
#include <charconv>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
namespace jdt
{
struct Segment
{
Segment(std::size_t offset, std::size_t count, std::size_t argument = std::numeric_limits<std::size_t>::max(), bool braceError = false)
: offset{ offset },
count{ count },
argument{ argument },
braceError{ braceError }
{
}
std::size_t offset;
std::size_t count;
std::size_t argument;
bool braceError;
};
struct Argument
{
Argument(const std::string& format)
: format{ format },
handled{ false }
{
}
std::string format;
std::string text;
bool handled;
};
template<typename T>
void format_helper(std::vector<Argument>& arguments, std::size_t& index, const T& value)
{
if (index < arguments.size())
{
Argument& arg = arguments[index];
std::ostringstream oss;
oss << value;
arg.text = oss.str();
arg.handled = true;
index++;
}
}
template<typename... Targs>
std::string format(std::string_view str, Targs...args)
{
// PHASE 1 - parse format string
std::vector<Segment> segments;
std::vector<Argument> arguments;
std::size_t offset = 0;
std::size_t index = str.find('{');
while (index != std::string::npos)
{
// check for literal open-braces character
if (str[index + 1] == '{')
{
segments.emplace_back(offset, index - offset + 1);
offset = index + 2;
}
else
{
// check if the next part is an argument index
std::size_t argindex = arguments.size();
std::size_t startOfArg = index;
auto ret = std::from_chars(str.data() + index + 1, str.data() + str.size(), argindex);
index = ret.ptr - str.data();
// find closing brace
bool braceError = false;
std::size_t closeBrace = str.find('}', index);
if (closeBrace == std::string::npos)
{
braceError = true;
closeBrace = str.size();
}
// check for format specifier
if (str[index] == ':')
index++;
// add to vectors
arguments.emplace_back(std::string(str.substr(index, closeBrace - index)));
segments.emplace_back(offset, startOfArg - offset, argindex, braceError);
offset = closeBrace + 1;
}
index = str.find('{', offset);
}
// PHASE 2 - Recursive variadic template
index = 0;
(format_helper(arguments, index, args), ...);
// PHASE 3 - Stich the string back together
std::ostringstream oss;
for (auto& segment : segments)
{
oss << str.substr(segment.offset, segment.count);
if (segment.braceError == true)
{
oss << "{ error: no matching '}' found";
}
else
{
if (segment.argument != std::numeric_limits<std::size_t>::max())
{
if (segment.argument < arguments.size() && arguments[segment.argument].handled)
oss << arguments[segment.argument].text;
else
oss << format("{{ error: invalid arg index: {} }", segment.argument);
}
}
}
if (offset < str.size())
oss << str.substr(offset);
return oss.str();
}
}
int main()
{
int a = 5;
double b = 3.14;
std::string c("hello");
// simple arguments
std::cout << jdt::format("a = {}, b = {}, c = {}\n", a, b, c);
// arguments with indexes
std::cout << jdt::format("a = {2}, b = {1}, c = {0}\n", a, b, c);
// literal open-braces character
std::cout << jdt::format("let object = {{\"a\": {0}, \"b\": {1}, \"c\": \"{2}\"};\n", a, b, c);
// too few arguments
std::cout << jdt::format("a = {0}, b = {1}, c = {2}\n", a, b);
// invalid argument indexes
std::cout << jdt::format("a = {0}, b = {10}, c = {2}\n", a, b, c);
// too many arguments
std::cout << jdt::format("a = {2}, b = {1}, c = {0}\n", a, b, c, 3, 4, 5);
// missing closing brace }
std::cout << jdt::format("a = {2}, b = {1}, c = {0\n", a, b, c);
}