🤔

[JS]文字列操作: 連続する文字列を変換する方法

2024/08/17に公開1

replaceを用いた文字列操作

replaceメソッドと正規表現を使って、連続する特定の文字列を一つにまとめる方法を解説します。
以下のコードは、連続するハイフンを一つのハイフンに置き換える例です。

let str = "hello---world--example";
let result = str.replace(/-+/g, "-");
console.log(result); // "hello-world-example"

g オプションを付けることで、文字列全体に対して正規表現を適用し、該当するすべての部分を置き換えます。ここで、正規表現の便利なオプションである g オプションと i オプションについて詳しく見ていきます。

gオプション: すべての一致部分を置換する

g オプションは、指定した文字列が複数回登場する場合、そのすべてを置き換えるためのオプションです。例えば、ウェブサイトのドメイン名が変更になり、古いURLをすべて新しいドメインに変更する必要がある場合を考えます。

var content = "私たちのウェブサイトはhttp://oldsite.comで見つけられます。詳細はhttp://oldsite.com/contactをご覧ください。";
content = content.replace(/http:\/\/oldsite\.com/g, "http://newsite.com");
console.log(content); 
// "私たちのウェブサイトはhttp://newsite.comで見つけられます。詳細はhttp://newsite.com/contactをご覧ください。"

正規表現で g オプションを使用することで文字列全体にわたって「http://oldsite.com」を検索し、すべての一致部分を「http://newsite.com」に置き換えています。g オプションがないと、最初の一致部分しか置き換えられません。

iオプション: 大文字と小文字を区別しない検索

i オプションは、大文字と小文字を区別せずに検索を行いたい場合に使います。例えば、ユーザーから入力されたメールアドレスが正しいかどうかをチェックする際に、大文字と小文字を区別せずに検索を行いたい場合があります。

var email = "User@Example.com";
var isValid = email.match(/user@example\.com/i);
console.log(isValid); // ["User@Example.com"]

i オプションを使用することで、「user@example.com」という文字列を大文字・小文字を区別せずに検索しています。この場合、「User@Example.com」という入力に対しても正しくマッチします。

名前付きグループとgオプションの併用

次に、正規表現の「名前付きグループ」とgオプションの併用について説明します。

名前付きグループとは

名前付きグループとは、正規表現のグループに名前を付け、その名前でマッチした文字列を取り出すことができる機能です。例えば、以下の例では、メールアドレスのドメイン部分を取り出しています。

const result = 'alice@example.com'.match(/@(?<domain>.+)/);
console.log(result.groups.domain); //=> example.com

この正規表現では、@の後に続く任意の文字列 (.+) を「domain」という名前付きグループに格納し、matchメソッドを使って、.groupsプロパティを利用し、domainという名前付きグループにマッチした部分を取得しています。

名前付きグループとgオプションの問題

gオプションと名前付きグループを組み合わせると、通常のmatchメソッドでは、名前付きグループを使った結果を.groupsで参照できなくなるという問題があります。

const result = '123'.match(/(?<digit>\d)/g);
console.log(result); //=> ["1", "2", "3"]

この正規表現では、数字 (\d) を「digit」という名前付きグループに格納し、g オプションでそれぞれの数字を全て検索しており、 matchメソッドを使うと、名前付きグループが無視され、マッチした文字列のみが返されます。この問題を解決する一つの方法として、RegExp.prototype.execメソッドを使用することができます。

const regexp = /(?<digit>\d)/g;
let match;
const result = [];

while ((match = regexp.exec('123')) !== null) {
    result.push(match.groups);
}
console.log(result);
// => [{ digit: '1' }, { digit: '2' }, { digit: '3' }]

execメソッドは、正規表現でマッチした各項目を繰り返し処理するために使います。ここでは、whileループを使って、各マッチ結果の名前付きグループをresult配列に追加しています。結果として、すべての数字が「digit」という名前付きグループに格納され、配列にまとめられます。

splitとjoinを使った文字列操作

文字列操作では、splitメソッドで文字列を分割し、joinメソッドで再度結合する方法も便利です。

let str = "This   

 is  a   test.";
let result = str.split(/\s+/).join(' ');
console.log(result); // "This is a test."

正規表現 \s+ を使って、複数の空白文字を一つの空白で分割し、join(' ')で分割した部分を一つの空白文字で結合しています。

reduceを使った文字列操作

また、reduceメソッドを使って、文字列を一つに変換することもできます。

let str = "This    is  a   test.";
let result = str.split('').reduce((acc, char) => {
  if (char === ' ' && acc.endsWith(' ')) {
    return acc;
  }
  return acc + char;
}, '');
console.log(result); // "This is a test."

split('')で文字列を一文字ずつ配列に変換し、reduceを使って、連続する空白を一つにまとめる処理を行います。accは蓄積された結果、charは現在の文字です。acc.endsWith(' ')で蓄積された結果が空白で終わっているかどうかを確認します。もしそうであれば、次の空白は無視されます。

Discussion

nap5nap5

例えば、以下の例では、メールアドレスのドメイン部分を取り出しています。

typed-regexライブラリを使うとパターンマッチした部分の補完も出てくれるようでした

定義側

import { TypedRegEx } from "typed-regex";

export function extractEmailDomains(value: string) {
  const regex = TypedRegEx("@(?<domain>[\\w.-]+\\.[A-Za-z]{2,})", "g");

  return regex.captureAll(value);
}

使用側

import { describe, test, expect } from "vitest";
import { extractEmailDomains } from "@/index";

describe("nice test", () => {
  test("extractEmailDomains", () => {
    const content =
      "私たちのウェブサイトはspike.spigel@cowboy.comで見つけられます。詳細はfaye.valentine@bebop.comをご覧ください。";

    const results = extractEmailDomains(content);

    const emailDomains = results.filter((d) => d != null);

    expect(emailDomains).toEqual([
      {
        domain: "cowboy.com",
      },
      {
        domain: "bebop.com",
      },
    ]);
  });
});