← 開発ノート

Java の新しい ID 推薦:正規化の順序と切り詰め

ID の正規化は変換順序に依存します。特に 15 文字に切り詰めた後、末尾のドットをもう一度削除します。元の末尾のドットを先に削除しても、切り詰めによって別のドットが末尾に現れることは防げません。

問題の規則では、ASCII 文字の大小を正規化し、許可された文字を残してから、ドット・空の結果・長さを順番に処理します。両解法とも、問題が保証する非 null の入力を前提とします。Optional 版は Java 11 の repeat() を使い、両方とも Java 11 向けにコンパイルできます。どちらかを Solution.java として保存し、二つのクラスを同じファイルに入れないでください。

処理の流れ

図は Optional 版の流れです。ループ版はフィルターと連続するドットの圧縮をまとめ、同じ規則を保ちます。

図は Optional 版の流れです。ループ版はフィルターと連続するドットの圧縮をまとめ、同じ規則を保ちます。

Optional で手順をつなぐ

import java.util.Locale;
import java.util.Optional;

public class Solution {
    public String solution(String new_id) {
        return Optional.of(new_id)
                .map(s -> s.toLowerCase(Locale.ROOT))
                .map(s -> s.replaceAll("[^a-z0-9._-]", ""))
                .map(s -> s.replaceAll("[.]+", "."))
                .map(s -> s.replaceAll("^[.]|[.]$", ""))
                .map(s -> s.isEmpty() ? "a" : s)
                .map(s -> s.length() > 15 ? s.substring(0, 15) : s)
                .map(s -> s.replaceAll("[.]$", ""))
                .map(s -> s.length() < 3
                        ? s + s.substring(s.length() - 1).repeat(3 - s.length()) : s)
                .orElseThrow();
    }
}

Locale.ROOT により、小文字化が既定のロケールに依存しなくなります。許可文字のパターンは、小文字の ASCII 文字、数字、ドット、アンダースコア、ハイフンだけを残します。連続するドットをまとめ、両端のドットを削除します。空なら a に置換し、15 文字に切り詰め、新たに末尾に現れたドットを削除してから、長さが 3 になるまで最後の文字を繰り返します。

Optional は変換をつなぐ手段であり、必須のデータ構造ではありません。Optional.of は null を空の ID として黙って扱わず、拒否します。

ループで同じ規則を適用する

この解法は、許可文字のフィルターと連続するドットの削除を一度の走査にまとめます。ASCII の大文字だけを変換するのは問題の入力範囲に基づくもので、一般的な Unicode 名の正規化ではありません。

import java.util.Objects;

public class Solution {
    public String solution(String new_id) {
        Objects.requireNonNull(new_id);
        StringBuilder result = new StringBuilder();
        for (char input : new_id.toCharArray()) {
            char c = input >= 'A' && input <= 'Z' ? (char) (input + 32) : input;
            boolean allowed = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
                    || c == '-' || c == '_' || c == '.';
            if (!allowed || (c == '.' && (result.length() == 0
                    || result.charAt(result.length() - 1) == '.'))) continue;
            result.append(c);
        }
        if (result.length() > 0 && result.charAt(result.length() - 1) == '.') {
            result.setLength(result.length() - 1);
        }
        if (result.length() == 0) result.append('a');
        if (result.length() > 15) result.setLength(15);
        if (result.charAt(result.length() - 1) == '.') result.setLength(result.length() - 1);
        while (result.length() < 3) result.append(result.charAt(result.length() - 1));
        return result.toString();
    }
}
入力 結果 確認する規則
=.= aaa 空の結果の代替と補完
z-+.^. z-- 許可文字と末尾のドットの削除
I iii 既定のロケールに依存しない小文字化
abcdefghijklmn.op abcdefghijklmn 切り詰めで末尾に現れたドットの削除

どちらも入力長 n に対して時間計算量 O(n)、追加領域 O(n) です。正規表現版は複数の中間文字列を割り当てることがあり、ループ版は StringBuilder を使います。測定なしに一方が常に速いとはいえません。置換、小文字化、繰り返しの動作は String API を参照してください。

数字名のリテラル置換は replace と replaceAllを、ロケールに依存しないキャッシュキーは LRU キャッシュの例を参照してください。

コメント

GitHubで見る ↗