MQL's OOP notes: Print data using overloads and templates

16 September 2016, 21:06
Stanislav Korotky
0
280
One of the most frequently used MQL functions is Print. It's simple and indispensable. Unfortunately it usually requires a lot of routine typing which makes code bulky. For example, if you need to print several values, you need to explicitly add some separators. To output datetime properly you should call TimeToString with specific formatting. And printing a number with floating point implies usage of DoubleToString where you specify number of digits after the point.

If you try to use a natural way of printing:

Print(M_PI, "test", boolean, day, dt, clr); 
you'll get something like this:

3.141592653589793testtrue12016.09.14 11:48:24clrRed

where M_PI constant is printed with default excessive precision, dt varibale, which is actualy a datetime, shows probably unnecessary seconds, and all values are stuck together. In order to get this right, one should add a lot of auxiliary stuff:

Print(DoubleToString(M_PI, 5), " test ", boolean, " ", day, " ", TimeToString(dt, TIME_DATE | TIME_MINUTES), " ", clr);

This line is more than 2 times longer than the previous one.  

Of course one can invent some shorthands, such as a popular DS(number, digits) macro for DoubleToString, but they do not actually solve the problem, but make it less obtrusive.  

Fortunately, OOP can simplify the matter. The idea of the solution comes from C++ output stream and its overloaded operator <<. Let us define a stream-like class which will hold all settings, such as formatting, separators, etc and then apply them automatically to all data passed into the output stream. The complete code is attached, here we'll consider most important parts.

First start the class and its private member variables.

class OutputStream
{
  private:
    int digits;      // default number of digits for floating point numbers
    char delimiter;  // default separator between values
    int timemode;    // default format for datetime
    string format;   // format string for numbers with floating point
    string line;     // internal buffer, where all printed data is accumulated
The string format should be initialized according to the given number of digits.

    void createFormat()
    {
      format = "%." + (string)digits + "f";
    }
We'll call this method from constructors shown below.

  public:
    OutputStream(): digits(_Digits), timemode(TIME_DATE|TIME_MINUTES)
    {
      createFormat();
    }
    OutputStream(int p): digits(p), timemode(TIME_DATE|TIME_MINUTES)
    {
      createFormat();
    }
    OutputStream(int p, char d): digits(p), delimiter(d), timemode(TIME_DATE|TIME_MINUTES)
    {
      createFormat();
    }
    OutputStream(int p, char d, int t): digits(p), delimiter(d), timemode(t)
    {
      createFormat();
    }

Default value for digits (if it's not specified) is the _Digits of current _Symbol. Default datetime format is without seconds. Default separator is empty.

Now most important part - the overloaded insertion operator <<.

    template<typename T>
    OutputStream *operator<<(const T v)
    {
      if(typename(v) == "int" || typename(v) == "uint"
      || typename(v) == "long" || typename(v) == "ulong"
      || typename(v) == "short" || typename(v) == "ushort")
      {
        if(typename(v) == "ushort" && v == '\n')
        {
          Print(trimmedLine());
          line = NULL;
          return GetPointer(this);
        }
        else
        {
          line += IntegerToString((int)v);
        }
      }
      else
      if(typename(v) == "double" || typename(v) == "float")
      {
        if(v == EMPTY_VALUE) line += "<EMPTY_VALUE>";
        else line += StringFormat(format, v);
      }
      else
      if(typename(v) == "datetime")
      {
        line += TimeToString((datetime)v, timemode);
      }
      else
      if(typename(v) == "color")
      {
        line += (string)v;
      }
      else
      if(typename(v) == "char" || typename(v) == "uchar")
      {
        if(v == '\n')
        {
          Print(trimmedLine());
          line = NULL;
          return GetPointer(this);
        }
        else
        {
          line += CharToString((char)v);
        }
      }
      else
      {
        line += (string)v;
      }
      if(delimiter != 0)
      {
        line += CharToString(delimiter);
      }
      return GetPointer(this);
    }

In this case it's implemented as a single templatized method which can accept value of any built-in type. Instead of this you could define multiple methods with different parameter types - one per every built-in type, but I decided the single universal one. In this method we process passed variable according to its type determined by typename function. For every type, specific API function - such as DoubleToString, TimeToString, CharToString - is used to get its string representation, which is added to the line member variable. The process uses predefined settings specified only once in constructor. When the input contains single newline character '\n', all the contents of the line varibale is flushed by Print call and the line is emptied. Printing uses a private method trimmedLine, which cuts latest separator. Separators are added automatically after every streamed value, because we assume a next value to come, but the latest separator makes no sense. Thus this method is required. Alternatively one could insert separator before every new value except very first one, but then you should check if the line is empty before every concatenation.

The method returns a pointer to the object itself. This allows for chaining its calls in the same line (as shown below).

Now we can create an object of this class.

OutputStream out(5, ',');

And then use it in the following way

out << M_PI << "test" << boolean << EN(day) << dt << clr << '\n';

which outputs

3.14159,test,true,two,2016.09.14 11:42,FF0000

The EN helper is a shorthand for enumeration printing.

template<typename T> string EN(T enum_value) { return(EnumToString(enum_value)); }

One important thing to note is that operator << defined above can process only built-in types. For objects we should add another templates.

    template<typename T>
    OutputStream *operator<<(const T &v)
    {
      line += typename(v) + StringFormat("%X", GetPointer(v));
      if(delimiter != 0)
      {
        line += CharToString(delimiter);
      }
      return GetPointer(this);
    }

    template<typename T>
    OutputStream *operator<<(const T *v)
    {
      line += typename(v) + StringFormat("%X", v);
      if(delimiter != 0)
      {
        line += CharToString(delimiter);
      }
      return GetPointer(this);
    }

The type T here is resolved to any object - they are passed by reference or as pointers. This implementation prints out not so much information about the objects. If you want objects to provide additional information, you can define a class Printable like that

class Printable
{
  public:
    virtual string toString() const
    {
      return "<?>";
    };
};
and then inherit all other classes from it, overriding the method with a code returning a meaningful result. Then the method can be used as appropriate inside the stream class, but details go outside the scope of this article for the sake of simplicity.

Please, use recent versions of MQL compiler to eliminate possible errors which may arise on older versions. MQL is constantly extending its support of more and more OOP features. 




Files:
fmtprntl.mqh  4 kb
Share it with friends: