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();
      }   
   }   
}