/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.util;
import io.bitsquare.btc.BitcoinNetwork;
import io.bitsquare.common.util.MathUtils;
import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.locale.LanguageUtil;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.trade.offer.Offer;
import io.bitsquare.user.Preferences;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import org.bitcoinj.utils.MonetaryFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class BSFormatter {
private static final Logger log = LoggerFactory.getLogger(BSFormatter.class);
private Locale locale = Preferences.getDefaultLocale();
private boolean useMilliBit;
private int scale = 3;
// We don't support localized formatting. Format is always using "." as decimal mark and no grouping separator.
// Input of "," as decimal mark (like in german locale) will be replaced with ".".
// Input of a group separator (1,123,45) lead to an validation error.
// Note: BtcFormat was intended to be used, but it lead to many problems (automatic format to mBit,
// no way to remove grouping separator). It seems to be not optimal for user input formatting.
private MonetaryFormat coinFormat = MonetaryFormat.BTC;
// private String currencyCode = CurrencyUtil.getDefaultFiatCurrencyAsCode();
// format is like: 1,00 never more then 2 decimals
private final MonetaryFormat fiatFormat = MonetaryFormat.FIAT.repeatOptionalDecimals(0, 0);
private DecimalFormat decimalFormat = new DecimalFormat("#.#");
@Inject
public BSFormatter() {
/* if (user.tradeCurrencyProperty().get() == null)
setFiatCurrencyCode(CurrencyUtil.getDefaultFiatCurrencyAsCode());
else if (user.tradeCurrencyProperty().get() != null)
setFiatCurrencyCode(user.tradeCurrencyProperty().get().getCode());
user.tradeCurrencyProperty().addListener((ov, oldValue, newValue) -> {
if (newValue != null)
setFiatCurrencyCode(newValue.getCode());
});*/
}
///////////////////////////////////////////////////////////////////////////////////////////
// Config
///////////////////////////////////////////////////////////////////////////////////////////
public void useMilliBitFormat(boolean useMilliBit) {
this.useMilliBit = useMilliBit;
coinFormat = getMonetaryFormat();
scale = useMilliBit ? 0 : 3;
}
/**
* Note that setting the locale does not set the currency as it might be independent.
*/
public void setLocale(Locale locale) {
this.locale = locale;
}
private MonetaryFormat getMonetaryFormat() {
if (useMilliBit)
return MonetaryFormat.MBTC;
else
return MonetaryFormat.BTC.minDecimals(2).repeatOptionalDecimals(1, 6);
}
/* public void setFiatCurrencyCode(String currencyCode) {
this.currencyCode = currencyCode;
fiatFormat.code(0, currencyCode);
}*/
///////////////////////////////////////////////////////////////////////////////////////////
// BTC
///////////////////////////////////////////////////////////////////////////////////////////
public String formatCoin(Coin coin) {
if (coin != null) {
try {
return coinFormat.noCode().format(coin).toString();
} catch (Throwable t) {
log.warn("Exception at formatBtc: " + t.toString());
return "";
}
} else {
return "";
}
}
public String formatCoinWithCode(Coin coin) {
if (coin != null) {
try {
// we don't use the code feature from coinFormat as it does automatic switching between mBTC and BTC and
// pre and post fixing
return coinFormat.postfixCode().format(coin).toString();
} catch (Throwable t) {
log.warn("Exception at formatBtcWithCode: " + t.toString());
return "";
}
} else {
return "";
}
}
public Coin parseToCoin(String input) {
if (input != null && input.length() > 0) {
try {
return coinFormat.parse(cleanInput(input));
} catch (Throwable t) {
log.warn("Exception at parseToBtc: " + t.toString());
return Coin.ZERO;
}
} else {
return Coin.ZERO;
}
}
/**
* Converts to a coin with max. 4 decimal places. Last place gets rounded.
* 0.01234 -> 0.0123
* 0.01235 -> 0.0124
*
* @param input
* @return
*/
public Coin parseToCoinWith4Decimals(String input) {
try {
return Coin.valueOf(new BigDecimal(parseToCoin(cleanInput(input)).value).setScale(-scale - 1,
BigDecimal.ROUND_HALF_UP).setScale(scale + 1).toBigInteger().longValue());
} catch (Throwable t) {
if (input != null && input.length() > 0)
log.warn("Exception at parseToCoinWith4Decimals: " + t.toString());
return Coin.ZERO;
}
}
public boolean hasBtcValidDecimals(String input) {
return parseToCoin(input).equals(parseToCoinWith4Decimals(input));
}
/**
* Transform a coin with the properties defined in the format (used to reduce decimal places)
*
* @param coin The coin which should be transformed
* @return The transformed coin
*/
public Coin reduceTo4Decimals(Coin coin) {
return parseToCoin(formatCoin(coin));
}
///////////////////////////////////////////////////////////////////////////////////////////
// FIAT
///////////////////////////////////////////////////////////////////////////////////////////
public String formatFiat(Fiat fiat) {
if (fiat != null) {
try {
return fiatFormat.noCode().format(fiat).toString();
} catch (Throwable t) {
log.warn("Exception at formatFiat: " + t.toString());
return "N/A " + fiat.getCurrencyCode();
}
} else {
return "N/A";
}
}
private String formatFiatWithCode(Fiat fiat) {
if (fiat != null) {
try {
return fiatFormat.noCode().format(fiat).toString() + " " + fiat.getCurrencyCode();
} catch (Throwable t) {
log.warn("Exception at formatFiatWithCode: " + t.toString());
return "N/A " + fiat.getCurrencyCode();
}
} else {
return "N/A";
}
}
private Fiat parseToFiat(String input, String currencyCode) {
if (input != null && input.length() > 0) {
try {
return Fiat.parseFiat(currencyCode, cleanInput(input));
} catch (Exception e) {
log.warn("Exception at parseToFiat: " + e.toString());
return Fiat.valueOf(currencyCode, 0);
}
} else {
return Fiat.valueOf(currencyCode, 0);
}
}
/**
* Converts to a fiat with max. 2 decimal places. Last place gets rounded.
* 0.234 -> 0.23
* 0.235 -> 0.24
*
* @param input
* @return
*/
public Fiat parseToFiatWithPrecision(String input, String currencyCode) {
if (input != null && input.length() > 0) {
try {
return parseToFiat(new BigDecimal(cleanInput(input)).setScale(2, BigDecimal.ROUND_HALF_UP).toString(), currencyCode);
} catch (Throwable t) {
log.warn("Exception at parseToFiatWithPrecision: " + t.toString());
return Fiat.valueOf(currencyCode, 0);
}
}
return Fiat.valueOf(currencyCode, 0);
}
public boolean isFiatAlteredWhenPrecisionApplied(String input, String currencyCode) {
return parseToFiat(input, currencyCode).equals(parseToFiatWithPrecision(input, currencyCode));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Volume
///////////////////////////////////////////////////////////////////////////////////////////
public String formatVolume(Fiat fiat) {
return formatFiat(fiat);
}
public String formatVolumeWithCode(Fiat fiat) {
return formatFiatWithCode(fiat);
}
public String formatVolumeLabel(String currencyCode) {
return formatVolumeLabel(currencyCode, "");
}
public String formatVolumeLabel(String currencyCode, String postFix) {
return CurrencyUtil.getNameByCode(currencyCode) + " amount" + postFix;
}
public String formatMinVolumeAndVolume(Offer offer) {
return formatVolume(offer.getMinOfferVolume()) + " - " + formatVolume(offer.getOfferVolume());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Amount
///////////////////////////////////////////////////////////////////////////////////////////
public String formatAmount(Offer offer) {
return formatCoin(offer.getAmount());
}
public String formatAmountWithMinAmount(Offer offer) {
return formatCoin(offer.getMinAmount()) + " - " + formatCoin(offer.getAmount());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Price
///////////////////////////////////////////////////////////////////////////////////////////
public String formatPrice(Fiat fiat) {
if (fiat != null) {
final String currencyCode = fiat.getCurrencyCode();
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
decimalFormat.setMinimumFractionDigits(8);
decimalFormat.setMaximumFractionDigits(8);
final double value = fiat.value != 0 ? 10000D / fiat.value : 0;
return decimalFormat.format(MathUtils.roundDouble(value, 8)).replace(",", ".");
} else
return formatFiat(fiat);
} else {
return "N/A";
}
}
public String formatPriceWithCode(Fiat fiat) {
return formatPrice(fiat) + " " + getCurrencyPair(fiat.getCurrencyCode());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Market price
///////////////////////////////////////////////////////////////////////////////////////////
public String formatMarketPrice(double price, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return formatMarketPrice(price, 2);
else
return formatMarketPrice(price, 8);
}
public String formatMarketPrice(double price, int precision) {
return formatRoundedDoubleWithPrecision(price, precision);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Other
///////////////////////////////////////////////////////////////////////////////////////////
public String formatRoundedDoubleWithPrecision(double value, int precision) {
decimalFormat.setMinimumFractionDigits(precision);
decimalFormat.setMaximumFractionDigits(precision);
return decimalFormat.format(MathUtils.roundDouble(value, precision)).replace(",", ".");
}
public String getDirectionWithCode(Offer.Direction direction, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return (direction == Offer.Direction.BUY) ? "Buy BTC" : "Sell BTC";
else
return (direction == Offer.Direction.SELL) ? "Buy " + currencyCode : "Sell " + currencyCode;
}
public String getDirectionWithCodeDetailed(Offer.Direction direction, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return (direction == Offer.Direction.BUY) ? "buying BTC with " + currencyCode : "selling BTC for " + currencyCode;
else
return (direction == Offer.Direction.SELL) ? "buying " + currencyCode + " (selling BTC)" : "selling " + currencyCode + " (buying BTC)";
}
public String arbitratorAddressesToString(List<NodeAddress> nodeAddresses) {
return nodeAddresses.stream().map(NodeAddress::getFullAddress).collect(Collectors.joining(", "));
}
public String languageCodesToString(List<String> languageLocales) {
return languageLocales.stream().map(LanguageUtil::getDisplayName).collect(Collectors.joining(", "));
}
public String formatDateTime(Date date) {
if (date != null) {
DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
return dateFormatter.format(date) + " " + timeFormatter.format(date);
} else {
return "";
}
}
public String formatDateTimeSpan(Date dateFrom, Date dateTo) {
if (dateFrom != null && dateTo != null) {
DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
return dateFormatter.format(dateFrom) + " " + timeFormatter.format(dateFrom) + " - " + timeFormatter.format(dateTo);
} else {
return "";
}
}
public String formatTime(Date date) {
if (date != null) {
DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
return timeFormatter.format(date);
} else {
return "";
}
}
public String formatDate(Date date) {
if (date != null) {
DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
return dateFormatter.format(date);
} else {
return "";
}
}
public String formatToPercentWithSymbol(double value) {
return formatToPercent(value) + "%";
}
public String formatPercentagePrice(double value) {
return formatToPercentWithSymbol(value);
}
public String formatToPercent(double value) {
DecimalFormat decimalFormat = new DecimalFormat("#.##");
decimalFormat.setMinimumFractionDigits(2);
decimalFormat.setMaximumFractionDigits(2);
return decimalFormat.format(MathUtils.roundDouble(value * 100.0, 2)).replace(",", ".");
}
public double parseNumberStringToDouble(String percentString) throws NumberFormatException {
try {
String input = percentString.replace(",", ".");
input = input.replace(" ", "");
return Double.parseDouble(input);
} catch (NumberFormatException e) {
throw e;
}
}
public double parsePercentStringToDouble(String percentString) throws NumberFormatException {
try {
String input = percentString.replace("%", "");
input = input.replace(",", ".");
input = input.replace(" ", "");
double value = Double.parseDouble(input);
return value / 100d;
} catch (NumberFormatException e) {
throw e;
}
}
private String cleanInput(String input) {
input = input.replace(",", ".");
// don't use String.valueOf(Double.parseDouble(input)) as return value as it gives scientific
// notation (1.0E-6) which screw up coinFormat.parse
//noinspection ResultOfMethodCallIgnored
Double.parseDouble(input);
return input;
}
public String formatDurationAsWords(long durationMillis) {
return formatDurationAsWords(durationMillis, false);
}
public static String formatDurationAsWords(long durationMillis, boolean showSeconds) {
String format;
if (showSeconds)
format = "d\' days, \'H\' hours, \'m\' minutes, \'s\' seconds\'";
else
format = "d\' days, \'H\' hours, \'m\' minutes\'";
String duration = DurationFormatUtils.formatDuration(durationMillis, format);
String tmp;
duration = " " + duration;
tmp = StringUtils.replaceOnce(duration, " 0 days", "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 hours", "");
if (tmp.length() != duration.length()) {
tmp = StringUtils.replaceOnce(tmp, " 0 minutes", "");
duration = tmp;
if (tmp.length() != tmp.length()) {
duration = StringUtils.replaceOnce(tmp, " 0 seconds", "");
}
}
}
if (duration.length() != 0) {
duration = duration.substring(1);
}
tmp = StringUtils.replaceOnce(duration, " 0 seconds", "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 minutes", "");
if (tmp.length() != duration.length()) {
duration = tmp;
tmp = StringUtils.replaceOnce(tmp, " 0 hours", "");
if (tmp.length() != duration.length()) {
duration = StringUtils.replaceOnce(tmp, " 0 days", "");
}
}
}
duration = " " + duration;
duration = StringUtils.replaceOnce(duration, " 1 seconds", " 1 second");
duration = StringUtils.replaceOnce(duration, " 1 minutes", " 1 minute");
duration = StringUtils.replaceOnce(duration, " 1 hours", " 1 hour");
duration = StringUtils.replaceOnce(duration, " 1 days", " 1 day");
if (duration.startsWith(" ,"))
duration = duration.replace(" ,", "");
else if (duration.startsWith(", "))
duration = duration.replace(", ", "");
if (duration.equals(""))
duration = "Trade period is over";
return duration.trim();
}
public String booleanToYesNo(boolean value) {
return value ? "Yes" : "No";
}
public String formatBitcoinNetwork(BitcoinNetwork bitcoinNetwork) {
switch (bitcoinNetwork) {
case MAINNET:
return "Mainnet";
case TESTNET:
return "Testnet";
case REGTEST:
return "Regtest";
default:
return "";
}
}
public String getDirectionBothSides(Offer.Direction direction, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return direction == Offer.Direction.BUY ? "Offerer as BTC buyer / Taker as BTC seller" :
"Offerer as BTC seller / Taker as BTC buyer";
else
return direction == Offer.Direction.SELL ? "Offerer as " + currencyCode + " buyer / Taker as " + currencyCode + " seller" :
"Offerer as " + currencyCode + " seller / Taker as " + currencyCode + " buyer";
}
public String getDirectionForBuyer(boolean isMyOffer, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return isMyOffer ? "You are buying BTC as offerer / Taker is selling BTC" :
"You are buying BTC as taker / Offerer is selling BTC";
else
return isMyOffer ? "You are selling " + currencyCode + " as offerer / Taker is buying " + currencyCode + "" :
"You are selling " + currencyCode + " as taker / Offerer is buying " + currencyCode + "";
}
public String getDirectionForSeller(boolean isMyOffer, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return isMyOffer ? "You are selling BTC as offerer / Taker is buying BTC" :
"You are selling BTC as taker / Offerer is buying BTC";
else
return isMyOffer ? "You are buying " + currencyCode + " as offerer / Taker is selling " + currencyCode + "" :
"You are buying " + currencyCode + " as taker / Offerer is selling " + currencyCode + "";
}
public String getDirectionForTakeOffer(Offer.Direction direction, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return direction == Offer.Direction.BUY ? "You are selling BTC (buying " + currencyCode + ")" :
"You are buying BTC (selling " + currencyCode + ")";
else
return direction == Offer.Direction.SELL ? "You are selling " + currencyCode + " (buying BTC)" :
"You are buying " + currencyCode + " (selling BTC)";
}
public String getOfferDirectionForCreateOffer(Offer.Direction direction, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return direction == Offer.Direction.BUY ? "You are creating an offer to buy BTC" :
"You are creating an offer to sell BTC";
else
return direction == Offer.Direction.SELL ? "You are creating an offer to buy " + currencyCode + " (selling BTC)" :
"You are creating an offer to sell " + currencyCode + " (buying BTC)";
}
public String getRole(boolean isBuyerOffererAndSellerTaker, boolean isOfferer, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
if (isBuyerOffererAndSellerTaker)
return isOfferer ? "BTC buyer as offerer" : "BTC seller as taker";
else
return isOfferer ? "BTC seller as offerer" : "BTC buyer as taker";
} else {
if (isBuyerOffererAndSellerTaker)
return isOfferer ? currencyCode + " seller as offerer" : currencyCode + " buyer as taker";
else
return isOfferer ? currencyCode + " buyer as offerer" : currencyCode + " seller as taker";
}
}
public String formatBytes(long bytes) {
double kb = 1024;
double mb = kb * kb;
DecimalFormat decimalFormat = new DecimalFormat("#.##");
if (bytes < kb)
return bytes + " bytes";
else if (bytes < mb)
return decimalFormat.format(bytes / kb) + " KB";
else
return decimalFormat.format(bytes / mb) + " MB";
}
public String getCurrencyPair(String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return "BTC/" + currencyCode;
else
return currencyCode + "/BTC";
}
public String getCounterCurrency(String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode))
return currencyCode;
else
return "BTC";
}
public String getBaseCurrency(String currencyCode) {
if (CurrencyUtil.isCryptoCurrency(currencyCode))
return currencyCode;
else
return "BTC";
}
public String getCounterCurrencyAndCurrencyPair(String currencyCode) {
return getCounterCurrency(currencyCode) + " (" + getCurrencyPair(currencyCode) + ")";
}
public String getCurrencyNameAndCurrencyPair(String currencyCode) {
return CurrencyUtil.getNameByCode(currencyCode) + " (" + getCurrencyPair(currencyCode) + ")";
}
public String getPriceWithCurrencyCode(String currencyCode) {
if (CurrencyUtil.isCryptoCurrency(currencyCode))
return "Price in BTC for 1 " + currencyCode;
else
return "Price in " + currencyCode + " for 1 BTC";
}
}