Part 6. SWIG Interface File

Here in my last post, we talk about the SWIG interface file, which is what ties everything together. So, we’ll go into a little more detail on how it is created, since this is something you need to know how to do from scratch. The SWIG interface file has a file extension of ".i" and for the Unicode module, it is called “unicode.i” (but you can name it anything you like). So, first lets look at the file content, then I will go through it section by section.

unicode.i
/* -----------------------------------------------------------------------------
 * vim: fileencoding=utf8
 *
 * unicode.i
 *
 * SWIG typemaps for Unicode module
 * ----------------------------------------------------------------------------- */

%module Unicode
%newobject Unicode_export_string(const struct Unicode * i_pouniObject, size_t i_sizMaxbytes, const char * i_poszEncoding);
%newobject Unicode_from_string(const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding);
%newobject Unicode_from_int(int i_inValue);
%newobject Unicode_from_long(long i_loValue);
%newobject Unicode_from_longlong(long long i_llValue);
%newobject Unicode_from_float(float i_flValue);
%newobject Unicode_from_double(double i_doValue);
%newobject Unicode_from_longdouble(long double i_ldValue);
%newobject Unicode_extract(const struct Unicode * i_pouniObject, size_t i_sizOffset, size_t i_inCount);
%newobject Unicode_uppercase(const struct Unicode * i_pouniObject);
%newobject Unicode_lowercase(const struct Unicode * i_pouniObject);
%newobject Unicode_swapcase(const struct Unicode * i_pouniObject);
%newobject Unicode_concatenate(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniConcatenate);
%newobject Unicode_split(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniDelimiters, int i_inTrim);
%newobject Unicode_join(struct UnicodeArray * i_pounaObject, const struct Unicode * i_pouniDelimiter, int i_inTrim);
%newobject Unicode_from_array(const struct UnicodeArray * i_pounaObject);
%newobject Unicode_from_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS);
%newobject Unicode_extract_subvalue(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
%newobject Unicode_to_tesseract(const struct Unicode * i_pouniObject, size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4);
%newobject Unicode_from_tesseract(const struct UnicodeTesseract * i_pountObject);

%ignore Unicode_new;
%ignore Unicode_delete;
%ignore UnicodeArray_new;
%ignore UnicodeArray_delete;
%ignore UnicodeTesseract_new;
%ignore UnicodeTesseract_delete;
%rename("%(strip:[Unicode_])s") "";

/* Includes the header in the wrapper code */
%{
#include "unicode.h"
%}
 
/* This tells SWIG to treat char ** as a special case */
%typemap(in) char ** {
    AV *tempav;
    I32 len;
    int i;
    SV  **tv;
    if (!SvROK($input))
        croak("Argument $argnum is not a reference.");
    if (SvTYPE(SvRV($input)) != SVt_PVAV)
        croak("Argument $argnum is not an array.");
    tempav = (AV*)SvRV($input);
    len = av_len(tempav);
    $1 = (char **) malloc((len+2)*sizeof(char *));
    for (i = 0; i <= len; i++) {
        tv = av_fetch(tempav, i, 0);
        $1[i] = (char *) SvPV(*tv, PL_na);
    }
    $1[i] = NULL;
};

/* Creates a new Perl array and places a NULL-terminated char ** into it */
%typemap(out) char ** {
    AV *myav;
    SV **svs;
    int i = 0, len = 0;
    /* Figure out how many elements we have */
    while ($1[len]) len++;
    svs = (SV **) malloc(len*sizeof(SV *));
    for (i = 0; i < len ; i++) {
        svs[i] = sv_newmortal();
        sv_setpv((SV*)svs[i], $1[i]);
    };
    myav = av_make(len, svs);
    free(svs);
    $result = newRV_noinc((SV*)myav);
    sv_2mortal($result);
    argvi++;
}

%extend Unicode {
    Unicode() { 
        return Unicode_new(); 
    } 
    ~Unicode() { 
        Unicode_delete(&$self); 
    } 
}

%extend UnicodeArray {
    UnicodeArray(size_t i_sizElements) { 
        return UnicodeArray_new(i_sizElements); 
    } 
    ~UnicodeArray() { 
        UnicodeArray_delete(&$self); 
    } 
    struct Unicode * get_element(size_t i_sizOffset) {
        return(&($self->m_pouniObjects[i_sizOffset]));
    }
    void set_element(const struct Unicode * i_pouniObject, size_t i_sizOffset) {
        $self->m_pouniObjects[i_sizOffset].m_poszCodepoints = i_pouniObject->m_poszCodepoints;
        $self->m_pouniObjects[i_sizOffset].m_sizCodepoints = i_pouniObject->m_sizCodepoints;
        $self->m_pouniObjects[i_sizOffset].m_sizBytes = i_pouniObject->m_sizBytes;
    }
}

%extend UnicodeTesseract {
    UnicodeTesseract(size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4) {
        return UnicodeTesseract_new(i_sizCodepoints, i_sizDim1, i_sizDim2, i_sizDim3, i_sizDim4);
    } 
    ~UnicodeTesseract() { 
        UnicodeTesseract_delete(&$self); 
    } 
}

/* Parse the header file to generate wrappers */

%include "unicode.h"

%extend Unicode {
#ifdef SWIG
%perlcode %{
    use overload
        "=" => sub { my $class = ref($_[0]); $class->new($_[0]) },
        "+" => sub { $_[0]->concatenate($_[1]) },
        "<=>" => sub { $_[0]->compare_ascendingnumeric($_[1]) },
        "==" => sub { $_[0]->compare_ascendingnumeric($_[1]) == 0 },
        "!=" => sub { $_[0]->compare_ascendingnumeric($_[1]) != 0 },
        "<" => sub { $_[0]->compare_ascendingnumeric($_[1]) < 0 },
        "<=" => sub { $_[0]->compare_ascendingnumeric($_[1]) <= 0 },
        ">" => sub { $_[0]->compare_ascendingnumeric($_[1]) > 0 },
        ">=" => sub { $_[0]->compare_ascendingnumeric($_[1]) >= 0 },
        "cmp" => sub { $_[0]->compare_ascendingstring($_[1]) },
        "eq" => sub { $_[0]->compare_ascendingstring($_[1]) == 0 },
        "ne" => sub { $_[0]->compare_ascendingstring($_[1]) != 0 },
        "lt" => sub { $_[0]->compare_ascendingstring($_[1]) < 0 },
        "le" => sub { $_[0]->compare_ascendingstring($_[1]) <= 0 },
        "gt" => sub { $_[0]->compare_ascendingstring($_[1]) > 0 },
        "ge" => sub { $_[0]->compare_ascendingstring($_[1]) >= 0 },
        "fallback" => 1;
%}
#endif
};

The interface file uses C-style multi-line comments starting with /* and ending with */. The top lines contain a vim(1) directive to use UTF-8, the name of the file, along with a description.

Note the %module Unicode line. This is important, as this is how you name your module. It can be any name you like, and is how you can keep it from interfering with other CPAN package names when imported into your Perl code with the use statement.

Like Perl, but unlike C, SWIG adds code to automatically handle constructors and destructors in an object-oriented style. But to do this, it needs to know what C functions create and return new objects. This is where you list them by simply listing them as %newobject followed by the C function declaration. See the SWIG documentation for more details.

The %ignore lines list the C functions that we do not want SWIG to import. This is because SWIG will be handling our constructors and destructors for us automatically, and we want don’t want to conflict with what SWIG does. We will be revisiting this later when we use the %extend command to add the constructors and destructors.

Inside the Perl code, to reduce typing overhead, we are going to remove the Unicode_ prefix of all the C functions. The %rename statement does this for us automatically. So, instead of using in Perl the statement Unicode::Unicode_from_string(...) it is going to be renamed to Unicode::from_string(...).

Inside the %{ and %} brackets, you tell SWIG what C files to include to parse for function declarations. Sometimes in the simplest SWIG cases, this is the only thing you have to do.

The Unicode module has two methods called (ignoring the Unicode_ prefix from now on) from_subvalues() and to_subvalues() which allow Unicode to transfer an entire level of subvalues from/to a C dynamic array (a char** pointer to an array null-terminated string pointers). But the capability to go between a Perl dynamic array and a C char** didn’t exist, so some XS was required to create this interface. This required the use of a couple of custom %typemap SWIG sections containing XS C code. I am not going to go into XS code here, there is Perl documentation for that here. The %typemap sections are how SWIG interfaces with all other computer languages it supports, and really is the core of SWIG’s capabilities.

Now, we get to %extend the Unicode C structs and give them proper contstructors and destructors for object-oriented type usage. Note there are %extend sections for all three Unicode structs:

  • Unicode
  • UnicodeArray
  • UnicodeTesseract

If you look closely at the code, you will also see the usage of the C functions we previously used %ignore statements with. Now you know why.

Next is what seems to be a redundant %include "unicode.h" statement. Actually, it is not redundant, as the other was a #include "unicode.h" (note the use of % not #). The %include statement is used to generate the so-called “wrapper code” file named "unicode_wrap.c" for the Unicode module. See this SWIG documentation for more details.

In the same way that SWIG allows you to add needed C (think XS) code as needed, it also allows you to add Perl (or other scripting languages) code as needed to extend the module. In the last section, the Unicode module is extended using the %perlcode section to add C++ operator overloads strictly for Unicode objects in the Perl code. The #ifdef SWIG statement is included so that when compiling the C code, it is only included by SWIG when necessary. Basically, all the numeric and string comparison operators are overloaded for convenience. See the testunicode.pl code for examples of their use.

This wraps it up (pun intended)! SWIG does a lot, but really IMHO does not have a steep learning curve, and it is as flexible as I think you can make it. It can be used with Perl and many other languages. So, if your Perl code is too slow, SWIG it and you can even beat Python’s famous numpy module with your own custom C/C++. Happy SWIG’ing!

SWIG main site
SWIG HTML Documentation on one page

Part 5. Build and Run Scripts

While not part of the SWIG or Perl applications per se, the build and run shell scripts (bash on Linux) are probably the most problematic parts to set up. The SWIG website has a good example of setting them up in the SWIG documentation but for the first time user, it can be a little complicated and error prone to set up. Here is the code to the build script for Unicode first, and then I will explain more about it’s content and structure.

build.sh
#!/bin/sh
# vim: fileencoding=utf8
swig -perl unicode.i
gcc -Wall -pipe -c `perl -MConfig -e 'print join(" ", @Config{qw(ccflags optimize cccdlflags)}, "-I$Config{archlib}/CORE")'` unicode.c &
gcc -pipe -c `perl -MConfig -e 'print join(" ", @Config{qw(ccflags optimize cccdlflags)}, "-I$Config{archlib}/CORE")'` unicode_wrap.c &
wait
gcc `perl -MConfig -e 'print $Config{lddlflags}'` unicode.o unicode_wrap.o -o Unicode.so

Okay, let’s dive in. Line 1 is the “she-bang” line activating whatever program in your system’s default shell. On most (like my Ubuntu) it is bash(1). The second line is a convenience line for me in that I use vim(1), actually I use gvim(1) which is a graphical version of vim. It tells it to use UTF-8 encoding for the complete shell file.

The third line starting with “swig” actually generates the C source code files that will be used to create the dynamicly linked library (.so file extension in Linux). The file unicode.i is the SWIG interface file, which we will go into in detail in my next blog post. The SWIG interface file tells SWIG how to configure your dynamic module.

Something that is incredibly important to know, is that for any C dynamic library to be combined with any other code on the same machine, is that they really, really should be compiled with the same compiler options. In this case, you should compile your own C code with the same options that Perl was compiled with. So how do you do that? The magic is provided by Perl using the following code (note the surrounding backticks):

`perl -MConfig -e 'print join(" ", @Config{qw(ccflags optimize cccdlflags)}, "-I$Config{archlib}/CORE")'`

All that stuff returns the compiler flags used to compile your system’s version of Perl. All you then need to add is:

  • gcc = name of C compiler on your system
  • -Wall = turn on all warnings for your C code
  • -pipe = to be able to give the Perl flags to the C compiler
  • -c = tells the compiler to only compile, not link
  • unicode.c = name of the C source code file
  • & = compile in the background

The second gcc line is similar to the first compilation line, but with subtle differences:

  • gcc = name of C compiler on your system
  • -pipe = to be able to give the Perl flags to the C compiler
  • -c tells the compiler to only compile, not link
  • unicode_wrap.c = name of the SWIG generated wrapper file
  • & = compile in the background

The next line just has the shell command wait which just causes the shell file to stop and wait until all background compilations are done compiling before continuing.

The final gcc line will link all the stuff together for you into a dynamicly linked library:

  • gcc = name of C linker on your system
  • perl -MConfig -e 'print $Config{lddlflags}' = linker flags from Perl surrounded by backticks to include them on the compilation line
  • unicode.o = name of the object file created by the first compilation line
  • unicode_wrapper.o = name of the SWIG generated source code compilation line
  • -o Unicode.so = name of the dynamically linked library you want to use

What isn’t covered above, is that SWIG also generated a Perl file called Unicode.pm which will actually do all the heavy lifting when you use Unicode; in the testunicode.pl script. It is something you should be aware of, but you really shouldn’t modify the Unicode.pm file unless you really know what you are doing. It is usually a better idea to let SWIG simply regenerate it again.

By running the above build.sh script, you should be able to create your dynamically linked library on Ubuntu and/or most Linux distributions. If you have any problems, you can get helpful ideas from the SWIG documentation and the SWIG Wiki.

run.sh

This is really almost silly to describe, but it is being included for completeness.

#!/bin/sh
# vim: fileencoding=utf8
export PERL5LIB=.
perl testunicode.pl

The first two lines are exactly the same as in the build.sh shell script. The third line exports the current directory to the PERL5LIB environment variable so that Perl will include it in the folders in which it searchs for modules. Note that this setup requires your dynamically linked library to be put into the same folder that your testunicode.pl script executes in. This PERL5LIB environment variable enables that. If your system setup already has values in PERL5LIB, you may want to use a different line like:

export PERL5LIB=$PERL5LIB:.

The final line runs the test script testunicode.pl and you should have output similar to the following:

ok 1 - use POSIX;
ok 2 - use Time::Piece;
ok 3 - use Time::Seconds;
ok 4 - use Time::HiRes;
ok 5 - use Encode;
ok 6 - require Unicode;
ok 7 - Check availablity of all needed Unicode methods
ok 8 - Construct empty Unicode object
ok 9 - Determine if Unicode object is empty after construction
ok 10 - Initialize a Unicode object with a UTF string value
ok 11 - Get codepoint count of Unicode object content
ok 12 - Get byte count of Unicode object content
ok 13 - Initialize Unicode object from an integer
ok 14 - Initialize Unicode object from a long integer
ok 15 - Initialize Unicode object from a long long integer
ok 16 - Initialize Unicode object from a float
ok 17 - Initialize Unicode object from a double
ok 18 # skip Perl does not support long double type
ok 19 - Convert Unicode object to a string
ok 20 - Convert Unicode object to an integer
ok 21 - Convert Unicode object to a long integer
ok 22 - Convert Unicode object to a long long integer
ok 23 - Convert Unicode object to a float
ok 24 - Convert Unicode object to a double
ok 25 # skip Perl does not support long double type
ok 26 - Copy another Unicode object codepoints to Unicode Object
ok 27 - Append another Unicode object codepoints to Unicode Object
ok 28 - Append multiple copies of a Unicode object codepoints to Unicode Object
ok 29 - Swap another Unicode object codepoints with Unicode Object codepoints
ok 30 - Find offset of codepoints of a Unicode object inside another Unicode object
ok 31 - Return extracted codepoints inside Unicode object in new Unicode object
ok 32 - Replace existing Unicode object codepoints with other Unicode object codepoints
ok 33 - Compare Unicode object with another Unicode object as strings
ok 34 - Compare Unicode object with another Unicode object as numbers
ok 35 - Return uppercased Unicode object content in new Unicode object
ok 36 - Return lowercased Unicode object content in new Unicode object
ok 37 - Return swapcased Unicode object content in new Unicode object
ok 38 - Return concatenated Unicode objects in new Unicode Object
ok 39 - Split Unicode object into UnicodeArray object
ok 40 - Join UnicodeArray object into Unicode object
ok 41 - Convert Perl array of strings into UnicodeArray object
ok 42 - Convert UnicodeArray object into a Perl array of strings
ok 43 - Return Unicode object subvalues in Perl array
ok 44 - Store Perl array as Unicode object subvalues
ok 45 - Count Unicode object 4-dimensional dynamic array subvalues
ok 46 - Extract Unicode object 4-dimensional dynamic array subvalues
ok 47 - Replace Unicode object 4-dimensional dynamic array subvalues
ok 48 - Insert Unicode object 4-dimensional dynamic array subvalues
ok 49 - Append Unicode object 4-dimensional dynamic array subvalues
ok 50 - Delete Unicode object 4-dimensional dynamic array subvalues
ok 51 - Sort Unicode object 4-dimensional dynamic array subvalues
ok 52 - Locate Unicode object 4-dimensional dynamic array subvalues
ok 53 - Store one-year calendar data in 4-dimensional dynamic array
ok 54 - Get Unicode object codepoints
ok 55 - Set Unicode object codepoints
ok 56 - Find a specific codepoint inside Unicode object
ok 57 - Get POSIX class of a single codepoint inside Unicode object
ok 58 - Get lowercase values of codepoints inside Unicode object
ok 59 - Get uppercase values of codepoints inside Unicode object
ok 60 - Save and load Unicode object content in specified encoding with specified file
ok 61 - Test four-dimensional UnicodeTesseract object
Time: 0.672364 seconds
1..61

That’s it for now. I will go into detail on the creation of the SWIG interface file next time.

Part 4. Perl Source File

The Perl source is for a test program written to test the C/C++ functions in Perl before the Unicode library was utilized in live projects. It checks all of the functionality of the Unicode C/C++ library, although probably not as exhaustively as could be. For example, failure cases were not written, which should be part of any testing regime.

What the Perl file is really good for, is learning how a SWIG interfaced C/C++ library looks like at the coding level. Scalars are used as the package (class) object, and have the type of the SWIG project name, in this case, new objects have the ref() value of "Unicode::Unicode". Class methods are called using the "Unicode::"package prefix, and are not called directly as methods of the objects created. Note that three subclasses are created by the Unicode SWIG code based upon struct definitions in the C/C++ code. They are:

  • Unicode::Unicode
  • Unicode::UnicodeArray
  • Unicode::UnicodeTesseract

These are distinct ref() types in the Perl code.

In the code you will see goto START, goto DONE statements, they were only used for debugging initially and can be disregarded. You will see that a $debug variable is available on line 77, which is set to false (0) by default. If you set it to true (1), you will get extra verbose output on the timing of certain core functions of the C/C++ library.

The best way to understand the SWIG interface, is just to go through the file and see how it works in each test. So, here is the source code, with the POD documentation intact:

#!/usr/bin/env perl
# vim: fileencoding=utf8
#===============================================================================
#
#         FILE:  testunicode.pl
#
#        USAGE:  ./testunicode.pl  
#
#  DESCRIPTION:  Test SWIG "Unicode" Module for Perl 5.10+
#
# REQUIREMENTS:  Unicode.pm and Unicode.so
#        NOTES:  Contains POD documentation for Unicode SWIG Perl interface
#      VERSION:  1.6
#      CREATED:  12/23/2017
#     REVISION:  1.1 04/21/2018
#                Added Unicode_from_array(), Unicode_to_array()
#                Moved all ->can() routines into first test for easier maintenance
#     REVISION:  1.2 07/01/2018
#                Added Time::HiRes module and debug timing
#     REVISION:  1.3 10/22/2018
#                Added tests for operators eq,ne,lt,le,gt,ge,==,!=,<,<=,>,>=,+
#     REVISION:  1.4 11/08/2018
#                Changed utf8::decode to Encode::decode calls
#     REVISION:  1.5 12/24/2018
#                Added total execution time at end
#     REVISION:  1.6 05/13/2021
#                Added Config module to test for longlong and longdouble support
#
#  This is free software: you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the
#  Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#  See the GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License along
#  with this program.  If not, see <http://www.gnu.org/licenses/>.
#  
#===============================================================================

=head1 NAME

testunicode.pl - SWIG Unicode Test Program and Unicode.pm Interface Documentation

=cut

use strict;
use warnings;
use utf8;
use open qw(:std :utf8);
use locale;
use feature ':5.10';
use Test::More;

BEGIN {
    use_ok("POSIX", qw(locale_h));
    use_ok("Time::Piece");
    use_ok("Time::Seconds");
    use_ok("Time::HiRes", qw(gettimeofday));
    use_ok("Encode", qw(encode decode));
}

setlocale(LC_ALL, 'en_US.utf8');

sub Test ($&) {
    my ($test, $sub) = @_;
    my ($ok, $diag) = &$sub();
    ok($ok, $test) or diag($diag);
}

my $version = '1.6';
my $true = 1;
my $false = 0;
my $debug = 0;
my $starttime = 0;
my $endtime = 0;

require_ok('Unicode') or BAIL_OUT('Cannot require "Unicode" module');

goto START;
START:

my $begintime = gettimeofday();

Test('Check availablity of all needed Unicode methods', sub {
        return $false, 'Unicode::Unicode::new() method does not exist' unless Unicode::Unicode->can('new');
        return $false, 'Unicode::UnicodeArray::new() method does not exist' unless Unicode::UnicodeArray->can('new');
        return $false, 'Unicode::UnicodeTesseract::new() method does not exist' unless Unicode::UnicodeTesseract->can('new');
        return $false, 'clear() method does not exist' unless Unicode->can('clear');
        return $false, 'empty() method does not exist' unless Unicode->can('empty');
        return $false, 'codepoints() method does not exist' unless Unicode->can('codepoints');
        return $false, 'bytes() method does not exist' unless Unicode->can('bytes');
        return $false, 'import_string() method does not exist' unless Unicode->can('import_string');
        return $false, 'export_string() method does not exist' unless Unicode->can('export_string');
        return $false, 'from_string() method does not exist' unless Unicode->can('from_string');
        return $false, 'from_int() method does not exist' unless Unicode->can('from_int');
        return $false, 'from_long() method does not exist' unless Unicode->can('from_long');
        return $false, 'from_longlong() method does not exist' unless Unicode->can('from_longlong');
        return $false, 'from_float() method does not exist' unless Unicode->can('from_float');
        return $false, 'from_double() method does not exist' unless Unicode->can('from_double');
        return $false, 'from_longdouble() method does not exist' unless Unicode->can('from_longdouble');
        return $false, 'to_int() method does not exist' unless Unicode->can('to_int');
        return $false, 'to_long() method does not exist' unless Unicode->can('to_long');
        return $false, 'to_longlong() method does not exist' unless Unicode->can('to_longlong');
        return $false, 'to_float() method does not exist' unless Unicode->can('to_float');
        return $false, 'to_double() method does not exist' unless Unicode->can('to_double');
        return $false, 'to_longdouble() method does not exist' unless Unicode->can('to_longdouble');
        return $false, 'copy() method does not exist' unless Unicode->can('copy');
        return $false, 'append() method does not exist' unless Unicode->can('append');
        return $false, 'append_multiple() method does not exist' unless Unicode->can('append_multiple');
        return $false, 'swap() method does not exist' unless Unicode->can('swap');
        return $false, 'find() method does not exist' unless Unicode->can('find');
        return $false, 'extract() method does not exist' unless Unicode->can('extract');
        return $false, 'replace() method does not exist' unless Unicode->can('replace');
        return $false, 'compare_ascendingstring() method does not exist' unless Unicode->can('compare_ascendingstring');
        return $false, 'compare_descendingstring() method does not exist' unless Unicode->can('compare_descendingstring');
        return $false, 'compare_ascendingnumeric() method does not exist' unless Unicode->can('compare_ascendingnumeric');
        return $false, 'compare_descendingnumeric() method does not exist' unless Unicode->can('compare_descendingnumeric');
        return $false, 'uppercase() method does not exist' unless Unicode->can('uppercase');
        return $false, 'lowercase() method does not exist' unless Unicode->can('lowercase');
        return $false, 'swapcase() method does not exist' unless Unicode->can('swapcase');
        return $false, 'concatenate() method does not exist' unless Unicode->can('concatenate');
        return $false, 'split() method does not exist' unless Unicode->can('split');
        return $false, 'join() method does not exist' unless Unicode->can('join');
        return $false, 'from_array() method does not exist' unless Unicode->can('from_array');
        return $false, 'to_array() method does not exist' unless Unicode->can('to_array');
        return $false, 'Unicode::UnicodeArray::set_element() method does not exist' unless Unicode::UnicodeArray->can('set_element'); # see unicode.i
        return $false, 'Unicode::UnicodeArray::get_element() method does not exist' unless Unicode::UnicodeArray->can('get_element'); # see unicode.i
        return $false, 'from_subvalues() method does not exist' unless Unicode->can('from_subvalues');
        return $false, 'to_subvalues() method does not exist' unless Unicode->can('to_subvalues');
        return $false, 'count_subvalues() method does not exist' unless Unicode->can('count_subvalues');
        return $false, 'sort_subvalues() method does not exist' unless Unicode->can('sort_subvalues');
        return $false, 'locate_subvalue() method does not exist' unless Unicode->can('locate_subvalue');
        return $false, 'extract_subvalue() method does not exist' unless Unicode->can('extract_subvalue');
        return $false, 'replace_subvalue() method does not exist' unless Unicode->can('replace_subvalue');
        return $false, 'insert_subvalue() method does not exist' unless Unicode->can('insert_subvalue');
        return $false, 'append_subvalue() method does not exist' unless Unicode->can('append_subvalue');
        return $false, 'delete_subvalue() method does not exist' unless Unicode->can('delete_subvalue');
        return $false, 'get_codepoint() method does not exist' unless Unicode->can('get_codepoint');
        return $false, 'set_codepoint() method does not exist' unless Unicode->can('set_codepoint');
        return $false, 'find_codepoint() method does not exist' unless Unicode->can('find_codepoint');
        return $false, 'isalnum_codepoint() method does not exist' unless Unicode->can('isalnum_codepoint');
        return $false, 'isalpha_codepoint() method does not exist' unless Unicode->can('isalpha_codepoint');
        return $false, 'islower_codepoint() method does not exist' unless Unicode->can('islower_codepoint');
        return $false, 'isupper_codepoint() method does not exist' unless Unicode->can('isupper_codepoint');
        return $false, 'isdigit_codepoint() method does not exist' unless Unicode->can('isdigit_codepoint');
        return $false, 'isxdigit_codepoint() method does not exist' unless Unicode->can('isxdigit_codepoint');
        return $false, 'iscntrl_codepoint() method does not exist' unless Unicode->can('iscntrl_codepoint');
        return $false, 'isgraph_codepoint() method does not exist' unless Unicode->can('isgraph_codepoint');
        return $false, 'isspace_codepoint() method does not exist' unless Unicode->can('isspace_codepoint');
        return $false, 'isblank_codepoint() method does not exist' unless Unicode->can('isblank_codepoint');
        return $false, 'isprint_codepoint() method does not exist' unless Unicode->can('isprint_codepoint');
        return $false, 'ispunct_codepoint() method does not exist' unless Unicode->can('ispunct_codepoint');
        return $false, 'tolower_codepoint() method does not exist' unless Unicode->can('tolower_codepoint');
        return $false, 'toupper_codepoint() method does not exist' unless Unicode->can('toupper_codepoint');
        return $false, 'save() method does not exist' unless Unicode->can('save');
        return $false, 'load() method does not exist' unless Unicode->can('load');
        return $false, 'to_tesseract() method does not exist' unless Unicode->can('to_tesseract');
        return $false, 'from_tesseract() method does not exist' unless Unicode->can('from_tesseract');
        return $false, 'set_element() method does not exist' unless Unicode->can('set_element');
        return $false, 'get_element() method does not exist' unless Unicode->can('get_element');
        return $true;
    }
);

Test('Construct empty Unicode object', sub {
        $starttime = gettimeofday() if $debug;
        my $l_uniObject = new Unicode::Unicode();
        $endtime = gettimeofday() if $debug;
        printf "new Unicode::Unicode debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'undef returned instead of new Unicode::Unicode object' unless defined($l_uniObject);
        return $false, 'unexpected "' . ref($l_uniObject) . '" for ref(object)' unless ref($l_uniObject) eq 'Unicode::Unicode';
        return $true;
    }
);

Test('Determine if Unicode object is empty after construction', sub {
        my $l_uniObject = new Unicode::Unicode();
        return $false, 'new Unicode object $l_uniObject->empty() failure' unless $l_uniObject->empty();
        return $true;
    }
);

Test('Initialize a Unicode object with a UTF string value', sub {
        my $l_strValue = 'João Méroço';
        my $l_uniImportstring = new Unicode::Unicode();
        $starttime = gettimeofday() if $debug;
        my $l_inInbytes = $l_uniImportstring->import_string($l_strValue, 0, 'UTF8');
        $endtime = gettimeofday() if $debug;
        printf "import_string debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, '$l_uniImportstring object is empty' if $l_uniImportstring->empty();
        return $false, 'Expected 14 bytes in but got ' . $l_inInbytes unless $l_inInbytes == 14;
        return $false, 'Expected 11 codepoints out but got ' . $l_uniImportstring->{m_sizCodepoints} unless $l_uniImportstring->{m_sizCodepoints} == 11;
        return $false, 'Expected 44 bytes out but got ' . $l_uniImportstring->{m_sizBytes} unless $l_uniImportstring->{m_sizBytes} == 44;
        $starttime = gettimeofday() if $debug;
        my $l_uniFromstring = Unicode::from_string($l_strValue, 0, 'UTF8');
        $endtime = gettimeofday() if $debug;
        printf "Unicode::from_string debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, '$l_uniFromstring object is empty' if $l_uniImportstring->empty();
        return $false, 'Expected $l_uniImportstring->{m_sizCodepoints} == $l_uniFromstring->{m_sizCodepoints}' unless $l_uniImportstring->{m_sizCodepoints} == $l_uniFromstring->{m_sizCodepoints};
        return $false, 'Expected $l_uniImportstring->{m_sizBytes} == $l_uniFromstring->{m_sizBytes}' unless $l_uniImportstring->{m_sizBytes} == $l_uniFromstring->{m_sizBytes};
        return $true;
    }
);

Test('Get codepoint count of Unicode object content', sub {
        my $l_strValue = 'João Méroço';
        my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
        return $false, 'Expected $l_uniObject->codepoints() == $l_uniObject->{m_sizCodepoints}' unless $l_uniObject->codepoints() == $l_uniObject->{m_sizCodepoints};
        return $true;
    }
);

Test('Get byte count of Unicode object content', sub {
        my $l_strValue = 'João Méroço';
        my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
        my $l_inBytes = $l_uniObject->bytes();
        return $false, 'Expected $l_uniObject->bytes() == $l_uniObject->{m_sizBytes}' unless $l_uniObject->bytes() == $l_uniObject->{m_sizBytes};
        return $true;
    }
);

Test('Initialize Unicode object from an integer', sub {
        my $l_inValue = 12345;
        $starttime = gettimeofday() if $debug;
        my $l_uniInt = Unicode::from_int($l_inValue);  # %d
        $endtime = gettimeofday() if $debug;
        printf "Unicode::from_int debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'from_int() returned empty object' if $l_uniInt->empty();
        my $l_inCodepoints = $l_uniInt->codepoints();
        return $false, 'Expected 5 codepoints output but got ' . $l_inCodepoints unless $l_inCodepoints == 5;
        my $l_inBytes = $l_uniInt->bytes();
        return $false, 'Expected 20 bytes output but got ' . $l_inBytes unless $l_inBytes == 20;
        return $true;
    }
);

Test('Initialize Unicode object from a long integer', sub {
        my $l_loValue = 1234567890;
        $starttime = gettimeofday() if $debug;
        my $l_uniLong = Unicode::from_long($l_loValue);  # %ld
        $endtime = gettimeofday() if $debug;
        printf "Unicode::from_long debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'from_long() returned empty object' if $l_uniLong->empty();
        my $l_inCodepoints = $l_uniLong->codepoints();
        return $false, 'Expected 10 codepoints output but got ' . $l_inCodepoints unless $l_inCodepoints == 10;
        my $l_inBytes = $l_uniLong->bytes();
        return $false, 'Expected 40 bytes output but got ' . $l_inBytes unless $l_inBytes == 40;
        return $true;
    }
);

SKIP: {
    Test('Initialize Unicode object from a long long integer', sub {
            my $l_uniObject = Unicode::from_string('1234567890987654321', 0, 'UTF8');
            skip "Perl does not support long long type", 1 if ref($l_uniObject->to_longlong()) ne '';
            my $l_llValue = 1234567890987654321;
            $starttime = gettimeofday() if $debug;
            my $l_uniLonglong = Unicode::from_longlong($l_llValue);  # %lld
            $endtime = gettimeofday() if $debug;
            printf "Unicode::from_longlong debug time = %.9lf\n", $endtime - $starttime if $debug;
            return $false, 'from_longlong() returned empty object' if $l_uniLonglong->empty();
            my $l_inCodepoints = $l_uniLonglong->codepoints();
            return $false, 'Expected 19 codepoints output but got ' . $l_inCodepoints unless $l_inCodepoints == 19;
            my $l_inBytes = $l_uniLonglong->bytes();
            return $false, 'Expected 76 bytes output but got ' . $l_inBytes unless $l_inBytes == 76;
            return $true;
        }
    );
}

Test('Initialize Unicode object from a float', sub {
        my $l_flValue = 123.45;
        $starttime = gettimeofday() if $debug;
        my $l_uniFloat = Unicode::from_float($l_flValue);  # %.7e
        $endtime = gettimeofday() if $debug;
        printf "Unicode::from_float debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'from_float() returned empty object' if $l_uniFloat->empty();
        my $l_inCodepoints = $l_uniFloat->codepoints();
        return $false, 'Expected 13 codepoints output but got ' . $l_inCodepoints unless $l_inCodepoints == 13;
        my $l_inBytes = $l_uniFloat->bytes();
        return $false, 'Expected 52 bytes output but got ' . $l_inBytes unless $l_inBytes == 52;
        return $true;
    }
);

Test('Initialize Unicode object from a double', sub {
        my $l_doValue = 12345.6789;
        $starttime = gettimeofday() if $debug;
        my $l_uniDouble = Unicode::from_double($l_doValue);  # %.15le
        $endtime = gettimeofday() if $debug;
        printf "Unicode::from_double debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'from_double() returned empty object' if $l_uniDouble->empty();
        my $l_inCodepoints = $l_uniDouble->codepoints();
        return $false, 'Expected 21 codepoints output but got ' . $l_inCodepoints unless $l_inCodepoints == 21;
        my $l_inBytes = $l_uniDouble->bytes();
        return $false, 'Expected 84 bytes output but got ' . $l_inBytes unless $l_inBytes == 84;
        return $true;
    }
);

SKIP: {
    Test('Initialize Unicode object from a long double', sub {
            my $l_uniObject = Unicode::from_string('1234567890.987654321', 0, 'UTF8');
            skip "Perl does not support long double type", 1 if ref($l_uniObject->to_longdouble()) ne '';
            my $l_ldValue = $l_uniObject->to_longdouble();
            $starttime = gettimeofday() if $debug;
            my $l_uniLongdouble = Unicode::from_longdouble($l_ldValue);  # %.18Le
            $endtime = gettimeofday() if $debug;
            printf "Unicode::from_longdouble debug time = %.9lf\n", $endtime - $starttime if $debug;
            return $false, 'from_longdouble() returned empty object' if $l_uniLongdouble->empty();
            my $l_inCodepoints = $l_uniLongdouble->codepoints();
            return $false, 'Expected 36 codepoints output but got ' . $l_inCodepoints unless $l_inCodepoints == 36;
            my $l_inBytes = $l_uniLongdouble->bytes();
            return $false, 'Expected 144 bytes output but got ' . $l_inBytes unless $l_inBytes == 144;
            return $true;
        }
    );
}

Test('Convert Unicode object to a string', sub {
        my $l_uniObject = new Unicode::Unicode();
        my $l_strUtf8 = 'João Méroço';
        return $false, 'expected utf8::is_utf8($l_strUtf8) == 1 but got undef' unless utf8::is_utf8($l_strUtf8);
        my $l_inInbytes = $l_uniObject->import_string($l_strUtf8, 0, 'UTF8');
        return $false, 'Unicode::Unicode object is empty' if $l_uniObject->empty();
        return $false, 'Expected 14 bytes input but got ' . $l_inInbytes unless $l_inInbytes == 14;
        my $l_inCodepoints = $l_uniObject->codepoints();
        my $l_inBytes = $l_uniObject->bytes();
        return $false, 'Expected 11 codepoints output but got ' . $l_inCodepoints unless $l_inCodepoints == 11;
        return $false, 'Expected 44 bytes output but got ' . $l_inBytes unless $l_inBytes == 44;
        $starttime = gettimeofday() if $debug;
        my $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $endtime = gettimeofday() if $debug;
        printf "export_string debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected utf8::is_utf8($l_strResult) == undef but got 1' if utf8::is_utf8($l_strResult);
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected utf8::is_utf8($l_strResult) == 1 but got undef' unless utf8::is_utf8($l_strResult);
        return $false, 'expected $l_strResult eq $l_strUtf8 but got ne' unless $l_strResult eq $l_strUtf8;
        return $true;
    }
);

Test('Convert Unicode object to an integer', sub {
        my $l_strValue = '12345';
        my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
        return $false, 'Unicode::Unicode object is empty' if $l_uniObject->empty();
        $starttime = gettimeofday() if $debug;
        my $l_inValue = $l_uniObject->to_int();
        $endtime = gettimeofday() if $debug;
        printf "to_int debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected $l_strValue == $l_inValue but got !=' unless $l_strValue == $l_inValue;
        return $true;
    }
);

Test('Convert Unicode object to a long integer', sub {
        my $l_strValue = '1234567890';
        my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
        return $false, 'Unicode::Unicode object is empty' if $l_uniObject->empty();
        $starttime = gettimeofday() if $debug;
        my $l_loValue = $l_uniObject->to_long();
        $endtime = gettimeofday() if $debug;
        printf "to_long debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected $l_strValue == $l_loValue but got !=' unless $l_strValue == $l_loValue;
        return $true;
    }
);

SKIP: {
    Test('Convert Unicode object to a long long integer', sub {
            my $l_strValue = '1234567890987654321';
            my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
            return $false, 'Unicode::Unicode object is empty' if $l_uniObject->empty();
            skip "Perl does not support long long type", 1 if ref($l_uniObject->to_longlong()) ne '';
            $starttime = gettimeofday() if $debug;
            my $l_llValue = $l_uniObject->to_longlong();
            $endtime = gettimeofday() if $debug;
            printf "to_longlong debug time = %.9lf\n", $endtime - $starttime if $debug;
            skip 'Perl does not support long long type', 1 if ref($l_llValue) ne '';
            return $false, 'expected $l_strValue == $l_llValue but got !=' unless $l_strValue == $l_llValue;
            return $true;
        }
    );
}

Test('Convert Unicode object to a float', sub {
        my $l_strValue = '1.2345000e+02';
        my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
        return $false, 'Unicode::Unicode object is empty' if $l_uniObject->empty();
        $starttime = gettimeofday() if $debug;
        my $l_flValue = $l_uniObject->to_float();
        $endtime = gettimeofday() if $debug;
        printf "to_float debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected sprintf("%.2f", $l_strValue) eq sprintf("%.2f", $l_flValue) but got ' . sprintf('%.2f', $l_strValue) . ' ne ' . sprintf('%.2f', $l_flValue) unless sprintf('%.2f', $l_strValue) eq sprintf('%.2f', $l_flValue);
        return $true;
    }
);

Test('Convert Unicode object to a double', sub {
        my $l_strValue = '1.234567890000000e+04';
        my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
        return $false, 'Unicode::Unicode object is empty' if $l_uniObject->empty();
        $starttime = gettimeofday() if $debug;
        my $l_doValue = $l_uniObject->to_double();
        $endtime = gettimeofday() if $debug;
        printf "to_double debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected sprintf("%.4lf", $l_strValue) eq sprintf("%.4lf", $l_doValue) but got ' . sprintf('%.4lf', $l_strValue) . ' ne ' . sprintf('%.4lf', $l_doValue) unless sprintf('%.4lf', $l_strValue) eq sprintf('%.4lf', $l_doValue);
        return $true;
    }
);

SKIP: {
    Test('Convert Unicode object to a long double', sub {
            my $l_strValue = '1234567890.987654321';
            my $l_uniObject = Unicode::from_string($l_strValue, 0, 'UTF8');
            return $false, 'Unicode::Unicode object is empty' if $l_uniObject->empty();
            skip "Perl does not support long double type", 1 if ref($l_uniObject->to_longdouble()) ne '';
            $starttime = gettimeofday() if $debug;
            my $l_ldValue = $l_uniObject->to_longdouble();
            $endtime = gettimeofday() if $debug;
            printf "to_longdouble debug time = %.9lf\n", $endtime - $starttime if $debug;
            skip 'Perl does not support long double type', 1 if ref($l_ldValue) ne '';
            return $false, 'expected sprintf("%.9Lf", $l_strValue) eq sprintf("%.9Lf", $l_ldValue) but got ' . sprintf('%.9Lf', $l_strValue) . ' ne ' . sprintf('%.9Lf', $l_ldValue) unless sprintf('%.9Lf', $l_strValue) eq sprintf('%.9Lf', $l_ldValue);
            return $true;
        }
    );
}

Test('Copy another Unicode object codepoints to Unicode Object', sub {
        my $l_strValue = 'João Méroço';
        my $l_uniObject1 = Unicode::from_string($l_strValue, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_uniObject2 = new Unicode::Unicode();
        $starttime = gettimeofday() if $debug;
        $l_uniObject2->copy($l_uniObject1);
        $endtime = gettimeofday() if $debug;
        printf "copy debug time = %.9lf\n", $endtime - $starttime if $debug;
        # compare Unicode object pointer addresses
        return $false, 'expected int($l_uniObject1) != int($l_uniObject2) but got ==' unless int($l_uniObject1) != int($l_uniObject2);
        # compare raw exported UTF-8 byte strings
        return $false, 'expected $l_uniObject1->export_string() eq $l_uniObject2->export_string() but got ne' unless $l_uniObject1->export_string(0, 'UTF8') eq $l_uniObject2->export_string(0, 'UTF8');
        # compare UTF-8 codepoints in Unicode object buffer
        return $false, 'expected $l_uniObject1 eq $l_uniObject2 but got ne' unless $l_uniObject1 eq $l_uniObject2;
        return $true;
    }
);

Test('Append another Unicode object codepoints to Unicode Object', sub {
        my $l_strValue1 = 'João';
        my $l_uniObject1 = Unicode::from_string($l_strValue1, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_strValue2 = 'Méroço';
        my $l_uniObject2 = Unicode::from_string($l_strValue2, 0, 'UTF8');
        return $false, 'Unicode::Unicode object2 is empty' if $l_uniObject2->empty();
        $starttime = gettimeofday() if $debug;
        $l_uniObject1->append($l_uniObject2);
        $endtime = gettimeofday() if $debug;
        printf "append debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_strResult = $l_uniObject1->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected $l_strResult eq "$l_strValue1$l_strValue2" but got ne'
            unless $l_strResult eq "$l_strValue1$l_strValue2";
        return $true;
    }
);

Test('Append multiple copies of a Unicode object codepoints to Unicode Object', sub {
        my $l_inCount = 1000;
        my $l_strValue1 = 'João Méroço';
        my $l_uniObject1 = Unicode::from_string($l_strValue1, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_strValue2 = ' Woot!' x $l_inCount;
        my $l_uniObject2 = Unicode::from_string($l_strValue2, 0, 'UTF8');
        return $false, 'Unicode::Unicode object2 is empty' if $l_uniObject2->empty();
        $starttime = gettimeofday() if $debug;
        $l_uniObject1->append_multiple($l_uniObject2, $l_inCount);
        $endtime = gettimeofday() if $debug;
        printf "append_multiple debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_strResult = $l_uniObject1->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected $l_strResult eq "' . $l_strValue1 . '" + "' . $l_strValue2 . '" x ' . $l_inCount . ' but got ne'
            unless $l_strResult eq $l_strValue1 . $l_strValue2 x $l_inCount;
        return $true;
    }
);

Test('Swap another Unicode object codepoints with Unicode Object codepoints', sub {
        my $l_strValue1 = 'João';
        my $l_uniObject1 = Unicode::from_string($l_strValue1, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_strValue2 = ' Méroço';
        my $l_uniObject2 = Unicode::from_string($l_strValue2, 0, 'UTF8');
        return $false, 'Unicode::Unicode object2 is empty' if $l_uniObject2->empty();
        $starttime = gettimeofday() if $debug;
        $l_uniObject1->swap($l_uniObject2);
        $endtime = gettimeofday() if $debug;
        printf "swap debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_strResult1 = $l_uniObject1->export_string(0, 'UTF8');
        $l_strResult1 = decode('UTF8', $l_strResult1);
        my $l_strResult2 = $l_uniObject2->export_string(0, 'UTF8');
        $l_strResult2 = decode('UTF8', $l_strResult2);
        return $false, 'expected $l_strResult1 eq $l_strValue2 but got ne' unless $l_strResult1 eq $l_strValue2;
        return $false, 'expected $l_strResult2 eq $l_strValue1 but got ne' unless $l_strResult2 eq $l_strValue1;
        return $true;
    }
);

Test('Find offset of codepoints of a Unicode object inside another Unicode object', sub {
        my $l_strValue1 = 'João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço';
        my $l_uniObject1 = Unicode::from_string($l_strValue1, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_strValue2 = 'João Méroço';
        my $l_uniObject2 = Unicode::from_string($l_strValue2, 0, 'UTF8');
        return $false, 'Unicode::Unicode object2 is empty' if $l_uniObject2->empty();
        $starttime = gettimeofday() if $debug;
        my $l_sizOffset = $l_uniObject1->find($l_uniObject2, 4);
        $endtime = gettimeofday() if $debug;
        printf "find debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected $l_sizOffset == 48 but got ' . $l_sizOffset unless $l_sizOffset == 48;
        $l_sizOffset = $l_uniObject1->find($l_uniObject2, -19);
        return $false, 'expected $l_sizOffset == 48 but got ' . $l_sizOffset unless $l_sizOffset == 48;
        return $true;
    }
);

Test('Return extracted codepoints inside Unicode object in new Unicode object', sub {
        my $l_strValue1 = 'João Méroço';
        my $l_uniObject1 = Unicode::from_string($l_strValue1, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_strValue2 = 'Méroço';
        my $l_uniObject2 = Unicode::from_string($l_strValue2, 0, 'UTF8');
        return $false, 'Unicode::Unicode object2 is empty' if $l_uniObject2->empty();
        $starttime = gettimeofday() if $debug;
        my $l_uniObject3 = $l_uniObject1->extract(5, 6);
        $endtime = gettimeofday() if $debug;
        printf "extract debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'Unicode::Unicode object3 is empty' if $l_uniObject3->empty();
        my $l_strResult2 = $l_uniObject2->export_string(0, 'UTF8');
        $l_strResult2 = decode('UTF8', $l_strResult2);
        my $l_strResult3 = $l_uniObject3->export_string(0, 'UTF8');
        $l_strResult3 = decode('UTF8', $l_strResult3);
        return $false, 'expected $l_strResult2 eq $l_strResult3 but got ne' unless $l_strResult2 eq $l_strResult3;
        return $true;
    }
);

Test('Replace existing Unicode object codepoints with other Unicode object codepoints', sub {
        my $l_strValue1 = 'Jack Von Mercer';
        my $l_uniObject1 = Unicode::from_string($l_strValue1, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_strValue2 = 'João';
        my $l_uniObject2 = Unicode::from_string($l_strValue2, 0, 'UTF8');
        return $false, 'Unicode::Unicode object2 is empty' if $l_uniObject2->empty();
        my $l_strValue3 = 'Méroço';
        my $l_uniObject3 = Unicode::from_string($l_strValue3, 0, 'UTF8');
        return $false, 'Unicode::Unicode object3 is empty' if $l_uniObject3->empty();
        $starttime = gettimeofday() if $debug;
        $l_uniObject1->replace($l_uniObject2, 0, 4);  # "Jack Von Mercer" -> "João Von Mercer"
        $endtime = gettimeofday() if $debug;
        printf "replace debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_uniObject2->clear();
        $l_uniObject1->replace($l_uniObject2, 5, 4);  # -> "João Mercer"
        $l_uniObject1->replace($l_uniObject3, 5, 6);  # -> "João Méroço"
        my $l_strResult = 'João Méroço';
        my $l_strObject1 = $l_uniObject1->export_string(0, 'UTF8');
        $l_strObject1 = decode('UTF8', $l_strObject1);
        return $false, "expected \$l_strObject1 eq '$l_strResult' but got '$l_strObject1'" unless $l_strObject1 eq $l_strResult;
        return $true;
    }
);

Test('Compare Unicode object with another Unicode object as strings', sub {
        my $l_uniObject = Unicode::from_string('Joao Meroco', 0, 'ASCII');
        return $false, 'export_string() returned an empty Unicode::Unicode object' if $l_uniObject->empty();
        my $l_uniCompare = Unicode::from_string('João Méroço', 0, 'UTF8');
        return $false, 'export_string() returned an empty Unicode::Unicode object' if $l_uniCompare->empty();
        $starttime = gettimeofday() if $debug;
        my $l_inResult = $l_uniObject cmp $l_uniCompare;
        $endtime = gettimeofday() if $debug;
        printf "'cmp' debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII string to "cmp" compare less than UTF8 string' unless $l_inResult < 0;
        $starttime = gettimeofday() if $debug;
        $l_inResult = $l_uniObject eq $l_uniCompare;
        $endtime = gettimeofday() if $debug;
        printf "'eq' debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII string to "eq" compare not equal to UTF8 string' unless $l_inResult == 0;
        $starttime = gettimeofday() if $debug;
        $l_inResult = $l_uniObject->compare_ascendingstring($l_uniCompare);
        $endtime = gettimeofday() if $debug;
        printf "compare_ascendingstring debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII string to ascending compare less than UTF8 string' unless $l_inResult < 0;
        $starttime = gettimeofday() if $debug;
        $l_inResult = $l_uniObject->compare_descendingstring($l_uniCompare);
        $endtime = gettimeofday() if $debug;
        printf "compare_descendingstring debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII string to descending compare more than UTF8 string' unless $l_inResult > 0;
        return $true;
    }
);

Test('Compare Unicode object with another Unicode object as numbers', sub {
        my $l_uniObject = Unicode::from_string('1234567890.0', 0, 'ASCII');
        return $false, 'from_string() returned an empty Unicode::Unicode object' if $l_uniObject->empty();
        my $l_uniCompare = Unicode::from_string('1234567890.0', 0, 'UTF8');
        return $false, 'from_string() returned an empty Unicode::Unicode object' if $l_uniCompare->empty();
        $starttime = gettimeofday() if $debug;
        my $l_inResult = $l_uniObject <=> $l_uniCompare;
        $endtime = gettimeofday() if $debug;
        printf "'<=>' debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII number to "<=>" compare equal to UTF8 number' unless $l_inResult == 0;
        $starttime = gettimeofday() if $debug;
        $l_inResult = $l_uniObject == $l_uniCompare;
        $endtime = gettimeofday() if $debug;
        printf "'==' debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII number to be equal to UTF8 number' unless $l_inResult != 0;
        $starttime = gettimeofday() if $debug;
        $l_inResult = $l_uniObject->compare_ascendingnumeric($l_uniCompare);
        $endtime = gettimeofday() if $debug;
        printf "compare_ascendingnumeric debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII number to be equal to UTF8 number' unless $l_inResult == 0;
        $starttime = gettimeofday() if $debug;
        $l_inResult = $l_uniObject->compare_descendingnumeric($l_uniCompare);
        $endtime = gettimeofday() if $debug;
        printf "compare_descendingnumeric debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected ASCII number to be equal to UTF8 number' unless $l_inResult == 0;
        return $true;
    }
);

Test('Return uppercased Unicode object content in new Unicode object', sub {
        my $l_inCount = 1000;
        my $l_strObject = 'João Méroço' x $l_inCount;
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        return $false, 'from_string() returned an empty Unicode::Unicode object' if $l_uniObject->empty();
        $starttime = gettimeofday() if $debug;
        my $l_uniUpper = $l_uniObject->uppercase();
        $endtime = gettimeofday() if $debug;
        printf "uppercase debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_strUpper = $l_uniUpper->export_string(0, 'UTF8');
        $l_strUpper = decode('UTF8', $l_strUpper);
        return $false, "expected uppercase('$l_strObject') eq 'JOÃO MÉROÇO' x $l_inCount but got '$l_strUpper'"
            unless $l_strUpper eq 'JOÃO MÉROÇO' x $l_inCount;
        return $true;
    }
);

Test('Return lowercased Unicode object content in new Unicode object', sub {
        my $l_strObject = 'João Méroço';
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        return $false, 'from_string() returned an empty Unicode::Unicode object' if $l_uniObject->empty();
        $starttime = gettimeofday() if $debug;
        my $l_uniLower = $l_uniObject->lowercase();
        $endtime = gettimeofday() if $debug;
        printf "lowercase debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_strLower = $l_uniLower->export_string(0, 'UTF8');
        $l_strLower = decode('UTF8', $l_strLower);
        return $false, "expected lowercase('$l_strObject') eq 'joão méroço' but got '$l_strLower'" unless $l_strLower eq 'joão méroço';
        return $true;
    }
);

Test('Return swapcased Unicode object content in new Unicode object', sub {
        my $l_strObject = 'João Méroço';
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        return $false, 'from_string() returned an empty Unicode::Unicode object' if $l_uniObject->empty();
        $starttime = gettimeofday() if $debug;
        my $l_uniSwap = $l_uniObject->swapcase();
        $endtime = gettimeofday() if $debug;
        printf "swapcase debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_strSwap = $l_uniSwap->export_string(0, 'UTF8');
        $l_strSwap = decode('UTF8', $l_strSwap);
        return $false, "expected swapcase('$l_strObject') eq 'jOÃO mÉROÇO' but got '$l_strSwap'" unless $l_strSwap eq 'jOÃO mÉROÇO';
        return $true;
    }
);

Test('Return concatenated Unicode objects in new Unicode Object', sub {
        my $l_strValue1 = 'João';
        my $l_uniObject1 = Unicode::from_string($l_strValue1, 0, 'UTF8');
        return $false, 'Unicode::Unicode object1 is empty' if $l_uniObject1->empty();
        my $l_strValue2 = 'Méroço';
        my $l_uniObject2 = Unicode::from_string($l_strValue2, 0, 'UTF8');
        return $false, 'Unicode::Unicode object2 is empty' if $l_uniObject2->empty();
        $starttime = gettimeofday() if $debug;
        my $l_uniResult = $l_uniObject1->concatenate($l_uniObject2);
        $endtime = gettimeofday() if $debug;
        printf "concatenate debug time = %.9lf\n", $endtime - $starttime if $debug;
        $starttime = gettimeofday() if $debug;
        $l_uniResult = $l_uniObject1 + $l_uniObject2;
        $endtime = gettimeofday() if $debug;
        printf "'+' debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_strResult = $l_uniResult->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected $l_strResult eq "$l_strValue1$l_strValue2" but got ne'
            unless $l_strResult eq "$l_strValue1$l_strValue2";
        return $true;
    }
);

Test('Split Unicode object into UnicodeArray object', sub {
        my $l_strDelimiter = '*';
        my $l_uniDelimiter = Unicode::from_string($l_strDelimiter, 0, 'UTF8');
        my $l_strObject = 'ft1*ft2**ft4';
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $starttime = gettimeofday() if $debug;
        my $l_unaObject = $l_uniObject->split($l_uniDelimiter, 0);
        $endtime = gettimeofday() if $debug;
        printf "split debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected 4 elements but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 4;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 1);
        return $false, 'expected 3 elements but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 3;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 2);
        return $false, 'expected 2 elements but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 2;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 3);
        return $false, 'expected 1 element but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 1;
        $l_strObject = '*ft1*ft2**ft4*';
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 0);
        return $false, 'expected 6 elements but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 6;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 1);
        return $false, 'expected 3 elements but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 3;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 2);
        return $false, 'expected 4 elements but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 4;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 3);
        return $false, 'expected 3 element but got ' . $l_unaObject->{m_sizObjects} unless $l_unaObject->{m_sizObjects} == 3;
        return $true;
    }
);

Test('Join UnicodeArray object into Unicode object', sub {
        my $l_strDelimiter = '*';
        my $l_uniDelimiter = Unicode::from_string($l_strDelimiter, 0, 'UTF8');
        my $l_strObject = 'ft1*ft2**ft4';
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_unaObject = $l_uniObject->split($l_uniDelimiter, 0);
        my $l_strJoin = 'ft1*ft2**ft4';
        $starttime = gettimeofday() if $debug;
        my $l_uniJoin = $l_unaObject->join($l_uniDelimiter, 0);
        $endtime = gettimeofday() if $debug;
        printf "join debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected "' . $l_strJoin . '" but got "' . $l_uniJoin->export_string(0, 'UTF8') . '"'
            unless $l_strJoin eq $l_uniJoin->export_string(0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 1);
        $l_strJoin = 'ft1*ft2*ft4';
        $l_uniJoin = $l_unaObject->join($l_uniDelimiter, 1);
        return $false, 'expected "' . $l_strJoin . '" but got "' . $l_uniJoin->export_string(0, 'UTF8') . '"'
            unless $l_strJoin eq $l_uniJoin->export_string(0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 2);
        $l_strJoin = '*ft2**';
        $l_uniJoin = $l_unaObject->join($l_uniDelimiter, 2);
        return $false, 'expected "' . $l_strJoin . '" but got "' . $l_uniJoin->export_string(0, 'UTF8') . '"'
            unless $l_strJoin eq $l_uniJoin->export_string(0, 'UTF8');
        $l_unaObject = $l_uniObject->split($l_uniDelimiter, 3);
        $l_strJoin = '*ft2*';
        $l_uniJoin = $l_unaObject->join($l_uniDelimiter, 3);
        return $false, 'expected "' . $l_strJoin . '" but got "' . $l_uniJoin->export_string(0, 'UTF8') . '"'
            unless $l_strJoin eq $l_uniJoin->export_string(0, 'UTF8');
        return $true;
    }
);

Test('Convert Perl array of strings into UnicodeArray object', sub {
        my $l_arrStrings = ["The", "quick", "brown", "fox", "jumps", "over", "the", "cow"];
        $starttime = gettimeofday() if $debug;
        my $l_unaObject = new Unicode::UnicodeArray(1);
        $endtime = gettimeofday() if $debug;
        printf "new Unicode::UnicodeArray debug time = %.9lf\n", $endtime - $starttime if $debug;
        $starttime = gettimeofday() if $debug;
        $l_unaObject->to_array($l_arrStrings);
        $endtime = gettimeofday() if $debug;
        printf "to_array debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected 8 elements but got "' . $l_unaObject->{m_sizObjects} . '"' unless $l_unaObject->{m_sizObjects} == 8;
        for (my $l_inOffset = 0; $l_inOffset < $l_unaObject->{m_sizObjects}; $l_inOffset++) {
            my $l_strObject = $l_unaObject->get_element($l_inOffset)->export_string(0, "ASCII");
            return $false, 'expected element ' . $l_inOffset . ' equal to "' . $l_arrStrings->[$l_inOffset] . '" but got "' . $l_strObject . '"'
                unless $l_arrStrings->[$l_inOffset] eq $l_strObject;
        }
        return $true;
    }
);

Test('Convert UnicodeArray object into a Perl array of strings', sub {
        my $l_uniDelimiters = Unicode::from_string(" ,*", 0, "ASCII");
        my $l_uniObject = Unicode::from_string("The quick, brown fox jumps *over* the cow " x 1000, 0, "ASCII");
        my $l_unaObject = $l_uniObject->split($l_uniDelimiters, 1);  # 1=trim delimiters between tokens
        $starttime = gettimeofday() if $debug;
        my $l_arrWords = $l_unaObject->from_array();
        $endtime = gettimeofday() if $debug;
        printf "from_array debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected "The" but got "' . $l_arrWords->[0] . '"' unless $l_arrWords->[0] eq 'The';
        return $false, 'expected "quick" but got "' . $l_arrWords->[1] . '"' unless $l_arrWords->[1] eq 'quick';
        return $false, 'expected "brown" but got "' . $l_arrWords->[2] . '"' unless $l_arrWords->[2] eq 'brown';
        return $false, 'expected "fox" but got "' . $l_arrWords->[3] . '"' unless $l_arrWords->[3] eq 'fox';
        return $false, 'expected "jumps" but got "' . $l_arrWords->[4] . '"' unless $l_arrWords->[4] eq 'jumps';
        return $false, 'expected "over" but got "' . $l_arrWords->[5] . '"' unless $l_arrWords->[5] eq 'over';
        return $false, 'expected "the" but got "' . $l_arrWords->[6] . '"' unless $l_arrWords->[6] eq 'the';
        return $false, 'expected "cow" but got "' . $l_arrWords->[7] . '"' unless $l_arrWords->[7] eq 'cow';
    }
);

Test('Return Unicode object subvalues in Perl array', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "${fs}ft1${fs}ft2${fs}ft3${fs}ft4${fs}${gs}gt1${gs}gt2${gs}gt3${gs}gt4${gs}${rs}rt1${rs}rt2${rs}rt3${rs}rt4${rs}${us}üt1${us}üt2${us}üt3${us}üt4${us}üt5${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_rearFS = $l_uniObject->from_subvalues(0, 0, 0);
        return $false, 'expected array of five level 1 subvalues' unless $l_rearFS->[0] eq 'ft1' and $l_rearFS->[1] eq 'ft2' and $l_rearFS->[2] eq 'ft3' and $l_rearFS->[3] eq 'ft4' and length($l_rearFS->[4]) > 0;
        my $l_rearGS = $l_uniObject->from_subvalues(5, 0, 0);
        return $false, 'expected array of five level 2 subvalues' unless $l_rearGS->[0] eq 'gt1' and $l_rearGS->[1] eq 'gt2' and $l_rearGS->[2] eq 'gt3' and $l_rearGS->[3] eq 'gt4' and length($l_rearGS->[4]) > 0;
        my $l_rearRS = $l_uniObject->from_subvalues(5, -1, 0);
        return $false, 'expected array of five level 3 subvalues' unless $l_rearRS->[0] eq 'rt1' and $l_rearRS->[1] eq 'rt2' and $l_rearRS->[2] eq 'rt3' and $l_rearRS->[3] eq 'rt4' and length($l_rearRS->[4]) > 0;
        $starttime = gettimeofday() if $debug;
        my $l_rearUS = $l_uniObject->from_subvalues(-1, -1, 5);
        $endtime = gettimeofday() if $debug;
        printf "from_subvalues debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected array of five level 4 subvalues' unless decode('UTF-8', $l_rearUS->[0]) eq 'üt1' and decode('UTF-8', $l_rearUS->[1]) eq 'üt2' and decode('UTF-8', $l_rearUS->[2]) eq 'üt3' and decode('UTF-8', $l_rearUS->[3]) eq 'üt4' and decode('UTF-8', $l_rearUS->[4]) eq 'üt5';
        return $true;
    }
);

Test('Store Perl array as Unicode object subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_uniObject = new Unicode::Unicode;
        my $l_rearLevel1 = [ 'João-ft1', 'João-ft2', 'João-ft3', 'João-ft4' ];
        $l_uniObject->to_subvalues($l_rearLevel1, 0, 0, 0);
        my $l_strObject = $l_uniObject->export_string(0, 'UTF8');
        $l_strObject = decode('UTF8', $l_strObject);
        return $false, 'expected subvalues of four level 1 subvalues' unless $l_strObject eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}";
        my $l_rearLevel2 = [ 'João-gt1', 'João-gt2', 'João-gt3', 'João-gt4' ];
        $l_uniObject->to_subvalues($l_rearLevel2, 5, 0, 0);
        $l_strObject = $l_uniObject->export_string(0, 'UTF8');
        $l_strObject = decode('UTF8', $l_strObject);
        return $false, 'expected subvalues of four level 2 subvalues' unless $l_strObject eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${fs}";
        my $l_rearLevel3 = [ 'João-rt1', 'João-rt2', 'João-rt3', 'João-rt4' ];
        $l_uniObject->to_subvalues($l_rearLevel3, 5, 5, 0);
        $l_strObject = $l_uniObject->export_string(0, 'UTF8');
        $l_strObject = decode('UTF8', $l_strObject);
        return $false, 'expected subvalues of four level 3 subvalues' unless $l_strObject eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${gs}${fs}";
        my $l_rearLevel4 = [ 'João-ut1', 'João-ut2', 'João-ut3', 'João-ut4' ];
        $starttime = gettimeofday() if $debug;
        $l_uniObject->to_subvalues($l_rearLevel4, 5, 5, 5);
        $endtime = gettimeofday() if $debug;
        printf "to_subvalues debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_strObject = $l_uniObject->export_string(0, 'UTF8');
        $l_strObject = decode('UTF8', $l_strObject);
        return $false, 'expected subvalues of four level 4 subvalues' unless $l_strObject eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        return $true;
    }
);

Test('Count Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_inCount = $l_uniObject->count_subvalues(0, 0, 0);
        return $false, "expected subvalue count in (0, 0, 0) of 5 but got $l_inCount" unless $l_inCount == 5;
        $l_inCount = $l_uniObject->count_subvalues(5, 0, 0);
        return $false, "expected subvalue count in (5, 0, 0) of 5 but got $l_inCount" unless $l_inCount == 5;
        $l_inCount = $l_uniObject->count_subvalues(5, 5, 0);
        return $false, "expected subvalue count in (5, 5, 0) of 5 but got $l_inCount" unless $l_inCount == 5;
        $l_inCount = $l_uniObject->count_subvalues(5, 5, 5);
        return $false, "expected subvalue count in (5, 5, 5) of 4 but got $l_inCount" unless $l_inCount == 4;
        $l_inCount = $l_uniObject->count_subvalues(-1, -1, -1);
        return $false, "expected subvalue count in (-1, -1, -1) of 4 but got $l_inCount" unless $l_inCount == 4;
        $l_inCount = $l_uniObject->count_subvalues(999, 0, 0);
        return $false, "expected subvalue count in (999, 0, 0) of 0 but got $l_inCount" unless $l_inCount == 0;
        $l_inCount = $l_uniObject->count_subvalues(1, 999, 0);
        return $false, "expected subvalue count in (1, 999, 0) of 0 but got $l_inCount" unless $l_inCount == 0;
        $starttime = gettimeofday() if $debug;
        $l_inCount = $l_uniObject->count_subvalues(1, 1, 999);
        $endtime = gettimeofday() if $debug;
        printf "count debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, "expected subvalue count in (1, 1, 999) of 0 but got $l_inCount" unless $l_inCount == 0;
        return $true;
    }
);

Test('Extract Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "João Méroço${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_strSubvalue = $l_uniObject->extract_subvalue(0, 0, 0, 0)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected object value in (0, 0, 0, 0) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João Méroço";
        $l_strSubvalue = $l_uniObject->extract_subvalue(2, 0, 0, 0)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 1 subvalue in (2, 0, 0, 0) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-ft2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(-4, 0, 0, 0)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 1 subvalue in (-4, 0, 0, 0) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-ft2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(5, 2, 0, 0)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 2 subvalue in (5, 2, 0, 0) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-gt2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(5, -4, 0, 0)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 2 subvalue in (5, -4, 0, 0) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-gt2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(5, 5, 2, 0)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 3 subvalue in (5, 5, 2, 0) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-rt2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(5, 5, -4, 0)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 3 subvalue in (5, 5, -4, 0) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-rt2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, 2)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 4 subvalue in (5, 5, 5, 2) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-ut2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, -3)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 4 subvalue in (5, 5, 5, -3) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-ut2";
        $l_strSubvalue = $l_uniObject->extract_subvalue(-1, -1, -1, -1)->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 4 subvalue in (-1, -1, -1, -1) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-ut4";
        $starttime = gettimeofday() if $debug;
        $l_strSubvalue = $l_uniObject->extract_subvalue(-1, -1, -1, -999)->export_string(0, 'UTF8');
        $endtime = gettimeofday() if $debug;
        printf "extract_subvalue debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, "expected level 4 subvalue in (-1, -1, -1, -999) but got '$l_strSubvalue'" unless $l_strSubvalue eq "João-ut1";
        return $true;
    }
);

Test('Replace Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "João Méroço${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_strReplace = "Lee Trevino";
        my $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 0, 0, 0, 0);
        my $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (0, 0, 0, 0)' unless $l_strResult eq "Lee Trevino${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-ft7";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 7, 0, 0, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (7, 0, 0, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}${fs}João-ft7${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-ft6";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 6, 0, 0, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (6, 0, 0, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}João-ft6${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-ft5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 5, 0, 0, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (5, 0, 0, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}João-ft5${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-gt5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 5, 5, 0, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (5, 5, 0, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}João-gt5${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-rt5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 5, 5, 5, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (5, 5, 5, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}João-rt5${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-ut5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 5, 5, 5, 5);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (5, 5, 5, 5)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}João-ut5${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-ft3.5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 3, 0, 0, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (3, 0, 0, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}João-ft3.5${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-gt3.5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 3, 3, 0, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (3, 3, 0, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}${gs}${gs}${gs}João-gt3.5${gs}${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-rt3.5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 3, 3, 3, 0);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (3, 3, 3, 0)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}${gs}${gs}${gs}${rs}${rs}${rs}João-rt3.5${rs}${gs}${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-ut3.5";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $l_uniObject->replace_subvalue($l_uniReplace, 3, 3, 3, 3);
        $l_strResult = $l_uniObject->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (3, 3, 3, 3)' unless $l_strResult eq "${fs}João-ft1${fs}João-ft2${fs}${gs}${gs}${gs}${rs}${rs}${rs}${us}${us}${us}João-ut3.5${us}${rs}${gs}${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strReplace = "João-ft999999-gt999999-rt999999-ut999999";
        $l_uniReplace = Unicode::from_string($l_strReplace, 0, 'UTF8');
        $starttime = gettimeofday() if $debug;
        $l_uniObject->replace_subvalue($l_uniReplace, 999999, 999999, 999999, 999999);
        $endtime = gettimeofday() if $debug;
        printf "replace_subvalue debug time = %.9lf\n", $endtime - $starttime if $debug;
        my $l_uniResult = $l_uniObject->extract_subvalue(999999, 999999, 999999, 999999);
        $l_strResult = $l_uniResult->export_string(0, 'UTF8');
        $l_strResult = decode('UTF8', $l_strResult);
        return $false, 'expected replacement of subvalue in (999999, 999999, 999999, 999999)' unless $l_strResult eq $l_strReplace;
        return $true;
    }
);

Test('Insert Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_strInsert = "João Méroço";
        my $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 0, 0, 0, 0);
        my $l_uniSubvalue = $l_uniObject->extract_subvalue(0, 0, 0, 0);
        my $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (0, 0, 0, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft7";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 7, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(7, 0, 0, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (7, 0, 0, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft6";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 6, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(6, 0, 0, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (6, 0, 0, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft5";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 5, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 0, 0, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (5, 0, 0, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-gt5";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 5, 5, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 0, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (5, 5, 0, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-rt5";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 5, 5, 5, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (5, 5, 5, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ut5";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 5, 5, 5, 5);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, 5);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (5, 5, 5, 5)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft2.5";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 3, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 0, 0, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (3, 0, 0, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft3-gt3";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 3, 3, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 0, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (3, 3, 0, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft3-gt3-rt3";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 3, 3, 3, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 3, 0);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (3, 3, 3, 0)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft3-gt3-rt3-ut3";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, 3, 3, 3, 3);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 3, 3);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (3, 3, 3, 3)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft1-gt1-rt1-ut1";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $l_uniObject->insert_subvalue($l_uniInsert, -99, -99, -99, -99);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(-99, -99, -99, -99);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (-99, -99, -99, -99)' unless $l_strSubvalue eq $l_strInsert;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_strInsert = "João-ft99999-gt99999-rt99999-ut99999";
        $l_uniInsert = Unicode::from_string($l_strInsert, 0, 'UTF8');
        $starttime = gettimeofday() if $debug;
        $l_uniObject->insert_subvalue($l_uniInsert, 99999, 99999, 99999, 99999);
        $endtime = gettimeofday() if $debug;
        printf "insert_subvalue debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_uniSubvalue = $l_uniObject->extract_subvalue(99999, 99999, 99999, 99999);
        $l_strSubvalue = $l_uniSubvalue->export_string(0, 'UTF8');
        $l_strSubvalue = decode('UTF8', $l_strSubvalue);
        return $false, 'expected insertion of subvalue in (99999, 99999, 99999, 99999)' unless $l_strSubvalue eq $l_strInsert;
        return $true;
    }
);

Test('Append Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_uniObject = new Unicode::Unicode();
        my $l_uniAppend = Unicode::from_string("João-ft1", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, -1, 0, 0, 0);
        my $l_uniSubvalue = $l_uniObject->extract_subvalue(2, 0, 0, 0);
        return $false, 'expected append of subvalue at (-1, 0, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = new Unicode::Unicode();
        $l_uniAppend = Unicode::from_string("João-gt1", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 1, -1, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(1, 2, 0, 0);
        return $false, 'expected append of subvalue at (1, -1, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = new Unicode::Unicode();
        $l_uniAppend = Unicode::from_string("João-rt1", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 1, 1, -1, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(1, 1, 2, 0);
        return $false, 'expected append of subvalue at (1, 1, -1, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = new Unicode::Unicode();
        $l_uniAppend = Unicode::from_string("João-ut1", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 1, 1, 1, -1);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(1, 1, 1, 2);
        return $false, 'expected append of subvalue at (1, 1, 1, -1) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        my $l_strObject = "${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João Méroço", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 0, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(0, 0, 0, 0);
        return $false, 'expected append of subvalue in (0, 0, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft8", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 7, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(8, 0, 0, 0);
        return $false, 'expected append of subvalue in (7, 0, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft7", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 6, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(7, 0, 0, 0);
        return $false, 'expected append of subvalue in (6, 0, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft6", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 5, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(6, 0, 0, 0);
        return $false, 'expected append of subvalue in (5, 0, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft6", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, -1, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(6, 0, 0, 0);
        return $false, 'expected append of subvalue in (-1, 0, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-gt6", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 5, 5, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 6, 0, 0);
        return $false, 'expected append of subvalue in (5, 5, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-gt6", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 5, -1, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 6, 0, 0);
        return $false, 'expected append of subvalue in (5, -1, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-rt6", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 5, 5, 5, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 6, 0);
        return $false, 'expected append of subvalue in (5, 5, 5, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-rt6", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 5, 5, -1, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 6, 0);
        return $false, 'expected append of subvalue in (5, 5, -1, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ut6", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 5, 5, 5, 5);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, 6);
        return $false, 'expected append of subvalue in (5, 5, 5, 5) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ut5", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 5, 5, 5, -1);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, 5);
        return $false, 'expected append of subvalue in (5, 5, 5, -1) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft3.5", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 3, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(4, 0, 0, 0);
        return $false, 'expected append of subvalue in (3, 0, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft3-gt4", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 3, 3, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 4, 0, 0);
        return $false, 'expected append of subvalue in (3, 3, 0, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft3-gt3-rt4", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 3, 3, 3, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 4, 0);
        return $false, 'expected append of subvalue in (3, 3, 3, 0) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft3-gt3-rt3-ut4", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, 3, 3, 3, 3);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 3, 4);
        return $false, 'expected append of subvalue in (3, 3, 3, 3) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft-99-gt-99-rt-99-ut2", 0, 'UTF8');
        $l_uniObject->append_subvalue($l_uniAppend, -99, -99, -99, -99);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(-99, -99, -99, 2);
        return $false, 'expected append of subvalue in (-99, -99, -99, -99) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniAppend = Unicode::from_string("João-ft999999-gt999999-rt999999-ut1000000", 0, 'UTF8');
        $starttime = gettimeofday() if $debug;
        $l_uniObject->append_subvalue($l_uniAppend, 999999, 999999, 999999, 999999);
        $endtime = gettimeofday() if $debug;
        printf "append_subvalue debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_uniSubvalue = $l_uniObject->extract_subvalue(999999, 999999, 999999, 1000000);
        return $false, 'expected append of subvalue in (999999, 999999, 999999, 999999) but got "' . $l_uniObject->export_string(0, 'UTF8') . '"' unless $l_uniAppend->compare_ascendingstring($l_uniSubvalue) == 0;
        return $true;
    }
);

Test('Delete Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "João Méroço${fs}João-ft1${fs}João-ft2${fs}João-ft3${fs}João-ft4${fs}${gs}João-gt1${gs}João-gt2${gs}João-gt3${gs}João-gt4${gs}${rs}João-rt1${rs}João-rt2${rs}João-rt3${rs}João-rt4${rs}${us}João-ut1${us}João-ut2${us}João-ut3${us}João-ut4${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_uniCompare = Unicode::from_string('', 0, 'UTF8');
        $l_uniObject->delete_subvalue(0, 0, 0, 0);
        my $l_uniSubvalue = $l_uniObject->extract_subvalue(0, 0, 0, 0);
        return $false, 'expected delete of value in (0, 0, 0, 0)' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniCompare = Unicode::from_string('João-ft1', 0, 'UTF8');
        $l_uniSubvalue = $l_uniObject->extract_subvalue(1, 0, 0, 0);
        return $false, 'expected no delete of subvalue in (1, 0, 0, 0)' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(8, 0, 0, 0);
        $l_uniObject->delete_subvalue(7, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(7, 0, 0, 0);
        return $false, 'expected delete of subvalue in (7, 0, 0, 0)' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(7, 0, 0, 0);
        $l_uniObject->delete_subvalue(6, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(6, 0, 0, 0);
        return $false, 'expected delete of subvalue in (6, 0, 0, 0);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(6, 0, 0, 0);
        $l_uniObject->delete_subvalue(5, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 0, 0, 0);
        return $false, 'expected delete of subvalue in (5, 0, 0, 0);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(5, 6, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 0, 0);
        $l_uniObject->delete_subvalue(5, 5, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 0, 0);
        return $false, 'expected delete of subvalue in (5, 5, 0, 0);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(5, 5, 6, 0);
        $l_uniObject->delete_subvalue(5, 5, 5, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, 0);
        return $false, 'expected delete of subvalue in (5, 5, 5, 0);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(5, 5, 5, 6);
        $l_uniObject->delete_subvalue(5, 5, 5, 5);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 5, 5, 5);
        return $false, 'expected delete of subvalue in (5, 5, 5, 5);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(5, 1, 1, 2);
        $l_uniObject->delete_subvalue(5, 1, 1, 1);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 1, 1, 1);
        return $false, 'expected delete of subvalue in (5, 1, 1, 1);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(5, 2, 2, 3);
        $l_uniObject->delete_subvalue(5, 2, 2, 2);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 2, 2, 2);
        return $false, 'expected delete of subvalue in (5, 2, 2, 2);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(5, 3, 3, 4);
        $l_uniObject->delete_subvalue(5, 3, 3, 3);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 3, 3, 3);
        return $false, 'expected delete of subvalue in (5, 3, 3, 3);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(5, 4, 4, 5);
        $l_uniObject->delete_subvalue(5, 4, 4, 4);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(5, 4, 4, 4);
        return $false, 'expected delete of subvalue in (5, 4, 4, 4);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(4, 0, 0, 0);
        $l_uniObject->delete_subvalue(3, 0, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 0, 0, 0);
        return $false, 'expected delete of subvalue in (3, 0, 0, 0);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(3, 4, 0, 0);
        $l_uniObject->delete_subvalue(3, 3, 0, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 0, 0);
        return $false, 'expected delete of subvalue in (3, 3, 0, 0);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(3, 3, 4, 0);
        $l_uniObject->delete_subvalue(3, 3, 3, 0);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 3, 0);
        return $false, 'expected delete of subvalue in (3, 3, 3, 0);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(3, 3, 3, 4);
        $l_uniObject->delete_subvalue(3, 3, 3, 3);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(3, 3, 3, 3);
        return $false, 'expected delete of subvalue in (3, 3, 3, 3);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(-99, -99, -99, 2);
        $l_uniObject->delete_subvalue(-99, -99, -99, 1);
        $l_uniSubvalue = $l_uniObject->extract_subvalue(-99, -99, -99, 1);
        return $false, 'expected delete of subvalue in (-99, -99, -99, 1);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniCompare = $l_uniObject->extract_subvalue(99999, 99999, 99999, 10000);
        $starttime = gettimeofday() if $debug;
        $l_uniObject->delete_subvalue(99999, 99999, 99999, 99999);
        $endtime = gettimeofday() if $debug;
        printf "delete_subvalue debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_uniSubvalue = $l_uniObject->extract_subvalue(99999, 99999, 99999, 99999);
        return $false, 'expected delete of subvalue in (99999, 99999, 99999, 99999);' unless $l_uniSubvalue->compare_ascendingstring($l_uniCompare) == 0;
        return $true;
    }
);

Test('Sort Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(0, 0, 0, 0);
        my $l_strSorted = $l_uniObject->export_string(0, 'ASCII');
        return $false, 'expected sorted ascending string subvalues in (0, 0, 0)' unless $l_strSorted eq "${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(0, 0, 0, 1);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending string subvalues in (0, 0, 0)' unless $l_strSorted eq "${fs}4-ft4${fs}30-ft3${fs}200-ft2${fs}1000-ft1${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(0, 0, 0, 2);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted ascending numeric subvalues in (0, 0, 0)' unless $l_strSorted eq "${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}4-ft4${fs}30-ft3${fs}200-ft2${fs}1000-ft1${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(0, 0, 0, 3);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending numeric subvalues in (0, 0, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 0, 0, 0);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted ascending string subvalues in (5, 0, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 0, 0, 1);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending string subvalues in (5, 0, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}4-gt4${gs}30-gt3${gs}200-gt2${gs}1000-gt1${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 0, 0, 2);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted ascending numeric subvalues in (5, 0, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}4-gt4${gs}30-gt3${gs}200-gt2${gs}1000-gt1${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 0, 0, 3);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending numeric subvalues in (5, 0, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 5, 0, 0);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted ascending string subvalues in (5, 5, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 5, 0, 1);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending string subvalues in (5, 5, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}4-rt4${rs}30-rt3${rs}200-rt2${rs}1000-rt1${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 5, 0, 2);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted ascending numeric subvalues in (5, 5, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}4-rt4${rs}30-rt3${rs}200-rt2${rs}1000-rt1${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 5, 0, 3);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending numeric subvalues in (5, 5, 0)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 5, 5, 0);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted ascending string subvalues in (5, 5, 5)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 5, 5, 1);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending string subvalues in (5, 5, 5)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}4-ut4${us}30-ut3${us}200-ut2${us}1000-ut1${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $l_uniObject->sort_subvalues(5, 5, 5, 2);
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted ascending numeric subvalues in (5, 5, 5)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}4-ut4${us}30-ut3${us}200-ut2${us}1000-ut1${us}${rs}${gs}${fs}";
        $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $starttime = gettimeofday() if $debug;
        $l_uniObject->sort_subvalues(5, 5, 5, 3);
        $endtime = gettimeofday() if $debug;
        printf "sort_subvalues debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_strSorted = $l_uniObject->export_string(0, 'UTF8');
        return $false, 'expected sorted descending numeric subvalues in (5, 5, 5)' unless $l_strSorted eq "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}${us}1000-ut1${us}200-ut2${us}30-ut3${us}4-ut4${us}${rs}${gs}${fs}";
        return $true;
    }
);

Test('Locate Unicode object 4-dimensional dynamic array subvalues', sub {
        my $fs = "\x1c";  # ASCII FS control separator character (level 1 subvalue delimiter)
        my $gs = "\x1d";  # ASCII GS control separator character (level 2 subvalue delimiter)
        my $rs = "\x1e";  # ASCII RS control separator character (level 3 subvalue delimiter)
        my $us = "\x1f";  # ASCII US control separator character (level 4 subvalue delimiter)
        my $l_strObject = "${fs}1000-ft1${fs}200-ft2${fs}30-ft3${fs}4-ft4${fs}";
        $l_strObject .= "${gs}1000-gt1${gs}200-gt2${gs}30-gt3${gs}4-gt4${gs}";
        $l_strObject .= "${rs}1000-rt1${rs}200-rt2${rs}30-rt3${rs}4-rt4${rs}";
        $l_strObject .= "${us}${_}-ut${_}" for 1 ... 1000;
        $l_strObject .= "${us}${rs}${gs}${fs}";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_uniKey = Unicode::from_string('1000-ft1', 0, 'UTF8');
        my $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 0, 0, 0, 0);
        return $false, 'expected to locate key in unsorted subvalues in (0, 0, 0)' unless $l_inIndex == 1;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 0, 0, 0, 1);
        return $false, 'expected to locate key in ascending string subvalues in (0, 0, 0)' unless $l_inIndex == 1;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 0, 0, 0, 2);
        return $false, 'expected to locate key in descending string subvalues in (0, 0, 0)' unless $l_inIndex == 0;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 0, 0, 0, 3);
        return $false, 'expected to locate key in ascending numeric subvalues in (0, 0, 0)' unless $l_inIndex == 0;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 0, 0, 0, 4);
        return $false, 'expected to locate key in descending numeric subvalues in (0, 0, 0)' unless $l_inIndex == 1;
        $l_uniKey = Unicode::from_string('1000-gt1', 0, 'UTF8');
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 0, 0, 0);
        return $false, 'expected to locate key in unsorted subvalues in (5, 0, 0)' unless $l_inIndex == 1;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 0, 0, 1);
        return $false, 'expected to locate key in ascending string subvalues in (5, 0, 0)' unless $l_inIndex == 1;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 0, 0, 2);
        return $false, 'expected to locate key in descending string subvalues in (5, 0, 0)' unless $l_inIndex == 0;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 0, 0, 3);
        return $false, 'expected to locate key in ascending numeric subvalues in (5, 0, 0)' unless $l_inIndex == 0;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 0, 0, 4);
        return $false, 'expected to locate key in descending numeric subvalues in (5, 0, 0)' unless $l_inIndex == 1;
        $l_uniKey = Unicode::from_string('1000-rt1', 0, 'UTF8');
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 0, 0);
        return $false, 'expected to locate key in unsorted subvalues in (5, 5, 0)' unless $l_inIndex == 1;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 0, 1);
        return $false, 'expected to locate key in ascending string subvalues in (5, 5, 0)' unless $l_inIndex == 1;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 0, 2);
        return $false, 'expected to locate key in descending string subvalues in (5, 5, 0)' unless $l_inIndex == 0;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 0, 3);
        return $false, 'expected to locate key in ascending numeric subvalues in (5, 5, 0)' unless $l_inIndex == 0;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 0, 4);
        return $false, 'expected to locate key in descending numeric subvalues in (5, 5, 0)' unless $l_inIndex == 1;
        my $l_strKey = '500-ut500';
        $starttime = gettimeofday() if $debug;
        $l_uniObject->sort_subvalues(5, 5, 5, 0);
        $endtime = gettimeofday() if $debug;
        printf "sort_subvalues(ascending string) debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_uniKey = Unicode::from_string($l_strKey, 0, 'UTF8');
        $l_inIndex = 0;
        $starttime = gettimeofday() if $debug;
        my $l_rearSubvalues = $l_uniObject->from_subvalues(5, 5, 5);
        for (my $l_inOffset = 0; $l_inOffset <= $#$l_rearSubvalues; $l_inOffset++) {
            if ($l_rearSubvalues->[$l_inOffset] eq $l_strKey) {
                $l_inIndex = $l_inOffset + 1;
                last;
            }
        }
        $endtime = gettimeofday() if $debug;
        printf "for-loop(left-to-right scan) debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected to locate key in for loop in extracted (5, 5, 5)' unless $l_inIndex == 448;
        $starttime = gettimeofday() if $debug;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 5, 0);
        $endtime = gettimeofday() if $debug;
        printf "locate_subvalue(left-to-right scan) debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected to locate key in unsorted subvalues in (5, 5, 5)' unless $l_inIndex == 448;
        $starttime = gettimeofday() if $debug;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 5, 1);
        $endtime = gettimeofday() if $debug;
        printf "locate_subvalue(ascending string search) debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected to locate key in ascending string subvalues in (5, 5, 5)' unless $l_inIndex == 448;
        $starttime = gettimeofday() if $debug;
        $l_uniObject->sort_subvalues(5, 5, 5, 1);
        $endtime = gettimeofday() if $debug;
        printf "sort_subvalues(descending string) debug time = %.9lf\n", $endtime - $starttime if $debug;
        $starttime = gettimeofday() if $debug;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 5, 2);
        $endtime = gettimeofday() if $debug;
        printf "locate_subvalue(descending string search) debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected to locate key in descending string subvalues in (5, 5, 5)' unless $l_inIndex == 553;
        $starttime = gettimeofday() if $debug;
        $l_uniObject->sort_subvalues(5, 5, 5, 2);
        $endtime = gettimeofday() if $debug;
        printf "sort_subvalues(ascending numeric) debug time = %.9lf\n", $endtime - $starttime if $debug;
        $starttime = gettimeofday() if $debug;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 5, 3);
        $endtime = gettimeofday() if $debug;
        printf "locate_subvalue(ascending numeric search) debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected to locate key in ascending numeric subvalues in (5, 5, 5)' unless $l_inIndex == 500;
        $starttime = gettimeofday() if $debug;
        $l_uniObject->sort_subvalues(5, 5, 5, 3);
        $endtime = gettimeofday() if $debug;
        printf "sort_subvalues(descending numeric) debug time = %.9lf\n", $endtime - $starttime if $debug;
        $starttime = gettimeofday() if $debug;
        $l_inIndex = $l_uniObject->locate_subvalue($l_uniKey, 5, 5, 5, 4);
        $endtime = gettimeofday() if $debug;
        printf "locate_subvalue(descending numeric search) debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected to locate key in descending numeric subvalues in (5, 5, 5)' unless $l_inIndex == 501;
        return $true;
    }
);

Test('Store one-year calendar data in 4-dimensional dynamic array', sub {
        $starttime = gettimeofday() if $debug;
        my $l_uniObject = new Unicode::Unicode;
        my $date = Time::Piece->strptime('2017-01-01', '%Y-%m-%d');
        for (my $i = 0; $i < 365; $i++) {
            my $l_arrInfo = [ $date->ymd, $date->yday + 1, $date->wday, $date->fullmonth, $date->fullday ];
            $l_uniObject->to_subvalues($l_arrInfo, $date->year, $date->mon, $date->mday);
            $date += ONE_DAY;
        }
        my $l_arrLastday = $l_uniObject->from_subvalues(2017, 12, 31);
        return $false, 'expected calendar info on 2017-12-31'
            unless $l_arrLastday->[0] eq '2017-12-31'
                && $l_arrLastday->[1] == 365
                && $l_arrLastday->[2] == 1
                && $l_arrLastday->[3] eq 'December'
                && $l_arrLastday->[4] eq 'Sunday';
        $endtime = gettimeofday() if $debug;
        printf "one-year calendar debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $true;
    }
);

Test('Get Unicode object codepoints', sub {
        my $l_strObject = "João Méroço;";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        for (my $l_inOffset = 0; $l_inOffset < $l_uniObject->codepoints; $l_inOffset++) {
            $starttime = gettimeofday() if $debug && $l_inOffset == 0;
            my $l_loCodepoint = $l_uniObject->get_codepoint($l_inOffset);
            $endtime = gettimeofday() if $debug && $l_inOffset == 0;
            printf "get_codepoint debug time = %.9lf\n", $endtime - $starttime if $debug && $l_inOffset == 0;
            return $false, "expected to get codepoint at offset $l_inOffset" unless $l_loCodepoint == ord(substr($l_strObject, $l_inOffset, 1));
        }
        return $true;
    }
);

Test('Set Unicode object codepoints', sub {
        my $l_strObject = "João Méroço";
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        $starttime = gettimeofday() if $debug;
        $l_uniObject->set_codepoint(2, ord('a'));
        $endtime = gettimeofday() if $debug;
        printf "set_codepoint debug time = %.9lf\n", $endtime - $starttime if $debug;
        $l_uniObject->set_codepoint(6, ord('e'));
        $l_uniObject->set_codepoint(9, ord('c'));
        $l_strObject = $l_uniObject->export_string(0, 'UTF8');
        my $l_strResult = "Joao Meroco";
        return $false, "expected to set ASCII codepoints" unless $l_strObject eq $l_strResult;
        return $true;
    }
);

Test('Find a specific codepoint inside Unicode object', sub {
        my $l_strObject = 'João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço';
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_loAtilde = ord('ã');
        my $l_loEacute = ord('é');
        my $l_loCumlat = ord('ç');
        for (my $l_inCount = 0; $l_inCount < 23; $l_inCount++) {
            $starttime = gettimeofday() if $debug && $l_inCount == 0;
            my $l_inOffset = $l_uniObject->find_codepoint($l_loAtilde, $l_inCount);
            $endtime = gettimeofday() if $debug && $l_inCount == 0;
            printf "find_codepoint debug time = %.9lf\n", $endtime - $starttime if $debug && $l_inCount == 0;
            return $false, "expected to find offset of 'ã' #$l_inCount left-to-right" unless ($l_inOffset % 12) == 2;
            $l_inOffset = $l_uniObject->find_codepoint($l_loAtilde, -1 * ($l_inCount + 1));
            return $false, "expected to find offset of 'ã' #$l_inCount right-to-left" unless ($l_inOffset % 12) == 2;
            $l_inOffset = $l_uniObject->find_codepoint($l_loEacute, $l_inCount);
            return $false, "expected to find offset of 'é' #$l_inCount left-to-right" unless ($l_inOffset % 12) == 6;
            $l_inOffset = $l_uniObject->find_codepoint($l_loEacute, -1 * ($l_inCount + 1));
            return $false, "expected to find offset of 'é' #$l_inCount right-to-left" unless ($l_inOffset % 12) == 6;
            $l_inOffset = $l_uniObject->find_codepoint($l_loCumlat, $l_inCount);
            return $false, "expected to find offset of 'ç' #$l_inCount left-to-right" unless ($l_inOffset % 12) == 9;
            $l_inOffset = $l_uniObject->find_codepoint($l_loCumlat, -1 * ($l_inCount + 1));
            return $false, "expected to find offset of 'ç' #$l_inCount right-to-left" unless ($l_inOffset % 12) == 9;
        }
        return $true;
    }
);

Test('Get POSIX class of a single codepoint inside Unicode object', sub {
        my @l_arinClasses = (0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x340, 0x140, 0x140, 0x140, 0x140, 0x40, 0x40,
            0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40,
            0x700, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80,
            0x4b1, 0x4b1, 0x4b1, 0x4b1, 0x4b1, 0x4b1, 0x4b1, 0x4b1, 0x4b1, 0x4b1, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80,
            0xc80, 0x4ab, 0x4ab, 0x4ab, 0x4ab, 0x4ab, 0x4ab, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b,
            0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0x48b, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80,
            0xc80, 0x4a7, 0x4a7, 0x4a7, 0x4a7, 0x4a7, 0x4a7, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487,
            0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0x487, 0xc80, 0xc80, 0xc80, 0xc80, 0x40);
        my $l_strObject = join '', map { chr($_); } (0 .. 127);
        my $l_uniObject = Unicode::from_string($l_strObject, 128, 'ASCII');
        for (0 .. 127) {
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:alnum:] class"
                unless $l_uniObject->isalnum_codepoint($_) == ($l_arinClasses[$_] & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:alpha:] class"
                unless $l_uniObject->isalpha_codepoint($_) == ($l_arinClasses[$_] >> 1 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:lower:] class"
                unless $l_uniObject->islower_codepoint($_) == ($l_arinClasses[$_] >> 2 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:upper:] class"
                unless $l_uniObject->isupper_codepoint($_) == ($l_arinClasses[$_] >> 3 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:digit:] class"
                unless $l_uniObject->isdigit_codepoint($_) == ($l_arinClasses[$_] >> 4 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:xdigit:] class"
                unless $l_uniObject->isxdigit_codepoint($_) == ($l_arinClasses[$_] >> 5 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:cntrl:] class"
                unless $l_uniObject->iscntrl_codepoint($_) == ($l_arinClasses[$_] >> 6 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:graph:] class"
                unless $l_uniObject->isgraph_codepoint($_) == ($l_arinClasses[$_] >> 7 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:space:] class"
                unless $l_uniObject->isspace_codepoint($_) == ($l_arinClasses[$_] >> 8 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:blank:] class"
                unless $l_uniObject->isblank_codepoint($_) == ($l_arinClasses[$_] >> 9 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:print:] class"
                unless $l_uniObject->isprint_codepoint($_) == ($l_arinClasses[$_] >> 10 & 1);
            return $false, "ASCII character code $_ ('" . chr($_) . "') does not match POSIX [:punct:] class"
                unless $l_uniObject->ispunct_codepoint($_) == ($l_arinClasses[$_] >> 11 & 1);
        }
        return $true;
    }
);

Test('Get lowercase values of codepoints inside Unicode object', sub {
        my $l_strObject = 'João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço';
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_loJlower = ord('j');
        my $l_loMlower = ord('m');
        for (my $l_inCount = 0; $l_inCount < 23; $l_inCount++) {
            my $l_inOffset = $l_inCount * 12;
            return $false, "expected lowercase 'j' at offset $l_inOffset"
                unless $l_uniObject->tolower_codepoint($l_inOffset) == $l_loJlower;
            $l_inOffset = $l_inCount * 12 + 5;
            return $false, "expected lowercase 'm' at offset $l_inOffset"
                unless $l_uniObject->tolower_codepoint($l_inOffset) == $l_loMlower;
        }
        return $true;
    }
);

Test('Get uppercase values of codepoints inside Unicode object', sub {
        my $l_strObject = 'João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço';
        my $l_uniObject = Unicode::from_string($l_strObject, 0, 'UTF8');
        my $l_loAtilde = ord('Ã');
        my $l_loEacute = ord('É');
        my $l_loCumlat = ord('Ç');
        for (my $l_inCount = 0; $l_inCount < 23; $l_inCount++) {
            my $l_inOffset = $l_inCount * 12 + 2;
            return $false, "expected uppercase 'ã' at offset $l_inOffset"
                unless $l_uniObject->toupper_codepoint($l_inOffset) == $l_loAtilde;
            $l_inOffset = $l_inCount * 12 + 6;
            return $false, "expected uppercase 'é' at offset $l_inOffset"
                unless $l_uniObject->toupper_codepoint($l_inOffset) == $l_loEacute;
            $l_inOffset = $l_inCount * 12 + 9;
            return $false, "expected uppercase 'ç' at offset $l_inOffset"
                unless $l_uniObject->toupper_codepoint($l_inOffset) == $l_loCumlat;
        }
        return $true;
    }
);

Test('Save and load Unicode object content in specified encoding with specified file', sub {
        my $l_uniFile = Unicode::from_string('WideString.txt', 0, 'UTF8');
        my $l_uniSave = Unicode::from_string('João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço João Méroço', 0, 'UTF8');
        $starttime = gettimeofday() if $debug;
        my $l_inByteswritten = $l_uniSave->save($l_uniFile, 'UTF16LE');
        $endtime = gettimeofday() if $debug;
        printf "save debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected all bytes written to file "WideString.txt"' unless $l_inByteswritten > 0;
        my $l_uniLoad = new Unicode::Unicode();
        $starttime = gettimeofday() if $debug;
        my $l_inBytesread = $l_uniLoad->load($l_uniFile, 'UTF16LE');
        $endtime = gettimeofday() if $debug;
        printf "load debug time = %.9lf\n", $endtime - $starttime if $debug;
        return $false, 'expected all bytes read from file "WideString.txt"' unless $l_inBytesread == $l_inByteswritten;
        return $false, 'expected same content as written to file "WideString.txt"' unless $l_uniLoad->compare_ascendingstring($l_uniSave) == 0;
        unlink 'WideString.txt';
        return $true;
    }
);

Test('Test four-dimensional UnicodeTesseract object', sub {
        my ($l_inCodepoints, $l_inDim1, $l_inDim2, $l_inDim3, $l_inDim4) = (10, 1, 12, 31, 24);
        my ($l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4) = (0, 0, 0, 0);
        my ($l_untObject, $l_uniObject) = (undef, undef);
        $starttime = gettimeofday() if $debug;
        $l_untObject = new Unicode::UnicodeTesseract($l_inCodepoints, $l_inDim1, $l_inDim2, $l_inDim3, $l_inDim4);
        $endtime = gettimeofday() if $debug;
        printf "new Unicode::UnicodeTesseract debug time = %.9lf\n", $endtime - $starttime if $debug;
        say "new Unicode::UnicodeTesseract($l_inCodepoints, $l_inDim1, $l_inDim2, $l_inDim3, $l_inDim4) = $l_untObject->{m_sizBytes} bytes" if $debug;
        my $first = 1;
        for ($l_inLevel1 = 1; $l_inLevel1 <= $l_inDim1; $l_inLevel1++) {
            for ($l_inLevel2 = 1; $l_inLevel2 <= $l_inDim2; $l_inLevel2++) {
                for ($l_inLevel3 = 1; $l_inLevel3 <= $l_inDim3; $l_inLevel3++) {
                    for ($l_inLevel4 = 1; $l_inLevel4 <= $l_inDim4; $l_inLevel4++) {
                        my $l_uniElement = Unicode::from_string("$l_inLevel1$l_inLevel2$l_inLevel3$l_inLevel4", 0, 'UTF8');
                        if ($first) {
                            $starttime = gettimeofday() if $debug;
                        }
                        $l_untObject->set_element($l_uniElement, $l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
                        if ($first) {
                            $endtime = gettimeofday() if $debug;
                            printf "set_element debug time = %.9lf\n", $endtime - $starttime if $debug;
                            $first = 0;
                        }
                    }
                }
            }
        }
        $starttime = gettimeofday() if $debug;
        $l_uniObject = $l_untObject->from_tesseract();
        $endtime = gettimeofday() if $debug;
        printf "from_tesseract debug time = %.9lf\n", $endtime - $starttime if $debug;
        undef $l_untObject;
        $starttime = gettimeofday() if $debug;
        $l_untObject = $l_uniObject->to_tesseract($l_inCodepoints, $l_inDim1, $l_inDim2, $l_inDim3, $l_inDim4);
        $endtime = gettimeofday() if $debug;
        printf "to_tesseract debug time = %.9lf\n", $endtime - $starttime if $debug;
        undef $l_uniObject;
        $first = 1;
        for ($l_inLevel1 = 1; $l_inLevel1 <= $l_inDim1; $l_inLevel1++) {
            for ($l_inLevel2 = 1; $l_inLevel2 <= $l_inDim2; $l_inLevel2++) {
                for ($l_inLevel3 = 1; $l_inLevel3 <= $l_inDim3; $l_inLevel3++) {
                    for ($l_inLevel4 = 1; $l_inLevel4 <= $l_inDim4; $l_inLevel4++) {
                        if ($first) {
                            $starttime = gettimeofday() if $debug;
                        }
                        my $l_uniElement = $l_untObject->get_element($l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
                        if ($first) {
                            $endtime = gettimeofday() if $debug;
                            printf "get_element debug time = %.9lf\n", $endtime - $starttime if $debug;
                            $first = 0;
                        }
                        return $false, 'undef returned instead of new Unicode::Unicode object' unless defined($l_uniElement);
                        return $false, 'unexpected "' . ref($l_uniElement) . '" for ref(object)' unless ref($l_uniElement) eq 'Unicode::Unicode';
                        my $l_strElement = $l_uniElement->export_string(0, 'UTF8');
                        my $l_strExpected = "$l_inLevel1$l_inLevel2$l_inLevel3$l_inLevel4";
                        return $false, "expected tesseract[$l_inLevel1][$l_inLevel2][$l_inLevel3][$l_inLevel4] to equal '$l_strExpected' but got '$l_strElement'" unless $l_strElement eq $l_strExpected;
                    }
                }
            }
        }
        return $true;
    }
);

goto DONE;
DONE:

printf "Time: %.6lf seconds\n", gettimeofday() - $begintime;

done_testing();
1;
__END__

=head1 SYNOPSIS

=head2 RUN TESTS

    $ perl testunicode.pl

=head2 GENERATE DOCUMENTATION

    $ perldoc testunicode.pl

=head2 PERL5 METHODS

This documentation only provides a quick reference of all the B<< Unicode >> library class and object methods available. For code examples, refer to the source code of this test script.

=head3 VARIABLE NAMING CONVENTIONS

Originally adopted from C/C++ programming, this program documentation uses the following variable naming convention. It is a custom form of Hungarian Notation used to make the code more readable. Here is what it means.

=head4 FORMAT

    $scope_typePurpose

For example, you can read the name "$l_inError" as indicating a local scope variable that contains an integer indicating an error code.

=head4 SCOPE

Use only one of the following lowercase scopes followed by an underscore.

=over 4

=item * m = member scope -- Example: our $m_inError = 0;

=item * l = local scope -- Example: my $l_inError = 0;

=back

=head4 TYPE

=over 4

=item * bo = boolean

=item * in = integer

=item * lo = long int

=item * ll = long long

=item * fl = float

=item * do = double

=item * ld = long double

=item * str = null-terminated char string
 
=item * uni = Unicode object
 
=item * una = UnicodeArray object
 
=item * unt = UnicodeTesseract object
 
=back

=head3 CLASS CONSTRUCTORS

    my $l_uniObject = new Unicode::Unicode();
    my $l_unaObject = new Unicode::UnicodeArray($l_inElements);
    my $l_untObject = new Unicode::UnicodeTesseract($l_inCodepoints, $l_inDim1, $l_inDim2, $l_inDim3, $l_inDim4);

=head3 CLASS DESTRUCTORS

Normally unnecessary since Perl automatically destructs objects when they go out of scope. But you can manually delete B<< Unicode >> library objects using the following Perl commands.

    undef $l_uniObject;
    undef $l_unaObject;
    undef $l_untObject;

=head3 CLASS METHODS

These are class methods used to convert from a Perl5 built-in type to a B<< Unicode >> object directly. Consider these as static class methods, not object methods, and therefore they require the B<< Unicode:: >> package class prefix so that Perl5 can find them.

    my $l_uniString = Unicode::from_string($l_strString, $l_inMaxbytes, $l_strEncoding);
    my $l_uniInt = Unicode::from_int($l_inValue);
    my $l_uniLong = Unicode::from_long($l_loValue);
    my $l_uniLonglong = Unicode::from_longlong($l_llValue);
    my $l_uniFloat = Unicode::from_float($l_flValue);
    my $l_uniDouble = Unicode::from_double($l_doValue);
    my $l_uniLongdouble = Unicode::from_longdouble($l_ldValue);

=head3 OBJECT METHODS

    $l_uniObject->clear();
    my $l_boEmpty = $l_uniObject->empty();
    my $l_inCodepoints = $l_uniObject->codepoints();
    my $l_inBytes = $l_uniObject->bytes();
    my $l_inInbytes = $l_uniObject->import_string($l_strCodepoints, $l_inMaxbytes, $l_strEncoding);
    $l_strCodepoints = $l_uniObject->export_string($l_inMaxbytes, $l_strEncoding);
    my $l_inValue = $l_uniObject->to_int();
    my $l_loValue = $l_uniObject->to_long();
    my $l_llValue = $l_uniObject->to_longlong();
    my $l_flValue = $l_uniObject->to_float();
    my $l_doValue = $l_uniObject->to_double();
    my $l_ldValue = $l_uniObject->to_longdouble();
    $l_uniObject2->copy($l_uniObject1);
    $l_uniObject1->append($l_uniObject2);
    $l_uniObject1->append_multiple($l_uniObject2, $l_inCopies);
    $l_uniObject1->swap($l_uniObject2);
    my $l_inOffset = $l_uniObject1->find($l_uniObject2, $l_inCount);
    my $l_uniObject2 = $l_uniObject1->extract($l_inOffset, $l_inLength);
    $l_uniObject1->replace($l_uniObject2, $l_inOffset, $l_inLength);
    my $l_boEqual = $l_uniObject1->compare_ascendingstring($l_uniObject2) == 0;
    my $l_boNotequal = $l_uniObject1->compare_descendingstring($l_uniObject2) != 0;
    my $l_boLess = $l_uniObject1->compare_ascendingnumeric($l_uniObject2) < 0;
    my $l_boLess = $l_uniObject1->compare_descendingnumeric($l_uniObject2) > 0;
    my $l_uniUppercase = $l_uniObject->uppercase();
    my $l_uniLowercase = $l_uniObject->lowercase();
    my $l_uniSwapcase = $l_uniObject->swapcase();
    my $l_uniObject3 = $l_uniObject1->concatenate($l_uniObject2);
    my $l_unaObject = $l_uniObject->split($l_uniDelimiters, $l_inTrim);
    my $l_uniObject = $l_unaObject->join($l_uniDelimiter, $l_inTrim);
    my $l_strArrayref = $l_uniObject->from_subvalues($l_inLevel1, $l_inLevel2, $l_inLevel3);
    $l_uniObject->to_subvalues($l_strArrayref, $l_inLevel1, $l_inLevel2, $l_inLevel3);
    my $l_inCount = $l_uniObject->count_subvalues($l_inLevel1, $l_inLevel2, $l_inLevel3);
    $l_uniObject->sort_subvalues($l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inSort);
    $l_inIndex = $l_uniObject->locate_subvalue($l_uniSubvalue, $l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
    $l_uniSubvalue = $l_uniObject->extract_subvalue($l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
    $l_uniObject->replace_subvalue($l_uniSubvalue, $l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
    $l_uniObject->insert_subvalue($l_uniSubvalue, $l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
    $l_uniObject->append_subvalue($l_uniSubvalue, $l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
    $l_uniObject->delete_subvalue($l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
    my $l_loCodepoint = $l_uniObject->get_codepoint($l_inOffset);
    $l_uniObject->set_codepoint($l_inOffset, ord($l_strCodepoint));
    my $l_inOffset = $l_uniObject->find_codepoint($l_loCodepoint, $l_inCount);
    my $l_boFlag = $l_uniObject->isalnum_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isalpha_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->islower_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isupper_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isdigit_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isxdigit_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->iscntrl_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isgraph_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isspace_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isblank_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->isprint_codepoint($l_inOffset);
    my $l_boFlag = $l_uniObject->ispunct_codepoint($l_inOffset);
    my $l_loCodepoint = $l_uniObject->tolower_codepoint($l_inOffset);
    my $l_loCodepoint = $l_uniObject->toupper_codepoint($l_inOffset);
    my $l_inByteswritten = $l_uniObject->save($l_uniFile, $l_strEncoding);
    my $l_inBytesread = $l_uniObject->load($l_uniFile, $l_strEncoding);
    my $l_untObject = $l_uniObject->to_tesseract($l_inCodepoints, $l_inDim1, $l_inDim2, $l_inDim3, $l_inDim4);
    my $l_uniObject = $l_untObject->from_tesseract();
    $l_untObject->set_element($l_uniElement, $l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);
    my $l_uniElement = $l_untObject->get_element($l_inLevel1, $l_inLevel2, $l_inLevel3, $l_inLevel4);

=head2 PERL5 OVERLOADED OPERATORS

The following Perl5 overloaded operators work directly with B<< Unicode >> objects. This is only syntacic sugar, and due to the fact that they are Perl subroutines instead of C functions there is a bit of overhead when using them. But in some cases it makes sense for readability of the code. For example, using the concatentation operator B<< + >> if there are a lot of B<< Unicode >> objects can save a lot of typing instead of using the B<< contactenate() >> method. The comparison operators of Perl5 retain their meaning for number and string comparisons and can be useful if sorting arrays of B<< Unicode >> objects or testing values in a series of statements. It is advisable to use them where it makes the code more readable, but keep in mind the performance penalty they bring to the processing.

    use overload
        "=" => sub { my $class = ref($_[0]); $class->new($_[0]) },
        "+" => sub { $_[0]->concatenate($_[1]) },
        "<=>" => sub { $_[0]->compare_ascendingnumeric($_[1]) },
        "==" => sub { $_[0]->compare_ascendingnumeric($_[1]) == 0 },
        "!=" => sub { $_[0]->compare_ascendingnumeric($_[1]) != 0 },
        "<" => sub { $_[0]->compare_ascendingnumeric($_[1]) < 0 },
        "<=" => sub { $_[0]->compare_ascendingnumeric($_[1]) <= 0 },
        ">" => sub { $_[0]->compare_ascendingnumeric($_[1]) > 0 },
        ">=" => sub { $_[0]->compare_ascendingnumeric($_[1]) >= 0 },
        "cmp" => sub { $_[0]->compare_ascendingstring($_[1]) },
        "eq" => sub { $_[0]->compare_ascendingstring($_[1]) == 0 },
        "ne" => sub { $_[0]->compare_ascendingstring($_[1]) != 0 },
        "lt" => sub { $_[0]->compare_ascendingstring($_[1]) < 0 },
        "le" => sub { $_[0]->compare_ascendingstring($_[1]) <= 0 },
        "gt" => sub { $_[0]->compare_ascendingstring($_[1]) > 0 },
        "ge" => sub { $_[0]->compare_ascendingstring($_[1]) >= 0 },
        "fallback" => 1;

=head1 DESCRIPTION

This library brings B<< Unicode >> codepoint compliant string functions to Perl5 that can:

=over 4

=item * Construct and destruct B<< Unicode >>, B<< UnicodeArray >> and B<< UnicodeTesseract >> objects

=item * Get and set B<< Unicode >>, B<< UnicodeArray >> and B<< UnicodeTesseract >> object metadata

=item * Convert from/to C integral numeric types int, long and long long

=item * Convert from/to C floating point types float, double and long double

=item * Convert strings from/to any L<< iconv(3) >> supported encoding

=item * Count, copy, swap, append, find, extract and replace B<< Unicode >> codepoints

=item * Compare locale-aware B<< Unicode >> codepoints as strings

=item * Compare locale-aware B<< Unicode >> codepoints as numbers

=item * Convert B<< Unicode >> codepoints to uppercase, lowercase or swapcase

=item * Count, extract, replace, insert, append, delete, sort and locate delimited subvalues

=item * Extract or replace B<< Unicode >> subvalues using Perl5 dynamic arrays

=item * Get, set, determine character types and case-convert single codepoints

=item * File load/save B<< Unicode >> codepoints using any L<< iconv(3) >> supported encoding

=item * Separate virtual memory space support for big data that cannot fit on stack or heap

=back

=head2 DEPENDENCIES

This B<< Unicode >> library is generated using the L<< SWIG (Simple Wrapper Interface Generator)|http://www.swig.org >> software version 3.0.12 to generate the XS interface files used with Perl5. It is recommended that Perl versions 5.10 or later be used with this library. Earlier versions may work but have not been tested. It is also dependent on the GCC C compiler version 4.4 (or higher).  Other than this, there are no other library dependencies. This library was developed and tested using Ubuntu 12.04/16.04 x86_64 GNU/Linux.

=head2 DOCUMENTATION

The B<< Doxygen >> documentation inside the F<< unicode.h >> header file is for the C struct (object) definitions. The B<< Doxygen >> documentation inside the F<< unicode.c >> C source code file is for the C functions as used in C/C++. The Perl5 B<< perldoc >> documentation inside the F<< testunicode.pl >> file is written using Perl's B<< POD >> format.

=head2 IMPLEMENTATION

Most functions are passed a pointer to a struct as their first parameter, normally referred to as an object.  SWIG uses the first pointer to any imported C functions similar to how a I<< this >> pointer is used in C++ functions.  Internally, Unicode strings are stored in an internal buffer in UTF32BE encoding. Each codepoint is represented internally as a wide character type I<< wchar_t >>.  Since Perl5 does not support working directly with wide characters, there are conversion functions between B<< Unicode >> objects and Perl5 strings that are available.  Any encoding supported by the operating system's L<< iconv(3) >> function can be imported or exported from a B<< Unicode >> object, not just UTF8 or ASCII.

=head3 C/C++ INTERFACE

Unicode library functions are written in C as standalone functions. Pointers to struct objects are passed as parameters, along with other non-pointer parameters. Functions provided to construct new objects include B<< Unicode_new() >>, B<< UnicodeArray_new() >> and B<< UnicodeTesseract_new() >>. Functions are provided to destruct old objects include B<< Unicode_delete() >>, B<< UnicodeArray_delete() >> and B<< UnicodeTesseract_delete() >>. All other Unicode library function names begin with the prefix B<< Unicode_ >>.

=head3 PERL5 INTERFACE

The mapped SWIG interface in Perl5 of the Unicode library uses an object-oriented syntax.

=head4 OBJECT CONSTRUCTORS AND DESTRUCTORS

To construct new objects in Perl5, class methods are used with the B<< Unicode:: >> package prefix. Object destructors are called automatically by Perl5 when the objects fall out of scope, however, it is also possible to delete the object manually using the L<< perlfunc/undef >> command.

=head4 OBJECT METHODS

All other Unicode functions are called as Perl5 object methods, and are mapped by SWIG into the B<< Unicode >> Perl5 package. The names of the object methods do not have the B<< Unicode:: >> prefix that class methods have nor the B<< Unicode_ >> prefix that C functions have. Object methods are called by appending the B<< -> >> operator to the object itself, like for example:

    my $l_inByteswritten = $l_uniObject->save($l_uniFile, $l_strEncoding);
    
The first parameter in the C functions are pointers to a struct. SWIG uses this first parameter similarly to how a I<< this >> pointer is used in C++ functions. This means the first parameter passed to C functions is not passed as a parameter to the equivalent Perl5 object methods. An example follows.

=over 4

=item * C function call

    char * l_poszEbcdic = Unicode_export_string(l_pouniObject, 0, "EBCDIC-US");

=item * Perl5 object method call

    my $l_strEbcdic = $l_uniObject->export_string(0, 'EBCDIC-US');

=back

=head4 CHARACTER ENCODING

Internally, Unicode strings are stored in an internal buffer in UTF32BE encoding. Each codepoint is represented internally as a wide character type I<< wchar_t >>.  Since Perl5 does not support working directly with wide characters, there are conversion functions between B<< Unicode >> objects and Perl5 strings that are available.  Any encoding supported by the operating system's L<< iconv(3) >> function can be imported or exported from a B<< Unicode >> object, not just UTF8 or ASCII.

=head3 USING UTF8 IN PERL5

Perl scripts can often experience a large performance increase using these functions when working with B<< Unicode >> data. It is often helpful to include the following headers in your Perl script to get the most benefit from UTF8:

    use strict;
    use warnings;
    use utf8;                 # allow UTF-8 in Perl script
    use open qw(:std :utf8);  # assume UTF-8 encoding in standard I/O
    use locale;               # import and use server locale information
    use feature ':5.10';      # not tested with earlier versions

=head2 USING SWIG

Running SWIG and compiling the source code is really simple. A L<< sh(1) >> script can be created to re-generate the B<< Unicode >> library using SWIG and then using GCC to recompile the source code. Here is example script below. Note that it compiles in parallel for a faster response.

    #!/bin/sh
    # vim: fileencoding=utf8
    swig -perl unicode.i
    gcc -Wall -pipe -c `perl -MConfig -e 'print join(" ", @Config{qw(ccflags optimize cccdlflags)}, "-I$Config{archlib}/CORE")'` unicode.c &
    gcc -pipe -c `perl -MConfig -e 'print join(" ", @Config{qw(ccflags optimize cccdlflags)}, "-I$Config{archlib}/CORE")'` unicode_wrap.c &
    wait
    g++ `perl -MConfig -e 'print $Config{lddlflags}'` unicode.o unicode_wrap.o -o Unicode.so
    
=cut
1;

Part 3. C Source File

This is the longest post of the group due to it’s nature. Knowledge of C will of course be useful, but really shouldn’t be necessary to understand the purpose of each routine, since there is vebose documentation done using the Doxygen format.

Of particular interest for people interested in the UNICODE conversion capabilities of the source code using iconv(3), you may want to visit the following routines for more detail:

  • Unicode_import_string
  • Unicode_export_string
  • Unicode_save
  • Unicode_load

For those interested in the mmap(2) virtual memory code, you may want to visit the following routine for more details:

  • UnicodeTesseract_new

If you are interested in trying any of this yourself, it would be a good idea to copy and paste the source code in all the posts in this series, and save them to source code files in the same folder. All of the source code and shell script files will be provided, as well as their names used.

With no further ado, here is the source code for the unicode.c file:

/**
 * vim: fileencoding=utf8
 * @file unicode.c - SWIG C Extension Unicode Library
 * @brief Struct Unicode SWIG extended function definitions
 * @version 1.2
 *
 * @note This is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * @note This is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * @note You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "unicode.h"

#ifdef __cplusplus
extern "C" {
#endif

/*! @mainpage Unicode Codepoint Compliant String Library
 *
 * @section description Description
 *
 * This library brings unicode codepoint compliant string functions to
 * script languages that can:
 * @li Construct and destruct **Unicode**, **UnicodeArray** and **UnicodeTesseract** objects
 * @li Get and set **Unicode**, **UnicodeArray** and **UnicodeTesseract** object metadata
 * @li Convert from/to C integral numeric types int, long and long long
 * @li Convert from/to C floating point types float, double and long double
 * @li Convert strings from/to any **iconv(3)** supported encoding
 * @li Count, copy, swap, append, find, extract and replace **Unicode** codepoints
 * @li Compare locale-aware **Unicode** codepoints as strings
 * @li Compare locale-aware **Unicode** codepoints as numbers
 * @li Convert **Unicode** codepoints to uppercase, lowercase or swapcase
 * @li Count, extract, replace, insert, append, delete, sort and locate delimited subvalues
 * @li Extract or replace **Unicode** subvalues using a script language's native arrays
 * @li Get, set, determine character types and case-convert single codepoints
 * @li File load/save **Unicode** codepoints using any **iconv(3)** supported encoding
 * @li Separate virtual memory space support for big data that cannot fit on stack or heap
 *
 * @subsection dependencies Dependencies
 *
 * This **Unicode** library is generated using the SWIG (Simple Wrapper
 * Interface Generator) software version 3.0 (or higher) to generate the
 * interface code used by scripting languages.  It is also dependent on the GCC
 * C compiler version 4.4 (or higher).  Other than this, there are no other
 * library dependencies. This library was developed and tested using Ubuntu
 * 12.04/16.04 x86_64 GNU/Linux.
 *
 * @subsection documentation Documentation
 *
 * The **Doxygen** documentation is for the C functions only, which can be used
 * in both C or C++ programs directly. Scripting language interfaces are
 * documented separately.
 *
 * @subsection implementation Implementation
 *
 * Most functions are passed a pointer to a struct as their first parameter,
 * normally referred to as an object.  SWIG uses the first pointer to any
 * imported C functions similar to how a *this* pointer is used in C++
 * functions.  Internally, Unicode strings are stored in an internal buffer in
 * UTF32LE encoding. Each codepoint is represented internally as a wide
 * character type *wchar_t*.  Since most scripting languages do not support
 * working directly with wide characters, there are conversion functions
 * between **Unicode** objects and native strings that are available.  Any
 * encoding supported by the operating system's **iconv(3)** function can be
 * imported or exported from a **Unicode** object, not just UTF8 or ASCII.
 *
 */

/**
 * @fn "struct Unicode * Unicode_new()"
 * @brief Constructs an empty **Unicode** object
 * @details A new **struct Unicode** object is allocated on the heap and the
 * heap address is returned.  The object is considered to be in an empty state.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_new();
 * @endcode
 *
 * @retval "struct Unicode *" = Pointer to new empty **Unicode** object
 * @exception assert(3) Aborts if calloc(3) returns a null pointer
 */

struct Unicode * Unicode_new() 
{ 
    struct Unicode * l_pouniObject = (struct Unicode *) calloc(1, sizeof(struct Unicode)); 
    assert(l_pouniObject != 0);
    l_pouniObject->m_poszCodepoints = 0;
    l_pouniObject->m_sizCodepoints = 0;
    l_pouniObject->m_sizBytes = 0;
    return l_pouniObject; 
} 

/**
 * @fn "void Unicode_delete(struct Unicode ** u_popouniObject)"
 * @brief Destructs a **Unicode** object
 * @details When finished using a **Unicode** object, you can destruct it using
 * this function. The C library function **free(3)** is used to first
 * deallocate any codepoints on the heap, and then used to deallocate the
 * **struct Unicode** object itself. Note that the parameter is a
 * double-pointer to a **Unicode** object, as the value of the pointer is set
 * to null after calling **free(3)**.
 *
 * @attention Failure to call this on a constructed **Unicode** object can cause a memory leak.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * l_pouniObject = Unicode_from_string("0123456789abcdef", 0, "ASCII");
 * Unicode_delete(&l_pouniObject);
 * @endcode
 *
 * @param[in,out] u_popouniObject = Pointer to pointer of **Unicode** object
 * @retval "void" = None
 * @exception assert(3) Aborts if u_popouniObject contains a null pointer
 * @exception assert(3) Aborts if *u_popouniObject contains a null pointer
 */

void Unicode_delete(struct Unicode ** u_popouniObject)
{ 
    assert(u_popouniObject != 0);
    assert(*u_popouniObject != 0);
    if ((*u_popouniObject)->m_poszCodepoints != 0) free((*u_popouniObject)->m_poszCodepoints);
    free(*u_popouniObject);
    *u_popouniObject = 0;
} 

/**
 * @fn "void Unicode_clear(struct Unicode * u_pouniObject)"
 * @brief Clears the **Unicode** object of any codepoint content
 * @details When finished using the codepoint contents of a **Unicode** object,
 * you can clear it's contents using this function. The C library function
 * **free(3)** is used to deallocate any codepoints on the heap, but the
 * **struct Unicode** object itself is left intact.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * char * l_poszEbcdic = 0;
 * l_pouniObject = Unicode_from_string("0123456789abcdef", 0, "ASCII");
 * l_poszEbcdic = Unicode_export_string(l_pouniObject, 0, "EBCDIC-US");
 * Unicode_clear(l_pouniObject);
 * @endcode
 *
 * @param[in,out] u_pouniObject = The updated **Unicode** object
 * @retval "void" = None
 * @exception None
 */

void Unicode_clear(struct Unicode * u_pouniObject)
{
    if (u_pouniObject != 0) {
        if (u_pouniObject->m_poszCodepoints != 0) free(u_pouniObject->m_poszCodepoints);
        u_pouniObject->m_poszCodepoints = 0;
        u_pouniObject->m_sizCodepoints = 0;
        u_pouniObject->m_sizBytes = 0;
    }
}

/**
 * @fn "int Unicode_empty(const struct Unicode * i_pouniObject)"
 * @brief Returns true if **Unicode** object is empty.
 * @details Returns true if the **Unicode** object has not been allocated, the
 * codepoints buffer has not been allocated, or the number of codepoints or
 * bytes is zero.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * printf("The object is %s\n", Unicode_empty(l_pouniObject) ? "empty" : "not empty");
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @retval "bool" = true (1) if **Unicode** object is empty else false (0)
 * @exception None
 */

int Unicode_empty(const struct Unicode * i_pouniObject)
{
    if (i_pouniObject == 0 || i_pouniObject->m_poszCodepoints == 0 || i_pouniObject->m_sizCodepoints == 0 || i_pouniObject->m_sizBytes == 0)
        return(1);
    return(0);
}

/**
 * @fn "size_t Unicode_codepoints(const struct Unicode * i_pouniObject)"
 * @brief Returns codepoint count in the **Unicode** object
 * @details Returns the number of codepoints stored in the **Unicode** object content.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * size_t l_sizInbytes = 0;
 * size_t l_sizOutcodepoints = 0;
 * l_sizInbytes = Unicode_import_string(l_pouniObject, "João Méroço", 0, "UTF8");
 * l_sizOutcodepoints = Unicode_codepoints(l_pouniObject);
 * @endcode
 *
 * @retval "size_t" = Number of UTF codepoints stored in **Unicode** object,
 * or zero if the **Unicode** object is empty
 * @exception None
 */

size_t Unicode_codepoints(const struct Unicode * i_pouniObject)
{
    if (i_pouniObject == 0 || i_pouniObject->m_poszCodepoints == 0)
        return(0);
    return(i_pouniObject->m_sizCodepoints);
}

/**
 * @fn "size_t Unicode_bytes(const struct Unicode * i_pouniObject)"
 * @brief Returns the byte count of the codepoints in the **Unicode** object
 * @details Returns the number of bytes used to store the codepoints of the
 * **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * size_t l_sizInbytes = 0;
 * size_t l_sizOutbytes = 0;
 * l_sizInbytes = Unicode_import_string(l_pouniObject, "João Méroço", 0, "UTF8");
 * l_sizOutbytes = Unicode_bytes(l_pouniObject);
 * @endcode
 *
 * @retval "size_t" = Number of bytes in **Unicode** object, or zero if the
 * **Unicode** object is empty.
 * @exception None
 */

size_t Unicode_bytes(const struct Unicode * i_pouniObject)
{
    if (i_pouniObject == 0 || i_pouniObject->m_poszCodepoints == 0)
        return(0);
    return(i_pouniObject->m_sizBytes);
}

/**
 * @fn "size_t Unicode_import_string(struct Unicode * u_pouniObject, const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding)"
 * @brief Converts a null-terminated character string into a **Unicode** object
 * @details Convert a series of 8-bit, 16-bit or 32-bit wide characters stored
 * in a null-terminated **char** string into a **Unicode** object. Input buffer
 * is pointed to by **i_poszString** using a char pointer. The number of 8-bit
 * bytes in the buffer to convert is passed in **i_sizMaxbytes**. The
 * **i_poszEncoding** parameter contains a character encoding supported by
 * the implementation's **iconv(3)** C library function.  The **i_sizMaxbytes**
 * parameter should contain the number of bytes (not characters) to convert,
 * not including any null terminator. No error occurs nor is any conversion
 * performed if **i_poszString** contains a null value or if **i_sizMaxbytes** is
 * negative.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * size_t l_sizInbytes = 0;
 * l_sizInbytes = Unicode_import_string(l_pouniObject, "João Méroço", 0, "UTF8");
 * @endcode
 *
 * @param[in,out] u_pouniObject = The updated **Unicode** object
 * @param[in] i_poszString = Pointer to input char string buffer
 * @param[in] i_sizMaxbytes = Maximum number of input bytes to convert, if zero
 * uses strlen() to determine length
 * @param[in] i_poszEncoding = Null terminated string containing iconv(3)
 * encoding of input
 * @retval "size_t" = Number of input bytes (not characters) successfully
 * converted not including the null terminator. Should be equal to the positive
 * value of **i_sizMaxbytes** if conversion completes successfully.
 * @exception abort(3) Aborts if u_pouniObject is null
 * @exception abort(3) Aborts if i_poszString is null
 * @exception abort(3) Aborts if i_poszEncoding is null
 * @exception assert(3) Aborts if alloca(3) call returns null
 * @exception assert(3) Aborts if calloc(3) call returns null
 * @exception abort(3) Aborts on iconv_open(3) failure
 * @exception abort(3) Aborts on iconv(3) failure
 * @exception abort(3) Aborts on iconv_close(3) failure
 */

size_t Unicode_import_string(struct Unicode * u_pouniObject, const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding)
{
    size_t l_sizConverted = 0;
    size_t l_sizOutbytes = 0;
    size_t l_sizOutbytesleft = 0;
    size_t l_sizInbytes = 0;
    size_t l_sizInbytesleft = 0;
    size_t l_sizReturn = 0;
    int l_inReturn = 0;
    const char * l_poszToencoding = "UTF32LE";
    const char * l_poszFromencoding = i_poszEncoding;
    char * l_poszBuffer = 0;
    char * l_poszOutbuf = 0;
    char * l_poszInbuf = 0;
    iconv_t l_ictCd;

    if (u_pouniObject == 0 || i_poszString == 0 || i_poszEncoding == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_poszString = %p, i_poszEncoding = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_poszString, i_poszEncoding);
        abort();
    }
    Unicode_clear(u_pouniObject);
    l_ictCd = iconv_open(l_poszToencoding, l_poszFromencoding);
    if (l_ictCd == (iconv_t) -1) {
        fprintf(stderr, "%s(%d) = iconv_open() = -1, errno = %d, strerror = '%s', l_poszToencoding = '%s', l_poszFromencoding = '%s', i_poszString = '%-.255s'\n",
            __FILE__, __LINE__, errno, strerror(errno), l_poszToencoding, l_poszFromencoding, i_poszString);
        abort();
    }
    // worst case UTF32LE output buffer size expansion
    l_sizOutbytes = (i_sizMaxbytes == 0 ? strlen(i_poszString) : i_sizMaxbytes) * sizeof(wchar_t);
    l_sizOutbytesleft = l_sizOutbytes;
    // allocate dynamic buffer for string
    l_poszBuffer = (char *) alloca(l_sizOutbytes + sizeof(wchar_t));
    assert(l_poszBuffer != 0);
    memset(l_poszBuffer, 0, l_sizOutbytes + sizeof(wchar_t));
    l_poszOutbuf = l_poszBuffer;
    l_sizInbytes = i_sizMaxbytes == 0 ? strlen(i_poszString) : i_sizMaxbytes;
    l_sizInbytesleft = l_sizInbytes;
    l_poszInbuf = (char *) i_poszString;
    l_sizReturn = iconv(l_ictCd, &l_poszInbuf, &l_sizInbytesleft, &l_poszOutbuf, &l_sizOutbytesleft);
    if (l_sizReturn == (size_t) -1) {
        fprintf(stderr, "%s(%d) = iconv() = -1, errno = %d, strerror = '%s', l_poszToencoding = '%s', l_poszFromencoding = '%s', l_sizInbytesleft = %lu, l_sizOutbytesleft = %lu, i_poszString = '%-.255s'\n",
            __FILE__, __LINE__, errno, strerror(errno), l_poszToencoding, l_poszFromencoding, l_sizInbytesleft, l_sizOutbytesleft, i_poszString);
    }
    l_inReturn = iconv_close(l_ictCd);
    if (l_inReturn == -1) {
        fprintf(stderr, "%s(%d) = iconv_close() = -1, errno = %d, strerror = '%s', l_poszToencoding = '%s', l_poszFromencoding = '%s', i_poszString = '%-.255s'\n",
            __FILE__, __LINE__, errno, strerror(errno), l_poszToencoding, l_poszFromencoding, i_poszString);
        abort();
    }
    u_pouniObject->m_poszCodepoints = (char *) calloc(1, l_sizOutbytes - l_sizOutbytesleft + sizeof(wchar_t));
    assert(u_pouniObject->m_poszCodepoints != 0);
    memcpy(u_pouniObject->m_poszCodepoints, l_poszBuffer, l_sizOutbytes - l_sizOutbytesleft);
    u_pouniObject->m_sizCodepoints = (l_sizOutbytes - l_sizOutbytesleft) / sizeof(wchar_t);
    u_pouniObject->m_sizBytes = l_sizOutbytes - l_sizOutbytesleft;
    l_sizConverted = l_sizInbytes - l_sizInbytesleft;
    return(l_sizConverted);
}

/**
 * @fn "char * Unicode_export_string(const struct Unicode * i_pouniObject, size_t i_sizMaxbytes, const char * i_poszEncoding)"
 * @brief Converts **Unicode** object value into a character string value
 * @details Converts **Unicode** object into an 8-bit, 16-bit or 32-bit wide
 * character string. The output buffer is allocated on the heap and returned
 * using a char pointer. The maximum number of bytes the output buffer can hold
 * is passed in **i_sizMaxbytes**. The output encoding is passed in the
 * **i_poszEncoding** parameter.  Any 8-bit, 16-bit or 32-bit wide character
 * encoding can be specified using **i_poszEncoding** as long as it is
 * supported by the implementation's **iconv(3)** C library function. The
 * **i_sizMaxbytes** parameter should contain the number of bytes (not
 * characters) that the output buffer can store INCLUDING any null terminator.
 * It is not an error for the **i_sizMaxbytes** parameter to be zero, but if it
 * is, the current maximum size in bytes of the **Unicode** object is used
 * instead. The return value is a pointer to the heap allocated converted
 * string which is null-terminated at the end.
 *
 * @note A char null byte is added at the end of the string, but if the
 * encoding is not 8-bit, then the calling routine will have to handle any
 * further null terminator bytes at the end of the converted value.
 *
 * @note The return type of (char *) does not imply that the buffer contents
 * pointed to should be interpreted as a null-terminated C string.  The calling
 * routine needs to make sure that the returned buffer contents are
 * re-interpreted as suitable for access to characters from the appropriate
 * character set encoding. This includes alignment on platforms that have tight
 * restrictions on alignment.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * char * l_poszEbcdic = 0;
 * l_pouniObject = Unicode_from_string("0123456789abcdef", 0, "ASCII");
 * l_poszEbcdic = Unicode_export_string(l_pouniObject, 0, "EBCDIC-US");
 * // do string processing...
 * free(l_poszEbcdic);
 * Unicode_delete(&l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @param[in] i_sizMaxbytes = Maximum number of output bytes to convert
 * @param[in] i_poszEncoding = Optional **iconv(3)** encoding of output with no "//" qualifiers
 * @retval "char *" = Pointer to heap allocated null-terminated converted
 * string or heap-allocated zero-length string if **i_pouniObject** is empty.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_poszEncoding is null
 * @exception abort(3) Aborts on iconv_open(3) failure
 * @exception abort(3) Aborts on iconv(3) failure
 * @exception abort(3) Aborts on iconv_close(3) failure
 */

char * Unicode_export_string(const struct Unicode * i_pouniObject, size_t i_sizMaxbytes, const char * i_poszEncoding)
{
    char l_archToencoding[UNICODE_BUFFER_MAX + 1];
    const char * l_poszFromencoding = "UTF32LE";
    size_t l_sizOutbytesleft = 0;
    size_t l_sizInbytesleft = 0;
    size_t l_sizReturn = 0;
    int l_inReturn = 0;
    char * l_poszOutbuf = 0;
    char * l_poszOutbufleft = 0;
    char * l_poszInbuf = 0;
    iconv_t l_ictCd;

    if (i_pouniObject == 0 || i_poszEncoding == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_poszEncoding = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_poszEncoding);
        abort();
    }
    if (Unicode_empty(i_pouniObject)) {
        l_poszOutbuf = (char *) calloc(1, sizeof(char));
        assert(l_poszOutbuf != 0);
        return(l_poszOutbuf);
    }
    sprintf(l_archToencoding, "%s//TRANSLIT", i_poszEncoding);
    l_ictCd = iconv_open(l_archToencoding, l_poszFromencoding);
    if (l_ictCd == (iconv_t) -1) {
        fprintf(stderr, "%s(%d) = iconv_open() = -1, errno = %d, strerror = '%s', l_archToencoding = '%s', l_poszFromencoding = '%s', m_poszCodepoints = '%-.255s'\n",
            __FILE__, __LINE__, errno, strerror(errno), l_archToencoding, l_poszFromencoding, i_pouniObject->m_poszCodepoints);
        abort();
    }
    l_sizOutbytesleft = i_sizMaxbytes == 0 ? i_pouniObject->m_sizBytes + sizeof(wchar_t) : i_sizMaxbytes;
    l_poszOutbuf = (char *) calloc(1, l_sizOutbytesleft + sizeof(wchar_t));
    assert(l_poszOutbuf != 0);
    l_poszOutbufleft = l_poszOutbuf;
    l_sizInbytesleft = i_pouniObject->m_sizBytes;
    l_poszInbuf = i_pouniObject->m_poszCodepoints;
    l_sizReturn = iconv(l_ictCd, &l_poszInbuf, &l_sizInbytesleft, &l_poszOutbufleft, &l_sizOutbytesleft);
    if (l_sizReturn == (size_t) -1) {
        fprintf(stderr, "%s(%d) = iconv() = -1, errno = %d, strerror = '%s', l_archToencoding = '%s', l_poszFromencoding = '%s', l_sizInbytesleft = %lu, l_sizOutbytesleft = %lu\n",
            __FILE__, __LINE__, errno, strerror(errno), l_archToencoding, l_poszFromencoding, l_sizInbytesleft, l_sizOutbytesleft);
    }
    l_inReturn = iconv_close(l_ictCd);
    if (l_inReturn == -1) {
        fprintf(stderr, "%s(%d) = iconv_close() = -1, errno = %d, strerror = '%s', l_archToencoding = '%s', l_poszFromencoding = '%s', m_poszCodepoints = '%-.255s'\n",
            __FILE__, __LINE__, errno, strerror(errno), l_archToencoding, l_poszFromencoding, i_pouniObject->m_poszCodepoints);
        abort();
    }
    return(l_poszOutbuf);
}

/**
 * @fn "struct Unicode * Unicode_from_string(const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding)"
 * @brief Creates a new **Unicode** object from a null-terminated character string
 * @details Creates a **Unicode** object from a series of 8-bit, 16-bit or
 * 32-bit wide characters stored in a **char** null-terminated string. Input
 * buffer is pointed to by **i_poszString** using a **char** pointer. The
 * number of 8-bit bytes in the buffer to convert is passed in
 * **i_sizMaxbytes**. The **i_poszEncoding** parameter contains a character
 * encoding supported by the implementation's **iconv(3)** C library function.
 * The **i_sizMaxbytes** parameter should contain the number of bytes (not
 * characters) to convert, NOT INCLUDING any null terminator. If
 * **i_sizMaxbytes** is zero, then **strlen()** is used on **i_poszString** to
 * determine the appropriate value, which is only valid for 8-bit encodings.
 * If the **i_poszString** pointer is null then an empty **Unicode** object
 * is returned.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * char * l_poszEbcdic = 0;
 * l_pouniObject = Unicode_from_string("0123456789abcdef", 0, "ASCII");
 * l_poszEbcdic = Unicode_export_string(l_pouniObject, 0, "EBCDIC-US");
 * @endcode
 *
 * @param[in] i_poszString = Pointer to input **char** string buffer
 * @param[in] i_sizMaxbytes = Maximum number of input bytes to convert, use **strlen()** if zero
 * @param[in] i_poszEncoding = Null terminated string containing **iconv(3)** encoding of input
 * @retval "struct Unicode *" = Pointer to new **Unicode** object with codepoints from string
 * @exception abort(3) Aborts if i_poszEncoding is null
 * @see Unicode_import_string()
 */

struct Unicode * Unicode_from_string(const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding)
{
    struct Unicode * l_pouniObject = 0;

    if (i_poszEncoding == 0) {
        fprintf(stderr, "%s(%d) = i_poszEncoding = %p\n",
            __FILE__, __LINE__, i_poszEncoding);
        abort();
    }
    l_pouniObject = Unicode_new();
    if (i_poszString != 0) {
        Unicode_import_string(l_pouniObject, i_poszString, i_sizMaxbytes, i_poszEncoding);
    }
    return(l_pouniObject);
}

/**
 * @fn "struct Unicode * Unicode_from_int(int i_inValue)"
 * @brief Converts an **int** value into a new **Unicode** object
 * @details The passed parameter **l_inValue** containing a signed integer
 * value is converted into a **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * int l_inNumber = 10000;
 * l_pouniObject = Unicode_from_int(l_inNumber);
 * @endcode
 *
 * @param[in] i_inValue = Contains a signed integer value
 * @retval "struct Unicode *" = The new **Unicode** object
 * @see Unicode_import_string()
 */

struct Unicode * Unicode_from_int(int i_inValue)
{
    char l_archBuffer[UNICODE_BUFFER_MAX + 1];
    struct Unicode * l_pouniObject = 0;

    setlocale(LC_NUMERIC, "C");
    sprintf(l_archBuffer, "%d", i_inValue);
    setlocale(LC_NUMERIC, "");
    l_pouniObject = Unicode_new();
    Unicode_import_string(l_pouniObject, l_archBuffer, 0, "ASCII");
    return l_pouniObject;
}

/**
 * @fn "struct Unicode * Unicode_from_long(long i_loValue)"
 * @brief Converts a **long** integer value into a new **Unicode** object
 * @details The passed parameter **l_loValue** containing a signed **long** integer
 * value is converted into a **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * long l_loNumber = 1000000000L;
 * l_pouniObject = Unicode_from_long(l_loNumber);
 * @endcode
 *
 * @param[in] i_loValue = Contains a signed **long** integer value
 * @retval "struct Unicode *" The new **Unicode** object
 * @see Unicode_import_string()
 */

struct Unicode * Unicode_from_long(long i_loValue)
{
    char l_archBuffer[UNICODE_BUFFER_MAX + 1];
    struct Unicode * l_pouniObject = 0;

    setlocale(LC_NUMERIC, "C");
    sprintf(l_archBuffer, "%ld", i_loValue);
    setlocale(LC_NUMERIC, "");
    l_pouniObject = Unicode_new();
    Unicode_import_string(l_pouniObject, l_archBuffer, 0, "ASCII");
    return l_pouniObject;
}

/**
 * @fn "struct Unicode * Unicode_from_longlong(long long i_llValue)"
 * @brief Converts a **long long** integer value into a new **Unicode** object
 * @details The passed parameter **l_llValue** containing a signed **long long**
 * integer value is converted into a **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * long long l_llNumber = 1000000000000LL;
 * l_pouniObject = Unicode_from_longlong(l_llNumber);
 * @endcode
 *
 * @param[in] i_llValue = Contains a signed **long long** integer value
 * @retval "struct Unicode *" = The new **Unicode** object
 * @see Unicode_import_string()
 */

struct Unicode * Unicode_from_longlong(long long i_llValue)
{
    char l_archBuffer[UNICODE_BUFFER_MAX + 1];
    struct Unicode * l_pouniObject = 0;

    setlocale(LC_NUMERIC, "C");
    sprintf(l_archBuffer, "%lld", i_llValue);
    setlocale(LC_NUMERIC, "");
    l_pouniObject = Unicode_new();
    Unicode_import_string(l_pouniObject, l_archBuffer, 0, "ASCII");
    return l_pouniObject;
}

/**
 * @fn "struct Unicode * Unicode_from_float(float i_flValue)"
 * @brief Converts a **float** decimal value into a new **Unicode** object
 * @details The passed parameter **l_flValue** containing a signed
 * single-precision **float** value is converted into a **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * float l_flNumber = 10000.0F;
 * l_pouniObject = Unicode_from_float(l_flNumber);
 * @endcode
 *
 * @param[in] i_flValue = Contains a signed single-precision **float** value
 * @retval "struct Unicode *" = The updated **Unicode** object
 * @see Unicode_import_string()
 */

struct Unicode * Unicode_from_float(float i_flValue)
{
    char l_archBuffer[UNICODE_BUFFER_MAX + 1];
    struct Unicode * l_pouniObject = 0;

    setlocale(LC_NUMERIC, "C");
    sprintf(l_archBuffer, "%.7e", i_flValue);
    setlocale(LC_NUMERIC, "");
    l_pouniObject = Unicode_new();
    Unicode_import_string(l_pouniObject, l_archBuffer, 0, "ASCII");
    return l_pouniObject;
}

/**
 * @fn "struct Unicode * Unicode_from_double(double i_doValue)"
 * @brief Converts a **double** decimal value into a new **Unicode** object
 * @details The passed parameter **l_doValue** containing a signed
 * **double** decimal value is converted into a **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * double l_doNumber = 1.234567890e7;
 * l_pouniObject = Unicode_from_double(l_doNumber);
 * @endcode
 *
 * @param[in] i_doValue = Contains a signed **double** decimal value
 * @retval "struct Unicode *" = The new **Unicode** object
 * @see Unicode_import_string()
 */

struct Unicode * Unicode_from_double(double i_doValue)
{
    char l_archBuffer[UNICODE_BUFFER_MAX + 1];
    struct Unicode * l_pouniObject = 0;

    setlocale(LC_NUMERIC, "C");
    sprintf(l_archBuffer, "%.15le", i_doValue);
    setlocale(LC_NUMERIC, "");
    l_pouniObject = Unicode_new();
    Unicode_import_string(l_pouniObject, l_archBuffer, 0, "ASCII");
    return l_pouniObject;
}

/**
 * @fn "struct Unicode * Unicode_from_longdouble(long double i_ldValue)"
 * @brief Converts a long double decimal value into a new **Unicode** object
 * @details The passed parameter **l_ldValue** containing a signed
 * **long double** decimal value is converted into a **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = 0;
 * long double l_ldNumber = 1.234567890987654321e07L;
 * l_pouniObject = Unicode_from_longdouble(l_ldNumber);
 * @endcode
 *
 * @param[in] i_ldValue = Contains a signed **long double** decimal value
 * @retval "struct Unicode *" = The updated **Unicode** object
 * @see Unicode_import_string()
 */

struct Unicode * Unicode_from_longdouble(long double i_ldValue)
{
    char l_archBuffer[UNICODE_BUFFER_MAX + 1];
    struct Unicode * l_pouniObject = 0;

    setlocale(LC_NUMERIC, "C");
    sprintf(l_archBuffer, "%.18Le", i_ldValue);
    setlocale(LC_NUMERIC, "");
    l_pouniObject = Unicode_new();
    Unicode_import_string(l_pouniObject, l_archBuffer, 0, "ASCII");
    return l_pouniObject;
}

/**
 * @fn "int Unicode_to_int(const struct Unicode * i_pouniObject)"
 * @brief Converts **Unicode** object value to signed integer value
 * @details Converts the current **Unicode** object to a signed integer value
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("12345", 0, "ASCII");
 * int l_inNumber = Unicode_to_int(l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @retval "int" = Signed **int** value
 * @exception abort(3) Aborts if i_pouniObject is null
 * @see Unicode_export_string()
 */

int Unicode_to_int(const struct Unicode * i_pouniObject)
{
    int l_inValue = 0;
    char * l_poszValue = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (!Unicode_empty(i_pouniObject)) {
        l_poszValue = Unicode_export_string(i_pouniObject, 0, "ASCII");
        setlocale(LC_NUMERIC, "C");
        sscanf(l_poszValue, "%d", &l_inValue);
        setlocale(LC_NUMERIC, "");
        free(l_poszValue);
    }
    return(l_inValue);
}

/**
 * @fn "long Unicode_to_long(const struct Unicode * i_pouniObject)"
 * @brief Converts **Unicode** object value to signed **long** integer value
 * @details Converts the current **Unicode** object to a signed **long** integer value
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("123456789", 0, "ASCII");
 * long l_loNumber = Unicode_to_long(l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @retval "long" = Signed **long** integer value
 * @exception abort(3) Aborts if i_pouniObject is null
 * @see Unicode_export_string()
 */

long Unicode_to_long(const struct Unicode * i_pouniObject)
{
    long l_loValue = 0;
    char * l_poszValue = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (!Unicode_empty(i_pouniObject)) {
        l_poszValue = Unicode_export_string(i_pouniObject, 0, "ASCII");
        setlocale(LC_NUMERIC, "C");
        sscanf(l_poszValue, "%ld", &l_loValue);
        setlocale(LC_NUMERIC, "");
        free(l_poszValue);
    }
    return(l_loValue);
}

/**
 * @fn "long long Unicode_to_longlong(const struct Unicode * i_pouniObject)"
 * @brief Converts **Unicode** object value to signed **long long** integer value
 * @details Converts the current **Unicode** object to a signed **long long**
 * integer value
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("1234567890987654321", 0, "ASCII");
 * long long l_llNumber = Unicode_to_longlong(l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @retval "long long" = Signed **long long** integer value
 * @exception abort(3) Aborts if i_pouniObject is null
 * @see Unicode_export_string()
 */

long long Unicode_to_longlong(const struct Unicode * i_pouniObject)
{
    long long l_llValue = 0;
    char * l_poszValue = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (!Unicode_empty(i_pouniObject)) {
        l_poszValue = Unicode_export_string(i_pouniObject, 0, "ASCII");
        setlocale(LC_NUMERIC, "C");
        sscanf(l_poszValue, "%lld", &l_llValue);
        setlocale(LC_NUMERIC, "");
        free(l_poszValue);
    }
    return(l_llValue);
}

/**
 * @fn "float Unicode_to_float(const struct Unicode * i_pouniObject)"
 * @brief Converts **Unicode** object value to signed **float** decimal value
 * @details Converts the current **Unicode** object to a signed **float**
 * decimal value
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("123.45", 0, "ASCII");
 * float l_flNumber = Unicode_to_float(l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @retval "float" = Signed **float** decimal value or zero on error
 * @exception abort(3) Aborts if i_pouniObject is null
 * @see Unicode_export_string()
 */

float Unicode_to_float(const struct Unicode * i_pouniObject)
{
    float l_flValue = 0;
    char * l_poszValue = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (!Unicode_empty(i_pouniObject)) {
        l_poszValue = Unicode_export_string(i_pouniObject, 0, "ASCII");
        setlocale(LC_NUMERIC, "C");
        sscanf(l_poszValue, "%e", &l_flValue);
        setlocale(LC_NUMERIC, "");
        free(l_poszValue);
    }
    return(l_flValue);
}

/**
 * @fn "double Unicode_to_double(const struct Unicode * i_pouniObject)"
 * @brief Converts **Unicode** object value to signed **double** decimal value
 * @details Converts the current **Unicode** object to a signed **double**
 * decimal value
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("12345678.90", 0, "ASCII");
 * double l_doNumber = Unicode_to_double(l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @retval "double" = Signed **double** decimal value or zero on error
 * @exception abort(3) Aborts if i_pouniObject is null
 * @see Unicode_export_string()
 */

double Unicode_to_double(const struct Unicode * i_pouniObject)
{
    double l_doValue = 0;
    char * l_poszValue = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (!Unicode_empty(i_pouniObject)) {
        l_poszValue = Unicode_export_string(i_pouniObject, 0, "ASCII");
        setlocale(LC_NUMERIC, "C");
        sscanf(l_poszValue, "%le", &l_doValue);
        setlocale(LC_NUMERIC, "");
        free(l_poszValue);
    }
    return(l_doValue);
}

/**
 * @fn "long double Unicode_to_longdouble(const struct Unicode * i_pouniObject)"
 * @brief Converts **Unicode** object value to signed **long double** decimal value
 * @details Converts the current **Unicode** object to a signed **long double**
 * decimal value
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("1.234567890987654321e99", 0, "ASCII");
 * long double l_ldNumber = Unicode_to_longdouble(l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object
 * @retval "long double" = Signed **long double** decimal value or zero on error
 * @exception abort(3) Aborts if i_pouniObject is null
 * @see Unicode_export_string()
 */

long double Unicode_to_longdouble(const struct Unicode * i_pouniObject)
{
    long double l_ldValue = 0;
    char * l_poszValue = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (!Unicode_empty(i_pouniObject)) {
        l_poszValue = Unicode_export_string(i_pouniObject, 0, "ASCII");
        setlocale(LC_NUMERIC, "C");
        sscanf(l_poszValue, "%Le", &l_ldValue);
        setlocale(LC_NUMERIC, "");
        free(l_poszValue);
    }
    return(l_ldValue);
}

/**
 * @fn "void Unicode_copy(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject)"
 * @brief Deep-copy clone of one **Unicode** object to another **Unicode** object
 * @details The contents of the first parameter **u_pouniObject** are cleared
 * and it's contents are replaced by a deep-copy clone of the codepoints of the
 * second parameter **i_pouniObject**.  No validation is done on the validity
 * of the codepoints.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_new();
 * struct Unicode * l_pouniClonable = Unicode_from_string("Data not to be modified", 0, "ASCII");
 * Unicode_copy(l_pouniObject, l_pouniClonable);
 * @endcode
 *
 * @param[in,out] u_pouniObject = Update pointer to copy destination **Unicode** object
 * @param[in] i_pouniObject = Input pointer to copy source **Unicode** object
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject is null
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception assert(3) Aborts if calloc(3) call returns null
 * @see Unicode_clear()
 */

void Unicode_copy(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject)
{
    if (u_pouniObject == 0 || i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_pouniObject = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_pouniObject);
        abort();
    }
    if (u_pouniObject != i_pouniObject) {
        Unicode_clear(u_pouniObject);
        if (!Unicode_empty(i_pouniObject)) {
            u_pouniObject->m_sizCodepoints = i_pouniObject->m_sizCodepoints;
            u_pouniObject->m_sizBytes = i_pouniObject->m_sizBytes;
            u_pouniObject->m_poszCodepoints = (char *) calloc(1, u_pouniObject->m_sizBytes + sizeof(wchar_t));
            assert(u_pouniObject->m_poszCodepoints != 0);
            memcpy(u_pouniObject->m_poszCodepoints, i_pouniObject->m_poszCodepoints, u_pouniObject->m_sizBytes);
        }
    }
}

/**
 * @fn "void Unicode_append(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject)"
 * @brief Append the codepoints of one **Unicode** object content to another
 * @details The codepoints of the first parameter **u_pouniObject** are updated
 * by appending the codepoints of the second parameter **i_pouniObject** to it.
 * No validation is done on the validity of the codepoints.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("First Part", 0, "ASCII");
 * struct Unicode * l_pouniAppend = Unicode_from_string("and Second Part", 0, "ASCII");
 * Unicode_append(l_pouniObject, l_pouniAppend);
 * @endcode
 *
 * @param[in,out] u_pouniObject = Update pointer to existing **Unicode** object
 * @param[in] i_pouniObject = Input reference to **Unicode** object
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject is null
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception assert(3) Aborts if realloc(3) call returns null
 */

void Unicode_append(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject)
{
    size_t l_sizCodepoints = 0;
    size_t l_sizBytes = 0;

    if (u_pouniObject == 0 || i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_pouniObject = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_pouniObject);
        abort();
    }
    if (Unicode_empty(i_pouniObject))
        return;
    l_sizCodepoints = u_pouniObject->m_sizCodepoints + i_pouniObject->m_sizCodepoints;
    l_sizBytes = u_pouniObject->m_sizBytes + i_pouniObject->m_sizBytes;
    u_pouniObject->m_poszCodepoints = (char *) realloc(u_pouniObject->m_poszCodepoints, l_sizBytes + sizeof(wchar_t));
    assert(u_pouniObject->m_poszCodepoints != 0);
    memcpy(u_pouniObject->m_poszCodepoints + u_pouniObject->m_sizBytes, i_pouniObject->m_poszCodepoints, i_pouniObject->m_sizBytes);
    memset(u_pouniObject->m_poszCodepoints + l_sizBytes, 0, sizeof(wchar_t));
    u_pouniObject->m_sizCodepoints = l_sizCodepoints;
    u_pouniObject->m_sizBytes = l_sizBytes;
}

/**
 * @fn "void Unicode_append_multiple(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject, size_t i_sizCount)"
 * @brief Appends the codepoints of one **Unicode** object to another one or more times
 * @details Appends **Unicode** object codepoints of second parameter
 * **i_pouniObject** onto the existing codepoints of the first parameter
 * **u_pouniObject** for the number of copies passed in the third parameter
 * **i_sizCount**.  No validation is done on the codepoints.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_new();
 * struct Unicode * l_pouniHeader = Unicode_from_string(" Report Chapter ", 0, "ASCII");
 * struct Unicode * l_pouniFiller = Unicode_from_string("=", 0, "ASCII");
 * int l_inCount = 40 - Unicode_codepoints(l_pouniHeader);
 * Unicode_append_multiple(l_pouniObject, l_pouniFiller, l_inCount);
 * Unicode_append(l_pouniObject, l_pouniHeader);
 * Unicode_append_multiple(l_pouniObject, l_pouniFiller, l_inCount);
 * @endcode
 *
 * @param[in,out] u_pouniObject = Update pointer to destination **Unicode** object
 * @param[in] i_pouniObject = Input reference to source **Unicode** object
 * @param[in] i_sizCount = If greater than zero, the number of times to append
 * **i_pouniObject** codepoints to **u_pouniObject** codepoints
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject is null
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception assert(3) Aborts if realloc(3) call returns null
 */

void Unicode_append_multiple(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject, size_t i_sizCount)
{
    wchar_t * l_powzCodepoints = 0;
    size_t l_sizCodepoints = 0;
    size_t l_sizBytes = 0;
    size_t l_sizOffset = 0;
    size_t l_sizCount = 0;

    if (u_pouniObject == 0 || i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_pouniObject = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_pouniObject);
        abort();
    }
    if (Unicode_empty(i_pouniObject) || i_sizCount == 0)
        return;
    l_sizCodepoints = u_pouniObject->m_sizCodepoints + i_pouniObject->m_sizCodepoints * i_sizCount;
    l_sizBytes = u_pouniObject->m_sizBytes + i_pouniObject->m_sizBytes * i_sizCount;
    l_powzCodepoints = (wchar_t *) realloc(u_pouniObject->m_poszCodepoints, l_sizBytes + sizeof(wchar_t));
    assert(l_powzCodepoints != 0);
    l_powzCodepoints[l_sizCodepoints] = 0;
    u_pouniObject->m_poszCodepoints = (char *) l_powzCodepoints;
    for (l_sizCount = 0; l_sizCount < i_sizCount; l_sizCount++) {
        l_sizOffset = u_pouniObject->m_sizBytes + (i_pouniObject->m_sizBytes * l_sizCount);
        memcpy(u_pouniObject->m_poszCodepoints + l_sizOffset, i_pouniObject->m_poszCodepoints, i_pouniObject->m_sizBytes);
    }
    u_pouniObject->m_sizCodepoints = l_sizCodepoints;
    u_pouniObject->m_sizBytes = l_sizBytes;
}

/**
 * @fn "void Unicode_swap(struct Unicode * u_pouniObject, struct Unicode * u_pouniSwap)"
 * @brief Perform shallow-copy swap of two **Unicode** objects
 * @details A shallow-copy swap of the contents of both **Unicode** objects is
 * performed. This means the contents of only the structures are swapped. Any
 * codepoints currently referenced by either **Unicode** object is left alone.
 * Internally, direct struct assignment is used instead of **memcpy(3)** for
 * maximum code portability.
 *
 * #### Example ####
 *
 * @code
 * // move the first 50 objects from l_pounaArray1 to l_pounaArray2
 * struct UnicodeArray * l_pounaArray1 = UnicodeArray_new(101);
 * load_all_101_objects_into_array(l_pounaArray1);
 * struct UnicodeArray * l_pounaArray2 = UnicodeArray_new(50);
 * for (l_sizOffset = 0; l_sizOffset < 50; l_sizOffset++) {
 *     Unicode_swap(&l_pounaArray1->m_pouniObjects[l_sizOffset], &l_pounaArray2->m_pouniObjects[l_sizOffset]);
 * }
 * UnicodeArray_delete(l_pounaArray1);
 * @endcode
 *
 * @param[in,out] u_pouniObject = Update pointer to first **Unicode** object
 * @param[in,out] u_pouniSwap = Update pointer to second **Unicode** object
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject is null
 * @exception abort(3) Aborts if u_pouniSwap is null
 */

void Unicode_swap(struct Unicode * u_pouniObject, struct Unicode * u_pouniSwap)
{
    struct Unicode l_uniTemp;

    if (u_pouniObject == 0 || u_pouniSwap == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, u_pouniSwap = %p\n",
            __FILE__, __LINE__, u_pouniObject, u_pouniSwap);
        abort();
    }
    if (u_pouniObject != u_pouniSwap) {
        l_uniTemp = *u_pouniObject;
        *u_pouniObject = *u_pouniSwap;
        *u_pouniSwap = l_uniTemp;
    }
}

/**
 * @fn "int Unicode_find(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniFind, int i_inCount)"
 * @brief Finds a string of codepoints inside a **Unicode** object
 * @details Finds the passed input parameter **i_pouniFind** content in the
 * **Unicode** object **i_pouniObject**. When **i_inCount** is non-negative, then a count
 * of N+1 (0-based) matches are made scanning from left to right. If negative,
 * a count of N matches (1-based) are made scanning from right to left.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniPhrase = Unicode_from_string("His name is João Méroço and he arrived yesterday.", 0, "ASCII");
 * struct Unicode * l_pouniArrived = Unicode_from_string("arrived", 0, "ASCII");
 * struct Unicode * l_pouniPeriod = Unicode_from_string(".", 0, "ASCII");
 * int l_inArrived = Unicode_find(l_pouniPhrase, l_pouniArrived, 0);
 * int l_inPeriod = Unicode_find(l_pouniPhrase, l_pouniPeriod, -1);
 * struct Unicode * l_pouniWhen = Unicode_extract(l_pouniPhrase, l_inArrived, l_inPeriod - l_inArrived);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object to search in
 * @param[in] i_pouniFind = Input pointer to **Unicode** object to search for
 * @param[in] i_inCount = Count of number of matches to make
 * @retval "int" = Offset (0-based) of first (left-most) matching codepoint in
 * codepoints (not bytes) in **i_pouniObject** or -1 if not found.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniFind is null
 */

int Unicode_find(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniFind, int i_inCount)
{
    int l_inOffset = 0;
    int l_inEnd = 0;
    int l_inIter = 0;
    wchar_t * l_powzObject = 0;
    wchar_t * l_powzFind = 0;
    int l_inLastcounter = 0;
    int l_inCounter = 0;
    int l_inResult = -1;

    if (i_pouniObject == 0 || i_pouniFind == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniFind = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniFind);
        abort();
    }
    if (Unicode_empty(i_pouniObject) || Unicode_empty(i_pouniFind))
        return(-1);
    l_inOffset = i_inCount >= 0 ? 0 : i_pouniObject->m_sizCodepoints - 1;
    l_inEnd = i_inCount >= 0 ? (int) i_pouniObject->m_sizCodepoints : -1;
    l_inIter = i_inCount >= 0 ? 1 : -1;
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    l_powzFind = (wchar_t *) i_pouniFind->m_poszCodepoints;
    l_inLastcounter = i_inCount >= 0 ? i_inCount + 1 : -i_inCount;
    while (l_inCounter != l_inLastcounter) {
        if (l_inCounter != 0) {
            l_inOffset += l_inIter;
        }
        while (l_inOffset != l_inEnd) {
            if (*(l_powzObject + l_inOffset) == *l_powzFind) {
                if ((l_inResult = wcsncmp(l_powzObject + l_inOffset, l_powzFind, i_pouniFind->m_sizCodepoints)) == 0) {
                    break;
                }
            }
            l_inOffset += l_inIter;
        }
        l_inCounter++;
    }
    return(l_inResult == 0 ? l_inOffset : -1);
}

/**
 * @fn "struct Unicode * Unicode_extract(const struct Unicode * i_pouniObject, size_t i_sizOffset, size_t i_sizCount)"
 * @brief Extracts **Unicode** codepoints from a **Unicode** object
 * @details Extracts **Unicode** codepoints from the input parameter
 * **i_pouniObject** at the zero-based offset passed in the input parameter
 * **i_sizOffset** for a number of codepoints passed in the **i_sizCount**
 * input parameter. The codepoints extracted are returned in a new **Unicode**
 * object.  If the **i_sizOffset** value is outside the current **Unicode**
 * object content then an empty **Unicode** object will be returned. If the
 * **i_sizCount** value is larger than remaining number of codepoints, then the
 * return value will only contain any remaining codepoints.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniPhrase = Unicode_from_string("His name is João Méroço and he arrived yesterday.", 0, "ASCII");
 * struct Unicode * l_pouniArrived = Unicode_from_string("arrived", 0, "ASCII");
 * struct Unicode * l_pouniPeriod = Unicode_from_string(".", 0, "ASCII");
 * int l_inArrived = Unicode_find(l_pouniPhrase, l_pouniArrived, 0);
 * int l_inPeriod = Unicode_find(l_pouniPhrase, l_pouniPeriod, -1);
 * struct Unicode * l_pouniWhen = Unicode_extract(l_pouniPhrase, l_inArrived, l_inPeriod - l_inArrived);
 * @endcode
 *
 * @param[in] i_pouniObject = The input **Unicode** object to search
 * @param[in] i_sizOffset = Zero-based offset to start extraction process
 * @param[in] i_sizCount = Maximum number of codepoints to extract
 * @retval "struct Unicode *" = Pointer to **Unicode** object containing extracted codepoints
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception assert(3) Aborts if calloc(3) call returns null
 */

struct Unicode * Unicode_extract(const struct Unicode * i_pouniObject, size_t i_sizOffset, size_t i_sizCount)
{
    struct Unicode * l_pouniExtracted = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    l_pouniExtracted = Unicode_new();
    l_pouniExtracted->m_sizCodepoints = i_pouniObject->m_sizCodepoints - i_sizOffset;
    if (l_pouniExtracted->m_sizCodepoints > i_sizCount) {
        l_pouniExtracted->m_sizCodepoints = i_sizCount;
    }
    l_pouniExtracted->m_sizBytes = l_pouniExtracted->m_sizCodepoints * sizeof(wchar_t);
    l_pouniExtracted->m_poszCodepoints = (char *) calloc(l_pouniExtracted->m_sizCodepoints + 1, sizeof(wchar_t));
    assert(l_pouniExtracted->m_poszCodepoints != 0);
    memcpy(l_pouniExtracted->m_poszCodepoints, i_pouniObject->m_poszCodepoints + i_sizOffset * sizeof(wchar_t), l_pouniExtracted->m_sizBytes);
    return(l_pouniExtracted);
}

/**
 * @fn "void Unicode_replace(struct Unicode * u_pouniObject, const struct Unicode * i_pouniReplace, size_t i_sizOffset, size_t i_sizCount)"
 * @brief Replaces **Unicode** codepoints in one **Unicode** object with codepoints from another
 * @details Replaces **Unicode** codepoints in **u_pouniObject** beginning at the
 * **i_sizOffset** for a length of **i_sizCount** codepoints (not bytes) with the
 * codepoints contained in the **i_pouniReplace** parameter. If the offset is
 * outside the existing number of codepoints in **u_pouniObject** then no
 * codepoints are deleted, and the codepoints in **i_pouniReplace** will simply
 * be appended onto the existing codepoints of the **u_pouniObject** object.
 * If the **i_sizCount** value is greater than the number of remaining
 * codepoints, then all the remaining codepoints after **i_sizOffset** in the
 * **u_pouniObject** parameter will be deleted. In any case, all of the codepoints
 * of **i_pouniReplace** are either inserted or appended to the codepoints of
 * the **u_pouniObject** parameter.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniPhrase = Unicode_from_string("His name is %%NAME%%.", 0, "ASCII");
 * struct Unicode * l_pouniPattern = Unicode_from_string("%%NAME%%", 0, "ASCII");
 * struct Unicode * l_pouniName = Unicode_from_string("João Méroço", 0, "UTF8");
 * int l_inPattern = Unicode_find(l_pouniPhrase, l_pouniPattern, 0);
 * Unicode_replace(l_pouniPhrase, l_pouniName, l_inPattern, Unicode_codepoints(l_pouniPattern));
 * @endcode
 *
 * @param[in,out] u_pouniObject = Update pointer to the existing **Unicode** object
 * @param[in] i_pouniReplace = Input pointer to the replacement **Unicode** object
 * @param[in] i_sizOffset = Zero-based offset to start replacement process
 * @param[in] i_sizCount = Count of existing UTF codepoints to replace
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject is null
 * @exception abort(3) Aborts if i_pouniReplace is null
 */

void Unicode_replace(struct Unicode * u_pouniObject, const struct Unicode * i_pouniReplace, size_t i_sizOffset, size_t i_sizCount)
{
    struct Unicode * l_pouniLhs = 0;
    struct Unicode * l_pouniRhs = 0;

    if (u_pouniObject == 0 || i_pouniReplace == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_pouniReplace = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_pouniReplace);
        abort();
    }
    l_pouniLhs = Unicode_extract(u_pouniObject, 0, i_sizOffset);
    l_pouniRhs = Unicode_extract(u_pouniObject, i_sizOffset + i_sizCount, u_pouniObject->m_sizCodepoints);
    Unicode_clear(u_pouniObject);
    Unicode_copy(u_pouniObject, l_pouniLhs);
    Unicode_append(u_pouniObject, i_pouniReplace);
    Unicode_append(u_pouniObject, l_pouniRhs);
    Unicode_delete(&l_pouniLhs);
    Unicode_delete(&l_pouniRhs);
}

/**
 * @fn "int Unicode_compare_ascendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)"
 * @brief String comparison of **Unicode** objects in ascending order
 * @details This method compares the two passed **Unicode** object parameters
 * as strings lexigraphically assuming internal **Unicode** encoding. If both
 * parameters have the same memory address, the same value, or are both empty
 * then they are considered equal and zero (0) is returned. An empty value is
 * always considered to be less than a non-empty value. If the first parameter
 * is less than the second parameter then negative one (-1) is returned. If the
 * first parameter is greater than the second parameter then one (1) is
 * returned.  Similar in concept to the strcmp() function, but works with
 * **Unicode** codepoints.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniAscii = Unicode_from_string("Joao Meroco", 0, "ASCII");
 * struct Unicode * l_pouniUtf8 = Unicode_from_string("João Méroço", 0, "UTF8");
 * int l_inCompare = Unicode_compare_ascendingstring(l_pouniAscii, l_pouniUtf8);
 * @endcode
 *
 * @param[in] i_pouniObject = Input **Unicode** object to be compared
 * @param[in] i_pouniCompare = Input **Unicode** object for comparison
 * @retval "int" = Returns -1, 0, 1 if **i_pouniObject** value is less than,
 * equal to or greater than the **i_pouniCompare** value lexigraphically
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniCompare is null
 */

int Unicode_compare_ascendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)
{
    size_t l_sizCodepoints = 0;
    int l_inResult = 0;

    if (i_pouniObject == 0 || i_pouniCompare == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniCompare = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniCompare);
        abort();
    }
    if (i_pouniObject == i_pouniCompare
        || i_pouniObject->m_poszCodepoints == i_pouniCompare->m_poszCodepoints
        || (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints == 0))
    {
        return(0);
    }
    if (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints > 0)
    {
        return(-1);
    }
    if (i_pouniObject->m_sizCodepoints > 0 && i_pouniCompare->m_sizCodepoints == 0)
    {
        return(1);
    }
    l_sizCodepoints = i_pouniObject->m_sizCodepoints > i_pouniCompare->m_sizCodepoints ?
        i_pouniObject->m_sizCodepoints : i_pouniCompare->m_sizCodepoints;
    l_inResult = wcsncmp((wchar_t *) i_pouniObject->m_poszCodepoints, (wchar_t *) i_pouniCompare->m_poszCodepoints, l_sizCodepoints);
    if (l_inResult < 0) return(-1);
    if (l_inResult > 0) return(1);
    return(0);
}

/**
 * @fn "int Unicode_compare_descendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)"
 * @brief String comparison of **Unicode** objects in descending order
 * @details This method compares the two passed **Unicode** object parameters
 * as strings lexigraphically assuming internal **Unicode** encoding. If both
 * parameters have the same memory address, the same value, or are both empty
 * then they are considered equal and zero (0) is returned. An empty value is
 * always considered to be less than a non-empty value. If the first parameter
 * is less than the second parameter then one (1) is returned. If the first
 * parameter is greater than the second parameter then negative one (-1) is
 * returned.  Similar in concept to the strcmp() function, but works with
 * **Unicode** codepoints.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniAscii = Unicode_from_string("Joao Meroco", 0, "ASCII");
 * struct Unicode * l_pouniUtf8 = Unicode_from_string("João Méroço", 0, "UTF8");
 * int l_inCompare = Unicode_compare_descendingstring(l_pouniAscii, l_pouniUtf8);
 * @endcode
 *
 * @param[in] i_pouniObject = Input **Unicode** object to be compared
 * @param[in] i_pouniCompare = Input **Unicode** object for comparison
 * @retval "int" = Returns 1, 0, -1 if **i_pouniObject** value is less than,
 * equal to or greater than the **i_pouniCompare** value lexigraphically
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniCompare is null
 */

int Unicode_compare_descendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)
{
    size_t l_sizCodepoints = 0;
    int l_inResult = 0;

    if (i_pouniObject == 0 || i_pouniCompare == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniCompare = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniCompare);
        abort();
    }
    if (i_pouniObject == i_pouniCompare
        || i_pouniObject->m_poszCodepoints == i_pouniCompare->m_poszCodepoints
        || (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints == 0))
    {
        return(0);
    }
    if (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints > 0)
    {
        return(1);
    }
    if (i_pouniObject->m_sizCodepoints > 0 && i_pouniCompare->m_sizCodepoints == 0)
    {
        return(-1);
    }
    l_sizCodepoints = i_pouniObject->m_sizCodepoints > i_pouniCompare->m_sizCodepoints ?
        i_pouniObject->m_sizCodepoints : i_pouniCompare->m_sizCodepoints;
    l_inResult = wcsncmp((wchar_t *) i_pouniObject->m_poszCodepoints, (wchar_t *) i_pouniCompare->m_poszCodepoints, l_sizCodepoints);
    if (l_inResult < 0) return(1);
    if (l_inResult > 0) return(-1);
    return(0);
}

/**
 * @fn "int Unicode_compare_ascendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)"
 * @brief Numeric comparison of **Unicode** objects in ascending order
 * @details This method compares the two passed **Unicode** object parameters
 * after converting them to floating point (long double) values. If both
 * parameters have the same memory address, the same value or are both empty
 * then zero (0) is returned. An empty value is always considered to be less
 * than a non-empty value. If the first parameter is less than the second
 * parameter then negative one (-1) is returned. If the first parameter is
 * greater than the second parameter then one (1) is returned.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniLong = Unicode_from_long(1000000L);
 * struct Unicode * l_pouniDouble = Unicode_from_double(1000000.0);
 * int l_inCompare = Unicode_compare_ascendingnumeric(l_pouniLong, l_pouniDouble);
 * @endcode
 *
 * @param[in] i_pouniObject = Input **Unicode** object to be compared
 * @param[in] i_pouniCompare = Input **Unicode** object for comparison
 * @retval "int" = Returns -1, 0, 1 if **i_pouniObject** value is less than,
 * equal to or greater than the **i_pouniCompare** value numerically
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniCompare is null
 */

int Unicode_compare_ascendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)
{
    long double l_ldObject = 0;
    long double l_ldCompare = 0;

    if (i_pouniObject == 0 || i_pouniCompare == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniCompare = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniCompare);
        abort();
    }
    if (i_pouniObject == i_pouniCompare
        || i_pouniObject->m_poszCodepoints == i_pouniCompare->m_poszCodepoints
        || (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints == 0))
    {
        return(0);
    }
    if (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints > 0)
    {
        return(-1);
    }
    if (i_pouniObject->m_sizCodepoints > 0 && i_pouniCompare->m_sizCodepoints == 0)
    {
        return(1);
    }
    l_ldObject = wcstold((wchar_t *) i_pouniObject->m_poszCodepoints, 0);
    l_ldCompare = wcstold((wchar_t *) i_pouniCompare->m_poszCodepoints, 0);
    if (l_ldObject < l_ldCompare) return(-1);
    if (l_ldObject > l_ldCompare) return(1);
    return(0);
}

/**
 * @fn "int Unicode_compare_descendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)"
 * @brief Numeric comparison of **Unicode** objects in descending order
 * @details This method compares the two passed **Unicode** object parameters
 * after converting them to floating point (long double) values. If both
 * parameters have the same memory address, the same value or are both empty
 * then zero (0) is returned. An empty value is always considered to be less
 * than a non-empty value. If the first parameter is less than the second
 * parameter then one (1) is returned. If the first parameter is greater than
 * the second parameter then negative one (-1) is returned.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniLong = Unicode_from_long(1000000L);
 * struct Unicode * l_pouniDouble = Unicode_from_double(1000000.0);
 * int l_inCompare = Unicode_compare_ascendingnumeric(l_pouniLong, l_pouniDouble);
 * @endcode
 *
 * @param[in] i_pouniObject = Input **Unicode** object to be compared
 * @param[in] i_pouniCompare = Input **Unicode** object for comparison
 * @retval "int" = Returns 1, 0, -1 if **i_pouniObject** value is less than,
 * equal to or greater than the **i_pouniCompare** value numerically
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniCompare is null
 */

int Unicode_compare_descendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare)
{
    long double l_ldObject = 0;
    long double l_ldCompare = 0;

    if (i_pouniObject == 0 || i_pouniCompare == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniCompare = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniCompare);
        abort();
    }
    if (i_pouniObject == i_pouniCompare
        || i_pouniObject->m_poszCodepoints == i_pouniCompare->m_poszCodepoints
        || (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints == 0))
    {
        return(0);
    }
    if (i_pouniObject->m_sizCodepoints == 0 && i_pouniCompare->m_sizCodepoints > 0)
    {
        return(1);
    }
    if (i_pouniObject->m_sizCodepoints > 0 && i_pouniCompare->m_sizCodepoints == 0)
    {
        return(-1);
    }
    l_ldObject = wcstold((wchar_t *) i_pouniObject->m_poszCodepoints, 0);
    l_ldCompare = wcstold((wchar_t *) i_pouniCompare->m_poszCodepoints, 0);
    if (l_ldObject < l_ldCompare) return(1);
    if (l_ldObject > l_ldCompare) return(-1);
    return(0);
}

/**
 * @fn "struct Unicode * Unicode_uppercase(const struct Unicode * i_pouniObject)"
 * @brief Constructs new uppercase version of **Unicode** object
 * @details When used with a **Unicode** object the return value is the current
 * **Unicode** object contents in uppercase.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniName = Unicode_from_string("jOÃO mÉROÇO", 0, "UTF8");
 * struct Unicode * l_pouniUpper = Unicode_uppercase(l_pouniName);
 * struct Unicode * l_pouniLower = Unicode_lowercase(l_pouniName);
 * struct Unicode * l_pouniSwap = Unicode_swapcase(l_pouniName);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @retval "struct Unicode *" = Pointer to new **Unicode** object with
 * uppercased codepoints
 * @exception abort(3) Aborts if i_pouniObject is null
 */

struct Unicode * Unicode_uppercase(const struct Unicode * i_pouniObject)
{
    wchar_t * l_powzObject = 0;
    struct Unicode * l_pouniUppercased = 0;
    wchar_t * l_powzUppercased = 0;
    size_t l_sizCount = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    l_pouniUppercased = Unicode_new();
    l_pouniUppercased->m_poszCodepoints = (char *) calloc(i_pouniObject->m_sizCodepoints + 1, sizeof(wchar_t));
    assert(l_pouniUppercased->m_poszCodepoints != 0);
    l_powzUppercased = (wchar_t *) l_pouniUppercased->m_poszCodepoints;
    l_pouniUppercased->m_sizCodepoints = 0;
    l_pouniUppercased->m_sizBytes = 0;
    while (l_sizCount < i_pouniObject->m_sizCodepoints) {
        l_powzUppercased[l_sizCount] = towupper(l_powzObject[l_sizCount]);
        l_sizCount++;
    }
    l_pouniUppercased->m_sizCodepoints = l_sizCount;
    l_pouniUppercased->m_sizBytes = l_sizCount * sizeof(wchar_t);
    return(l_pouniUppercased);
}

/**
 * @fn "struct Unicode * Unicode_lowercase(const struct Unicode * i_pouniObject)"
 * @brief Constructs new lowercase version of **Unicode** object
 * @details When used with a **Unicode** object the return value is the current
 * **Unicode** object contents in lowercase.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniName = Unicode_from_string("jOÃO mÉROÇO", 0, "UTF8");
 * struct Unicode * l_pouniUpper = Unicode_uppercase(l_pouniName);
 * struct Unicode * l_pouniLower = Unicode_lowercase(l_pouniName);
 * struct Unicode * l_pouniSwap = Unicode_swapcase(l_pouniName);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @retval "struct Unicode *" = Pointer to new **Unicode** object with
 * lowercased codepoints
 * @exception abort(3) Aborts if i_pouniObject is null
 */

struct Unicode * Unicode_lowercase(const struct Unicode * i_pouniObject)
{
    wchar_t * l_powzObject = 0;
    struct Unicode * l_pouniLowercased = 0;
    wchar_t * l_powzLowercased = 0;
    size_t l_sizCount = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    l_pouniLowercased = Unicode_new();
    l_pouniLowercased->m_poszCodepoints = (char *) calloc(i_pouniObject->m_sizCodepoints + 1, sizeof(wchar_t));
    assert(l_pouniLowercased->m_poszCodepoints != 0);
    l_powzLowercased = (wchar_t *) l_pouniLowercased->m_poszCodepoints;
    l_pouniLowercased->m_sizCodepoints = 0;
    l_pouniLowercased->m_sizBytes = 0;
    while (l_sizCount < i_pouniObject->m_sizCodepoints) {
        l_powzLowercased[l_sizCount] = towlower(l_powzObject[l_sizCount]);
        l_sizCount++;
    }
    l_pouniLowercased->m_sizCodepoints = l_sizCount;
    l_pouniLowercased->m_sizBytes = l_sizCount * sizeof(wchar_t);
    return(l_pouniLowercased);
}

/**
 * @fn "struct Unicode * Unicode_swapcase(const struct Unicode * i_pouniObject)"
 * @brief Constructs new swapped lettercase version of **Unicode** object
 * @details When used with a **Unicode** object the return value is the current
 * **Unicode** object contents lettercase swapped, with uppercase converted to
 * lowercase, and lowercase converted to uppercase. Codepoints with no case are
 * not affected.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniName = Unicode_from_string("jOÃO mÉROÇO", 0, "UTF8");
 * struct Unicode * l_pouniUpper = Unicode_uppercase(l_pouniName);
 * struct Unicode * l_pouniLower = Unicode_lowercase(l_pouniName);
 * struct Unicode * l_pouniSwap = Unicode_swapcase(l_pouniName);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @retval "struct Unicode *" = Pointer to new **Unicode** object with swapped
 * lettercase codepoints
 * @exception abort(3) Aborts if i_pouniObject is null
 */

struct Unicode * Unicode_swapcase(const struct Unicode * i_pouniObject)
{
    wchar_t * l_powzObject = 0;
    struct Unicode * l_pouniSwapcased = 0;
    wchar_t * l_powzSwapcased = 0;
    size_t l_sizCount = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    l_pouniSwapcased = Unicode_new();
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    l_pouniSwapcased->m_poszCodepoints = (char *) calloc(i_pouniObject->m_sizCodepoints + 1, sizeof(wchar_t));
    assert(l_pouniSwapcased->m_poszCodepoints != 0);
    l_powzSwapcased = (wchar_t *) l_pouniSwapcased->m_poszCodepoints;
    l_pouniSwapcased->m_sizCodepoints = 0;
    l_pouniSwapcased->m_sizBytes = 0;
    while (l_sizCount < i_pouniObject->m_sizCodepoints) {
        if (iswupper(l_powzObject[l_sizCount]))
            l_powzSwapcased[l_sizCount] = towlower(l_powzObject[l_sizCount]);
        else if (iswlower(l_powzObject[l_sizCount]))
            l_powzSwapcased[l_sizCount] = towupper(l_powzObject[l_sizCount]);
        else
            l_powzSwapcased[l_sizCount] = l_powzObject[l_sizCount];
        l_sizCount++;
    }
    l_pouniSwapcased->m_sizCodepoints = l_sizCount;
    l_pouniSwapcased->m_sizBytes = l_sizCount * sizeof(wchar_t);
    return(l_pouniSwapcased);
}

/**
 * @fn "struct Unicode * Unicode_concatenate(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniConcatenate)"
 * @brief Constructs new **Unicode** object from the concatenation of of one
 * **Unicode** object to another
 * @details Constructs and returns a new **Unicode** object whose value is the
 * concatenated content of the **i_pouniObject** parameter with the
 * **i_pouniConcatenate** parameter.  While similar in operation to
 * **Unicode_append()**, the content of the passed input parameters are not
 * modified.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniFirst = Unicode_from_string("João ", 0, "UTF8");
 * struct Unicode * l_pouniLast = Unicode_from_string("Méroço", 0, "UTF8");
 * struct Unicode * l_pouniName = Unicode_concatenate(l_pouniFirst, l_pouniLast);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to lvalue **Unicode** object
 * @param[in] i_pouniConcatenate = Input pointer to rvalue **Unicode** object
 * @retval "struct Unicode *" = Pointer to new **Unicode** object with
 * concatenated codepoints
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniConcatenate is null
 */

struct Unicode * Unicode_concatenate(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniConcatenate)
{
    struct Unicode * l_pouniResult = 0;
    char * l_poszCodepoints = 0;
    size_t l_sizCodepoints = 0;
    size_t l_sizBytes = 0;

    if (i_pouniObject == 0 || i_pouniConcatenate == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniConcatenate = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniConcatenate);
        abort();
    }
    l_pouniResult = Unicode_new();
    l_poszCodepoints = (char *) calloc(i_pouniObject->m_sizCodepoints + i_pouniConcatenate->m_sizCodepoints + 1, sizeof(wchar_t));
    assert(l_poszCodepoints != 0);
    l_sizCodepoints = i_pouniObject->m_sizCodepoints + i_pouniConcatenate->m_sizCodepoints;
    l_sizBytes = i_pouniObject->m_sizBytes + i_pouniConcatenate->m_sizBytes;
    if (i_pouniObject->m_sizBytes != 0)
        memcpy(l_poszCodepoints, i_pouniObject->m_poszCodepoints, i_pouniObject->m_sizBytes);
    memcpy(l_poszCodepoints + i_pouniObject->m_sizBytes, i_pouniConcatenate->m_poszCodepoints, i_pouniConcatenate->m_sizBytes);
    l_pouniResult->m_poszCodepoints = l_poszCodepoints;
    l_pouniResult->m_sizCodepoints = l_sizCodepoints;
    l_pouniResult->m_sizBytes = l_sizBytes;
    return(l_pouniResult);
}

/**
 * @fn "struct UnicodeArray * Unicode_split(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniDelimiters, int i_inTrim)"
 * @brief Splits a **Unicode** object codepoints into tokens using delimiters
 * @details The codepoints of the input parameter **i_pouniObject** are split
 * into tokens based on the delimiter codepoints passed in the input parameter
 * **i_pouniDelimiters**. The return value is a newly constructed
 * **UnicodeArray** object which contains all of the tokens split into
 * individual **Unicode** objects.  Delimiters are treated as single
 * codepoints, and more than one can be specified.  The input parameter
 * **i_inTrim** can be set to the following values. Values of 1 and 3 can be
 * used to prevent zero-length tokens.
 * @li 0 = Each delimiter is significant between tokens
 * @li 1 = Multiple delimiters are trimmed to one delimiter between tokens
 * @li 2 = Each delimiter is significant at the start, end and between tokens
 * @li 3 = Mulitple delimiters are trimmed to one delimiter at start, end and between tokens
 *
 * @note For **i_inTrim** modes of 1 and 3 only the first (leftmost) delimiter
 * in **i_pouniObject** will be kept and any other additional delimiters after
 * it will be trimmed when multiple codepoints are passed in **i_pouniDelimiters**.
 *
 * @note The codepoints are scanned from left-to-right, and the left-most part
 * will be returned in the lowest-indexed **UnicodeArray** element and so on.
 * The **Unicode** objects returned in the **UnicodeArray** object return
 * pointers to the actual codepoints stored in the **i_pouniObject** input
 * parameter. This means that no heap allocations are used to copy the data.
 * So you should not use **Unicode_clear()** or **Unicode_delete()** on the
 * **Unicode** objects contained inside the **UnicodeArray** object. Just call
 * the **UnicodeArray_delete()** function to delete the **UnicodeArray**
 * object. When deleting the orginal content that was split, use the
 * **Unicode_delete()** function after **UnicodeArray_delete()** so that the
 * pointers in the **Unicode** objects do not point to unallocated memory.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniObject = Unicode_from_string("The quick brown fox jumps over the cow.", 0, "ASCII");
 * struct Unicode * l_pouniDelimiters = Unicode_from_string(" .", 0, "ASCII");
 * struct UnicodeArray * l_pounaArray = Unicode_split(i_pouniObject, l_pouniDelimiters, 1);
 * int l_inIndex = 0;
 * while (l_inIndex < l_pounaArray->m_sizObjects) {
 *     printf("%d: %s\n", Unicode_export_string(&l_pounaArray->m_pouniObjects[l_inIndex], 0, "ASCII"));
 *     l_inIndex++;
 * }
 * UnicodeArray_delete(&l_pounaArray);  // deletes array of objects pointing to original content
 * Unicode_delete(&l_pouniObject);      // deletes the original content
 * Unicode_delete(&l_pouniDelimiters);  // deletes the delimiters object
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_pouniDelimiters = One or more codepoints used as delimiters
 * @param[in] i_inTrim = Delimiter processing mode value
 * @retval "struct UnicodeArray *" = Pointer to **UnicodeArray** object
 * containing a heap-allocated array of **Unicode** objects pointing to content
 * inside **i_pouniObject**
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniDelimiters is null
 * @exception assert(3) Aborts if **UnicodeArray_new()** call returns null
 */

struct UnicodeArray * Unicode_split(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniDelimiters, int i_inTrim)
{
    wchar_t * l_powzObjectbeg = 0;
    wchar_t * l_powzObjectend = 0;
    wchar_t * l_powzDelimitersbeg = 0;
    wchar_t * l_powzDelimitersend = 0;
    wchar_t * l_powzTokenbeg = 0;
    wchar_t * l_powzTokenend = 0;
    wchar_t * l_powzObjectpos = 0;
    wchar_t * l_powzDelimiterspos = 0;
    int l_inIsdelimiter = 0;
    int l_inWasdelimiter = 0;
    int l_inIntoken = 0;
    size_t l_sizTokens = 0;
    struct UnicodeArray * l_pounaTokens = 0;

    if (i_pouniObject == 0 || i_pouniDelimiters == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniDelimiters = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniDelimiters);
        abort();
    }
    if (Unicode_empty(i_pouniObject)) {
        l_pounaTokens = UnicodeArray_new(0);
        assert(l_pounaTokens != 0);
        return(l_pounaTokens);
    }
    if (Unicode_empty(i_pouniDelimiters)) {
        l_pounaTokens = UnicodeArray_new(1);
        assert(l_pounaTokens != 0);
        l_pounaTokens->m_pouniObjects[0] = *i_pouniObject;
        l_pounaTokens->m_sizObjects = 1;
        return(l_pounaTokens);
    }
    // compute number of tokens "l_sizTokens"
    l_powzObjectbeg = (wchar_t *) i_pouniObject->m_poszCodepoints;
    l_powzObjectend = (wchar_t *) i_pouniObject->m_poszCodepoints + i_pouniObject->m_sizCodepoints;
    l_powzDelimitersbeg = (wchar_t *) i_pouniDelimiters->m_poszCodepoints;
    l_powzDelimitersend = (wchar_t *) i_pouniDelimiters->m_poszCodepoints + i_pouniDelimiters->m_sizCodepoints;
    l_inWasdelimiter = 0;
    l_inIntoken = 0;
    for (l_powzObjectpos = l_powzObjectbeg; l_powzObjectpos <= l_powzObjectend; l_powzObjectpos++) {
        l_inIsdelimiter = 0;
        for (l_powzDelimiterspos = l_powzDelimitersbeg; l_powzDelimiterspos < l_powzDelimitersend; l_powzDelimiterspos++) {
            if (*l_powzObjectpos == *l_powzDelimiterspos) {
                l_inIsdelimiter = 1;
                break;
            }
        }
        // trim extraneous delimiters only between tokens
        if (i_inTrim == 1 && l_inIsdelimiter == 1 && (l_inWasdelimiter == 1 || l_powzObjectpos == l_powzObjectbeg) && l_powzObjectpos < l_powzObjectend) {
            continue;
        }
        // delimiter or end of text found indicating end of token
        if ((i_inTrim == 0 || (i_inTrim == 1 && l_inWasdelimiter == 0)) && (l_inIsdelimiter == 1 || l_powzObjectpos == l_powzObjectend)) {
            l_sizTokens++;
            l_inWasdelimiter = 1;
            continue;
        }
        // trim extraneous delimiters before, after and between tokens
        if (i_inTrim == 3 && l_inIsdelimiter == 1 && l_inWasdelimiter == 1) {
            continue;
        }
        // delimiter found indicating end of token (non-delimited tokens are ignored)
        if ((i_inTrim == 2 || i_inTrim == 3) && l_inIntoken == 1 && l_inIsdelimiter == 1) {
            l_sizTokens++;
            l_inWasdelimiter = 1;
            continue;
        }
        // delimiter found indicating beginning of token (non-delimited tokens are ignored)
        if ((i_inTrim == 2 || i_inTrim == 3) && l_inIsdelimiter == 1) {
            l_inIntoken = 1;
            l_inWasdelimiter = 1;
            continue;
        }
        // indicate "last" codepoint found is non-delimiter
        if (l_inIsdelimiter == 0) {
            l_inWasdelimiter = 0;
        }
    }
    // allocate memory for the returned UnicodeArray object
    l_pounaTokens = UnicodeArray_new(l_sizTokens);
    assert(l_pounaTokens != 0);
    // calculate each token's pointer, codepoints and bytes inside Unicode object and store in UnicodeArray object
    l_sizTokens = 0;
    l_powzTokenbeg = 0;
    l_powzTokenend = 0;
    l_inWasdelimiter = 0;
    l_inIntoken = 0;
    for (l_powzObjectpos = l_powzObjectbeg; l_powzObjectpos <= l_powzObjectend; l_powzObjectpos++) {
        l_inIsdelimiter = 0;
        for (l_powzDelimiterspos = l_powzDelimitersbeg; l_powzDelimiterspos < l_powzDelimitersend; l_powzDelimiterspos++) {
            if (*l_powzObjectpos == *l_powzDelimiterspos) {
                l_inIsdelimiter = 1;
                break;
            }
        }
        // trim extraneous delimiters only between tokens
        if (i_inTrim == 1 && l_inIsdelimiter == 1 && (l_inWasdelimiter == 1 || l_powzObjectpos == l_powzObjectbeg) && l_powzObjectpos < l_powzObjectend) {
            continue;
        }
        // delimiter or end of text found indicating end of token
        if ((i_inTrim == 0 || (i_inTrim == 1 && l_inWasdelimiter == 0)) && (l_inIsdelimiter == 1 || l_powzObjectpos == l_powzObjectend)) {
            if (l_powzTokenbeg == 0 && l_powzTokenend == 0)
                l_powzTokenbeg = l_powzObjectpos;
            if (l_powzTokenbeg != 0 && l_powzTokenend == 0)
                l_powzTokenend = l_powzObjectpos;
            if (l_powzTokenbeg != 0 && l_powzTokenend != 0) {
                l_pounaTokens->m_pouniObjects[l_sizTokens].m_poszCodepoints = (char *) l_powzTokenbeg;
                l_pounaTokens->m_pouniObjects[l_sizTokens].m_sizCodepoints = l_powzTokenend - l_powzTokenbeg;
                l_pounaTokens->m_pouniObjects[l_sizTokens].m_sizBytes = (l_powzTokenend - l_powzTokenbeg) * sizeof(wchar_t);
                l_pounaTokens->m_sizObjects = ++l_sizTokens;
                l_powzTokenbeg = 0;
                l_powzTokenend = 0;
            }
            l_inWasdelimiter = 1;
            continue;
        }
        // delimiter found indicating beginning of token
        if ((i_inTrim == 0 || i_inTrim == 1) && l_inIsdelimiter == 0 && l_powzTokenbeg == 0 && l_powzObjectpos < l_powzObjectend) {
            l_powzTokenbeg = l_powzObjectpos;
            l_inWasdelimiter = 0;
            continue;
        }
        // trim extraneous delimiters before, after and between tokens
        if (i_inTrim == 3 && l_inIsdelimiter == 1 && l_inWasdelimiter == 1) {
            continue;
        }
        // delimiter found indicating end of token (non-delimited tokens are ignored)
        if ((i_inTrim == 2 || i_inTrim == 3) && l_inIntoken == 1 && l_inIsdelimiter == 1) {
            if (l_powzTokenbeg == 0 && l_powzTokenend == 0)
                l_powzTokenbeg = l_powzObjectpos;
            if (l_powzTokenbeg != 0 && l_powzTokenend == 0)
                l_powzTokenend = l_powzObjectpos;
            if (l_powzTokenbeg != 0 && l_powzTokenend != 0) {
                l_pounaTokens->m_pouniObjects[l_sizTokens].m_poszCodepoints = (char *) l_powzTokenbeg;
                l_pounaTokens->m_pouniObjects[l_sizTokens].m_sizCodepoints = l_powzTokenend - l_powzTokenbeg;
                l_pounaTokens->m_pouniObjects[l_sizTokens].m_sizBytes = (l_powzTokenend - l_powzTokenbeg) * sizeof(wchar_t);
                l_pounaTokens->m_sizObjects = ++l_sizTokens;
                l_powzTokenbeg = 0;
                l_powzTokenend = 0;
            }
            l_inWasdelimiter = 1;
            continue;
        }
        // delimiter found indicating beginning of token (non-delimited tokens are ignored)
        if ((i_inTrim == 2 || i_inTrim == 3) && l_inIsdelimiter == 1) {
            l_inIntoken = 1;
            l_inWasdelimiter = 1;
            continue;
        }
        // non-delimiter found indicating beginning of token content (non-delimited tokens are ignored)
        if ((i_inTrim == 2 || i_inTrim == 3) && l_inIntoken == 1 && l_inIsdelimiter == 0 && l_powzTokenbeg == 0) {
            l_inWasdelimiter = 0;
            l_powzTokenbeg = l_powzObjectpos;
            continue;
        }
        // non-delimiter found
        if (l_inIsdelimiter == 0) {
            l_inWasdelimiter = 0;
        }
    }
    // return UnicodeArray object containing array of Unicode objects
    return(l_pounaTokens);
}

/**
 * @fn "struct Unicode * Unicode_join(struct UnicodeArray * i_pounaObject, const struct Unicode * i_pouniDelimiter, int i_inTrim)"
 * @brief Joins **UnicodeArray** object elements together separated by a delimiter
 * @details The codepoints of all the **Unicode** objects passed in the
 * **UnicodeArray** parameter are concatentated together and separated by the
 * delimiter codepoints passed in the **i_pouniDelimiter** parameter.  The
 * **Unicode** objects are all appended left-to-right.  The input parameter
 * **i_inTrim** can be set to the following values. Values of 1 and 3 can be
 * used to prevent zero-length tokens.
 * @li 0 = A delimiter will be put between all tokens
 * @li 1 = A delimiter will be put between all non-empty tokens
 * @li 2 = A delimiter will be put at the start, end and between all tokens
 * @li 3 = A delimiter will be put at the start, end and between all non-empty tokens
 *
 * If the **i_pouniDelimiter** parameter is empty, then the **Unicode**
 * objects' codepoints are simply concatenated together.  It is the
 * responsibility of the calling routine to free the memory of the returned
 * **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * struct UnicodeArray * l_pounaArray = UnicodeArray_new(101);
 * load_all_101_objects_into_array(l_pounaArray);
 * struct Unicode * l_pouniDelimiter = Unicode_from_string("|", 0, "ASCII");
 * struct Unicode * l_pouniObject = Unicode_join(l_pounaArray, l_pouniDelimiter, 1);
 * UnicodeArray_delete(&l_pounaArray);
 * Unicode_delete(&l_pouniDelimiter);
 * @endcode
 *
 * @param[in] i_pounaObject = Input pointer to a **UnicodeArray** object
 * @param[in] i_pouniDelimiter = Input delimiter **Unicode** codepoints
 * @param[in] i_inTrim = If non-zero ignores emtpy **Unicode** objects in **i_pounaObject**
 * @retval "struct Unicode *" = Heap allocated **Unicode** object with final value
 * of all **Unicode** objects concatenated together with the passed delimiter
 * @exception abort(3) Aborts if i_pounaObject is null
 * @exception abort(3) Aborts if i_pouniDelimiter is null
 * @exception assert(3) Aborts if calloc(3) call returns null
 */

struct Unicode * Unicode_join(struct UnicodeArray * i_pounaObject, const struct Unicode * i_pouniDelimiter, int i_inTrim)
{
    size_t l_sizCount = 0;
    size_t l_sizBytes = 0;
    size_t l_sizOffset = 0;
    struct Unicode * l_pouniObject = 0;

    if (i_pounaObject == 0 || i_pouniDelimiter == 0) {
        fprintf(stderr, "%s(%d) = i_pounaObject = %p, i_pouniDelimiter = %p\n",
            __FILE__, __LINE__, i_pounaObject, i_pouniDelimiter);
        abort();
    }
    // calculate space for initial delimiter
    if (i_inTrim == 2 || i_inTrim == 3) {
        l_sizBytes = i_pouniDelimiter->m_sizBytes;
    }
    // calculate total bytes used for content of Unicode objects plus delimiters
    for (l_sizCount = 0; l_sizCount < i_pounaObject->m_sizObjects; l_sizCount++) {
        if ((i_inTrim == 1 || i_inTrim == 3) && i_pounaObject->m_pouniObjects[l_sizCount].m_sizCodepoints == 0) {
            continue;
        }
        if (l_sizCount > 0) l_sizBytes += i_pouniDelimiter->m_sizBytes;
        l_sizBytes += i_pounaObject->m_pouniObjects[l_sizCount].m_sizBytes;
    }
    // calculate space for final delimiter
    if (i_inTrim == 2 || i_inTrim == 3) {
        l_sizBytes += i_pouniDelimiter->m_sizBytes;
    }
    // make sure size in bytes is on a **whcar_t** alignment
    if ((l_sizBytes % sizeof(wchar_t)) != 0) {
        l_sizBytes = (l_sizBytes + sizeof(wchar_t)) / sizeof(wchar_t) * sizeof(wchar_t);
    }
    // create new Unicode object to return joined results in
    l_pouniObject = Unicode_new();
    l_pouniObject->m_poszCodepoints = (char *) calloc(l_sizBytes + sizeof(wchar_t), sizeof(char));
    assert(l_pouniObject->m_poszCodepoints != 0);
    l_pouniObject->m_sizBytes = l_sizBytes;
    l_pouniObject->m_sizCodepoints = l_sizBytes / sizeof(wchar_t);
    // copy content for initial delimiter
    if (i_inTrim == 2 || i_inTrim == 3) {
        l_sizBytes = i_pouniDelimiter->m_sizBytes;
        memcpy(l_pouniObject->m_poszCodepoints + l_sizOffset, i_pouniDelimiter->m_poszCodepoints, l_sizBytes);
        l_sizOffset += l_sizBytes;
    }
    // copy content from UnicodeArray object
    for (l_sizCount = 0; l_sizCount < i_pounaObject->m_sizObjects; l_sizCount++) {
        if ((i_inTrim == 1 || i_inTrim == 3) && i_pounaObject->m_pouniObjects[l_sizCount].m_sizCodepoints == 0) {
            continue;
        }
        if (l_sizCount > 0) {
            l_sizBytes = i_pouniDelimiter->m_sizBytes;
            memcpy(l_pouniObject->m_poszCodepoints + l_sizOffset, i_pouniDelimiter->m_poszCodepoints, l_sizBytes);
            l_sizOffset += l_sizBytes;
        }
        l_sizBytes = i_pounaObject->m_pouniObjects[l_sizCount].m_sizBytes;
        memcpy(l_pouniObject->m_poszCodepoints + l_sizOffset, i_pounaObject->m_pouniObjects[l_sizCount].m_poszCodepoints, l_sizBytes);
        l_sizOffset += l_sizBytes;
    }
    // copy content for final delimiter
    if (i_inTrim == 2 || i_inTrim == 3) {
        l_sizBytes = i_pouniDelimiter->m_sizBytes;
        memcpy(l_pouniObject->m_poszCodepoints + l_sizOffset, i_pouniDelimiter->m_poszCodepoints, l_sizBytes);
        l_sizOffset += l_sizBytes;
    }
    return(l_pouniObject);
}

/**
 * @fn "char ** Unicode_from_array(const struct UnicodeArray * i_pounaObject)"
 * @brief Extracts dynamic array of C strings from **UnicodeArray** object
 * @details Extracts all **Unicode** objects contained in the **UnicodeArray**
 * object passed in the **i_pounaObject** input parameter. The return value is
 * a dynamic array of C strings that contain UTF8 encoded codepoints, with each
 * element corresponding to the same indexed element in the **UnicodeArray**
 * object.
 *
 * @note The returned dynamic array and all of it's C string elements are
 * allocated on the heap, so the calling routine must use **free(3)** to
 * deallocate the memory used by the C strings and then the dynamic array,
 * else a memory leak will occur.
 *
 * #### Example ####
 *
 * @code
 * struct Unicode * l_pouniDelimiters = Unicode_from_string(" ,*", 0, "ASCII");
 * struct Unicode * l_pouniObject = Unicode_from_string("The quick, brown fox jumps *over* the cow", 0, "ASCII");
 * struct UnicodeArray * l_pounaArray = Unicode_split(i_pouniObject, l_pouniDelimiters, 1);
 * char ** l_poposzWords = Unicode_from_array(l_pounaObject);
 * int l_inOffset = 0;
 * assert(strcmp(l_poposzWords[0], "The") == 0);
 * assert(strcmp(l_poposzWords[1], "quick") == 0);
 * assert(strcmp(l_poposzWords[2], "brown") == 0);
 * assert(strcmp(l_poposzWords[3], "fox") == 0);
 * assert(strcmp(l_poposzWords[4], "jumps") == 0);
 * assert(strcmp(l_poposzWords[5], "over") == 0);
 * assert(strcmp(l_poposzWords[6], "the") == 0);
 * assert(strcmp(l_poposzWords[7], "cow") == 0);
 * for (l_inOffset = 0; l_popoWords[l_inOffset] != 0; l_inOffset++) {
 *     free(l_poposzWords[l_inOffset];
 * }
 * free(l_poposzWords);
 * UnicodeArray_delete(&l_pounaObject);
 * Unicode_delete(&l_pouniObject);
 * Unicode_delete(&l_pouniDelimiters);
 * @endcode
 *
 * @param[in] i_pounaObject = Input pointer to **UnicodeArray** object
 * @retval "char **" = Pointer to dynamic heap-allocated array of pointers to
 * null-terminated C strings, with the end of list indicated by a null pointer.
 * @exception abort(3) Aborts if i_pounaObject is null
 * @exception abort(3) Aborts if calloc(3) returns a null pointer
 */

char ** Unicode_from_array(const struct UnicodeArray * i_pounaObject)
{
    char ** l_poposzStrings = 0;
    size_t l_sizCount = 0;
    size_t l_sizOffset = 0;

    if (i_pounaObject == 0) {
        fprintf(stderr, "%s(%d) = i_pounaObject = %p\n",
            __FILE__, __LINE__, i_pounaObject);
        abort();
    }
    l_sizCount = i_pounaObject->m_sizObjects;
    l_poposzStrings = (char **) calloc(l_sizCount + 1, sizeof(char *));
    assert(l_poposzStrings != 0);
    for (l_sizOffset = 0; l_sizOffset < l_sizCount; l_sizOffset++) {
        l_poposzStrings[l_sizOffset] = Unicode_export_string(&i_pounaObject->m_pouniObjects[l_sizOffset], 0, "UTF8");
    }
    return(l_poposzStrings);
}

/**
 * @fn "void Unicode_to_array(struct UnicodeArray * u_pounaObject, const char ** i_poposzValues)"
 * @brief Replaces **UnicodeArray** elements with dynamic C string array elements
 * @details The passed **UnicodeArray** object heap memory is deallocated, but
 * any existing **Unicode** member objects will not be deallocated and is
 * considered the responsibility of the calling routine. New **UnicodeArray**
 * object memory is then allocated on the heap.  The passed dynamic C string
 * array elements are then converted into new **Unicode** objects that are
 * referenced using the same element offsets inside the **UnicodeArray** object
 * as they are in the dynamic C string array. The **UnicodeArray** will be
 * dynamically resized as necessary to contain the new **Unicode** elements.
 * All of the returned **Unicode** object elements are newly allocated on the
 * heap. None of the passed dynamic C string array or it's elements are
 * modified or deallocated.
 *
 * @note This method does not deallocate any **Unicode** objects. It is the
 * responsibility of the calling routine to deallocate any existing **Unicode**
 * objects in the passed **UnicodeArray** object, as well as any newly returned
 * **Unicode** object allocated on the heap by this method.
 *
 * @note This routine calls the **UnicodeArray_delete()** method to delete the
 * old **UnicodeArray** object contents, and calls **UnicodeArray_new()** to
 * allocate new memory on the heap for it. Since it is being updated, the value
 * of the passed **UnicodeArray** object pointer is almost guaranteed to
 * change.
 *
 * @note It is important to keep in mind that **UnicodeArray** objects are
 * considered to be frame-like containers, and do not own their **Unicode**
 * object elements. It is the responsibility of the calling routine to take
 * ownership of any existing and new **Unicode** objects and deallocate them
 * when appropriate, or a memory leak may occur.
 *
 * #### Example ####
 *
 * @code
 * char * l_poposzStrings[] = { "The", "quick", "brown", "fox", "jumps", "over", "the", "cow", 0 };
 * struct UnicodeArray * l_pounaObject = UnicodeArray_new(1);
 * struct Unicode * l_pouniObject = 0;
 * int l_inOffset = 0;
 * Unicode_to_array(l_pounaObject, l_poposzStrings);
 * assert(l_pounaObject->m_sizObjects == 8);
 * for (l_inOffset = 0; l_inOffset < l_pounaObject->m_sizObjects; l_inOffset++) {
 *     l_pouniObject = Unicode_from_string(l_poposzStrings[l_inOffset], 0, "ASCII");
 *     assert(Unicode_compare_ascendingstring(l_pouniObject, &l_pounaObject->m_pouniObjects[l_inOffset]) == 0);
 *     Unicode_delete(&l_pouniObject);
 * }
 * UnicodeArray_delete(&l_pounaObject);
 * @endcode
 *
 * @param[in,out] u_pounaObject = Update pointer to existing **UnicodeArray** object
 * @param[in] i_poposzValues = Pointer to dynamic heap-allocated array of
 * pointers to null-terminated C strings, with the end of list indicated by a
 * null pointer
 * @retval "void" = None
 * @exception abort(3) Aborts if **u_pounaObject** or **i_poposzValues** is null
 * @exception assert(3) Aborts if UnicodeArray_new() call returns null
 */

void Unicode_to_array(struct UnicodeArray * u_pounaObject, const char ** i_poposzValues)
{
    struct Unicode * l_pouniObject = 0;
    size_t l_sizOffset = 0;
    size_t l_sizCount = 0;

    if (u_pounaObject == 0 || i_poposzValues == 0) {
        fprintf(stderr, "%s(%d) = u_pounaObject = %p, i_poposzValues = %p\n",
            __FILE__, __LINE__, u_pounaObject, i_poposzValues);
        abort();
    }
    // Count number of string values to be turned into elements
    l_sizCount = 0;
    while (i_poposzValues[l_sizCount] != 0) {
        l_sizCount++;
    }
    // Create new UnicodeArray content holding the strings converted to Unicode objects
    if (u_pounaObject->m_pouniObjects != 0) free(u_pounaObject->m_pouniObjects);
    u_pounaObject->m_pouniObjects = (struct Unicode *) calloc(l_sizCount, sizeof(struct Unicode));
    assert(u_pounaObject->m_pouniObjects != 0);
    u_pounaObject->m_sizObjects = l_sizCount;
    for (l_sizOffset = 0; l_sizOffset < l_sizCount; l_sizOffset++) {
        l_pouniObject = Unicode_from_string(i_poposzValues[l_sizOffset], 0, "UTF8");
        assert(l_pouniObject != 0);
        Unicode_swap(&u_pounaObject->m_pouniObjects[l_sizOffset], l_pouniObject);
    }
}

/**
 * @fn "char ** Unicode_from_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS)"
 * @brief Extracts all subvalues at the specified level and splits them into a
 * __char **__ dynamic array of C strings
 * @details Retrieves a subvalue from within the 4-dimensional dynamic array
 * stored within the current **Unicode** object, splits it's contents on the
 * next lower level subvalue, and returns the split tokens as a dynamic
 * null-terminated array of C strings. The implementation of the 4-dimensional
 * dynamic array inside the **Unicode** object is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive, zero or negative value. A positive value will
 * extract a specific subvalue at a specific level. A zero value returns all
 * subvalues at a specific level. A negative value will extract a subvalue
 * relative to the last one at the specified level. If a negative value has an
 * absolute value greater than the number of subvalues at the specified level,
 * then the first subvalue will be extracted. It is an error for a non-zero
 * index to follow (to be to the right in the method parameter list) any index
 * with a value of zero. The extracted subvalue is returned.
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple subvalues inside of a single **Unicode** value
 * using delimiters that are UNICODE safe.
 *
 * @note The calling routine is responsible for freeing the heap-allocated
 * memory that is returned. First, for each non-null list element pointer
 * **free()** should be called on each __char *__ pointer in the list.  Finally
 * **free()** should be called on the __char **__ list pointer.
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_inFS = Level 1 index of level 2 subvalues or zero
 * @param[in] i_inGS = Level 2 index of level 3 subvalues or zero
 * @param[in] i_inRS = Level 3 index of level 4 subvalues or zero
 * @retval "char **" = Pointer to dynamic heap-allocated array of pointers to
 * null-terminated C strings, with the end of list indicated by a null pointer.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if subvalue index is zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 */

char ** Unicode_from_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS)
{
    struct Unicode * l_pouniFS = 0;
    struct Unicode * l_pouniGS = 0;
    struct Unicode * l_pouniRS = 0;
    struct Unicode * l_pouniUS = 0;
    struct UnicodeArray * l_pounaResults = 0;
    struct UnicodeArray * l_pounaCount = 0;
    char ** l_poposzResults = 0;
    size_t l_sizCount = 0;
    size_t l_sizOffset = 0;
    size_t l_sizFS = 0;
    size_t l_sizGS = 0;
    size_t l_sizRS = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if ((i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS);
        abort();
    }
    if (Unicode_empty(i_pouniObject)) {
        l_poposzResults = (char **) calloc(1, sizeof(char *));
        assert(l_poposzResults != 0);
        return(l_poposzResults);
    }
    l_pouniFS = Unicode_from_string("\x1C", 1, "ASCII");
    l_pouniGS = Unicode_from_string("\x1D", 1, "ASCII");
    l_pouniRS = Unicode_from_string("\x1E", 1, "ASCII");
    l_pouniUS = Unicode_from_string("\x1F", 1, "ASCII");
    l_pounaResults = Unicode_split(i_pouniObject, l_pouniFS, 2);
    assert(l_pounaResults != 0);
    if (i_inFS != 0) {
        l_sizCount = l_pounaResults->m_sizObjects;
        if (i_inFS > 0) l_sizFS = i_inFS - 1;
        if (i_inFS < 0) l_sizFS = i_inFS + l_sizCount;
        if ((int) l_sizFS < 0) l_sizFS = 0;
        l_pounaCount = l_pounaResults;
        if (l_sizFS >= l_sizCount) {
            l_pounaResults = UnicodeArray_new(0);
            assert(l_pounaResults != 0);
        } else {
            l_pounaResults = Unicode_split(&l_pounaCount->m_pouniObjects[l_sizFS], l_pouniGS, 2);
            assert(l_pounaResults != 0);
        }
        UnicodeArray_delete(&l_pounaCount);
    }
    if (i_inGS != 0) {
        l_sizCount = l_pounaResults->m_sizObjects;
        if (i_inGS > 0) l_sizGS = i_inGS - 1;
        if (i_inGS < 0) l_sizGS = i_inGS + l_sizCount;
        if ((int) l_sizGS < 0) l_sizGS = 0;
        l_pounaCount = l_pounaResults;
        if (l_sizGS >= l_sizCount) {
            l_pounaResults = UnicodeArray_new(0);
            assert(l_pounaResults != 0);
        } else {
            l_pounaResults = Unicode_split(&l_pounaCount->m_pouniObjects[l_sizGS], l_pouniRS, 2);
            assert(l_pounaResults != 0);
        }
        UnicodeArray_delete(&l_pounaCount);
    }
    if (i_inRS != 0) {
        l_sizCount = l_pounaResults->m_sizObjects;
        if (i_inRS > 0) l_sizRS = i_inRS - 1;
        if (i_inRS < 0) l_sizRS = i_inRS + l_sizCount;
        if ((int) l_sizRS < 0) l_sizRS = 0;
        l_pounaCount = l_pounaResults;
        if (l_sizRS >= l_sizCount) {
            l_pounaResults = UnicodeArray_new(0);
            assert(l_pounaResults != 0);
        } else {
            l_pounaResults = Unicode_split(&l_pounaCount->m_pouniObjects[l_sizRS], l_pouniUS, 2);
            assert(l_pounaResults != 0);
        }
        UnicodeArray_delete(&l_pounaCount);
    }
    l_sizCount = l_pounaResults->m_sizObjects;
    l_poposzResults = (char **) calloc(l_sizCount + 1, sizeof(char *));
    assert(l_poposzResults != 0);
    for (l_sizOffset = 0; l_sizOffset < l_sizCount; l_sizOffset++) {
        l_poposzResults[l_sizOffset] = Unicode_export_string(&l_pounaResults->m_pouniObjects[l_sizOffset], 0, "UTF8");
    }
    UnicodeArray_delete(&l_pounaResults);
    Unicode_delete(&l_pouniFS);
    Unicode_delete(&l_pouniGS);
    Unicode_delete(&l_pouniRS);
    Unicode_delete(&l_pouniUS);
    return(l_poposzResults);
}

/**
 * @fn "void Unicode_to_subvalues(struct Unicode * u_pouniObject, const char ** i_poposzValues, int i_inFS, int i_inGS, int i_inRS)"
 * @brief Replaces all of the subvalues at a specified level with C strings
 * @details This replaces all of the subvalues one level below the right-most
 * non-zero passed subvalue index. For example, if **i_inFS** is zero, then all
 * of the level 1 subvalues (i.e. the entire content) of **u_pouniObject** will
 * get replaced by the passed C strings delimited by the level 1 delimiter
 * (FS). But if **i_inFS** is non-zero, then all of the level 2 subvalues within
 * the specified level 1 subvalue will be replaced delimited by the level 2
 * delimiter (GS), and so on.  Any existing subvalues within the 4-dimensional
 * dynamic array contained at the specified level within the passed **Unicode**
 * object will be deleted first. The implementation of the 4-dimensional
 * dynamic array is structured as follows:
 * - Object content contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive, zero or negative value. A positive value will
 * replace all lower-level subvalues at that specific index. A zero value will
 * replace subvalues at that specified level. A negative subvalue index value
 * is relative to the last one at the specified level. If a negative value has
 * an absolute value greater than the number of subvalues at the specified
 * level, then the first subvalue will be replaced. It is an error for a
 * non-zero index to follow (to be to the right in the method parameter list)
 * any index with a value of zero.
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple subvalues inside of a single **Unicode** value
 * using delimiters that are UNICODE safe.
 *
 * #### Example ####
 *
 * @code
 * char * l_poposzStrings[] = { "The", "quick", "brown", "fox", "jumps", "over", "the", "cow", 0 };
 * struct Unicode * l_pouniObject = Unicode_new();
 * Unicode_to_subvalues(l_pouniObject, l_poposzStrings, 0, 0, 0);
 * @endcode
 *
 * @param[in,out] u_pouniObject = Update pointer to existing **Unicode** object
 * @param[in] i_poposzValues = Pointer to dynamic heap-allocated array of
 * pointers to null-terminated C strings, with the end of list indicated by a
 * null pointer
 * @param[in] i_inFS = Level 1 index of level 2 subvalues or zero
 * @param[in] i_inGS = Level 2 index of level 3 subvalues or zero
 * @param[in] i_inRS = Level 3 index of level 4 subvalues or zero
 * @retval "void" = None
 * @exception abort(3) Aborts if **u_pouniObject** or **i_poposzValues** is null
 * @exception abort(3) Aborts if subvalue index is zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero
 * @exception assert(3) Aborts if UnicodeArray_new() call returns null
 * @exception assert(3) Aborts if Unicode_split() call returns null
 */

void Unicode_to_subvalues(struct Unicode * u_pouniObject, const char ** i_poposzValues, int i_inFS, int i_inGS, int i_inRS)
{
    struct UnicodeArray * l_pounaLevel1 = 0;
    struct UnicodeArray * l_pounaLevel2 = 0;
    struct UnicodeArray * l_pounaLevel3 = 0;
    struct UnicodeArray * l_pounaRealloc = 0;
    struct UnicodeArray * l_pounaSubvalues = 0;
    struct Unicode * l_pouniResult = 0;
    struct Unicode * l_pouniValue = 0;
    struct Unicode * l_pouniSubvalues = 0;
    struct Unicode * l_pouniFS = 0;
    struct Unicode * l_pouniGS = 0;
    struct Unicode * l_pouniRS = 0;
    struct Unicode * l_pouniUS = 0;
    size_t l_sizLevel1 = 0;
    size_t l_sizLevel2 = 0;
    size_t l_sizLevel3 = 0;
    size_t l_sizFS = 0;
    size_t l_sizGS = 0;
    size_t l_sizRS = 0;
    size_t l_sizOffset = 0;
    size_t l_sizCount = 0;

    if (u_pouniObject == 0 || i_poposzValues == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_poposzValues = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_poposzValues);
        abort();
    }
    if ((i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS);
        abort();
    }
    // Subvalue level 1 to 4 delimiter characters
    l_pouniFS = Unicode_from_string("\x1C", 1, "ASCII");
    l_pouniGS = Unicode_from_string("\x1D", 1, "ASCII");
    l_pouniRS = Unicode_from_string("\x1E", 1, "ASCII");
    l_pouniUS = Unicode_from_string("\x1F", 1, "ASCII");
    // Count number of string values to be turned into subvalues
    l_sizCount = 0;
    while (i_poposzValues[l_sizCount] != 0) {
        l_sizCount++;
    }
    // Create UnicodeArray object holding the strings converted to Unicode objects
    l_pounaSubvalues = UnicodeArray_new(l_sizCount);
    assert(l_pounaSubvalues != 0);
    for (l_sizOffset = 0; l_sizOffset < l_sizCount; l_sizOffset++) {
        l_pouniValue = Unicode_from_string(i_poposzValues[l_sizOffset], 0, "UTF8");
        assert(l_pouniValue != 0);
        Unicode_swap(&l_pounaSubvalues->m_pouniObjects[l_sizOffset], l_pouniValue);
    }
    // Replace complete contents of object with Level 1 delimited subvalues
    if (i_inFS == 0 && i_inGS == 0 && i_inRS == 0) {
        Unicode_delete(&u_pouniObject);
        u_pouniObject = Unicode_join(l_pounaSubvalues, l_pouniFS, 2);
        goto done;
    }
    // Create temporary Unicode dynamic array of Level 1 subvalues and resize as necessary
    if (i_inFS != 0) {
        l_pounaLevel1 = Unicode_split(u_pouniObject, l_pouniFS, 2);
        assert(l_pounaLevel1 != 0);
        l_sizLevel1 = l_pounaLevel1->m_sizObjects;
        if (i_inFS > 0) l_sizFS = i_inFS - 1;
        if (i_inFS < 0) l_sizFS = i_inFS + l_sizLevel1;
        if ((int) l_sizFS < 0) l_sizFS = 0;
        if (l_sizLevel1 <= l_sizFS) {
            l_pounaRealloc = l_pounaLevel1;
            l_pounaLevel1 = UnicodeArray_new(l_sizFS + 1);
            assert(l_pounaLevel1 != 0);
            for (l_sizOffset = 0; l_sizOffset < l_sizLevel1; l_sizOffset++) {
                Unicode_swap(&l_pounaLevel1->m_pouniObjects[l_sizOffset], &l_pounaRealloc->m_pouniObjects[l_sizOffset]);
            }
            l_sizLevel1 = l_sizFS + 1;
            UnicodeArray_delete(&l_pounaRealloc);
        }
    }
    // Create temporary Unicode dynamic array of Level 2 subvalues and resize as necessary
    if (i_inGS != 0) {
        l_pounaLevel2 = Unicode_split(&l_pounaLevel1->m_pouniObjects[l_sizFS], l_pouniGS, 2);
        assert(l_pounaLevel2 != 0);
        l_sizLevel2 = l_pounaLevel2->m_sizObjects;
        if (i_inGS > 0) l_sizGS = i_inGS - 1;
        if (i_inGS < 0) l_sizGS = i_inGS + l_sizLevel2;
        if ((int) l_sizGS < 0) l_sizGS = 0;
        if (l_sizLevel2 <= l_sizGS) {
            l_pounaRealloc = l_pounaLevel2;
            l_pounaLevel2 = UnicodeArray_new(l_sizGS + 1);
            assert(l_pounaLevel2 != 0);
            for (l_sizOffset = 0; l_sizOffset < l_sizLevel2; l_sizOffset++) {
                Unicode_swap(&l_pounaLevel2->m_pouniObjects[l_sizOffset], &l_pounaRealloc->m_pouniObjects[l_sizOffset]);
            }
            l_sizLevel2 = l_sizGS + 1;
            UnicodeArray_delete(&l_pounaRealloc);
        }
    }
    // Create temporary Unicode dynamic array of Level 3 subvalues and resize as necessary
    if (i_inRS != 0) {
        l_pounaLevel3 = Unicode_split(&l_pounaLevel2->m_pouniObjects[l_sizGS], l_pouniRS, 2);
        assert(l_pounaLevel3 != 0);
        l_sizLevel3 = l_pounaLevel3->m_sizObjects;
        if (i_inRS > 0) l_sizRS = i_inRS - 1;
        if (i_inRS < 0) l_sizRS = i_inRS + l_sizLevel3;
        if ((int) l_sizRS < 0) l_sizRS = 0;
        if (l_sizLevel3 <= l_sizRS) {
            l_pounaRealloc = l_pounaLevel3;
            l_pounaLevel3 = UnicodeArray_new(l_sizRS + 1);
            assert(l_pounaLevel3 != 0);
            for (l_sizOffset = 0; l_sizOffset < l_sizLevel3; l_sizOffset++) {
                Unicode_swap(&l_pounaLevel3->m_pouniObjects[l_sizOffset], &l_pounaRealloc->m_pouniObjects[l_sizOffset]);
            }
            l_sizLevel3 = l_sizRS + 1;
            UnicodeArray_delete(&l_pounaRealloc);
        }
    }
    //
    // Do not attempt to use Unicode_delete() or Unicode_clear() on any of the
    // UnicodeArray objects since Unicode_split() returned references to the
    // codepoints already existing inside u_pouniObject.
    //
    // Assemble C string values as level 1 Unicode object subvalues
    if (i_inFS != 0 && i_inGS == 0 && i_inRS == 0) {
        l_pouniSubvalues = Unicode_join(l_pounaSubvalues, l_pouniGS, 2);
        Unicode_swap(&l_pounaLevel1->m_pouniObjects[l_sizFS], l_pouniSubvalues);
        l_pouniResult = Unicode_join(l_pounaLevel1, l_pouniFS, 2);
        Unicode_swap(u_pouniObject, l_pouniResult);
        UnicodeArray_delete(&l_pounaLevel1);
        goto done;
    }
    // Assemble C string values as level 2 Unicode object subvalues
    if (i_inFS != 0 && i_inGS != 0 && i_inRS == 0) {
        l_pouniSubvalues = Unicode_join(l_pounaSubvalues, l_pouniRS, 2);
        Unicode_swap(&l_pounaLevel2->m_pouniObjects[l_sizGS], l_pouniSubvalues);
        l_pouniSubvalues = Unicode_join(l_pounaLevel2, l_pouniGS, 2);
        Unicode_swap(&l_pounaLevel1->m_pouniObjects[l_sizFS], l_pouniSubvalues);
        l_pouniResult = Unicode_join(l_pounaLevel1, l_pouniFS, 2);
        Unicode_swap(u_pouniObject, l_pouniResult);
        UnicodeArray_delete(&l_pounaLevel2);
        UnicodeArray_delete(&l_pounaLevel1);
        goto done;
    }
    // Assemble C string values as level 3 Unicode object subvalues
    if (i_inFS != 0 && i_inGS != 0 && i_inRS != 0) {
        l_pouniSubvalues = Unicode_join(l_pounaSubvalues, l_pouniUS, 2);
        Unicode_swap(&l_pounaLevel3->m_pouniObjects[l_sizRS], l_pouniSubvalues);
        l_pouniSubvalues = Unicode_join(l_pounaLevel3, l_pouniRS, 2);
        Unicode_swap(&l_pounaLevel2->m_pouniObjects[l_sizGS], l_pouniSubvalues);
        l_pouniSubvalues = Unicode_join(l_pounaLevel2, l_pouniGS, 2);
        Unicode_swap(&l_pounaLevel1->m_pouniObjects[l_sizFS], l_pouniSubvalues);
        l_pouniResult = Unicode_join(l_pounaLevel1, l_pouniFS, 2);
        Unicode_swap(l_pouniResult, u_pouniObject);
        UnicodeArray_delete(&l_pounaLevel3);
        UnicodeArray_delete(&l_pounaLevel2);
        UnicodeArray_delete(&l_pounaLevel1);
        goto done;
    }
done:
    UnicodeArray_delete(&l_pounaSubvalues);
    Unicode_delete(&l_pouniFS);
    Unicode_delete(&l_pouniGS);
    Unicode_delete(&l_pouniRS);
    Unicode_delete(&l_pouniUS);
}

/**
 * @fn "int Unicode_count_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS)"
 * @brief Count number of subvalues in a 4-dimensional dynamic array
 * @details Counts the next level of subvalues inside the specified subvalue
 * level of the 4-dimensional dynamic array stored within the **Unicode**
 * object. For example, that means that if the **i_inFS** level 1 index is the
 * only non-zero parameter, then the number of **GS** delimited level 2
 * subvalues in the level 1 subvalue passed in the **i_inFS** parameter. The
 * implementation of the dynamic array is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * The count is of subvalues one level deeper than the last non-zero index
 * specified.  Indicies can have a positive, zero or negative value. A positive
 * value will count subvalues one level deeper than the specified level. A zero
 * index is effectively ignored. A negative index is relative to the last
 * subvalue at the specified level. If a negative index has an absolute value
 * greater than the number of subvalues at the specified level, then the deeper
 * level subvalues contained in the first subvalue will be counted. It is an
 * error for a non-zero index to follow (to be to the right in the method
 * parameter list) any index with a value of zero. The subvalue count, not the
 * number of subvalue delimiters, is returned as the return value.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_count_subvalues(l_pouniObject, 0, 0, 0);@n
 *   Returns count of level 1 subvalues
 * @li Case 2. Unicode_count_subvalues(l_pouniObject, 3, 0, 0);@n
 *   Returns count of level 2 subvalues in third level 1 subvalue
 * @li Case 3. Unicode_count_subvalues(l_pouniObject, 3, 3, 0);@n
 *   Returns count of level 3 subvalues in third level 2 subvalue in third
 *   level 1 subvalue
 * @li Case 4. Unicode_count_subvalues(l_pouniObject, 3, 3, 3);@n
 *   Returns count of level 4 subvalues in third level 3 subvalue in third
 *   level 2 subvalue in third level 1 subvalue
 * @li Case 5. Unicode_count_subvalues(l_pouniObject, -1, 0, 0);@n
 *   Returns count of level 2 subvalues in last level 1 subvalue
 * @li Case 6. Unicode_count_subvalues(l_pouniObject, 0, 3, 0);@n
 *   Error! Non-zero indicies cannot follow zero indicies
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple subvalues inside of a single **Unicode**
 * value using delimiters that are UNICODE safe.
 *
 * @note It is an error for any subvalue index to be zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero. Error
 * will cause a **Exception** to be thrown.
 *
 * #### Example ####
 *
 * @code
 * int l_inCount = 0;
 * char * l_poposzStrings[] = { "The", "quick", "brown", "fox", "jumps", "over", "the", "cow", 0 };
 * struct Unicode * l_pouniObject = Unicode_new();
 * Unicode_to_subvalues(l_pouniObject, l_poposzStrings, 1, 0, 0);
 * l_inCount = Unicode_count_subvalues(l_pouniObject, 1, 0, 0);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_inFS = Index of level 1 subvalue, or ignored if zero
 * @param[in] i_inGS = Index of level 2 subvalue, or ignored if zero
 * @param[in] i_inRS = Index of level 3 subvalue, or ignored if zero
 * @retval int = Count of subvalues specified by the passed indicies
 * @exception abort(3) Aborts if u_pouniObject or i_poposzValues is null
 * @exception abort(3) Aborts if subvalue index is zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 */

int Unicode_count_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS)
{
    struct UnicodeArray * l_pounaSubvalues = 0;
    int l_arinIndex[4] = { i_inFS, i_inGS, i_inRS, 0 };
    const char * l_arposzDelimiters[4] = { "\x1c", "\x1d", "\x1e", "\x1f" };
    struct Unicode * l_pouniDelimiter = 0;
    struct Unicode l_uniSubvalues;
    int l_inLevel = 0;
    int l_inCount = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if ((i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS);
        abort();
    }
    // No data is modified since we are only counting objects and pointers only point to existing data
    l_uniSubvalues = *i_pouniObject;
    // Loop through each level of subvalue to refine the returned subvalue
    for (l_inLevel = 0; l_inLevel < 4; l_inLevel++) {
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
        l_pounaSubvalues = Unicode_split(&l_uniSubvalues, l_pouniDelimiter, 2);
        Unicode_delete(&l_pouniDelimiter);
        if (l_pounaSubvalues->m_sizObjects == 0) {
            l_inCount = 0;
            break;
        }
        // Check for supplied zero index from passed parameters
        if (l_arinIndex[l_inLevel] == 0 ) {
            l_inCount = l_pounaSubvalues->m_sizObjects;
            break;
        }
        // Recompute passed index to 0-based positive array index
        if (l_arinIndex[l_inLevel] > 0) l_arinIndex[l_inLevel]--;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] += l_pounaSubvalues->m_sizObjects;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] = 0;
        // Check for offset out of range of existing objects
        if ((size_t) l_arinIndex[l_inLevel] >= l_pounaSubvalues->m_sizObjects) {
            l_inCount = 0;
            break;
        }
        l_uniSubvalues = l_pounaSubvalues->m_pouniObjects[l_arinIndex[l_inLevel]];
        UnicodeArray_delete(&l_pounaSubvalues);
    }
    UnicodeArray_delete(&l_pounaSubvalues);
    return l_inCount;
}

/**
 * @fn "struct Unicode * Unicode_extract_subvalue(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)"
 * @brief Retrieve a subvalue from dynamic array with up to 4 dimensions
 * @details Retrieves a subvalue from within the 4-dimensional dynamic array
 * stored within the current **Unicode** object. The implementation of the dynamic
 * array is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive, zero or negative value. A positive value will
 * extract a specific subvalue at a specific level. A zero value returns all
 * subvalues at a specific level. A negative value will extract a subvalue
 * relative to the last one at the specified level. If a negative value has an
 * absolute value greater than the number of subvalues at the specified level,
 * then the first subvalue will be extracted. It is an error for a non-zero
 * index to follow (to be to the right in the method parameter list) any index
 * with a value of zero. The extracted subvalue is returned.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_extract_subvalue(3, 0, 0, 0);@n
 *   Returns third level 1 subvalue
 * @li Case 2. Unicode_extract_subvalue(3, 3, 0, 0);@n
 *   Returns third level 2 subvalue inside third level 1 subvalue
 * @li Case 3. Unicode_extract_subvalue(3, 3, 3, 0);@n
 *   Returns third level 3 subvalue inside third level 2 subvalue inside third
 *   level 1 subvalue
 * @li Case 4. Unicode_extract_subvalue(3, 3, 3, 3);@n
 *   Returns third level 4 subvalue inside third level 3 subvalue inside third
 *   level 2 subvalue inside third level 1 subvalue
 * @li Case 5. Unicode_extract_subvalue(-1, 0, 0, 0);@n
 *   Returns last level 1 subvalue
 * @li Case 6. Unicode_extract_subvalue(3, -1, 0, 0);@n
 *   Returns last level 2 subvalue  inside third level 1 subvalue
 * @li Case 7. Unicode_extract_subvalue(0, 3, 0, 0);@n
 *   Error! Positive indicies cannot follow zero indicies
 * @li Case 8. Unicode_extract_subvalue(0, 0, 0, 0);@n
 *   Returns entire content of **Unicode** object
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple subvalues inside of a single **Unicode** value
 * using delimiters that are UNICODE safe.
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_inFS = Index of level 1 subvalue, or zero for all level 1 to level 4 subvalues
 * @param[in] i_inGS = Index of level 2 subvalue, or zero for all level 2 to level 4 subvalues
 * @param[in] i_inRS = Index of level 3 subvalue, or zero for all level 3 to level 4 subvalues
 * @param[in] i_inUS = Index of level 4 subvalue, or zero for all level 4 subvalues
 * @retval "struct Unicode *" = Unicode subvalue that was extracted
 * @exception abort(3) Aborts if **i_pouniObject** is null
 * @exception abort(3) Aborts if a subvalue index is zero and any of the
 * indices following it (to the right in the method parameter list) are
 * non-zero.
 */

struct Unicode * Unicode_extract_subvalue(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)
{
    int l_arinIndex[4] = { i_inFS, i_inGS, i_inRS, i_inUS };
    const char * l_arposzDelimiters[4] = { "\x1c", "\x1d", "\x1e", "\x1f" };
    const char * l_poszAlldelimiters = { "\x1c\x1d\x1e\x1f" };
    struct UnicodeArray * l_pounaSubvalues = 0;
    struct Unicode * l_pouniSubvalue = 0;
    struct Unicode * l_pouniDelimiter = 0;
    struct Unicode l_uniSubvalues;
    int l_inLevel = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if ((i_inUS > 0 && (i_inFS == 0 || i_inGS == 0 || i_inRS == 0))
        || (i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d, i_inUS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS, i_inUS);
        abort();
    }
    // No data is modified since we are only extracting objects and pointers only point to existing data
    l_uniSubvalues = *i_pouniObject;
    // If all passed indicies are zero then just return any value with no subvalues
    if (i_inFS == 0) {
        l_pouniDelimiter = Unicode_from_string(l_poszAlldelimiters, 4, "ASCII");
        l_pounaSubvalues = Unicode_split(&l_uniSubvalues, l_pouniDelimiter, 0);
        Unicode_delete(&l_pouniDelimiter);
        l_pouniSubvalue = Unicode_new();
        if (l_pounaSubvalues->m_sizObjects > 0) {
            Unicode_copy(l_pouniSubvalue, &l_pounaSubvalues->m_pouniObjects[0]);
        }
        UnicodeArray_delete(&l_pounaSubvalues);
        goto done;
    }
    // Loop through each level of subvalue to refine the returned subvalue
    for (l_inLevel = 0; l_inLevel < 4; l_inLevel++) {
        // Check for supplied zero index from passed parameters
        if (l_arinIndex[l_inLevel] == 0 ) {
            l_pouniSubvalue = Unicode_new();
            Unicode_copy(l_pouniSubvalue, &l_uniSubvalues);
            goto done;
        }
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
        l_pounaSubvalues = Unicode_split(&l_uniSubvalues, l_pouniDelimiter, 2);
        Unicode_delete(&l_pouniDelimiter);
        if (l_pounaSubvalues->m_sizObjects == 0) {
            l_pouniSubvalue = Unicode_new();
            UnicodeArray_delete(&l_pounaSubvalues);
            goto done;
        }
        // Recompute passed index to 0-based positive array index
        if (l_arinIndex[l_inLevel] > 0) l_arinIndex[l_inLevel]--;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] += l_pounaSubvalues->m_sizObjects;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] = 0;
        // If out of range of existing objects return empty Unicode object
        if ((size_t) l_arinIndex[l_inLevel] >= l_pounaSubvalues->m_sizObjects) {
            l_pouniSubvalue = Unicode_new();
            goto done;
        }
        l_uniSubvalues = l_pounaSubvalues->m_pouniObjects[l_arinIndex[l_inLevel]];
        UnicodeArray_delete(&l_pounaSubvalues);
    }
    l_pouniSubvalue = Unicode_new();
    Unicode_copy(l_pouniSubvalue, &l_uniSubvalues);
done:
    return(l_pouniSubvalue);
}

/**
 * @fn "void Unicode_replace_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniReplace, int i_inFS, int i_inGS, int i_inRS, int i_inUS)"
 * @brief Replace a subvalue in a dynamic array with up to 4 dimensions
 * @details Replaces a subvalue within the 4-dimensional dynamic array stored
 * within the **Unicode** object. The implementation of the dynamic array is
 * structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive, zero or negative value. A positive value will
 * replace a specific subvalue at a specific level. A zero value is effectively
 * ignored. A negative value will replace a subvalue relative to the last one
 * at the specified level. If a negative value has an absolute value greater
 * than the number of subvalues at the specified level, then the first subvalue
 * will be replaced. It is an error for a non-zero index to follow (to be to
 * the right in the method parameter list) any index with a value of zero. The
 * value of the **i_pouniReplace** input parameter replaces the subvalue
 * specified by the indicies.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, 0, 0, 0, 0);@n
 *   Replaces entire object content with l_pouniReplace
 * @li Case 2. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, 3, 0, 0, 0);@n
 *   Replaces third level 1 subvalue by l_pouniReplace
 * @li Case 3. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, 3, 3, 0, 0);@n
 *   Replaces third level 2 subvalue inside third level 1 subvalue
 * @li Case 4. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, 3, 3, 3, 0);@n
 *   Replaces third level 3 subvalue inside third level 2 subvalue inside third
 *   level 1 subvalue
 * @li Case 5. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, 3, 3, 3, 3);@n
 *   Replaces third level 4 subvalue inside third level 3 subvalue inside third
 *   level 2 subvalue inside third level 1 subvalue
 * @li Case 6. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, -1, 0, 0, 0);@n
 *   Replaces last level 1 subvalue by l_pouniReplace
 * @li Case 7. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, -3, -3, 0, 0);@n
 *   Replaces third-from-last level 2 subvalue inside third-from-last level 3 subvalue
 * @li Case 8. Unicode_replace_subvalue(l_pouniObject, l_pouniReplace, 0, 3, -3, 0);@n
 *   Error! Non-zero indicies cannot follow zero indicies
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple dimensions of subvalues inside of a single
 * **Unicode** value using delimiters that are UNICODE safe.
 *
 * @note Any negative index beyond the first subvalue will be set to the first
 * subvalue at that level. Any index supplied beyond the number of subvalues at
 * that level will cause sufficient empty subvalues to be appended at that
 * level in order to accomodate the specified subvalue index.
 *
 * @note It is an error for any subvalue index to be zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 *
 * @param[in,out] u_pouniObject = Update pointer to existing **Unicode** object
 * @param[in] i_pouniReplace = Unicode subvalue replacement value
 * @param[in] i_inFS = Index of level 1 subvalue, or zero for all level 1 subvalues
 * @param[in] i_inGS = Index of level 2 subvalue, or zero for all level 2 subvalues
 * @param[in] i_inRS = Index of level 3 subvalue, or zero for all level 3 subvalues
 * @param[in] i_inUS = Index of level 4 subvalue, or zero for all level 4 subvalues
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject or i_pouniReplace is null
 * @exception abort(3) Aborts if a subvalue index is zero and any of the
 * indices following it (to the right in the method parameter list) are
 * non-zero.
 * @exception assert(3) Aborts if Unicode_split() call returns null
 * @exception assert(3) Aborts if UnicodeArray_new() call returns null
 */

void Unicode_replace_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniReplace, int i_inFS, int i_inGS, int i_inRS, int i_inUS)
{
    struct UnicodeArray * l_arpounaLevel[4] = { 0, 0, 0, 0 };
    int l_arinLevel[4] = { 0, 0, 0, 0 };
    const char * l_arposzDelimiters[4] = { "\x1c", "\x1d", "\x1e", "\x1f" };
    int l_arinParam[4] = { i_inFS, i_inGS, i_inRS, i_inUS };
    int l_arinIndex[4] = { 0, 0, 0, 0 };
    struct UnicodeArray * l_pounaSubvalues = 0;
    struct UnicodeArray * l_pounaRealloc = 0;
    struct Unicode * l_pouniSubvalues = 0;
    struct Unicode * l_pouniDelimiter = 0;
    int l_inLevel = 0;
    int l_inOffset = 0;

    if (u_pouniObject == 0 || i_pouniReplace == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_pouniReplace = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_pouniReplace);
        abort();
    }
    if ((i_inUS > 0 && (i_inFS == 0 || i_inGS == 0 || i_inRS == 0))
        || (i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d, i_inUS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS, i_inUS);
        abort();
    }
    // If all indicies are zero then replace only the value of the Unicode object but do not affect any subvalues
    if (i_inFS == 0 && i_inGS == 0 && i_inRS == 0 && i_inUS == 0) {
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[0], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_pounaSubvalues = Unicode_split(u_pouniObject, l_pouniDelimiter, 0);
        assert(l_pounaSubvalues != 0);
        if (l_pounaSubvalues->m_sizObjects == 0) {
            l_pouniSubvalues = Unicode_new();
            Unicode_copy(l_pouniSubvalues, i_pouniReplace);
        } else {
            l_pounaSubvalues->m_pouniObjects[0] = *i_pouniReplace;
            l_pouniSubvalues = Unicode_join(l_pounaSubvalues, l_pouniDelimiter, 0);
        }
        UnicodeArray_delete(&l_pounaSubvalues);
        goto done;
    }
    // No data is modified for now so l_pouniSubvalues acts as a reference to the existing data inside the update object
    l_pouniSubvalues = Unicode_new();
    *l_pouniSubvalues = *u_pouniObject;
    // Loop through each level of subvalue to refine the returned subvalue
    for (l_inLevel = 0; l_inLevel < 4; l_inLevel++) {
        // Check for supplied zero index from passed parameters
        if (l_arinParam[l_inLevel] == 0 ) {
            break;
        }
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_arpounaLevel[l_inLevel] = Unicode_split(l_pouniSubvalues, l_pouniDelimiter, 2);
        assert(l_arpounaLevel[l_inLevel] != 0);
        Unicode_delete(&l_pouniDelimiter);
        l_arinLevel[l_inLevel] = l_arpounaLevel[l_inLevel]->m_sizObjects;
        // Recompute passed index to 0-based positive array index
        l_arinIndex[l_inLevel] = l_arinParam[l_inLevel];
        if (l_arinIndex[l_inLevel] > 0) l_arinIndex[l_inLevel]--;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] += l_arinLevel[l_inLevel];
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] = 0;
        // if number of elements is less than or equal to 0-based positive offset then add more elements
        if (l_arinLevel[l_inLevel] <= l_arinIndex[l_inLevel]) {
            l_pounaRealloc = UnicodeArray_new(l_arinIndex[l_inLevel] + 1);
            assert(l_pounaRealloc != 0);
            for (l_inOffset = 0; l_inOffset < l_arinLevel[l_inLevel]; l_inOffset++) {
                Unicode_swap(&l_pounaRealloc->m_pouniObjects[l_inOffset], &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset]);
            }
            l_arinLevel[l_inLevel] = l_arinIndex[l_inLevel] + 1;
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
            l_arpounaLevel[l_inLevel] = l_pounaRealloc;
            l_pounaRealloc = 0;
        }
        *l_pouniSubvalues = l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]];
    }
    // Replace replace object at appropriate level and join into higher subvalue levels
    *l_pouniSubvalues = *i_pouniReplace;
    // Put replace object content within appropriate delimited subvalues
    for (l_inLevel = 3; l_inLevel >= 0; l_inLevel--) {
        if (l_arinParam[l_inLevel] != 0) {
            Unicode_swap(l_pouniSubvalues, &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]]);
            l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
            l_pouniSubvalues = Unicode_join(l_arpounaLevel[l_inLevel], l_pouniDelimiter, 2);
            Unicode_delete(&l_pouniDelimiter);
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
        }
    }
done:
    // Update Unicode object with final result
    Unicode_swap(l_pouniSubvalues, u_pouniObject);
    Unicode_delete(&l_pouniSubvalues);
}

/**
 * @fn "void Unicode_insert_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniInsert, int i_inFS, int i_inGS, int i_inRS, int i_inUS)"
 * @brief Insert a subvalue before another subvalue in a dynamic array with up to 4 dimensions
 * @details Inserts a subvalue before another subvalue within the 4-dimensional
 * dynamic array stored within the **Unicode** object. The implementation of the
 * dynamic array is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive, zero or negative value. A positive value will
 * insert a specific subvalue at a specific level. A zero value is effectively
 * ignored. A negative value will insert a subvalue relative to the last one at
 * the specified level. If a negative value has an absolute value greater than
 * the number of subvalues at the specified level, then the insertion will be
 * done before the first subvalue.  It is an error for a non-zero index to
 * follow (to be to the right in the method parameter list) any index with a
 * value of zero. The value of the **i_pouniInsert** input parameter inserts the
 * subvalue before the subvalue specified by the indicies.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, 0, 0, 0, 0);@n
 *   Prepends subvalue to beginning of existing content
 * @li Case 2. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, 3, 0, 0, 0);@n
 *   Inserts subvalue before third level 1 subvalue
 * @li Case 3. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, 3, 3, 0, 0);@n
 *   Inserts subvalue before third level 2 subvalue inside third level 1 subvalue
 * @li Case 4. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, 3, 3, 3, 0);@n
 *   Inserts subvalue before third level 3 subvalue inside third level 2 subvalue
 *   inside third level 1 subvalue
 * @li Case 5. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, 3, 3, 3, 3);@n
 *   Inserts subvalue before third level 4 subvalue inside third level 3 subvalue
 *   inside third level 2 subvalue inside third level 1 subvalue
 * @li Case 6. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, -1, 0, 0, 0);@n
 *   Inserts subvalue before last level 1 subvalue
 * @li Case 7. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, -3, -3, 0, 0);@n
 *   Inserts subvalue before third-from-last level 2 subvalue inside third-from-last
 *   level 3 subvalue
 * @li Case 8. Unicode_insert_subvalue(l_pouniObject, l_pouniInsert, 0, 3, -3, 0);@n
 *   Error! Non-zero indicies cannot follow zero indicies
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple dimensions of subvalues inside of a single
 * **Unicode** value using delimiters that are UNICODE safe.
 *
 * @note Any negative index beyond the first subvalue will be set to the first
 * subvalue at that level. Any index supplied beyond the number of subvalues at
 * that level will cause sufficient empty subvalues to be appended at that
 * level in order to accomodate the specified subvalue index.
 *
 * @note It is an error for any subvalue index to be zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 *
 * @param[in,out] u_pouniObject = Update pointer to existing **Unicode** object
 * @param[in] i_pouniInsert = Unicode subvalue insertion value
 * @param[in] i_inFS = Index of level 1 subvalue, or zero for all level 1 subvalues
 * @param[in] i_inGS = Index of level 2 subvalue, or zero for all level 2 subvalues
 * @param[in] i_inRS = Index of level 3 subvalue, or zero for all level 3 subvalues
 * @param[in] i_inUS = Index of level 4 subvalue, or zero for all level 4 subvalues
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject or i_pouniInsert is null
 * @exception abort(3) Aborts if a subvalue index is zero and any of the
 * indices following it (to the right in the method parameter list) are
 * non-zero.
 */

void Unicode_insert_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniInsert, int i_inFS, int i_inGS, int i_inRS, int i_inUS)
{
    struct UnicodeArray * l_arpounaLevel[4] = { 0, 0, 0, 0 };
    int l_arinLevel[4] = { 0, 0, 0, 0 };
    const char * l_arposzDelimiters[4] = { "\x1c", "\x1d", "\x1e", "\x1f" };
    int l_arinParam[4] = { i_inFS, i_inGS, i_inRS, i_inUS };
    int l_arinIndex[4] = { 0, 0, 0, 0 };
    struct UnicodeArray * l_pounaSubvalues = 0;
    struct UnicodeArray * l_pounaRealloc = 0;
    struct Unicode * l_pouniValue = 0;
    struct Unicode * l_pouniSubvalues = 0;
    struct Unicode * l_pouniDelimiter = 0;
    int l_inLevel = 0;
    int l_inOffset = 0;
    int l_inInsertlevel = 0;

    if (u_pouniObject == 0 || i_pouniInsert == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_pouniInsert = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_pouniInsert);
        abort();
    }
    if ((i_inUS > 0 && (i_inFS == 0 || i_inGS == 0 || i_inRS == 0))
        || (i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d, i_inUS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS, i_inUS);
        abort();
    }
    // Determine subvalue level where insertion operation will be performed
    // If all indicies are zero then prepend to existing Unicode object value
    if (i_inUS != 0) {
        l_inInsertlevel = 3;
    } else if (i_inRS != 0) {
        l_inInsertlevel = 2;
    } else if (i_inGS != 0) {
        l_inInsertlevel = 1;
    } else if (i_inFS != 0) {
        l_inInsertlevel = 0;
    } else {
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[0], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_pounaSubvalues = Unicode_split(u_pouniObject, l_pouniDelimiter, 0);
        assert(l_pounaSubvalues != 0);
        if (l_pounaSubvalues->m_sizObjects == 0) {
            l_pouniValue = Unicode_new();
            assert(l_pouniValue != 0);
            Unicode_copy(l_pouniValue, i_pouniInsert);
            UnicodeArray_delete(&l_pounaSubvalues);
            l_pounaSubvalues = UnicodeArray_new(1);
            assert(l_pounaSubvalues != 0);
        } else {
            l_pouniValue = Unicode_new();
            Unicode_copy(l_pouniValue, &l_pounaSubvalues->m_pouniObjects[0]);
            Unicode_replace(l_pouniValue, i_pouniInsert, 0, 0);
        }
        l_pounaSubvalues->m_pouniObjects[0] = *l_pouniValue;
        l_pouniSubvalues = Unicode_join(l_pounaSubvalues, l_pouniDelimiter, 0);
        Unicode_delete(&l_pouniValue);
        Unicode_delete(&l_pouniDelimiter);
        UnicodeArray_delete(&l_pounaSubvalues);
        goto done;
    }
    // No data is modified for now so l_pouniSubvalues acts as a reference to the existing data inside the update object
    l_pouniSubvalues = Unicode_new();
    *l_pouniSubvalues = *u_pouniObject;
    // Loop through each level of subvalue to refine the returned subvalue
    for (l_inLevel = 0; l_inLevel < 4; l_inLevel++) {
        // Check for supplied zero index from passed parameters
        if (l_arinParam[l_inLevel] == 0 ) {
            break;
        }
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_arpounaLevel[l_inLevel] = Unicode_split(l_pouniSubvalues, l_pouniDelimiter, 2);
        assert(l_arpounaLevel[l_inLevel] != 0);
        Unicode_delete(&l_pouniDelimiter);
        l_arinLevel[l_inLevel] = l_arpounaLevel[l_inLevel]->m_sizObjects;
        // Recompute passed index to 0-based positive array index
        l_arinIndex[l_inLevel] = l_arinParam[l_inLevel];
        if (l_arinIndex[l_inLevel] > 0) l_arinIndex[l_inLevel]--;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] += l_arinLevel[l_inLevel];
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] = 0;
        // if number of elements is less than or equal to 0-based positive offset then add more elements
        if (l_arinLevel[l_inLevel] <= l_arinIndex[l_inLevel]) {
            l_pounaRealloc = UnicodeArray_new(l_arinIndex[l_inLevel] + 1);
            assert(l_pounaRealloc != 0);
            for (l_inOffset = 0; l_inOffset < l_arinLevel[l_inLevel]; l_inOffset++) {
                Unicode_swap(&l_pounaRealloc->m_pouniObjects[l_inOffset], &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset]);
            }
            l_arinLevel[l_inLevel] = l_arinIndex[l_inLevel] + 1;
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
            l_arpounaLevel[l_inLevel] = l_pounaRealloc;
            l_pounaRealloc = 0;
        }
        // If insert level then insert subvalue into Unicode object array
        if (l_inLevel == l_inInsertlevel) {
            l_arinLevel[l_inLevel]++;
            l_pounaRealloc = UnicodeArray_new(l_arinLevel[l_inLevel]);
            assert(l_pounaRealloc != 0);
            for (l_inOffset = 0; l_inOffset < l_arinLevel[l_inLevel] - 1; l_inOffset++) {
                Unicode_swap(&l_pounaRealloc->m_pouniObjects[l_inOffset], &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset]);
            }
            for (l_inOffset = l_arinLevel[l_inLevel] - 1; l_inOffset > l_arinIndex[l_inLevel]; l_inOffset--) {
                l_pounaRealloc->m_pouniObjects[l_inOffset].m_poszCodepoints = l_pounaRealloc->m_pouniObjects[l_inOffset - 1].m_poszCodepoints;
                l_pounaRealloc->m_pouniObjects[l_inOffset].m_sizCodepoints = l_pounaRealloc->m_pouniObjects[l_inOffset - 1].m_sizCodepoints;
                l_pounaRealloc->m_pouniObjects[l_inOffset].m_sizBytes = l_pounaRealloc->m_pouniObjects[l_inOffset - 1].m_sizBytes;
            }
            l_pounaRealloc->m_pouniObjects[l_arinIndex[l_inLevel]].m_poszCodepoints = i_pouniInsert->m_poszCodepoints;
            l_pounaRealloc->m_pouniObjects[l_arinIndex[l_inLevel]].m_sizCodepoints = i_pouniInsert->m_sizCodepoints;
            l_pounaRealloc->m_pouniObjects[l_arinIndex[l_inLevel]].m_sizBytes = i_pouniInsert->m_sizBytes;
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
            l_arpounaLevel[l_inLevel] = l_pounaRealloc;
            l_pounaRealloc = 0;
        }
        *l_pouniSubvalues = l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]];
    }
    // Put insert object content within appropriate delimited subvalues
    for (l_inLevel = 3; l_inLevel >= 0; l_inLevel--) {
        if (l_arinParam[l_inLevel] != 0) {
            if (l_inLevel != l_inInsertlevel) {
                Unicode_swap(l_pouniSubvalues, &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]]);
            }
            l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
            assert(l_pouniDelimiter != 0);
            l_pouniSubvalues = Unicode_join(l_arpounaLevel[l_inLevel], l_pouniDelimiter, 2);
            Unicode_delete(&l_pouniDelimiter);
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
        }
    }
done:
    // Update Unicode object with final result
    Unicode_swap(l_pouniSubvalues, u_pouniObject);
    Unicode_delete(&l_pouniSubvalues);
}

/**
 * @fn "void Unicode_append_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniAppend, int i_inFS, int i_inGS, int i_inRS, int i_inUS)"
 * @brief Append a subvalue after another subvalue in a dynamic array with up to 4 dimensions
 * @details Appends a subvalue after another subvalue within the 4-dimensional
 * dynamic array stored within the **Unicode** object. The implementation of the
 * dynamic array is structured as follows:
 * @li Value contains level 1 subvalues delimited by FS characters
 * @li Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * @li Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * @li Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive, zero or negative value. A positive value will
 * append a specific subvalue at a specific level. A zero value is effectively
 * ignored. A negative value will append a subvalue relative to the last one at
 * the specified level. If a negative value has an absolute value greater than
 * the number of subvalues at the specified level, then the appendion will be
 * done after the first subvalue.  It is an error for a non-zero index to
 * follow (to be to the right in the method parameter list) any index with a
 * value of zero. The value of the **i_pouniAppend** input parameter appends the
 * subvalue after the subvalue specified by the indicies.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, 0, 0, 0, 0);@n
 *   Appends subvalue to end of existing content
 * @li Case 2. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, 3, 0, 0, 0);@n
 *   Appends subvalue after third level 1 subvalue
 * @li Case 3. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, 3, 3, 0, 0);@n
 *   Appends subvalue after third level 2 subvalue inside third level 1 subvalue
 * @li Case 4. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, 3, 3, 3, 0);@n
 *   Appends subvalue after third level 3 subvalue inside third level 2 subvalue
 *   inside third level 1 subvalue
 * @li Case 5. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, 3, 3, 3, 3);@n
 *   Appends subvalue after third level 4 subvalue inside third level 3 subvalue
 *   inside third level 2 subvalue inside third level 1 subvalue
 * @li Case 6. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, -1, 0, 0, 0);@n
 *   Appends subvalue after last level 1 subvalue
 * @li Case 7. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, -3, -3, 0, 0);@n
 *   Appends subvalue after third-from-last level 2 subvalue inside third-from-last
 *   level 3 subvalue
 * @li Case 8. Unicode_append_subvalue(l_pouniObject, l_pouniAppend, 0, 3, -3, 0);@n
 *   Error! Non-zero indicies cannot follow zero indicies
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple dimensions of subvalues inside of a single
 * **Unicode** value using delimiters that are UNICODE safe.
 *
 * @note Any negative index beyond the first subvalue will bet set to the first
 * subvalue at that level. Any index supplied beyond the number of subvalues at
 * that level will cause sufficient empty subvalues to be appended at that
 * level in order to accomodate the specified subvalue index.
 *
 * @note It is an error for any subvalue index to be zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 *
 * @param[in,out] u_pouniObject = Update pointer to existing **Unicode** object
 * @param[in] i_pouniAppend = Unicode subvalue append value
 * @param[in] i_inFS = Index of level 1 subvalue, or zero for all level 1 subvalues
 * @param[in] i_inGS = Index of level 2 subvalue, or zero for all level 2 subvalues
 * @param[in] i_inRS = Index of level 3 subvalue, or zero for all level 3 subvalues
 * @param[in] i_inUS = Index of level 4 subvalue, or zero for all level 4 subvalues
 * @retval "void" = None
 * @exception abort(3) Aborts if **u_pouniObject** or **i_pouniAppend** is null
 * @exception abort(3) Aborts if a subvalue index is zero and any of the
 * indices following it (to the right in the method parameter list) are
 * non-zero.
 */

void Unicode_append_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniAppend, int i_inFS, int i_inGS, int i_inRS, int i_inUS)
{
    struct UnicodeArray * l_arpounaLevel[4] = { 0, 0, 0, 0 };
    int l_arinLevel[4] = { 0, 0, 0, 0 };
    const char * l_arposzDelimiters[4] = { "\x1c", "\x1d", "\x1e", "\x1f" };
    int l_arinParam[4] = { i_inFS, i_inGS, i_inRS, i_inUS };
    int l_arinIndex[4] = { 0, 0, 0, 0 };
    struct UnicodeArray * l_pounaSubvalues = 0;
    struct UnicodeArray * l_pounaRealloc = 0;
    struct Unicode * l_pouniValue = 0;
    struct Unicode * l_pouniSubvalues = 0;
    struct Unicode * l_pouniDelimiter = 0;
    int l_inLevel = 0;
    int l_inOffset = 0;
    int l_inAppendlevel = 0;

    if (u_pouniObject == 0 || i_pouniAppend == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p, i_pouniAppend = %p\n",
            __FILE__, __LINE__, u_pouniObject, i_pouniAppend);
        abort();
    }
    if ((i_inUS > 0 && (i_inFS == 0 || i_inGS == 0 || i_inRS == 0))
        || (i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d, i_inUS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS, i_inUS);
        abort();
    }
    // Determine subvalue level where append operation will be performed
    // If all indicies are zero then append content to existing Unicode object
    if (i_inUS != 0) {
        l_inAppendlevel = 3;
    } else if (i_inRS != 0) {
        l_inAppendlevel = 2;
    } else if (i_inGS != 0) {
        l_inAppendlevel = 1;
    } else if (i_inFS != 0) {
        l_inAppendlevel = 0;
    } else {
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[0], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_pounaSubvalues = Unicode_split(u_pouniObject, l_pouniDelimiter, 0);
        assert(l_pounaSubvalues != 0);
        if (l_pounaSubvalues->m_sizObjects == 0) {
            l_pouniValue = Unicode_new();
            assert(l_pouniValue != 0);
            Unicode_copy(l_pouniValue, i_pouniAppend);
            UnicodeArray_delete(&l_pounaSubvalues);
            l_pounaSubvalues = UnicodeArray_new(1);
            assert(l_pounaSubvalues != 0);
        } else {
            l_pouniValue = Unicode_new();
            Unicode_copy(l_pouniValue, &l_pounaSubvalues->m_pouniObjects[0]);
            Unicode_append(l_pouniValue, i_pouniAppend);
        }
        l_pounaSubvalues->m_pouniObjects[0] = *l_pouniValue;
        l_pouniSubvalues = Unicode_join(l_pounaSubvalues, l_pouniDelimiter, 0);
        Unicode_delete(&l_pouniValue);
        Unicode_delete(&l_pouniDelimiter);
        UnicodeArray_delete(&l_pounaSubvalues);
        goto done;
    }
    // No data is modified for now so l_pouniSubvalues acts as a reference to the existing data inside the update object
    l_pouniSubvalues = Unicode_new();
    *l_pouniSubvalues = *u_pouniObject;
    // Loop through each level of subvalue to refine the returned subvalue
    for (l_inLevel = 0; l_inLevel < 4; l_inLevel++) {
        // Check for supplied zero index from passed parameters
        if (l_arinParam[l_inLevel] == 0 ) {
            break;
        }
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_arpounaLevel[l_inLevel] = Unicode_split(l_pouniSubvalues, l_pouniDelimiter, 2);
        assert(l_arpounaLevel[l_inLevel] != 0);
        Unicode_delete(&l_pouniDelimiter);
        l_arinLevel[l_inLevel] = l_arpounaLevel[l_inLevel]->m_sizObjects;
        // Recompute passed index to 0-based positive array index
        l_arinIndex[l_inLevel] = l_arinParam[l_inLevel];
        if (l_arinIndex[l_inLevel] > 0) l_arinIndex[l_inLevel]--;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] += l_arinLevel[l_inLevel];
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] = 0;
        // if number of elements is less than or equal to 0-based positive offset then add more elements
        if (l_arinLevel[l_inLevel] <= l_arinIndex[l_inLevel]) {
            l_pounaRealloc = UnicodeArray_new(l_arinIndex[l_inLevel] + 1);
            assert(l_pounaRealloc != 0);
            for (l_inOffset = 0; l_inOffset < l_arinLevel[l_inLevel]; l_inOffset++) {
                Unicode_swap(&l_pounaRealloc->m_pouniObjects[l_inOffset], &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset]);
            }
            l_arinLevel[l_inLevel] = l_arinIndex[l_inLevel] + 1;
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
            l_arpounaLevel[l_inLevel] = l_pounaRealloc;
            l_pounaRealloc = 0;
        }
        // If append level then append subvalue into Unicode object array
        if (l_inLevel == l_inAppendlevel) {
            l_arinLevel[l_inLevel]++;
            l_pounaRealloc = UnicodeArray_new(l_arinLevel[l_inLevel]);
            assert(l_pounaRealloc != 0);
            for (l_inOffset = 0; l_inOffset < l_arinLevel[l_inLevel] - 1; l_inOffset++) {
                Unicode_swap(&l_pounaRealloc->m_pouniObjects[l_inOffset], &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset]);
            }
            for (l_inOffset = l_arinLevel[l_inLevel] - 1; l_inOffset > l_arinIndex[l_inLevel] + 1; l_inOffset--) {
                l_pounaRealloc->m_pouniObjects[l_inOffset].m_poszCodepoints = l_pounaRealloc->m_pouniObjects[l_inOffset - 1].m_poszCodepoints;
                l_pounaRealloc->m_pouniObjects[l_inOffset].m_sizCodepoints = l_pounaRealloc->m_pouniObjects[l_inOffset - 1].m_sizCodepoints;
                l_pounaRealloc->m_pouniObjects[l_inOffset].m_sizBytes = l_pounaRealloc->m_pouniObjects[l_inOffset - 1].m_sizBytes;
            }
            l_pounaRealloc->m_pouniObjects[l_arinIndex[l_inLevel] + 1].m_poszCodepoints = i_pouniAppend->m_poszCodepoints;
            l_pounaRealloc->m_pouniObjects[l_arinIndex[l_inLevel] + 1].m_sizCodepoints = i_pouniAppend->m_sizCodepoints;
            l_pounaRealloc->m_pouniObjects[l_arinIndex[l_inLevel] + 1].m_sizBytes = i_pouniAppend->m_sizBytes;
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
            l_arpounaLevel[l_inLevel] = l_pounaRealloc;
            l_pounaRealloc = 0;
        }
        *l_pouniSubvalues = l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]];
    }
    // Put append object content within appropriate delimited subvalues
    for (l_inLevel = 3; l_inLevel >= 0; l_inLevel--) {
        if (l_arinParam[l_inLevel] != 0) {
            if (l_inLevel != l_inAppendlevel) {
                Unicode_swap(l_pouniSubvalues, &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]]);
            }
            l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
            assert(l_pouniDelimiter != 0);
            l_pouniSubvalues = Unicode_join(l_arpounaLevel[l_inLevel], l_pouniDelimiter, 2);
            Unicode_delete(&l_pouniDelimiter);
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
        }
    }
done:
    // Update Unicode object with final result
    Unicode_swap(l_pouniSubvalues, u_pouniObject);
    Unicode_delete(&l_pouniSubvalues);
}

/**
 * @fn "void Unicode_delete_subvalue(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)"
 * @brief Delete a subvalue in a dynamic array with up to 4 dimensions
 * @details Deletes a subvalue within the 4-dimensional dynamic array stored
 * within the **Unicode** object. The implementation of the dynamic array is
 * structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive, zero or negative value. A positive value will
 * delete a specific subvalue at a specific level. A zero value is effectively
 * ignored. A negative value will delete a subvalue relative to the last one at
 * the specified level. If a negative value has an absolute value greater than
 * the number of subvalues at the specified level, then the deletion will be
 * done to the specified subvalue.  It is an error for a non-zero index to
 * follow (to be to the right in the method parameter list) any index with a
 * value of zero.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_delete_subvalue(l_pouniObject, 0, 0, 0, 0);@n
 *   Deletes content of existing Unicode object like Unicode_clear()
 * @li Case 2. Unicode_delete_subvalue(l_pouniObject, 3, 0, 0, 0);@n
 *   Deletes third level 1 subvalue
 * @li Case 3. Unicode_delete_subvalue(l_pouniObject, 3, 3, 0, 0);@n
 *   Deletes third level 2 subvalue inside third level 1 subvalue
 * @li Case 4. Unicode_delete_subvalue(l_pouniObject, 3, 3, 3, 0);@n
 *   Deletes third level 3 subvalue inside third level 2 subvalue inside third
 *   level 1 subvalue
 * @li Case 5. Unicode_delete_subvalue(l_pouniObject, 3, 3, 3, 3);@n
 *   Deletes third level 4 subvalue inside third level 3 subvalue inside third
 *   level 2 subvalue inside third level 1 subvalue
 * @li Case 6. Unicode_delete_subvalue(l_pouniObject, -1, 0, 0, 0);@n
 *   Deletes last level 1 subvalue
 * @li Case 7. Unicode_delete_subvalue(l_pouniObject, -3, -3, 0, 0);@n
 *   Deletes third-from-last level 2 subvalue inside third-from-last level 3
 *   subvalue
 * @li Case 8. Unicode_delete_subvalue(l_pouniObject, 0, 3, -3, 0);@n
 *   Error! Non-zero indicies cannot follow zero indicies
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple dimensions of subvalues inside of a single
 * **Unicode** value using delimiters that are UNICODE safe.
 *
 * @note Any negative index beyond the first subvalue will be set to the first
 * subvalue at that level. Any index supplied beyond the number of subvalues at
 * that level will be ignored.
 *
 * @note It is an error for any subvalue index to be zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 *
 * @param[in,out] u_pouniObject = Update pointer to existing **Unicode** object
 * @param[in] i_inFS = Index of level 1 subvalue, or zero for all level 1 subvalues
 * @param[in] i_inGS = Index of level 2 subvalue, or zero for all level 2 subvalues
 * @param[in] i_inRS = Index of level 3 subvalue, or zero for all level 3 subvalues
 * @param[in] i_inUS = Index of level 4 subvalue, or zero for all level 4 subvalues
 * @retval "void" = None
 * @exception abort(3) Aborts if **u_pouniObject** is null
 * @exception abort(3) Aborts if a subvalue index is zero and any of the
 * indices following it (to the right in the method parameter list) are
 * non-zero.
 */

void Unicode_delete_subvalue(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)
{
    struct UnicodeArray * l_arpounaLevel[4] = { 0, 0, 0, 0 };
    int l_arinLevel[4] = { 0, 0, 0, 0 };
    const char * l_arposzDelimiters[4] = { "\x1c", "\x1d", "\x1e", "\x1f" };
    int l_arinParam[4] = { i_inFS, i_inGS, i_inRS, i_inUS };
    int l_arinIndex[4] = { 0, 0, 0, 0 };
    struct UnicodeArray * l_pounaSubvalues = 0;
    struct UnicodeArray * l_pounaRealloc = 0;
    struct Unicode * l_pouniSubvalues = 0;
    struct Unicode * l_pouniDelimiter = 0;
    int l_inLevel = 0;
    int l_inOffset = 0;
    int l_inDeletelevel = 0;

    if (u_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p\n",
            __FILE__, __LINE__, u_pouniObject);
        abort();
    }
    if ((i_inUS > 0 && (i_inFS == 0 || i_inGS == 0 || i_inRS == 0))
        || (i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d, i_inUS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS, i_inUS);
        abort();
    }
    // Determine subvalue level where deletion operation will be performed
    // If all indicies are zero then delete value and ignore subvalues
    if (i_inUS != 0) {
        l_inDeletelevel = 3;
    } else if (i_inRS != 0) {
        l_inDeletelevel = 2;
    } else if (i_inGS != 0) {
        l_inDeletelevel = 1;
    } else if (i_inFS != 0) {
        l_inDeletelevel = 0;
    } else {
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[0], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_pounaSubvalues = Unicode_split(u_pouniObject, l_pouniDelimiter, 0);
        assert(l_pounaSubvalues != 0);
        if (l_pounaSubvalues->m_sizObjects > 0) {
            l_pounaSubvalues->m_pouniObjects[0].m_poszCodepoints = 0;
            l_pounaSubvalues->m_pouniObjects[0].m_sizCodepoints = 0;
            l_pounaSubvalues->m_pouniObjects[0].m_sizBytes = 0;
        }
        l_pouniSubvalues = Unicode_join(l_pounaSubvalues, l_pouniDelimiter, 0);
        Unicode_delete(&l_pouniDelimiter);
        UnicodeArray_delete(&l_pounaSubvalues);
        goto done;
    }
    // No data is modified for now so l_pouniSubvalues acts as a reference to the existing data inside the update object
    l_pouniSubvalues = u_pouniObject;
    // Loop through each level of subvalue to refine the returned subvalue
    for (l_inLevel = 0; l_inLevel < 4; l_inLevel++) {
        // Check for supplied zero index from passed parameters
        if (l_arinParam[l_inLevel] == 0 ) {
            break;
        }
        l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
        assert(l_pouniDelimiter != 0);
        l_arpounaLevel[l_inLevel] = Unicode_split(l_pouniSubvalues, l_pouniDelimiter, 2);
        assert(l_arpounaLevel[l_inLevel] != 0);
        Unicode_delete(&l_pouniDelimiter);
        l_arinLevel[l_inLevel] = l_arpounaLevel[l_inLevel]->m_sizObjects;
        // Recompute passed index to 0-based positive array index
        l_arinIndex[l_inLevel] = l_arinParam[l_inLevel];
        if (l_arinIndex[l_inLevel] > 0) l_arinIndex[l_inLevel]--;
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] += l_arinLevel[l_inLevel];
        if (l_arinIndex[l_inLevel] < 0) l_arinIndex[l_inLevel] = 0;
        // if number of elements is less than or equal to 0-based positive offset then add more elements
        if (l_arinLevel[l_inLevel] <= l_arinIndex[l_inLevel]) {
            l_pounaRealloc = UnicodeArray_new(l_arinIndex[l_inLevel] + 1);
            assert(l_pounaRealloc != 0);
            for (l_inOffset = 0; l_inOffset < l_arinLevel[l_inLevel]; l_inOffset++) {
                Unicode_swap(&l_pounaRealloc->m_pouniObjects[l_inOffset], &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset]);
            }
            l_arinLevel[l_inLevel] = l_arinIndex[l_inLevel] + 1;
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
            l_arpounaLevel[l_inLevel] = l_pounaRealloc;
            l_pounaRealloc = 0;
        }
        // if delete level then delete subvalue in Unicode object array
        if (l_inLevel == l_inDeletelevel) {
            for (l_inOffset = l_arinIndex[l_inLevel]; l_inOffset < l_arinLevel[l_inLevel] - 1; l_inOffset++) {
                l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset] = l_arpounaLevel[l_inLevel]->m_pouniObjects[l_inOffset + 1];
            }
            l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinLevel[l_inLevel] - 1].m_poszCodepoints = 0;
            l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinLevel[l_inLevel] - 1].m_sizCodepoints = 0;
            l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinLevel[l_inLevel] - 1].m_sizBytes = 0;
            l_arpounaLevel[l_inLevel]->m_sizObjects--;
        }
        l_pouniSubvalues = &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]];
    }
    // Put delete object content within appropriate delimited subvalues
    for (l_inLevel = 3; l_inLevel >= 0; l_inLevel--) {
        if (l_arinParam[l_inLevel] != 0) {
            if (l_inLevel != l_inDeletelevel) {
                Unicode_swap(l_pouniSubvalues, &l_arpounaLevel[l_inLevel]->m_pouniObjects[l_arinIndex[l_inLevel]]);
            }
            l_pouniDelimiter = Unicode_from_string(l_arposzDelimiters[l_inLevel], 1, "ASCII");
            assert(l_pouniDelimiter != 0);
            l_pouniSubvalues = Unicode_join(l_arpounaLevel[l_inLevel], l_pouniDelimiter, 2);
            Unicode_delete(&l_pouniDelimiter);
            UnicodeArray_delete(&l_arpounaLevel[l_inLevel]);
        }
    }
done:
    // Update Unicode object with final result
    Unicode_swap(l_pouniSubvalues, u_pouniObject);
    Unicode_delete(&l_pouniSubvalues);
}

/**
 * @fn "void Unicode_sort_subvalues(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inSort)"
 * @brief Sort the subvalues at a specified level in a 4-dimensional dynamic array
 * @details Sorts the next deeper level of subvalues of the specified level
 * inside the 4-dimensional dynamic array stored within the **Unicode** object.
 * For example, that means that if the **i_inFS** level 1 index is the only
 * non-zero parameter, then the **GS** delimited level 2 subvalues inside the
 * level 1 subvalue will be sorted. The implementation of the dynamic array is
 * structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * The sort is performed on the subvalues one level deeper than the last
 * non-zero index specified.  Indicies can have a positive, zero or negative
 * value. A positive value will sort the subvalues one level deeper than the
 * specified level. A zero index is effectively ignored. A negative index is
 * relative to the last subvalue at the specified level. If a negative index
 * has an absolute value greater than the number of subvalues at the specified
 * level, then the deeper level subvalues contained in the first subvalue will
 * be sorted. It is an error for a non-zero index to follow (to be to the right
 * in the method parameter list) any index with a value of zero.
 *
 * The **i_inSort** parameter is required and can have a value from 0 (default)
 * to 3. Depending on it's value, the sorting will be performed using the
 * following comparison functions:
 * @li i_inSort == 0 - Unicode_compare_ascendingstring(i_pouniObject, i_pouniCompare)
 * @li i_inSort == 1 - Unicode_compare_descendingstring(i_pouniObject, i_pouniCompare)
 * @li i_inSort == 2 - Unicode_compare_ascendingnumeric(i_pouniObject, i_pouniCompare)
 * @li i_inSort == 3 - Unicode_compare_descendingnumeric(i_pouniObject, i_pouniCompare)
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple subvalues inside of a single **Unicode**
 * value using delimiters that are UNICODE safe.
 *
 * @note It is an error for any subvalue index to be zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero. Error
 * will cause a **Exception** to be thrown.
 *
 * @note When using the **Unicode_compare_ascendingnumeric()** or the
 * **Unicode_compare_descendingnumeric()** comparison functions each subvalue
 * is converted to a long double value and only those codepoints that are
 * considered part of the numeric value are used. For example, "1000-gt1" would
 * be equal to "1000.0" in the comparison.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_sort_subvalues(l_pouniObject, 0, 0, 0, 0);@n
 *   Returns sort of level 1 subvalues
 * @li Case 2. Unicode_sort_subvalues(l_pouniObject, 3, 0, 0, 0);@n
 *   Returns sort of level 2 subvalues in third level 1 subvalue
 * @li Case 3. Unicode_sort_subvalues(l_pouniObject, 3, 3, 0, 0);@n
 *   Returns sort of level 3 subvalues in third level 2 subvalue in third
 *   level 1 subvalue
 * @li Case 4. Unicode_sort_subvalues(l_pouniObject, 3, 3, 3, 0);@n
 *   Returns sort of level 4 subvalues in third level 3 subvalue in third
 *   level 2 subvalue in third level 1 subvalue
 * @li Case 5. Unicode_sort_subvalues(l_pouniObject, -1, 0, 0, 0);@n
 *   Returns sort of level 2 subvalues in last level 1 subvalue
 * @li Case 6. Unicode_sort_subvalues(l_pouniObject, 0, 3, 0, 0);@n
 *   Error! Non-zero indicies cannot follow zero indicies
 *
 * @param[in,out] u_pouniObject = Update pointer to **Unicode** object
 * @param[in] i_inFS = Index of level 1 subvalue, or ignored if zero
 * @param[in] i_inGS = Index of level 2 subvalue, or ignored if zero
 * @param[in] i_inRS = Index of level 3 subvalue, or ignored if zero
 * @param[in] i_inSort = Sort comparison algorithm value from 0 to 3
 * @retval "void" = None
 * @exception abort(3) Aborts if u_pouniObject is null
 * @exception abort(3) Aborts if subvalue index is zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 * @exception assert(3) Aborts if Unicode_from_string() call returns null
 * @exception assert(3) Aborts if Unicode_extract_subvalue() call returns null
 * @exception assert(3) Aborts if Unicode_split() call returns null
 * @exception assert(3) Aborts if Unicode_join() call returns null
 */

void Unicode_sort_subvalues(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inSort)
{
    int (*l_poFunction)(const void *, const void *) = 0;
    struct Unicode * l_pouniDelimiter = 0;
    struct Unicode * l_pouniUnsorted = 0;
    struct Unicode * l_pouniSorted = 0;
    struct UnicodeArray * l_pounaArray = 0;
    
    if (u_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p\n",
            __FILE__, __LINE__, u_pouniObject);
        abort();
    }
    if ((i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS);
        abort();
    }
    switch (i_inSort) {
        case 3: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_descendingnumeric; break;
        case 2: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_ascendingnumeric; break;
        case 1: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_descendingstring; break;
        default: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_ascendingstring; break;
    }
    l_pouniDelimiter = Unicode_from_string(i_inFS == 0 ? "\x1C" : i_inGS == 0 ? "\x1D" : i_inRS == 0 ? "\x1E" : "\x1F", 1, "ASCII");
    assert(l_pouniDelimiter != 0);
    if (i_inFS == 0) {
        l_pouniUnsorted = Unicode_new();
        Unicode_copy(l_pouniUnsorted, u_pouniObject);
    } else {
        l_pouniUnsorted = Unicode_extract_subvalue(u_pouniObject, i_inFS, i_inGS, i_inRS, 0);
    }
    assert(l_pouniUnsorted != 0);
    l_pounaArray = Unicode_split(l_pouniUnsorted, l_pouniDelimiter, 2);
    assert(l_pounaArray != 0);
    qsort(l_pounaArray->m_pouniObjects, l_pounaArray->m_sizObjects, sizeof(struct Unicode), l_poFunction);
    l_pouniSorted = Unicode_join(l_pounaArray, l_pouniDelimiter, 2);
    assert(l_pouniSorted != 0);
    if (i_inFS == 0) {
        Unicode_copy(u_pouniObject, l_pouniSorted);
    } else {
        Unicode_replace_subvalue(u_pouniObject, l_pouniSorted, i_inFS, i_inGS, i_inRS, 0);
    }
    UnicodeArray_delete(&l_pounaArray);
    Unicode_delete(&l_pouniSorted);
    Unicode_delete(&l_pouniUnsorted);
    Unicode_delete(&l_pouniDelimiter);
}

/**
 * @fn "int Unicode_locate_subvalue(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniKey, int i_inFS, int i_inGS, int i_inRS, int i_inSort)"
 * @brief Locate a subvalue at a specified level in a 4-dimensional dynamic array
 * @details Locates the index of the subvalue equal to the codepoints contained
 * in the **i_pouniKey** parameter by searching the next deeper level of subvalues of the specified level
 * inside the 4-dimensional dynamic array stored within the **Unicode** object.
 * For example, that means that if the **i_inFS** level 1 index is the only
 * non-zero parameter, then the **GS** delimited level 2 subvalues inside the
 * level 1 subvalue will be searched. The implementation of the dynamic array is
 * structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * The search is performed on the subvalues one level deeper than the last
 * non-zero index specified.  Indicies can have a positive, zero or negative
 * value. A positive value will sort the subvalues one level deeper than the
 * specified level. A zero index is effectively ignored. A negative index is
 * relative to the last subvalue at the specified level. If a negative index
 * has an absolute value greater than the number of subvalues at the specified
 * level, then the deeper level subvalues contained in the first subvalue will
 * be searched. It is an error for a non-zero index to follow (to be to the right
 * in the method parameter list) any index with a value of zero.
 *
 * The **i_inSort** parameter is required and can have a value from 0 to 4. A
 * value of zero will perform a simple left-to-right scan for the
 * **l_pouniKey** codepoints in the list of subvalues. If the list of subvalues
 * are guaranteed to be sorted in a particular order, using **i_inSort** values
 * 1 to 4 can significantly speed up the search using a binary search
 * algorithm. Depending on the value of **i_inSort**, one of the following
 * comparison functions can be used:
 * @li i_inSort == 0 - Unordered left-to-right value comparison scan
 * @li i_inSort == 1 - Unicode_compare_ascendingstring(i_pouniObject, i_pouniCompare)
 * @li i_inSort == 2 - Unicode_compare_descendingstring(i_pouniObject, i_pouniCompare)
 * @li i_inSort == 3 - Unicode_compare_ascendingnumeric(i_pouniObject, i_pouniCompare)
 * @li i_inSort == 4 - Unicode_compare_descendingnumeric(i_pouniObject, i_pouniCompare)
 *
 * @note The ASCII field delimiters FS, GS, RS and US provide a 4-level deep
 * method for storing multiple subvalues inside of a single **Unicode**
 * value using delimiters that are UNICODE safe.
 *
 * @note It is an error for any subvalue index to be zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero. Error
 * will cause a **Exception** to be thrown.
 *
 * @note When using the **Unicode_compare_ascendingnumeric()** or the
 * **Unicode_compare_descendingnumeric()** comparison functions each subvalue
 * is converted to a long double value and only those codepoints that are
 * considered part of the numeric value are used. For example, "1000-gt1" would
 * be equal to "1000.0" in the comparison.
 *
 * #### Examples ####
 *
 * @li Case 1. Unicode_locate_subvalues(l_pouniObject, 0, 0, 0, 0);@n
 *   Returns index of located level 1 subvalues using left-to-right search
 * @li Case 2. Unicode_locate_subvalues(l_pouniObject, 3, 0, 0, 0);@n
 *   Returns index of located level 2 subvalues in third level 1 subvalue using
 *   left-to-right search
 * @li Case 3. Unicode_locate_subvalues(l_pouniObject, 3, 3, 0, 0);@n
 *   Returns index of located level 3 subvalues in third level 2 subvalue in
 *   third level 1 subvalue using left-to-right search
 * @li Case 4. Unicode_locate_subvalues(l_pouniObject, 3, 3, 3, 0);@n
 *   Returns index of located level 4 subvalues in third level 3 subvalue in
 *   third level 2 subvalue in third level 1 subvalue using left-to-right
 *   search
 * @li Case 5. Unicode_locate_subvalues(l_pouniObject, -1, 0, 0, 0);@n
 *   Returns index of located level 2 subvalues in last level 1 subvalue using
 *   left-to-right search
 * @li Case 6. Unicode_locate_subvalues(l_pouniObject, 0, 3, 0, 0);@n
 *   Error! Non-zero indicies cannot follow zero indicies
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object to be searched
 * @param[in] i_pouniKey = Input pointer to **Unicode** object to search for
 * @param[in] i_inFS = Index of level 1 subvalue, or ignored if zero
 * @param[in] i_inGS = Index of level 2 subvalue, or ignored if zero
 * @param[in] i_inRS = Index of level 3 subvalue, or ignored if zero
 * @param[in] i_inSort = Sort comparison algorithm value from 0 to 4
 * @retval "int" = 1-based index of located subvalue in list of subvalues or
 * zero if not found
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniKey is null
 * @exception abort(3) Aborts if subvalue index is zero if any of the indices
 * following it (to the right in the method parameter list) are non-zero.
 * @exception assert(3) Aborts if Unicode_from_string() call returns null
 * @exception assert(3) Aborts if Unicode_extract_subvalue() call returns null
 * @exception assert(3) Aborts if Unicode_split() call returns null
 */

int Unicode_locate_subvalue(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniKey, int i_inFS, int i_inGS, int i_inRS, int i_inSort)
{
    int (*l_poFunction)(const void *, const void *) = 0;
    struct Unicode * l_pouniDelimiter = 0;
    struct Unicode * l_pouniList = 0;
    struct Unicode * l_pouniLocated = 0;
    struct UnicodeArray * l_pounaArray = 0;
    int l_inIndex = 0;
    
    if (i_pouniObject == 0 || i_pouniKey == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniKey = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniKey);
        abort();
    }
    if ((i_inRS > 0 && (i_inFS == 0 || i_inGS == 0))
        || (i_inGS > 0 && (i_inFS == 0)))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS);
        abort();
    }
    switch (i_inSort) {
        case 4: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_descendingnumeric; break;
        case 3: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_ascendingnumeric; break;
        case 2: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_descendingstring; break;
        case 1: l_poFunction = (int (*)(const void *, const void *)) Unicode_compare_ascendingstring; break;
        default: l_poFunction = 0;
    }
    l_pouniDelimiter = Unicode_from_string(i_inFS == 0 ? "\x1C" : i_inGS == 0 ? "\x1D" : i_inRS == 0 ? "\x1E" : "\x1F", 1, "ASCII");
    assert(l_pouniDelimiter != 0);
    if (i_inFS == 0) {
        l_pouniList = Unicode_new();
        Unicode_copy(l_pouniList, i_pouniObject);
    } else {
        l_pouniList = Unicode_extract_subvalue(i_pouniObject, i_inFS, i_inGS, i_inRS, 0);
    }
    assert(l_pouniList != 0);
    l_pounaArray = Unicode_split(l_pouniList, l_pouniDelimiter, 2);
    assert(l_pounaArray != 0);
    if (l_poFunction == 0) {
        for (l_inIndex = 0; (size_t) l_inIndex < l_pounaArray->m_sizObjects; l_inIndex++) {
            if (Unicode_compare_ascendingstring(i_pouniKey, &l_pounaArray->m_pouniObjects[l_inIndex]) == 0) {
                break;
            }
        }
        l_inIndex = (size_t) l_inIndex == l_pounaArray->m_sizObjects ? 0 : l_inIndex + 1;
    }
    else {
        l_pouniLocated = (struct Unicode *) bsearch(i_pouniKey, l_pounaArray->m_pouniObjects, l_pounaArray->m_sizObjects, sizeof(struct Unicode), l_poFunction);
        l_inIndex = l_pouniLocated == 0 ? 0 : (l_pouniLocated - l_pounaArray->m_pouniObjects) + 1;
    }
    UnicodeArray_delete(&l_pounaArray);
    Unicode_delete(&l_pouniList);
    Unicode_delete(&l_pouniDelimiter);
    return(l_inIndex);
}

/**
 * @fn "struct UnicodeArray * UnicodeArray_new(size_t i_sizElements)"
 * @brief Function that constructs an empty **UnicodeArray** object.
 * @details The heap is used to hold the newly constructed **UnicodeArray** object,
 * with all heap-allocated **Unicode** elements in an empty state.
 *
 * #### Example ####
 *
 * @code
 * // move the first 50 objects from l_pounaArray1 to l_pounaArray2
 * struct UnicodeArray * l_pounaArray1 = UnicodeArray_new(101);
 * load_all_101_objects_into_array(l_pounaArray1);
 * struct UnicodeArray * l_pounaArray2 = UnicodeArray_new(50);
 * for (l_sizOffset = 0; l_sizOffset < 50; l_sizOffset++) {
 *     Unicode_swap(&l_pounaArray1->m_pouniObjects[l_sizOffset], &l_pounaArray2->m_pouniObjects[l_sizOffset]);
 * }
 * UnicodeArray_delete(l_pounaArray1);
 * @endcode
 *
 * @param[in] i_sizElements = Number of heap-allocated **Unicode** objects in the array
 * @retval "struct UnicodeArray *" = Pointer to new **UnicodeArray** object
 * @exception abort(3) Aborts if calloc(3) returns a null pointer
 */

struct UnicodeArray * UnicodeArray_new(size_t i_sizElements)
{
    struct UnicodeArray * l_pounaObject = (struct UnicodeArray *) calloc(1, sizeof(struct UnicodeArray));
    assert(l_pounaObject != 0);
    l_pounaObject->m_pouniObjects = (struct Unicode *) calloc(i_sizElements, sizeof(struct Unicode));
    l_pounaObject->m_sizObjects = i_sizElements;
    return l_pounaObject;
}

/**
 * @fn "void UnicodeArray_delete(struct UnicodeArray ** u_popounaObject)"
 * @brief Function that destructs a **UnicodeArray** object
 * @details The **free()** function is called to release the heap allocations
 * for the **m_pouniObjects** struct member and the **UnicodeArray** object
 * itself.  Responsibility for calling **Unicode_clear()** on the **Unicode**
 * objects themselves is left to the calling routine, which if not done, can
 * lead to memory leaks of any allocated codepoints still on the heap.
 *
 * #### Example ####
 *
 * @code
 * // move the first 50 objects from l_pounaArray1 to l_pounaArray2
 * struct UnicodeArray * l_pounaArray1 = UnicodeArray_new(101);
 * load_all_101_objects_into_array(l_pounaArray1);
 * struct UnicodeArray * l_pounaArray2 = UnicodeArray_new(50);
 * for (l_sizOffset = 0; l_sizOffset < 50; l_sizOffset++) {
 *     Unicode_swap(&l_pounaArray1->m_pouniObjects[l_sizOffset], &l_pounaArray2->m_pouniObjects[l_sizOffset]);
 * }
 * UnicodeArray_delete(l_pounaArray1);
 * @endcode
 *
 * @param[in,out] u_popounaObject = Update pointer to pointer to **UnicodeArray** object
 * @retval "void" = None
 * @exception assert(3) Aborts if u_popounaObject is null
 * @exception assert(3) Aborts if *u_popounaObject is null
 */

void UnicodeArray_delete(struct UnicodeArray ** u_popounaObject)
{
    assert(u_popounaObject != 0);
    assert(*u_popounaObject != 0);
    if ((*u_popounaObject)->m_pouniObjects != 0) {
        free((*u_popounaObject)->m_pouniObjects);
    }
    free(*u_popounaObject);
    *u_popounaObject = 0;
}

/**
 * @fn "long Unicode_get_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Gets a codepoint at a particular offset inside a **Unicode** object
 * @details The codepoint value at the specified 0-based offset passed in the
 * parameter **i_sizOffset** is returned as a **long** value. The numeric value
 * is based upon UTF32LE encoding of the codepoints in the **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * // get UTF numeric value of first codepoint
 * long l_loCodepoint = Unicode_get_codepoint(l_pouniObject, 0);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to get codepoint in **Unicode** object
 * @retval "long" = Long value containing value of UTF32LE encoded codepoint,
 * or zero if **i_sizOffset** is outside the number of codepoints contained in
 * **i_pouniObject**.
 * @exception abort(3) Aborts if i_pouniObject is null
 */

long Unicode_get_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzCodepoints = (wchar_t *) i_pouniObject->m_poszCodepoints;
    long l_loCodepoint = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset < i_pouniObject->m_sizCodepoints) {
        l_loCodepoint = l_powzCodepoints[i_sizOffset];
    }
    return(l_loCodepoint);
}

/**
 * @fn "void Unicode_set_codepoint(struct Unicode * u_pouniObject, size_t i_sizOffset, long i_loCodepoint)"
 * @brief Sets a codepoint at a particular offset inside a **Unicode** object
 * @details The codepoint value placed at the specified 0-based offset passed
 * in the parameter **i_sizOffset** is passed as a **long** value in the
 * parameter **i_loCodepoint**. The numeric value is based upon UTF32LE
 * encoding of the codepoints in the **Unicode** object.
 *
 * #### Example ####
 *
 * @code
 * // set UTF numeric value of first codepoint
 * long l_loCodepoint = (long) U'©';  // U+00A9 COPYRIGHT SIGN
 * Unicode_set_codepoint(l_pouniObject, 0, l_loCodepoint);
 * @endcode
 *
 * @param[in,out] u_pouniObject = Update pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @param[in] i_loCodepoint = Long value of UTF32LE encoded codepoint
 * @retval "void" = None
 * @exception abort(3) Aborts if i_pouniObject is null
 */

void Unicode_set_codepoint(struct Unicode * u_pouniObject, size_t i_sizOffset, long i_loCodepoint)
{
    wchar_t * l_powzCodepoints = (wchar_t *) u_pouniObject->m_poszCodepoints;

    if (u_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = u_pouniObject = %p\n",
            __FILE__, __LINE__, u_pouniObject);
        abort();
    }
    if (i_sizOffset < u_pouniObject->m_sizCodepoints) {
        l_powzCodepoints[i_sizOffset] = i_loCodepoint;
    }
}

/**
 * @fn "int Unicode_find_codepoint (const struct Unicode * i_pouniObject, long i_loCodepoint, int i_inCount)"
 * @brief Find a codepoint and return offset inside a **Unicode** object
 * @details The input parameter **i_loCodepoint** value is searched for inside
 * the current **Unicode** object codepoints. The input parameter **i_inCount**
 * is used to return the offset of the first  match.  If a match is found, a
 * zero-based offset into the current object value in codepoints is returned. A
 * value of negative one (-1) is returned if the length of the string is zero,
 * or if a match cannot be found.
 *
 * #### Example ####
 *
 * @code
 * long l_loCodepoint = (long) U'©';  // U+00A9 COPYRIGHT SIGN
 * // find numeric offset of first left-to-right matching codepoint in object
 * int l_inFirst = Unicode_find_codepoint(l_pouniObject, l_loCodepoint, 0);
 * // find numeric offset of first right-to-left matching codepoint in object
 * int l_inLast = Unicode_find_codepoint(l_pouniObject, l_loCodepoint, -1);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_loCodepoint = Long integer value of UTF-32 codepoint element to
 * search for
 * @param[in] i_inCount = A zero indicates to return the offset of the first
 * match searching left to right in codepoints. A positive value indicates the
 * number of matches to skip first when searching left to right.  A negative
 * value indicates to search right to left and return the match at the absolute
 * value of the **i_inCount** input parameter. For example, a value of -1 will
 * return the offset of the right-most match.
 * @retval "int" = Returns a zero (0) based offset inside the **Unicode** object
 * content in codepoints if a match is found, or negative one (-1) if not.
 * @exception abort(3) Aborts if i_pouniObject is null
 */

int Unicode_find_codepoint (const struct Unicode * i_pouniObject, long i_loCodepoint, int i_inCount)
{
    wchar_t * l_powzCodepoints = (wchar_t *) i_pouniObject->m_poszCodepoints;
    int l_inOffset = -1;
    int l_inCounter = 0;
    int l_inIndex = 0;
    int l_inPos = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    for (l_inIndex = 0; (size_t) l_inIndex < i_pouniObject->m_sizCodepoints; l_inIndex++) {
        l_inPos = i_inCount >= 0 ? l_inIndex : (int) i_pouniObject->m_sizCodepoints - l_inIndex - 1;
        if (i_loCodepoint == (long) l_powzCodepoints[l_inPos]) {
            if ((i_inCount >= 0 && l_inCounter++ == i_inCount)
                || (i_inCount < 0 && --l_inCounter == i_inCount))
            {
                l_inOffset = l_inPos;
                break;
            }
        }
    }
    return l_inOffset;
}

/**
 * @fn "int Unicode_isalnum_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:alnum:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:alnum:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:alnum:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isalnum_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:alnum:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isalnum_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswalnum(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isalpha_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:alpha:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:alpha:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:alpha:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isalpha_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:alpha:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isalpha_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswalpha(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_islower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:lower:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:lower:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:lower:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_islower_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:lower:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_islower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswlower(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:upper:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:upper:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:upper:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isupper_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:upper:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswupper(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:digit:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:digit:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:digit:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isdigit_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:digit:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswdigit(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isxdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:xdigit:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:xdigit:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:xdigit:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isxdigit_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:xdigit:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isxdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswxdigit(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_iscntrl_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:cntrl:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:cntrl:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:cntrl:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_iscntrl_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:cntrl:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_iscntrl_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswcntrl(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isgraph_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:graph:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:graph:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:graph:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isgraph_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:graph:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isgraph_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswgraph(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isspace_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:space:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:space:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:space:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isspace_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:space:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isspace_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswspace(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isblank_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:blank:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:blank:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:blank:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isblank_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:blank:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isblank_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswblank(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_isprint_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:print:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:print:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:print:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_isprint_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:print:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_isprint_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswprint(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_ispunct_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Returns true if a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:punct:]" character class
 * @details The input parameter **i_sizOffset** value is used as the codepoint
 * offset inside the **Unicode** object. The codepoint at that offset is
 * matched against the POSIX "[:punct:]" character class. If it matches, the
 * integer 1 (true) is returned, else 0 (false) is returned.
 *
 * #### Example ####
 *
 * @code
 * // determine if first letter matches the POSIX "[:punct:]" character class
 * size_t l_sizOffset = 0;
 * int l_inMatches = Unicode_ispunct_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "int" = Returns an integer value of 1 (true) if the codepoint at the
 * passed offset matches the POSIX "[:punct:]" character class, else 0 (false)
 * is returned.
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

int Unicode_ispunct_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return !!iswpunct(l_powzObject[i_sizOffset]);
}

/**
 * @fn "long Unicode_tolower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Get lowercase value of a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:punct:]" character class
 * @details Returns the lowercase version (if possible) of the codepoint at
 * offset **i_sizOffset** in the **Unicode** object according to the current
 * global locale. The original codepoint is not modified. Offset is zero-based.
 * Any attempt to access beyond the last codepoint stored in the **Unicode**
 * object will throw an exception.
 *
 * #### Example ####
 *
 * @code
 * // get the lowercase value of first codepoint
 * size_t l_sizOffset = 0;
 * long l_loLowercase = Unicode_tolower_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "long" = Long value containing lowercase (if possible) value of UTF32LE encoded codepoint
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

long Unicode_tolower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return (long) towlower(l_powzObject[i_sizOffset]);
}

/**
 * @fn "long Unicode_toupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)"
 * @brief Get uppercase value of a codepoint at an offset inside a **Unicode** object
 * matches the POSIX "[:punct:]" character class
 * @details Returns the uppercase version (if possible) of the codepoint at
 * offset **i_sizOffset** in the **Unicode** object according to the current
 * global locale. The original codepoint is not modified. Offset is zero-based.
 * Any attempt to access beyond the last codepoint stored in the **Unicode**
 * object will throw an exception.
 *
 * #### Example ####
 *
 * @code
 * // get the uppercase value of first codepoint
 * size_t l_sizOffset = 0;
 * long l_loUppercase = Unicode_toupper_codepoint(l_pouniObject, l_sizOffset);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizOffset = Offset to put codepoint in **Unicode** object
 * @retval "long" = Long value containing uppercase (if possible) value of UTF32LE encoded codepoint
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_sizOffset >= i_pouniObject->m_sizCodepoints
 */

long Unicode_toupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset)
{
    wchar_t * l_powzObject = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    if (i_sizOffset >= i_pouniObject->m_sizCodepoints) {
        fprintf(stderr, "%s(%d) = i_pouniObject->m_sizCodepoints = %lu, i_sizOffset = %lu\n",
            __FILE__, __LINE__, i_pouniObject->m_sizCodepoints, i_sizOffset);
        abort();
    }
    l_powzObject = (wchar_t *) i_pouniObject->m_poszCodepoints;
    return (long) towupper(l_powzObject[i_sizOffset]);
}

/**
 * @fn "int Unicode_save(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding)"
 * @brief Save the current **Unicode** object content to a file in the local filesystem
 * @details The contents of the **Unicode** object are converted and written
 * into a file using any **iconv(3)** supported encoding.  The name of the file
 * along with any parent folders is passed in **i_pouniPath**.  The character
 * encoding should be passed as a null-terminated char string.  Any character
 * encoding can be specified using **i_poszEncoding** as long as it is
 * supported by the implementation's **iconv(3)** C library function.
 *
 * #### Example ####
 *
 * @code
 * // save the current Unicode object to file
 * struct Unicode * l_pouniObject = Unicode_new();
 * struct Unicode * l_pouniInput = Unicode_from_string("ASCII.txt", 0, "UTF8");
 * struct Unicode * l_pouniOutput = Unicode_from_string("EBCDIC.txt", 0, "UTF8");
 * int l_inBytesread = Unicode_load(l_pouniObject, l_pouniInput, "ASCII");
 * int l_inByteswritten = Unicode_save(l_pouniObject, l_pouniOutput, "EBCDIC-US");
 * Unicode_delete(&l_pouniOutput);
 * Unicode_delete(&l_pouniInput);
 * Unicode_delete(&l_pouniObject);
 * @endcode
 *
 * @param[in] i_pouniObject = Input **Unicode** object containing file data
 * @param[in] i_pouniPath = Input **Unicode** object containing absolute or
 * relative path of the file containing the output text.
 * @param[in] i_poszEncoding = C null-terminated string containing **iconv(3)**
 * supported encoding code (eg: "ASCII", "POSIX", "UTF8")
 * @retval "int" = Returns number of bytes written to file
 * @exception abort(3) Aborts if i_pouniObject is null
 * @exception abort(3) Aborts if i_pouniPath is null
 * @exception abort(3) Aborts if i_poszEncoding is null
 * @exception abort(3) Aborts on iconv_open(3) failure
 * @exception abort(3) Aborts on iconv(3) failure
 * @exception abort(3) Aborts on iconv_close(3) failure
 * @exception assert(3) Aborts if calloc() returns null
 * @see Unicode_export_string(), iconv_open(3), iconv(3), iconv_close(3)
 */

int Unicode_save(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding)
{
    char l_archToencoding[UNICODE_BUFFER_MAX + 1];
    const char * l_poszFromencoding = "UTF32LE";
    char * l_poszOutbuf = 0;
    char * l_poszOutbufleft = 0;
    char * l_poszInbuf = 0;
    char * l_poszPath = 0;
    FILE * l_fileSave = 0;
    size_t l_sizOutbytesleft = 0;
    size_t l_sizInbytesleft = 0;
    size_t l_sizReturn = 0;
    int l_inBytesconverted = 0;
    int l_inByteswritten = 0;
    int l_inReturn = 0;
    iconv_t l_ictCd;

    if (i_pouniObject == 0 || i_pouniPath == 0 || i_poszEncoding == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p, i_pouniPath = %p, i_poszEncoding = %p\n",
            __FILE__, __LINE__, i_pouniObject, i_pouniPath, i_poszEncoding);
        abort();
    }
    sprintf(l_archToencoding, "%s//TRANSLIT", i_poszEncoding);
    l_ictCd = iconv_open(l_archToencoding, l_poszFromencoding);
    if (l_ictCd == (iconv_t) -1) {
        fprintf(stderr, "%s(%d) = iconv_open() = -1, errno = %d, strerror = '%s', l_archToencoding = '%s', l_poszFromencoding = '%s', m_poszCodepoints = '%-.255s'\n",
            __FILE__, __LINE__, errno, strerror(errno), l_archToencoding, l_poszFromencoding, i_pouniObject->m_poszCodepoints);
        abort();
    }
    l_sizOutbytesleft = i_pouniObject->m_sizBytes + sizeof(wchar_t);
    l_poszOutbuf = (char *) calloc(1, l_sizOutbytesleft + sizeof(wchar_t));
    assert(l_poszOutbuf != 0);
    l_poszOutbufleft = l_poszOutbuf;
    l_sizInbytesleft = i_pouniObject->m_sizBytes;
    l_poszInbuf = i_pouniObject->m_poszCodepoints;
    l_sizReturn = iconv(l_ictCd, &l_poszInbuf, &l_sizInbytesleft, &l_poszOutbufleft, &l_sizOutbytesleft);
    if (l_sizReturn == (size_t) -1) {
        fprintf(stderr, "%s(%d) = iconv() = -1, errno = %d, strerror = '%s', l_archToencoding = '%s', l_poszFromencoding = '%s', l_sizInbytesleft = %lu, l_sizOutbytesleft = %lu\n",
            __FILE__, __LINE__, errno, strerror(errno), l_archToencoding, l_poszFromencoding, l_sizInbytesleft, l_sizOutbytesleft);
    }
    l_inReturn = iconv_close(l_ictCd);
    if (l_inReturn == -1) {
        fprintf(stderr, "%s(%d) = iconv_close() = -1, errno = %d, strerror = '%s', l_archToencoding = '%s', l_poszFromencoding = '%s', m_poszCodepoints = '%-.255s'\n",
            __FILE__, __LINE__, errno, strerror(errno), l_archToencoding, l_poszFromencoding, i_pouniObject->m_poszCodepoints);
        abort();
    }
    l_inBytesconverted = l_poszOutbufleft - l_poszOutbuf;
    l_poszPath = Unicode_export_string(i_pouniPath, i_pouniPath->m_sizBytes, "UTF8");
    l_fileSave = fopen(l_poszPath, "wb");
    if (l_fileSave != 0) {
        l_inByteswritten = fwrite(l_poszOutbuf, 1, l_inBytesconverted, l_fileSave);
        fclose(l_fileSave);
    }
    free(l_poszOutbuf);
    free(l_poszPath);
    return l_inByteswritten;
}

/**
 * @fn "int Unicode_load(struct Unicode * o_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding)"
 * @brief Load the current **Unicode** object content from a file in the local filesystem
 * @details The contents of the **Unicode** object are read from a file and
 * converted using any **iconv(3)** supported encoding.  The name of the file
 * along with any parent folders is passed in **i_pouniPath**.  The character
 * encoding should be passed as a null-terminated char string.  Any character
 * encoding can be specified using **i_poszEncoding** as long as it is
 * supported by the implementation's **iconv(3)** C library function.
 *
 * #### Example ####
 *
 * @code
 * // save the current Unicode object to file
 * struct Unicode * l_pouniObject = Unicode_new();
 * struct Unicode * l_pouniInput = Unicode_from_string("EBCDIC.txt", 0, "UTF8");
 * struct Unicode * l_pouniOutput = Unicode_from_string("ASCII.txt", 0, "UTF8");
 * int l_inBytesread = Unicode_load(l_pouniObject, l_pouniInput, "EBCDIC-US");
 * int l_inByteswritten = Unicode_save(l_pouniObject, l_pouniOutput, "ASCII");
 * Unicode_delete(&l_pouniOutput);
 * Unicode_delete(&l_pouniInput);
 * Unicode_delete(&l_pouniObject);
 * @endcode
 *
 * @param[out] o_pouniObject = Output **Unicode** object containing file data
 * @param[in] i_pouniPath = Input **Unicode** object containing absolute or
 * relative path of the file containing the input text.
 * @param[in] i_poszEncoding = C null-terminated string containing **iconv(3)**
 * supported encoding code (eg: "ASCII", "POSIX", "UTF8")
 * @retval "int" = Returns number of bytes read from file before conversion
 * @exception abort(3) Aborts if o_pouniObject is null
 * @exception abort(3) Aborts if i_pouniPath is null
 * @exception abort(3) Aborts if i_poszEncoding is null
 * @exception assert(3) Aborts if calloc() returns null
 * @see Unicode_export_string(), Unicode_import_string(), iconv_open(3), iconv(3), iconv_close(3)
 */

int Unicode_load(struct Unicode * o_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding)
{
    char * l_poszInbuf = 0;
    char * l_poszPath = 0;
    FILE * l_fileLoad = 0;
    int l_inBytesread = 0;

    if (o_pouniObject == 0 || i_pouniPath == 0 || i_poszEncoding == 0) {
        fprintf(stderr, "%s(%d) = o_pouniObject = %p, i_pouniPath = %p, i_poszEncoding = %p\n",
            __FILE__, __LINE__, o_pouniObject, i_pouniPath, i_poszEncoding);
        abort();
    }
    l_poszPath = Unicode_export_string(i_pouniPath, i_pouniPath->m_sizBytes, "UTF8");
    l_fileLoad = fopen(l_poszPath, "r+b");
    if (l_fileLoad != 0) {
        fseek(l_fileLoad, 0L, SEEK_END);
        l_inBytesread = ftell(l_fileLoad);
        fseek(l_fileLoad, 0L, SEEK_SET);
        l_poszInbuf = (char *) calloc(1, l_inBytesread + sizeof(wchar_t));
        assert(l_poszInbuf != 0);
        l_inBytesread = fread(l_poszInbuf, 1, l_inBytesread, l_fileLoad);
        fclose(l_fileLoad);
    }
    if (l_poszInbuf != 0) {
        Unicode_import_string(o_pouniObject, l_poszInbuf, l_inBytesread, i_poszEncoding);
        free(l_poszInbuf);
    }
    free(l_poszPath);
    return l_inBytesread;
}

/**
 * @fn "struct UnicodeTesseract * UnicodeTesseract_new(size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4)"
 * @brief Function that constructs an empty **UnicodeTesseract** object.
 * @details A new **UnicodeTesseract** object is created on the heap. It also
 * creates a separate virtual memory space backed up by a temporary file for
 * the lifetime of the **UnicodeTesseract** object. The separate virtual memory
 * space is neither stored on the stack or the heap, and is used to store the
 * following data:
 * @li **UnicodeArray** object
 * @li One dimensional array of **Unicode** objects
 * @li Four dimensional array of codepoints for the **Unicode** objects
 * 
 * @note A **UnicodeTesseract** object uses the **tmpfile(3)** and **mmap(2)**
 * system calls to create a new virtual address space that caches memory access
 * to a private temporary file unique to each **UnicodeTesseract** object. When
 * using the **Unicode_get_element()** to extact **Unicode** objects from the
 * **UnicodeTesseract** object, you should not use **Unicode_clear()** or
 * **Unicode_delete()** on these objects since they exist in an alternate
 * virtual address memory space, in other words, they do not exist on the heap.
 * This has ramifications when using SWIG, as the **Unicode_get_element()**
 * function should not be used in a **%newobject** directive since it only
 * returns the address of an already existing **Unicode** object, and does not
 * create a new one.
 *
 * #### Example ####
 *
 * @code
 * int l_inCodepoints = 10, l_inDim1 = 10, l_inDim2 = 10, l_inDim3 = 10, l_inDim4 = 10;
 * int l_inLevel1 = 0, l_inLevel2 = 0, l_inLevel3 = 0, l_inLevel4 = 0, l_inElement = 0, l_inSubvalue = 0;
 * struct UnicodeTesseract * l_pountObject = 0;
 * struct Unicode * l_pouniObject = 0;
 * struct Unicode * l_pouniElement = 0;
 * l_pountObject = UnicodeTesseract_new(l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_inElement = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 l_pouniElement = Unicode::from_int(l_inElement);
 *                 assert(l_pouniElement != 0);
 *                 Unicode_set_element(l_pountObject, l_pouniElement, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 Unicode_clear(&l_pouniElement);
 *             }
 *         }
 *     }
 * }
 * l_pouniObject = Unicode_from_tesseract(l_pountObject);
 * l_pountObject = Unicode_to_tesseract(l_pouniObject, l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_pouniElement = Unicode_get_element(l_pountObject, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 assert(l_pouniElement != 0);
 *                 l_inElement = l_pouniElement->to_int();
 *                 l_inSubvalue = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 assert(l_inElement == l_inSubvalue);
 *             }
 *         }
 *     }
 * }
 * UnicodeTesseract_delete(&l_pountObject);
 * @endcode
 *
 * @param[in] i_sizCodepoints = Maximum size in codepoints of each **Unicode** object
 * @param[in] i_sizDim1 = Number of **Unicode** objects at dimension 1
 * @param[in] i_sizDim2 = Number of **Unicode** objects at dimension 2
 * @param[in] i_sizDim3 = Number of **Unicode** objects at dimension 3
 * @param[in] i_sizDim4 = Number of **Unicode** objects at dimension 4
 * @retval "struct UnicodeTesseract *" = Pointer to new **UnicodeTesseract** object
 * @exception assert(3) Aborts if calloc(3) returns a null pointer
 * @exception abort(3) Aborts if any dimension parameter is zero
 * @exception abort(3) Aborts if mkstemp(3) returns -1
 * @exception abort(3) Aborts if mmap(2) returns (void *) -1
 * @see ftruncate(2), mmap(2), munmap(2)
 */

struct UnicodeTesseract * UnicodeTesseract_new(size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4)
{
    struct UnicodeTesseract * l_pountObject = 0;
    char * l_poszMemory = 0;
    FILE * l_poFile = 0;

    if (i_sizDim1 == 0 || i_sizDim2 == 0 || i_sizDim3 == 0 || i_sizDim4 == 0)
    {
        fprintf(stderr, "%s(%d) = i_sizDim1 = %lu, i_sizDim2 = %lu, i_sizDim3 = %lu, i_sizDim4 = %lu\n",
            __FILE__, __LINE__, i_sizDim1, i_sizDim2, i_sizDim3, i_sizDim4);
        abort();
    }
    l_pountObject = (struct UnicodeTesseract *) calloc(1, sizeof(struct UnicodeTesseract));
    assert(l_pountObject != 0);
    // number of elements stored inside the UnicodeTesseract virtual memory
    l_pountObject->m_sizElements = i_sizDim1 * i_sizDim2 * i_sizDim3 * i_sizDim4;
    // amount of virtual memory to hold UnicodeArray, Unicode objects, and Codepoints data
    l_pountObject->m_sizBytes = sizeof(struct UnicodeArray);
    l_pountObject->m_sizBytes += l_pountObject->m_sizElements * sizeof(struct Unicode);
    l_pountObject->m_sizBytes += l_pountObject->m_sizElements * i_sizCodepoints * sizeof(wchar_t);
    // create a temporary virtual memory file whose directory entry is deleted right after opening
    l_poFile = tmpfile();
    l_pountObject->m_inFile = fileno(l_poFile);
    if (l_pountObject->m_inFile == -1) {
        fprintf(stderr, "%s(%d) = tmpfile() = -1, errno = %d, strerror = '%s'\n",
            __FILE__, __LINE__, errno, strerror(errno));
        abort();
    }
    if (ftruncate(l_pountObject->m_inFile, l_pountObject->m_sizBytes) == -1) {
        fprintf(stderr, "%s(%d) = ftruncate(%lu) = -1, errno = %d, strerror = '%s'\n",
            __FILE__, __LINE__, l_pountObject->m_sizBytes, errno, strerror(errno));
        abort();
    }
    l_poszMemory = (char *) mmap(0, l_pountObject->m_sizBytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, l_pountObject->m_inFile, 0);
    if (l_poszMemory == (void *) -1) {
        fprintf(stderr, "%s(%d) = mmap(%lu) = -1, errno = %d, strerror = '%s'\n",
            __FILE__, __LINE__, l_pountObject->m_sizBytes, errno, strerror(errno));
        abort();
    }
    l_pountObject->m_pounaObject = (struct UnicodeArray *) l_poszMemory;
    l_pountObject->m_pounaObject->m_pouniObjects = (struct Unicode *) (l_poszMemory + sizeof(struct UnicodeArray));
    l_pountObject->m_pounaObject->m_sizObjects = l_pountObject->m_sizElements;
    l_pountObject->m_poszCodepoints = l_poszMemory + sizeof(struct UnicodeArray) + l_pountObject->m_sizElements * sizeof(struct Unicode);
    l_pountObject->m_sizCodepoints = i_sizCodepoints;
    l_pountObject->m_sizDim1 = i_sizDim1;
    l_pountObject->m_sizDim2 = i_sizDim2;
    l_pountObject->m_sizDim3 = i_sizDim3;
    l_pountObject->m_sizDim4 = i_sizDim4;
    return l_pountObject;
}

/**
 * @fn "void UnicodeTesseract_delete(struct UnicodeTesseract ** u_popountObject)"
 * @brief Function that destructs a **UnicodeTesseract** object
 * @details The **free()** function is called to release the heap allocations
 * for the **m_pouniObjects** struct members, the **m_pounaLevel[1-4]** members and the **UnicodeTesseract** object
 * itself.  Responsibility for calling **Unicode_clear()** on the **Unicode**
 * objects themselves is left to the calling routine, which if not done, can
 * lead to memory leaks of any allocated codepoints still on the heap.
 *
 * #### Example ####
 *
 * @code
 * int l_inCodepoints = 10, l_inDim1 = 10, l_inDim2 = 10, l_inDim3 = 10, l_inDim4 = 10;
 * int l_inLevel1 = 0, l_inLevel2 = 0, l_inLevel3 = 0, l_inLevel4 = 0, l_inElement = 0, l_inSubvalue = 0;
 * struct UnicodeTesseract * l_pountObject = 0;
 * struct Unicode * l_pouniObject = 0;
 * struct Unicode * l_pouniElement = 0;
 * l_pountObject = UnicodeTesseract_new(l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_inElement = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 l_pouniElement = Unicode::from_int(l_inElement);
 *                 assert(l_pouniElement != 0);
 *                 Unicode_set_element(l_pountObject, l_pouniElement, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 Unicode_clear(&l_pouniElement);
 *             }
 *         }
 *     }
 * }
 * l_pouniObject = Unicode_from_tesseract(l_pountObject);
 * l_pountObject = Unicode_to_tesseract(l_pouniObject, l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_pouniElement = Unicode_get_element(l_pountObject, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 assert(l_pouniElement != 0);
 *                 l_inElement = l_pouniElement->to_int();
 *                 l_inSubvalue = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 assert(l_inElement == l_inSubvalue);
 *             }
 *         }
 *     }
 * }
 * UnicodeTesseract_delete(&l_pountObject);
 * @endcode
 *
 * @param[in,out] u_popountObject = Update pointer to pointer to **UnicodeTesseract** object
 * @retval "void" = None
 * @exception assert(3) Aborts if u_popounaObject is null
 * @exception assert(3) Aborts if *u_popounaObject is null
 * @see munmap(3), close(2), free(3)
 */

void UnicodeTesseract_delete(struct UnicodeTesseract ** u_popountObject)
{
    assert(u_popountObject != 0);
    assert(*u_popountObject != 0);
    if ((*u_popountObject)->m_pounaObject != 0) {
        munmap((*u_popountObject)->m_pounaObject, (*u_popountObject)->m_sizBytes);
        close((*u_popountObject)->m_inFile);
    }
    free(*u_popountObject);
    *u_popountObject = 0;
}

/**
 * @fn "void Unicode_set_element(struct UnicodeTesseract * u_pountObject, const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)"
 * @brief Function to set a **UnicodeTesseract** object subvalue
 * @details The codepoints of the **i_pouniObject** object are assigned to an
 * element in the four dimensional **u_pountObject** array using the specified
 * 1-based indicies.  The **UnicodeTesseract** object mirrors the four
 * dimensional dynamic array contained inside a **Unicode** object. The
 * implementation of the 4-dimensional dynamic array inside a **Unicode**
 * object is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies are 1-based and can have a positive or negative value. Zero values
 * are not permitted. A positive value will access a specific subvalue at a
 * specific level.  A negative value will access a subvalue relative to the
 * highest dimensional index at the specified level in the
 * **UnicodeTesseract**. If a negative value has an absolute value greater than
 * the number of subvalues at the specified level, then the first subvalue will
 * be accessed. It is an error for any index to have a zero value.
 *
 * @note Inside a **Unicode** object, the ASCII field delimiters FS, GS, RS and
 * US provide a 4-level deep method for storing multiple subvalues inside of a
 * single **Unicode** object using delimiters that are UNICODE safe. For this
 * reason, any and all FS, GS, RS and US delimiters should be removed from the
 * codepoints in **i_pouniObject** before the codepoints are set inside an
 * element in **u_pountObject**. Any delimiters included in the codepoints
 * inserted into any of the **u_pountObject** elements can lead to undefined
 * behavior.
 *
 * @note This routine does not allocate or free any heap memory, but only
 * returns the address of an already existing **Unicode** object that exists in
 * the **UnicodeTesseract** object virtual memory address space.
 *
 * #### Example ####
 *
 * @code
 * int l_inCodepoints = 10, l_inDim1 = 10, l_inDim2 = 10, l_inDim3 = 10, l_inDim4 = 10;
 * int l_inLevel1 = 0, l_inLevel2 = 0, l_inLevel3 = 0, l_inLevel4 = 0, l_inElement = 0, l_inSubvalue = 0;
 * struct UnicodeTesseract * l_pountObject = 0;
 * struct Unicode * l_pouniObject = 0;
 * struct Unicode * l_pouniElement = 0;
 * l_pountObject = UnicodeTesseract_new(l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_inElement = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 l_pouniElement = Unicode::from_int(l_inElement);
 *                 assert(l_pouniElement != 0);
 *                 Unicode_set_element(l_pountObject, l_pouniElement, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 Unicode_clear(&l_pouniElement);
 *             }
 *         }
 *     }
 * }
 * l_pouniObject = Unicode_from_tesseract(l_pountObject);
 * l_pountObject = Unicode_to_tesseract(l_pouniObject, l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_pouniElement = Unicode_get_element(l_pountObject, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 assert(l_pouniElement != 0);
 *                 l_inElement = l_pouniElement->to_int();
 *                 l_inSubvalue = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 assert(l_inElement == l_inSubvalue);
 *             }
 *         }
 *     }
 * }
 * UnicodeTesseract_delete(&l_pountObject);
 * @endcode
 *
 * @param[in,out] u_pountObject = Update pointer to pointer to **UnicodeTesseract** object
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_inFS = Level 1 index of level 1 subvalues
 * @param[in] i_inGS = Level 2 index of level 2 subvalues
 * @param[in] i_inRS = Level 3 index of level 3 subvalues
 * @param[in] i_inUS = Level 4 index of level 4 subvalues
 * @retval "void" = None
 * @exception abort(3) Aborts if **u_pountObject** is null
 * @exception abort(3) Aborts if **i_pouniObject** is null
 * @exception abort(3) Aborts if any subvalue index is zero
 * @exception abort(3) Aborts if a positive subvalue index is larger than the
 * original dimensional value at the appropriate level when the
 * **UnicodeTesseract** object was created.
 */

void Unicode_set_element(struct UnicodeTesseract * u_pountObject, const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)
{
    size_t l_sizFS = 0;                         // 0-based array offsets converted from input index parameters
    size_t l_sizGS = 0;
    size_t l_sizRS = 0;
    size_t l_sizUS = 0;
    size_t l_sizOffset = 0;
    size_t l_sizBytes = 0;
    size_t l_sizCodepoints = 0;

    if (u_pountObject == 0 || i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = u_pountObject = %p, i_pouniObject = %p\n",
            __FILE__, __LINE__, u_pountObject, i_pouniObject);
        abort();
    }
    if ((i_inFS == 0 || (size_t) i_inFS > u_pountObject->m_sizDim1)
        || (i_inGS == 0 || (size_t) i_inGS > u_pountObject->m_sizDim2)
        || (i_inRS == 0 || (size_t) i_inRS > u_pountObject->m_sizDim3)
        || (i_inUS == 0 || (size_t) i_inUS > u_pountObject->m_sizDim4))
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d, i_inUS = %d, m_sizDim1 = %lu, m_sizDim2 = %lu, m_sizDim3 = %lu, m_sizDim4 = %lu\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS, i_inUS, u_pountObject->m_sizDim1, u_pountObject->m_sizDim2, u_pountObject->m_sizDim3, u_pountObject->m_sizDim4);
        abort();
    }
    l_sizFS = i_inFS > 0 ? (size_t) i_inFS : i_inFS + u_pountObject->m_sizDim1 < 1 ? 1 : i_inFS + u_pountObject->m_sizDim1 + 1;
    l_sizGS = i_inGS > 0 ? (size_t) i_inGS : i_inGS + u_pountObject->m_sizDim2 < 1 ? 1 : i_inGS + u_pountObject->m_sizDim2 + 1;
    l_sizRS = i_inRS > 0 ? (size_t) i_inRS : i_inRS + u_pountObject->m_sizDim3 < 1 ? 1 : i_inRS + u_pountObject->m_sizDim3 + 1;
    l_sizUS = i_inUS > 0 ? (size_t) i_inUS : i_inUS + u_pountObject->m_sizDim4 < 1 ? 1 : i_inUS + u_pountObject->m_sizDim4 + 1;
    l_sizOffset = (l_sizFS - 1) * u_pountObject->m_sizDim2 * u_pountObject->m_sizDim3 * u_pountObject->m_sizDim4;
    l_sizOffset += (l_sizGS - 1) * u_pountObject->m_sizDim3 * u_pountObject->m_sizDim4;
    l_sizOffset += (l_sizRS - 1) * u_pountObject->m_sizDim4;
    l_sizOffset += (l_sizUS - 1);
    l_sizBytes = u_pountObject->m_sizCodepoints * sizeof(wchar_t);
    u_pountObject->m_pounaObject->m_pouniObjects[l_sizOffset].m_poszCodepoints = u_pountObject->m_poszCodepoints + l_sizOffset * l_sizBytes;
    l_sizCodepoints = u_pountObject->m_sizCodepoints <= i_pouniObject->m_sizCodepoints ? u_pountObject->m_sizCodepoints : i_pouniObject->m_sizCodepoints;
    memcpy(u_pountObject->m_pounaObject->m_pouniObjects[l_sizOffset].m_poszCodepoints, i_pouniObject->m_poszCodepoints, l_sizCodepoints * sizeof(wchar_t));
    u_pountObject->m_pounaObject->m_pouniObjects[l_sizOffset].m_sizCodepoints = l_sizCodepoints;
    u_pountObject->m_pounaObject->m_pouniObjects[l_sizOffset].m_sizBytes = l_sizCodepoints * sizeof(wchar_t);
}

/**
 * @fn "struct Unicode * Unicode_get_element(const struct UnicodeTesseract * i_pountObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)"
 * @brief Function to get a **UnicodeTesseract** object subvalue
 * @details A pointer to the **Unicode** object element in the four dimensional
 * **UnicodeTesseract** object pointed to by the **i_pountObject** parameter
 * using specified 1-based indicies is returned.  The **UnicodeTesseract**
 * object mirrors the four dimensional dynamic array contained inside a
 * **Unicode** object. The implementation of the 4-dimensional dynamic array
 * inside a **Unicode** object is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * Indicies can have a positive or negative value. Zero values are not
 * permitted. A positive value will access a specific subvalue at a specific
 * level.  A negative value will access a subvalue relative to the highest
 * dimensional index at the specified level in the **UnicodeTesseract**. If a
 * negative value has an absolute value greater than the number of subvalues at
 * the specified level, then the first subvalue will be accessed. It is an
 * error for any subvalue index to have a zero value.
 *
 * #### Example ####
 *
 * @code
 * int l_inCodepoints = 10, l_inDim1 = 10, l_inDim2 = 10, l_inDim3 = 10, l_inDim4 = 10;
 * int l_inLevel1 = 0, l_inLevel2 = 0, l_inLevel3 = 0, l_inLevel4 = 0, l_inElement = 0, l_inSubvalue = 0;
 * struct UnicodeTesseract * l_pountObject = 0;
 * struct Unicode * l_pouniObject = 0;
 * struct Unicode * l_pouniElement = 0;
 * l_pountObject = UnicodeTesseract_new(l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_inElement = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 l_pouniElement = Unicode::from_int(l_inElement);
 *                 assert(l_pouniElement != 0);
 *                 Unicode_set_element(l_pountObject, l_pouniElement, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 Unicode_clear(&l_pouniElement);
 *             }
 *         }
 *     }
 * }
 * l_pouniObject = Unicode_from_tesseract(l_pountObject);
 * l_pountObject = Unicode_to_tesseract(l_pouniObject, l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_pouniElement = Unicode_get_element(l_pountObject, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 assert(l_pouniElement != 0);
 *                 l_inElement = l_pouniElement->to_int();
 *                 l_inSubvalue = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 assert(l_inElement == l_inSubvalue);
 *             }
 *         }
 *     }
 * }
 * UnicodeTesseract_delete(&l_pountObject);
 * @endcode
 *
 * @param[in] i_pountObject = Input pointer to pointer to **UnicodeTesseract** object
 * @param[in] i_inFS = Level 1 index of level 1 subvalues
 * @param[in] i_inGS = Level 2 index of level 2 subvalues
 * @param[in] i_inRS = Level 3 index of level 3 subvalues
 * @param[in] i_inUS = Level 4 index of level 4 subvalues
 * @retval "struct Unicode *" = Pointer to **Unicode** object inside
 * **UnicodeTesseract** object containing subvalue at specified indicies
 * @exception abort(3) Aborts if **i_pountObject** is null
 * @exception abort(3) Aborts if a subvalue index is zero and any of the
 * indices following it (to the right in the method parameter list) are
 * non-zero.
 * @exception abort(3) Aborts if a positive subvalue index is larger than the
 * original dimensional value at the appropriate level when the
 * **UnicodeTesseract** object was created.
 */

struct Unicode * Unicode_get_element(const struct UnicodeTesseract * i_pountObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS)
{
    size_t l_sizFS = 0;                         // 0-based array offsets converted from input index parameters
    size_t l_sizGS = 0;
    size_t l_sizRS = 0;
    size_t l_sizUS = 0;
    size_t l_sizOffset = 0;

    if (i_pountObject == 0) {
        fprintf(stderr, "%s(%d) = i_pountObject = %p\n",
            __FILE__, __LINE__, i_pountObject);
        abort();
    }
    if (i_inFS == 0 || (size_t) i_inFS > i_pountObject->m_sizDim1
        || i_inGS == 0 || (size_t) i_inGS > i_pountObject->m_sizDim2
        || i_inRS == 0 || (size_t) i_inRS > i_pountObject->m_sizDim3
        || i_inUS == 0 || (size_t) i_inUS > i_pountObject->m_sizDim4)
    {
        fprintf(stderr, "%s(%d) = i_inFS = %d, i_inGS = %d, i_inRS = %d, i_inUS = %d, m_sizDim1 = %lu, m_sizDim2 = %lu, m_sizDim3 = %lu, m_sizDim4 = %lu\n",
            __FILE__, __LINE__, i_inFS, i_inGS, i_inRS, i_inUS, i_pountObject->m_sizDim1, i_pountObject->m_sizDim2, i_pountObject->m_sizDim3, i_pountObject->m_sizDim4);
        abort();
    }
    l_sizFS = i_inFS > 0 ? (size_t) i_inFS : i_inFS + i_pountObject->m_sizDim1 < 1 ? 1 : i_inFS + i_pountObject->m_sizDim1 + 1;
    l_sizGS = i_inGS > 0 ? (size_t) i_inGS : i_inGS + i_pountObject->m_sizDim2 < 1 ? 1 : i_inGS + i_pountObject->m_sizDim2 + 1;
    l_sizRS = i_inRS > 0 ? (size_t) i_inRS : i_inRS + i_pountObject->m_sizDim3 < 1 ? 1 : i_inRS + i_pountObject->m_sizDim3 + 1;
    l_sizUS = i_inUS > 0 ? (size_t) i_inUS : i_inUS + i_pountObject->m_sizDim4 < 1 ? 1 : i_inUS + i_pountObject->m_sizDim4 + 1;
    l_sizOffset = (l_sizFS - 1) * i_pountObject->m_sizDim2 * i_pountObject->m_sizDim3 * i_pountObject->m_sizDim4;
    l_sizOffset += (l_sizGS - 1) * i_pountObject->m_sizDim3 * i_pountObject->m_sizDim4;
    l_sizOffset += (l_sizRS - 1) * i_pountObject->m_sizDim4;
    l_sizOffset += (l_sizUS - 1);
    return &i_pountObject->m_pounaObject->m_pouniObjects[l_sizOffset];
}

/**
 * @fn "struct UnicodeTesseract * Unicode_to_tesseract(const struct Unicode * i_pouniObject, size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4)"
 * @brief Function to split a **Unicode** object into a **UnicodeTesseract** object
 * @details Creates a **UnicodeTesseract** object from a **Unicode** object and
 * determines the dimensions of the **UnicodeTesseract** directly from the
 * codepoints of the **Unicode** object. Only a new **UnicodeTesseract** object
 * is created in dynamic memory.  The **UnicodeTesseract** object mirrors the
 * four dimensional dynamic array contained inside a **Unicode** object. The
 * implementation of the 4-dimensional dynamic array inside a **Unicode**
 * object is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * @note This routine allocates dynamic memory for a new **UnicodeTesseract**
 * object.  It must be deleted by the calling routine after processing is
 * complete (as appropriate) by a call to **UnicodeTesseract_delete()**.  If
 * not done, a memory leak will be created.
 *
 * #### Example ####
 *
 * @code
 * int l_inCodepoints = 10, l_inDim1 = 10, l_inDim2 = 10, l_inDim3 = 10, l_inDim4 = 10;
 * int l_inLevel1 = 0, l_inLevel2 = 0, l_inLevel3 = 0, l_inLevel4 = 0, l_inElement = 0, l_inSubvalue = 0;
 * struct UnicodeTesseract * l_pountObject = 0;
 * struct Unicode * l_pouniObject = 0;
 * struct Unicode * l_pouniElement = 0;
 * l_pountObject = UnicodeTesseract_new(l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_inElement = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 l_pouniElement = Unicode::from_int(l_inElement);
 *                 assert(l_pouniElement != 0);
 *                 Unicode_set_element(l_pountObject, l_pouniElement, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 Unicode_clear(&l_pouniElement);
 *             }
 *         }
 *     }
 * }
 * l_pouniObject = Unicode_from_tesseract(l_pountObject);
 * l_pountObject = Unicode_to_tesseract(l_pouniObject, l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_pouniElement = Unicode_get_element(l_pountObject, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 assert(l_pouniElement != 0);
 *                 l_inElement = l_pouniElement->to_int();
 *                 l_inSubvalue = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 assert(l_inElement == l_inSubvalue);
 *             }
 *         }
 *     }
 * }
 * UnicodeTesseract_delete(&l_pountObject);
 * @endcode
 *
 * @param[in] i_pouniObject = Input pointer to **Unicode** object
 * @param[in] i_sizCodepoints = Maximum size in codepoints of any **UnicodeTesseract** object element
 * @param[in] i_sizDim1 = Number of **Unicode** objects at dimension 1
 * @param[in] i_sizDim2 = Number of **Unicode** objects at dimension 2
 * @param[in] i_sizDim3 = Number of **Unicode** objects at dimension 3
 * @param[in] i_sizDim4 = Number of **Unicode** objects at dimension 4
 * @retval "struct UnicodeTesseract *" = Pointer to newly allocated **UnicodeTesseract** object
 * @exception abort(3) Aborts if **i_pouniObject** is null
 * @exception assert(3) Aborts if **UnicodeTesseract_new** call returns null
 */

struct UnicodeTesseract * Unicode_to_tesseract(
    const struct Unicode * i_pouniObject,
    size_t i_sizCodepoints,
    size_t i_sizDim1,
    size_t i_sizDim2,
    size_t i_sizDim3,
    size_t i_sizDim4)
{
    struct UnicodeTesseract * l_pountObject = 0;
    struct UnicodeArray * l_pounaLevel1 = 0;
    struct UnicodeArray * l_pounaLevel2 = 0;
    struct UnicodeArray * l_pounaLevel3 = 0;
    struct UnicodeArray * l_pounaLevel4 = 0;
    struct Unicode * l_pouniElement = 0;
    struct Unicode * l_pouniFS = 0;
    struct Unicode * l_pouniGS = 0;
    struct Unicode * l_pouniRS = 0;
    struct Unicode * l_pouniUS = 0;
    size_t l_sizCodepoints = 0;
    size_t l_sizBytes = 0;
    int l_inIndex1 = 0;
    int l_inIndex2 = 0;
    int l_inIndex3 = 0;
    int l_inIndex4 = 0;
    int l_inOffset = 0;

    if (i_pouniObject == 0) {
        fprintf(stderr, "%s(%d) = i_pouniObject = %p\n",
            __FILE__, __LINE__, i_pouniObject);
        abort();
    }
    l_pouniFS = Unicode_from_string("\x1C", 1, "ASCII");
    l_pouniGS = Unicode_from_string("\x1D", 1, "ASCII");
    l_pouniRS = Unicode_from_string("\x1E", 1, "ASCII");
    l_pouniUS = Unicode_from_string("\x1F", 1, "ASCII");
    l_pountObject = UnicodeTesseract_new(i_sizCodepoints, i_sizDim1, i_sizDim2, i_sizDim3, i_sizDim4);
    assert(l_pountObject != 0);
    l_pounaLevel1 = Unicode_split(i_pouniObject, l_pouniFS, 2);
    for (l_inIndex1 = 1; (size_t) l_inIndex1 <= l_pounaLevel1->m_sizObjects; l_inIndex1++) {
        l_pounaLevel2 = Unicode_split(&l_pounaLevel1->m_pouniObjects[l_inIndex1 - 1], l_pouniGS, 2);
        for (l_inIndex2 = 1; (size_t) l_inIndex2 <= l_pounaLevel2->m_sizObjects; l_inIndex2++) {
            l_pounaLevel3 = Unicode_split(&l_pounaLevel2->m_pouniObjects[l_inIndex2 - 1], l_pouniRS, 2);
            for (l_inIndex3 = 1; (size_t) l_inIndex3 <= l_pounaLevel3->m_sizObjects; l_inIndex3++) {
                l_pounaLevel4 = Unicode_split(&l_pounaLevel3->m_pouniObjects[l_inIndex3 - 1], l_pouniUS, 2);
                for (l_inIndex4 = 1; (size_t) l_inIndex4 <= l_pounaLevel4->m_sizObjects; l_inIndex4++) {
                    l_inOffset = (l_inIndex1 - 1) * l_pountObject->m_sizDim2 * l_pountObject->m_sizDim3 * l_pountObject->m_sizDim4;
                    l_inOffset += (l_inIndex2 - 1) * l_pountObject->m_sizDim3 * l_pountObject->m_sizDim4;
                    l_inOffset += (l_inIndex3 - 1) * l_pountObject->m_sizDim4;
                    l_inOffset += (l_inIndex4 - 1);
                    l_pouniElement = &l_pounaLevel4->m_pouniObjects[l_inIndex4 - 1];
                    l_sizBytes = i_sizCodepoints * sizeof(wchar_t);
                    l_pountObject->m_pounaObject->m_pouniObjects[l_inOffset].m_poszCodepoints = l_pountObject->m_poszCodepoints + l_inOffset * l_sizBytes;
                    l_sizCodepoints = i_sizCodepoints <= l_pouniElement->m_sizCodepoints ? i_sizCodepoints : l_pouniElement->m_sizCodepoints;
                    memcpy(l_pountObject->m_pounaObject->m_pouniObjects[l_inOffset].m_poszCodepoints, l_pouniElement->m_poszCodepoints, l_sizCodepoints * sizeof(wchar_t));
                    l_pountObject->m_pounaObject->m_pouniObjects[l_inOffset].m_sizCodepoints = l_sizCodepoints;
                    l_pountObject->m_pounaObject->m_pouniObjects[l_inOffset].m_sizBytes = l_sizCodepoints * sizeof(wchar_t);
                }
                UnicodeArray_delete(&l_pounaLevel4);
            }
            UnicodeArray_delete(&l_pounaLevel3);
        }
        UnicodeArray_delete(&l_pounaLevel2);
    }
    UnicodeArray_delete(&l_pounaLevel1);
    Unicode_delete(&l_pouniFS);
    Unicode_delete(&l_pouniGS);
    Unicode_delete(&l_pouniRS);
    Unicode_delete(&l_pouniUS);
    return l_pountObject;
}

/**
 * @fn "struct Unicode * Unicode_from_tesseract(const struct UnicodeTesseract * i_pountObject)"
 * @brief Function to join **UnicodeTesseract** object elements in a **Unicode** object
 * @details Creates a **Unicode** object from the elements stored inside a
 * **UnicodeTesseract** object.  Only a new **Unicode** object is created in
 * dynamic memory.  The **Unicode** object will contain a four dimensional
 * dynamic array that mirrors the elements in the **UnicodeTesseract** object.
 * The implementation of the 4-dimensional dynamic array inside a **Unicode**
 * object is structured as follows:
 * - Value contains level 1 subvalues delimited by FS characters
 * - Level 1 subvalues contain level 2 subvalues delimited by GS characters
 * - Level 2 subvalues contain level 3 subvalues delimited by RS characters
 * - Level 3 subvalues contain level 4 subvalues delimited by US characters
 *
 * @note This routine allocates dynamic memory for a new **Unicode**
 * object.  It must be deleted by the calling routine after processing is
 * complete (as appropriate) by a call to **UnicodeTesseract_delete()**.  If
 * not done, a memory leak will be created.
 *
 * #### Example ####
 *
 * @code
 * int l_inCodepoints = 10, l_inDim1 = 10, l_inDim2 = 10, l_inDim3 = 10, l_inDim4 = 10;
 * int l_inLevel1 = 0, l_inLevel2 = 0, l_inLevel3 = 0, l_inLevel4 = 0, l_inElement = 0, l_inSubvalue = 0;
 * struct UnicodeTesseract * l_pountObject = 0;
 * struct Unicode * l_pouniObject = 0;
 * struct Unicode * l_pouniElement = 0;
 * l_pountObject = UnicodeTesseract_new(l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_inElement = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 l_pouniElement = Unicode::from_int(l_inElement);
 *                 assert(l_pouniElement != 0);
 *                 Unicode_set_element(l_pountObject, l_pouniElement, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 Unicode_clear(&l_pouniElement);
 *             }
 *         }
 *     }
 * }
 * l_pouniObject = Unicode_from_tesseract(l_pountObject);
 * l_pountObject = Unicode_to_tesseract(l_pouniObject, l_inCodepoints, l_inDim1, l_inDim2, l_inDim3, l_inDim4);
 * for (l_inLevel1 = 1; l_inLevel1 <= l_inDim1; l_inLevel1++) {
 *     for (l_inLevel2 = 1; l_inLevel2 <= l_inDim2; l_inLevel2++) {
 *         for (l_inLevel3 = 1; l_inLevel3 <= l_inDim3; l_inLevel3++) {
 *             for (l_inLevel4 = 1; l_inLevel4 <= l_inDim4; l_inLevel4++) {
 *                 l_pouniElement = Unicode_get_element(l_pountObject, l_inLevel1, l_inLevel2, l_inLevel3, l_inLevel4);
 *                 assert(l_pouniElement != 0);
 *                 l_inElement = l_pouniElement->to_int();
 *                 l_inSubvalue = (l_inLevel1 - 1) * 1000000 + (l_inLevel2 - 1) * 10000 + (l_inLevel3 - 1) * 100 + (l_inLevel4 - 1);
 *                 assert(l_inElement == l_inSubvalue);
 *             }
 *         }
 *     }
 * }
 * UnicodeTesseract_delete(&l_pountObject);
 * @endcode
 *
 * @param[in] i_pountObject = Input pointer to **UnicodeTesseract** object
 * @retval "struct Unicode *" = Pointer to newly allocated **Unicode** object
 * @exception abort(3) Aborts if **i_pouniObject** is null
 * @exception assert(3) Aborts if **Unicode_new** call returns null
 * @exception assert(3) Aborts if **realloc(3)** call returns null
 */

struct Unicode * Unicode_from_tesseract(const struct UnicodeTesseract * i_pountObject)
{
    const int l_inBytes = 2097152;  //!< Expanding buffer extent size of 2 megabytes
    struct Unicode * l_pouniObject = 0;
    wchar_t * l_powzObject = 0;
    wchar_t * l_powzElement = 0;
    wchar_t l_wcFS = L'\x1C';
    wchar_t l_wcGS = L'\x1D';
    wchar_t l_wcRS = L'\x1E';
    wchar_t l_wcUS = L'\x1F';
    int l_inIndex1 = 0;
    int l_inIndex2 = 0;
    int l_inIndex3 = 0;
    int l_inIndex4 = 0;
    int l_inElement = 0;
    int l_inOffset = 0;
    int l_inCodepoints = 0;
 
    if (i_pountObject == 0) {
        fprintf(stderr, "%s(%d) = i_pountObject = %p\n",
            __FILE__, __LINE__, i_pountObject);
        abort();
    }
    l_pouniObject = Unicode_new();
    l_pouniObject->m_poszCodepoints = (char *) calloc(1, l_inBytes);  //!< Buffer extent size
    assert(l_pouniObject->m_poszCodepoints != 0);
    l_powzObject = (wchar_t *) l_pouniObject->m_poszCodepoints;
    for (l_inIndex1 = 1; (size_t) l_inIndex1 <= i_pountObject->m_sizDim1; l_inIndex1++) {
        if (l_inIndex1 == 1) {
            l_powzObject[l_inOffset++] = l_wcFS;
            l_pouniObject->m_sizCodepoints++;
            l_pouniObject->m_sizBytes += sizeof(wchar_t);
            if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                assert(l_powzObject != 0);
            }
        }
        for (l_inIndex2 = 1; (size_t) l_inIndex2 <= i_pountObject->m_sizDim2; l_inIndex2++) {
            if (l_inIndex2 == 1) {
                l_powzObject[l_inOffset++] = l_wcGS;
                l_pouniObject->m_sizCodepoints++;
                l_pouniObject->m_sizBytes += sizeof(wchar_t);
                if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                    l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                    assert(l_powzObject != 0);
                }
            }
            for (l_inIndex3 = 1; (size_t) l_inIndex3 <= i_pountObject->m_sizDim3; l_inIndex3++) {
                if (l_inIndex3 == 1) {
                    l_powzObject[l_inOffset++] = l_wcRS;
                    l_pouniObject->m_sizCodepoints++;
                    l_pouniObject->m_sizBytes += sizeof(wchar_t);
                    if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                        l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                        assert(l_powzObject != 0);
                    }
                }
                for (l_inIndex4 = 1; (size_t) l_inIndex4 <= i_pountObject->m_sizDim4; l_inIndex4++) {
                    if (l_inIndex4 == 1) {
                        l_powzObject[l_inOffset++] = l_wcUS;
                        l_pouniObject->m_sizCodepoints++;
                        l_pouniObject->m_sizBytes += sizeof(wchar_t);
                        if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                            l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                            assert(l_powzObject != 0);
                        }
                    }
                    l_inElement = (l_inIndex1 - 1) * i_pountObject->m_sizDim2 * i_pountObject->m_sizDim3 * i_pountObject->m_sizDim4;
                    l_inElement += (l_inIndex2 - 1) * i_pountObject->m_sizDim3 * i_pountObject->m_sizDim4;
                    l_inElement += (l_inIndex3 - 1) * i_pountObject->m_sizDim4;
                    l_inElement += (l_inIndex4 - 1);
                    l_powzElement = (wchar_t *) i_pountObject->m_pounaObject->m_pouniObjects[l_inElement].m_poszCodepoints;
                    for (l_inCodepoints = 0; (size_t) l_inCodepoints < i_pountObject->m_pounaObject->m_pouniObjects[l_inElement].m_sizCodepoints; l_inCodepoints++) {
                        l_powzObject[l_inOffset++] = l_powzElement[l_inCodepoints];
                        if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                            l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                            assert(l_powzObject != 0);
                        }
                    }
                    l_pouniObject->m_sizCodepoints += l_inCodepoints;
                    l_pouniObject->m_sizBytes += l_inCodepoints * sizeof(wchar_t);
                    l_powzObject[l_inOffset++] = l_wcUS;
                    l_pouniObject->m_sizCodepoints++;
                    l_pouniObject->m_sizBytes += sizeof(wchar_t);
                    if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                        l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                        assert(l_powzObject != 0);
                    }
                }
                l_powzObject[l_inOffset++] = l_wcRS;
                l_pouniObject->m_sizCodepoints++;
                l_pouniObject->m_sizBytes += sizeof(wchar_t);
                if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                    l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                    assert(l_powzObject != 0);
                }
            }
            l_powzObject[l_inOffset++] = l_wcGS;
            l_pouniObject->m_sizCodepoints++;
            l_pouniObject->m_sizBytes += sizeof(wchar_t);
            if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
                l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
                assert(l_powzObject != 0);
            }
        }
        l_powzObject[l_inOffset++] = l_wcFS;
        l_pouniObject->m_sizCodepoints++;
        l_pouniObject->m_sizBytes += sizeof(wchar_t);
        if (l_inOffset % (l_inBytes / sizeof(wchar_t)) == 0) {
            l_powzObject = (wchar_t *) realloc(l_powzObject, l_inOffset * sizeof(wchar_t) + l_inBytes);
            assert(l_powzObject != 0);
        }
    }
    l_powzObject[l_pouniObject->m_sizCodepoints] = 0;
    l_pouniObject->m_poszCodepoints = (char *) realloc(l_powzObject, l_pouniObject->m_sizBytes + sizeof(wchar_t));
    return l_pouniObject;
}

#ifdef __cplusplus
}
#endif

Part 2. C Header File

Planned release of source code

Rather than hit you with all of the project files at one time, I will post the source code online, one file per post, so that I can explain a few things that you may need to know as we go along to getting your own version working. Here are the parts planned to be released to the public domain as we go:

  • Part 2. C Header File
  • Part 3. C Source File
  • Part 4. Perl Source File
  • Part 5. Build and Run Scripts
  • Part 6. SWIG Interface File
The Unicode C header file

Before trying to understand what SWIG brings to the table, it is best to understand what the C code does and it’s structure. The C header file does a good job of summarizing what the Unicode module does, at least in terms of the functions it performs. Here is the source code for the include file:

/**
 * vim: fileencoding=utf8
 * @file unicode.h - SWIG Perl5 version
 * @brief Declarations for struct Unicode and SWIG extended member functions
 * @version 1.2
 *
 * This is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef _UNICODE_H
#define _UNICODE_H

#ifdef __cplusplus
extern "C" {
#endif

#define UNICODE_BUFFER_MAX 1024         //!< Maximum number of bytes of stack-allocated temporary buffer

#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <iconv.h>
#include <locale.h>
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>

/**
 * @struct Unicode unicode.h
 * @brief Struct defining members of a **Unicode** object
 * @details A **Unicode** object consists of the properties that are used to
 * contain both the metadata and content of the **Unicode** object. The
 * codepoints are encoded internally as UTF32LE which is normally represented
 * by **wchar_t** characters. However, the **char** datatype was chosen to be
 * more compatible with the **iconv(3)** C library function which does the
 * conversions between encodings.
 * @see Unicode_new(), Unicode_delete()
 */

struct Unicode {
    char * m_poszCodepoints;    //!< Pointer to **char** buffer of UTF32LE encoded codepoints
    size_t m_sizCodepoints;     //!< Number of UTF32LE codepoints not including NULL terminator
    size_t m_sizBytes;          //!< Number of **char** bytes allocated to store UTF32LE codepoints
};

/**
 * @struct UnicodeArray unicode.h
 * @brief Struct defining members of a **UnicodeArray** object
 * @details A **UnicodeArray** object consists of the properties that are used
 * to contain both the metadata and content of a **UnicodeArray** object.  Note
 * that a single allocation for an array of **Unicode** objects is performed,
 * and not an array of pointers that would require multiple heap allocations
 * for the **Unicode** objects.  This eliminates the need for allocating and
 * deallocating each **Unicode** object separately and greatly speeds up
 * processing. The **Unicode** objects in the array do not own the data they
 * point to, that is, you should treat the array objects like you would a
 * **weak pointer** in C++. The **UnicodeArray** object should be deleted with
 * **UnicodeArray_delete()** by the calling routine.
 * @see UnicodeArray_new(), UnicodeArray_delete(), Unicode_split(), Unicode_join()
 */

struct UnicodeArray {
    struct Unicode * m_pouniObjects;    //!< Pointer to heap allocation of array of **Unicode** objects
    size_t m_sizObjects;                //!< Number of **Unicode** objects in heap allocated array
};

/**
 * @struct UnicodeTesseract unicode.h
 * @brief Struct defining members of a **UnicodeTesseract** multi-dimensional array
 * @details A **UnicodeTesseract** object consists of the properties that are
 * used to operate on a **Unicode** object that has been converted into a
 * multi-dimensional array of up to four dimensions with almost no overhead.
 * This allows extremely fast direct access to elements within the
 * **UnicodeTesseract** object when performing operations like
 * cross-tabulating, cross-referencing or other such individual element access
 * intensive operations which can cause a lot of string manipulation overhead
 * when using the normal **Unicode_..._subvalue()** type functions. To use this
 * feature, first one or more **Unicode** objects are converted into
 * **UnicodeTesseract** objects.  After the processing is performed on the
 * indiviual elements as needed on the **UnicodeTesseract** objects, the
 * **UnicodeTesseract** objects can then be converted back into new **Unicode**
 * objects.
 *
 * @note Internally the four-dimensional cube (tesseract) is represented by one
 * large **UnicodeArray** object allocated in a separate virtual memory address
 * space. The 1-based dimensional field values allow bounds checking and an
 * easy way to calculate the offset into the **UnicodeArray** object (see list
 * below). Each **Unicode** object inside the tesseract has a limited size in
 * codepoints that is specified in the call to **UnicodeTesseract_new()**. If
 * any operations exceed this value on any one of the **Unicode** objects, this
 * could result in undefined behavior. A null terminator codepoint may also
 * need to be factored in depending on what operations are performed.
 *
 * @note This can handle huge amounts of memory usage with large dimensional
 * values.  This is a **Linux** specific feature that uses **mmap()** and other
 * related system calls to generate a separate virtual memory address space for
 * this structure's memory, backed up by a temporary file that acts like a
 * separate swap area for the memory. The **mmap()** set of functions
 * automatically optimizes caching the data in memory for the fastest access
 * possible.
 *
 * @see UnicodeTesseract_new(), UnicodeTesseract_delete(), Unicode_to_tesseract(), Unicode_from_tesseract(), Unicode_get_element(), Unicode_set_element()
 */

struct UnicodeTesseract {
    size_t m_sizDim1;                           //!< Number of Unicode objects in first dimension
    size_t m_sizDim2;                           //!< Number of Unicode objects in second dimension
    size_t m_sizDim3;                           //!< Number of Unicode objects in third dimension
    size_t m_sizDim4;                           //!< Number of Unicode objects in fourth dimension
    size_t m_sizElements;                       //!< Total number of Unicode object elements
    size_t m_sizCodepoints;                     //!< Fixed maximum size of elements in Codepoints
    size_t m_sizBytes;                          //!< Total bytes of virtual memory allocated
    int m_inFile;                               //!< File descriptor of temporary virtual memory file
    struct UnicodeArray * m_pounaObject;        //!< Pointer to allocated memory for UnicodeArray object
    char * m_poszCodepoints;                    //!< Pointer to allocated memory for Unicode codepoints
};

struct Unicode * Unicode_new(void);
void Unicode_delete(struct Unicode ** u_popouniObject);
struct UnicodeArray * UnicodeArray_new(size_t i_sizElements);
void UnicodeArray_delete(struct UnicodeArray ** u_popounaObjects);
struct UnicodeTesseract * UnicodeTesseract_new(size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4);
void UnicodeTesseract_delete(struct UnicodeTesseract ** u_popountObject);
void Unicode_clear(struct Unicode * u_pouniObject);
int Unicode_empty(const struct Unicode * i_pouniObject);
size_t Unicode_codepoints(const struct Unicode * i_pouniObject);
size_t Unicode_bytes(const struct Unicode * i_pouniObject);
size_t Unicode_import_string(struct Unicode * u_pouniObject, const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding);
char * Unicode_export_string(const struct Unicode * i_pouniObject, size_t i_sizMaxbytes, const char * i_poszEncoding);
struct Unicode * Unicode_from_string(const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding);
struct Unicode * Unicode_from_int(int i_inValue);
struct Unicode * Unicode_from_long(long i_loValue);
struct Unicode * Unicode_from_longlong(long long i_llValue);
struct Unicode * Unicode_from_float(float i_flValue);
struct Unicode * Unicode_from_double(double i_doValue);
struct Unicode * Unicode_from_longdouble(long double i_ldValue);
int Unicode_to_int(const struct Unicode * i_pouniObject);
long Unicode_to_long(const struct Unicode * i_pouniObject);
long long Unicode_to_longlong(const struct Unicode * i_pouniObject);
float Unicode_to_float(const struct Unicode * i_pouniObject);
double Unicode_to_double(const struct Unicode * i_pouniObject);
long double Unicode_to_longdouble(const struct Unicode * i_pouniObject);
void Unicode_copy(struct Unicode * o_pouniObject, const struct Unicode * i_pouniObject);
void Unicode_append(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject);
void Unicode_append_multiple(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject, size_t i_sizCount);
void Unicode_swap(struct Unicode * u_pouniObject, struct Unicode * u_pouniSwap);
int Unicode_find(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniFind, int i_inCount);
struct Unicode * Unicode_extract(const struct Unicode * i_pouniObject, size_t i_sizOffset, size_t i_inCount);
void Unicode_replace(struct Unicode * i_pouniObject, const struct Unicode * i_pouniReplace, size_t i_sizOffset, size_t i_sizCount);
int Unicode_compare_ascendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare);
int Unicode_compare_descendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare);
int Unicode_compare_ascendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare);
int Unicode_compare_descendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare);
struct Unicode * Unicode_uppercase(const struct Unicode * i_pouniObject);
struct Unicode * Unicode_lowercase(const struct Unicode * i_pouniObject);
struct Unicode * Unicode_swapcase(const struct Unicode * i_pouniObject);
struct Unicode * Unicode_concatenate(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniConcatenate);
struct UnicodeArray * Unicode_split(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniDelimiters, int i_inTrim);
struct Unicode * Unicode_join(struct UnicodeArray * i_pounaObject, const struct Unicode * i_pouniDelimiter, int i_inTrim);
char ** Unicode_from_array(const struct UnicodeArray * i_pounaObject);
void Unicode_to_array(struct UnicodeArray * u_pounaObject, const char ** i_poposzValues);
char ** Unicode_from_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS);
void Unicode_to_subvalues(struct Unicode * u_pouniObject, const char ** i_poposzValues, int i_inFS, int i_inGS, int i_inRS);
int Unicode_count_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS);
void Unicode_sort_subvalues(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inSort);
int Unicode_locate_subvalue(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniKey, int i_inFS, int i_inGS, int i_inRS, int i_inSort);
struct Unicode * Unicode_extract_subvalue(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
void Unicode_replace_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniReplace, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
void Unicode_insert_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniInsert, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
void Unicode_append_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniAppend, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
void Unicode_delete_subvalue(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
long Unicode_get_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
void Unicode_set_codepoint(struct Unicode * i_pouniObject, size_t i_sizOffset, long i_loCodepoint);
int Unicode_find_codepoint (const struct Unicode * i_pouniObject, long i_loCodepoint, int i_inCount);
int Unicode_isalnum_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isalpha_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_islower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isxdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_iscntrl_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isgraph_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isspace_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isblank_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_isprint_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_ispunct_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
long Unicode_tolower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
long Unicode_toupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset);
int Unicode_save(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding);
int Unicode_load(struct Unicode * o_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding);
struct UnicodeTesseract * Unicode_to_tesseract(const struct Unicode * i_pouniObject, size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4);
struct Unicode * Unicode_from_tesseract(const struct UnicodeTesseract * i_pountObject);
void Unicode_set_element(struct UnicodeTesseract * u_pountObject, const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
struct Unicode * Unicode_get_element(const struct UnicodeTesseract * i_pountObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS);

#ifdef __cplusplus
}
#endif

#endif        /* _UNICODE_H */

The Unicode fundamental functions

Let’s go through the file and I will explain both the function purpose, and the meaning of it’s parameters to give you an idea of what each function does.

  • struct Unicode – This is the basic “class” for Unicode objects in Perl. It basically contains the raw Unicode data stored in UTF32LE (4-byte UTF codepoints in Little Endian order), as well as the number of codepoints and bytes used.
  • struct UnicodeArray – An array structure containing the start of the array, and the number of Unicode Objects stored in the array.
  • struct UnicodeTesseract – A four-dimensional array structure whose storage is stored in it’s own virtual memory address space using “mmap()”. This is a Linux/Unix only feature, and will not work on Windows. It contains the fixed number of objects possible in each dimension, the maximum number of Unicode codepoints in any element, and the total number of actual Unicode objects stored in the Tesseract.
  • struct Unicode * Unicode_new(void) – Creates a new Unicode object.
  • void Unicode_delete(struct Unicode ** u_popouniObject) – Deallocates memory for a Unicode object and it’s pointer.
  • struct UnicodeArray * UnicodeArray_new(size_t i_sizElements) – Creates a new UnicodeArray object. You pass the number of elements you want in the array.
  • void UnicodeArray_delete(struct UnicodeArray ** u_popounaObjects) – Deallocates memory for a UnicodeArray object and it’s pointer.
  • struct UnicodeTesseract * UnicodeTesseract_new(size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4) – Creates a new UnicodeTesseract object. You pass the maximum number of codepoints per element, and the number of elements in each dimension respectively.
  • void UnicodeTesseract_delete(struct UnicodeTesseract ** u_popountObject) – Deallocates memory for a UnicodeTesseract object and it’s pointer.
  • void Unicode_clear(struct Unicode * u_pouniObject) – Clears a Unicode object, deallocating memory if allocated.
  • int Unicode_empty(const struct Unicode * i_pouniObject) – Returns true (1) if the Unicode object has no data, else returns false (0).
  • size_t Unicode_codepoints(const struct Unicode * i_pouniObject) – Returns the number of UNICODE codepoints (UTF32LE elements) in a Unicode object.
  • size_t Unicode_bytes(const struct Unicode * i_pouniObject) – Returns the number of bytes used to store the UNICODE codepoints (UTF32LE elements) in a Unicode object.
  • size_t Unicode_import_string(struct Unicode * u_pouniObject, const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding) – Imports a string into an existing Unicode object. You pass a pointer to the buffer, the maximum number of bytes to convert, and the encoding of the buffer’s contents (e.g. “LATIN1”). If the maximum number of bytes is zero, then strlen(3) is used to determine the length of the input buffer.
  • char * Unicode_export_string(const struct Unicode * i_pouniObject, size_t i_sizMaxbytes, const char * i_poszEncoding – Export an existing Unicode object’s contents into a string. You pass the maximum size of the output buffer in bytes, and the expected encoding of the string (e.g. “UTF-8”).
  • struct Unicode * Unicode_from_string(const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding) – Creates a new Unicode object initialized with the contents of a string. You pass the maximum number of input bytes to convert, and the encoding of the string (e.g. “LATIN1”). If the maximum number of bytes is zero, then strlen(3) will be used to determine the length of the input buffer.
  • struct Unicode * Unicode_from_int(int i_inValue) – Create a new Unicode object initialized from an integer value. You pass the integer value.
  • struct Unicode * Unicode_from_long(long i_loValue) – Create a new Unicode object initialized from a long integer value. You pass the long integer value.
  • struct Unicode * Unicode_from_longlong(long long i_llValue) – Create a new Unicode object from a long long integer value. You pass the long long integer value.
  • struct Unicode * Unicode_from_float(float i_flValue) – Create a new Unicode object from a float value. You pass the float value.
  • struct Unicode * Unicode_from_double(double i_doValue) – Create a new Unicode object from a double value. You pass the double value.
  • struct Unicode * Unicode_from_longdouble(long double i_ldValue) – Create a new Unicode object from a long double value. You pass the long double value.
  • int Unicode_to_int(const struct Unicode * i_pouniObject) – Convert an existing Unicode object value to an integer.
  • long Unicode_to_long(const struct Unicode * i_pouniObject) – Convert an existing Unicode object value to a long integer.
  • long long Unicode_to_longlong(const struct Unicode * i_pouniObject) – Convert an existing Unicode object value to a long long integer.
  • float Unicode_to_float(const struct Unicode * i_pouniObject) – Convert an existing Unicode object value to a float value.
  • double Unicode_to_double(const struct Unicode * i_pouniObject) – Convert an existing Unicode object value to a double value.
  • long double Unicode_to_longdouble(const struct Unicode * i_pouniObject) – Convert an existing Unicode object value to a long double value.
  • void Unicode_copy(struct Unicode * o_pouniObject, const struct Unicode * i_pouniObject) – Copy one Unicode object value to another existing Unicode object.
  • void Unicode_append(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject) – Append the value of one Unicode object to another Unicode object value.
  • void Unicode_append_multiple(struct Unicode * u_pouniObject, const struct Unicode * i_pouniObject, size_t i_sizCount) – Append the value of one Unicode object to another Unicode object multiple times in one operation.
  • void Unicode_swap(struct Unicode * u_pouniObject, struct Unicode * u_pouniSwap) – Swap the contents of two existing Unicode objects.
  • int Unicode_find(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniFind, int i_inCount) – Finds an existing Unicode object value inside another existing Unicode object. You pass the count of number times to skip (0 = find first match, 1 or more = skip this number of matches, -1 or less match this number of times right-to-left from the end of the Unicode object value.
  • struct Unicode * Unicode_extract(const struct Unicode * i_pouniObject, size_t i_sizOffset, size_t i_inCount) – Extract a number of UNICODE codepoints starting at an offset inside the existing Unicode object. You pass the offset in codepoints to start, and the maximum number of codepoints to return.
  • void Unicode_replace(struct Unicode * i_pouniObject, const struct Unicode * i_pouniReplace, size_t i_sizOffset, size_t i_sizCount) – Replace a number of UNICODE codepoints in an existing Unicode object starting at an offset. You pass the offset in codepoints to start, and the maximum number of codepoints to replace.
  • int Unicode_compare_ascendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare) – Compare two Unicode objects’ values as strings in ascending order. Returns -1, 0 or 1 if the first Unicode object is less than, equal to or more than the second Unicode object value.
  • int Unicode_compare_descendingstring(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare) – Compare two Unicode objects’ values as strings in descending order. Returns -1, 0 or 1 if the first Unicode object is more than, equal to or less than the second Unicode object value.
  • int Unicode_compare_ascendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare) – Compare two Unicode objects’ values as numbers in ascending order. Returns -1, 0 or 1 if the first Unicode object is less than, equal to or more than the second Unicode object value.
  • int Unicode_compare_descendingnumeric(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniCompare) – Compare two Unicode objects’ values as numbers in descending order. Returns -1, 0 or 1 if the first Unicode object is more than, equal to or less than the second Unicode object value.
  • struct Unicode * Unicode_uppercase(const struct Unicode * i_pouniObject) – Returns a new Unicode object whose value is the uppercased value of the existing Unicode object.
  • struct Unicode * Unicode_lowercase(const struct Unicode * i_pouniObject) – Returns a new Unicode object whose value is the lowercased value of the existing Unicode object.
  • struct Unicode * Unicode_swapcase(const struct Unicode * i_pouniObject) – Returns a new Unicode object whose value is the swapcased value of the existing Unicode object. Uppercase becomes lowercase and vice versa.
  • struct Unicode * Unicode_concatenate(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniConcatenate) – Concatenates values of two existing Unicode objects and returns the concatenated value in a new Unicode object.
  • struct UnicodeArray * Unicode_split(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniDelimiters, int i_inTrim) – Splits a Unicode object codepoints into tokens using delimiters. The tokens are stored as elements in the returned UnicodeArray object. There can be more than one delimiter where each delimiter is a single UNICODE codepoint. You pass a value of 0 to 3 to tell how to process delimiters. 0 means each delimiter is significant between tokens. 1 means multiple delimiters are trimmed to one delimiter between tokens. 2 means each delimiter is significant at the start, end and between tokens. 3 means mulitple delimiters are trimmed to one delimiter at start, end and between tokens.
  • struct Unicode * Unicode_join(struct UnicodeArray * i_pounaObject, const struct Unicode * i_pouniDelimiter, int i_inTrim) – Joins UnicodeArray object elements together separated by a delimiter. You pass a value of 0 to 3 to tell how to process delimiters. 0 means a delimiter will be put between all tokens. 1 means a delimiter will be put between all non-empty tokens. 2 means a delimiter will be put at the start, end and between all tokens. 3 means a delimiter will be put at the start, end and between all non-empty tokens.
  • char ** Unicode_from_array(const struct UnicodeArray * i_pounaObject) – Converts a UnicodeArray object into a C array of string pointers.
  • void Unicode_to_array(struct UnicodeArray * u_pounaObject, const char ** i_poposzValues) – Converts a C array of string pointers into a UnicodeArray object.
Pick OS like subvalue functions

This was an added functionality to the Unicode library to enable storing arrays of arrays up to 4 dimensions (or levels) deep. The top level of subvalues is based on the ASCII control character delimiter FS (0x1c). The second, third and fourth levels are based upon the GS (0x1d), RS (0x1e) and US (0x1f) control characters respectively. This allows for a four-dimensional array to be stored inside a Unicode object. Indexing begins with 1 for all dimensions. Zero indicates “ignore this dimension”. It is an error to have a non-zero dimension after a zero indexed dimension to the left. For example, if GS is zero then RS and US must be zero. The functions check for this, and will abort if this rule is violated.

  • char ** Unicode_from_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS) – Converts a group of subvalues in an existing Unicode object into a C array of string pointers.
  • void Unicode_to_subvalues(struct Unicode * u_pouniObject, const char ** i_poposzValues, int i_inFS, int i_inGS, int i_inRS) – Converts a C array of string pointers to a group of subvalues inside an existing Unicode object.
  • void Unicode_sort_subvalues(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inSort) – Sorts the subvalue level below the deepest non-zero dimension specified. You pass to the sorting parameter to mean to sort as ascending strings. 1 means sort as descending strings. 2 means sort as ascending numbers. 3 means sort as descending numbers.
  • int Unicode_locate_subvalue(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniKey, int i_inFS, int i_inGS, int i_inRS, int i_inSort) – Returns the offset (1 based) of the located subvalue in a group of subvalues below the deepest non-zero dimension specified. You pass the Unicode object value to search for in the existing Unicode object, and the sort parameter where 0 means unordered left-to-right value comparison scan. 1 means assume subvalues sorted as ascending string values (quicksort). 2 means assume subvalues are sorted as descending string values (quicksort), 3 means assume subvalues are sorted as ascending numbers (quicksort), 4 means assume subvalues are sorted as descending numbers (quicksort).
  • struct Unicode * Unicode_extract_subvalue(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS) – Extracts a single subvalue from within a Unicode object and returns the value in a new Unicode object. You pass the dimensions that index the subvalue.
  • void Unicode_replace_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniReplace, int i_inFS, int i_inGS, int i_inRS, int i_inUS) – Replace a subvalue in an existing Unicode object. You pass the replacement value in a Unicode object, and the dimensions that index the subvalue.
  • void Unicode_insert_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniInsert, int i_inFS, int i_inGS, int i_inRS, int i_inUS) – Inserts a new subvalue in an existing Unicode object. You pass the inserted subvalue in a Unicode object, and the dimensions that index the newly inserted subvalue.
  • void Unicode_append_subvalue(struct Unicode * u_pouniObject, const struct Unicode * i_pouniAppend, int i_inFS, int i_inGS, int i_inRS, int i_inUS) – Appends a subvalue to a group of subvalues inside an existing Unicode object. you pass the appended value and the dimensions where to append the subvalue. For example:
    Unicode_append_subvalue(l_pouniObject, l_pouniAppend, 3, 3, 3, 3);
    appends subvalue after third level 4 subvalue inside third level 3 subvalue inside third level 2 subvalue inside third level 1 subvalue.
  • void Unicode_delete_subvalue(struct Unicode * u_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS) – Deletes a subvalue in the existing Unicode object indexed by the passed dimensions.
Single Unicode codepoint functions

When Unicode objects are being used to store full values (strings) and no subvalues, then you can process a single codepoint within a Unicode object. These functions were added later to allow that functionality for UNICODE codepoints.

  • long Unicode_get_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns the Perl ord() value as a long integer of the codepoint. You pass the 0-based offset into the Unicode object data buffer.
  • void Unicode_set_codepoint(struct Unicode * i_pouniObject, size_t i_sizOffset, long i_loCodepoint) – Replaces (sets) the value of a codepoint inside an existing Unicode object. you pass the Perl ord() value and the offset inside the Unicode object buffer.
  • int Unicode_find_codepoint (const struct Unicode * i_pouniObject, long i_loCodepoint, int i_inCount) – Finds a single UNICODE codepoint inside an existing Unicode object and returns the 0-based offset. You pass the codepoint as a Perl ord() value and the count of matches to skip. Scans left-to-right if zero or positive, or right-to-left if negative.
  • int Unicode_isalnum_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:alnum:] character class, else returns false (0).
  • int Unicode_isalpha_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:alpha:] character class, else returns false (0).
  • int Unicode_islower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:lower:] character class, else returns false (0).
  • int Unicode_isupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:upper:] character class, else returns false (0).
  • int Unicode_isdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:digit:] character class, else returns false (0).
  • int Unicode_isxdigit_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:xdigit:] character class, else returns false (0).
  • int Unicode_iscntrl_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:cntrl:] character class, else returns false (0).
  • int Unicode_isspace_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:space:] character class, else returns false (0).
  • int Unicode_isblank_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:blank:] character class, else returns false (0).
  • int Unicode_isprint_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:print:] character class, else returns false (0).
  • int Unicode_ispunct_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns true (1) if codepoint at passed offset matches POSIX [:punct:] character class, else returns false (0).
  • long Unicode_tolower_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns the Perl ord() value of the lowercase value of the codepoint at the specified offset. Does not modify the existing codepoint.
  • long Unicode_toupper_codepoint(const struct Unicode * i_pouniObject, size_t i_sizOffset) – Returns the Perl ord() value of the uppercase value of the codepoint at the specified offset. Does not modify the existing codepoint.
File operations

These functions were added to be able to save and/or load a complete file in any encoding from/to a Unicode object. They are very useful for converting files from one encoding to another.

  • int Unicode_save(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding) – Saves the contents of the existing Unicode object to a file in the filesystem whose path is specified. The specified encoding is what the codepoints will be converted into inside the file. A good example would be saving a file in UTF-16 for use with Windows.
  • int Unicode_load(struct Unicode * o_pouniObject, const struct Unicode * i_pouniPath, const char * i_poszEncoding) – Loads the contents of a file in the filesystem into an existing Unicode object whose path is specified. The specified encoding is what the data inside the file is expected to be in. A good example is loading a file in UTF-16 encode that was created in Windows.
Large data support

Sometimes data needs to be loaded that might not fit in memory. Linux’s mmap() system call allows software to create it’s own virtual memory space, backed up by a separate virtual memory swap file. The UnicodeTesseract object was developed to allow a scripting language like Perl to access large datasets without running out of memory. Tables or arrays up to 4 dimensions can be created and used with this feature possibly with sizes in the tens of gigabytes (or more). The UnicodeTesseract functions below make access fast and simple.

  • struct UnicodeTesseract * Unicode_to_tesseract(const struct Unicode * i_pouniObject, size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4) – Converts the data inside an existing Unicode object into a new UnicodeTesseract object to offload the data into a separate virtual memory space. You pass the maximum number of codepoints for each element, and the size of all four dimensions, respectively.
  • struct Unicode * Unicode_from_tesseract(const struct UnicodeTesseract * i_pountObject) – An existing UnicodeTesseract object is converted into a new Unicode object. This can be useful to store the data to disk for example for later processing.
  • void Unicode_set_element(struct UnicodeTesseract * u_pountObject, const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS) – High-performance function to set the value of one of the existing UnicodeTesseract object’s elements.
  • struct Unicode * Unicode_get_element(const struct UnicodeTesseract * i_pountObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS) – High-performance function to get the value of an existing element inside an existing UnicodeTesseract object.

This has been a rather long introduction to the Unicode library as it exists. One objection that might be raised is that the “Unicode” package name may be taken by an existing CPAN module. This is okay, because SWIG allows you to alias your interface package name to whatever you want.

This introduction to the Unicode library will help you understand better the design of the Unicode library source code that we will cover in the next part.

Part 1. Small Intro to SWIG

To start off with, SWIG stands for “Simplified Wrapper and Interface Generator”. It’s primary purpose is to allow a developer to integrate C/C++ code into both scripting and non-scripted languages for performance boost or enhanced functionality. One of the most important aspects for me is that SWIG requires no modifications to the underlying C/C++ code. SWIG supports several target languages such as Perl, PHP, Python, D, Go, Guile, Java, Javascript, Lua, OCaml, Octave, R, Ruby, Scilab and Tcl/Tk (and perhaps more).

SWIG has an excellent site with version-dependent documentation as well as a WIKI with user-contributed content, which I heartily recommend you browse for more information.

The SWIG main site is at: http://www.swig.org

To use SWIG with Perl, you need to create a SWIG interface file which has a file extension of “.i” (dot I). In it, you tell SWIG how to process your source code:

  • what routines return pointers to objects allocated on the heap
  • what (if any) defined C functions to ignore
  • how to shorten the C function names in the target script by ignoring a library prefix
  • tell SWIG what C headers to import
  • import any custom C code to add functionality not already in the C library code
  • extend C structs to give them constructors and destructors in the script
  • define any operator overloading you would like to use in the script language
  • and more…

Here is the SWIG interface file we will be using for this project named “unicode.i” which we will cover in more detail in a later post. It is a little over 100 lines long, and that was all that was needed to get SWIG to generate the appropriate XS code to import the C library into Perl.

/* -----------------------------------------------------------------------
 * vim: fileencoding=utf8
 *
 * unicode.i
 *
 * SWIG typemaps for Unicode module
 * ----------------------------------------------------------------------- */

%module Unicode
%newobject Unicode_export_string(const struct Unicode * i_pouniObject, size_t i_sizMaxbytes, const char * i_poszEncoding);
%newobject Unicode_from_string(const char * i_poszString, size_t i_sizMaxbytes, const char * i_poszEncoding);
%newobject Unicode_from_int(int i_inValue);
%newobject Unicode_from_long(long i_loValue);
%newobject Unicode_from_longlong(long long i_llValue);
%newobject Unicode_from_float(float i_flValue);
%newobject Unicode_from_double(double i_doValue);
%newobject Unicode_from_longdouble(long double i_ldValue);
%newobject Unicode_extract(const struct Unicode * i_pouniObject, size_t i_sizOffset, size_t i_inCount);
%newobject Unicode_uppercase(const struct Unicode * i_pouniObject);
%newobject Unicode_lowercase(const struct Unicode * i_pouniObject);
%newobject Unicode_swapcase(const struct Unicode * i_pouniObject);
%newobject Unicode_concatenate(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniConcatenate);
%newobject Unicode_split(const struct Unicode * i_pouniObject, const struct Unicode * i_pouniDelimiters, int i_inTrim);
%newobject Unicode_join(struct UnicodeArray * i_pounaObject, const struct Unicode * i_pouniDelimiter, int i_inTrim);
%newobject Unicode_from_array(const struct UnicodeArray * i_pounaObject);
%newobject Unicode_from_subvalues(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS);
%newobject Unicode_extract_subvalue(const struct Unicode * i_pouniObject, int i_inFS, int i_inGS, int i_inRS, int i_inUS);
%newobject Unicode_to_tesseract(const struct Unicode * i_pouniObject, size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4);
%newobject Unicode_from_tesseract(const struct UnicodeTesseract * i_pountObject);

%ignore Unicode_new;
%ignore Unicode_delete;
%ignore UnicodeArray_new;
%ignore UnicodeArray_delete;
%ignore UnicodeTesseract_new;
%ignore UnicodeTesseract_delete;
%rename("%(strip:[Unicode_])s") "";

/* Includes the header in the wrapper code */
%{
#include "unicode.h"
%}
 
/* This tells SWIG to treat char ** as a special case */
%typemap(in) char ** {
    AV *tempav;
    I32 len;
    int i;
    SV  **tv;
    if (!SvROK($input))
        croak("Argument $argnum is not a reference.");
    if (SvTYPE(SvRV($input)) != SVt_PVAV)
        croak("Argument $argnum is not an array.");
    tempav = (AV*)SvRV($input);
    len = av_len(tempav);
    $1 = (char **) malloc((len+2)*sizeof(char *));
    for (i = 0; i <= len; i++) {
        tv = av_fetch(tempav, i, 0);
        $1[i] = (char *) SvPV(*tv, PL_na);
    }
    $1[i] = NULL;
};

/* Creates a new Perl array and places a NULL-terminated char ** into it */
%typemap(out) char ** {
    AV *myav;
    SV **svs;
    int i = 0, len = 0;
    /* Figure out how many elements we have */
    while ($1[len]) len++;
    svs = (SV **) malloc(len*sizeof(SV *));
    for (i = 0; i < len ; i++) {
        svs[i] = sv_newmortal();
        sv_setpv((SV*)svs[i], $1[i]);
    };
    myav = av_make(len, svs);
    free(svs);
    $result = newRV_noinc((SV*)myav);
    sv_2mortal($result);
    argvi++;
}

%extend Unicode {
    Unicode() { 
        return Unicode_new(); 
    } 
    ~Unicode() { 
        Unicode_delete(&$self); 
    } 
}

%extend UnicodeArray {
    UnicodeArray(size_t i_sizElements) { 
        return UnicodeArray_new(i_sizElements); 
    } 
    ~UnicodeArray() { 
        UnicodeArray_delete(&$self); 
    } 
    struct Unicode * get_element(size_t i_sizOffset) {
        return(&($self->m_pouniObjects[i_sizOffset]));
    }
    void set_element(const struct Unicode * i_pouniObject, size_t i_sizOffset) {
        $self->m_pouniObjects[i_sizOffset].m_poszCodepoints = i_pouniObject->m_poszCodepoints;
        $self->m_pouniObjects[i_sizOffset].m_sizCodepoints = i_pouniObject->m_sizCodepoints;
        $self->m_pouniObjects[i_sizOffset].m_sizBytes = i_pouniObject->m_sizBytes;
    }
}

%extend UnicodeTesseract {
    UnicodeTesseract(size_t i_sizCodepoints, size_t i_sizDim1, size_t i_sizDim2, size_t i_sizDim3, size_t i_sizDim4) {
        return UnicodeTesseract_new(i_sizCodepoints, i_sizDim1, i_sizDim2, i_sizDim3, i_sizDim4);
    } 
    ~UnicodeTesseract() { 
        UnicodeTesseract_delete(&$self); 
    } 
}

/* Parse the header file to generate wrappers */

%include "unicode.h"

%extend Unicode {
#ifdef SWIG
%perlcode %{
    use overload
        "=" => sub { my $class = ref($_[0]); $class->new($_[0]) },
        "+" => sub { $_[0]->concatenate($_[1]) },
        "<=>" => sub { $_[0]->compare_ascendingnumeric($_[1]) },
        "==" => sub { $_[0]->compare_ascendingnumeric($_[1]) == 0 },
        "!=" => sub { $_[0]->compare_ascendingnumeric($_[1]) != 0 },
        "<" => sub { $_[0]->compare_ascendingnumeric($_[1]) < 0 },
        "<=" => sub { $_[0]->compare_ascendingnumeric($_[1]) <= 0 },
        ">" => sub { $_[0]->compare_ascendingnumeric($_[1]) > 0 },
        ">=" => sub { $_[0]->compare_ascendingnumeric($_[1]) >= 0 },
        "cmp" => sub { $_[0]->compare_ascendingstring($_[1]) },
        "eq" => sub { $_[0]->compare_ascendingstring($_[1]) == 0 },
        "ne" => sub { $_[0]->compare_ascendingstring($_[1]) != 0 },
        "lt" => sub { $_[0]->compare_ascendingstring($_[1]) < 0 },
        "le" => sub { $_[0]->compare_ascendingstring($_[1]) <= 0 },
        "gt" => sub { $_[0]->compare_ascendingstring($_[1]) > 0 },
        "ge" => sub { $_[0]->compare_ascendingstring($_[1]) >= 0 },
        "fallback" => 1;
%}
#endif
};

Even more interesting though, is what SWIG generated. It generated both C “wrapper” files as well as the Perl interface file named “Unicode.pm” which is really interesting to look at to really understand how the C code is integrated into the Perl code in an object-oriented way. Here is the code generated by SWIG to integrate the C library code into Perl below.

# This file was automatically generated by SWIG (http://www.swig.org).
# Version 4.0.2
#
# Do not make changes to this file unless you know what you are doing--modify
# the SWIG interface file instead.
 
package Unicode;
use base qw(Exporter);
use base qw(DynaLoader);
package Unicodec;
bootstrap Unicode;
package Unicode;
@EXPORT = qw();
 
# ---------- BASE METHODS -------------
 
package Unicode;
 
sub TIEHASH {
    my ($classname,$obj) = @_;
    return bless $obj, $classname;
}
 
sub CLEAR { }
 
sub FIRSTKEY { }
 
sub NEXTKEY { }
 
sub FETCH {
    my ($self,$field) = @_;
    my $member_func = "swig_${field}_get";
    $self->$member_func();
}
 
sub STORE {
    my ($self,$field,$newval) = @_;
    my $member_func = "swig_${field}_set";
    $self->$member_func($newval);
}
 
sub this {
    my $ptr = shift;
    return tied(%$ptr);
}
 
 
# ------- FUNCTION WRAPPERS --------
 
package Unicode;
 
*clear = *Unicodec::clear;
*empty = *Unicodec::empty;
*codepoints = *Unicodec::codepoints;
*bytes = *Unicodec::bytes;
*import_string = *Unicodec::import_string;
*export_string = *Unicodec::export_string;
*from_string = *Unicodec::from_string;
*from_int = *Unicodec::from_int;
*from_long = *Unicodec::from_long;
*from_longlong = *Unicodec::from_longlong;
*from_float = *Unicodec::from_float;
*from_double = *Unicodec::from_double;
*from_longdouble = *Unicodec::from_longdouble;
*to_int = *Unicodec::to_int;
*to_long = *Unicodec::to_long;
*to_longlong = *Unicodec::to_longlong;
*to_float = *Unicodec::to_float;
*to_double = *Unicodec::to_double;
*to_longdouble = *Unicodec::to_longdouble;
*copy = *Unicodec::copy;
*append = *Unicodec::append;
*append_multiple = *Unicodec::append_multiple;
*swap = *Unicodec::swap;
*find = *Unicodec::find;
*extract = *Unicodec::extract;
*replace = *Unicodec::replace;
*compare_ascendingstring = *Unicodec::compare_ascendingstring;
*compare_descendingstring = *Unicodec::compare_descendingstring;
*compare_ascendingnumeric = *Unicodec::compare_ascendingnumeric;
*compare_descendingnumeric = *Unicodec::compare_descendingnumeric;
*uppercase = *Unicodec::uppercase;
*lowercase = *Unicodec::lowercase;
*swapcase = *Unicodec::swapcase;
*concatenate = *Unicodec::concatenate;
*split = *Unicodec::split;
*join = *Unicodec::join;
*from_array = *Unicodec::from_array;
*to_array = *Unicodec::to_array;
*from_subvalues = *Unicodec::from_subvalues;
*to_subvalues = *Unicodec::to_subvalues;
*count_subvalues = *Unicodec::count_subvalues;
*sort_subvalues = *Unicodec::sort_subvalues;
*locate_subvalue = *Unicodec::locate_subvalue;
*extract_subvalue = *Unicodec::extract_subvalue;
*replace_subvalue = *Unicodec::replace_subvalue;
*insert_subvalue = *Unicodec::insert_subvalue;
*append_subvalue = *Unicodec::append_subvalue;
*delete_subvalue = *Unicodec::delete_subvalue;
*get_codepoint = *Unicodec::get_codepoint;
*set_codepoint = *Unicodec::set_codepoint;
*find_codepoint = *Unicodec::find_codepoint;
*isalnum_codepoint = *Unicodec::isalnum_codepoint;
*isalpha_codepoint = *Unicodec::isalpha_codepoint;
*islower_codepoint = *Unicodec::islower_codepoint;
*isupper_codepoint = *Unicodec::isupper_codepoint;
*isdigit_codepoint = *Unicodec::isdigit_codepoint;
*isxdigit_codepoint = *Unicodec::isxdigit_codepoint;
*iscntrl_codepoint = *Unicodec::iscntrl_codepoint;
*isgraph_codepoint = *Unicodec::isgraph_codepoint;
*isspace_codepoint = *Unicodec::isspace_codepoint;
*isblank_codepoint = *Unicodec::isblank_codepoint;
*isprint_codepoint = *Unicodec::isprint_codepoint;
*ispunct_codepoint = *Unicodec::ispunct_codepoint;
*tolower_codepoint = *Unicodec::tolower_codepoint;
*toupper_codepoint = *Unicodec::toupper_codepoint;
*save = *Unicodec::save;
*load = *Unicodec::load;
*to_tesseract = *Unicodec::to_tesseract;
*from_tesseract = *Unicodec::from_tesseract;
*set_element = *Unicodec::set_element;
*get_element = *Unicodec::get_element;
 
############# Class : Unicode::Unicode ##############
 
package Unicode::Unicode;
use vars qw(@ISA %OWNER %ITERATORS %BLESSEDMEMBERS);
@ISA = qw( Unicode );
%OWNER = ();
%ITERATORS = ();
*swig_m_poszCodepoints_get = *Unicodec::Unicode_m_poszCodepoints_get;
*swig_m_poszCodepoints_set = *Unicodec::Unicode_m_poszCodepoints_set;
*swig_m_sizCodepoints_get = *Unicodec::Unicode_m_sizCodepoints_get;
*swig_m_sizCodepoints_set = *Unicodec::Unicode_m_sizCodepoints_set;
*swig_m_sizBytes_get = *Unicodec::Unicode_m_sizBytes_get;
*swig_m_sizBytes_set = *Unicodec::Unicode_m_sizBytes_set;
sub new {
    my $pkg = shift;
    my $self = Unicodec::new_Unicode(@_);
    bless $self, $pkg if defined($self);
}
 
sub DESTROY {
    return unless $_[0]->isa('HASH');
    my $self = tied(%{$_[0]});
    return unless defined $self;
    delete $ITERATORS{$self};
    if (exists $OWNER{$self}) {
        Unicodec::delete_Unicode($self);
        delete $OWNER{$self};
    }
}
 
sub DISOWN {
    my $self = shift;
    my $ptr = tied(%$self);
    delete $OWNER{$ptr};
}
 
sub ACQUIRE {
    my $self = shift;
    my $ptr = tied(%$self);
    $OWNER{$ptr} = 1;
}
 
 
############# Class : Unicode::UnicodeArray ##############
 
package Unicode::UnicodeArray;
use vars qw(@ISA %OWNER %ITERATORS %BLESSEDMEMBERS);
@ISA = qw( Unicode );
%OWNER = ();
%ITERATORS = ();
*swig_m_pouniObjects_get = *Unicodec::UnicodeArray_m_pouniObjects_get;
*swig_m_pouniObjects_set = *Unicodec::UnicodeArray_m_pouniObjects_set;
*swig_m_sizObjects_get = *Unicodec::UnicodeArray_m_sizObjects_get;
*swig_m_sizObjects_set = *Unicodec::UnicodeArray_m_sizObjects_set;
sub new {
    my $pkg = shift;
    my $self = Unicodec::new_UnicodeArray(@_);
    bless $self, $pkg if defined($self);
}
 
sub DESTROY {
    return unless $_[0]->isa('HASH');
    my $self = tied(%{$_[0]});
    return unless defined $self;
    delete $ITERATORS{$self};
    if (exists $OWNER{$self}) {
        Unicodec::delete_UnicodeArray($self);
        delete $OWNER{$self};
    }
}
 
*get_element = *Unicodec::UnicodeArray_get_element;
*set_element = *Unicodec::UnicodeArray_set_element;
sub DISOWN {
    my $self = shift;
    my $ptr = tied(%$self);
    delete $OWNER{$ptr};
}
 
sub ACQUIRE {
    my $self = shift;
    my $ptr = tied(%$self);
    $OWNER{$ptr} = 1;
}
 
 
############# Class : Unicode::UnicodeTesseract ##############
 
package Unicode::UnicodeTesseract;
use vars qw(@ISA %OWNER %ITERATORS %BLESSEDMEMBERS);
@ISA = qw( Unicode );
%OWNER = ();
%ITERATORS = ();
*swig_m_sizDim1_get = *Unicodec::UnicodeTesseract_m_sizDim1_get;
*swig_m_sizDim1_set = *Unicodec::UnicodeTesseract_m_sizDim1_set;
*swig_m_sizDim2_get = *Unicodec::UnicodeTesseract_m_sizDim2_get;
*swig_m_sizDim2_set = *Unicodec::UnicodeTesseract_m_sizDim2_set;
*swig_m_sizDim3_get = *Unicodec::UnicodeTesseract_m_sizDim3_get;
*swig_m_sizDim3_set = *Unicodec::UnicodeTesseract_m_sizDim3_set;
*swig_m_sizDim4_get = *Unicodec::UnicodeTesseract_m_sizDim4_get;
*swig_m_sizDim4_set = *Unicodec::UnicodeTesseract_m_sizDim4_set;
*swig_m_sizElements_get = *Unicodec::UnicodeTesseract_m_sizElements_get;
*swig_m_sizElements_set = *Unicodec::UnicodeTesseract_m_sizElements_set;
*swig_m_sizCodepoints_get = *Unicodec::UnicodeTesseract_m_sizCodepoints_get;
*swig_m_sizCodepoints_set = *Unicodec::UnicodeTesseract_m_sizCodepoints_set;
*swig_m_sizBytes_get = *Unicodec::UnicodeTesseract_m_sizBytes_get;
*swig_m_sizBytes_set = *Unicodec::UnicodeTesseract_m_sizBytes_set;
*swig_m_inFile_get = *Unicodec::UnicodeTesseract_m_inFile_get;
*swig_m_inFile_set = *Unicodec::UnicodeTesseract_m_inFile_set;
*swig_m_pounaObject_get = *Unicodec::UnicodeTesseract_m_pounaObject_get;
*swig_m_pounaObject_set = *Unicodec::UnicodeTesseract_m_pounaObject_set;
*swig_m_poszCodepoints_get = *Unicodec::UnicodeTesseract_m_poszCodepoints_get;
*swig_m_poszCodepoints_set = *Unicodec::UnicodeTesseract_m_poszCodepoints_set;
sub new {
    my $pkg = shift;
    my $self = Unicodec::new_UnicodeTesseract(@_);
    bless $self, $pkg if defined($self);
}
 
sub DESTROY {
    return unless $_[0]->isa('HASH');
    my $self = tied(%{$_[0]});
    return unless defined $self;
    delete $ITERATORS{$self};
    if (exists $OWNER{$self}) {
        Unicodec::delete_UnicodeTesseract($self);
        delete $OWNER{$self};
    }
}
 
sub DISOWN {
    my $self = shift;
    my $ptr = tied(%$self);
    delete $OWNER{$ptr};
}
 
sub ACQUIRE {
    my $self = shift;
    my $ptr = tied(%$self);
    $OWNER{$ptr} = 1;
}
 
 
# ------- VARIABLE STUBS --------
 
package Unicode;
 
*UNICODE_BUFFER_MAX = *Unicodec::UNICODE_BUFFER_MAX;
 
    use overload
        "=" => sub { my $class = ref($_[0]); $class->new($_[0]) },
        "+" => sub { $_[0]->concatenate($_[1]) },
        "<=>" => sub { $_[0]->compare_ascendingnumeric($_[1]) },
        "==" => sub { $_[0]->compare_ascendingnumeric($_[1]) == 0 },
        "!=" => sub { $_[0]->compare_ascendingnumeric($_[1]) != 0 },
        "<" => sub { $_[0]->compare_ascendingnumeric($_[1]) < 0 },
        "<=" => sub { $_[0]->compare_ascendingnumeric($_[1]) <= 0 },
        ">" => sub { $_[0]->compare_ascendingnumeric($_[1]) > 0 },
        ">=" => sub { $_[0]->compare_ascendingnumeric($_[1]) >= 0 },
        "cmp" => sub { $_[0]->compare_ascendingstring($_[1]) },
        "eq" => sub { $_[0]->compare_ascendingstring($_[1]) == 0 },
        "ne" => sub { $_[0]->compare_ascendingstring($_[1]) != 0 },
        "lt" => sub { $_[0]->compare_ascendingstring($_[1]) < 0 },
        "le" => sub { $_[0]->compare_ascendingstring($_[1]) <= 0 },
        "gt" => sub { $_[0]->compare_ascendingstring($_[1]) > 0 },
        "ge" => sub { $_[0]->compare_ascendingstring($_[1]) >= 0 },
        "fallback" => 1;
1;

If none of any of this code makes sense to you, don’t panic! I will be covering all of this in more detail in future posts. But for right now, especially for people who understand at least the Perl code, it should be giving you a small idea of the how SWIG works. At the very least it defines the new Unicode package, and uses a tied hash as the package (class) object. OOP object methods are accessed like $object->method(). Any subclasses are called as class methods like my $object = new Unicode::Unicode() which creates a new instance variable of type (ref) “Unicode::Unicode”.

About the Unicode C Library

I wrote the Unicode C library when I was doing work in C to support a CMS I was creating in C++. It was developed to replace use of the ICU libraries and Boost libraries which were simply too large and cumbersome to include into the CMS. Originally it was only used for simple Unicode conversions and string operations. But with time it grew to encompass the use of Pick OS like subvalues to store up to 4 dimensional tables inside what is known as the Unicode::Tesseract class. It also gained the ability to exchange an entire Perl dynamic array with a Unicode::Array object, or with any of the 4 diminsional levels of a Unicode::Tesseract object. Eventually, the Unicode::Tesseract object was released from memory constraints by using mmap() so that it could hold more data than could fit in RAM. Later, it was optimized for high performance.

In a later post, I will be releasing all the source code so that you can use it on your own computer, including the test suites written for it, which documents well how to use it in Perl. I have also used SWIG to integrate it into Python 2/3 and PHP 5/7. But in this series of posts we will be focusing only on Perl. If you have any questions, please leave a comment and I promise to read all of them and respond as I can.

Design a site like this with WordPress.com
Get started