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 > 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 > 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 '>' 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/<serverID>/runtime/<account_or_group>/numberGroupSep
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/numberGroupSep
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/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/<serverID>/runtime/<account_or_group>/numberDecimalSep
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/numberDecimalSep
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/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 [<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 > 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 > 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 > 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 > 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 > 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 > 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 > 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 > 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 > 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 > 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 > 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 '>' 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 > 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 > 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 > 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/<serverID>/runtime/<account_or_group>/numberGroupSep
* /server/<serverID>/runtime/<account_or_group>/numberDecimalSep
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/numberGroupSep
* /server/<serverID>/runtime/default/numberDecimalSep
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/numberGroupSep
* /server/default/runtime/<account_or_group>/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();
}
}
}