Search code examples
javabigdecimal

BigDecimal hasNextBigDecimal behavior


The following code does exactly what is expected:

import java.math.BigDecimal;
import java.util.Scanner;

public class Test2 {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        BigDecimal number = new BigDecimal("0");
        System.out.print("Enter a number: ");
        try {
            number = new BigDecimal(input.next());
        }
        catch(Exception e) {
            System.out.println("Not a number.");
        }
        System.out.println(number);
    }
}
  • Correct: It fails if I type a string.
  • Correct: It fails if I type 1,1.
  • Correct: It succeeds if I type 1.
  • Correct: It succeeds if I type 1.1

If I remove the try catch block and replace it with a while loop, as follows, it does not do what is expected:

import java.math.BigDecimal;
import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("Enter a number: ");
        while (!input.hasNextBigDecimal()) {
            System.out.print("Not a number, try again: ");
            input.next();
        }
        BigDecimal number = input.nextBigDecimal();
        System.out.println(number);
    }
}
  • Correct: It fails if I type a string.
  • Not correct: It fails if I type 1.1.
  • Correct: It succeeds if I type 1.
  • Not correct: It succeeds if I type 1,1

Here is the output when I run: java Test

C:\>java Test
Enter a number: 1,1
1.1

Why would it accept a comma, but print a period?

If I run the following code, it prints out: en_ZA

import java.util.Locale;

public class Test3 {
    public static void main(String[] args) {
        System.out.println(Locale.getDefault());
    }
}

As you can see at the following link, the decimal separator for my locale is the period:

http://www.localeplanet.com/java/en-ZA/index.html

Please advise where I am going wrong.

Edit:

On further investigation, I found the following: https://www.sadev.co.za/content/how-correctly-format-currency-south-africa

So the locale technically does use the comma. Mind blown :)


Solution

  • Scanner.hasNextBigDecimal() uses default locale's grouping and decimal separator (it seems '.' for grouping and ',' for decimal in your locale) to validate if next token matches decimalpattern:

    ...
    DecimalFormat df =
        (DecimalFormat)NumberFormat.getNumberInstance(locale);
    DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale);
    
    // These must be literalized to avoid collision with regex
    // metacharacters such as dot or parenthesis
    groupSeparator =   "\\" + dfs.getGroupingSeparator();
    decimalSeparator = "\\" + dfs.getDecimalSeparator();
    ...
    

    this piece of code is from Scanner class and executed when initializing your scanner instance. then groupSeparator and decimalSeparator are used when call to Scanner.hasNextBigDecimal() for checking if the next token matches a decimal format.


    new BigDecimal(String) uses directly '.' as the decimal separator.


    So, if you want to use Scanner.hasNextBigDecimal() and '.' as decimal separator for user inputs then you should use Scanners useLocale(Locale) method. Your code shold look like:

    Scanner input = new Scanner(System.in);
    input.useLocale(Locale.ENGLISH);
    System.out.print("Enter a number: ");
    while (!input.hasNextBigDecimal()) {
        System.out.print("Not a number, try again: ");
        input.next();
    }
    BigDecimal number = input.nextBigDecimal();
    System.out.println(number);
    

    the program accepts a comma as valid input, but then prints a period as the decimal point.

    Again in Scanner's nextBigDecimal() method replaces all groupSeparator's with "" and decimalSeparator with "." before calling 'new BigDecimal(String)'. The piece of code from Scanner class is:

    private String processFloatToken(String token) {
        String result = token.replaceAll(groupSeparator, "");
        if (!decimalSeparator.equals("\\."))
            result = result.replaceAll(decimalSeparator, ".");
    

    This is why it is accepting , and printing ..