StringFormatter


#1

Well, it seems the String::formatted is still broken by last change to git, so I propose this class, providing the main advantages I see to printf like functions:
[list]
[]String format not linked to the data to format[/]
[]Stateless format specifications for argument[/]
[]Translatable format with no source code modification[/][/list]

Here it is, that’s advanced pseudo code, since I’m not allowed to provide my code for this:

// Could be a inner class of String, so it could be used as String::Format() in the code
class Format
{
public:
    enum Justification
    {
        Left,
        Centered,
        Right,
    }; 

    enum Type
    {
        Integer,
        VoidPointer,
        String,
        FloatingPoint,
        FloatingPointScientific,
    };   


private:
    struct FormatList
    {
        const Type            type;
        const int               position;
        const int               lengthSpecifier;
        const int               precision;
        const Justification justification;
        const int               base;
        
 
        FormatList(const Type type, const int position, const int lengthSpecifier = -1, const int precision = -1, const Justification justification = Center, const int base = 10) :
             type(type), position(position), lengthSpecifier(lengthSpecifier), precision(precision), justification(justification), base(base) {}
    };
  
    // The main formatting logic is here
    struct Formatter
    {
        /** The format to follow */
        const FormatList & format;

         /** Should be overridden for each type, of course.
              If the compiler stops here, you probably need to cast the object you're passing to the 
              String::Format to a basic type, or provide your own template specialization for your object */
         virtual String format() const = 0;

         Formatter(const FormatList & format) : format(format) {}
         virtual ~Formatter() {}
    };

    template<class Param>
    struct FormatListItem;
 
    OwnedArray<Formatter> arguments;
    Array<FormatList>          expectedArguments;
    StringArray                    pieces;     
    String                            formattedString;
   

    void prepareExpectedList()
    {
         // Printf like parser here, I can send you the code for this part by email, but you'll need adaptation to the current string class
         // Basically, at the end of this function, "expectedArguments" array is filled, and so is "pieces" containing all the pieces between each parameter 
    };

    /** This is used to figure out of position format for the i-th argument */
    const FormatList & getExpectedArgumentForPosition(const int position)
    {   // Could be improved if the array was sorted by position first 
        for (int i = 0; i < expectedArguments.size(); i++) 
            if (expectedArguments.getUnchecked(i).position == position) return expectedArguments.getUnchecked(i);

        return expectedArguments[expectedArguments.size()];
    }

    /** Compute the final formatted string */
    void computeString()
    {
        if (formattedString) return;
        
        // If you use this method, you must provide the number of arguments you've said you will in your format chain.
        jassert(pieces.size() == expectedArguments.size() + 1);
        for (int i = 0; i < expectedArguments.size(); i++)
             formattedString = pieces.getUnchecked(i) + expectedArguments.getUnchecked(i)->format();

        formattedString += pieces.getUnchecked(expectedArguments.size());
    }
 
public:
    Format(const String & formatString) 
    {
        prepareExpectedList(formatString);
    }
    ~Format() { computeString(); }

    template <typename Param>
    Format & operator % (Param param) 
    {
         arguments.add(new FormatListItem<Param>(param, getExpectedArgumentsForPosition(arguments.size()));
         return *this;
    }

    operator String() { computeString(); return formattedString; } 
};

Then you can use it like this:

// Example sprintf code
sprintf(buffer, "There is %d egg in the basket", eggsCount);
// Equivalent Juce code, with support for internationalization
String ret = String::Format(TRANS("There is %d egg in the basket")) % eggsCount;

// Another harder example, self explicit
String ret = String::Format(TRANS("Pointer %p contains %4d objects : %08X") % objPtr % objPtr->size() % (*objPtr)[0];

// This one show very convinient method for argument position
String blue = TRANS("blue"); 
String car = TRANS("car");
String ret = String::Format(TRANS("I love the %s %s")) % blue % car;  // Translation text could be "J'aime la %2$s %1$s" giving, at runtime the correct: "J'aime la voiture bleue"

This class could prove very useful, is typesafe (that is, you won’t crash if you pass a String instead of a const char*), doesn’t compile if you pass in object that doesn’t match the supported format types (but you can provide your own format if you want, provided you specialize a template)


#2

Sounds like you just wrote your own boost::format


#3

Not bad! The problem with these things is always which operator to use for the concatenation, and I’m not a fan of ‘%’. ‘<<’ might be a better bet. What operator do they use in boost::format?


#4

They use %. I’m not fond of “<<” since if you pass a string as parameter, the “<<” operator from String class will predate the Format’s version (and the same happen for the std::stream), so better using a less error prone operator. Did you know you could use the “,” operator too ?
It might be nice (but a bit disturbing for a novice) to have:
String ret = String::Format(“something %d %d %d”), 3, 4, 5;

@Vinn, in fact, I’ve reworked Alexandrescu’s SafePrintF a few years ago, my version used () operator, but by that time, I didn’t though about Macro, and the () interfer with macro which are evaluated at preprocessor time.


#5

boost::format uses ‘%’, and leaves ‘<<’ available so you can mix formatting with streams:

[code] Rational ratio(16,9);
cerr << format("%#x \n") % ratio; // -> “0x10/0x9 \n”

cerr << format("%-8d")  % ratio;  // -> "16/9    "      and not    "16      /9       "
cerr << format("%=8d")  % ratio;  // -> "  16/9  "      and not    "   16   /    9   "

cerr << format("%+08d \n")  % ratio;  // -> "+00016/9"
cerr << format("% 08d \n")  % ratio;  // -> "000 16/9"[/code]

#6

BTW, the above code is public domain, so you can hack it as much as you want, and relicense it if you want.


#7

Wouldn’t there be operator precedence issues with ‘%’? If you mix it with + or *, that’s bound to create some confusion.

Nice idea, anyway.


#8

Yes, you are right. So left, there is the “,” and “()” operator, or simply, don’t overload any operator, but instead call the method “with()” instead.
It’s hard to find an operator with no side effects (even << have side effect on integers by the way)