at.spardat.enterprise.fmt.ABcdFmtDefault Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2003, 2007 s IT Solutions AT Spardat GmbH .
* 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:
* s IT Solutions AT Spardat GmbH - initial API and implementation
*******************************************************************************/
// @(#) $Id: ABcdFmtDefault.java 2582 2008-05-07 14:10:55Z webok $
package at.spardat.enterprise.fmt;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Locale;
import at.spardat.enterprise.util.BigDecimalHelper;
import at.spardat.enterprise.util.NumberUtil;
/**
* This implementation allows to parse and format internationalized numbers. It's a performance
* optimized implementation to support formatting and parsing of values coming from ABcds.
*
* The internal encoding is the canonical encoding defined in class ABcd, i.e., decimal
* separator is the point, for example 100000.23.
*/
public class ABcdFmtDefault extends ABcdFmt {
// keys are Strings denoting Locales, values are two character strings holding the separators.
// the first character is the thousand separator, followed by the decimal separator.
private static final HashMap seps_ = new HashMap();
// initialize the separators_ HashMap
static {
seps_.put ("cs", "\u00A0,");
seps_.put ("cs_CZ", "\u00A0,");
seps_.put ("de", ".,");
seps_.put ("de_AT", ".,");
seps_.put ("en", ",.");
seps_.put ("en_GB", ",.");
seps_.put ("en_US", ",.");
seps_.put ("hr", ".,");
seps_.put ("hr_HR", ".,");
seps_.put ("hu", "\u00A0,");
seps_.put ("hu_HU", "\u00A0,");
seps_.put ("sk", "\u00A0,");
seps_.put ("sk_SK", "\u00A0,");
}
// defaults if Locale cannot be found
private static final String defaultSeps_ = ",.";
// max digits before comma or -1
private int maxBeforeC_;
// max digits after comma or -1
private int maxAfterC_;
// thousands separation character
private char tSep_;
// decimal separation character
private char dSep_;
/**
* Constructs an ABcdFmt.
*
* @see #set
*/
public ABcdFmtDefault (int maxBeforeC, int maxAfterC, int style, Locale l) {
set (maxBeforeC, maxAfterC, style, l);
}
/**
* Allows to set all parameters that determine the formatting process.
*
* @param maxBeforeC max number of digits before the comma or -1 if unrestricted. Must not be zero.
* @param maxAfterC max number of digits after the comma or -1 if unrestricted
* @param style may be DEFAULT, MANDATORY, NO_THOUS_SEPS, THOUS_SEPS, NO_NEG or ROUND_FRACTION.
* Either NO_THOUS_SEPS or THOUS_SEPS may be specified.
* @param l the Locale. Must not be null.
* @exception IllegalArgumentException if maxBeforeC is zero
*/
public void set (int maxBeforeC, int maxAfterC, int style, Locale l) {
if (maxBeforeC == 0) throw new IllegalArgumentException();
if (l == null) throw new IllegalArgumentException();
maxBeforeC_ = maxBeforeC;
maxAfterC_ = maxAfterC;
style_ = style;
// compute separation characters from locale
String sepChars = getSepsFor(l);
tSep_ = sepChars.charAt(0);
dSep_ = sepChars.charAt(1);
}
/**
* Sets maximum number of digits after decimal separator.
*
* @param maxAfterC max number of digits after the comma or -1 if unrestricted
*/
public void setMaxAfterC (int maxAfterC_) {
this.maxAfterC_ = maxAfterC_;
}
/**
* Sets maximum number of digits before the decimal separator.
*
* @param maxBeforeC max number of digits before the comma or -1 if unrestricted. Must not be zero.
*/
public void setMaxBeforeC (int maxBeforeC_) {
if (maxBeforeC_ == 0) throw new IllegalArgumentException();
this.maxBeforeC_ = maxBeforeC_;
}
/**
* Returns maximum number of digits after decimal separator or -1, if number of
* digits after decimal separator is unrestricted.
*/
public int getMaxAfterC () {
return maxAfterC_;
}
/**
* Return maximum number of digits before decimal separator or -1, if number of
* digits before decimal separator is unrestricted.
*/
public int getMaxBeforeC () {
return maxBeforeC_;
}
/**
* Sets the style.
*
* @param style may be DEFAULT, MANDATORY, NO_THOUS_SEPS, THOUS_SEPS or NO_NEG. Either NO_THOUS_SEPS or THOUS_SEPS may be specified.
*/
public void setStyle (int style_) {
this.style_ = style_;
}
/**
* @see at.spardat.enterprise.fmt.IFmt#format(String)
*/
public String format (String internal) {
char tSep = tSep_;
if ((style_ & THOUS_SEPS) == 0) tSep = 0;
String result = format (internal, tSep, dSep_, maxAfterC_);
if (internal.length() > 0 && (style_ & SUPPRESS_ZERO) != 0) {
/**
* if the value is numeric zero, this converts to an empty string
*/
try {
double d = Double.parseDouble(internal);
if (d == 0.0) result = "";
} catch (NumberFormatException x) {
result = "";
}
}
return result;
}
/**
* @see at.spardat.enterprise.fmt.IFmt#isLegalExternalChar(char)
*/
public boolean isLegalExternalChar (char aChar) {
// digits are allowed
if (NumberUtil.isDigit(aChar)) return true;
// decimal separators are allowed if maxAfterC is not zero
if (aChar == dSep_ && maxAfterC_ != 0) return true;
// thousand separators are allowed with MONEYs
if (aChar == tSep_ && (style_ & THOUS_SEPS) != 0) return true;
// minus sign if allowed if style NO_NEG is not set
if (aChar == '-' && (style_ & NO_NEG) == 0) return true;
// the remaining chars are not allowed
return false;
}
/**
* @see at.spardat.enterprise.fmt.IFmt#isLegalInternal(String)
*/
public boolean isLegalInternal (String v) {
if (v == null || v.length() == 0) return true;
NumberUtil.Metric m = NumberUtil.getMetric(v);
if (m == null) return false;
// check the limits
if (maxBeforeC_ >= 0 && m.lenVorKomma_-m.lenZerosVK_ > maxBeforeC_) return false;
if (maxAfterC_ >= 0 && m.lenNachKomma_ > maxAfterC_) return false;
if ((style_ & NO_NEG) != 0 && m.lenSign_ == 1) return false;
return true;
}
/**
* @see at.spardat.enterprise.fmt.IFmt#isOneWay()
*/
public boolean isOneWay () {
return false;
}
// an upper limit for maxLenOfExternal
private static final int MAX_LEN = 100;
/**
* @see at.spardat.enterprise.fmt.IFmt#maxLenOfExternal()
*/
public int maxLenOfExternal () {
if (maxBeforeC_ <= -1 || maxAfterC_ <= -1) return MAX_LEN;
// first guess
int maxLen = maxBeforeC_ + maxAfterC_;
// decimal separator
if (maxAfterC_ > 0) maxLen++;
// sign
if ((style_ & NO_NEG) == 0) maxLen++;
// for MONEYs add space for thousands separation chars
if ((style_ & THOUS_SEPS) != 0) maxLen += (maxBeforeC_-1)/3;
return maxLen;
}
/**
* @see at.spardat.enterprise.fmt.IFmt#parse(String)
*/
public String parse (String external) throws AParseException {
/**
* empty string is converted to zero if style SUPPRESS_ZERO is set
*/
if ((style_ & SUPPRESS_ZERO) != 0) {
if (external == null || external.length() == 0) return "0";
}
checkMandatory (external); // MANDATORY check on the input string
if (external == null || external.length() == 0) return "";
StringBuffer internal = new StringBuffer (32);
try {
parse (external, internal, maxBeforeC_, maxAfterC_, tSep_, dSep_, NK_ERROR);
} catch (AParseException ex) {
/**
* If style ROUND_FRACTION is set, we give it a second chance and
* round the number of digits after the comma
*/
if ((style_ & ROUND_FRACTION) != 0 && maxAfterC_ != -1) {
internal.setLength(0);
parse (external, internal, maxBeforeC_, -1, tSep_, dSep_, NK_ERROR);
/**
* Parsing with relaxed max fraction length succeeded. Here, we extract
* the number, round it and do an ordinary parse again.
*/
BigDecimal val = new BigDecimal (internal.toString());
val = val.setScale(maxAfterC_, BigDecimal.ROUND_HALF_UP);
internal.setLength(0);
parse (format (BigDecimalHelper.toPlainString(val)), internal, maxBeforeC_, maxAfterC_, tSep_, dSep_, NK_ERROR);
} else {
throw ex;
}
}
// finally, if style NO_NEG is present, check that the number is not negative.
if ((style_ & NO_NEG) != 0) {
if (internal.length() > 0 && internal.charAt(0) == '-') {
throw new FmtParseException ("ABcdNoNegative");
}
}
String toReturn = internal.toString();
/**
* Mandatory check on the internal string again, because the external may consists of blanks only
*/
checkMandatory (toReturn);
return toReturn;
}
// returns a sample Bcd format string for error messages
private static String sampleString (char tSep, char decSep) {
if (tSep != 0) return "100" + tSep + "000" + decSep + "15";
else return "100000" + decSep + "15";
}
// if there are excess places after the decimal point, truncate
public static final int NK_TRUNC = 2;
// if there are more places after the decimal point, throw an exception
public static final int NK_ERROR = 3;
/**
* This method parses an external representation of a ABcd and converts it to the canonic format.
*
* @param in the input string. May contain leading and trailing zeros.
* @param result must be an empty StringBuffer which is going to hold the result.
* The computed string in the canonic format, which may be safely assigned to an ABcd, if the precision
* attributes maxVK and maxNK are drawn from it. An empty string is returned
* if in is empty or just consists of blanks.
* @param maxVK maximum number of digits in front of the decimal point. Specify -1, if no
* restriction should be imposed.
* @param maxNK maximum number of digits after the decimal point or -1, if no restriction
* should be applied.
* @param tSep A thousands separation character, which is generally ignored in the
* part before the comma. May be 0, then in must not contain thousand
* separation characters.
* @param decSep The decimal separation character expected in the input.
* @param excessNKMode either NK_TRUNC or NK_ERROR. Defines how to react if more
* than maxNK places after the decimal point are present (after removing
* trailing zeros).
* @exception FmtParseException if in cannot be converted to the internal format.
*/
public static void parse (String in, StringBuffer result, int maxVK, int maxNK, char tSep, char decSep, int excessNKMode) {
int inLen = in.length();
if (inLen == 0) return;
int inIndex = 0;
int lenSign = 0;
int numVK = 0, numVKLeadingZ = 0;
boolean lookForLeadingZ = true;
int numNK = 0;
char c;
// skip leading space
while (inIndex < inLen) {
c = in.charAt(inIndex);
if (c == ' ') inIndex++;
else break;
}
// an optional sign
if (inIndex < inLen) {
c = in.charAt(inIndex);
if (c == '-') { inIndex++; lenSign=1; result.append('-'); }
else if (c == '+') { inIndex++; }
}
// sequence of digits or tSeps before the decimal-point
while (inIndex < inLen) {
c = in.charAt(inIndex);
if (c == tSep) { inIndex++; }
else if (NumberUtil.isDigit(c)) {
numVK++;
if (lookForLeadingZ) {
if (c == '0') numVKLeadingZ++;
else lookForLeadingZ = false;
}
inIndex++;
result.append(c);
} else break;
}
// optional decimal point
if (inIndex < inLen && in.charAt(inIndex) == decSep) {
inIndex++; result.append('.');
// sequence of digits after decimal-point
while (inIndex < inLen) {
c = in.charAt(inIndex);
if (NumberUtil.isDigit(c)) {
numNK++; result.append(c); inIndex++;
} else break;
}
}
// trailing space
while (inIndex < inLen) {
if (in.charAt(inIndex) == ' ') inIndex++;
else break;
}
// are there characters remaining in the input-string?
if (inIndex < inLen) {
throw new FmtParseException ("ABcdNotFullyParsed", in.substring(inIndex), sampleString(tSep, decSep));
}
// are there too many significant digits before the comma?
if (maxVK != -1) {
if (numVK - numVKLeadingZ > maxVK) {
throw new FmtParseException ("ABcdToManyVKs2", String.valueOf(maxVK));
}
}
// the case where nothing is left in result
if (result.length() == 0) return;
// the case where no digit is left in result; but there must be some other char,
// since we have come to this point. Either a decimal point or a sign.
if (numVK + numNK == 0) {
throw new FmtParseException ("ABcdNotValid", sampleString(tSep, decSep));
}
// are there too many digits after the comma?
if (maxNK != -1 && numNK > maxNK) {
// first, remove trailing zeros
while (result.length() > 0 && result.charAt(result.length()-1) == '0') {
result.setLength(result.length()-1);
numNK--;
}
// if still to long, throw an exception if NK_ERROR
if (numNK > maxNK && excessNKMode == NK_ERROR) {
throw new FmtParseException ("ABcdToManyNKs", String.valueOf(maxNK));
}
// truncate until we have no more than maxNK digits
while (numNK > maxNK) {
result.setLength(result.length()-1);
numNK--;
}
}
}
/**
* Takes a canonic string as input and returns a formatted string.
*
* @param canonic canonic format string of a ABcd
* @param tSep if not equal to zero, this character is inserted
* at thousand separation position
* @param decSep this character is used as decimal point character
* in the result
* @param numNachKomma if greater than or equal zero, the result will
* have numNachKomma places after the decimal point.
* If it is -1, the result consists of as many digits
* after the comma as necessary.
* @return the formatted string. It does not have leading zeros.
*/
public static String format (String canonic, char tSep, char decSep, int numNachKomma) {
if (canonic == null) return "";
int cLen = canonic.length();
int cIndex = 0;
if (cLen == 0) return "";
int cntSign = 0;
int cntVorkomma = 0;
int cntNachkomma = 0;
int indexDecPoint = -1;
StringBuffer result = new StringBuffer (canonic.length() * 2);
// optionales Vorzeichen
char c = canonic.charAt(cIndex);
if (c == '-') { cntSign = 1; cIndex++; result.append('-'); }
// Vorkommateil
boolean lookForLeadingZeros = true;
while (cIndex < cLen) {
c = canonic.charAt(cIndex);
if (NumberUtil.isDigit(c)) {
if (lookForLeadingZeros) {
// falls lookForLeadingZeros true, werden zeros ignoriert
if (c != '0') { lookForLeadingZeros = false; result.append(c); cntVorkomma++; }
} else {
result.append(c); cntVorkomma++;
}
} else break;
cIndex++;
}
// mindestens eine Vorkommastelle?
if (cntVorkomma == 0) { result.append('0'); cntVorkomma++; }
// Dezimalpunkt
if (cIndex < cLen && canonic.charAt(cIndex) == '.') {
result.append (decSep); cIndex++; indexDecPoint = result.length()-1;
}
// Nachkommateil
while (cIndex < cLen) {
c = canonic.charAt(cIndex); result.append(c); cntNachkomma++; cIndex++;
}
// auf numNachKomma richtigstellen
if (numNachKomma != -1) {
// sicherstellen, dass ein Komma da ist
if (indexDecPoint == -1) {
indexDecPoint = result.length(); result.append(decSep);
}
while (cntNachkomma < numNachKomma) { result.append('0'); cntNachkomma++; }
while (cntNachkomma > numNachKomma) {
result.setLength (result.length()-1); cntNachkomma--;
}
} else {
// wenn ein Nachkommateil da ist
if (indexDecPoint != -1) {
// trailing zeros entfernen
while (result.charAt(result.length()-1) == '0') {
result.setLength (result.length()-1); cntNachkomma--;
}
}
}
// wenn ein Decimaltrennzeichen an letzter Stelle ?brig bleibt, dieses weg
if (result.length()-1 == indexDecPoint) {
result.setLength(result.length()-1); indexDecPoint = -1;
}
// Vorzeichen weg, wenn keine Ziffern ungleich 0 mehr vorhanden sind
boolean nonZeroFound = false;
if (cntSign == 1) {
for (int i=result.length()-1; i>=0; i--) {
c = result.charAt(i);
if (NumberUtil.isDigit(c) && c != '0') { nonZeroFound = true; break; }
}
if (!nonZeroFound) {
result.deleteCharAt(0);
if (indexDecPoint != -1) indexDecPoint--;
}
}
// Tausendertrennzeichen einfuegen
if (tSep != 0 && cntVorkomma > 3) {
int insertionIndex;
if (indexDecPoint != -1) insertionIndex = indexDecPoint - 3;
else insertionIndex = result.length() - 3;
while (insertionIndex > 0 && NumberUtil.isDigit(result.charAt(insertionIndex-1))) {
result.insert(insertionIndex, tSep);
insertionIndex -= 3;
}
}
return result.toString();
}
/**
* Returns two digit string containing the separation characters for a particular Locale.
*
* @param l the Locale
* @return two character String whose first char is the thousands and whose second is the decimal sep char.
*/
private String getSepsFor (Locale l) {
String locStr = l.toString();
String seps = (String) seps_.get(locStr); if (seps != null) return seps;
int iUnder = locStr.lastIndexOf('_');
if (iUnder != -1) {
locStr = locStr.substring(0, iUnder);
seps = (String) seps_.get(locStr); if (seps != null) return seps;
}
iUnder = locStr.lastIndexOf('_');
if (iUnder != -1) {
locStr = locStr.substring(0, iUnder);
seps = (String) seps_.get(locStr); if (seps != null) return seps;
}
return defaultSeps_;
}
}