package com.jbidwatcher.util;
/*
* Copyright (c) 2000-2007, CyberFOX Software, Inc. All Rights Reserved.
*
* Developed by mrs (Morgan Schweers)
*/
import com.jbidwatcher.util.config.JConfig;
import org.jetbrains.annotations.NotNull;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;
import java.beans.PersistenceDelegate;
import java.beans.DefaultPersistenceDelegate;
public class Currency implements Comparable {
public static final String VALUE_REGEX="^(\\s?\\$)?[0-9]+([,.0-9]*)$";
public static final String NAME_REGEX = "(USD|GBP|JPY|CHF|FRF|EUR|CAD|AUD|NTD|TWD|HKD|MYR|SGD|INR|US)";
private static NumberFormat df = NumberFormat.getNumberInstance(Locale.US); // We create a lot of these, so minimizing memory usage is good.
public static final int NONE=0, US_DOLLAR=1, UK_POUND=2, JP_YEN=3, GER_MARK=4, FR_FRANC=5, CAN_DOLLAR=6;
public static final int EURO=7, AU_DOLLAR=8, CH_FRANC=9, NT_DOLLAR=10, TW_DOLLAR=10, HK_DOLLAR=11;
public static final int MY_REAL=12, SG_DOLLAR=13, IND_RUPEE=14;
private static Currency _noValue = null;
/**
* @brief This provides a concept of a currency value that is
* invalid, not just 'zero' in some arbitrary currency.
*
* @return A single, consistent, 'Empty Value', which indicates an
* invalid currency.
*/
public static Currency NoValue() {
if(_noValue == null) _noValue = new Currency(NONE, 0.0);
return _noValue;
}
protected int mCurrencyType;
protected double mValue;
private static final char pound = '\u00A3';
private static final Character objPound = '\u00A3';
private static Map<Integer,Double> sCurrencyMap = new HashMap<Integer,Double>();
/**
* Convert a non-US currency to USD, usually for sorting purposes.
*
* Takes two values (usd, non-usd) which are believed to be
* equivalent, and a currency amount to convert based off the
* ratio between the first two. If the USD amount is null or $0,
* it looks in a table it keeps around for converting. If that fails,
* it just returns the non-USD's value.
*
* @param usd - A sample US dollar amount.
* @param nonusd - A non-US dollar amount that is equivalent to the usd paramter.
* @param cvt - The non-USD amount to convert to USD.
*
* @return - 'cvt' converted by the ratio of usd:nonusd, or by an internal table if
* it couldn't figure out the ratio, or just the non-usd's amount as a USD amount if
* there wasn't even an entry in the table.
*/
public static Currency convertToUSD(Currency usd, Currency nonusd, Currency cvt) {
if(cvt != null && !cvt.isNull() && cvt.getCurrencyType() != US_DOLLAR) {
double multiple;
if(usd == null || usd.isNull() || usd.getValue() == 0.0 ||
nonusd == null || nonusd.isNull() || nonusd.getValue() == 0.0) {
if(sCurrencyMap.containsKey(cvt.getCurrencyType())) {
multiple = sCurrencyMap.get(cvt.getCurrencyType());
} else {
// If we have nothing else to go on, treat it as exactly equal to USD.
multiple = 1.0;
}
} else {
multiple = usd.getValue() / nonusd.getValue();
if(multiple != 0.0) sCurrencyMap.put(nonusd.getCurrencyType(), multiple);
}
return getCurrency(US_DOLLAR, multiple*cvt.getValue());
}
return cvt;
}
/*!@class CurrencyTypeException
*
* @brief A class to yell about currency type comparison exceptions.
*
* This is used when comparing two currencies of disparate monies.
*/
public static class CurrencyTypeException extends Exception {
String _associatedString;
public CurrencyTypeException(String inString) {
_associatedString = inString;
}
public String toString() {
return _associatedString;
}
}
private static final Integer CurDollar = US_DOLLAR; // American Dollar
private static final Integer CurPound = UK_POUND; // British Pound
private static final Integer CurYen = JP_YEN; // Japanese Yen
private static final Integer CurMark = GER_MARK; // German Mark
private static final Integer CurFranc = FR_FRANC; // French Franc
private static final Integer CurSwiss = CH_FRANC; // Swiss Franc
private static final Integer CurCan = CAN_DOLLAR; // Canadian Dollar
private static final Integer CurEuro = EURO; // Euro
private static final Integer CurAu = AU_DOLLAR; // Australian Dollar
private static final Integer CurTaiwan = NT_DOLLAR; // New Taiwanese Dollar
private static final Integer CurHK = HK_DOLLAR; // Hong Kong Dollar
private static final Integer CurMyr = MY_REAL; // Malaysia Real(?)
private static final Integer CurSGD = SG_DOLLAR; // Singapore Dollar
private static final Integer CurRupee = IND_RUPEE; // Indian Rupee
// The fundamental list of the textual representation for different
// currencies, and the Currency type it translates to.
private static final Object xlateTable[][] = {
{ "USD", CurDollar },
{ "US $", CurDollar },
{ "AU $", CurAu },
{ "au$", CurAu },
{ "AU", CurAu },
{ "AUD", CurAu },
{ "US", CurDollar },
{ "USD $", CurDollar },
{ "$", CurDollar },
{ "C", CurCan },
{ "C $", CurCan },
{ "CAD", CurCan },
{ "c$", CurCan },
{ "GBP", CurPound },
{ objPound.toString(), CurPound },
{ "pound", CurPound },
{ "\u00A3", CurPound },
{ "£", CurPound },
{ "Y", CurYen },
{ "JPY", CurYen },
{ "¥", CurYen },
{ "\u00A5", CurYen },
{ "DM", CurMark },
{ "FRF", CurFranc },
{ "fr", CurFranc },
{ "CHF", CurSwiss },
{ "chf", CurSwiss },
{ "dm", CurMark },
{ "\u20AC", CurEuro },
{ "eur", CurEuro },
{ "EUR", CurEuro },
{ "Eur", CurEuro },
{ "NT$", CurTaiwan },
{ "nt$", CurTaiwan },
{ "NTD", CurTaiwan },
{ "HK$", CurHK },
{ "hk$", CurHK },
{ "HKD", CurHK },
{ "MYR", CurMyr },
{ "myr", CurMyr },
{ "SGD", CurSGD },
{ "sgd", CurSGD },
{ "INR", CurRupee },
{ "inr", CurRupee }
};
/**
* @brief Convert from a string containing a recognized symbol into
* a currency type.
*
* @param symbol - The string representation of a currency.
*
* @return - The integer value associated with the provided
* currency, or NONE for unrecognized currencies.
*/
private int xlateSymbolToType(String symbol) {
for (Object[] aXlateTable : xlateTable) {
if (symbol.equals(aXlateTable[0])) {
return (Integer) aXlateTable[1];
}
}
return NONE;
}
private boolean isDigit(char ch) {
return(ch>='0' && ch<='9');
}
public static boolean isCurrency(String test) {
return !getCurrency(test).isNull();
}
@NotNull
public static Currency getCurrency(String wholeValue) {
if(wholeValue == null || wholeValue.length() == 0 || wholeValue.startsWith("UNK")) return NoValue();
return new Currency(wholeValue);
}
public static Currency getCurrency(int whatType, double startValue) {
if(whatType == NONE) return NoValue();
return new Currency(whatType, startValue);
}
public static Currency getCurrency(String symbol, double startValue) {
if(symbol == null || symbol.equalsIgnoreCase("UNK")) return NoValue();
return new Currency(symbol, startValue);
}
public static Currency getCurrency(String symbol, String startValue) {
if(symbol == null || symbol.equalsIgnoreCase("UNK")) return NoValue();
return new Currency(symbol, startValue);
}
public Currency(String wholeValue) {
setValues(wholeValue);
}
public Currency(int whatType, double startValue) {
setValues(whatType, startValue);
}
public Currency(String symbol, double startValue) {
setValues(symbol, startValue);
}
public Currency(String symbol, String startValue) {
setValues(symbol, Double.parseDouble(cleanCommas(startValue)));
}
// Convert [###.###.]###,## to [###,###,]###.##
private static String cleanCommas(String startValue) {
int decimalPos = startValue.length()-3;
if(decimalPos > 0) {
if (startValue.charAt(decimalPos) == '.') {
startValue = startValue.replaceAll(",", "");
} else if(startValue.charAt(decimalPos) == ',') {
startValue = startValue.replaceAll("\\.", "").replaceAll(",", ".");
}
}
return startValue;
}
private int checkLengthMatchStart(String value, String currencyName) {
String lowVal = value.toLowerCase();
String curNam = currencyName.toLowerCase();
if(lowVal.startsWith(curNam + " ")) {
return currencyName.length()+1;
}
if(lowVal.startsWith(curNam)) {
int len = currencyName.length();
while(len < value.length() && !Character.isDigit(value.charAt(len))) len++;
return len;
}
return 0;
}
/**
* @brief Provided an entire string containing a currency prefix and
* an amount, extract the two and set this object's value to equal
* the result.
*
* Is there a reason this doesn't use xlateSymbolToType?
* BUGBUG -- mrs: 03-January-2003 01:28
*
* @param wholeValue - The string containing an entire currency+amount text.
*/
private void setValues(String wholeValue) {
if(wholeValue == null || wholeValue.equals("null")) {
setValues(Currency.NONE, 0.0);
} else {
char firstChar = wholeValue.charAt(0);
int eurLen = checkLengthMatchStart(wholeValue, "EUR");
int gbpLen = checkLengthMatchStart(wholeValue, "GBP");
int frfLen = checkLengthMatchStart(wholeValue, "FRF");
int chfLen = checkLengthMatchStart(wholeValue, "CHF");
int cdnLen = checkLengthMatchStart(wholeValue, "CAD");
int ntdLen = checkLengthMatchStart(wholeValue, "NTD");
int audLen = checkLengthMatchStart(wholeValue, "AUD");
int usdLen = checkLengthMatchStart(wholeValue, "USD");
String parseCurrency;
String valuePortion;
if(wholeValue.startsWith("US $")) {
parseCurrency = "US $";
valuePortion = wholeValue.substring(4);
} else if(wholeValue.startsWith("USD $")) {
// In case eBay ever corrects to the RIGHT currency code for USD.
parseCurrency = "USD $";
valuePortion = wholeValue.substring(5);
} else if(wholeValue.startsWith("AU $")) {
parseCurrency = "AU $";
valuePortion = wholeValue.substring(4);
} else if(usdLen != 0) {
parseCurrency = "USD";
valuePortion = wholeValue.substring(usdLen);
} else if(eurLen != 0) {
parseCurrency = "EUR";
valuePortion = wholeValue.substring(eurLen);
} else if(gbpLen != 0) {
parseCurrency = "GBP";
valuePortion = wholeValue.substring(gbpLen);
} else if(frfLen != 0) {
parseCurrency = "FRF";
valuePortion = wholeValue.substring(frfLen);
} else if(chfLen != 0) {
parseCurrency = "CHF";
valuePortion = wholeValue.substring(chfLen);
} else if(cdnLen != 0) {
parseCurrency = "CAD";
valuePortion = wholeValue.substring(cdnLen);
} else if(ntdLen != 0) {
parseCurrency = "NTD";
valuePortion = wholeValue.substring(ntdLen);
} else if(audLen != 0) {
parseCurrency = "AUD";
valuePortion = wholeValue.substring(audLen);
} else if(wholeValue.startsWith("NT$")) {
parseCurrency = "NTD";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.startsWith("SGD")) {
parseCurrency = "SGD";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.startsWith("sgd")) {
parseCurrency = "SGD";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.startsWith("INR")) {
parseCurrency = "INR";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.startsWith("inr")) {
parseCurrency = "INR";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.startsWith("nt$")) {
parseCurrency = "NTD";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.startsWith("au$")) {
parseCurrency = "AUD";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.startsWith("C $")) {
parseCurrency = "C $";
valuePortion = wholeValue.substring(3);
} else if(wholeValue.charAt(0) == pound) {
parseCurrency = "GBP";
valuePortion = wholeValue.substring(1);
} else {
if(!isDigit(firstChar) && firstChar != '$') {
int semiIndex = wholeValue.indexOf(";");
if(semiIndex == -1) {
semiIndex = wholeValue.indexOf(" ");
}
if(semiIndex != -1) {
parseCurrency = wholeValue.substring(0, semiIndex);
valuePortion = wholeValue.substring(parseCurrency.length()+1);
} else {
parseCurrency = "$";
valuePortion = wholeValue;
}
} else {
parseCurrency = "$";
if(isDigit(firstChar)) {
valuePortion = wholeValue;
} else {
valuePortion = wholeValue.substring(1);
}
}
}
// Kill off non-digit characters.
while(valuePortion.length() != 0 && !Character.isDigit(valuePortion.charAt(0))) valuePortion = valuePortion.substring(1);
// If anything's left, try and parse it.
if(valuePortion.length() != 0) {
double actualValue;
try {
String cvt = cleanCommas(valuePortion);
actualValue = df.parse(cvt).doubleValue();
} catch(java.text.ParseException e) {
JConfig.log().handleException("currency parse!", e);
actualValue = 0.0;
}
setValues(parseCurrency, actualValue);
} else {
setValues(null);
}
}
}
/**
* @brief If it's set as two seperate entries, then we use the MUCH
* cleaner xlateSymbolToType function.
*
* This should be the basic method that setValues works also.
*
* @param symbol - The string form of a currency symbol.
* @param startValue - The amount associated with the currency.
*/
private void setValues(String symbol, double startValue) {
setValues(xlateSymbolToType(symbol), startValue);
}
/**
* @brief The underlying setter that assigns the currency and amounts.
*
* @param whatType - The Currency type to set to.
* @param startValue - The amount represented.
*/
private void setValues(int whatType, double startValue) {
mCurrencyType = whatType;
mValue = startValue;
df.setMinimumFractionDigits(2);
df.setMaximumFractionDigits(2);
}
/**
* @brief Get the full, storable textual name for the currency type
* of this object.
*
* @return A string containing a full ISO currency name.
*/
public String fullCurrencyName() {
switch(mCurrencyType) {
case US_DOLLAR: return("USD");
case AU_DOLLAR: return("AUD");
case NT_DOLLAR: return("NTD");
case HK_DOLLAR: return("HKD");
case MY_REAL: return("MYR");
case SG_DOLLAR: return("SGD");
case IND_RUPEE: return("INR");
case UK_POUND: return("GBP");
case JP_YEN: return("JPY");
case GER_MARK: return("DM");
case FR_FRANC: return("FRF");
case CH_FRANC: return("CHF");
case CAN_DOLLAR: return("CAD");
case EURO: return("EUR");
default: return("UNK");
}
}
public double getValue() { return mValue; }
public String fullCurrency() {
return fullCurrencyName() + " " + getValueString();
}
/**
* @brief Add two currencies and return a new currency containing
* the result of the two added together.
*
* @param addValue - The currency value/amount to add. It must be
* of the same currency type as 'this'.
*
* @return A new currency object containing the sum of the two
* amounts provided, with the same currency type as them.
*
* @throws CurrencyTypeException if the two objects are of different currencies.
*/
public Currency add(Currency addValue) throws CurrencyTypeException {
if(addValue == null) throw new CurrencyTypeException("Cannot add null Currency.");
if(addValue.getCurrencyType() == mCurrencyType) {
return new Currency(mCurrencyType, mValue + addValue.getValue());
}
// If only one currency is known, return the result as the known currency.
if (mCurrencyType == NONE) return new Currency(addValue.getCurrencyType(), mValue + addValue.getValue());
if (addValue.getCurrencyType() == NONE) return new Currency(mCurrencyType, mValue + addValue.getValue());
throw new CurrencyTypeException("Cannot add " + fullCurrencyName() + " to " + addValue.fullCurrencyName() + ".");
}
/**
* @brief Subtract two currencies and return a new currency containing
* the result of the passed value subtracted from this objects value.
*
* @param subValue - The currency value/amount to subtract. It must be
* of the same currency type as 'this'.
*
* @return A new currency object containing the difference of the two
* amounts provided, with the same currency type as them.
*
* @throws CurrencyTypeException if the two objects are of different currencies.
*/
public Currency subtract(Currency subValue) throws CurrencyTypeException {
if(subValue == null) throw new CurrencyTypeException("Cannot subtract null Currency.");
if(subValue.getCurrencyType() == mCurrencyType) {
return new Currency(mCurrencyType, mValue - subValue.getValue());
}
// If only one currency is known, return the result as the known currency.
if(mCurrencyType == NONE) return new Currency(subValue.getCurrencyType(), mValue - subValue.getValue());
if(subValue.getCurrencyType() == NONE) return new Currency(mCurrencyType, mValue - subValue.getValue());
throw new CurrencyTypeException("Cannot subtract " + fullCurrencyName() + " from " + subValue.fullCurrencyName() + ".");
}
public int getCurrencyType() { return mCurrencyType; }
public String getCurrencySymbol() {
switch(mCurrencyType) {
case US_DOLLAR: return("$");
case NT_DOLLAR: return("nt$");
case HK_DOLLAR: return("hk$");
case MY_REAL: return("myr");
case SG_DOLLAR: return("sgd");
case IND_RUPEE: return("Rs.");
case UK_POUND: return(objPound.toString());
case JP_YEN: return("\u00A5"); // HACKHACK
case FR_FRANC: return("fr");
case CH_FRANC: return("chf");
case GER_MARK: return("dm");
case CAN_DOLLAR: return("c$");
case AU_DOLLAR: return("au$");
case EURO: return("\u20AC");
default: return("unk");
}
}
/**
* @brief Format the currency and amount as appropriate for the
* current locale.
*
* This is kind of interesting, because it will display in one
* fashion, but when it snipes or bids, it's all against the
* US sites, so it's all operating in US forms at that point.
*
* @return A nicely formatted, locale-correct money value, prefixed
* with the best currency symbol for the currency type.
*/
public String toString() {
if(isNull()) {
return("null");
} else {
String cvtToString = getCurrencySymbol();
cvtToString += df.format(mValue);
return(cvtToString);
}
}
/**
* @brief Format the amount as appropriate for the current locale.
*
* This is kind of interesting, because it will display in one
* fashion, but when it snipes or bids, it's all against the
* US sites, so it's all operating in US forms at that point.
*
* @return A nicely formatted, locale-correct money value, prefixed
* with the best currency symbol for the currency type.
*/
public String getValueString() {
if(isNull()) {
return("null");
} else {
return df.format(mValue);
}
}
/**
* @brief Implementing equals means I should implement hashCode().
*
* @return - The hash code of the string consisting of the full
* currency named followed by the value as a string. Null/invalid
* currency entries return 0.
*/
public int hashCode() {
if(isNull()) return 0;
String tmp = fullCurrencyName() + getValueString();
return tmp.hashCode();
}
/**
* @brief Must be able to compare currency values for equality.
*
* @param inValue - The value to compare against.
*
* @return True if the two values are the same, or the currency and
* amount are the same. False otherwise, including false if it is
* an entirely different class. Differing currencies are always
* unequal.
*/
public boolean equals(Object inValue) {
// Be careful not to compare with null.
if(inValue == null) return false;
// Shortcut for this.equals(this)
if(inValue == this) return true;
// Is it this class even?
if(!(inValue instanceof Currency)) return false;
// Okay, now cast it because it's safe.
Currency otherValue = (Currency) inValue;
boolean sameCurrency = (otherValue.getCurrencyType() == mCurrencyType);
boolean sameValue = ((int) (otherValue.getValue() * 1000)) == ((int) (mValue * 1000));
return(sameCurrency && sameValue);
}
/**
* @brief Determine if (this < otherValue).
*
* This only works for items of the same currency type.
*
* @param otherValue - The value to compare against.
*
* @return - True if this amount is less than the otherValue amount
* and both currency types are equal. If the otherValue is null,
* the same object as this (this.less(this)), or this amount is
* actually less, then it returns false.
*
* @throws CurrencyTypeException if you try to compare different currencies.
*/
public boolean less(Currency otherValue) throws CurrencyTypeException {
// Be careful
if(otherValue == null) return false;
// Shortcut
if(otherValue == this) return false;
boolean sameCurrency = (otherValue.getCurrencyType() == mCurrencyType);
if(!sameCurrency) {
throw new CurrencyTypeException("Cannot compare different currencies.");
}
boolean lowerValue = Double.compare((double) ((int) (otherValue.getValue() * 1000)), (double) (int) (mValue * 1000)) == 1;
return(lowerValue);
}
/**
* @brief Utility function to check if this is a purely invalid currency.
*
* It should probably check against the invalid currency object first...
*
* @return True if this is a 'null currency' object.
*/
public boolean isNull() {
return(mValue == 0.0 && mCurrencyType == NONE);
}
/**
* @brief The comparable interface defines this, and so I'm
* comparing using the well defined set of rules for Comparables.
*
* Defined with 'equals' and less', but both should be special cases
* of this, since some checks are duplicated.
*
* @param o - The object to compare against.
*
* @return -1 if o's class is Currency, it's the same currency type,
* and the amount of this is less than o's amount.
* 0 if o's class is Currency, it's the same currency type,
* and the amount of this is the same as o's amount.
* 1 if o's class is Currency, it's the same currency type,
* and the amount of this is greater than o's amount.
*
* @throws ClassCastException if you try to compareTo non-Currency classes.
*/
public int compareTo(Object o) {
// We are always greater than null
if(o == null) return 1;
// We are always equal to ourselves
if(o == this) return 0;
// This is an incorrect usage and should be caught.
if(!(o instanceof Currency)) throw new ClassCastException("Currency cannot compareTo different classes!");
// Okay, now cast it because it's safe.
Currency otherValue = (Currency) o;
if(otherValue.isNull()) return 1;
if(isNull()) return -1;
try {
if(less(otherValue)) return -1;
} catch(ClassCastException e) {
/* This should be impossible */
throw new ClassCastException("Currency cannot compareTo different classes!\n" + e);
} catch (CurrencyTypeException e) {
// Can't re-throw (or not catch!) because Object.compareTo doesn't throw CurrencyTypeException!
throw new ClassCastException("Currency cannot compareTo different currencies!\n" + e);
}
if(equals(otherValue)) return 0;
return 1;
}
public static PersistenceDelegate getDelegate() {
return new DefaultPersistenceDelegate(new String[]{"mCurrencyType", "mValue"});
}
}