在Java中确定一个字符串是否为数字并进行转换?

24

我知道类似的问题之前已经经常被问到了(请参见这里这里),但这不是那些问题的完全重复

我想检查一个String是否为数字,如果是,则将其存储为double。 有几种方法可以做到这一点,但它们似乎都不适用于我的目的。

一种解决方法是使用Double.parseDouble(s)或类似的new BigDecimal(s)。 但是,如果存在逗号(例如,“1,234”会导致异常),则这些解决方案无法工作。 当然,在使用这些技术之前删除所有逗号可能是一个解决方案,但是这似乎会在其他地区引起很多问题。

我看了Apache Commons NumberUtils.isNumber(s),但它也有相同的逗号问题。

我考虑过NumberFormatDecimalFormat,但这些似乎太宽容了。 例如,“1A”被格式化为“1”而不是指出它不是数字。此外,像“127.0.0.1”这样的内容将被视为数字127,而不是指出它不是数字。

我觉得我的要求并不那么奇特,以至于我不是第一个这样做的人,但是没有一种解决方案完全符合我的需求。 我想即使是我自己也不完全知道我需要什么(否则我可以编写自己的解析器),但我知道上述解决方案由于所指出的原因而无法工作。是否存在任何解决方案,或者我需要确定我需要什么并为其编写自己的代码?


2
代码真的需要提供国际化支持吗?区域设置的担忧可能是没有根据的。 - Thomas Langston
这看起来是重复的 https://dev59.com/D2855IYBdhLWcg3weUMQ 。你有在这里检查答案吗? - YoK
@YoK 是的,我已经在那里检查过了;在发起悬赏之前,maaron就已经提到了那个问题。那里的答案建议使用 NumberFormat,然而我明确指出这在这里不起作用的原因。 - Michael McGowan
如果字符串是字母数字混合的,而不仅仅是数字,您希望发生什么样的格式化?抛出一个非法参数异常吗? - A. R. Younce
3
我认为你在这里面临的问题是,你没有让人们指定他们来自哪里。如果你知道某些人使用 ',' 或 '.' 来定义小数,你就可以基于地区进行数字解析,一切都会很好。如果你坚持要一个不受地区限制的输入字段,然后将其转换为数据库期望的格式,你可能最终会自己验证和解析格式。 - Ben
显示剩余4条评论
15个回答

16

听起来很奇怪,但我会尝试遵循这个答案并使用java.util.Scanner

Scanner scanner = new Scanner(input);
if (scanner.hasNextInt())
    System.out.println(scanner.nextInt());
else if (scanner.hasNextDouble())
    System.out.println(scanner.nextDouble());
else
    System.out.println("Not a number");

对于输入的内容,例如1A127.0.0.11,2346.02e-23,我得到以下输出:

Not a number
Not a number
1234
6.02E-23

Scanner.useLocale 可以用于更改为所需的语言环境。


2
之前没见过这个扫描器...很不错。但是,如果你传递“1 A”,那么你现在的代码会失效,因为扫描器识别出多个标记。所以,你需要修改以上代码,在 hasNextInt 和 hasNextDouble 中设置一个数字'n',跳过最后一个else,然后再添加一个单独的if语句来检查扫描器是否有任何后续标记,如n!= null && scanner.hasNext()。这两种方法可以允许前导和尾随空格 - 因此将某些适当处理交给运算符即可。但是通过这些修复,我认为你已经得到了一个非常好的解决方案。 - philwb
这个答案如果提到 useLocale 以及 philwb 的补充会更好。否则,很棒。 - Konrad Rudolph
1
@KonradRudolph,Scanner.useLocale已经在答案的最后一行提到了。至于@philwb的建议,我认为它取决于程序接收到的输入可能有的限制。例如,如果允许字符串包含多个标记,并且使用空格来分隔它们,那么修剪、拆分,然后使用我提供的代码片段对生成的数组进行循环处理就可以了。OP没有提到任何这样的限制,只是提供了一些示例标记,所以我不想引导我的答案朝一个方向或另一个方向发展。 - Giulio Piancastelli
@Giulio 是的,我忽略了那个。 - Konrad Rudolph

4
您可以使用ParsePosition来检查NumberFormat.parse操作中字符串是否完全被消耗。如果字符串被消耗完,则不会出现"1A"的情况。如果未被消耗,则会出现相关情况,您可以相应地进行处理。请参见此处了解解决方案的简要概述,以及此处有关关闭相关JDK bug的解释,因为有ParsePosition选项。

虽然这很有趣,但似乎不能处理科学计数法。例如,“6.02e-23”在这种技术中不被识别为数字。 - Michael McGowan
1
显然,这是NumberFormat的限制,而不是特定的基于ParsePosition的技术的限制:"DecimalFormat只能通过模式来指示格式化和解析科学计数法; 当前没有工厂方法可以创建科学计数法格式。" - Giulio Piancastelli
没错!DecimalFormat是最好的选择。 - Rade_303

4

您可以指定所需的区域设置(Locale):

NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN);
double myNumber = nf.parse(myString).doubleValue();

在您的示例中,这应该可以工作,因为德语区域设置使用逗号作为小数分隔符。


1
无论地区如何,我已经明确指定NumberFormat过于宽松,因为它接受像“1A”和“127.0.0.1”这样的内容作为数字。 - Michael McGowan

3

不确定它是否符合您的要求,但在这里找到的代码可能会指引您正确的方向。

从文章中得知:

为了进行正确的输入处理,需要执行以下步骤:

  1. 获取适当的NumberFormat并定义ParsePosition变量。
  2. 将ParsePosition索引设置为零。
  3. 使用parse(String source, ParsePosition parsePosition)解析输入值。
  4. 如果输入长度和ParsePosition索引值不匹配或解析的Number为null,则执行错误操作。
  5. 否则,该值通过验证。

听起来很有前途,但您介意在回答中总结相关部分吗?这样,一个过时的链接就不会破坏它的实用性。 - Konrad Rudolph

3
这非常有趣,我认为人们试图把它过于复杂化了。我会按照以下规则进行解析:
1)检查科学计数法(是否与所有数字、逗号、句点、-/+且其中包含“e”的模式匹配?)——如果是,可以自行解析。
2)它是否匹配有效数字字符的正则表达式(0-9 , . - +)(只允许1个. - 或+)?如果是,则剥离不是数字的内容并适当地解析,否则失败。
我看不出有什么捷径可以在这里起作用,只能采用粗暴的方法,不是编程中的所有内容都必须完美无缺。

3

不幸的是,Double.parseDouble(s)或new BigDecimal(s)似乎是您最好的选择。

你提到了本地化问题,但不幸的是,没有一种方法可以在没有用户指定的情况下可靠地支持所有语言环境。这是不可能的。

有时,您可以根据逗号或句点先使用哪个来推断方案,如果两者都使用,则可能无法推断,那么为什么要尝试呢?最好拥有您知道在某些情况下可靠工作的系统,而不是依赖可能在更多情况下工作但也可能产生错误结果的系统......

数字“123,456”代表什么?123456还是123.456?

只需根据用户指定的语言环境删除逗号、空格或句点。默认删除空格和逗号。如果要使其更严格,只删除逗号或空格,而不是两者都删除,并且仅在有句点的情况下才在句点前删除。实际上,手动检查它们是否按三个间隔可以很容易地完成。事实上,自定义解析器可能在这里最容易。

这是一个概念验证。虽然有点(非常)混乱,但我认为它有效,而且您可以理解其中的思路 :)。

public class StrictNumberParser {
  public double parse(String numberString) throws NumberFormatException {
    numberString = numberString.trim();
    char[] numberChars = numberString.toCharArray();

    Character separator = null;
    int separatorCount = 0;
    boolean noMoreSeparators = false;
    for (int index = 1; index < numberChars.length; index++) {
      char character = numberChars[index];

      if (noMoreSeparators || separatorCount < 3) {
        if (character == '.') {
          if (separator != null) {
            throw new NumberFormatException();
          } else {
            noMoreSeparators = true;
          }
        } else if (separator == null && (character == ',' || character == ' ')) {
          if (noMoreSeparators) {
            throw new NumberFormatException();
          }
          separator = new Character(character);
          separatorCount = -1;
        } else if (!Character.isDigit(character)) {
          throw new NumberFormatException();
        }

        separatorCount++;
      } else {
        if (character == '.') {
          noMoreSeparators = true;
        } else if (separator == null) {
          if (Character.isDigit(character)) {
            noMoreSeparators = true;
          } else if (character == ',' || character == ' ') {
            separator = new Character(character);
          } else {
            throw new NumberFormatException();
          }
        } else if (!separator.equals(character)) {
          throw new NumberFormatException();
        }

        separatorCount = 0;
      }
    }

    if (separator != null) {
      if (!noMoreSeparators && separatorCount != 3) {
        throw new NumberFormatException();
      }
      numberString = numberString.replaceAll(separator.toString(), "");
    }

    return Double.parseDouble(numberString);
  }

  public void testParse(String testString) {
    try {
      System.out.println("result: " + parse(testString));
    } catch (NumberFormatException e) {
      System.out.println("Couldn't parse number!");
    }
  }

  public static void main(String[] args) {
    StrictNumberParser p = new StrictNumberParser();
    p.testParse("123 45.6");
    p.testParse("123 4567.8");
    p.testParse("123 4567");
    p.testParse("12 45");
    p.testParse("123 456 45");
    p.testParse("345.562,346");
    p.testParse("123 456,789");
    p.testParse("123,456,789");
    p.testParse("123 456 789.52");
    p.testParse("23,456,789");
    p.testParse("3,456,789");
    p.testParse("123 456.12");
    p.testParse("1234567.8");
  }
}

编辑:显然,这需要扩展以识别科学计数法,但这应该很简单,特别是因为你不必在e之后实际验证任何内容,如果它格式不正确,你只需让parseDouble失败即可。

此外,将NumberFormat适当地扩展也可能是一个好主意。有一个getSeparator()用于解析数字和一个setSeparator用于提供所需的输出格式... 这样可以处理本地化,但要支持','作为小数点还需要做更多的工作...


3

这是一个有趣的问题。但也许有点不够明确?您是想特别识别十进制数,还是十六进制或其他什么?我假设是十进制。货币方面呢?那很重要吗?还是只是数字。

无论如何,我认为您可以利用Number格式的缺陷来达到目的。既然您知道类似于“1A”的内容将被解释为1,为什么不通过格式化并将其与原始字符串进行比较来检查结果呢?

public static boolean isNumber(String s){
    try{
        Locale l = Locale.getDefault();
        DecimalFormat df = new DecimalFormat("###.##;-##.##");
        Number n = df.parse(s);
        String sb = df.format(n);
        return sb.equals(s);
    }
    catch(Exception e){
        return false;
    }
} 

你认为怎么样?


1
不幸的是,在北美,这通常会导致 (1,024 != 1024),这是一个错误的负面结果。 - Falkreon

3
我的理解是您想覆盖西方/拉丁语言,同时尽可能保留严格的解释。因此,在这里我通过请求DecimalFormatSymbols告诉我组分隔符、小数点分隔符、负号和零的分隔符是什么,并将它们替换为Double将识别的符号。
它的表现如何?
在美国,它拒绝:"1A","127.100.100.100"并接受"1.47E-9"。
在德国,它仍然拒绝"1A"。
它接受"1,024.00"但将其正确解释为1.024。同样,它接受"127.100.100.100"作为127100100100.0。
事实上,德语环境正确地识别和解析了"1,47E-9"。
如果您在不同的环境中遇到任何问题,请告诉我。
import java.util.Locale;
import java.text.DecimalFormatSymbols;

public class StrictNumberFormat {

public static boolean isDouble(String s, Locale l) {
    String clean = convertLocaleCharacters(s,l);

    try {
        Double.valueOf(clean);
        return true;
    } catch (NumberFormatException nfe) {
        return false;
    }
}

public static double doubleValue(String s, Locale l) {
    return Double.valueOf(convertLocaleCharacters(s,l));
}

public static boolean isDouble(String s) {
    return isDouble(s,Locale.getDefault());
}

public static double doubleValue(String s) {
    return doubleValue(s,Locale.getDefault());
}

private static String convertLocaleCharacters(String number, Locale l) {
    DecimalFormatSymbols symbols = new DecimalFormatSymbols(l);
    String grouping = getUnicodeRepresentation( symbols.getGroupingSeparator() );
    String decimal = getUnicodeRepresentation( symbols.getDecimalSeparator() );
    String negative = getUnicodeRepresentation( symbols.getMinusSign() );
    String zero = getUnicodeRepresentation( symbols.getZeroDigit() );

    String clean = number.replaceAll(grouping, "");
    clean = clean.replaceAll(decimal, ".");
    clean = clean.replaceAll(negative, "-");
    clean = clean.replaceAll(zero, "0");

    return clean;
}

private static String getUnicodeRepresentation(char ch) {
    String unicodeString = Integer.toHexString(ch); //ch implicitly promoted to int
    while(unicodeString.length()<4) unicodeString = "0"+unicodeString;

    return "\\u"+unicodeString;
}

}

3

最好手动进行操作。确定您可以接受的数字,并忽略其他所有内容:

   import java.lang.NumberFormatException;
   import java.util.regex.Pattern;
   import java.util.regex.Matcher;

   public class ParseDouble {
   public static void main(String[] argv) {

       String line = "$$$|%|#|1A|127.0.0.1|1,344|95|99.64";

       for (String s : line.split("\\|")) {
           try {
               System.out.println("parsed: " + 
               any2double(s)
                       );

           }catch (NumberFormatException ne) {
               System.out.println(ne.getMessage());
           }
       }   
   }
   public static double any2double(String input) throws NumberFormatException {

       double out =0d;

       Pattern special         = Pattern.compile("[^a-zA-Z0-9\\.,]+");
       Pattern letters         = Pattern.compile("[a-zA-Z]+");
       Pattern comma           = Pattern.compile(",");
       Pattern allDigits       = Pattern.compile("^[0-9]+$");
       Pattern singleDouble    = Pattern.compile("^[0-9]+\\.[0-9]+$");

       Matcher[] goodCases = new Matcher[]{
           allDigits.matcher(input),
           singleDouble.matcher(input)
       };           

       Matcher[] nanCases = new Matcher[]{
           special.matcher(input),
           letters.matcher(input)
       };


       // maybe cases 
       if (comma.matcher(input).find()){
           out = Double.parseDouble( 
               comma.matcher(input).replaceFirst("."));
           return out;

       }

       for (Matcher m : nanCases) {
           if (m.find()) {
               throw new NumberFormatException("Bad input "+input);
           }
       }

       for (Matcher m : goodCases) {

           if (m.find()) {
               try {
                   out = Double.parseDouble(input);
                   return out;
               } catch (NumberFormatException ne){
                   System.out.println(ne.getMessage());
               }
           }
       }
       throw new NumberFormatException("Could not parse "+input);
   }
   }

这个答案与Double.valueOf(input)完全相同,只是去掉了逗号。问题在于它不考虑不同的分组分隔符或减号,并且如果你想要开始,你必须彻底重写正则表达式 - 这是一个我不希望我的敌人做的任务。 - Falkreon
1
line.split("\\|") 这让我很不舒服。在我看来,line.split(Pattern.quote("|")) 是更好的解决方案。 - Garbage

1
如果您设置了正确的区域设置,内置的parseDouble将能够处理逗号。示例在这里

1
你的链接并不是指 parseDouble,而是指 NumberFormat,正如 OP 所述,它不能正常工作。 - Konrad Rudolph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接