Last active
August 29, 2015 14:03
-
-
Save lioncash/946ea114fdfde182749c to your computer and use it in GitHub Desktop.
Formatting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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