QUBTree Delphi 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 4
	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 -------------------------   

uses QUB_Node;

...

var
  tree, subtree, child           :INode;
  i, j                           :Integer;

// creating the root node:
tree   := TNode.Create('Sample');

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

// putting data in the child node
child.setupNumData( NODE_DATATYPE_INT, 4, 1 );
for i := 0 to child.DataCount - 1 do begin
   child.Data[i] := i;
end;

// adding a node with matrix data (reusing the INode variable)
child  := tree['matrix of ints'];
child.setupNumData( NODE_DATATYPE_INT, 2, 3 );

// filling the data
for i := 0 to child.Rows - 1 do begin
   for j := 0 to child.Cols - 1 do begin
      child.MatrixData[i, j] := j;
   end;
end;

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

// adding a subtree
subtree  := tree['sample subtree'];
child    := subtree.AppendChild( 'element' );
child['name'].SetData( 'foo' );

// cloning an "element" and inserting it after the original
// note that inserting is more efficient than appending
child = subtree.InsertClone( child, child, True );
child["name"].SetData( 'bar' );

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

// adding a node with one double as data
child = tree['a double'];
child.SetData( 0.3 );
child.LineComment := ' with a comment';


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

// saving in binary -- the file remains open until you close it
if not tree.SaveAs('sample.qtr') then raise Exception.Create('can''t save');

// saving a copy as text -- the binary file remains open
if not tree.SaveAsText('sample.qtt') then raise Exception.Create('can''t save as text');

// do some modifications here ...
tree.InsertChild('abc').SetData( 123 );

// saving changes to disk
tree.Save;

// closing the binary file
tree.Close;                                  1

// reading the text file
tree = TNode.CreateFromText('sample.qtt');

// opening the binary file for reading and writing
tree = TNode.Open('sample.qtr');
if tree.IsNull then raise Exception.Create('can''t open');


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

var
  someString          :String;
  aDouble             :Double;
  iarr                :TPAInteger;

// reading some data
someString            := tree['some string'].DataAsString;
aDouble               := Double(tree['a double'].Data[0]);

// raw array access
child                 := tree['array of ints'];
iarr                  := TPAInteger( child.DataPtr );
for i := 0 to child.DataCount - 1 do begin
   someString         := Format( '%d', [ iarr[i] ] );
end;

// read/write a row of data from a buffer
child                 := tree['matrix of ints'];
iarr                  := IAlloc1( child.Cols );
child.ReadRows( iarr, QUB_FirstLast(0, 0) );
child.WriteRows( iarr, QUB_FirstLast(1, 1) );

// replacing data
child                 := tree['pqz'];
child.SetData( 'foo' );
child.SetupNumData( NODE_DATATYPE_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.

var
  qdf, segment, channels        :INode;
  mplexData                     :TPASmallInt;
  firstRow, i                   :Integer;

qdf                             := TNode.Create('DataFile');
if qdf.SaveAs('sample.qdf') then begin

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

   // set up structure and metadata
   segment                      := qdf['Segments']['Segment'];
   segment['StartTime'].SetData( 0.0 );
   channels                     := segment['Channels'];

   qdf['Sampling'].SetData( 5.0e-5 );
   qdf['Scaling'].SetData( 320.0 );
   qdf['ADChannelCount'].SetData( 2 );
   qdf['ADDataSize'].SetData( 2 );
   qdf['ADDataType'].SetData( Integer(NODE_DATATYPE_SMALLINT) );

   // 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.Preload              := False;
   channels.SetupNumData( NODE_DATATYPE_SMALLINT, 5000000, 2 );

   // in a fit of lameness, just set the data points to zero, 100000 samples at a time
   firstRow := 0;
   while firstRow < 5000000 do begin
      channels.LoadRows( firstRow, firstRow + 100000 - 1, False ); // false: don't bother reading the (uninitialized) contents
      mplexData := TPASmallInt( channels.DataPtr );
      for i := 0 to 200000 - 1 do begin
         mplexData[i] := 0;
      end;

      firstRow := firstRow + 100000;
   end;
   channels.UnloadRows( True );

   qdf.Save;
end else begin
   raise Exception.Create('can''t save sample.qdf');
end;


----------- Working with child nodes ----------------------------------

// tree.Find(name) vs. tree[name]
tree[name] looks for the first child with that name; if none, it creates one automatically.
tree.Find(name) acts identically if such a child exists, but if none, it returns a null node.

// seeing if tree has a child named 'qaz'
if tree.Find('qaz').IsNull then begin
  print "there is no child named 'qaz'"
end;

// printing the name of each child node:
child  := tree.Child;
while not child.IsNull do begin
  print child.Name;
  child := child.Sibling;
end;

// printing the "name" of each "element" in "sample subtree"
child := tree['sample subtree'].Find('element');
while not child.IsNull do begin
  print child['name'].DataAsString;
  child := child.Sibling('element');
end;


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

// Making a copy of the whole tree
treecopy = tree.Clone(True);

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


----------- Working with TQUB_QFS_PNodeMem (aka QTR_Impl*)  -----------

INode acts as a wrapper around TQUB_QFS_PNodeMem.
INode 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
function func( treeptr :TQUB_QFS_PNodeMem ): TQUB_QFS_PNodeMem;
var
  tree, clone        :INode;
begin
  // wrap treeptr in TNode for easy manipulation
  tree               := TNode.CreateUsing( treeptr );
  clone              := tree.Clone(True);

  // get a pointer for the clone, and incref it so it outlasts the INode variable
  Result             := clone.GetNodeMem;
  Node_INCREF( Result );
end;

// calling func
tree        := TNode.Create('tree');
resultPtr   := func( tree.GetNodeMem );
resultTree  := TNode.CreateUsing( resultPtr );
Node_DECREF( resultPtr );

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

// storing a qub tree in a TList
var
  List          :TList;
begin
  List          := TList.Create;

  List.Add( tree.GetNodeMem );
  Node_INCREF( tree.GetNodeMem );

  // using a node from List
  tree2         := TNode.CreateUsing( TQUB_QFS_PNodeMem(List[0]) );
  ...

  // removing it from the list
  Node_DECREF( TQUB_QFS_PNodeMem(List[0]) );
  List.Delete( 0 );
end;


1
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 'tree :INode' 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 nil:

child = nil;
subtree = nil;
tree = nil;

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.