Java Tips | 文字列処理:数値形式チェック

Java Java
スポンサーリンク

数値形式チェックは「パースする前に落とすためのフィルタ」

業務システムでは、画面から入力された文字列を
「整数として扱いたい」「金額として扱いたい」「小数として扱いたい」
という場面が山ほどあります。

ここで何もチェックせずに Integer.parseIntBigDecimal に渡すと、
abc123a が混ざっていた瞬間に NumberFormatException で落ちます。

数値形式チェックの役割は、まさにここです。
「この文字列は“数値としてパースしてよさそうか”を事前に判定し、
おかしなものは早めにはじく」ためのフィルタだと思ってください。


まずは「どんな数値を許したいか」を決める

整数だけか、小数もありか、マイナスは許すか

数値形式チェックを考えるとき、いきなり正規表現を書き始めるのは危険です。
先に決めるべきなのは、「どんな数値を許したいか」です。

例えば、こんなパターンがあります。

「年齢」「個数」「在庫数」など
→ 0以上の整数だけ(小数・マイナスはNG)

「金額」「売上」「単価」など
→ 小数あり(ただし桁数制限あり)、マイナスは基本NG

「温度」「差分」「増減」など
→ 小数あり、マイナスもOK

用途ごとに“許したい数値の範囲・形”が違うので、
「とりあえず全部 double にパースできればOK」
という発想は、実務ではかなり危険です。

ここではまず、
「整数チェック」と「小数チェック」に分けて考えていきます。


整数形式チェックの基本

Java標準のパースをそのまま使うシンプル版

一番素直なやり方は、
「実際に Integer.parseInt を呼んでみて、例外が出るかどうかで判定する」方法です。

public final class NumberValidator {

    private NumberValidator() {}

    public static boolean isInteger(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        try {
            Integer.parseInt(text);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}
Java

使い方はこうなります。

System.out.println(NumberValidator.isInteger("123"));     // true
System.out.println(NumberValidator.isInteger("-5"));      // true
System.out.println(NumberValidator.isInteger("0"));       // true

System.out.println(NumberValidator.isInteger(""));        // false
System.out.println(NumberValidator.isInteger(" 123 "));   // false(空白がある)
System.out.println(NumberValidator.isInteger("12.3"));    // false
System.out.println(NumberValidator.isInteger("123a"));    // false
Java

ここでの重要ポイントは二つです。

一つ目は、「“形式チェック”と“実際のパース”を同じロジックに任せている」ことです。
Integer.parseInt が受け付ける形式=「整数として扱える形式」なので、
それをそのまま使うのは理にかなっています。

二つ目は、「空白を含む文字列はこのままだとNGになる」ことです。
" 123 " のような入力を許したい場合は、
事前に trim() するかどうかを、ポリシーとして決める必要があります。


整数形式チェックを「0以上」に絞る

年齢・個数・在庫数などの“負の値を許さない”ケース

多くの業務項目は、「マイナスの値を許さない」ものが多いです。
年齢、個数、在庫数、ページ番号など。

その場合は、「整数かどうか」に加えて「0以上かどうか」も見ます。

public final class NumberValidator {

    private NumberValidator() {}

    public static boolean isNonNegativeInteger(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        try {
            int value = Integer.parseInt(text);
            return value >= 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}
Java

使い方はこうです。

System.out.println(NumberValidator.isNonNegativeInteger("10"));   // true
System.out.println(NumberValidator.isNonNegativeInteger("0"));    // true

System.out.println(NumberValidator.isNonNegativeInteger("-1"));   // false
System.out.println(NumberValidator.isNonNegativeInteger("abc"));  // false
Java

ここでのポイントは、「“形式”と“値の範囲”をセットで見る」ことです。
形式だけ見て -1 を通してしまうと、
「年齢に -1 が入っている」といったおかしなデータがDBに残ります。

数値形式チェックは、“パースできるか”だけでなく、“その用途として妥当な範囲か”まで含めて考える
という感覚を持っておくと、実務レベルのバリデーションに近づきます。


小数形式チェックの基本(BigDecimalベース)

金額やレートなど「精度が大事な数値」の扱い

金額やレートなど、精度が大事な数値は、
double ではなく BigDecimal で扱うのが定番です。

形式チェックも、BigDecimal のパースに任せるのが素直です。

import java.math.BigDecimal;

public final class DecimalValidator {

    private DecimalValidator() {}

    public static boolean isDecimal(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        try {
            new BigDecimal(text);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}
Java

使い方はこうなります。

System.out.println(DecimalValidator.isDecimal("123"));       // true
System.out.println(DecimalValidator.isDecimal("123.45"));    // true
System.out.println(DecimalValidator.isDecimal("-0.5"));      // true

System.out.println(DecimalValidator.isDecimal("123."));      // false(BigDecimalはNG)
System.out.println(DecimalValidator.isDecimal(".5"));        // false
System.out.println(DecimalValidator.isDecimal("1,234"));     // false(カンマ付きはNG)
System.out.println(DecimalValidator.isDecimal("abc"));       // false
Java

ここで深掘りしたいポイントは三つです。

一つ目は、「BigDecimal が許す形式=“実務で扱いやすい数値文字列”に近い」ことです。
123.45-0.5 はOKですが、123..5 のような曖昧な表現はNGになります。
これにより、「人間が普通に書く数値」に自然と寄せられます。

二つ目は、「カンマ付き(1,234)はこのままだとNGになる」ことです。
カンマを許したい場合は、事前にカンマを除去する“正規化”処理を挟む必要があります。

三つ目は、「小数の桁数制限は別途見る必要がある」ことです。
例えば「金額は小数第2位まで」といった制約は、
BigDecimal にパースしたあとでスケール(小数桁数)を見て判定します。


小数の桁数を制限する(例:小数第2位まで)

金額や税率などでよくある要件

「金額は小数第2位まで」「税率は小数第3位まで」など、
小数の桁数に制限があるケースはよくあります。

BigDecimal にパースしたあとで、scale() を使って判定できます。

import java.math.BigDecimal;

public final class DecimalValidator {

    private DecimalValidator() {}

    public static boolean isDecimalWithScale(String text, int maxScale) {
        if (text == null || text.isBlank()) {
            return false;
        }
        try {
            BigDecimal bd = new BigDecimal(text);
            return bd.scale() <= maxScale;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}
Java

使い方はこうです。

// 小数第2位までOK
System.out.println(DecimalValidator.isDecimalWithScale("123", 2));      // true(scale=0)
System.out.println(DecimalValidator.isDecimalWithScale("123.4", 2));    // true(scale=1)
System.out.println(DecimalValidator.isDecimalWithScale("123.45", 2));   // true(scale=2)
System.out.println(DecimalValidator.isDecimalWithScale("123.456", 2));  // false(scale=3)
Java

ここでのポイントは、「“形式チェック”と“桁数チェック”を分けて考えつつ、同じメソッドにまとめている」ことです。
new BigDecimal(text) が形式チェック、
bd.scale() <= maxScale が桁数チェック、
という二段階を一つのメソッドに閉じ込めています。


正規表現で「整数っぽいか」を軽く見る方法

例外を使いたくない場面向けの“軽量版”

「例外を使った判定は重いから避けたい」という場面では、
正規表現で「整数っぽいか」を軽く見る方法もあります。

例えば、「マイナス記号付きの整数」を許すなら、こう書けます。

import java.util.regex.Pattern;

public final class NumberValidatorRegex {

    private NumberValidatorRegex() {}

    private static final Pattern INTEGER_PATTERN =
            Pattern.compile("^-?\\d+$");

    public static boolean isIntegerByRegex(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        return INTEGER_PATTERN.matcher(text).matches();
    }
}
Java

使い方はこうです。

System.out.println(NumberValidatorRegex.isIntegerByRegex("123"));   // true
System.out.println(NumberValidatorRegex.isIntegerByRegex("-5"));    // true

System.out.println(NumberValidatorRegex.isIntegerByRegex("12.3"));  // false
System.out.println(NumberValidatorRegex.isIntegerByRegex("1 2 3")); // false
System.out.println(NumberValidatorRegex.isIntegerByRegex("abc"));   // false
Java

ここでの重要ポイントは、「正規表現は“形式”しか見ていない」ことです。
-99999999999999999999 のような、int に収まらない値も true になります。
実際に int として扱えるかどうかは、
結局 Integer.parseInt などで別途チェックする必要があります。

つまり、「正規表現だけで完結させよう」とは思わないこと。
形式チェック+実際のパース、という二段構えを意識しておくのが安全です。


例題:入力フォームでの数値形式チェック

例えば、「年齢」を入力してもらうフォームを考えます。

サーバ側では、こんな感じで使えます。

String ageText = request.getParameter("age");

if (ageText == null || ageText.isBlank()) {
    errors.add("年齢を入力してください。");
} else if (!NumberValidator.isNonNegativeInteger(ageText.trim())) {
    errors.add("年齢は0以上の整数で入力してください。");
} else {
    int age = Integer.parseInt(ageText.trim());
    if (age > 120) {
        errors.add("年齢が不正です。");
    }
}
Java

ここでのポイントは三つです。

一つ目は、「未入力チェック」「形式チェック」「値の範囲チェック」を段階的に行っている」ことです。
どこで何を見ているかが明確なので、後から読んでも理解しやすくなります。

二つ目は、「形式チェックに通ったあとで、安心して parseInt している」ことです。
isNonNegativeInteger が true なら、NumberFormatException は起きない前提で書けます。

三つ目は、「ビジネスルール(120歳まで)を、形式チェックとは別に書いている」ことです。
形式チェックはあくまで「数値として成立しているか」、
ビジネスルールは「そのシステムとして妥当な値か」、
という役割分担を意識しておくと、設計がきれいになります。


まとめ:数値形式チェックユーティリティで身につけたい感覚

数値形式チェックは、「パースできるかどうか」だけを見るのではなく、
「その用途として妥当な形・範囲かどうか」をセットで考えるフィルタ です。

整数なら
isInteger(形式)
isNonNegativeInteger(0以上)

小数なら
isDecimal(形式)
isDecimalWithScale(小数桁数制限)

といったメソッドを用意して、
「用途ごとにどれを使うか」を決めていくイメージを持ってください。

もしあなたのコードのどこかに、

int age = Integer.parseInt(request.getParameter("age"));
Java

のような行がそのまま書かれていたら、
そこを題材にして、ここで作った NumberValidator.isNonNegativeInteger を一度挟んでみてください。

それだけで、「おかしな文字列が原因で落ちる」「あり得ない値がDBに残る」といった事故を、かなり減らすことができます。

タイトルとURLをコピーしました