# NumberType.java

    /*
    ** Module   : NumberType.java
    ** Abstract : Progress 4GL compatible operator logic
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
    ** 001 GES 20050518   @21248 Created initial version with full support
    **                           for all Progress logical operators and
    **                           common functions.
    ** 002 GES 20050614   @21488 Added increment and decrement methods.
    ** 003 GES 20050829   @22363 Added processing to generate a string of all
    **                           '?' when there is a negative num that has
    **                           no sign char in the format string OR when a
    **                           left side overflow has occurred.
    ** 004 GES 20050830   @22387 Added error condition processing.
    ** 005 GES 20060118   @23976 Refactored, fixed and made additions to the
    **                           number formatting code.
    ** 006 GES 20060123   @24025 Move to directory for groupSep/decSep
    **                           defaults and context-local for storage.
    ** 007 GES 20060202   @24216 Honor the Progress behavior that creating
    **                           a number based on an empty string is valid
    **                           AND the result is 0!
    ** 008 GES 20060208   @24392 Added a default format string getter.
    ** 009 GES 20060303   @24874 Added subscript processing.
    ** 010 GES 20060508   @26027 toString() fix for suppressing a zero display
    **                           in cases where the entire left side value is
    **                           zero and a 'z' or '*' is in use in that char
    **                           position in the format.
    ** 011 GES 20060512   @26077 Better handling of string parsing with no
    **                           digits but with whitespace, sign chars...
    ** 012 GES 20060621   @27496 Fixes to number formatting including the
    **                           addition of support for the re-purposing of
    **                           a left minus/plus when a non-negative number
    **                           has 1 more digit than its format has digit
    **                           positions (instead of overflowing).
    ** 013 GES 20060807   @28478 Support to allow sub-classes to implement
    **                           the externalizable interface.
    ** 014 GES 20060825   @28898 Moved context local default format data into
    **                           subclasses because storing only 1 string for
    **                           all subclasses is a bad idea.
    ** 015 GES 20060830   @28989 Formats with '>' must be left padded by the
    **                           shifted number of chars (1 for each leading
    **                           zero in a '>' position).  Added toString
    **                           signature to expose control over right
    **                           alignment (insertion of padding on the left).
    ** 016 GES 20060927   @29967 Enabled generateSimpleFormat() to be sign
    **                           aware (to bypass the gen of a sign in the
    **                           case where there is no sign in the format).
    **                           Also fixed the case where an error message
    **                           needed to be displayed for an overflow
    **                           condition.
    ** 017 NVS 20061026   @30700 Added support for separate left side and
    **                           right side signs since both can be used at
    **                           the same time.
    ** 018 GES 20061201   @31533 Fix for a quirk (probably a bug) in format
    **                           processing where the position of the space
    **                           for a left side '-' sign char in a non-
    **                           negative number will precede any user-defined
    **                           text rather than appear in its normal
    **                           position.  The change was made in
    **                           processSign().
    ** 019 SIY 20070406   @32824 Added handling for leading chars in numbers.
    **                           Progress does allow some combinations to pass
    **                           the conversion silently.
    ** 020 ECF 20070608   @34011 Extracted parseDecimal() from parseDouble()
    **                           implementation. This allows use of the
    **                           intermediate string parsed from the Progress
    **                           style numeric string, without further parsing
    **                           to a double. This is necessary to create more
    **                           efficient and accurate BigDecimal instances.
    ** 021 ECF 20071025   @35524 Minor optimization. Where practical, prevent
    **                           duplicative calls to lookup context-local
    **                           WorkArea, as this lookup can be relatively
    **                           expensive. Also reworked ContextContainer to
    **                           parameterize the ContextLocal superclass.
    ** 022 GES 20080415   @38018 BigDecimal and Double parsing from a string
    **                           must not be locale specific (this is a J2SE
    **                           design point). Our string preparation code
    **                           was changed to force this. Also made changes
    **                           to how the context-specific group/dec seps
    **                           are initialized to allow that to be done
    **                           from a remote client.
    ** 023 GES 20080418   @38045 Added overrideDefaultSeparators().
    ** 024 NVS 20080611   @38687 Fixed formatting of negative values.
    ** 025 CA  20080909   @39739 Fixed formatting of decimal value 0, for a
    **                           format like '$->,>>>,>>>.99'.
    ** 026 GES 20090422   @41912 Converted to standard string formatting.
    ** 027 GES 20090424   @41972 Import change.
    ** 028 GES 20090625   @42957 Removed unnecessary subscript(int, BDT) method.
    ** 029 NVS 20090919   @43696 Fixed parseDecimal() method to properly detect
    **                           bad characters on input after the first digit.
    **                           See number_format_err.p. Diagnosed by GES.
    ** 030 NVS 20090919   @43698 Fix #029 was too aggressive against dots. Fixed.
    ** 031 GES 20090820   @43710 Massive rewrite of parseDecimal() to handle the
    **                           full range of behavior in a first class manner.
    ** 032 SVL 20090824   @43724 Fixed parseDecimal() for the "." case.
    ** 033 SVL 20090908   @43846 Fixed handling of large numbers.
    ** 034 GES 20111013          Resolve deadlock that occurs on the client when
    **                           context-local initialization calls to the server
    **                           to read the directory. The solution is to separate
    **                           the server communication from the context-local
    **                           get() method. Once the get() is complete, it is
    **                           safe to call the server.
    ** 035 CS  20130128          Added conversion support for MEMPTR related functions task #1635.
    ** 036 CA  20130221          Added stubs for SESSION-related attributes and methods.
    ** 037 GES 20130307          Remove leading and trailing zeros in parseDecimal() when
    **                           they have no effect on the value.
    ** 038 OM  20130328          Adding missing implementation for PUT-BITS and GET-BITS.
    ** 039 CA  20131001          Eliminated custom group/dec separators when format is used. In this
    **                           case, they must default to comma (,) for group and dot (.) for
    **                           decimal separator.
    ** 040 OM  20131021          Fixed NPE when running with no SessionManager (import).
    ** 041 HC  20140213          Added precise conversion to BigDecimal.
    ** 042 EVL 20140418          Fixes for different format variations.  Added new method to handle
    **                           preparation for numeric types rendering.
    ** 043 CA  20140526          If PUT BITS is used as an assignment style statement, the setBits
    **                           APIs need to return the current instance.
    ** 044 VIG 20140914          Changed ErrorManager.displayError call to the method that also
    **                           adds error num to _MSG error stack.
    ** 045 ECF 20150715          Replace StringBuffer with StringBuilder.
    ** 046 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 047 EVL 20180111          Fix for wrong invaid format consideration cases.  Include format
    **                           group separator in left-right balance condition.
    ** 048 HC  20180223          Improved assignment of SCREEN-VALUE value, the value may contain
    **                           fill characters according to the widget format and data type.
    ** 049 CA  20190726          DO NOT USE work.get(), as this will not initialize properly the
    **                           WorkArea - use work.obtain().
    ** 050 ME  20200528          Add assign for "legacy" object - resource id.
    ** 051 VVT 20210317          Updated for #4880: Format parsing is re-written
    **     VVT 20210322          Fixed after the review. See #4880-107.
    **     RFB 20210402          Added parseDoubleTest, which is very similar to parseDouble, but no exceptions
    **                           are thrown. It's just for validating. The parseDecimal signatures had to change
    **                           to accept the new boolean. Ref # 5210.
    **     VVT 20210414          Fixed: parseFormat(). See #5258.
    **     CA  20220201          The numeric format is lazily pushed to the client via state sync, to avoid
    **                           unnecessary calls if the client-side is not interested of these.
    **     CA  20220203          Avoid resolving the WorkArea instance in processDigits.
    **     CA  20220305          Fixed problems with formats like 9999.99 - digitsLeft must contain only the
    **                           actual digits, without group separators.
    **     AL2 20220328          Added proxy checks for BDT.
    **     CA  20220514          The active session is now set for leaf sessions, too - this is required for a
    **                           change in RemoteObject.obtainInstance, where first a check is done for an existing
    **                           local proxy, and after that a network instance is obtained (a requirement for the
    **                           server-side OS resources support, like memptr).
    ** 052 CA  202300707         'parseFormat' must include the group separator for the left-side and right-side
    **                           balance check, beside the '>' and the '<' characters.
    ** 053 ICP 20250129          Used logical and character constants to leverage cached instances.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import java.math.BigDecimal;
    import java.text.*;
    import java.text.NumberFormat;
    import java.util.*;
    import com.goldencode.util.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.security.ContextLocal;
    import com.goldencode.p2j.ui.LogicalTerminal;

    /**
     * An abstract class that implements a Progress 4GL compatible numeric data
     * type.  Meant to be the parent class for <code>integer</code> and
     * <code>decimal</code> Progress compatible data type wrappers. The major
     * role of this class is to allow a common method implementation for all
     * subclasses rather than an orthogonal implementation.
     * <p>
     * In addition to the implementation requirements for {@link BaseDataType},
     * each concrete subclass must implement:
     * <p>
     * <pre>
     * double       doubleValue()
     * int          intValue()
     * long         longValue()
     *              increment()
     *              decrement()
     * String       buildDefaultFormat()
     * String       buildExportFormat()
     * </pre>
     *
     * @author    GES
     */
    public abstract class NumberType
    extends BaseDataType
    {
       /** Decimal separator used in format definition. */
       protected static final char FORMAT_DEC_SEP = '.';

       /** Group separator used in format definition. */
       protected static final char FORMAT_GROUP_SEP = ',';

       /** Constant identifying the AMERICAN format. */
       private static final String AMERICAN_FORMAT = "AMERICAN";

       /** Constant identifying the EURPEAN format. */
       private static final String EUROPEAN_FORMAT = "EUROPEAN";

       /**
        * The locale-specific group separator as reported by the JVM. This is
        * used as the default for the current context's separator.
        */
       private static char GROUP_SEP = initGroupSeparator();

       /**
        * The locale-specific decimal separator (decimal point) as reported by
        * the JVM.  This is used as the default for the current context's
        * separator.
        */
       private static char DEC_SEP = initDecimalSeparator();

       /** Stores context-local state variables. */
       private static ContextContainer work = new ContextContainer();

       /** Format has no sign specification. */
       public static final int SIGN_NONE = 0;

       /** Format has a left minus as the sign specification. */
       public static final int SIGN_LEFT_MINUS = 1;

       /** Format has a left plus as the sign specification. */
       public static final int SIGN_LEFT_PLUS = 2;

       /** Format has non-embedded parenthesis as the sign specification. */
       public static final int SIGN_PAREN = 3;

       /** Format has embedded parenthesis as the sign specification. */
       public static final int SIGN_PAREN_EMBED = 4;

       /** Format has a right minus as the sign specification. */
       public static final int SIGN_RIGHT_MINUS = 5;

       /** Format has a right plus as the sign specification. */
       public static final int SIGN_RIGHT_PLUS = 6;

       /** Format has a "CR" as the sign specification. */
       public static final int SIGN_CR = 7;

       /** Format has a "DB" as the sign specification. */
       public static final int SIGN_DB = 8;

       /** Format has a "DR" as the sign specification. */
       public static final int SIGN_DR = 9;

       /**
        * Default constructor (only used for de-serialization).
        */
       public NumberType()
       {
       }

       /**
        * Returns the value of this instance as a <code>long</code>.
        * <p>
        * This method will fail if the instance represents the
        * <code>unknown value</code>.
        *
        * @return   The value cast into a <code>long</code>.
        *
        * @throws   NullPointerException
        *           If this instance represents the <code>unknown value</code>.
        */
       public abstract long longValue();

       /**
        * Returns the value of this instance as an <code>int</code>.
        * <p>
        * This method will fail if the instance represents the
        * <code>unknown value</code>.
        *
        * @return   The value as an <code>int</code>.
        *
        * @throws   NullPointerException
        *           If this instance represents the <code>unknown value</code>.
        */
       public abstract int intValue();

       /**
        * Returns the value of this instance as a <code>double</code>. Loss
        * of precision may occur in this conversion. Use {@linkplain #toBigDecimal()}
        * if keeping the precision is the concern.
        * <p>
        * If this instance represents the <code>unknown value</code>, the
        * returned <code>double</code> will be set to <code>NaN</code>.
        *
        * @return   The value cast into a <code>double</code>.
        */
       public abstract double doubleValue();

       /**
        * Returns the value of this instance as a <code>BigDecimal</code>. No loss
        * of precision will occur in this conversion.
        * <p>
        * If this instance represents the <code>unknown value</code>, the
        * return value will be <code>null</code>.
        *
        * @return   The <code>BigDecimal</code> instance, see above for details.
        */
       public abstract BigDecimal toBigDecimal();

       /**
        * Increments the value of this instance by 1.
        * <p>
        * If this instance represents the <code>unknown value</code>, nothing
        * is done.
        */
       public abstract void increment();

       /**
        * Decrements the value of this instance by 1.
        * <p>
        * If this instance represents the <code>unknown value</code>, nothing
        * is done.
        */
       public abstract void decrement();

       /**
        * Dynamically builds the default format string for this given number type
        * which honors the given group and decimal separators.
        *
        * @return   The default format string for this type of number.
        */
       public abstract String buildDefaultFormat();

       /**
        * Returns the default format string for this given number type (which
        * honors the given group and decimal separators), building that format
        * and caching it if this is the first call in this context.
        *
        * @return   The default format string for this type of number.
        */
       public abstract String obtainDefaultFormat();

       /**
        * Returns the export format string for this given number type which
        * honors the given group and decimal separators.
        *
        * @return   The export format string for this type of number or
        *           <code>null</code> if this type does not use an export
        *           format.
        */
       public abstract String buildExportFormat();

       /**
        * Worker method for getBits, this returns the number of bits from the
        * given position as an <code>integer</code>.
        *
        * @param  pos
        *         The 1-based index position to set, must be &gt; 0.
        * @param  len
        *         The number of bytes to take into consideration when performing
        *         the operation.
        *
        * @return The number of bits from the given position as an <code>integer
        *         </code>.
        */
       public abstract NumberType getBitsWorker(double pos , double len);

       /**
        * Worker method for setBits, this copies the bit representation of an
        * integer to a location with a given number of bits within the current
        * integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bytes to take into consideration when performing
        *           the operation.
        */
       public abstract void setBitsWorker(double data, double pos, double len);

       /**
        * Generate a valid 0-based integral subscript from the 1-based numeric
        * value in this instance. No bounds checking is done since the target
        * array length is not known.  The basic algorithm is to convert this
        * instance's value to a primitive <code>int</code>, decrement it and
        * return.
        *
        * @return   The 0-based integral subscript or -1 if the instance is
        *           <code>unknown value</code>.
        */
       public int subscript()
       {
          if (isUnknown())
          {
             return -1;
          }

          return intValue() - 1;
       }

       /**
        * Convert the specified object instance to number using resource identifier,
        * and assign it to this instance.
        *
        * @param    value
        *           The instance holding a object type.
        */
       public void assign(object<?> value)
       {
          // the value may be a proxy; make sure to unwrap and work with the real value
          if (BaseDataType.isProxy(value))
          {
             assign(value.val());
             return;
          }
          assign(object.resourceId(value));
       }

       /**
        * Manually override the default group and decimal separators which are
        * read from the JVM-wide values at this class' static initialization.
        *
        * @param    grp
        *           The new group separator character.
        * @param    dec
        *           The new decimal separator character.
        */
       public static void overrideDefaultSeparators(char grp, char dec)
       {
          GROUP_SEP = grp;
          DEC_SEP   = dec;
       }

       /**
        * Attempt to converts a Progress 4GL style string which represents an integer or
        * a decimal into a Java double primitive with the intention of testing tha ability
        * to do so. It performs the same, but no errors are thrown. In particular, all group
        * separators and whitespace must be removed from the string before
        * using the standard Java facilities for conversion.
        * <p>
        * The default decimal and group separators for the JVM locale are used.
        *
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    parsingInteger
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        *
        * @return   <code>true</code> if parsing is possible, <code>false</code>
        *           if not.
        */
       public static boolean parseDoubleTest(String str, boolean parsingInteger)
       {
          WorkArea wa = work.obtain();

          String dstr = parseDecimal(true, str, wa.decSep, wa.groupSep, parsingInteger, false);

          return (dstr != null);
       }

       /**
        * Converts a Progress 4GL style string which represents an integer or
        * a decimal into a Java double primitive.  In particular, all group
        * separators and whitespace must be removed from the string before
        * using the standard Java facilities for conversion.
        * <p>
        * The default decimal and group separators for the JVM locale are used.
        *
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    parsingInteger
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        *
        * @return   The parsed <code>double</code>.
        *
        * @throws   NumberFormatException
        *           if the given string cannot be parsed to a double value.
        */
       public static double parseDouble(String str, boolean parsingInteger)
       throws NumberFormatException
       {
          WorkArea wa = work.obtain();

          return parseDouble(str, wa.decSep, wa.groupSep, parsingInteger);
       }

       /**
        * Converts a Progress 4GL style string which represents an integer or
        * a decimal into a Java double primitive.  In particular, all group
        * separators and whitespace must be removed from the string before
        * using the standard Java facilities for conversion.
        * <p>
        * This method also handles Progress 4GL string to number conversion quirk,
        * which in some cases allows entering certain characters without
        * triggering error condition.
        *
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    decSep
        *           The decimal separator to use.
        * @param    grpSep
        *           The group separator to use.
        * @param    parsingInteger
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        *
        * @return   The parsed <code>double</code>.
        *
        * @throws   NumberFormatException
        *           if the given string cannot be parsed to a double value.
        */
       public static double parseDouble(String str,
                                        char decSep,
                                        char grpSep,
                                        boolean parsingInteger)
       throws NumberFormatException
       {
          String dstr = parseDecimal(str, decSep, grpSep, parsingInteger);

          return (dstr == null ? Double.NaN : Double.parseDouble(dstr));
       }

       /**
        * Converts a Progress 4GL style string which represents an integer or
        * a decimal into a Java style string representing the same number.  In
        * particular, all group separators and whitespace are removed from the
        * string.
        * <p>
        * The Java <code>BigDecimal</code> and <code>Double</code> string parsing
        * is done using a '.' as the decimal separator (it deliberately ignores
        * the locale-specific separator). This method handles this conversion.
        * <p>
        * The default decimal and group separators for the JVM locale are used.
        * <p>
        * This method also handles Progress 4GL string to number conversion quirk,
        * which in some cases allows entering certain characters without
        * triggering error condition.
        *
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    parsingInteger
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        *
        * @return   A Java style string representing the same numeric value as
        *           original string, but suitable for parsing into a primitive
        *           <code>double</code> or into a <code>BigDecimal</code>.
        */
       public static String parseDecimal(String str, boolean parsingInteger)
       {
          return parseDecimal(str, parsingInteger, false);
       }

       /**
        * Converts a Progress 4GL style string which represents an integer or
        * a decimal into a Java style string representing the same number.  In
        * particular, all group separators and whitespace are removed from the
        * string.
        * <p>
        * The Java <code>BigDecimal</code> and <code>Double</code> string parsing
        * is done using a '.' as the decimal separator (it deliberately ignores
        * the locale-specific separator). This method handles this conversion.
        * <p>
        * The default decimal and group separators for the JVM locale are used.
        * <p>
        * This method also handles Progress 4GL string to number conversion quirk,
        * which in some cases allows entering certain characters without
        * triggering error condition.
        *
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    parsingInteger
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        * @param    skipFillChars
        *           When <code>true</code> fill characters appearing in the input are not
        *           reported as error but instead ignored.
        *
        * @return   A Java style string representing the same numeric value as
        *           original string, but suitable for parsing into a primitive
        *           <code>double</code> or into a <code>BigDecimal</code>.
        */
       public static String parseDecimal(String str, boolean parsingInteger, boolean skipFillChars)
       {
          WorkArea wa = work.obtain();

          return parseDecimal(false, str, wa.decSep, wa.groupSep, parsingInteger, skipFillChars);
       }

       /**
        * See {@link #parseDecimal(String, boolean, boolean)}.
        *
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    decSep
        *           The decimal separator to use.
        * @param    grpSep
        *           The group separator to use.
        * @param    isInt
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        *
        * @return   A Java style string representing the same numeric value as
        *           original string, but suitable for parsing into a primitive
        *           <code>double</code> or into a <code>BigDecimal</code>.
        */
       public static String parseDecimal(String str,
                                         char decSep,
                                         char grpSep,
                                         boolean isInt)
       {
          return parseDecimal(str, decSep, grpSep, isInt, false);
       }

       /**
        * See {@link #parseDecimal(String, boolean, boolean)}.
        *
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    decSep
        *           The decimal separator to use.
        * @param    grpSep
        *           The group separator to use.
        * @param    isInt
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        * @param    skipFillChars
        *           When <code>true</code> fill characters appearing in the input are not
        *           reported as error but instead ignored.
        *
        * @return   A Java style string representing the same numeric value as
        *           original string, but suitable for parsing into a primitive
        *           <code>double</code> or into a <code>BigDecimal</code>.
        */
       public static String parseDecimal(String str,
                                         char decSep,
                                         char grpSep,
                                         boolean isInt,
                                         boolean skipFillChars)
       {
          return parseDecimal(false, str, decSep, grpSep, isInt, skipFillChars);
       }

       /**
        * Converts a Progress 4GL style string which represents an integer or
        * a decimal into a Java style string representing the same number. There
        * are 2 objectives. First, in order for J2SE to parse this string into
        * a number, several incompatibilities from the 4GL input format must be
        * translated into their J2SE form. Otherwise, the J2SE code will throw
        * an exception and we will catastrophically fail. Second, all of the 4GL
        * error handling must be faithfully replicated. To do so means that this
        * method must scan the entire string character by character until an
        * error is found or there are no more characters to scan. In addition, the
        * same character can generate different errors (or no error at all)
        * depending on its position in the string. All of these arcane rules must
        * be faithfully implemented.
        * <p>
        * On input in the 4GL, only the following characters may be present:
        * <p>
        * <ul>
        *    <li> numeric digits '0' through '9'
        *    <li> the currently configured group separator (',' by default)
        *    <li> the currently configured decimal separator ('.' by default)
        *    <li> the sign characters '-' or '+' which may be prefixed or postfixed
        *         on the numeric portion
        *    <li> whitespace may appear anywhere however it can have an affect on
        *         the parsing
        *    <li> the asterisk character '*' may appear in certain cases
        * </ul>
        * <p>
        * During scanning, if any other character (than those noted above) is
        * encountered, it will cause an invalid character error if some other error
        * has not been found previously.
        * <p>
        * The parsing can be considered as having 3 phases:
        * <p>
        * <ol>
        *    <li> pre-number
        *    <li> number parsing
        *    <li> post-number
        * </ol>
        * <p>
        * Phase 1 (pre-number) ends when the first digit or "near digit" character
        * is encountered. The near digits are the group and decimal separators. In
        * the pre-number phase, there can be zero or one sign characters, zero or
        * more asterisks and any amount of whitespace. The whitespace can be
        * anywhere in this section and the amount of contiguous whitespace is not
        * limited. If there are more one asterisk, they may be separated by
        * whitespace however, all asterisks must occur on only one side of the
        * sign (not on both sides of any sign character). Once phase 1 is complete,
        * asterisks may no longer be present in the string.
        * <p>
        * Phase 2 (number parsing) is the only phase in which digits or near digits
        * may be present. In addition, no other characters besides digits and near
        * digits may be present in this phase. So there can be no whitespace, sign
        * characters or asterisks.  The first non-digit (or non-near-digit) ends
        * number parsing. There may be zero or more group separators in any
        * configuration in the number parsing phase. Group separators are simply
        * dropped and have no meaning other than being a near-digit (and thus
        * affecting parsing). There may be zero or one decimal separator in the
        * number parsing section. There may be any number of digits (so long as it
        * is not larger than can be converted into a valid integer or decimal). Leading
        * zeros (in the portion before the decimal point) and trailing zeros (in the
        * portion after the decimal point) are dropped.  This corresponds to the 4GL
        * behavior where an unlimited number of leading and trailing zeros can be
        * present and are ignored (since they have no effect on the value of the
        * number).
        * <p>
        * Phase 3 (post-number) extends after phase 2 to the end of the string. Any
        * amount of whitespace is allowed in this section. In addition, if there was
        * no sign character in the pre-number section, then one sign character is
        * allowed here (as a post-fixed sign). Any other character is invalid.
        * <p>
        * There is a special case where the number parsing phase is empty. This can
        * happen if there are no digits (or near-digits) on input.  If the first
        * sign character follows an asterisk, this is taken as the end of the
        * number parsing phase, even if there were no digits and/or there are digits
        * following the sign. If there were such following digits, they would be
        * in the post-number phase and would trigger an error. If there are no
        * digits on output (effectively the empty string or just a "-", this is
        * a valid string and will be converted to the number "0".
        * <p>
        * Translations:
        * <p>
        * <pre>
        *  Input Character                    Output Result
        * ------------------  --------------------------------------------------
        * digit 0             If a leading 0 to the left of the decimal point or if a
        *                     trailing 0 to the right of the decimal point, then this
        *                     zero is dropped.  Otherwise it is copied to the output
        *                     string.
        * digit (1 - 9)       Copied to output string.
        * group separator     Dropped.
        * decimal separator   1st is copied to output string. 2nd causes an error.
        * whitespace          Dropped.
        * + sign character    1st is dropped. 2nd causes one of 2 possible errors.
        * - sign character    1st is prefixed in the output string. 2nd causes one of 2 possible errors.
        * asterisks           Dropped.
        * </pre>
        * <p>
        * Sign characters are normalized (prefixed and postfixed '+' is removed,
        * '-'. The 2nd sign character will always cause an error, but the error
        * that occurs will depend on the parsing phase. A post-fixed sign is fine
        * so long as it is the first sign character encountered. But any sign
        * character (first or second) appearing in the post-number phase will
        * generate an invalid character error. Before the 3rd phase, the 1st sign
        * will be honored and the second sign character will raise a "more than
        * 1 sign" error. Note that for the purposes of counting sign characters,
        * the direction of the sign has no meaning (-+, --, ++, +- are all
        * equivalent errors).
        * <p>
        * The Java <code>BigDecimal</code> and <code>Double</code> string parsing
        * is done using a '.' as the decimal separator (it deliberately ignores
        * the locale-specific separator). This method handles this conversion.
        *
        * @param    silentFailure
        *           When <code>true</code> we don't thrown an error if there is a failure in format.
        *           There are instances when this is used as a test for formatting capability.
        * @param    str
        *           The Progress style integer or decimal encoded as a string.
        *           If this is an empty string, then the value 0 will be returned
        *           (just like Progress).
        * @param    decSep
        *           The decimal separator to use.
        * @param    grpSep
        *           The group separator to use.
        * @param    isInt
        *           <code>true</code> if the result of this function will be
        *           interpreted as an integer. Affects error handling.
        * @param    skipFillChars
        *           When <code>true</code> fill characters appearing in the input are not
        *           reported as error but instead ignored.
        *
        * @return   A Java style string representing the same numeric value as
        *           original string, but suitable for parsing into a primitive
        *           <code>double</code> or into a <code>BigDecimal</code>.
        */
       public static String parseDecimal(boolean silentFailure,
                                         String str,
                                         char decSep,
                                         char grpSep,
                                         boolean isInt,
                                         boolean skipFillChars)
       {
          StringBuilder sb = new StringBuilder();

          int     len       = str.length();
          boolean hadSign   = false;
          boolean hadDigit  = false;
          boolean negative  = false;

          // conversion quirk flags
          boolean hadDot    = false;
          boolean hadAstrsk = false;
          boolean isDone    = false;

          // error handling flags
          int intDigits     = 0;
          int fractDigits   = 0;

          // cleanup the input string to make it safe for Double.parseDouble()
          for (int i = 0; i < len; i++)
          {
             char c = str.charAt(i);

             // detect all valid kinds of chars
             boolean isDigit  = Character.isDigit(c);
             boolean isSpace  = Character.isWhitespace(c);
             boolean isAstrsk = (c == '*');
             boolean isGrpSep = (c == grpSep);
             boolean isDecSep = (c == decSep);
             boolean isSign   = (c == '+' || c == '-');

             if (isDigit)
             {
                if (hadDot)
                {
                   fractDigits++;
                }
                else
                {
                   // trim leading zeros
                   if (intDigits == 0 && c == '0')
                   {
                      if (i < (len - 1))
                      {
                         char nextCh = str.charAt(i + 1);

                         // leave at least 1 leading zero in front of the decimal point
                         if (nextCh != decSep)
                         {
                            hadDigit = true;
                            continue;
                         }
                      }
                   }

                   intDigits++;
                }

                // handling of large numbers
                if (intDigits > 50 || (intDigits > 40 && (intDigits + fractDigits > 50)))
                {
                   String errorMessage = "Decimal number is too large";
                   int errorCode       = 536;

                   if (isInt && intDigits <= 50)
                   {
                      // this exception will be caught in integer.setValue(String)
                      throw new ErrorConditionException(errorCode, errorMessage);
                   }

                   if (!silentFailure)
                      ErrorManager.recordOrThrowError(errorCode, errorMessage);
                   return null;
                }
             }

             // anything other than space after number parsing is complete is
             // invalid (even digits!)
             if (isDone && !isSpace)
             {
                if (!silentFailure)
                   errorInvalidChar(c);
                return null;
             }

             // digits and near-digits start number parsing which changes the
             // rules
             if (isDigit || isGrpSep || isDecSep)
             {
                // we have encountered at least 1 digit at this point
                hadDigit = true;

                // these may appear during number parsing and are just dropped
                if (isGrpSep)
                   continue;

                // there may be one and only one decimal separator
                if (isDecSep)
                {
                   if (hadDot)
                   {
                      String err = "Decimal point appeared more than once " +
                                   "in numeric input";
                      if (!silentFailure)
                         ErrorManager.recordOrThrowError(75, err);
                      return null;
                   }

                   // output the decimal separator and remember that we saw it
                   hadDot = true;

                   // hardcode '.' as the decimal separator to satisfy J2SE
                   sb.append('.');
                   continue;
                }

                // anything else is a digit and is just output
                sb.append(c);
                continue;
             }

             // process whitespace
             if (isSpace)
             {
                // whitespace after digits/near-digits ends number parsing
                if (hadDigit)
                {
                   isDone = true;
                }

                // spaces are always dropped and never cause an error by themselves
                continue;
             }

             // process asterisks (which are kind of like spaces but they don't
             // turn off number parsing)
             if (isAstrsk)
             {
                // they can only appear before number parsing mode begins
                if (hadDigit)
                {
                   if (!silentFailure)
                      errorInvalidChar(c);
                   return null;
                }

                // remember that we had an asterisk, this changes any sign char
                // into a post-fixed sign EVEN THOUGH it does NOT enable number
                // parsing mode
                hadAstrsk = true;

                // drop and move on
                continue;
             }

             // process a sign character
             if (isSign)
             {
                // there can only be one sign character
                if (hadSign)
                {
                   String err = "Sign appeared more than once in numeric value";
                   if (!silentFailure)
                      ErrorManager.recordOrThrowError(77, err);
                   return null;
                }

                // remember we had a sign
                hadSign = true;

                // a post-fixed sign is OK (so long as there wasn't a prefixed
                // sign), but it has the extra behavior of ending number parsing
                if (hadDigit || hadAstrsk)
                {
                   isDone = true;
                }

                // remember the direction
                negative = (c == '-');
                continue;
             }

             if (skipFillChars)
             {
                continue;
             }

             // anything else is invalid
             if (!silentFailure)
                errorInvalidChar(c);
             return null;
          }

          // only decimals with longer than 10 digits to the right of the decimal
          // point will have trailing zeros removed; for now we remove all trailing
          // zeros except for 1
          if (hadDot && !isInt && fractDigits > 10)
          {
             int idx = sb.length() - 1;

             // trim trailing zeros (optimally it would be done above, where the extra
             // zeros would not affect the error handling, but hopefully this is OK); always
             // leave at least 1 zero to the right of the decimal point
             while (idx > 0 && sb.charAt(idx) == '0' && sb.charAt(idx - 1) != decSep)
             {
                sb.deleteCharAt(idx);
                idx--;
             }
          }

          // update the known length of the string as formatting chars or
          // whitespace may have been present in the input string BUT without
          // any digits
          len = sb.length();

          // honor that an empty string is the same as 0 in Progress!
          if (len == 0 || (hadDot && len == 1))
          {
             sb.append("0");
          }

          // honor the sign
          if (negative)
          {
             sb.insert(0, "-");
          }

          return sb.toString();
       }

       /**
        * Convert a numeric string argument into a formatted string based on
        * a Progress compatible formatting pattern.
        * <p>
        * This method provides processing that can handle both integers and
        * floating-point numbers.
        *
        * @param    left
        *           The simple, unformatted string representation of the digits
        *           of this number that reside on the left side of the decimal
        *           point.  This string should NOT have a sign indicator nor any
        *           grouping/decimal separators or leading zeros.  It should only
        *           have numeric digits.
        * @param    right
        *           The simple, unformatted string representation of the digits
        *           of this number that reside on the right side of the decimal
        *           point.  This string should NOT have a sign indicator nor any
        *           grouping/decimal separators or trailing zeros.  It should
        *           only have numeric digits.
        * @param    negative
        *           Flag that indicates the sign of the number.
        * @param    fmt
        *           The Progress 4GL compatible format string.
        *
        * @return   The formatted string.
        */
       public static String toString(String  left,
                                     String  right,
                                     boolean negative,
                                     String  fmt)
       {
          return toString(left, right, negative, fmt, true);
       }

       /**
        * Convert a numeric string argument into a formatted string based on
        * a Progress compatible formatting pattern.
        * <p>
        * This method provides processing that can handle both integers and
        * floating-point numbers.
        *
        * @param    left
        *           The simple, unformatted string representation of the digits
        *           of this number that reside on the left side of the decimal
        *           point.  This string should NOT have a sign indicator nor any
        *           grouping/decimal separators or leading zeros.  It should only
        *           have numeric digits.
        * @param    right
        *           The simple, unformatted string representation of the digits
        *           of this number that reside on the right side of the decimal
        *           point.  This string should NOT have a sign indicator nor any
        *           grouping/decimal separators or trailing zeros.  It should
        *           only have numeric digits.
        * @param    negative
        *           Flag that indicates the sign of the number.
        * @param    fmt
        *           The Progress 4GL compatible format string.
        * @param    rightAlign
        *           Pad on the left to the full size of the format string if the
        *           format has '&gt;' characters that match the positions of
        *           leading zeroes.
        *
        * @return   The formatted string.
        */
       public static String toString(String  left,
                                     String  right,
                                     boolean negative,
                                     String  fmt,
                                     boolean rightAlign)
       {
          StringBuilder sb = new StringBuilder();

          int len = fmt.length();

          // a simple check to ensure we have a non-empty format string
          if (len == 0)
          {
             return genNoDigitsError(fmt);
          }

          char c;
          int  i = 0;

          // init sign processing
          int leftSignIdx  = -1;
          int eat          = 0;

          // first emit the contents of any leading chars that are not valid
          // formatting characters, since this is a user-specified prefix
          for (; i < len; i++)
          {
             c = fmt.charAt(i);

             // these chars cannot be part of the user prefix, as soon as one
             // is encountered, this copying phase halts
             if (isFormatChar(c) && c != '<' ||
                 (c == '(' && (i + 1 < len) && isDigitChar(fmt.charAt(i + 1))))
             {
                // stop copying the prefix to output (halt loop)
                break;
             }
             else if (c == '(')
             {
                // this char can be embedded inside the misc text and it still
                // acts as the sign char!
                eat = processSign(true,
                                  leftSignIdx,
                                  negative,
                                  fmt,
                                  i,
                                  sb,
                                  rightAlign);

                if (eat > 0)
                {
                   leftSignIdx = i;
                }
             }
             else
             {
                // copy this char to our output
                sb.append(c);
             }
          }

          // quick validity test
          if (i == len)
          {
             return genNoDigitsError(fmt);
          }

          WorkArea wa = work.obtain();
          int missing = 0;

          try
          {
             int extra = -1;

             if (leftSignIdx == -1)
             {
                // process left side sign chars if needed
                eat = processSign(true,
                                  leftSignIdx,
                                  negative,
                                  fmt,
                                  i,
                                  sb,
                                  rightAlign);

                if (eat > 0)
                {
                   // detect the special case where there is a left side minus or
                   // plus sign that can be re-purposed for a digit in a
                   // non-negative number
                   if (eat == 1 && !negative)
                   {
                      char sign = fmt.charAt(i);

                      if (sign == '+' || sign == '-')
                      {
                         extra = sb.length() - 1;
                      }
                   }

                   leftSignIdx = i;
                   i += eat;
                }
             }

             // process digits (left, decimal point, right)
             int digs = processDigits(wa, left, right, fmt, i, extra, sb);

             // an error has occurred
             if (digs < 0)
             {
                // build the string that should be displayed
                return StringHelper.repeatChar('?', len);
             }

             int decIdx  = fmt.indexOf(FORMAT_DEC_SEP, i);

             // no error
             i += digs;

             // calculate if we had any removed digit positions due to use of '<'
             // (this can only happen in a format with a decimal point)
             if (rightAlign && decIdx != -1)
             {
                int rtDigs  = right.length();
                int rtFmtSz = i - (decIdx + 1);

                if (rtDigs > rtFmtSz)
                {
                   missing = rtDigs - rtFmtSz;
                }
             }

             // process right side sign chars if needed
             i += processSign(false,
                              leftSignIdx,
                              negative,
                              fmt,
                              i,
                              sb,
                              rightAlign);
          }

          catch (IllegalArgumentException iae)
          {
             StringBuilder esb = new StringBuilder();
             if (negative)
             {
                esb.append("-");
             }
             esb.append(left);

             if (right != "")
             {
                if (wa == null)
                {
                   wa = work.obtain();
                }
                esb.append(wa.decSep).append(right);
             }

             NumberType.genCannotBeDisplayed(esb.toString(), fmt);

             // build the string that should be displayed
             return StringHelper.repeatChar('?', len);
          }

          // emit the contents of any trailing chars
          if (i < len)
          {
             sb.append(fmt.substring(i));
          }

          int sbl    = sb.length();
          int needed = len - missing;

          // formats using '>' with leading zeros in these positions shift their
          // positions over but do not reduce the overall length of the result
          // string so we must left pad in this case
          // also this should include cases for formats with '<' and digits
          // after decimal point
          if (rightAlign && needed > sbl && (missing > 0 || fmt.indexOf("<") == -1))
          {
             sb.insert(0, StringHelper.repeatChar(' ', needed - sbl));
          }

          return sb.toString();
       }

       /**
        * Returns the current context's group separator which may have been
        * obtained from the directory or may have defaulted to the locale-
        * specific version.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/numberGroupSep
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/numberGroupSep
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/numberGroupSep
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/numberGroupSep
        * <p>
        * If no value is found via this lookup, then the JVM default is tried.
        * If the JVM default cannot be obtained, then this defaults to a ','.
        *
        * @return   The character used to group formatted numbers.
        */
       public static char getGroupSeparator()
       {
          return work.obtain().groupSep;
       }

       /**
        * Returns the current context's decimal separator (decimal point) which
        * may have been obtained from the directory or may have defaulted to the
        * locale-specific version.
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/numberDecimalSep
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/numberDecimalSep
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/numberDecimalSep
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/numberDecimalSep
        * <p>
        * If no value is found via this lookup, then the JVM default is tried.
        * If the JVM default cannot be obtained, then this defaults to a '.'.
        *
        * @return   The character used as the decimal separator.
        */
       public static char getDecimalSeparator()
       {
          return work.obtain().decSep;
       }

       /**
        * Returns the context-specific default format string (which honors
        * the user's defined group and decimal separators).
        *
        * @param    num
        *           The instance which can initialize the format if needed.
        *
        * @return   The default format string for this type of number.
        */
       public static String getDefaultFormat(NumberType num)
       {
          return num.obtainDefaultFormat();
       }

       /**
        * Returns the context-specific export format string (which honors
        * the user's defined group and decimal separators).
        *
        * @param    num
        *           The instance which can initialize the format if needed.
        *
        * @return   The export format string or <code>null</code> if one is
        *           not used for this type of number.
        */
       public static String getExportFormat(NumberType num)
       {
          WorkArea wa = work.obtain();

          if (wa.exportFmt == null)
          {
             wa.exportFmt = num.buildExportFormat();
          }

          return wa.exportFmt;
       }

       /**
        * Tests the passed character to determine if it is a recognized format
        * character or not.
        *
        * @param    c
        *           The character to test.
        *
        * @return   <code>true</code> if the character is a format character.
        */
       private static boolean isFormatChar(char c)
       {
          return (c == '+'              ||
                  c == '-'              ||
                  c == FORMAT_GROUP_SEP ||
                  c == FORMAT_DEC_SEP   ||
                  isDigitChar(c));
       }

       /**
        * Tests the passed character to determine if it is a recognized digit
        * character or not.
        *
        * @param    c
        *           The character to test.
        *
        * @return   <code>true</code> if the character is a digit character.
        */
       private static boolean isDigitChar(char c)
       {
          if (c == '>' || c == '<' || c == 'z' || c == 'Z' || c == '*' ||
              (c >= '0' && c <= '9'))
          {
             return true;
          }

          return false;
       }

       /**
        * Converts a given format string into a simple form that maintains the
        * digit specifications (number, position, zero/space fill...) and the
        * decimal separator (position and type) BUT converts any sign characters
        * into a left minus and drops all group separators/user text.
        * <p>
        * The advantage of this method is to make numbers that can be easily
        * parsed WHICH ALSO have the same exact number of digits (on both sides
        * of the decimal separator) which would be the result of the input
        * format string.
        *
        * @param    fmt
        *           Any valid Progress-compatible numeric format string.
        * @param    sign
        *           <code>true</code> if a signed value can be represented (if
        *           the format has a sign specification), in which case the
        *           result will have a left minus as a sign character.  On
        *           <code>false</code>, the resulting simple format will NOT have
        *           a sign character.
        *
        * @return   The converted string.
        */
       public static String generateSimpleFormat(String  fmt, boolean sign)
       {
          StringBuilder sb = new StringBuilder();
          int leftBal = 0;
          int rightBal = 0;

          if (sign)
          {
             sb.append('-');
          }

          for (int i = 0; i < fmt.length(); i++)
          {
             char ch = fmt.charAt(i);

             if (ch == '>')
             {
                leftBal++;
             }
             else if (ch == '<')
             {
                rightBal++;
             }

             if ((isDigitChar(ch) || ch == FORMAT_DEC_SEP) &&
                 (ch != '<' || rightBal <= leftBal))
             {
                sb.append(ch);
             }
          }

          return sb.toString();
       }

       /**
        * Parse the numeric format string and store the resulting format
        * specification for subsequent use.  If a format spec already exists
        * for a given format string, that spec will be returned with no further
        * parsing.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @throws  ErrorConditionException
        *          if the format string is not a valid number format
        */
       public static FormatSpec parseFormat(final String fmt)
       throws ErrorConditionException
       {
          // Try to use the cached value first.
          WorkArea wa = work.obtain();
          FormatSpec fs = wa.parsed.get(fmt);

          if (fs != null)
          {
             // we already had done this one
             return fs;
          }

          // No cached value found, need to parse.
          fs = new FormatSpec(fmt);

          // The index of the sign spec format portion if any, or -1 if no
          // sign spec.  Only the first sigh spec found matters.
          int leftSignIdx  = -1;

          // The length of format string
          final int len = fmt.length();

          // The current character read
          char c = 0;

          // The current index into format string
          int i = 0;

          // Collect the optional user-specified prefix: leading
          // characters that are not valid formatting characters. The user
          // prefix can also include an embedded sign specification.
          final StringBuilder sb = new StringBuilder();
          for (; i < len; i++)
          {
             c = fmt.charAt(i);

             if (isFormatChar(c) && c != '<')
             {
                break;
             }

             if (c == '(')
             {
                if ((i + 1 < len) && isDigitChar(fmt.charAt(i + 1)))
                {
                   break;
                }

                // this char can be embedded inside the misc text and it still
                // acts as the sign char!
                if (parseSignLeft(fmt, i, fs))
                {
                   leftSignIdx = i;

                   // save some state
                   if (fs.signLeft == SIGN_PAREN)
                   {
                      fs.signLeft = SIGN_PAREN_EMBED;
                      fs.embedIndex = i;
                   }
                }
                continue;
             }

             sb.append(c);
          }

          // Preliminary end check
          if (i == len)
          {
             genNoDigitsError(fmt);
             return null;
          }

          // save the user prefix in the fs
          fs.userLeft = sb.toString();

          // Parse optional left sign spec, if it was not found earlier.
          if (leftSignIdx == -1)
          {
             if (parseSignLeft(fmt, i, fs))
             {
                leftSignIdx = i;
                i++;

                if (i == len)
                {
                   genNoDigitsError(fmt);
                   return null;
                }

             }
          }

          // Scan the digits spec section

          // The start of currently parsed part
          int sectionStart = i;

          // Left separator counter
          int leftSeps = 0;

          // Counter of '>' characters in the digits section
          int leftBalance = 0;

          // Counter of digit format characters
          int digits = 0;
          String leftDigitFmt = "";

          // Scan optional left balance and delimiters portion: [>,]*, update counters
          endLeftSection:
          for(; i < len; i++)
          {
             switch (fmt.charAt(i))
             {
             case '>':
                leftBalance++;
                digits++;
                leftDigitFmt += fmt.charAt(i);
                break;

             case FORMAT_GROUP_SEP:
                leftBalance++;
                leftSeps++;
                break;

             default:
                break endLeftSection;
             }
          }

          // Scan optional left [9zZ*,]* portion
          endDigitsSection:
          for (; i < len; i++)
          {
             switch (fmt.charAt(i))
             {
                case '9':
                case 'z':
                case 'Z':
                case '*':
                   digits++;
                   leftDigitFmt += fmt.charAt(i);
                   break;

                case FORMAT_GROUP_SEP:
                   leftSeps++;
                   break;

                default:
                   break endDigitsSection;
             }
          }

          // save off the left side
          if (i > sectionStart)
          {
             // do not include the separators here
             fs.digitsLeft = leftDigitFmt;
             fs.seps       = leftSeps;
          }

          // Scan optional right side part
          if (i < len)
          {
             switch (fmt.charAt(i))
             {
                case '.':
                   fs.decimalPoint = i;
                   i++;

                   sectionStart = i;

                   // Scan optional digits and FORMAT_GROUP_SEP [9zZ*,]*
                   endDigitsSection:
                   for (; i < len; i++)
                   {
                      switch (fmt.charAt(i))
                      {
                         case '9':
                         case 'z':
                         case 'Z':
                         case '*':
                            digits++;
                            break;

                         case FORMAT_GROUP_SEP:
                            break;

                         default:
                            break endDigitsSection;
                      }
                   }

                   // Scan optional right balance [&lt;FORMAT_GROUP_SEP]*, check the right balance is no more than
                   // the left one.
                   for (int rightBalance = 0; i < len; i++)
                   {
                      char ch = fmt.charAt(i);
                      if (!(ch == '<' || ch == FORMAT_GROUP_SEP))
                      {
                         break;
                      }

                      rightBalance++;

                      if (rightBalance > leftBalance)
                      {
                         ErrorManager.genInvalidCharError(i, fmt);
                         return null;
                      }
                   }

                   // save off the right side if any
                   if (i > sectionStart)
                   {
                      fs.digitsRight = fmt.substring(sectionStart, i);
                   }

                   break;

                case '<':
                   // If there is no right part, there must be no right balance either.
                   ErrorManager.genInvalidCharError(i, fmt);
                   return null;

                default:
             }
          }

          // process right side sign chars if needed
          if (i < len)
          {
             i += parseSignRight(leftSignIdx, fmt, i, fs);
          }

          // Scan the rest of format as right user section.
          sectionStart = i;
          for (; i < len; i++)
          {
             switch (fmt.charAt(i))
             {
             case '+':
             case '-':
             case '>':
             case FORMAT_GROUP_SEP:
             case '*':
             case 'z':
             case 'Z':
             case '0':
             case '1':
             case '2':
             case '3':
             case '4':
             case '5':
             case '6':
             case '7':
             case '8':
             case '9':
                ErrorManager.genInvalidCharError(i, fmt);
                return null;

             default:
             }
          }

          // There must be at least one digit in the format.
          if (digits == 0)
          {
             genNoDigitsError(fmt);
             return null;
          }

          if (i > sectionStart)
          {
             fs.userRight = fmt.substring(sectionStart);
          }

          // save the format spec in our cache
          wa.parsed.put(fmt, fs);

          return fs;
       }

       /**
        * Parse the numeric format string and return the index of any embedded
        * parenthesis (the parenthesis can be used as a sign and the left
        * parenthesis can be embedded inside the user string on the left).
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @return   The index of the embedded parenthesis or -1 if no embedded
        *           parenthesis was found.
        */
       public static int getEmbedIndex(String fmt)
       {
          FormatSpec fs = obtainParsed(fmt, null);

          return fs.signLeft == SIGN_PAREN_EMBED ? fs.embedIndex : -1;
       }

       /**
        * Parse the numeric format string and return the user-specified string on
        * the left if it exists.
        * <p>
        * If the format string has not yet been parsed, it will be parsed using
        * the default JVM decimal separator and group separator.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @return   The user-specified text on the left side or empty string
        *           if no text exists there.
        */
       public static String getUserLeft(String fmt)
       {
          WorkArea wa = work.obtain();

          return obtainParsed(fmt, wa).userLeft;
       }

       /**
        * Parse the numeric format string and return the user-specified string on
        * the right if it exists.
        * <p>
        * If the format string has not yet been parsed, it will be parsed using
        * the default JVM decimal separator and group separator.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @return   The user-specified text on the right side or empty string
        *           if no text exists there.
        */
       public static String getUserRight(String fmt)
       {
          WorkArea wa = work.obtain();

          return obtainParsed(fmt, wa).userRight;
       }

       /**
        * Parse the numeric format string and return the digit string on
        * the left if it exists.
        * <p>
        * If the format string has not yet been parsed, it will be parsed using
        * the default JVM decimal separator and group separator.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @return   The digit text on the left side or empty string
        *           if no text exists there.
        */
       public static String getDigitsLeft(String fmt)
       {
          WorkArea wa = work.obtain();

          return obtainParsed(fmt, wa).digitsLeft;
       }

       /**
        * Parse the numeric format string and return the digit string on
        * the right if it exists.
        * <p>
        * If the format string has not yet been parsed, it will be parsed using
        * the default JVM decimal separator and group separator.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @return   The digit text on the right side or empty string
        *           if no text exists there.
        */
       public static String getDigitsRight(String fmt)
       {
          WorkArea wa = work.obtain();

          return obtainParsed(fmt, wa).digitsRight;
       }

       /**
        * Parse the numeric format string and return the digit string on
        * the right if it exists.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @return   The digit text on the right side or <code>null</code>
        *           if no text exists there.
        */
       public static int getDecimalSeparatorIndex(String fmt)
       {
          return obtainParsed(fmt, null).decimalPoint;
       }

       /**
        * Parse the numeric format string and return the number of group
        * separators on the left side.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        *
        * @return   The number of group separators on the left side.
        */
       public static int getNumGroupSeparators(String fmt)
       {
          return obtainParsed(fmt, null).seps;
       }

       /**
        * Get the NUMERIC-SEPARATOR attribute, which is the character that represents a number's
        * thousands separator, in its formatted representation.
        *
        * @return   See above.
        */
       public static character getNumericSeparator()
       {
          return new character(getGroupSeparator());
       }

       /**
        * Get the NUMERIC-DECIMAL-POINT attribute, which is the character that represents a number's
        * decimal point, in its formatted representation.
        *
        * @return   See above.
        */
       public static character getNumericDecimalPoint()
       {
          return new character(getDecimalSeparator());
       }

       /**
        * Get the NUMERIC-FORMAT attribute, which provides the way of interpreting commas and periods
        * within numeric values.
        * <p>
        * The possible values are "American", "European" or a character string consisting of the
        * thousands separator followed by the decimal point. This attribute provides the same
        * functionality as the European Numeric Format (-E) parameter.
        *
        * @return   See above.
        */
       public static character getNumericFormat()
       {
          // we have two cases: "AMERICAN" or "EUROPEAN"; these are determined based on the current
          // format
          WorkArea wa = work.obtain();

          if (wa.groupSep == ',' && wa.decSep == '.')
          {
             return character.of(AMERICAN_FORMAT);
          }

          if (wa.groupSep == '.' && wa.decSep == ',')
          {
             return character.of(EUROPEAN_FORMAT);
          }

          StringBuilder sb = new StringBuilder();
          sb.append(wa.groupSep);
          sb.append(wa.decSep);

          return character.of(sb.toString());
       }

       /**
        * Set the NUMERIC-FORMAT attribute. See {@link #getNumericFormat()} for its representation.
        *
        * @param    format
        *           The new value.
        */
       public static void setNumericFormat(String format)
       {
          setNumericFormat(character.of(format));
       }

       /**
        * Set the NUMERIC-FORMAT attribute. See {@link #getNumericFormat()} for its representation.
        *
        * @param    format
        *           The new value.
        */
       public static void setNumericFormat(Text format)
       {
          String fmt = format.toStringMessage();

          if (fmt.equalsIgnoreCase(AMERICAN_FORMAT))
          {
             setNumericFormat(",", ".");
          }
          else if (fmt.equalsIgnoreCase(EUROPEAN_FORMAT))
          {
             setNumericFormat(".", ",");
          }

          // if not one of the two constants, nothing is done
       }

       /**
        * Sets the NUMERIC-SEPARATOR and NUMERIC-DECIMAL-POINT attributes simultaneously.
        *
        * @param    separator
        *           The new NUMERIC-SEPARATOR value.
        * @param    decPoint
        *           The new NUMERIC-DECIMAL-POINT value.
        *
        * @return   <code>true</code> if the operation was successful.
        */
       public static logical setNumericFormat(String separator, String decPoint)
       {
          return setNumericFormat(new character(separator), new character(decPoint));
       }

       /**
        * Sets the NUMERIC-SEPARATOR and NUMERIC-DECIMAL-POINT attributes simultaneously.
        *
        * @param    separator
        *           The new NUMERIC-SEPARATOR value.
        * @param    decPoint
        *           The new NUMERIC-DECIMAL-POINT value.
        *
        * @return   <code>true</code> if the operation was successful.
        */
       public static logical setNumericFormat(character separator, String decPoint)
       {
          return setNumericFormat(separator, new character(decPoint));
       }

       /**
        * Sets the NUMERIC-SEPARATOR and NUMERIC-DECIMAL-POINT attributes simultaneously.
        *
        * @param    separator
        *           The new NUMERIC-SEPARATOR value.
        * @param    decPoint
        *           The new NUMERIC-DECIMAL-POINT value.
        *
        * @return   <code>true</code> if the operation was successful.
        */
       public static logical setNumericFormat(String separator, character decPoint)
       {
          return setNumericFormat(new character(separator), decPoint);
       }

       /**
        * Sets the NUMERIC-SEPARATOR and NUMERIC-DECIMAL-POINT attributes simultaneously.
        *
        * @param    separator
        *           The new NUMERIC-SEPARATOR value.
        * @param    decPoint
        *           The new NUMERIC-DECIMAL-POINT value.
        *
        * @return   <code>true</code> if the operation was successful.
        */
       public static logical setNumericFormat(character separator, character decPoint)
       {
          final String errMsg8903 =
             "An invalid character '%s' (%d) was specified as the numeric separator or decimal point";

          String sep = separator.toStringMessage();
          String dec = decPoint.toStringMessage();

          if (separator.isUnknown() || decPoint.isUnknown() || sep.length() == 0 || dec.length() == 0)
          {
             ErrorManager.recordOrShowError(8903, String.format(errMsg8903, "", 0),
                                            false, false, false);

             return logical.FALSE;
          }

          final String invalidSep = "BCDRZz0123456789+-<>()*?";
          final String invalidDec = "BCDRZz0123456789+-<>()*? ";

          if (sep.length() > 1 || invalidSep.indexOf(sep) >= 0)
          {
             String msg = String.format(errMsg8903, sep, (byte) sep.charAt(0));
             ErrorManager.recordOrShowError(8903, msg, false, false, false);

             return logical.FALSE;
          }

          if (dec.length() > 1 || invalidDec.indexOf(dec) >= 0)
          {
             String msg = String.format(errMsg8903, dec, (byte) dec.charAt(0));
             ErrorManager.recordOrShowError(8903, msg, false, false, false);

             return logical.FALSE;
          }

          if (sep.equals(dec))
          {
             final String msg = "The numeric separator and decimal point may not be the same character";
             ErrorManager.recordOrShowError(8904, msg, false, false, false);
             return logical.FALSE;
          }

          char decSep = dec.charAt(0);
          char groupSep = sep.charAt(0);

          setNumericFormat(decSep, groupSep);

          return logical.TRUE;
       }

       /**
        * Replace the existing separators with the given ones.
        *
        * @param    decSep
        *           The new decimal separator.
        * @param    groupSep
        *           The new group separator.
        */
       public static void setNumericFormat(char decSep, char groupSep)
       {
          WorkArea wa = work.obtain();

          if (decSep != wa.decSep || groupSep != wa.groupSep)
          {
             try
             {
                final SessionManager sessionManager = SessionManager.get();
                if (sessionManager == null)
                {
                   // running with no session manager - when importing data from P4GL
                   return;
                }

                if (!sessionManager.isLeaf())
                {
                   LogicalTerminal.setNumericFormat(decSep, groupSep);
                }
             }
             finally
             {
                // this needs to be done last, as the LogicalTerminal.setNumericFormat requires knowledge about
                // the previous separators

                wa.decSep = decSep;
                wa.groupSep = groupSep;

                // invalidate the cached formats (which include the user-specific separators) and other
                // data
                decimal.removeExplicitFormats();
             }
          }
       }

       /**
        * Getting the length of the text for numeric value according to the given format string.
        *
        * @param    text
        *           The text value to measure.
        * @param    fmt
        *           The format string to help.
        *
        * @return The number of the valuable characters in the text value string.
        */
        public static int getValueTextLength(String text, String fmt)
        {
           int len = fmt.length();

           // the text can not be null in this call
           int textLen = text.length();

           // the code below works only for "floating decimal" formats
           if (fmt != null && (fmt.indexOf("9<") != -1 || fmt.indexOf(".<") != -1) &&
               len > textLen)
           {
              // this is fractional part of the format string
              String fractFmt = fmt.substring(fmt.indexOf(NumberType.FORMAT_DEC_SEP));
              // this is fractional part of the text value without trailing spaces
              String fractTxt = text.substring(text.indexOf(NumberType.FORMAT_DEC_SEP)).trim();
              // the mandatory length of chars in format fractional part(digits before '<' sign)
              int mandatoryLength = fractFmt.indexOf('<') - 1;
              // the real length of chars in value string
              int fractLength = fractTxt.length() - 1;

              // let's calculate the final length of the value
              // first the whole part of the digit including decimal point
              len = fmt.indexOf(NumberType.FORMAT_DEC_SEP) + 1;
              // second the length of the fractional part
              // when mandatory part is smaller than actual we need to use it instead of text length
              len += mandatoryLength < fractLength ? mandatoryLength : fractLength;
              // but when the last part of the value is not a digit and not decimal separator then
              // we need to reserve one additional char for it looking from last char to text start
              for (int i = textLen - 1; i >= 0; i--)
              {
                 // look to the previous character
                 char c = text.charAt(i);
                 // not a digit and not a decimal separator
                 // this includes handling of the space char and '-' on the right end of the digit
                 if (!isDigitChar(c) && c != FORMAT_DEC_SEP)
                 {
                    len++;
                 }
                 else
                 {
                    // stop the loop imediately when the char is digit or separator
                    break;
                 }
              }
              // final check the length for validity
              // the final length of the text to display should be more or equal to text length
              if (textLen > len)
              {
                 len = textLen;
              }
           }

           return len;
        }

       /**
        * Return the default display format string for this type.
        *
        * @return   The default format string.
        */
       public String defaultFormatString()
       {
          return NumberType.getDefaultFormat(this);
       }

       /**
        * Returns the number of bits from the given position as an <code>integer</code>.
        *
        * @param  pos
        *         The 1-based index position to set, must be &gt; 0.
        * @param  len
        *         The number of bits to take into consideration when performing the operation.
        *
        * @return The number of bits from the given position as an <code>integer</code>.
        */
       public NumberType getBits(double pos, double len)
       {
          return isUnknown() ? (NumberType) instantiateUnknown() : getBitsWorker(pos, len);
       }

       /**
        * Returns the number of bits from the given position as an <code>integer</code>.
        *
        * @param  pos
        *         The 1-based index position to set, must be &gt; 0.
        * @param  len
        *         The number of bits to take into consideration when performing the operation.
        *
        * @return The number of bits from the given position as an <code>integer</code>.
        */
       public NumberType getBits(NumberType pos, double len)
       {
          return getBits(pos, new decimal(len));
       }

       /**
        * Returns the number of bits from the given position as an <code>integer</code>.
        *
        * @param  pos
        *         The 1-based index position to set, must be &gt; 0.
        * @param  len
        *         The number of bits to take into consideration when performing the operation.
        *
        * @return The number of bits from the given position as an <code>integer</code>.
        */
       public NumberType getBits(double pos, NumberType len)
       {
          return getBits(new decimal(pos), len);
       }

       /**
        * Returns the number made of the bits from the given position as an <code>integer</code>.
        *
        * @param  pos
        *         The 1-based index position to set, must be &gt; 0.
        * @param  len
        *         The number of bits to take into consideration when performing the operation.
        *
        * @return The number of bits from the given position as an <code>integer</code>.
        */
       public NumberType getBits(NumberType pos, NumberType len)
       {
          NumberType returnVal = null;
          if (isUnknown())
          {
             returnVal = (NumberType) instantiateUnknown();
          }
          else if (pos == null || pos.isUnknown() || len == null || len.isUnknown())
          {
             ErrorManager.recordOrThrowError(4391,
                   "Unable to evaluate expression with UNKNOWN value in argument");
          }
          else
          {
             returnVal = getBitsWorker(pos.doubleValue(), len.doubleValue());
          }
          return returnVal;
       }

       /**
        * Copies the bit representation of an integer to a location with a given number of bits
        * within the current integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bits to take into consideration when performing the operation.
        *
        * @return   This instance.
        */
       public NumberType setBits(double data, double pos, double len)
       {
          setBitsWorker(data, pos, len);

          return this;
       }

       /**
        * Copies the bit representation of an integer to a location with a given number of bits
        * within the current integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bits to take into consideration when performing the operation.
        *
        * @return   This instance.
        */
       public NumberType setBits(NumberType data, double pos, double len)
       {
          if (data == null || data.isUnknown() || isUnknown())
          {
             setUnknown();
          }
          else
          {
             setBitsWorker(data.doubleValue(), pos, len);
          }

          return this;
       }

       /**
        * Copies the bit representation of an integer to a location with a given number of bits
        * within the current integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bits to take into consideration when performing the operation.
        *
        * @return   This instance.
        */
       public NumberType setBits(NumberType data, NumberType pos, double len)
       {
          if (data == null || data.isUnknown() || isUnknown())
          {
             setUnknown();
          }
          else if (pos == null || pos.isUnknown())
          {
             ErrorManager.recordOrThrowError(4391,
                   "Unable to evaluate expression with UNKNOWN value in argument");
          }
          else
          {
             setBitsWorker(data.doubleValue(), pos.doubleValue(), len);
          }

          return this;
       }

       /**
        * Copies the bit representation of an integer to a location with a given number of bits
        * within the current integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bits to take into consideration when performing the operation.
        *
        * @return   This instance.
        */
       public NumberType setBits(NumberType data, double pos, NumberType len)
       {
          if (data == null || data.isUnknown() || isUnknown())
          {
             setUnknown();
          }
          else if (len == null || len.isUnknown())
          {
             ErrorManager.recordOrThrowError(4391,
                   "Unable to evaluate expression with UNKNOWN value in argument");
          }
          else
          {
             setBitsWorker(data.doubleValue(), pos, len.doubleValue());
          }

          return this;
       }

       /**
        * Copies the bit representation of an integer to a location with a given number of bits
        * within the current integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bits to take into consideration when performing the operation.
        *
        * @return   This instance.
        */
       public NumberType setBits(NumberType data, NumberType pos, NumberType len)
       {
          if (data == null || data.isUnknown() || isUnknown())
          {
             setUnknown();
          }
          else if (pos == null || pos.isUnknown() || len == null || len.isUnknown())
          {
             ErrorManager.recordOrThrowError(4391,
                   "Unable to evaluate expression with UNKNOWN value in argument");
          }
          else
          {
             setBitsWorker(data.doubleValue(), pos.doubleValue(), len.doubleValue());
          }

          return this;
       }

       /**
        * Copies the bit representation of an integer to a location with a given number of bits
        * within the current integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bits to take into consideration when performing the operation.
        *
        * @return   This instance.
        */
       public NumberType setBits(double data, NumberType pos, double len)
       {
          if (isUnknown())
          {
             return this;
          }
          else if (pos == null || pos.isUnknown())
          {
             ErrorManager.recordOrThrowError(4391,
                   "Unable to evaluate expression with UNKNOWN value in argument");
          }
          else
          {
             setBitsWorker(data, pos.doubleValue(), len);
          }

          return this;
       }

       /**
        * Copies the bit representation of an integer to a location with a given number of bits
        * within the current integer variable.
        *
        * @param    data
        *           The bit representation of an integer to be copied.
        * @param    pos
        *           The 1-based index position to set, must be &gt; 0.
        * @param    len
        *           The number of bits to take into consideration when performing the operation.
        *
        * @return   This instance.
        */
       public NumberType setBits(double data, double pos, NumberType len)
       {
          if (isUnknown())
          {
             return this;
          }
          else if (len == null || len.isUnknown())
          {
             ErrorManager.recordOrThrowError(4391,
                   "Unable to evaluate expression with UNKNOWN value in argument");
          }
          else
          {
             setBitsWorker(data, pos, len.doubleValue());
          }

          return this;
       }

       /**
        * Generate (and either display OR log) an error message if there is an
        * invalid format, or the value cannot fit the format, BUT DO NOT raise an
        * error condition.
        *
        * @param    value
        *           The simple text form of the value that cannot be displayed.
        * @param    fmt
        *           The format string.
        */
       public static void genCannotBeDisplayed(String value, String fmt)
       {
          String err = String.format("Value %s cannot be displayed using %s", value, fmt);

          // at runtime, this should only be generated by a negative number
          // that has no corresponding sign char OR by a left side (of the
          // decimal point) value that is too large to fit into the format
          // string
          if (ErrorManager.isSilentError())
          {
             // we don't throw an error here, as we have to return a string
             // even when one of these two failures occurs, however if silent
             // mode is enabled we do need to add the error text to the list
             ErrorManager.addError(74, err, false);
          }
          else
          {
             // DO NOT RAISE AN ERROR CONDITION!  Simply display this on the
             // terminal's message line.
             ErrorManager.displayError(74, err);
          }
       }

       /**
        * Gets the already parsed format specification of the given
        * Progress-compatible format string.  If it has not already been
        * parsed, this method will parse it.
        *
        * @param    fmt
        *           The format string to parse.  Must be a valid
        *           Progress-compatible numeric format.
        * @param    wa
        *           Optional <code>WorkArea</code> object.  Many callers of this
        *           method look up the context local work area and so can provide
        *           it to this method to avoid an additional, unnecessary lookup
        *           here.  If <code>null</code>, this method will perform the
        *           lookup itself.
        *
        * @return   The format specification or <code>null</code> if it could not
        *           be obtained or parsed.
        */
       private static FormatSpec obtainParsed(String fmt, WorkArea wa)
       {
          if (wa == null)
          {
             wa = work.obtain();
          }

          FormatSpec fs = wa.parsed.get(fmt);

          if (fs == null)
          {
             fs = parseFormat(fmt);
          }

          return fs;
       }

       /**
        * Parse out any sign-related characters (which can occur on the left,
        * the right or both sides of the format string) and add the corresponding
        * characters to the output string based on the sign of the number and
        * upon other state variables.  The characters '+', '-', '(' are matched
        * on the left side and '+', '-', ')', 'DR', 'CR' and 'DB' are matched
        * on the right side.
        *
        * @param    left
        *           <code>true</code> if this is the left side sign processing.
        * @param    leftSignIdx
        *           -1 if this is the left side processing or if it is the right
        *           side processing and no left side processing occurred.
        *           Otherwise this is the index of the single character on the
        *           left side of the format string that was matched/processed
        *           during the left side sign processing.
        * @param    negative
        *           <code>true</code> if the number being formatted is negative.
        * @param    fmt
        *           The Progress compatible format string.
        * @param    index
        *           The character index to examine for sign indicators.
        * @param    sb
        *           The buffer in which our formatted output is built.
        * @param    rightAlign
        *           Pad on the left to the full size of the format string if the
        *           format has '&gt;' characters that match the positions of
        *           leading zeroes.
        *
        * @return   The number of characters starting at index which are
        *           attributed to sign processing. 0 if no sign characters were
        *           found.  1 or 2 if valid sign characters were found. If &gt; 0,
        *           then these characters must be skipped over in the calling
        *           method.
        */
       private static int processSign(boolean       left,
                                      int           leftSignIdx,
                                      boolean       negative,
                                      String        fmt,
                                      int           index,
                                      StringBuilder sb,
                                      boolean       rightAlign)
       {
          int eat = 1;
          int len = fmt.length();

          // quick test to see if we are at the end of the string already
          if (index >= len)
          {
             // were we supposed to find a sign char?
             if (negative && leftSignIdx < 0)
             {
                String errmsg = "Missing sign char in format for negative #.";
                throw new IllegalArgumentException(errmsg);
             }

             // nothing to do
             return 0;
          }

          char c = fmt.charAt(index);

          if (left)
          {
             // left side sign processing
             if (c == '-' || c == '(')
             {
                // flag to determine if we need to bypass normal left side
                // empty sign processing
                boolean bail = false;

                // the index of the digit following the sign
                int digidx = index + 1;

                // there is a quirk in the 4GL where the following conditions
                // cause the extra blank space to precede any user defined
                // text rather than follow it as is normal:
                // 1. the left side sign is a '-'
                // 2. the next format string digit is a '>' and NOT a '9', 'z',
                //    'Z', '*' or a group separator
                // 3. the number is non-negative (so the sign would normally be
                //    emitted as a space AND at the same relative position in the
                //    output as it appears in the format string)
                // 4. there is user-defined left side text in the format string
                //    (arbitrary text that will normally be output verbetim to
                //    the left of the sign)
                // 5. the formatted value must be right aligned (it will be
                //    padded on the left by the calling method so we don't have
                //    to insert the blank sign here)
                // example 1: 12345 using "abc->>>>9" yields " abc12345"
                // example 2: 12345 using "abc-99999" yields "abc 12345"
                if (digidx < len                 &&
                    c == '-'                     &&
                    fmt.charAt(digidx) == '>'    &&
                    !negative                    &&
                    index > 0                    &&
                    rightAlign)
                {
                   bail = true;
                }

                if (!bail)
                   sb.append(negative ? c : ' ');
             }
             else if (c == '+')
             {
                sb.append(negative ? '-' : '+');
             }
             else
             {
                // no left side sign char
                eat = 0;
             }
          }
          else
          {
             // right side sign processing
             if (c == '-' || c == '+')
             {
                if (leftSignIdx > 0)
                {
                   // left side sign processing did actually do something so this
                   // limits what is possible on the right side
                   throw new IllegalArgumentException("Invalid right-side sign.");
                }

                sb.append(negative ? '-' : (c == '+' ? c : ' '));
             }
             else if (c == ')')
             {
                if (leftSignIdx >= 0 && fmt.charAt(leftSignIdx) == '(')
                {
                   sb.append(negative ? ')' : ' ');
                }
                else
                {
                   return ErrorManager.genInvalidCharError(index, fmt);
                }
             }
             else
             {
                // the special sign chars (DR, CR, DB) case seems to be processed
                // independently by Progress (whether or not a left side sign
                // was processed) AND ONLY IF they are the last 2 chars in the
                // format string
                eat = 0;

                int spaceCount = 0;

                // we need to scan possible space chars before "dr", "cr" or "db"
                for (int i = index; i < len - 1; i++)
                {
                   char chCurr = fmt.charAt(i);

                   if (chCurr == ' ')
                   {
                      // remember number of spaces
                      spaceCount++;
                   }
                   else
                   {
                      String match = fmt.substring(i);

                      // check for special characters
                      if ("dr".equalsIgnoreCase(match) ||
                          "cr".equalsIgnoreCase(match) ||
                          "db".equalsIgnoreCase(match))
                      {
                         eat = 2 + spaceCount;

                         // insert space chars encountered before
                         for (int j = 0; j < spaceCount; j++)
                         {
                            sb.append(' ');
                         }
                         // insert replacement for special characters
                         sb.append(negative ? match : "  ");
                      }
                      else
                      {
                         break;
                      }
                   }
                }
             }

             if (negative && leftSignIdx < 0 && eat == 0)
             {
                String errmsg = "Missing sign char in format for negative #.";
                throw new IllegalArgumentException(errmsg);
             }
          }

          return eat;
       }

       /**
        * Parse out any sign-related characters, which can occur on the
        * left side of the format string, and add the specifications to
        * the format spec instance.  The characters '+', '-', '(' are
        * matched.
        * @param    fmt
        *           The Progress compatible format string.
        * @param    index
        *           The character index to examine for sign indicators.
        *           The caller must assure the index does not point outside
        *           the format string.
        * @param    fs
        *           The format spec in which our calculations are to be stored.
        *
        * @return   {@code true} if if valid sign character was found.
        */
       private static boolean parseSignLeft(String fmt, int index, FormatSpec fs)
       {
          switch (fmt.charAt(index))
          {
             case '-':
                fs.signLeft = SIGN_LEFT_MINUS;
                return true;
             case '(':
                fs.signLeft = SIGN_PAREN;
                return true;
             case '+':
                fs.signLeft = SIGN_LEFT_PLUS;
                return true;
             default:
                return false;
          }
       }

       /**
        * Parse out any sign-related characters (which can occur on the
        * right side of the format string) and add the specifications to
        * the format spec instance.  The characters '+', '-', ')', 'DR',
        * 'CR' and 'DB' are matched.
        *
        * @param    leftSignIdx
        *           -1 if this is the left side processing or if it is the right
        *           side processing and no left side processing occurred.
        *           Otherwise this is the index of the single character on the
        *           left side of the format string that was matched/processed
        *           during the left side sign processing.
        * @param    fmt
        *           The Progress compatible format string.
        * @param    index
        *           The character index to examine for sign indicators.
        *           The caller must assure there the index does not point outside
        *           the format string.
        * @param    fs
        *           The format spec in which our calculations are to be stored.
        *
        * @return   The number of characters starting at index which are
        *           attributed to sign processing. 0 if no sign characters were
        *           found.  1 or 2 if valid sign characters were found. If &gt; 0,
        *           then these characters must be skipped over in the calling
        *           method.
        *
        * @throws   ErrorConditionException
        */
       private static int parseSignRight(int        leftSignIdx,
                                         String     fmt,
                                         int        index,
                                         FormatSpec fs)
       throws ErrorConditionException
       {
          final char c = fmt.charAt(index);

          switch (c)
          {
             case '-':
             case '+':
                if (leftSignIdx >= 0)
                {
                   ErrorManager.genInvalidCharError(index, fmt);

                    // FIXME: is this point ever reached?
                   return 0;
                }

                fs.signRight = (c == '+') ? SIGN_RIGHT_PLUS : SIGN_RIGHT_MINUS;

                return 1;

             case ')':
                if (leftSignIdx >= 0 && fmt.charAt(leftSignIdx) == '(')
                {
                   // FIXME: this better be asserted by proper unit-tests.
                   if (fs.signLeft != SIGN_PAREN && fs.signLeft != SIGN_PAREN_EMBED)
                   {
                      throw new IllegalStateException("Incorrect PAREN sign parsing.");
                   }
                }
                else
                {
                   ErrorManager.genInvalidCharError(index, fmt);
                   return 0;
                }

                return 1;
             default:
          }

          // the special sign chars (DR, CR, DB) case seems to be processed
          // independently by Progress (whether or not a left side sign
          // was processed) AND ONLY IF they are the last 2 chars in the
          // format string
          switch (fmt.substring(index).toLowerCase())
          {
             case "dr":
                fs.signRight = SIGN_DR;
                return 2;
             case "cr":
                fs.signRight = SIGN_CR;
                return 2;
             case "db":
                fs.signRight = SIGN_DB;
                return 2;
             default:
                // not a sign after all
                return 0;
          }
       }

       /**
        * Parse out any digit characters of the format string and use these
        * specifications to convert the left and right portions of the number's
        * characters into the output string.
        *
        * @param    wa
        *           The {@link WorkArea} instance.
        * @param    left
        *           The digits from the left side of the decimal point.
        * @param    right
        *           The digits from the right side of the decimal point.
        * @param    fmt
        *           The Progress compatible format string.
        * @param    index
        *           The character index to begin the left side digit characters.
        * @param    extra
        *           The index into the output buffer of any replaceable minus or
        *           plus sign in the case where a one digit overflow of a
        *           non-negative number can be cured or -1 if such a special
        *           case re-purposing of a left side sign position is not allowed.
        * @param    sb
        *           The buffer in which our formatted output is built.
        *
        * @return   The number of characters starting at index which are
        *           attributed to digit processing. If &gt; 0, then these characters
        *           must be skipped over in the calling method.
        */
       private static int processDigits(WorkArea      wa,
                                        String        left,
                                        String        right,
                                        String        fmt,
                                        int           index,
                                        int           extra,
                                        StringBuilder sb)
       {
          int  eat    = 0;
          int  togo   = 0;
          int  len    = fmt.length();

          // quick test
          if (index >= len)
          {
             throw new IllegalArgumentException("Index " + index +
                                                "is out of bounds for format (" +
                                                fmt + ").");
          }

          // determine the size of each portion of our number
          int llen = left.length();
          int rlen = right.length();

          // detect if the left side is only a single zero digit
          boolean loneZero = (llen == 1 && left.charAt(0) == '0');

          // find the end of this portion of the format string and keep track of
          // the number of separators
          int  i    = index;
          int  seps = 0;
          int  gt   = 0;
          char c    = fmt.charAt(i);
          boolean  ltValid = true;

          // find the end of the left side of the decimal point in the format
          // string
          while (isDigitChar(c) || c == FORMAT_GROUP_SEP)
          {
             // some digit chars are invalid in a left side format string
             // including '<' after '9' or '>'
             if ((c >= '0' && c <= '8') || (c == '<' && (!ltValid || gt > 0)))
             {
                return ErrorManager.genInvalidCharError(i, fmt);
             }

             // the '<' char is not valid after '9' in format string
             if (c == '9' && ltValid)
             {
                ltValid = false;
             }

             // special processing for '>'
             if (c == '>')
             {
                gt++;

                // the '>' can only appear with seps at the front of the left
                // side format, once another digit char appears, '>' can't be
                // used again
                if ((i - index) == (seps + gt))
                {
                   return ErrorManager.genInvalidCharError(i, fmt);
                }
             }

             // track the number of separators
             if (c == FORMAT_GROUP_SEP)
             {
                seps++;
             }

             i++;

             // if there is no decimal point, right sign chars or appended user
             // text, we may hit the end of the string here
             if (i == len)
             {
                break;
             }

             c = fmt.charAt(i);
          }

          int decPt = -1;

          // if the next char is a '.', then we have 2 parts
          if (c == FORMAT_DEC_SEP)
          {
             decPt = i;
          }

          // calculate the size of our portion of the format string
          eat  = i - index;
          togo = eat - seps;

          // test for overflow
          if ((eat > 0 && togo < llen) || (eat == 0 && llen > 0 && !loneZero))
          {
             // detect if this is the special case where we must re-purpose the
             // left minus or plus sign on a non-negative number that has 1 more
             // digit than the format has digit chars
             if (extra != -1 && (togo + 1) == llen)
             {
                // recursively call ourselves while pretending that the left side
                // format had a digit instead of a minus or plus sign

                // find the replaceable sign char
                int idx = index - 1;

                // rewrite the fmt (WARNING: if this is an unbalanced floating
                // decimal format (e.g. ->9.9<<) then this may INAPPROPRIATELY
                // MASK a failure by replacing the sign with a '>' on the right
                // side; note that this is still the best approach since any other
                // format char would cause new failures if there are any other
                // '>' chars in the left side format
                StringBuilder tmp = new StringBuilder(fmt.substring(0, idx));
                tmp.append('>').append(fmt.substring(idx + 1));

                // truncate the current screen buffer to remove the sign char
                // that was put there
                sb.deleteCharAt(sb.length() - 1);

                // make the recursive call
                return processDigits(wa, left, right, tmp.toString(), idx, -1, sb);
             }

             // caught in the caller, triggers the return of a string of
             // "?" chars
             throw new IllegalArgumentException("Number format overflow.");
          }

          // remember some facts for right side processing
          int leftBalance  = 0;
          int rightBalance = 0;
          int zeroed       = 0;
          int leftSep      = 0;

          // loop control vars
          String  lfmt = fmt.substring(index, i);
          char    last;
          char    digit;
          boolean test;

          // the first pass also needs the last character if we start not from the first character
          // of the initial format string
          if (index > 0)
          {
             c = fmt.charAt(index - 1);
          }

          // left side processing
          for (int j = 0; j < eat; j++)
          {
             // remember the previous pass through the loop (to "look back"
             // when processing separator chars (commas)
             last = c;

             // pull the next format char
             c = lfmt.charAt(j);

             // is this a group separator that needs replacement?
             boolean replaceGrp  = (c == FORMAT_GROUP_SEP && togo >= llen);

             // certain format digits require special replacement
             boolean special = (c == 'z' || c == 'Z' || c == '*' || c == '>');

             // is this the special case of a left side value that is a "0" and
             // our format char requires a replacement of this rightmost char of
             // the left side?
             boolean replaceLead = (special && (togo == llen) && loneZero);

             // detect if the next output char is a real digit or a
             // replacement
             test = (llen == 0 || togo > llen || replaceGrp || replaceLead);

             // calculate the next character to display OR a '!' if the
             // implicit form should be chosen because no digit exists to
             // populate that position
             digit = test ? '!' : left.charAt(llen - togo);

             // based on the format char, generate output
             if (c == '9')
             {
                sb.append(digit == '!' ? '0' : digit);
                togo--;
             }
             else if (c == 'z' || c == 'Z')
             {
                sb.append(digit == '!' ? ' ' : digit);
                togo--;
             }
             else if (c == '*')
             {
                sb.append(digit == '!' ? '*' : digit);
                togo--;
             }
             else if (c == '>')
             {
                if (digit != '!')
                {
                   sb.append(digit);
                }
                else
                {
                   zeroed++;
                }
                // keep track of the number of '<' chars to detect if the
                // 'floating decimal' format is being used (there must be
                // an equal or balanced number of '>' chars to the right of
                // the decimal point in that case
                leftBalance++;
                togo--;
             }
             else if (c == '<')
             {
                // this is handling of formats like <<<,<9.9
                // need to leave the < character or digit
                if (digit != '!')
                {
                   sb.append(digit);
                }
                else
                {
                   sb.append('<');
                }
                togo--;
             }
             else if (c == FORMAT_GROUP_SEP)
             {
                if (digit == '!' && last != '9')
                {
                   if (last == 'z' || last == 'Z')
                   {
                      sb.append(' ');
                   }
                   else if (last == '>')
                   {
                      // skip this
                      leftSep++;
                   }
                   else if (last == '<')
                   {
                      // in the case of having this char on left side we need to leave group
                      // separator as is in result
                      sb.append(wa.groupSep);
                   }
                   else if (last == '*')
                   {
                      sb.append('*');
                   }
                }
                else
                {
                   sb.append(wa.groupSep);
                }
             }
          }

          // output the decimal point if it exists
          if (decPt > -1)
          {
             sb.append(wa.decSep);
             eat++;

             seps = 0;

             // move past the decimal point
             i++;

             // return if we have no right side
             if (i == len)
                return eat;

             c = fmt.charAt(i);

             // find the end of the right side of the decimal point in the format
             // string
             while (isDigitChar(c) || c == FORMAT_GROUP_SEP)
             {
                // track the number of separators
                if (c == FORMAT_GROUP_SEP)
                {
                   // one cannot use a sep after the start of the right bal chars
                   if (rightBalance > 0)
                   {
                      return ErrorManager.genInvalidCharError(i, fmt);
                   }

                   seps++;
                }
                else if (c == '<')
                {
                   // this is the "floating decimal" format!
                   rightBalance++;

                   // test balance
                   if (rightBalance > leftBalance + leftSep)
                   {
                      return ErrorManager.genInvalidCharError(i, fmt);
                   }
                }
                else
                {
                   // must be one of the other digit chars
                   if (rightBalance > 0)
                   {
                      // one cannot use any digit char but a '<' once this
                      // balanced mode is started
                      return ErrorManager.genInvalidCharError(i, fmt);
                   }
                }

                i++;

                // if there are no right sign chars or appended user
                // text, we may hit the end of the string here
                if (i == len)
                {
                   break;
                }

                c = fmt.charAt(i);
             }

             index  = decPt + 1;
             int t  = i - index;
             eat   += t;

             // nothing more to do
             if (t < 1)
                return eat;

             String rfmt   = fmt.substring(index, i);
             int    sepNum = 0;
             int    actual = 0;

             // reset c
             c = '\0';

             // right side processing
             for (int j = 0; j < t; j++)
             {
                // remember the previous pass through the loop (to "look back"
                // when processing commas)
                last = c;

                // pull the next format char
                c = rfmt.charAt(j);

                // detect if the next output char is a real digit or a
                // replacement

                // calculate non-separator index in the format string
                actual = j - sepNum;

                if (c == '<')
                {
                   // this is the ridiculous "floating decimal" format so the
                   // normal logic is reversed (we only output if there is a
                   // valid char AND if the "corresponding" '>' in the output
                   // from the left side was ZEROED
                   test = (rlen == 0 || actual >= rlen || zeroed < 1);
                }
                else
                {
                   // must be a '9' or ','
                   test = (rlen == 0 || actual >= rlen);
                }

                // calculate the next character to display OR a '!' if the
                // implicit form should be chosen because no digit exists to
                // populate that position
                digit = test ? '!' : right.charAt(actual);

                // based on the format char, generate output
                if (c == '9')
                {
                   // we only output the separator when there is a following
                   // digit being output
                   if (last == wa.groupSep)
                   {
                      sb.append(wa.groupSep);
                   }

                   // output a manufactured zero or a real digit
                   sb.append(digit == '!' ? '0' : digit);
                }
                else if (c == '<')
                {
                   if (digit != '!')
                   {
                      zeroed--;

                      // we only output the separator when there is a following
                      // digit being output
                      if (last == wa.groupSep)
                      {
                         sb.append(wa.groupSep);
                      }

                      // output a real digit
                      sb.append(digit);
                   }
                }
                else if (c == wa.groupSep)
                {
                   sepNum++;
                }
                else
                {
                   return ErrorManager.genInvalidCharError((index + j), fmt);
                }
             }
          }

          return eat;
       }

       /**
        * Generate an error if no digits are specified in a numeric format string.
        *
        * @param    fmt
        *           The incorrect format string.
        *
        * @return   The empty string "".
        *
        * @throws   ErrorConditionException
        */
       private static String genNoDigitsError(String fmt) throws ErrorConditionException
       {
          String spec = "Numeric format %s provides for no digits";
          ErrorManager.recordOrThrowError(148, String.format(spec, fmt));

          // this is the safest thing to return (it should be undone anyway)
          return "";
       }

       /**
        * Returns the default locale's group separator (defaults to comma).
        *
        * @return   The character used to group formatted numbers.
        */
       private static char initGroupSeparator()
       {
          NumberFormat nf = NumberFormat.getInstance();
          char grpSep = ',';

          if (nf instanceof DecimalFormat)
          {
             // reset sep to something locale specific, if we can
             DecimalFormat        df  = (DecimalFormat) nf;
             DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();
             grpSep = dfs.getGroupingSeparator();
          }

          return grpSep;
       }

       /**
        * Returns the default locale's decimal separator (defaults to period).
        *
        * @return   The character used as the decimal point.
        */
       private static char initDecimalSeparator()
       {
          NumberFormat nf = NumberFormat.getInstance();
          char decSep = '.';

          if (nf instanceof DecimalFormat)
          {
             // reset sep to something locale specific, if we can
             DecimalFormat        df  = (DecimalFormat) nf;
             DecimalFormatSymbols dfs = df.getDecimalFormatSymbols();
             decSep = dfs.getDecimalSeparator();
          }

          return decSep;
       }

       /**
        * Determines the group and decimal separators for the current context
        * based on reading override values from the directory.  The default values
        * are locale-specific (e.g. in the U.S. a '.' is the decimal separator
        * and ',' is the group separator while in much of Europe it is reversed).
        * <p>
        * The value returned may have been found via a search algorithm that
        * is account (user or process) specific or group specific
        * within the current server or a global default for all servers.
        * <p>
        * The implementation iteratively looks up the directory node under:
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/numberGroupSep
        * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/numberDecimalSep
        * <p>
        * If no user/process or group nodes are present, then this is checked:
        * /server/&lt;serverID&gt;/runtime/default/numberGroupSep
        * /server/&lt;serverID&gt;/runtime/default/numberDecimalSep
        * <p>
        * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
        * (it is the global default area for all servers):
        * /server/default/runtime/&lt;account_or_group&gt;/numberGroupSep
        * /server/default/runtime/&lt;account_or_group&gt;/numberDecimalSep
        * <p>
        * Finally, if no user/process or group nodes are present in the global
        * default area, then this is checked:
        * /server/default/runtime/default/numberGroupSep
        * /server/default/runtime/default/numberDecimalSep
        * <p>
        * Each value is controlled independently.  One or the other or both can
        * be missing from the directory.  In addition, even if both appear in the
        * directory, there is no requirement for them to be both specified in
        * the same path in the directory.
        *
        * @param    wa
        *           The context's work area to edit.
        */
       private static void initContextSpecificSeps(WorkArea wa)
       {
          Directory dir = DirectoryManager.getInstance();

          wa.groupSep = dir.getChar(Directory.ID_RELATIVE, "numberGroupSep",   GROUP_SEP);
          wa.decSep   = dir.getChar(Directory.ID_RELATIVE, "numberDecimalSep", DEC_SEP);
       }

       /**
        * Common method for reporting invalid char error.
        *
        * @param   c
        *          Character which caused the error.
        */
       private static void errorInvalidChar(char c)
       {
          String spec = "Invalid character in numeric input %c";
          String err  = String.format(spec, c);

          ErrorManager.recordOrThrowError(76, err);
       }

       /**
        * Serves as a container for parsed format specifications regarding
        * a specific Progress-compatible numeric format string.
        */
       public static class FormatSpec
       {
          /** The full format string that this specification defines. */
          public String fullFmt = null;

          /** The left side sign specification of the mapped format string. */
          public int signLeft = SIGN_NONE;

          /** The right side sign specification of the mapped format string. */
          public int signRight = SIGN_NONE;

          /** The index position of an embedded parenthesis sign char. */
          public int embedIndex = -1;

          /** The left side user-specified text (emitted verbatim).  */
          public String userLeft = "";

          /** The right side user-specified text (emitted verbatim).  */
          public String userRight = "";

          /** Index of the decimal separator in this format. */
          public int decimalPoint = -1;

          /** Left side digit specification. */
          public String digitsLeft = "";

          /** Right side digit specification. */
          public String digitsRight = "";

          /** Valid (left side) separators that can appear in output. */
          public int seps = -1;

          /**
           * Simple constructor.
           *
           * @param    fmt
           *           The format string for which this spec is created.
           */
          public FormatSpec(String fmt)
          {
             fullFmt = fmt;
          }

          /**
           * Writes the current state of the object into a formatted string.
           *
           * @return   The formatted string.
           */
          public String toString()
          {
             StringBuilder sb = new StringBuilder();

             sb.append("Input Format    = '").append(fullFmt).append("';\n");
             sb.append("Sign Left       = ").append(signLeft).append(";\n");
             sb.append("Embedded Index  = ").append(embedIndex).append(";\n");
             sb.append("User Left       = '").append(userLeft).append("';\n");
             sb.append("Digits Left     = '").append(digitsLeft).append("';\n");
             sb.append("Num Separators  = ").append(seps).append(";\n");
             sb.append("Decimal Point   = ").append(decimalPoint).append(";\n");
             sb.append("Digits Right    = '").append(digitsRight).append("';\n");
             sb.append("Sign Right      = ").append(signRight).append(";\n");
             sb.append("User Right      = '").append(userRight).append("';\n");

             return sb.toString();
          }
       }

       /**
        * Stores global data relating to the state of the current context.
        */
       private static class WorkArea
       {
          /** Track if initialization has occurred yet. */
          private boolean initialized = false;

          /** The context-specific group separator. */
          private char groupSep;

          /** The context-specific decimal separator (decimal point). */
          private char decSep;

          /**
           * Stores the context-specific export format string (which honors
           * the user's defined group and decimal separators). Initialization
           * is delegated to the implementation class.
           */
          private String exportFmt = null;

          /** Storage for already parsed format strings. */
          private Map<String, FormatSpec> parsed =
             new HashMap<String, FormatSpec>();
       }

       /**
        * Simple container that stores and returns a context-local instance of
        * the global work area.
        */
       private static class ContextContainer
       extends ContextLocal<WorkArea>
       {
          /**
           * Obtains the context-local instance of the contained global work
           * area.
           *
           * @return   The work area associated with this context.
           */
          public synchronized WorkArea obtain()
          {
             WorkArea wa = this.get();

             // normally, we would place this init logic in the initialValue()
             // method OR inline in the WorkArea constructor/init (which is also
             // done during initialValue(); however this is can cause a deadlock;
             // the get() MUST be separated from the initialization routines since
             // on the client these routines call the server and can deadlock with
             // other classes which are needed to complete the communications path;
             // once the get() returns, the access to the context-local maps is
             // complete and it is safe to trigger processing that may call the
             // server
             if (!wa.initialized)
             {
                initContextSpecificSeps(wa);
                wa.initialized = true;
             }

             return wa;
          }

          /**
           * Initializes the work area, the first time it is requested within a
           * new context.
           *
           * @return   The newly instantiated work area.
           */
          protected WorkArea initialValue()
          {
             return new WorkArea();
          }
       }
    }