The low-level ASCII interface

At the very bottom of the ASCII IO interface are

  • parsers reading ASCII data from a stream

  • and formatters which can be used to write ASCII data to a stream

Parsers

The major job of this interface is to provided save number parsing. It supports all primitive data types provided by libpninexus along with instances of std::vector of such types.

At the heart of the parser API is the pni::parser class template. It takes one template parameter which is the primitive or container type to parse. To use the parser API just include pni/parsers.hpp in your source file.

Parsing primitive scalars

A very simple example would be something like this

#include <iostream>
#include <pni/types.hpp>
#include <pni/parsers.hpp>

using namespace pni;

using Float64Parser = pni::parser<float64>;

int main(int argc,char **argv)
{
    Float64Parser p;

    float64 data = p("1.234");
    std::cout<<data<<std::endl;

    return 0;
}

This example should be rather self explaining. When used with scalar values the parser template provides only a default constructor. No additional information is required to configure the parser code.

Besides primitive types the pni::parser template can also be used with the pni::value type erasure. In this case the resulting parser matches either a

  • pni::int64

  • or a pni::float64

  • or a pni::complex64 type.

Parsing a vector of primitives

Besides single scalars the pni::parser template can also be used with std::vector based containers where the element type should be one of the primitive types or a value. For this purpose a specialization of the pni::parser template of the form

template<typename ElementT> class parser<std::vector<ElementT>> {...};

is provided. A particularly interesting choice as an element is the pni::value type erasure as it allows to parse a series of inhomogeneous types. The following program

#include <iostream>
#include <vector>
#include <pni/types.hpp>
#include <pni/parsers.hpp>

using namespace pni;

using Record       = std::vector<value>;
using RecordParser = pni::parser<Record>;

int main(int argc,char **argv)
{
    RecordParser p;
    Record record = p("1.234  12 1+I3.4");
    for(auto v: data)
        std::cout<<v.type_id()<<std::endl;

    return 0;
}

would produce this output

FLOAT64
INT64
COMPLEX64

When using the default constructor of the pni::parser template with a container type the individual elements are considered to be separated by at least one blank. There are three more constructors allowing you to customize the behavior for the container parser.

The first allows to use a custom delimiter symbol

RecordParser parser(','); // set , as an element delimiter
Record data = parser("1.234,12 , 1+I3.4");

It is important to note that the delimiter symbol can be surrounded by an arbitrary number of blanks. The second constructor provides the constructor with additional start and stop symbols.

RecordParser parser('[',']');
Record data = parser("[1.234 12  1+I3.4]");

However, the elements in the string are now again separated only by blanks. Full customization of the parser is provided by the third constructor which allows the user to provide not only start and stop symbols but also a custom delimiter symbol

RecordParser parser('[',']',';');
Record data = parser("[1.234;12 ; 1+I3.4]");

Formatters

Formatters perform literally the inverse operation of parsers. They write data to a stream. Like for parsers the major concern here was to write numeric data without loss of precision to a stream.

Note

It is a common error when writing numbers in ASCII format to use the wrong precision. In the best case only 0 are written which is usually recognized rather early during software development. However, also truncations and thus loss of precision can occur which sometimes can lead to hard to recognize and thus difficult to debug bugs.

Thus, the formatter functions provided by libpninexus usually write numeric data with the maximum precision to avoid such issues.

Formatters are currently implemented as functions returning a string with the formatted output. You can use them after including pni/formatters.hpp in your source code.

For scalar data their usage is rather simple

uint8 number = ...;
std::cout<<pni::format(number)<<std::endl;

The format function takes care that the number if converted to a string without loss of precision.

As for parsers, there are also overloaded formatters for containers like std::vector. In this case the format() function takes an optional second argument which is a reference to pni::container_io_config. This class controls how such container data is written to disk. Taking the record example from the above parser section we could do something like this

//using a ; as a separator between record elements
pni::container_io_config config(';');

Record record = ...;
std::cout<<pni::format(record,config)<<std::endl;