Skip to content

Instantly share code, notes, and snippets.

@lioncash
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lioncash/946ea114fdfde182749c to your computer and use it in GitHub Desktop.
Save lioncash/946ea114fdfde182749c to your computer and use it in GitHub Desktop.
Formatting
#pragma once
#include <cctype>
#include <iomanip>
#include <sstream>
#include <string>
#include <type_traits>
// A type-safe formatter that performs formatting using variadics and does
// not use any form of the printf family to do formatting.
//
// Since this formatter is type-safe, there is no need for type indicators such
// as "%d". Instead a number is provided to indicate which argument to the formatter
// is to be placed where. These numeric indexes are positional, meaning they can
// be used to place the same string multiple times.
//
// For example:
// - Format("{0} {0} {1}", "test", 2)
//
// Will produce the string "test test 2".
//
// Formatting for the formatting arguments can also be done:
// - {n:X} - Uppercase hex-representation of the n-th argument.
// - {n:x} - Lowercase hex-representation of the n-th argument.
// - {n:pX} - Uppercase hex representation of the n-th argument padded by p zeroes
// - {n:px} - Lowercase hex representation of the n-th argument padded by p zeroes
//
// Since '{' and '}' are the characters which start and end a formatter sequence
// these need to be escaped if they are wanted to be used within a formatting sequence.
// They can be escaped by duplicating the respective character:
// - "{{" will print "{"
// - "}}" will print "}"
//
// So an example would be Format("{{0}}", "test"), which will print "{test}"
//
//
// If an unterminated formatting specifier is discovered (e.g. Format("Test {0", var)),
// the formatter will tell you this within the resulting string. It will put
// "{index: 0 | Unterminated formatter present}" at the location of the formatting specifier
// which ensures all formatting specifiers are properly closed.
//
class Formatter final
{
public:
// Base case
std::string Format(std::string format)
{
// Reset the iteration index.
m_current_iteration = 0;
return format;
}
template<typename T, typename... Args>
std::string Format(std::string format, const T& arg, const Args... args)
{
size_t formatter_position = 0;
// '{' is the start of a formatting specifier.
while ((formatter_position = format.find('{', formatter_position)) != std::string::npos)
{
// Check for the end of the string after '{'
if (formatter_position + 1 >= format.length())
{
break;
}
// If we found a '{' but no number follows it, just skip it.
else if (!::isdigit(format[formatter_position + 1]))
{
++formatter_position;
}
else
{
// Where the number portion of the formatter begins.
// e.g. In "{3}" this would be the offset to '3'.
const size_t format_start_pos = formatter_position + 1;
size_t format_end_pos = formatter_position + 1;
// Reserve for the common case. Most string formatting doesn't exceed 10 placements.
std::string formatting_index;
formatting_index.reserve(2);
// Retrieve the formatting index number
while (format_end_pos < format.length() && ::isdigit(format[format_end_pos]))
{
formatting_index += format[format_end_pos];
++format_end_pos;
}
// It's a valid index in relation to the current argument in the parameter pack. Replace it.
if (StringToSizeT(formatting_index) == m_current_iteration)
{
std::string formatted_arg = HandleFormatting(format, arg, format_end_pos);
format.replace(formatter_position, (format_end_pos - formatter_position), formatted_arg);
// Don't bother checking the already-formatted string argument.
formatter_position += formatted_arg.length();
}
else // Continue along the string.
{
++formatter_position;
}
}
}
++m_current_iteration;
return Format(format, args...);
}
private:
// Determines the number from the string "{x}" where x is any number that can fit in size_t.
// formatting_postfix is all numeric characters after the '{' format indicator.
static size_t StringToSizeT(const std::string& formatting_postfix)
{
size_t result = 0;
// NOTE: This can be made to be std::stoull whenever Android implements C99 correctly.
std::istringstream iss(formatting_postfix);
iss >> result;
return result;
}
template<typename T>
std::string HandleFormatting(const std::string& format, const T& arg, size_t& formatter_pos)
{
std::string padding_cache;
std::stringstream formatter;
bool specifier_terminated = false;
while (formatter_pos < format.length() && !specifier_terminated)
{
if (::isdigit(format[formatter_pos]))
{
padding_cache += format[formatter_pos];
}
// Hitting a character signifies the end of padding possibilities.
else if (::isalpha(format[formatter_pos]))
{
size_t padding = StringToSizeT(padding_cache);
if (format[formatter_pos] == 'x')
{
formatter << std::hex;
}
else if (format[formatter_pos] == 'X')
{
formatter << std::uppercase << std::hex;
}
if (padding > 0)
{
formatter << std::setw(padding) << std::setfill('0');
}
}
else if (format[formatter_pos] == '}')
{
specifier_terminated = true;
}
++formatter_pos;
}
if (specifier_terminated)
{
formatter << arg;
}
else // Incorrect formatting
{
formatter << "{index: " << m_current_iteration << " | unterminated formatter present}";
}
return formatter.str();
}
// We start at formatter index "{0}"
size_t m_current_iteration = 0;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment