543 lines
12 KiB
C++
543 lines
12 KiB
C++
// Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
|
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
|
|
|
#include "StringF.h"
|
|
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#include <cassert>
|
|
#include <climits>
|
|
#include <cstring>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ## FormatSpec
|
|
|
|
FormatSpec::FormatSpec() :
|
|
format("")
|
|
{
|
|
for (int i = 0; i <= MAX_PARAMS; ++i)
|
|
params[i] = 0;
|
|
}
|
|
|
|
FormatSpec::FormatSpec(const char *format_) :
|
|
format(format_)
|
|
{
|
|
assert(format_);
|
|
parseFormat(strlen(format));
|
|
}
|
|
|
|
FormatSpec::FormatSpec(const char *format_, int formatlen_) :
|
|
format(format_)
|
|
{
|
|
assert(format_ && (formatlen_ >= 0));
|
|
parseFormat(formatlen_);
|
|
}
|
|
|
|
bool FormatSpec::empty() const
|
|
{
|
|
return (params[MAX_PARAMS] == 0);
|
|
}
|
|
|
|
bool FormatSpec::specifierIs(const char *specifier) const
|
|
{
|
|
int len = params[0];
|
|
if (len && (format[len - 1] == ':'))
|
|
--len;
|
|
return (strncmp(specifier, format, len) == 0);
|
|
}
|
|
|
|
int FormatSpec::paramCount() const
|
|
{
|
|
int i = 0;
|
|
while ((i < MAX_PARAMS) && (params[i] < params[i + 1]))
|
|
++i;
|
|
return i;
|
|
}
|
|
|
|
std::string FormatSpec::param(int idx) const
|
|
{
|
|
const char *beg, *end;
|
|
paramPtr(idx, beg, end);
|
|
|
|
std::string str;
|
|
str.reserve(end - beg);
|
|
|
|
const char *c = beg;
|
|
while (c != end) {
|
|
// scan to the next escape character or the end of the segment
|
|
while ((c != end) && (*c != '\\'))
|
|
++c;
|
|
// append this run of unescaped characters
|
|
str.append(beg, (c - beg));
|
|
// if we're not at the end, we must have hit a backslash
|
|
if (c != end) {
|
|
++c; // skip the backslash
|
|
if (c != end) {
|
|
beg = c; // start the next run from the escaped character (whatever it is)
|
|
++c; // skip the escaped character (important so we don't interpret a second '\\' as another escape char)
|
|
} else {
|
|
// XXX warning here? or even assert that the format spec doesn't have backslash as the last character?
|
|
str += '\\';
|
|
}
|
|
}
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
void FormatSpec::paramPtr(int idx, const char *&begin, const char *&end) const
|
|
{
|
|
assert(idx >= 0 && idx < MAX_PARAMS);
|
|
begin = &format[params[idx]];
|
|
end = &format[params[idx + 1]];
|
|
// if it's not the last parameter, then we need to shift end back one,
|
|
// to account for the '|' character that separates parameters
|
|
// note: the last character of a parameter may legitimately be a '|', e.g.
|
|
// in %foo{bar:qux\|}
|
|
// param 0 (before escape processing) is exactly ``qux\|''
|
|
if ((idx + 1 < MAX_PARAMS) && (params[idx + 2] > params[idx + 1]))
|
|
--end;
|
|
}
|
|
|
|
void FormatSpec::parseFormat(int length)
|
|
{
|
|
// length limit due to the type used for the params[] array
|
|
assert((length >= 0) && (length <= USHRT_MAX));
|
|
assert(format); // format should already have been set
|
|
|
|
const char *c = format;
|
|
const char *end = &format[length];
|
|
|
|
// scan the specifier
|
|
while ((c != end) && isalpha(*c))
|
|
++c;
|
|
|
|
// skip the optional ':' separating the specifier and the first parameter
|
|
if (*c == ':') ++c;
|
|
|
|
for (int i = 0; i < MAX_PARAMS; ++i) {
|
|
params[i] = (c - format);
|
|
// scan to the beginning of the next parameter
|
|
while (c != end) {
|
|
if (*c == '\\') {
|
|
++c;
|
|
if (c != end) ++c;
|
|
} else if (*c == '|') {
|
|
++c;
|
|
break;
|
|
} else
|
|
++c;
|
|
}
|
|
}
|
|
params[MAX_PARAMS] = (c - format);
|
|
|
|
assert((c == end) && "FormatSpec::MAX_PARAMS isn't big enough");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ## to_string() implementations and helpers
|
|
|
|
struct PrintfSpec {
|
|
enum Flags {
|
|
AlignLeft = 1 << 0, // corresponds with '-' flag
|
|
ForceSign = 1 << 1, // corresponds with '+' flag
|
|
PadSign = 1 << 2, // corresponds with ' ' flag
|
|
PadZero = 1 << 3, // corresponds with '0' flag
|
|
ShowType = 1 << 4 // corresponds with '#' flag
|
|
};
|
|
|
|
int width;
|
|
int precision;
|
|
int flags;
|
|
|
|
PrintfSpec() :
|
|
width(-1),
|
|
precision(-1),
|
|
flags(0) {}
|
|
};
|
|
|
|
static const char *parse_printfspec(PrintfSpec &spec, const char *fmt, const char *end = 0)
|
|
{
|
|
const char *c = fmt;
|
|
//at_flags:
|
|
while ((c != end) && *c) {
|
|
// skip backslash
|
|
if (*c == '\\') ++c;
|
|
if (c == end) return c;
|
|
int addflag = 0;
|
|
switch (*c) {
|
|
// printf flags
|
|
case '-': addflag = PrintfSpec::AlignLeft; break;
|
|
case '+': addflag = PrintfSpec::ForceSign; break;
|
|
case ' ': addflag = PrintfSpec::PadSign; break;
|
|
case '0': addflag = PrintfSpec::PadZero; break;
|
|
case '#': addflag = PrintfSpec::ShowType; break;
|
|
|
|
// valid characters for the width specifier
|
|
case '*':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
goto at_width;
|
|
|
|
// valid characters for the precision specifier
|
|
case '.':
|
|
goto at_precision;
|
|
|
|
// unknown char
|
|
default:
|
|
return c;
|
|
}
|
|
if (addflag && !(spec.flags & addflag)) {
|
|
spec.flags |= addflag;
|
|
} else {
|
|
// duplicate flag
|
|
return c;
|
|
}
|
|
++c;
|
|
}
|
|
|
|
at_width:
|
|
// variable-width specifier not supported
|
|
if ((c == end) || (*c == '*')) return c;
|
|
if (isdigit(*c)) {
|
|
int width = 0;
|
|
while ((c != end) && isdigit(*c)) {
|
|
width *= 10;
|
|
width += (*c - '0');
|
|
++c;
|
|
}
|
|
spec.width = width;
|
|
}
|
|
|
|
at_precision:
|
|
if ((c != end) && (*c == '.')) {
|
|
++c;
|
|
// variable-precision specifier not supported
|
|
if ((c == end) || (*c == '*')) return c;
|
|
int prec = 0;
|
|
while ((c != end) && isdigit(*c)) {
|
|
prec *= 10;
|
|
prec += (*c - '0');
|
|
++c;
|
|
}
|
|
spec.precision = prec;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
void init_iosflags(std::ostream &ss, const PrintfSpec &spec)
|
|
{
|
|
if (spec.width != -1)
|
|
ss.width(spec.width);
|
|
if (spec.precision != -1)
|
|
ss.precision(spec.precision);
|
|
|
|
if (spec.flags & PrintfSpec::AlignLeft)
|
|
ss.setf(std::ios::left, std::ios::adjustfield);
|
|
else {
|
|
if (spec.flags & PrintfSpec::PadZero)
|
|
ss.setf(std::ios::internal, std::ios::adjustfield);
|
|
else
|
|
ss.setf(std::ios::right, std::ios::adjustfield);
|
|
}
|
|
if (spec.flags & PrintfSpec::PadZero) ss.fill('0');
|
|
if (spec.flags & PrintfSpec::ForceSign) ss.setf(std::ios::showpos);
|
|
if (spec.flags & PrintfSpec::ShowType) {
|
|
ss.setf(std::ios::showpoint);
|
|
ss.setf(std::ios::showbase);
|
|
}
|
|
}
|
|
|
|
/*
|
|
if (spec.flags & PrintfSpec::AlignLeft) { ss << '-'; }
|
|
if (spec.flags & PrintfSpec::PadZero) { ss << '0'; }
|
|
if (spec.flags & PrintfSpec::ForceSign) { ss << '+'; }
|
|
if (spec.flags & PrintfSpec::PadSign) { ss << ' '; }
|
|
if (spec.flags & PrintfSpec::ShowType) { ss << '#'; }
|
|
if (spec.width != -1) { ss << spec.width; }
|
|
if (spec.precision != -1) { ss << '.' << spec.precision; }
|
|
*/
|
|
|
|
std::string to_string(int64_t value, const FormatSpec &fmt)
|
|
{
|
|
std::ostringstream ss;
|
|
PrintfSpec spec;
|
|
|
|
if (!fmt.empty()) {
|
|
if (fmt.specifierIs("d") || fmt.specifierIs("i")) {
|
|
ss.setf(std::ios::dec, std::ios::basefield);
|
|
} else
|
|
return std::string("%(err: bad format)");
|
|
|
|
const char *fmtbegin, *fmtend;
|
|
fmt.paramPtr(0, fmtbegin, fmtend);
|
|
if (fmtend != fmtbegin)
|
|
parse_printfspec(spec, fmtbegin, fmtend);
|
|
}
|
|
|
|
// sign padding is not supported
|
|
if (spec.flags & PrintfSpec::PadSign)
|
|
return std::string("%(err: bad format)");
|
|
|
|
// precision is ignore for integer arguments
|
|
if (spec.precision != -1)
|
|
return std::string("%(err: bad format)");
|
|
|
|
init_iosflags(ss, spec);
|
|
|
|
ss << value;
|
|
return ss.str();
|
|
}
|
|
|
|
std::string to_string(uint64_t value, const FormatSpec &fmt)
|
|
{
|
|
std::ostringstream ss;
|
|
PrintfSpec spec;
|
|
|
|
if (!fmt.empty()) {
|
|
if (fmt.specifierIs("u")) {
|
|
ss.setf(std::ios::dec, std::ios::basefield);
|
|
} else if (fmt.specifierIs("x")) {
|
|
ss.setf(std::ios::hex, std::ios::basefield);
|
|
} else if (fmt.specifierIs("X")) {
|
|
ss.setf(std::ios::hex, std::ios::basefield);
|
|
ss.setf(std::ios::uppercase);
|
|
} else if (fmt.specifierIs("o")) {
|
|
ss.setf(std::ios::oct, std::ios::basefield);
|
|
} else
|
|
return std::string("%(err: bad format)");
|
|
|
|
const char *fmtbegin, *fmtend;
|
|
fmt.paramPtr(0, fmtbegin, fmtend);
|
|
if (fmtend != fmtbegin)
|
|
parse_printfspec(spec, fmtbegin, fmtend);
|
|
}
|
|
|
|
// sign padding is not supported
|
|
if (spec.flags & PrintfSpec::PadSign)
|
|
return std::string("%(err: bad format)");
|
|
|
|
// precision is ignore for integer arguments
|
|
if (spec.precision != -1)
|
|
return std::string("%(err: bad format)");
|
|
|
|
init_iosflags(ss, spec);
|
|
|
|
ss << value;
|
|
return ss.str();
|
|
}
|
|
|
|
std::string to_string(double value, const FormatSpec &fmt)
|
|
{
|
|
std::ostringstream ss;
|
|
PrintfSpec spec;
|
|
|
|
if (!fmt.empty()) {
|
|
if (fmt.specifierIs("f")) {
|
|
ss.setf(std::ios::fixed, std::ios::floatfield);
|
|
} else if (fmt.specifierIs("g")) {
|
|
} else if (fmt.specifierIs("G")) {
|
|
ss.setf(std::ios::uppercase);
|
|
} else if (fmt.specifierIs("e")) {
|
|
ss.setf(std::ios::scientific, std::ios::floatfield);
|
|
} else if (fmt.specifierIs("E")) {
|
|
ss.setf(std::ios::scientific, std::ios::floatfield);
|
|
ss.setf(std::ios::uppercase);
|
|
} else
|
|
return std::string("%(err: bad format)");
|
|
|
|
const char *fmtbegin, *fmtend;
|
|
fmt.paramPtr(0, fmtbegin, fmtend);
|
|
if (fmtend != fmtbegin)
|
|
parse_printfspec(spec, fmtbegin, fmtend);
|
|
}
|
|
|
|
// sign padding is not supported
|
|
if (spec.flags & PrintfSpec::PadSign)
|
|
return std::string("%(err: bad format)");
|
|
|
|
init_iosflags(ss, spec);
|
|
|
|
ss << value;
|
|
return ss.str();
|
|
}
|
|
|
|
std::string to_string(const char *value, const FormatSpec &fmt)
|
|
{
|
|
if (fmt.empty()) {
|
|
return std::string(value);
|
|
} else {
|
|
return std::string("%(err: bad format)");
|
|
}
|
|
}
|
|
|
|
std::string to_string(const std::string &value, const FormatSpec &fmt)
|
|
{
|
|
if (fmt.empty()) {
|
|
return value;
|
|
} else {
|
|
return std::string("%(err: bad format)");
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ## string_format() and helpers
|
|
|
|
// static inline const char* scan_unformatted(const char* fmt) {
|
|
// const char *c = fmt;
|
|
// return c;
|
|
// }
|
|
|
|
static inline const char *scan_int(const char *fmt, int &value)
|
|
{
|
|
value = 0;
|
|
const char *c = fmt;
|
|
while (*c >= 0 && isdigit(*c)) {
|
|
value *= 10;
|
|
value += (*c - '0');
|
|
++c;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
static inline const char *scan_ident(const char *fmt)
|
|
{
|
|
const char *c = fmt;
|
|
if (isalpha(*c) || (*c == '_')) {
|
|
++c;
|
|
while (isalnum(*c) || (*c == '_'))
|
|
++c;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
static inline const char *scan_bracetext(const char *fmt)
|
|
{
|
|
const char *c = fmt;
|
|
while (*c && (*c != '}')) {
|
|
if (*c == '\\') {
|
|
++c;
|
|
if (*c) ++c;
|
|
} else
|
|
++c;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
std::string string_format(const char *fmt, int numargs, FormatArg const *const args[])
|
|
{
|
|
std::string out;
|
|
const char *c = fmt;
|
|
while (*c) {
|
|
while (*c && *c != '%')
|
|
++c;
|
|
if (c != fmt) out.append(fmt, c - fmt);
|
|
fmt = c;
|
|
|
|
if (*c == '%') {
|
|
++c;
|
|
if (*c == '%') {
|
|
fmt = c;
|
|
++c;
|
|
} else {
|
|
int argid = -1;
|
|
const char *identBegin = c;
|
|
const char *identEnd = c;
|
|
|
|
// parse and match reference
|
|
if (isdigit(*c)) {
|
|
c = identEnd = scan_int(identBegin = c, argid);
|
|
} else {
|
|
if (*c == '{') {
|
|
identBegin = ++c;
|
|
while (*c && (*c != '}'))
|
|
++c;
|
|
identEnd = c;
|
|
if (*c == '}')
|
|
++c;
|
|
else {
|
|
out.append("%(err: unfinished reference)");
|
|
goto bad_reference;
|
|
}
|
|
} else if (isalpha(*c) || (*c == '_')) {
|
|
c = identEnd = scan_ident(identBegin = c);
|
|
} else {
|
|
out.append("%(err: unfinished reference)");
|
|
goto bad_reference;
|
|
}
|
|
|
|
if (identBegin == identEnd) {
|
|
out.append("%(err: blank reference)");
|
|
goto bad_reference;
|
|
}
|
|
|
|
for (int i = 0; i < numargs; ++i) {
|
|
size_t identLen = (identEnd - identBegin);
|
|
if (args[i]->name && (strncmp(args[i]->name, identBegin, identLen) == 0) && (args[i]->name[identLen] == '\0')) {
|
|
argid = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (argid >= 0 && argid < numargs) {
|
|
const FormatArg &arg = *args[argid];
|
|
const char *fmtBegin = 0;
|
|
const char *fmtEnd = 0;
|
|
// scan format specifier, if provided
|
|
if (*c == '{') {
|
|
++c;
|
|
if (!*c) {
|
|
out.append("%(err: unfinished format)");
|
|
goto bad_reference;
|
|
}
|
|
|
|
c = fmtEnd = scan_bracetext(fmtBegin = c);
|
|
|
|
if (fmtBegin == fmtEnd) {
|
|
fmtBegin = fmtEnd = 0;
|
|
}
|
|
|
|
if (!*c) {
|
|
out.append("%(err: unfinished format)");
|
|
goto bad_reference;
|
|
} else
|
|
++c;
|
|
}
|
|
|
|
if (fmtBegin) {
|
|
assert(fmtEnd > fmtBegin);
|
|
FormatSpec fspec(fmtBegin, fmtEnd - fmtBegin);
|
|
out.append(arg.format(fspec));
|
|
} else if (arg.defaultformat) {
|
|
FormatSpec fspec(arg.defaultformat);
|
|
out.append(arg.format(fspec));
|
|
} else
|
|
out.append(arg.format(FormatSpec()));
|
|
|
|
} else {
|
|
out.append("%(err: unknown arg '");
|
|
out.append(identBegin, identEnd - identBegin);
|
|
out.append("')");
|
|
goto bad_reference;
|
|
}
|
|
|
|
bad_reference:
|
|
fmt = c;
|
|
}
|
|
}
|
|
}
|
|
// append the final range
|
|
if (c != fmt)
|
|
out.append(fmt, c - fmt);
|
|
return out;
|
|
}
|