JavaScript Tips | 文字列操作:文字列が指定文字列で終わるか判定する

JavaScript JavaScript
スポンサーリンク
スポンサーリンク
  1. 業務・実務で使える JavaScript ユーティリティ:文字列が指定文字列で終わるか判定する方法
  2. 基本:末尾一致を判定する endsWith()
    1. 含まれているかではなく、末尾が一致するかを調べる
    2. includes()・startsWith()・完全一致との違い
  3. 実装前に決める仕様
  4. ステップ1:対象文字列と末尾文字列を分ける
  5. ステップ2:大文字小文字を区別するか決める
    1. 小文字化を万能な比較方法と考えない
  6. ステップ3:末尾の空白や改行をどう扱うか決める
    1. 前処理は対象文字列と検索条件を分けて考える
  7. ステップ4:空文字の扱いを決める
  8. ステップ5:文字列以外の入力に備える
    1. null や undefined に直接メソッドを呼ばない
    2. 自動で文字列化するか、型を検証するか
  9. 業務で使える完成版テンプレート
    1. 入力値と設定ミスを分けて扱う理由
  10. 完成版の処理を具体例で確認する
  11. 実務での活用例
    1. 複数の拡張子を候補として判定する
    2. 商品コードの接尾辞を判定する
    3. 候補の一覧から対象ファイル名を抽出する
  12. 補足:第2引数で比較対象の終了位置を指定する
  13. 重要:末尾一致だけでは安全性を判断できない
    1. ファイル名が一致しても、ファイルの内容は保証されない
    2. URLやドメインは文字列全体の末尾だけで判定しない
    3. 任意の末尾文字列を正規表現へ変換する必要はない
  14. テストで境界条件を確認する
  15. まとめ

業務・実務で使える JavaScript ユーティリティ:文字列が指定文字列で終わるか判定する方法

JavaScriptで文字列が指定した文字列で終わるかを判定するには、endsWith() を使います。たとえば、ファイル名が ".csv" で終わるか、商品コードが "-SALE" で終わるか、メッセージの末尾に特定の記号があるかを確認できます。

基本的な書き方は簡単ですが、実務では大文字小文字、末尾の空白や改行、空文字、文字列以外の入力などに注意が必要です。また、ファイル名の末尾が一致しただけで、ファイルの内容まで正しいと判断することはできません。

この記事では、まず基本動作を確認し、その後に入力の扱い方を段階的に整理します。最後には、業務で流用できる関数とテスト例を紹介します。

基本:末尾一致を判定する endsWith()

含まれているかではなく、末尾が一致するかを調べる

endsWith() は、対象の文字列が指定した文字列で終わっていれば true、そうでなければ false を返します。大文字小文字は区別されます。

// ファイル名が ".csv" で終わるかを確認する
const fileName = "sales.csv";

console.log(fileName.endsWith(".csv")); // true
console.log(fileName.endsWith(".pdf")); // false

// ".csv" が途中にあるだけでは末尾一致にならない
console.log("sales.csv.bak".endsWith(".csv")); // false
JavaScript

重要なのは、文字列の途中に検索対象があっても一致しないことです。"sales.csv.bak" は ".csv" を含みますが、末尾は ".bak" なので、endsWith(".csv") の結果は false になります。

includes()・startsWith()・完全一致との違い

比較方法は、確認したい条件に合わせて選びます。「どこかに含まれる」「先頭にある」「末尾にある」「全体が等しい」は、それぞれ別の条件です。

// 同じ文字列でも、比較する条件によって結果が変わる
const fileName = "report.csv.bak";

// 途中に含まれていれば true
console.log(fileName.includes(".csv")); // true

// 先頭から一致していれば true
console.log(fileName.startsWith("report")); // true

// 末尾から一致していれば true
console.log(fileName.endsWith(".csv")); // false

// 文字列全体が同じ場合だけ true
console.log(fileName === "report.csv"); // false
JavaScript

「CSVという文字があるか」ではなく「ファイル名が ".csv" で終わるか」を調べたいなら、includes() ではなく endsWith() が適しています。ただし、これはあくまでファイル名の文字列判定です。

実装前に決める仕様

実務向けの関数では、コードを書く前に入力と出力のルールを決めます。特に、次の項目を明確にすると、呼び出し側が結果を予測しやすくなります。

  • 大文字小文字を区別するか。
  • 末尾の空白や改行を無視するか。
  • 指定する末尾文字列が空の場合に、どう扱うか。
  • null や undefined を受け取った場合に、どう扱うか。
  • 数値やオブジェクトを文字列に変換するか、文字列以外は受け付けないか。
  • 末尾一致だけで要件を満たすか、追加の形式チェックが必要か。

この記事の完成版では、文字列だけを受け取り、文字列以外と空の末尾文字列には false を返す方針にします。大文字小文字の無視と末尾空白の除去は、必要なときだけ有効にできるようにします。

ステップ1:対象文字列と末尾文字列を分ける

まずは「何を調べるのか」と「何で終わっている必要があるのか」を分けます。変数名を text と suffix にすると、それぞれの役割が伝わります。suffix は接尾辞、つまり末尾に付く文字列を意味します。

// 判定する対象
const text = "invoice_001.csv";

// 期待する末尾文字列
const suffix = ".csv";

// 末尾一致の結果を真偽値として受け取る
const matches = text.endsWith(suffix);

console.log(matches); // true
JavaScript

返り値は真偽値なので、そのまま if 文の条件に使えます。matches === true と書かなくても、if (matches) で同じ条件を表せます。

// ファイル名の末尾によって表示を分ける
const fileName = "invoice_001.csv";

if (fileName.endsWith(".csv")) {
  console.log("ファイル名は .csv で終わっています");
} else {
  console.log("ファイル名は .csv で終わっていません");
}
JavaScript

ステップ2:大文字小文字を区別するか決める

"report.csv" と "report.CSV" は、見た目では同じ拡張子として扱いたくなる場面があります。しかし、通常の endsWith() では大文字小文字が異なるため、同じ結果にはなりません。

// 大文字小文字の違いによって結果が変わる
console.log("report.csv".endsWith(".csv")); // true
console.log("report.CSV".endsWith(".csv")); // false
JavaScript

大文字小文字を区別しない仕様なら、対象文字列と末尾文字列の両方を小文字にそろえてから比較します。

// 両方を小文字にそろえてから比較する
const fileName = "REPORT.CSV";
const suffix = ".Csv";

const matches = fileName
  .toLowerCase()
  .endsWith(suffix.toLowerCase());

console.log(matches); // true

// 小文字化は元の文字列を書き換えない
console.log(fileName); // "REPORT.CSV"
JavaScript

小文字化を万能な比較方法と考えない

この方法は、英字の拡張子や、英数字中心の業務コードに対して使いやすい方法です。一方、言語ごとの複雑な大文字小文字の対応や、全角・半角、見た目が似ている文字の違いまで、すべて吸収するわけではありません。

また、大文字小文字に意味がある識別子では、区別をなくすと別の値を同一視してしまいます。「比較前には必ず小文字化する」と決めるのではなく、そのデータで無視してよい違いなのかを確認します。

ステップ3:末尾の空白や改行をどう扱うか決める

フォームやコピーしたテキストには、末尾に空白や改行が付いていることがあります。末尾一致では、その空白や改行も文字列の一部です。

// 空白や改行も末尾の文字として扱われる
console.log("report.csv ".endsWith(".csv"));  // false
console.log("report.csv\n".endsWith(".csv")); // false
JavaScript

末尾の余分な空白を無視する要件なら、trimEnd() を使います。前後両方を削除する trim() と異なり、末尾側だけを整えられます。

// 末尾の空白や改行を削除してから判定する
const input = "report.csv \n";

const matches = input.trimEnd().endsWith(".csv");

console.log(matches); // true

// 元の文字列は変更されない
console.log(JSON.stringify(input)); // "report.csv \n"
JavaScript

前処理は対象文字列と検索条件を分けて考える

対象文字列の末尾空白を無視する場合でも、指定する末尾文字列まで自動的に空白除去するとは限りません。たとえば "END " という末尾文字列では、最後の空白まで条件の一部かもしれません。

完成版では、末尾空白の除去は対象文字列だけに適用します。こうすることで、「入力の余分な空白を無視する」という処理と、「検索条件そのものを書き換える」という処理を分離できます。

ステップ4:空文字の扱いを決める

標準の endsWith() は、指定した末尾文字列が空文字の場合に true を返します。対象文字列が空文字の場合も同様です。

// 標準メソッドでは、空文字の末尾条件に一致する
console.log("hello".endsWith("")); // true
console.log("".endsWith(""));      // true

// 空の対象文字列は、空でない文字列では終わらない
console.log("".endsWith("csv"));   // false
JavaScript

この動作は不具合ではありません。ただし、業務画面で末尾条件の入力が必須なのに空欄だった場合など、そのまま一致と扱いたくないケースがあります。

そこで完成版では、末尾文字列が "" なら false にします。これは標準メソッドの仕様ではなく、今回作るユーティリティ独自の仕様です。

// 空の末尾条件を不一致として扱う簡易版
function endsWithNonEmpty(text, suffix) {
  // この段階では、両方が文字列であることを前提にする
  if (suffix === "") {
    return false;
  }

  return text.endsWith(suffix);
}

console.log(endsWithNonEmpty("hello", "")); // false
JavaScript

ステップ5:文字列以外の入力に備える

null や undefined に直接メソッドを呼ばない

入力が null や undefined の場合、そのまま endsWith() を呼び出すとエラーになります。APIの項目欠落や、値の取得に失敗するケースを想定しておく必要があります。

// エラーの動作を確認するための例
const text = null;

try {
  // null には endsWith() を呼び出せない
  text.endsWith(".csv");
} catch (error) {
  console.log(error.name); // "TypeError"
}
JavaScript

自動で文字列化するか、型を検証するか

String(value) を使えば数値などを文字列として扱えます。しかし、文字列化することが常に正しいとは限りません。数値が来たこと自体が、呼び出し側の不具合かもしれないからです。

// 数値を文字列として比較する明示的な例
const code = 12345;

console.log(String(code).endsWith("45")); // true
JavaScript

今回の関数では、自動変換を行わず、typeof で文字列かどうかを確認します。数値を扱いたい場面だけ、呼び出し側で明示的に変換します。

// 文字列以外と空の末尾条件を不一致として扱う
function endsWithText(text, suffix) {
  // null、undefined、数値、オブジェクトなどは受け付けない
  if (typeof text !== "string" || typeof suffix !== "string") {
    return false;
  }

  // 空の末尾条件は無効とする
  if (suffix === "") {
    return false;
  }

  return text.endsWith(suffix);
}

console.log(endsWithText("report.csv", ".csv")); // true
console.log(endsWithText(null, ".csv"));         // false
console.log(endsWithText(12345, "45"));          // false
console.log(endsWithText(String(12345), "45"));  // true
JavaScript

文字列以外を受け取った場合に false を返す設計は、画面上のフィルターなどで扱いやすい一方、呼び出し側の間違いを見えにくくする場合もあります。内部処理で型の間違いを早期発見したいなら、例外を投げる設計も選択肢です。重要なのは、どちらを採用したかを明示することです。

業務で使える完成版テンプレート

ここまでの方針をまとめた関数が、次の endsWithText() です。大文字小文字の無視と末尾空白の除去を、オプションで指定できます。

/**
 * 文字列が指定した末尾文字列で終わるかを判定します。
 *
 * 仕様:
 * - text と suffix は文字列のみ受け付けます。
 * - 文字列以外、または空の suffix には false を返します。
 * - デフォルトでは大文字小文字と末尾空白を区別します。
 * - suffix 自体の空白は削除しません。
 *
 * @param {unknown} text 判定対象
 * @param {unknown} suffix 期待する末尾文字列
 * @param {Object} [options] 比較方法の設定
 * @param {boolean} [options.ignoreCase=false] 大文字小文字を無視するか
 * @param {boolean} [options.trimEndInput=false] 対象の末尾空白を除去するか
 * @returns {boolean} 条件を満たす場合は true
 */
function endsWithText(text, suffix, options = {}) {
  // 文字列以外は不一致として扱う
  if (typeof text !== "string" || typeof suffix !== "string") {
    return false;
  }

  // 空の末尾条件は、この関数では無効とする
  if (suffix === "") {
    return false;
  }

  // オプションは設定オブジェクトとして渡す
  // null や配列などは、設定ミスとして例外にする
  if (
    options === null ||
    typeof options !== "object" ||
    Array.isArray(options)
  ) {
    throw new TypeError("options は設定オブジェクトで指定してください");
  }

  const {
    ignoreCase = false,
    trimEndInput = false
  } = options;

  // "false" などの文字列を誤って指定することを防ぐ
  if (
    typeof ignoreCase !== "boolean" ||
    typeof trimEndInput !== "boolean"
  ) {
    throw new TypeError("各オプションは boolean で指定してください");
  }

  // 元の値を変更せず、比較用の文字列を用意する
  let source = text;
  let target = suffix;

  // 必要な場合だけ、対象文字列の末尾空白を除去する
  if (trimEndInput) {
    source = source.trimEnd();
  }

  // 必要な場合だけ、両方を小文字へそろえる
  if (ignoreCase) {
    source = source.toLowerCase();
    target = target.toLowerCase();
  }

  // 最終的な末尾一致を判定する
  return source.endsWith(target);
}

// 通常の末尾一致
console.log(endsWithText("report.csv", ".csv")); // true

// デフォルトでは大文字小文字を区別する
console.log(endsWithText("report.CSV", ".csv")); // false

// 大文字小文字を無視する
console.log(
  endsWithText("report.CSV", ".csv", { ignoreCase: true })
); // true

// 末尾空白と大文字小文字の違いを無視する
console.log(
  endsWithText("report.CSV \n", ".csv", {
    ignoreCase: true,
    trimEndInput: true
  })
); // true

// 無効な入力は不一致にする
console.log(endsWithText(null, ".csv")); // false
console.log(endsWithText("report.csv", "")); // false
JavaScript

入力値と設定ミスを分けて扱う理由

このテンプレートでは、対象文字列が欠けている場合は false を返します。一方、ignoreCase: "false" のような設定ミスには例外を投げます。

対象データの欠落は業務データとして起こり得ますが、オプションの型が違うことは、通常はコードの修正が必要な問題です。両者を分けることで、データの不一致と実装上の誤りを区別できます。

ただし、この設計だけが正解ではありません。プロジェクトで例外処理の方針が決まっている場合は、それに合わせて統一します。

完成版の処理を具体例で確認する

入力が "report.CSV \n"、末尾条件が ".csv" で、両方のオプションを有効にした場合を追ってみます。

  1. 対象文字列と末尾条件が、どちらも文字列であることを確認します。
  2. 末尾条件が空文字ではないことを確認します。
  3. オプションの指定が正しいことを確認します。
  4. 対象文字列の末尾空白と改行を削除し、"report.CSV" にします。
  5. 比較する両方の文字列を小文字にそろえます。
  6. "report.csv".endsWith(".csv") を実行し、true を返します。

このように、入力の確認、必要な前処理、比較という順番で分けると、処理の意味を説明しやすくなります。新しい要件を追加するときも、どの段階を変更するべきか判断しやすくなります。

実務での活用例

複数の拡張子を候補として判定する

画像ファイル名の候補を絞るなど、複数の末尾文字列のどれかに一致すればよい場合は、配列の some() と組み合わせます。

// ファイル名が、指定した拡張子のいずれかで終わるか調べる
const fileName = "photo.JPEG";
const extensions = [".jpg", ".jpeg", ".png"];

const matches = extensions.some(extension =>
  endsWithText(fileName, extension, { ignoreCase: true })
);

console.log(matches); // true
JavaScript

some() は、いずれかの要素で条件が成立すると true を返します。条件が増えても、拡張子の配列を更新するだけで対応できます。

拡張子にはドットを含める

"csv" だけを指定すると、"notcsv" のような文字列にも一致します。拡張子として確認したいなら、区切りであるドットを含めて ".csv" と指定します。

// "csv" という文字列で終わることしか確認していない
console.log("notcsv".endsWith("csv")); // true

// ドットまで含めることで、意図をより正確に表せる
console.log("notcsv".endsWith(".csv")); // false

// 後ろに別の拡張子がある場合は一致しない
console.log("report.csv.bak".endsWith(".csv")); // false
JavaScript

商品コードの接尾辞を判定する

接尾辞に区切り文字を含める考え方は、商品コードや帳票名にも応用できます。

// "-SALE" という区切り付きの接尾辞を判定する
const productCode = "ITEM-001-SALE";

if (endsWithText(productCode, "-SALE")) {
  console.log("セール区分の接尾辞が付いています");
}
JavaScript

ただし、この判定で確認できるのは接尾辞だけです。商品コード全体が正しい形式か、実在する商品か、販売対象かは別途確認します。

候補の一覧から対象ファイル名を抽出する

配列の filter() と組み合わせれば、条件に合う文字列だけを取り出せます。

// CSVで終わるファイル名だけを抽出する
const fileNames = [
  "sales.csv",
  "manual.pdf",
  "customers.CSV",
  "sales.csv.bak"
];

const csvFileNames = fileNames.filter(fileName =>
  endsWithText(fileName, ".csv", { ignoreCase: true })
);

console.log(csvFileNames);
// ["sales.csv", "customers.CSV"]
JavaScript

補足:第2引数で比較対象の終了位置を指定する

標準の endsWith() には、第2引数として終了位置を指定できます。指定位置の直前までを比較対象にして、その範囲が指定文字列で終わるかを判定します。省略時は文字列全体が対象になります。

// 先頭から10までの範囲、つまり "report.csv" を対象にする
const text = "report.csv.bak";

console.log(text.endsWith(".csv"));     // false
console.log(text.endsWith(".csv", 10)); // true
JavaScript

第2引数は「検索を始める位置」ではありません。また、この例の文字列全体が ".csv" で終わるわけでもありません。通常の末尾一致を確認したい場面では、省略するほうが意図が明確です。

位置や length はUTF-16のコード単位で扱われるため、絵文字などを含む文字列では、見た目の文字数と一致しない場合があります。位置を手計算する必要がない処理では、無理に第2引数を使わないほうが保守しやすくなります。

重要:末尾一致だけでは安全性を判断できない

ファイル名が一致しても、ファイルの内容は保証されない

"document.pdf" という名前でも、中身が正しいPDFとは限りません。ファイル名は利用者が変更できるためです。

アップロード処理では、末尾一致を画面上の案内や候補の絞り込みに使うことはできますが、それだけを安全性の根拠にはできません。サーバー側でも、許可形式、サイズ、内容の解析結果などを確認し、用途に応じた検証を行う必要があります。

また、ブラウザー側の判定は利用者によって変更・回避できるため、重要な検証をブラウザーだけで完結させないことが大切です。

URLやドメインは文字列全体の末尾だけで判定しない

URLには、ホスト名だけでなくパス、クエリ、フラグメントなども含まれます。URL全体に対して endsWith() を使っても、どの部分に一致したかを区別できません。

また、ホスト名を取り出した場合でも、単に "example.com" で終わるかを調べるだけでは、"notexample.com" まで一致してしまいます。ドメインを確認する場合は、URLとして解析し、ホスト名の完全一致やドット区切りの境界など、許可ルールに応じて検証します。

任意の末尾文字列を正規表現へ変換する必要はない

固定文字列との末尾一致なら、endsWith() を使うことで、ドットや括弧などを正規表現の特殊文字として扱う心配がありません。利用者が指定した文字列を、そのまま正規表現へ組み込む設計は避け、パターン検索が必要なときだけ別途検討します。

// "." は特別なパターンではなく、通常の文字として比較される
console.log("report.csv".endsWith(".csv")); // true
console.log("reportXcsv".endsWith(".csv")); // false
JavaScript

テストで境界条件を確認する

ユーティリティ関数は、正常な入力だけでなく、空文字、型の違い、空白、大文字小文字なども確認します。以下は、先ほどの完成版 endsWithText() に対するテスト例です。

// [対象文字列, 末尾条件, オプション, 期待値]
const testCases = [
  ["report.csv", ".csv", {}, true],
  ["report.csv.bak", ".csv", {}, false],
  ["report.CSV", ".csv", {}, false],
  ["report.CSV", ".csv", { ignoreCase: true }, true],
  ["report.csv ", ".csv", {}, false],
  ["report.csv\n", ".csv", { trimEndInput: true }, true],
  ["report.CSV \n", ".csv", {
    ignoreCase: true,
    trimEndInput: true
  }, true],
  ["", ".csv", {}, false],
  ["report.csv", "", {}, false],
  ["", "", {}, false],
  [null, ".csv", {}, false],
  [undefined, ".csv", {}, false],
  [12345, "45", {}, false],
  ["12345", "45", {}, true],
  ["report.csv", undefined, {}, false],
  ["処理完了", "完了", {}, true]
];

// 期待値と実際の結果を比較する
for (const [text, suffix, options, expected] of testCases) {
  const actual = endsWithText(text, suffix, options);

  console.log({
    text,
    suffix,
    options,
    expected,
    actual,
    pass: actual === expected
  });
}
JavaScript

これに加えて、オプションの型を間違えたときに例外が発生することも確認します。

// 設定ミスを例外として検出できるか確認する
try {
  endsWithText("report.CSV", ".csv", {
    // boolean ではなく文字列なので、設定ミスとして扱われる
    ignoreCase: "false"
  });
} catch (error) {
  console.log(error instanceof TypeError); // true
}
JavaScript

このように期待値を明示すると、将来の変更時にも仕様を確認できます。特に、標準メソッドと異なる「空文字なら false」という方針は、テストで固定しておくことが重要です。

まとめ

JavaScriptで文字列が指定文字列で終わるかを判定する基本は、endsWith() です。固定文字列の末尾一致を、読みやすく直接表現できます。

  • 途中に含まれるかではなく、末尾が一致するかを判定します。
  • 標準では大文字小文字と末尾の空白を区別します。
  • 空文字を指定すると標準では true になるため、独自関数では扱いを明示します。
  • 文字列以外を変換するか、受け付けないかを先に決めます。
  • 拡張子や接尾辞は、ドットやハイフンなどの区切りまで含めて考えます。
  • 末尾一致を、ファイル内容の検証やURLの許可判定、認可の代わりにしないようにします。

実務で役立つのは、単にメソッドを覚えることではなく、「どの入力を受け付け、どの違いを無視し、何を保証する関数なのか」を明確にすることです。基本的な末尾一致、必要な前処理、境界条件のテストを分けて設計すると、小さなユーティリティでも安心して再利用しやすくなります。

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