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 にも同じ内容を投稿しています。