/*******************************************************************************
* Copyright © 2006, 2013 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*
*******************************************************************************/
package org.eclipse.edt.javart.util;
import java.math.BigDecimal;
import org.eclipse.edt.javart.Constants;
import org.eclipse.edt.javart.resources.LocalizedText;
/**
* This class makes a formatted string from a BigDecimal and a formatting pattern.
*
* It began as a line by line translation, from C to Java, of code from I4GL. Parts
* have been rewritten to improve its performance. Currently it's twice as fast as before.
*
* TODO I think the performance could get even better. At the end of fmtNum we call
* the rpfs method, which copies characters off of the prstack variable. The
* characters are in reverse order. If we can write them to prstack in the correct
* order, then most of rpfs can be removed.
*/
public class NumberFormatter
{
/*
* rfmtnum format the given value to a character string according to the
* given fmtstring
*/
public static String fmtNum( BigDecimal value, String fmtstring, LocalizedText locText )
{
if ( fmtstring == null || fmtstring.trim().length() == 0 )
{
return "";
}
return fmtNum( value.toPlainString(), value.signum(), fmtstring, locText );
}
public static String fmtNum( Float value, String fmtstring, LocalizedText locText )
{
if ( fmtstring == null || fmtstring.trim().length() == 0 )
{
return "";
}
if (value > 0.0)
return fmtNum( value.toString(), 1, fmtstring, locText );
else if (value < 0.0)
return fmtNum( value.toString(), -1, fmtstring, locText );
return fmtNum( value.toString(), 0, fmtstring, locText );
}
public static String fmtNum( Double value, String fmtstring, LocalizedText locText )
{
if ( fmtstring == null || fmtstring.trim().length() == 0 )
{
return "";
}
if (value > 0.0)
return fmtNum( value.toString(), 1, fmtstring, locText );
else if (value < 0.0)
return fmtNum( value.toString(), -1, fmtstring, locText );
return fmtNum( value.toString(), 0, fmtstring, locText );
}
private static String fmtNum( String valueString, int signum, String fmtstring,
LocalizedText locText )
{
// We don't want the first digit to be a negative sign or a zero.
if ( valueString.charAt( 0 ) == '-' )
{
valueString = valueString.substring( 1 );
}
if ( valueString.charAt( 0 ) == '0' )
{
valueString = valueString.substring( 1 );
}
// The default FMTSIZE is assumed to be 80
int FMTSIZE = 80;
if(valueString.length() > FMTSIZE)
{
// The reason for this fix is in some cases a number
// could be represented with an exponent 3e89
// The to string representation in EGL returns a string
// that could be more than the default size, and the format
// specifier could include decimal point #.##.
// To accomodate for everything the fmtsize needs to be
// size of string representation + extra digits after decimal.
FMTSIZE = valueString.length();
int decpos = fmtstring.indexOf('.');
int stlen = fmtstring.length();
int ndecchars = stlen - decpos;
FMTSIZE += ndecchars;
}
int AREASIZE = FMTSIZE + 2;
/*
* set the first and last space in formatter to 0, copy the fmtstring
* starting at the second space
*/
char[] formatter = new char[ AREASIZE ];
fmtstring.getChars( 0, Math.min( FMTSIZE, fmtstring.length() ),
formatter, 1 );
/*
* Set p_frmt to point to one char beyond dec point. This decimal point
* is from the format string, thus is always represented by a period
* rather than locale dependent.
*/
char[] p_frmt = formatter;
int p_frmtIndex = 1;
int firstDecCharLocation = -1;
for (; p_frmt[p_frmtIndex] != 0; p_frmtIndex++) {
if (p_frmt[p_frmtIndex] == '.')
{
p_frmtIndex++;
firstDecCharLocation = p_frmtIndex;
break;
}
}
/*
* p_frmt should now point to one character beyond the decimal point in
* the format string Loop forward from p_frmt and set deccnt accordingly
*/
/*
* Loop forward from p_frmt and set deccnt accordingly
*/
int deccnt = 0;
for (; p_frmt[p_frmtIndex] != 0; p_frmtIndex++) {
switch (p_frmt[p_frmtIndex]) {
/*
* Since p_frmt starts right after the decimal point and there can
* only be one decimal point in the format string, we should not
* encounter any more '.'.
*/
case '$':
case '#':
case '&':
case '*':
case '(':
case '+':
case '-':
case '<':
deccnt++;
}
}
/*
* get string of text digits (without decimal point) from value
*/
char[] dgtstr = new char[ AREASIZE ];
int decpt_pos = deccvt(valueString, signum, deccnt, dgtstr);
CCharArray eedecpoint = new CCharArray(AREASIZE);
eedecpoint.ccharArrayIndex = 1 + decpt_pos;
for ( int i = 0; dgtstr[ i ] != 0; i++ )
{
eedecpoint.ccharArray[ i + 1 ] = dgtstr[ i ];
}
CBoolean useMonetary = new CBoolean( false );
int rightmost = fmtanalysis( formatter, useMonetary );
/*
* leaderprinted is FALSE iff a leading currency symbol needs to be
* printed, but hasn't yet been printed
*/
CBoolean leaderprinted = new CBoolean(true);
/*
* trailerprinted is FALSE iff a trailing currency symbol needs to be
* printed, but hasn't yet been printed
*/
CBoolean trailerprinted = new CBoolean(true);
CBoolean parprinted = new CBoolean(true), minprinted = new CBoolean(
true), plusprinted = new CBoolean(true);
CInteger sym_index = new CInteger(0);
char[] signa = new char[ 8 ];
chksigns(formatter, sym_index, parprinted, minprinted, leaderprinted,
trailerprinted, plusprinted, signa);
/* load leading signs into signa array */
CCharArray p_source = new CCharArray();
p_frmtIndex = align(firstDecCharLocation - 1, p_source, eedecpoint, p_frmt, p_frmtIndex, rightmost);
/* go right to left in format */
char[] prstack = new char[AREASIZE];
char lastchar = 0;
CInteger stack_index = new CInteger(0);
char currentChar = p_frmt[p_frmtIndex];
boolean isNegative = signum == -1;
for (; currentChar != 0; currentChar = p_frmt[--p_frmtIndex])
{
if ( currentChar == ',' && p_source.ccharArray[p_source.ccharArrayIndex] == 0 )
{
p_frmt[p_frmtIndex] = lastchar; /* just fill leading commas */
currentChar = lastchar;
}
switch ( currentChar )
{
case ',':
prstack[stack_index.value++] = locText.getSeparatorSymbol();
break;
case '.':
char decimalSeparator =
useMonetary.value ? locText.getMonetaryDecimalSeparator() : locText.getDecimalSymbol();
prstack[stack_index.value++] = decimalSeparator;
break;
case '<':
leadingLT(p_source, prstack, stack_index);
break;
case '*':
leadingNotLT(p_source, prstack, stack_index, '*');
break;
case '&':
leadingNotLT(p_source, prstack, stack_index, '0');
break;
case '#':
leadingNotLT(p_source, prstack, stack_index, ' ');
break;
case '-':
prsigns(minprinted.value, p_source, prstack, sym_index,
stack_index, signa, minprinted, plusprinted,
parprinted, leaderprinted, trailerprinted, isNegative,
locText);
break;
case '+':
prsigns(plusprinted.value, p_source, prstack, sym_index,
stack_index, signa, minprinted, plusprinted,
parprinted, leaderprinted, trailerprinted, isNegative,
locText);
break;
case '(':
prsigns(parprinted.value, p_source, prstack, sym_index,
stack_index, signa, minprinted, plusprinted,
parprinted, leaderprinted, trailerprinted, isNegative,
locText);
break;
case '$':
prsigns(leaderprinted.value, p_source, prstack, sym_index,
stack_index, signa, minprinted, plusprinted,
parprinted, leaderprinted, trailerprinted, isNegative,
locText);
break;
case '@':
prsigns(trailerprinted.value, p_source, prstack,
sym_index, stack_index, signa, minprinted, plusprinted,
parprinted, leaderprinted, trailerprinted, isNegative,
locText);
break;
case ')':
prstack[stack_index.value++] = isNegative ? ')' : ' ';
break;
default:
prstack[stack_index.value++] = currentChar;
break;
} /* end of switch */
lastchar = currentChar;
}
char[] result = new char[ AREASIZE ];
int len =
rpfs(result, prstack, stack_index.value - 1, trailerprinted, leaderprinted,
minprinted, plusprinted, parprinted, p_source);
return new String( result, 0, len );
}
/*
* Function: deccvt
*
* Parameters:
* np - pointer to a decimal structure containing the DECIMAL value
* to be converted.
* ndigit - number of digits to the right of decimal point
* decpt - pointer to an integer that is the position of the decimal point
* relative to the beginning of the string.
* decstr - returned decimal string buffer.
* decstrlen - length of decimal string buffer.
*/
private static int deccvt(String number, int signum, int ndigit, char[] decstr)
{
int decpt = 0;
CharSequence seq;
int index = number.indexOf('.');
if (index == -1) {
StringBuilder buf = new StringBuilder( number.length() + 1 + ndigit );
buf.append( number );
buf.append( '.' );
while ( ndigit >= 50 )
{
buf.append( Constants.STRING_50_ZEROS );
ndigit -= 50;
}
if ( ndigit > 0 )
{
buf.append( Constants.STRING_50_ZEROS.substring( 0, ndigit ) );
}
seq = buf;
} else {
if (ndigit == 0)
seq = number.substring( 0, index );
else {
// See if we have too few or too many decimal digits in the string.
int decDigitsInNumber = number.length() - index - 1;
if ( decDigitsInNumber == ndigit )
{
seq = number;
}
else if ( decDigitsInNumber < ndigit )
{
int zerosNeeded = ndigit - decDigitsInNumber;
StringBuilder buf = new StringBuilder( number.length() + zerosNeeded );
buf.append( number );
while ( zerosNeeded >= 50 )
{
buf.append( Constants.STRING_50_ZEROS );
zerosNeeded -= 50;
}
if ( zerosNeeded > 0 )
{
buf.append( Constants.STRING_50_ZEROS.substring( 0, zerosNeeded ) );
}
seq = buf;
}
else
{
seq = number.substring( 0, number.length() - (decDigitsInNumber - ndigit) );
}
}
}
/*
* calculate the correct position of the decimal point. handle the value
* 0 differently (since the routine returns empty buffer for this
* value).
*/
if (signum != 0)
{
int length = seq.length();
decpt = length;
for (int j = 0, i = 0; i < length; i++) {
char c = seq.charAt(i);
if (c == '.')
decpt = i;
else
decstr[j++] = c;
}
}
return decpt;
}
/*
*
*/
private static int fmtanalysis( char[] formatter, CBoolean useMonetary )
{
int i = 1;
while ( true )
{
switch ( formatter[ i ] )
{
case 0:
return i;
case '$':
case '@':
useMonetary.value = true;
break;
}
i++;
}
}
/*
* chksigns function: Set value of the following global variables according
* to the format parprinted minprinted leaderprinted trailerprinted
* plusprinted The idea here is that if the format tells us we are to print
* a symbol then we try to make sure that we have done so by setting the
* appropriate flag to FALSE. During calls to prsi, the appropriate flags
* are set to TRUE. Finally, during the call to rpfs, we check each of these
* four flags are checked and if any are FALSE, then the outgoing string is
* filled with *'s (indicating error.)
*/
private static void chksigns(char[] formatter, CInteger sym_index,
CBoolean parprinted, CBoolean minprinted, CBoolean leaderprinted,
CBoolean trailerprinted, CBoolean plusprinted, char[] signa) {
char c = formatter[ 1 ];
for ( int i = 2; c != 0; i++ )
{
switch ( c ) {
case '(':
ldsigna('(', sym_index, signa);
parprinted.value = false;
break;
case '-':
ldsigna('-', sym_index, signa);
minprinted.value = false;
break;
case '$':
ldsigna('$', sym_index, signa);
leaderprinted.value = false;
break;
case '@':
ldsigna('@', sym_index, signa);
trailerprinted.value = false;
break;
case '+':
ldsigna('+', sym_index, signa);
plusprinted.value = false;
break;
}
c = formatter[ i ];
}
}
private static int align(int decpoint_location, CCharArray p_source,
CCharArray eedecpoint, char[] p_frmt, int p_frmtIndex, int rightmost) {
/*
* If we found a decimal point in the format...
*/
if (decpoint_location >= 0) {
/* align using decimal points */
p_source.ccharArray = eedecpoint.ccharArray;
p_source.ccharArrayIndex = eedecpoint.ccharArrayIndex;
for (p_frmtIndex = decpoint_location + 1;
p_frmt[p_frmtIndex] != 0; p_frmtIndex++)
{
if (p_source.ccharArray[p_source.ccharArrayIndex] == 0) {
p_source.ccharArray[p_source.ccharArrayIndex] = '0';
}
switch (p_frmt[p_frmtIndex]) {
case '*':
case '$':
case '#':
case '&':
case '(':
case '+':
case '-':
case '<':
p_source.ccharArrayIndex++;
}
}
} else {
/*
* align using first character to the left of the decimal
*/
p_frmtIndex = rightmost;
p_source.ccharArray = eedecpoint.ccharArray;
p_source.ccharArrayIndex = eedecpoint.ccharArrayIndex;
}
p_source.ccharArrayIndex--;
return p_frmtIndex - 1;
}
/*
* leading function: Call pr to fill in a symbol. If p_source points to the
* end of a string, then we call pr on the fillchar symbol (unless the
* symbol is a ' <', which means "left justify"). If p_source does not point
* to the end of a string, we simply yank another character off of p_source
* and call pr on that.
*/
private static void leadingNotLT(CCharArray p_source,
char[] prstack, CInteger stack_index, char fchar) {
if (p_source.ccharArray[p_source.ccharArrayIndex] == 0) {
prstack[stack_index.value++] = fchar;
} else {
prstack[stack_index.value++] = p_source.ccharArray[p_source.ccharArrayIndex--];
}
}
private static void leadingLT(CCharArray p_source,
char[] prstack, CInteger stack_index) {
if (p_source.ccharArray[p_source.ccharArrayIndex] != 0)
{
prstack[stack_index.value++] = p_source.ccharArray[p_source.ccharArrayIndex--];
}
}
/*
* prsigns function: Make calls to prsi or pr, depending upon the value
* pointed to by p_source. If p_source does not point to the end of string,
* then we simply call pr(*p_source--). Otherwise, we (conditionally) call
* prsi on a character from the signa array.
*/
private static void prsigns(boolean printed, CCharArray p_source,
char[] prstack, CInteger sym_index, CInteger stack_index,
char[] signa, CBoolean minprinted, CBoolean plusprinted,
CBoolean parprinted, CBoolean leaderprinted,
CBoolean trailerprinted, boolean isNegative, LocalizedText locText) {
if (p_source.ccharArray[p_source.ccharArrayIndex] == 0) {
if (!printed) /* character has not yet been used */
{
/*
* print according to the character on top of signa
*/
sym_index.value--;
switch (signa[sym_index.value]) {
case '-':
prsi(prstack, stack_index, minprinted, '-', ' ', isNegative,
' ', locText );
break;
case '+':
prsi(prstack, stack_index, plusprinted, '-', '+', isNegative,
' ', locText);
break;
case '(':
prsi(prstack, stack_index, parprinted, '(', ' ', isNegative,
' ', locText);
break;
case '$':
prsi(prstack, stack_index, leaderprinted, '$', '$',
isNegative, ' ', locText);
break;
case '@':
prsi(prstack, stack_index, trailerprinted, '@', '@',
isNegative, ' ', locText);
break;
}
} else {
// character has already been used
prstack[stack_index.value++] = ' ';
}
} else /* ...there are symbols yet to be pushed onto the stack */{
/*
* If the symbol in question has yet to be printed
*/
if (!printed)
if (signa[sym_index.value - 1] == '@') /*
* ..and
* if a
* trailing
* cur.
* sym.
*/
{
/*
* Call prsi on it
*/
sym_index.value--;
prsi(prstack, stack_index, trailerprinted, '@', '@',
isNegative, ' ', locText);
/* use it */
} else
/* Otherwise, just push the symbol */
prstack[stack_index.value++] = p_source.ccharArray[p_source.ccharArrayIndex--];
}
}
/*
* rpfs function: Pop each character off of prstack, and place it in cp. If
* any of the flags leaderprinted, minprinted, plusprinted, or parprinted
* are FALSE, this indicates that they should have been printed, but were
* not (probably because there wasn't enough room for them in the format).
*/
private static int rpfs(char[] cp, char[] prstack, int prIndex,
CBoolean trailerprinted, CBoolean leaderprinted,
CBoolean minprinted, CBoolean plusprinted, CBoolean parprinted,
CCharArray p_source)
{
int cpIndex = 0;
if (trailerprinted.value && leaderprinted.value
&& minprinted.value && plusprinted.value
&& parprinted.value && p_source.ccharArray[p_source.ccharArrayIndex] == 0)
{
for ( ; prIndex >= 0; cpIndex++, prIndex-- )
{
if ( prstack[ prIndex ] == 0 )
{
return cpIndex;
}
cp[ cpIndex ] = prstack[ prIndex ];
}
}
else
{
for ( ; prIndex >= 0; prIndex-- )
{
cp[ cpIndex++ ] = '*';
}
}
return cpIndex;
}
/*
* prsi function: Check to see if the symbol has been printed. If not, make
* a call to pr, and set the "printed" flag to TRUE. The call made to pr is
* of the form: pr(pr_sign ? neg: pos); ...unless pr_sign is a '$', in which
* case we use monfront (see documentation for chksigns)
*/
private static void prsi(char[] prstack, CInteger stack_index,
CBoolean printed, char neg, char pos, boolean isNegative,
char fillchar, LocalizedText locText ) {
if (!printed.value) {
if (neg == '$') /*
* push money prefix onto prstack
*/
{
String currencySymbol = locText.getCurrencySymbol();
int len = currencySymbol.length();
/*
* If there is no leading currency symbol, print a space
*/
if ( len == 1 )
{
prstack[stack_index.value++] = currencySymbol.charAt( 0 );
}
else if ( len == 0 )
{
prstack[stack_index.value++] = ' ';
}
else {
while (len > 0)
prstack[stack_index.value++] = currencySymbol.charAt(--len);
}
} else if (neg == '@') {
String currencySymbol = locText.getCurrencySymbol();
int len = currencySymbol.length();
/*
* If there is no trailing currency symbol, print a space
*/
if ( len == 1 )
{
pl(prstack, stack_index, currencySymbol.charAt( 0 ));
}
else if ( len == 0 )
{
pl(prstack, stack_index, ' ');
}
else
{
for (int i = 0; i < len; i++)
pl(prstack, stack_index, currencySymbol.charAt(i));
}
} else
prstack[stack_index.value++] = isNegative ? neg : pos;
printed.value = true;
} else {
if (fillchar != '<')
prstack[stack_index.value++] = fillchar;
}
}
/*
* pl function: Push a character onto the prstack. Eventually these will get
* popped off by a call to rpfs.
*/
private static void pl(char[] prstack, CInteger stack_index, char c) {
int index = stack_index.value;
while (index > 0)
prstack[index] = prstack[--index];
prstack[index] = c;
stack_index.value++;
}
/*
* ldsigna function: Load c into the signa array (if it has not already been
* loaded)
*/
private static void ldsigna(char c, CInteger sym_index, char[] signa) {
if ( sym_index.value == 0 || c != signa[ sym_index.value - 1 ] )
{
signa[ sym_index.value++ ] = c;
}
}
static class CBoolean
{
boolean value;
public CBoolean( boolean value )
{
this.value = value;
}
}
static class CCharArray
{
char ccharArray[];
int ccharArrayIndex;
public CCharArray() {
}
public CCharArray(int ccharArraySize) {
ccharArray = new char[ccharArraySize];
ccharArrayIndex = 0;
}
}
static class CInteger
{
int value;
public CInteger( int value )
{
this.value = value;
}
}
}