1

I have written a small program for our company, which takes care of the sale of drinks and every user has his own account. To top up his account there is a JSpinner, which looks like this:

spinner

An employee asked me if I could add the currency to this spinner. So I implemented it, but now you can only deposit with the currency symbol and not without it, which disturbed other staff members, so let's get to my question, how do I manage to accept both entries with currency and without?

Basic Spinner(like in the image i posted above):

final SpinnerNumberModel spinnerModel = new SpinnerNumberModel( 1, 1, 1000, 1 );
final JSpinner valueSpinner = new JSpinner( spinnerModel );

To add the currency I used this code snippet, which works fine

    String pattern = "0€";
    JSpinner.NumberEditor editor = new JSpinner.NumberEditor( valueSpinner, pattern );
    valueSpinner.setEditor( editor );

I have already tried to write a custom JSpinner, but I couldn't achieve that the Spinner would take both Entries.

Kcits970
  • 640
  • 1
  • 5
  • 23
Niklas
  • 31
  • 6

2 Answers2

0

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 JSpinners, 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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
gthanop
  • 3,035
  • 2
  • 10
  • 27
  • The code in this answer is honestly overkill. I feel like this answer gives way more than what the original question asked for. – Kcits970 Jan 24 '22 at 14:53
  • @Kcits970 other than the `main` method, it is, in my humble opinion, just an implementation of an `AbstractFormatter`. It surely is not a one-liner, but on the other hand it is parsing user input. Maybe the original poster could use a spinner or checkbox side by side (one for the amount and one for the currency), but I as I understood they are requesting to do this on the same component. Can you please suggest improvements? – gthanop Jan 24 '22 at 16:47
  • After going through the API documentation, I can now see what's going on. I was just overwhelmed by long code. There's one thing I do want to point out about this answer. I think the OP simply wants to format the spinner so that it displays the euro symbol, and allows unformatted and formatted entries. This answer regards multiple currencies, so I think it becomes surplus at that point. – Kcits970 Jan 25 '22 at 00:30
  • Another thing I want to point out: If you type in numbers without the currency text, the spinner does not display the currency anymore. I believe the OP wants a spinner that displays the currency symbol regardless of the input format. – Kcits970 Jan 26 '22 at 07:10
0

I'm assuming that your desired functionality of the JSpinner is something like this:

User Input Actual Value Display Text
10€ 10 10€
10 10 10€

In that case, you can use JFormattedTextField.AbstractFormatterFactory.

First, there are a couple things to note about JSpinners. According to Oracle's JSpinner Documentation, they mention that:

A spinner is a compound component with three subcomponents: two small buttons and an editor. The editor can be any JComponent, but by default it is implemented as a panel that contains a formatted text field.

In order to specify the format of the display text, we need the instance of the formatted text field in the spinner editor. This can be achieved with JSpinner.DefaultEditor.getTextField(), which returns the JFormattedTextField child of this editor.

JFormattedTextField provides a method called setFormatterFactory, which takes an instance of JFormattedTextField.AbstractFormatterFactory as its argument. We can override this class, and specify the exact format that it will accept.

Here's a SSCCE that demonstrates what I've said so far.

import javax.swing.*;
import java.awt.*;
import java.text.ParseException;

public class Main extends JFrame {
    JSpinner spinner;
    JButton button;

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(() -> new Main());
    }

    public Main() {
        int min = 0;
        int max = 10;

        spinner = new JSpinner(new SpinnerNumberModel(0, min, max, 1));
        button = new JButton("Button");

        ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setFormatterFactory(new MyIntegerFormatterFactory("\u20AC", min, max));
        ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setColumns(6);

        addComponents();
        configureSettings();
    }

    public void addComponents() {
        getContentPane().setLayout(new FlowLayout());
        getContentPane().add(spinner);
        getContentPane().add(button);
    }

    public void configureSettings() {
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }
}

class MyIntegerFormatterFactory extends JFormattedTextField.AbstractFormatterFactory {
    String suffix;
    int min;
    int max;

    public MyIntegerFormatterFactory(String suffix, int min, int max) {
        this.suffix = suffix;
        this.min = min;
        this.max = max;
    }

    @Override
    public JFormattedTextField.AbstractFormatter getFormatter(JFormattedTextField tf) {
        return new JFormattedTextField.AbstractFormatter() {
            @Override
            public Object stringToValue(String text) throws ParseException {
                text = (text.endsWith(suffix)) ? text.substring(0, text.length() - suffix.length()) : text;

                try {
                    int parse = Integer.parseInt(text);
                    if (parse < min || parse > max) throw new ParseException("", 0);
                    return parse;
                } catch (Exception e) {
                    throw new ParseException("", 0);
                }
            }

            @Override
            public String valueToString(Object value) {
                return value + suffix;
            }
        };
    }
}

Result:

enter image description here enter image description here

This JSpinner accepts both inputs like "10" and "6€", and any other invalid inputs are ignored. (For instance, if you enter "asdf" and click the JButton to switch the focus, the input gets ignored.)

Kcits970
  • 640
  • 1
  • 5
  • 23