0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

camelCase / snake_case / kebab-case を相互変換する。入力形式を判定せず『単語配列』に正規化する設計

0
Posted at

3行まとめ

  • 変数名を camelCase / snake_case / kebab-case など10種の命名規則に相互変換するツールを作った
  • 設計の肝は 入力形式を判定しないこと。どんな形式でも一度「単語の配列」に正規化してから、目的の規則で再結合する
  • 一番難しいのは camelCase の単語境界検出。XMLParser を XML + Parser に割るには2段階の正規表現が要る

my_variable_name を myVariableName にしたい、API_BASE_URL を api-base-url にしたい。言語やレイヤーをまたぐと命名規則の変換は地味に頻発する。Python は snake_case、JavaScript は camelCase、URL は kebab-case、定数は UPPER_SNAKE…と使い分けがあるからだ。

ぱんだツールズに作ったケース変換ツールは、入力した識別子を10種類の命名規則に一括変換する。外部ライブラリは使わず純粋な文字列処理だが、「入力形式をどう扱うか」と「camelCase の単語境界をどう割るか」に設計上の面白さがある。

入力形式を「判定しない」設計にする

命名規則の相互変換を素朴に考えると、「入力が snake_case なのか camelCase なのか kebab-case なのかを判定して、対応する変換を選ぶ」という分岐を書きたくなる。だが形式が10種類あると、判定ロジックも変換の組み合わせも爆発する。

そこで入力形式を一切判定しない。どんな形式で来ても、まず**「単語の配列」という中間表現に正規化**してしまう。my_variable も myVariable も my-variable も、すべて ['my', 'variable'] という同じ配列にする。あとはこの配列を目的の規則で再結合するだけ。

// 入力 → 単語配列 → 目的の形式、の2段
const words = splitToWords(input)   // どの形式でも ['my', 'variable']
const snake = words.map(w => w.toLowerCase()).join('_')   // my_variable
const camel = toCamelCase(words)                          // myVariable

この設計だと、「入力が何形式か」を気にする必要がまったくない。変換先ごとの関数(toCamelCase / toSnakeCase …)を10個用意するだけで、10×10通りの相互変換が成立する。分岐ではなく正規化で複雑さを畳む、という発想だ。

単語への分割 — 全セパレータを1つの正規表現で割る

正規化の本体が splitToWords。区切り文字(_ - . / スペース)と camelCase の大文字境界、両方を単語の切れ目として扱う。

function splitToWords(input: string): string[] {
  const trimmed = input.trim()
  if (!trimmed) return []

  return trimmed
    // ① camelCase/PascalCase の大文字境界をスペースに変換
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
    // ② あらゆる区切り文字でまとめて分割
    .split(/[\s_\-.\/]+/)
    .filter(Boolean)
}

後半 .split(/[\s_\-.\/]+/) がうまい。スペース・アンダースコア・ハイフン・ドット・スラッシュを1つの文字クラスにまとめて区切りにし、+ で連続区切り(foo__bar のような重複)もまとめて1回で割る。.filter(Boolean) で空文字を落とす。snake / kebab / dot / path 系はこれだけで単語に割れる。

問題は camelCase。区切り文字が無くて大文字で単語が切れるので、split の前に大文字境界をスペースに変換しておく。これが前半の2つの replace。

camelCase の境界検出に正規表現が2本要る理由

camelCase の単語境界は、1本の正規表現では足りない。2本必要になる。

.replace(/([a-z])([A-Z])/g, '$1 $2')          // ①
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')    // ②

① ([a-z])([A-Z]) は「小文字の直後の大文字」を境界にする。myVariable → my Variable、getUserName → get User Name。普通の camelCase はこれで割れる。

だがこれだけだと**連続する大文字(頭字語)**が処理できない。XMLParser は L も P も大文字で、「小文字→大文字」の境界が無いので①では割れず、XMLParser が丸ごと1単語になってしまう。

② ([A-Z]+)([A-Z][a-z]) がこれを救う。「大文字の連なり」と「その後に続く"大文字+小文字"」の間を境界にする。XMLParser でいうと、XML(大文字の連なり)と Parser(P + arser)の間で割れて、XML Parser になる。HTTPSConnection なら HTTPS Connection、parseJSONData なら(①と合わせて)parse JSON Data。

つまり、

  • ① camelCase の通常の境界(小文字→大文字)
  • ② 頭字語の終わりと次の単語の始まり(大文字連続→大文字+小文字)

の2つを順に適用して初めて、XMLHttpRequest のような頭字語混じりの識別子もきれいに XML Http Request に割れる。頭字語を考慮しないと、API名やクラス名の変換でよく崩れるので、この2本目が地味に効く。

あとは各規則で再結合するだけ

単語配列さえ手に入れば、各命名規則への変換は素直。大文字小文字を整えて区切り文字で join するだけ。以下は代表的な4種だが、残り6種も同じ構造で書ける。

// camelCase: 先頭だけ小文字、以降は頭大文字
function toCamelCase(words: string[]): string {
  return words
    .map((w, i) => i === 0
      ? w.toLowerCase()
      : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
    .join('')
}

function toSnakeCase(words: string[]): string {
  return words.map(w => w.toLowerCase()).join('_')        // my_variable
}
function toKebabCase(words: string[]): string {
  return words.map(w => w.toLowerCase()).join('-')        // my-variable
}
function toUpperSnakeCase(words: string[]): string {
  return words.map(w => w.toUpperCase()).join('_')        // MY_VARIABLE
}

snake_case / kebab-case / dot.case / path/case は「全部小文字にして join 文字を変えるだけ」で済むので、ほぼ同型。UPPER_SNAKE_CASE や UPPER-KEBAB-CASE は toUpperCase。PascalCase は camelCase の先頭も大文字にした版、Title Case / Sentence case はスペース区切り。区切り文字と大文字小文字のルールだけが違う、同じ構造の関数が並ぶ。

入力を \n で分割すれば複数行を一括変換でき、各行が独立して splitToWords → 再結合に流れる。すべて useMemo で入力変更時にリアルタイム計算している。

まとめ

命名規則の相互変換ツールを作るときの要点。

  • 入力形式を判定しない。どの形式も一度「単語配列」に正規化 → 目的の規則で再結合。10×10通りを、正規化+変換関数10個に畳める
  • 区切り分割は split(/[\s_\-.\/]+/) で全セパレータを1つにまとめる。+ で連続区切り、filter(Boolean) で空要素除去
  • camelCase の境界検出は 正規表現が2本。① ([a-z])([A-Z]) で通常の境界、② ([A-Z]+)([A-Z][a-z]) で頭字語(XMLParser → XML Parser)
  • 再結合は「大文字小文字を整えて join 文字を変えるだけ」の同型関数が並ぶ

「形式を判定して分岐」ではなく「一度ばらして組み直す」に発想を変えると、命名規則の変換はぐっと素直になる。外部ライブラリも要らず、正規表現2本と join だけでブラウザ完結する。

ぱんだツールズ では他にも文字数カウント・テキスト整形・正規表現テスト・JSON整形など、開発者向けのブラウザ完結ツールを多数公開中。全部無料・登録不要・入力はサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?