できない.dev

文字列が含まれるか調べるには

ある文字列の中に別の文字列が含まれるかを判定する基本形を各言語で示す。
大文字小文字を区別するかどうかの既定値が言語ごとに違う点と、見つからなかったときの返り値の形までを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

text = "config.local.yaml"
 
print("local" in text)                       # -> True(部分文字列の判定は in 演算子)
print("Local" in text)                       # -> False(既定で大文字小文字を区別する)
print("local" in text.casefold())            # 区別したくないときは畳んでから比較する
print(text.find("yaml"), text.find("json"))  # 位置が要るとき -> 13 -1
print(text.startswith("config"), text.endswith(".yaml"))

部分文字列が含まれるかどうかは in 演算子で判定する。
比較は既定で大文字小文字を区別するため、区別したくないときは casefold で畳んでから比較する。
位置が必要なら find を使うが、見つからない場合は例外ではなく -1 が返る点に注意する。

うまくいかない時: Python で「'NoneType' object has no attribute」が解消できない

JavaScript 実行確認済み

const text = "config.local.yaml";
 
console.log(text.includes("local"));   // true(ES2015 以降はこれが基本形)
console.log(text.includes("Local"));   // false(既定で大文字小文字を区別する)
console.log(text.toLowerCase().includes("local"));
console.log(text.indexOf("yaml"), text.indexOf("json")); // 位置が要るとき 13 -1
 
try {
  text.includes(/local/); // 正規表現は渡せない
} catch (e) {
  console.log(e.constructor.name); // TypeError
}

String.prototype.includes が基本形で、真偽値をそのまま得られる。
引数に正規表現を渡すと TypeError になるため、パターンで探したいときは includes ではなく test や match を使う。
古い実行環境に合わせる必要があるときだけ indexOf(...) !== -1 を使う。

TypeScript 実行確認済み

const text: string = "config.local.yaml";
const hit: boolean = text.includes("local");
console.log(hit); // true
 
// 許可リストとの照合。as const で固定した配列は includes の引数まで狭まるため、
// string を渡すと TS2345 になる。readonly string[] に広げてから呼ぶ。
const ALLOWED = ["yaml", "json", "toml"] as const;
const ext: string = text.split(".").pop() ?? "";
console.log((ALLOWED as readonly string[]).includes(ext)); // true

実行時の挙動は JavaScript と同じで、戻り値が boolean として型付けされるだけである。
as const で固定した配列に対して includes を呼ぶと引数の型がリテラルの合併に狭まり、string を渡した時点で TS2345 になるため、判定に使う配列は readonly string[] に広げてから呼ぶ。

うまくいかない時: TypeScript の as const で型が絞り込めないTypeScript で「Argument of type ... is not assignable」が解消できない

Go 実行確認済み

package main
 
import (
	"fmt"
	"strings"
)
 
func main() {
	text := "config.local.yaml"
 
	fmt.Println(strings.Contains(text, "local"))    // true
	fmt.Println(strings.Contains(text, "Local"))    // false(既定で大文字小文字を区別する)
	fmt.Println(strings.ContainsAny(text, "Local")) // true(部分文字列ではなく「文字の集合」)
	fmt.Println(strings.Index(text, "yaml"), strings.Index(text, "json")) // 13 -1
	fmt.Println(strings.EqualFold("YAML", "yaml")) // 大文字小文字を無視した一致
}

strings.Contains が部分文字列の判定で、既定では大文字小文字を区別する。
名前の似た ContainsAny は「第2引数の文字のいずれかが含まれるか」を見る別物なので、部分文字列の判定に使うと意図せず true になる。
大文字小文字を無視した一致だけが要るなら EqualFold を使う。

Rust 静的確認

fn main() {
    let text = "config.local.yaml";
 
    println!("{}", text.contains("local")); // true
    println!("{}", text.contains("Local")); // false(既定で大文字小文字を区別する)
    println!("{}", text.to_lowercase().contains("local"));
 
    // contains は Pattern を受け取るので char やクロージャも渡せる
    println!("{}", text.contains(char::is_numeric)); // false
    println!("{:?}", text.find("yaml")); // Some(13)、無ければ None
}

str::contains は Pattern を受け取るため、&str だけでなく char や「char を判定するクロージャ」も渡せる。
大文字小文字は既定で区別するので、無視したいときは to_lowercase で新しい String を作ってから判定する。
位置が必要なときの find は -1 ではなく Option<usize> を返す。

Java 実行確認済み

public class Main {
    public static void main(String[] args) {
        String text = "config.local.yaml";
 
        System.out.println(text.contains("local")); // true
        System.out.println(text.contains("Local")); // false(既定で大文字小文字を区別する)
 
        // String に containsIgnoreCase は無いので、片方を畳んでから判定する
        System.out.println(text.toLowerCase().contains("local".toLowerCase()));
        System.out.println(text.indexOf("yaml") + " " + text.indexOf("json")); // 13 -1
        System.out.println(text.regionMatches(true, 13, "YAML", 0, 4)); // 位置が決まっているとき
    }
}

String.contains が部分文字列の判定で、既定では大文字小文字を区別する。
標準の String には containsIgnoreCase が無く、それは Apache Commons Lang の StringUtils にあるメソッドなので、依存を足したくなければ片方を toLowerCase で畳んで比較する。
位置が決まっているなら regionMatches の第1引数に true を渡して部分一致を無視比較できる。

C# 実行確認済み

using System;
 
class Program {
    static void Main() {
        var text = "config.local.yaml";
 
        Console.WriteLine(text.Contains("local")); // True
        Console.WriteLine(text.Contains("Local")); // False(既定は序数比較で大文字小文字を区別する)
        Console.WriteLine(text.Contains("Local", StringComparison.OrdinalIgnoreCase)); // True
        Console.WriteLine($"{text.IndexOf("yaml", StringComparison.Ordinal)} {text.IndexOf("json", StringComparison.Ordinal)}"); // 13 -1
    }
}

string.Contains(string) は序数比較で、既定では大文字小文字を区別する。
無視したいときは第2引数に StringComparison.OrdinalIgnoreCase を渡す。
IndexOf(string) は既定がカルチャ依存の比較になるため、実行環境のロケールで結果が変わらないよう StringComparison.Ordinal を明示するのが安全である。

つまずき: 大文字小文字の既定はそろっていない

ここで挙げたどの言語も、既定の判定は大文字小文字を区別する。
区別しない判定が要る場合の書き方は言語ごとに分かれ、C# は Contains の第2引数に StringComparison.OrdinalIgnoreCase を渡せるが、Java の String には同等の引数が無く片方を畳む必要がある。
ユーザー入力や環境変数のように表記が揺れる値を照合するときは、どちらの挙動を求めているのかを先に決めてから書く。

つまずき: 部分文字列と「文字の集合」は別物

Go の strings.ContainsAny は名前が Contains に似ているが、第2引数を「文字の集合」として扱い、そのいずれか1文字でも含まれれば true を返す。
上の例で ContainsAny(text, "Local") が true になるのは、text に l や o や c が含まれているからであって "Local" という並びがあるからではない。
部分文字列を探しているつもりで集合系の API を選ぶと、常に true を返す判定ができあがる。

つまずき: 見つからなかったときの返り値

含まれるかどうかだけが要るなら真偽値を返す API を使えばよいが、位置まで要るときは戻り値の形が言語ごとに変わる。
Python の find・JavaScript の indexOf・Go の strings.Index・Java の indexOf はいずれも -1 を返し、Rust の find は None を返す。
-1 を返す言語で戻り値をそのまま真偽値として扱うと、先頭一致の 0 が偽になったり -1 が真になったりするため、必ず -1 と比較する。

この記事は役立ちましたか?