Following this answer of a question about measuring units of length, you can do it in a similar way for currencies:
import java.text.ParseException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.SpinnerNumberModel;
public class MainWithFormatter {
//For more currencies and their ISO codes, visit https://en.wikipedia.org/wiki/List_of_circulating_currencies
public static enum Currency {
EUR, //Euro
USD, //United States Dollar
GBP, //British Pound
JPY //Japanese Yen
}
public static class CurrencyFormatter extends AbstractFormatter {
private static final Pattern PATTERN;
static {
//Building the Pattern is not too tricky. It just needs some attention.
final String blank = "\\p{Blank}"; //Match any whitespace character.
final String blankGroupAny = "(" + blank + "*)"; //Match any whitespace character, zero or more times and group them.
final String digits = "\\d"; //Match any digit.
final String digitsGroup = "(" + digits + "+)"; //Match any digit, at least once and group them.
final String digitsSuperGroup = "(\\-?" + digitsGroup + "\\.?" + digitsGroup + "?)"; //Matches for example "-2.4" or "2.4" or "2" or "-2" in the same group!
//Create the pattern part which matches any of the available currencies...
final Currency[] currencies = Currency.values();
final StringBuilder currenciesBuilder = new StringBuilder(Pattern.quote("")); //Empty currency value is valid.
for (int i = 0; i < currencies.length; ++i)
currenciesBuilder.append('|').append(Pattern.quote(currencies[i].name()));
final String currenciessGroup = "(" + currenciesBuilder + ")";
final String full = "^" + blankGroupAny + digitsSuperGroup + blankGroupAny + currenciessGroup + blankGroupAny + "$"; //Compose full pattern.
PATTERN = Pattern.compile(full);
}
private final Currency defaultCurrency;
private Currency lastCurrency;
private boolean verbose; //Show the default currency while spinning or not?
public CurrencyFormatter(final Currency defaultCurrency) {
this.defaultCurrency = Objects.requireNonNull(defaultCurrency);
lastCurrency = defaultCurrency;
verbose = true;
}
@Override
public Object stringToValue(final String text) throws ParseException {
if (text == null || text.trim().isEmpty())
throw new ParseException("Null or empty text.", 0);
try {
final Matcher matcher = PATTERN.matcher(text.toUpperCase());
if (!matcher.matches())
throw new ParseException("Invalid input.", 0);
final String amountStr = matcher.group(2),
currencyStr = matcher.group(6);
final double amount = Double.parseDouble(amountStr);
if (currencyStr.trim().isEmpty()) {
lastCurrency = defaultCurrency;
verbose = false;
}
else {
lastCurrency = Currency.valueOf(currencyStr);
verbose = true;
}
return amount;
}
catch (final IllegalArgumentException iax) {
throw new ParseException("Failed to parse input \"" + text + "\".", 0);
}
}
public Currency getLastCurrency() {
return lastCurrency;
}
@Override
public String valueToString(final Object value) throws ParseException {
final String amount = String.format("%.2f", value).replace(',', '.');
return verbose ? (amount + ' ' + lastCurrency.name()) : amount;
}
}
public static class CurrencyFormatterFactory extends AbstractFormatterFactory {
@Override
public AbstractFormatter getFormatter(final JFormattedTextField tf) {
if (!(tf.getFormatter() instanceof CurrencyFormatter))
return new CurrencyFormatter(Currency.USD);
return tf.getFormatter();
}
}
public static void main(final String[] args) {
final JSpinner spin = new JSpinner(new SpinnerNumberModel(0d, -1000000d, 1000000d, 0.01d));
final JFormattedTextField jftf = ((DefaultEditor) spin.getEditor()).getTextField();
jftf.setFormatterFactory(new CurrencyFormatterFactory());
//Added a button to demonstrate how to obtain the value the user has selected:
final JButton check = new JButton("Check!");
check.addActionListener(e -> {
final CurrencyFormatter cf = (CurrencyFormatter) jftf.getFormatter();
JOptionPane.showMessageDialog(check, Objects.toString(spin.getValue()) + ' ' + cf.getLastCurrency().name() + '!');
});
final JPanel contents = new JPanel(); //FlowLayout.
contents.add(spin);
contents.add(check);
final JFrame frame = new JFrame("JSpinner currencies.");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(contents);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
You just need to create a custom AbstractFormatter
for the formatted text field of the default editor of the spinner that will handle such strings.
Although you could do it simply by putting two JSpinner
s, one for the amount and the other for the currency.
Edit 1: or, you can work with the built-in java.util.Currency
class:
import java.text.ParseException;
import java.util.Currency;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.SpinnerNumberModel;
public class MainWithCurrency {
public static class CurrencyFormatter extends AbstractFormatter {
private static final Pattern PATTERN;
static {
//Building the Pattern is not too tricky. It just needs some attention.
final String blank = "\\p{Blank}"; //Match any whitespace character.
final String blankGroupAny = "(" + blank + "*)"; //Match any whitespace character, zero or more times and group them.
final String digits = "\\d"; //Match any digit.
final String digitsGroup = "(" + digits + "+)"; //Match any digit, at least once and group them.
final String digitsSuperGroup = "(\\-?" + digitsGroup + "\\.?" + digitsGroup + "?)"; //Matches for example "-2.4" or "2.4" or "2" or "-2" in the same group!
//Create the pattern part which matches any of the available currencies...
final String currencyCodes = "[A-Z]{3}|" + Pattern.quote(""); //Currency code consists of 3 letters, or is empty for default value.
final String currenciessGroup = "(" + currencyCodes + ")";
final String full = "^" + blankGroupAny + digitsSuperGroup + blankGroupAny + currenciessGroup + blankGroupAny + "$"; //Compose full pattern.
PATTERN = Pattern.compile(full);
}
private final Set<String> supportedCurrencies;
private final String defaultCurrency;
private String lastCurrency;
private boolean verbose; //Show the default currency while spinning or not?
public CurrencyFormatter(final Set<Currency> supportedCurrencies,
final Currency defaultCurrency) {
if (!supportedCurrencies.contains(defaultCurrency))
throw new IllegalArgumentException("Default currency is not supported.");
this.supportedCurrencies = supportedCurrencies.stream().map(currency -> currency.getCurrencyCode()).collect(Collectors.toSet());
this.defaultCurrency = defaultCurrency.getCurrencyCode();
lastCurrency = this.defaultCurrency;
verbose = true;
}
@Override
public Object stringToValue(final String text) throws ParseException {
if (text == null || text.trim().isEmpty())
throw new ParseException("Null or empty text.", 0);
try {
final Matcher matcher = PATTERN.matcher(text.toUpperCase());
if (!matcher.matches())
throw new ParseException("Invalid input.", 0);
final String amountStr = matcher.group(2).trim(),
currencyStr = matcher.group(6).trim();
final double amount = Double.parseDouble(amountStr);
if (currencyStr.isEmpty()) {
lastCurrency = defaultCurrency;
verbose = false;
}
else {
if (!supportedCurrencies.contains(currencyStr))
throw new ParseException("Unsupported currency.", 0);
lastCurrency = currencyStr;
verbose = true;
}
return amount;
}
catch (final IllegalArgumentException iax) {
throw new ParseException("Failed to parse input \"" + text + "\".", 0);
}
}
public Currency getLastCurrency() {
return Currency.getInstance(lastCurrency);
}
@Override
public String valueToString(final Object value) throws ParseException {
final String amount = String.format("%.2f", value).replace(',', '.');
return verbose ? (amount + ' ' + lastCurrency) : amount;
}
}
public static class CurrencyFormatterFactory extends AbstractFormatterFactory {
@Override
public AbstractFormatter getFormatter(final JFormattedTextField tf) {
if (!(tf.getFormatter() instanceof CurrencyFormatter))
return new CurrencyFormatter(Currency.getAvailableCurrencies(), Currency.getInstance("USD"));
return tf.getFormatter();
}
}
public static void main(final String[] args) {
final JSpinner spin = new JSpinner(new SpinnerNumberModel(0d, -1000000d, 1000000d, 0.01d));
final JFormattedTextField jftf = ((DefaultEditor) spin.getEditor()).getTextField();
jftf.setFormatterFactory(new CurrencyFormatterFactory());
//Added a button to demonstrate how to obtain the value the user has selected:
final JButton check = new JButton("Check!");
check.addActionListener(e -> {
final CurrencyFormatter cf = (CurrencyFormatter) jftf.getFormatter();
JOptionPane.showMessageDialog(check, Objects.toString(spin.getValue()) + ' ' + cf.getLastCurrency().getCurrencyCode() + '!');
});
final JPanel contents = new JPanel(); //FlowLayout.
contents.add(spin);
contents.add(check);
final JFrame frame = new JFrame("JSpinner currencies.");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(contents);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
In this code (of the first edit) I am parsing the currency ISO code and checking it against a Set
of supported currencies.
Note: I've read DecimalFormat
documentation and the corresponding Java Tutorial, but cannot find anything about specifying optional currency symbols, so that's why I think you have to work with a custom Pattern
like the preceding sample codes in this answer and I'm also posting those links here, in case someone else finds the solution within them.