QUBTree c++ tutorial

This document shows how to build the following tree, then how to save, read, and modify it.


Sample
{
	array of ints = 1 2 3 5
	MATRIX matrix of ints = (1 2 3
				1 2 3)
	some string = blah blah blah
	sample subtree
	{
		#\ a comment
		element
		{
			name = foo
		}
		#\ another comment
		element
		{
			name = bar
		}
	}
	a double = 0.3 #\ with a comment
}

------- Building the tree -------------------------   

#include "QUB_Tree.h"

...

// creating the root node:
QUB_Tree tree = QUB_Tree::Create("Sample");

// accessing a child node by name -- it will be created and appended if it doesn't exist
QUB_Tree child = tree["array of ints"];

// putting data in the child node            -- initializing with an array
int *initialData = [1, 2, 3, 5];
child.setNumData( QTR_TYPE_INT, 4, 1, initialData ); // (type, rows, cols, initializers or NULL)

// adding a node with matrix data (reusing the QUB_Tree variable)
child = tree["matrix of ints"];
child.setNumData( QTR_TYPE_INT, 2, 3, NULL );

// filling the data
for ( int i=0; i<matrixnode.dataRows(); ++i )
   for ( int j=0; j<matrixnode.dataCols(); ++j )
      child.dataAs( i, j, (int)0 ) = j;        1

// adding a node with string data
tree["some string"].setData( "blah blah blah" );

// adding a subtree                           2
QUB_Tree subtree = tree["sample subtree"];
child = subtree.appendChild( "element" );
child["name"].setData( "foo" );

// cloning an "element"
child = subtree.appendClone( child );
child["name"].setData( "bar" );

// inserting a comment line (which is represented as a node)
// as the first child
subtree.insertChild( QUB_Tree::CreateComment( " a comment" ) );
// and after the first "element"
subtree.insertChild( subtree["element"], QUB_Tree::CreateComment( " another comment" ) );

// adding a node with one double as data
child = tree["a double"];
child.setData( QTR_TYPE_DOUBLE, 0.3 );         3
child.setLineComment(" with a comment");

-------- Saving and reopening ---------------------------------

// saving in binary -- the file remains open until you close it
if ( ! tree.saveAs("sample.qtr") )
   printf("can't save\n");

// saving a copy as text -- the binary file remains open
if ( ! tree.saveTextCopy("sample.qtt") )
   printf("can't save text\n");

// do some modifications here ...
tree.insertChild("abc").setData( QTR_TYPE_INT, 123 );

// saving changes to disk
tree.save();

// closing the binary file
tree.close();                                  4

// reading the text file
tree = QUB_Tree::ReadText("sample.qtt");

// opening the binary file for reading and writing
tree = QUB_Tree::Open("sample.qtr");
if ( tree.isNull() )
   printf("couldn't open\n");


-------- Reading and modifying data ---------------------------------

// reading some data
string someString = tree["some string"].dataAsString();
double aDouble = tree["a double"].dataAsDouble();

// reading some data that totally doesn't exist (returns the given default of 1.0)
double bDouble = tree["b double"].dataAsDouble( 1.0 );

// raw array access
child = tree["array of ints"];
int *iarr = (int *) child.data();
for ( int i=0; i<child.dataCount(); ++i )
   printf("%d, ", iarr[i]);

// read/write a row of data from a buffer
child = tree["matrix of ints"];
vector<int> ivec( child.dataCols() );
child.readDataInto( &(ivec[0]), 0, 0 );
child.writeDataFrom( &(ivec[0]), 1, 1 );

// replacing data
child = tree["pqz"];
child.setData( "foo" );
child.setNumData( QTR_TYPE_DOUBLE, 3, 2 );

// resizing the number of rows (so it becomes 4x2)
child.resizeData( 4 );

// removing data
child.clearData();


-------- Working with large data --------------------------------------

In this section we create a QDF file, but never is the entire data in memory at once.

QUB_Tree qdf = QUB_Tree::Create("DataFile");
if ( qdf.saveAs("sample.qdf") ) {

   // note that saveAs is the first step;
   // this establishes storage so we can selectively load data

   // set up structure and metadata
   QUB_Tree segment = qdf["Segments"]["Segment"];
   segment["StartTime"].setData( QTR_TYPE_DOUBLE, 0.0 );
   QUB_Tree channels = segment["Channels"];

   qdf["Sampling"].setData( QTR_TYPE_DOUBLE, 5.0e-5 );
   qdf["Scaling"].setData( QTR_TYPE_DOUBLE, 320.0 );
   qdf["ADChannelCount"].setData( QTR_TYPE_INT, 2 );
   qdf["ADDataSize"].setData( QTR_TYPE_INT, 2 );
   qdf["ADDataType"].setData( QTR_TYPE_INT, QTR_TYPE_SHORT );

   // fill channels with 2 channels of 5 megapoints each;
   // each row is a sample; each column is a channel;
   // set PRELOAD to false first, so no RAM is allocated.
   channels.setPreload( false );
   channels.setNumData( QTR_TYPE_SHORT, 5000000, 2 );

   // in a fit of lameness, just set the data points to zero, 100000 samples at a time
   for ( int firstRow = 0; firstRow < 5000000; firstRow += 100000 ) {
      channels.loadData( firstRow, firstRow + 100000 - 1, false ); // false: don't bother reading the (uninitialized) contents
      short *mplexData = (short *) channels.data(); // alternating channel 0 with channel 1
      for ( int i=0; i<200000; ++i )
         mplexData[i] = 0;
   }
   channels.unloadData();

   qdf.save();
}
else {
   printf("can't save as sample.qdf");
}


-------- Using QUB_TreeIter to access children ------------------------

Stylistically a QUB_TreeIter resembles an STL iterator:
	* it points to a child node, or to "the end"
	* dereference it (*iter) to get a QUB_Tree
	* ++iter to point to the next child

QUB_TreeIter is bidirectional.  QUB_TreeMonoIter is forward-only
and can't insert or remove children, but is more efficient.

QUB_TreeIter chi = tree.children();                    5
if ( (*chi) != tree["array of ints"] )
   printf("the first child is not what I expected\n");

// printing the name of each child node
for ( chi = tree.children(); ! chi->isNull(); ++chi )
   printf("%s\n", chi->name());

// removing [the first child named] "some string"
chi = tree.find("some string");
chi.remove();                            // now chi points at "sample subtree"

// inserting two "x" nodes in its place
chi.insert( QUB_Tree::Create("x") );     // now chi points at "x"
++chi;                                   // now chi points at "sample subtree"
chi.insert( QUB_Tree::Create("x") );

// checking whether there is a child named "sue"
if ( tree.find("sue")->isNull() )
   printf("there is not\n");

// printing the "name" of each "element" in "sample subtree"
subtree = tree["sample subtree"];
for ( QUB_TreeMonoIter mchi = subtree.find("element"); ! mchi->isNull(); mchi.next("element") )
   printf( "%s\n", (*mchi)["name"].dataAsString() );


// appending child nodes more efficiently
chi = subtree.end();

chi.insert("element");
(*chi)["name"].setData("aa");
++chi;

chi.insert("element");
(*chi)["name"].setData("bb");
++chi;


---------------------------------------------------------------------------


// Making a copy of the whole tree
QUB_Tree treecopy = tree.clone(true);

// or just the node and its data
QUB_Tree treecopy = tree.clone(false);


----------- Working with QTR_Impl* ---------------------------------

class QUB_Tree acts as a wrapper around struct QTR_Impl *.
QUB_Tree is more convenient and less error prone, but sometimes
you need to pass or store qub trees as pointers.

// a function of a tree, returning a tree
QTR_Impl * func( QTR_Impl *treeptr )
{
   // wrap treeptr in a QUB_Tree for easy manipulation
   QUB_Tree tree( treeptr );
   QUB_Tree clone = tree.clone();

   // get a pointer for the clone, and incref it so it outlasts the QUB_Tree variable
   QTR_Impl *cloneptr = clone.getImpl();
   QTR_INCREF( cloneptr );

   return cloneptr;
}

// calling func
QUB_Tree tree = QUB_Tree::Create("tree");
QTR_Impl *resultPtr = func( tree.getImpl() );
QUB_Tree result( resultPtr );
QTR_DECREF( resultPtr );

In general, you should pass a "borrowed reference", and return a "new reference".
On receiving the new reference, wrap it in a QUB_Tree, then decref to release the new ref.

// storing a qub tree in a void*
static void *whatever;

whatever = tree.getImpl();
QTR_INCREF( (QTR_Impl*) whatever );

// using "whatever"
QUB_Tree xtree( whatever );
...

// releasing whatever
QTR_DECREF( (QTR_Impl*) whatever );
whatever = NULL;


---------------------------------------------------------------------

C++'s strong typing can make working with node data kind of a pain.
Here are some conveniences:

- The "data as" family, for reading the first data item.
  Conversions are automatic, and if there's no data they
  will return their argument (default 0).

int i    = node.dataAsInt(-1); // if no data, it will return -1
double d = node.dataAsDouble();
string s = node.dataAsString(); // if no data, this returns an empty string

// Reading "a double"
printf( "%f", tree["a double"].dataAsDouble() );

- T& dataAs( int i, T dummy )
  T& dataAs( int row, int col, T dummy )

short &sh = node.dataAs( 0, (short) 0 );

- getDataAs*: fills your array, converting as necessary

int *buf = new int[ node.dataRows() * node.dataSize() ];
node.getDataAsInts( buf, 0, node.dataRows() - 1 );



1
T& dataAs( int i, T dummy );
T& dataAs( int row, int col, T dummy );

These return a reference that's essentially a pointer into the node's data,
so you can assign to it.  T must match node.dataType().
The dummy argument specifies the return type but is otherwise ignored.


2
appendChild is rather inefficient: it crawls through the chain of children to find the end, each time.
A more efficient way to add several children is shown in the iterator section

3
When calling setData( QTR_TYPE, T ), T must match TYPE exactly.
For example, to set the data as a single float:
node.setData( QTR_TYPE_FLOAT, (float) 0.3 );


4
Recall that nodes are reference-counted: they are released when the last reference expires.
The flip side is that nodes stay valid as long as they are referenced, even if the
file has been closed.  Not a big deal when all the data is in memory anyway, but
for files with large partially-loaded data you could end up with the whole thing in RAM by mistake.

Unfortunately, the variable 'QUB_Tree tree' itself counts as a reference to the underlying root node,
so calling tree.close() guarantees the whole thing will stay in memory.

To avoid this problem, don't call close().  Files are closed automatically when no
variables reference them, so either let the variables go out of scope, or assign the null node:

child = QUB_Tree();
subtree = QUB_Tree();
tree = QUB_Tree();

Notice that the root was the last to go.  Otherwise subtree would still have been referenced
when the file closed, and would have jumped into RAM in order to stay valid.


5
QUB_TreeMonoIter is a more efficient substitute for QUB_TreeIter,
at the cost of --, prev(), insert(), and remove()