JavaScript Tips | 文字列操作:snake_caseに変換する

JavaScript JavaScript
スポンサーリンク
スポンサーリンク

業務・実務で使えるJavaScriptユーティリティ:snake_caseに変換する方法

プログラミングの現場では、「UserName」のようなパスカルケースや「userName」のようなキャメルケースを、「user_name」のようなスネークケースに変換する必要が頻繁に生じます。今回は、様々な形式の文字列をスネークケースに統一するユーティリティ関数について、ステップバイステップで解説します。

なぜsnake_caseに変換する必要があるのか

実務では、データベースのカラム名、ファイル名、定数名など、特定の識別子にスネークケースを使用する場面が多くあります。特にSQLのデータベースでは、カラム名やテーブル名にスネークケースを採用することが一般的です。また、環境変数や設定ファイルのキー名としても、スネークケースが広く使われています。

例えば、JavaScriptのコード内で「userName」という変数を使用している場合、データベースに保存する際には「user_name」というカラム名に変換する必要があります。また、APIのレスポンスで受け取った「UserName」を、システム間連携用のフォーマット「user_name」に変換したい場合などが典型的です。

具体的な使用場面

  • JavaScriptの変数名をデータベースのカラム名に変換(例:「userName」→「user_name」)
  • APIレスポンスのフィールド名を統一(例:「UserName」→「user_name」)
  • ファイル名やディレクトリ名の生成(例:「UserProfile」→「user_profile」)
  • 環境変数名の生成(例:「apiKey」→「api_key」)
  • 定数名の生成(例:「maxValue」→「max_value」)

基本的な実装方法

スネークケースへの変換は、主に以下の3つのパターンに対応する必要があります。それぞれのパターンを段階的に実装していきましょう。

パターン1:キャメルケースからスネークケースへの変換

大文字で区切られた文字列を、アンダースコア(_)で区切り、すべて小文字に変換する方法です。

// キャメルケースをスネークケースに変換する関数
function camelToSnake(str) {
  // 空文字やundefinedの場合はそのまま返す(エラー防止)
  if (!str) return str;
  
  // 大文字の前にアンダースコアを挿入し、すべて小文字に変換
  return str
    .replace(/([A-Z])/g, '_$1')  // 大文字の前に_を追加
    .toLowerCase()               // すべて小文字に変換
    .replace(/^_/, '');          // 先頭のアンダースコアを削除(最初の文字が大文字の場合)
}

// 使用例
console.log(camelToSnake('userName'));      // 'user_name'
console.log(camelToSnake('userEmail'));     // 'user_email'
console.log(camelToSnake('orderId'));       // 'order_id'
console.log(camelToSnake('user'));          // 'user'(変換不要な場合はそのまま)
JavaScript

コードの解説

この関数は3つのステップで動作します。

  1. アンダースコアの挿入:replace(/([A-Z])/g, '_$1')で、すべての大文字の前にアンダースコアを挿入します。$1はキャプチャした大文字を表します。
  2. 小文字への変換:toLowerCase()で文字列全体を小文字に変換します。
  3. 先頭のアンダースコア削除:replace(/^_/, '')で、もし先頭にアンダースコアがあれば削除します。これは「UserName」のようなパスカルケースに対応するためです。

パターン2:パスカルケースからスネークケースへの変換

キャメルケースとほぼ同様ですが、先頭が大文字の場合に対応する必要があります。

// パスカルケースをスネークケースに変換する関数
function pascalToSnake(str) {
  // 空文字やundefinedの場合はそのまま返す
  if (!str) return str;
  
  // 先頭の大文字を小文字に変換し、その後キャメルケースと同じ処理
  return str
    .replace(/([A-Z])/g, '_$1')  // 大文字の前に_を追加
    .toLowerCase()               // すべて小文字に変換
    .replace(/^_/, '');          // 先頭のアンダースコアを削除
}

// 使用例
console.log(pascalToSnake('UserName'));      // 'user_name'
console.log(pascalToSnake('UserEmail'));     // 'user_email'
console.log(pascalToSnake('OrderId'));       // 'order_id'
JavaScript

連続した大文字への対応

「HTMLParser」のような略語を含む文字列の場合、単純な変換では「h_t_m_l_parser」のような不自然な結果になります。より洗練された変換が必要な場合は、以下のような対応を検討してください。

// より洗練されたスネークケース変換(連続大文字への配慮版)
function pascalToSnakeAdvanced(str) {
  if (!str) return str;
  
  return str
    // 連続する大文字の区切りを適切に処理(HTMLParser → html_parser)
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
    // 小文字と大文字の区切りを処理(HtmlParser → html_parser)
    .replace(/([a-z\d])([A-Z])/g, '$1_$2')
    // すべて小文字に変換
    .toLowerCase();
}

// 使用例
console.log(pascalToSnakeAdvanced('UserName'));      // 'user_name'
console.log(pascalToSnakeAdvanced('HTMLParser'));    // 'html_parser'(自然な結果)
console.log(pascalToSnakeAdvanced('URL'));           // 'url'(すべて大文字の場合)
JavaScript

パターン3:ケバブケースからスネークケースへの変換

ハイフン(-)で区切られた文字列を、アンダースコア(_)に置き換え、すべて小文字に変換する方法です。

// ケバブケースをスネークケースに変換する関数
function kebabToSnake(str) {
  // 空文字やundefinedの場合はそのまま返す
  if (!str) return str;
  
  // ハイフンをアンダースコアに置き換え、小文字に変換
  return str
    .replace(/-/g, '_')   // ハイフンをアンダースコアに置き換え
    .toLowerCase();       // すべて小文字に変換
}

// 使用例
console.log(kebabToSnake('user-name'));      // 'user_name'
console.log(kebabToSnake('user-email'));     // 'user_email'
console.log(kebabToSnake('order-id'));       // 'order_id'
JavaScript

汎用的なsnake_case変換関数の実装

実務では、どの形式で入力されるか分からない場合があります。そこで、キャメルケース・パスカルケース・ケバブケースのいずれにも対応できる汎用的な関数を作成しましょう。

統合版スネークケース変換関数

/**
 * 任意の文字列をスネークケースに変換する汎用関数
 * @param {string} str - 変換対象の文字列
 * @returns {string} スネークケースに変換された文字列
 */
function toSnakeCase(str) {
  // 入力チェック(null、undefined、空文字対策)
  if (!str || typeof str !== 'string') {
    return '';
  }

  // ステップ1: 文字列の正規化
  let result = str
    // 連続する大文字の区切りを適切に処理(HTMLParser → html_parser)
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
    // 小文字と大文字の区切りを処理(userName → user_name)
    .replace(/([a-z\d])([A-Z])/g, '$1_$2')
    // ハイフンとスペースをアンダースコアに置き換え
    .replace(/[-\s]+/g, '_')
    // 連続するアンダースコアを1つにまとめる
    .replace(/_+/g, '_')
    // 先頭と末尾のアンダースコアを削除
    .replace(/^_+|_+$/g, '');

  // ステップ2: すべて小文字に変換
  return result.toLowerCase();
}

// 使用例
console.log(toSnakeCase('userName'));          // 'user_name'
console.log(toSnakeCase('UserName'));          // 'user_name'
console.log(toSnakeCase('user-name'));         // 'user_name'
console.log(toSnakeCase('user name'));         // 'user_name'
console.log(toSnakeCase('USER_NAME'));         // 'user_name'
console.log(toSnakeCase('user_name'));         // 'user_name'(既にスネークケース)
console.log(toSnakeCase('HTMLParser'));        // 'html_parser'
console.log(toSnakeCase('userId123'));         // 'user_id_123'
console.log(toSnakeCase('  user  name  '));   // 'user_name'(スペースも処理)
JavaScript

コードの解説

この関数は複数の正規表現を順番に適用して、文字列を正規化していきます。

  1. 連続大文字の処理:([A-Z]+)([A-Z][a-z])で、「HTMLParser」のような連続する大文字を適切に区切ります。
  2. キャメルケースの処理:([a-z\d])([A-Z])で、「userName」のような小文字と大文字の区切りを処理します。
  3. ハイフンとスペースの処理:[-\s]+で、ハイフンやスペースをアンダースコアに置き換えます。
  4. アンダースコアの正規化:連続するアンダースコアを1つにまとめ、先頭と末尾のアンダースコアを削除します。
  5. 小文字への変換:最後にすべて小文字に変換します。

重要な注意点:正規表現の順序

上記の正規表現は順序が重要です。連続大文字の処理を先に行わないと、「HTMLParser」が「h_t_m_l_parser」のような不自然な結果になってしまいます。正規表現を組み合わせる場合は、処理の順序を慎重に検討してください。

より実践的な応用パターン

実際の業務では、単一の文字列だけでなく、オブジェクトのキーや配列の要素を一括で変換する場面が多くあります。

1. オブジェクトのキーを一括変換

APIから受け取ったオブジェクトのすべてのキーをスネークケースに変換したい場合、以下のような実装が有効です。

/**
 * オブジェクトのキーをすべてスネークケースに変換
 * @param {Object} obj - 変換対象のオブジェクト
 * @returns {Object} キーがスネークケースに変換された新しいオブジェクト
 */
function objectKeysToSnake(obj) {
  // オブジェクトでない場合はそのまま返す
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  // 配列の場合は各要素に再帰的に適用
  if (Array.isArray(obj)) {
    return obj.map(item => objectKeysToSnake(item));
  }
  
  // オブジェクトの各キーを変換
  return Object.keys(obj).reduce((acc, key) => {
    // キーをスネークケースに変換
    const snakeKey = toSnakeCase(key);
    // 値もオブジェクトの場合は再帰的に変換
    acc[snakeKey] = objectKeysToSnake(obj[key]);
    return acc;
  }, {});
}

// 使用例
const apiResponse = {
  userName: 'Taro',
  userEmail: '[email protected]',
  addressInfo: {
    zipCode: '123-4567',
    cityName: 'Tokyo'
  },
  orderHistory: [
    { orderId: 1, orderDate: '2024-01-01' },
    { orderId: 2, orderDate: '2024-01-02' }
  ]
};

const snakeCased = objectKeysToSnake(apiResponse);
console.log(snakeCased);
// 結果:
// {
//   user_name: 'Taro',
//   user_email: '[email protected]',
//   address_info: {
//     zip_code: '123-4567',
//     city_name: 'Tokyo'
//   },
//   order_history: [
//     { order_id: 1, order_date: '2024-01-01' },
//     { order_id: 2, order_date: '2024-01-02' }
//   ]
// }
JavaScript

再帰処理の重要性

上記の関数は「再帰(自分自身を呼び出す処理)」を使用しています。これにより、ネストされたオブジェクトや配列の中身も完全に変換できます。実務では、APIのレスポンスが深くネストされていることが多いため、この再帰的なアプローチが非常に重要です。

2. 配列内の文字列を一括変換

// 配列内のすべての文字列をスネークケースに変換
function arrayToSnake(arr) {
  if (!Array.isArray(arr)) return arr;
  
  return arr.map(item => {
    if (typeof item === 'string') {
      return toSnakeCase(item);
    }
    return item; // 文字列でない場合はそのまま
  });
}

// 使用例
const columns = ['userName', 'userEmail', 'orderId'];
const snakeColumns = arrayToSnake(columns);
console.log(snakeColumns); // ['user_name', 'user_email', 'order_id']
JavaScript

3. 安全な文字列変換(null・undefined対策)

実務では、予期せぬタイミングでnullやundefinedが渡されることがあります。より堅牢な関数を作成しましょう。

// 型チェックを厳密に行う安全な関数
function safeToSnake(str) {
  // 文字列型でない、または空文字の場合は空文字を返す
  if (typeof str !== 'string' || str.length === 0) {
    return '';
  }
  
  // 前後のスペースを除去してから変換
  const trimmed = str.trim();
  return toSnakeCase(trimmed);
}

// 使用例
console.log(safeToSnake('  userName  '));  // 'user_name'(スペースも除去)
console.log(safeToSnake(null));              // ''(エラーにならない)
console.log(safeToSnake(123));               // ''(数値でもエラーにならない)
console.log(safeToSnake(undefined));         // ''(エラーにならない)
JavaScript

よくある間違いと注意点

初心者の方がつまずきやすいポイントを3つ挙げます。

1. 文字列の変更不可性を理解していない

JavaScriptの文字列は「イミュータブル(変更不可)」です。つまり、replace()やtoLowerCase()を実行しても元の文字列は変わらず、新しい文字列が返されるだけです。以下のコードは間違いです。

// 間違った例
let str = 'userName';
str.replace(/([A-Z])/g, '_$1');
str.toLowerCase();
console.log(str); // 依然として 'userName'

// 正しい例
let str = 'userName';
str = str.replace(/([A-Z])/g, '_$1').toLowerCase();
console.log(str); // 'user_name'
JavaScript

2. 連続した大文字の扱い

「HTMLParser」や「URL」のような連続した大文字を含む文字列は、単純な変換では意図しない結果になることがあります。以下の例を確認してください。

// 単純な変換の問題点
function simpleCamelToSnake(str) {
  return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
}

console.log(simpleCamelToSnake('userName'));     // 'user_name'(問題なし)
console.log(simpleCamelToSnake('HTMLParser'));   // 'h_t_m_l_parser'(不自然)

// 対策:連続する大文字を適切に処理
function advancedCamelToSnake(str) {
  return str
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
    .replace(/([a-z\d])([A-Z])/g, '$1_$2')
    .toLowerCase();
}

console.log(advancedCamelToSnake('userName'));     // 'user_name'(自然)
console.log(advancedCamelToSnake('HTMLParser'));   // 'html_parser'(自然)
JavaScript

3. 日本語を含む文字列への対応

日本語には大文字・小文字の概念がないため、日本語を含む文字列の場合は注意が必要です。以下のコードを確認してください。

// 日本語を含む場合
console.log(toSnakeCase('ユーザー名'));        // 'ユーザー名'(変化なし)
console.log(toSnakeCase('userName名前'));      // 'user_name名前'(日本語はそのまま)
console.log(toSnakeCase('user_name_テスト'));  // 'user_name_テスト'(日本語はそのまま)
JavaScript

日本語を含む場合でも、基本的には区切り文字(大文字やハイフン)で区切られた部分が正しく処理されます。ただし、日本語同士が連続している場合は、単語の区切りとして認識されないため、意図した結果にならないことがあります。

実務で使えるテンプレート集

コピー&ペーストですぐに使える形でまとめました。プロジェクトの共通ユーティリティファイルに保存しておくと便利です。

/**
 * 文字列ケース変換ユーティリティ集
 * 業務・実務で使用する文字列操作の関数群
 */

const CaseConverter = {
  /**
   * キャメルケースをスネークケースに変換
   * @param {string} str - 変換対象の文字列
   * @returns {string} スネークケースに変換された文字列
   */
  camelToSnake: function(str) {
    if (!str || typeof str !== 'string') return '';
    return str
      .replace(/([A-Z])/g, '_$1')
      .toLowerCase()
      .replace(/^_/, '');
  },

  /**
   * パスカルケースをスネークケースに変換
   * @param {string} str - 変換対象の文字列
   * @returns {string} スネークケースに変換された文字列
   */
  pascalToSnake: function(str) {
    if (!str || typeof str !== 'string') return '';
    return str
      .replace(/([A-Z])/g, '_$1')
      .toLowerCase()
      .replace(/^_/, '');
  },

  /**
   * ケバブケースをスネークケースに変換
   * @param {string} str - 変換対象の文字列
   * @returns {string} スネークケースに変換された文字列
   */
  kebabToSnake: function(str) {
    if (!str || typeof str !== 'string') return '';
    return str
      .replace(/-/g, '_')
      .toLowerCase();
  },

  /**
   * 任意の文字列をスネークケースに変換(汎用版)
   * @param {string} str - 変換対象の文字列
   * @returns {string} スネークケースに変換された文字列
   */
  toSnakeCase: function(str) {
    if (!str || typeof str !== 'string') return '';
    
    return str
      .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
      .replace(/([a-z\d])([A-Z])/g, '$1_$2')
      .replace(/[-\s]+/g, '_')
      .replace(/_+/g, '_')
      .replace(/^_+|_+$/g, '')
      .toLowerCase();
  },

  /**
   * オブジェクトのキーをすべてスネークケースに変換
   * @param {Object} obj - 変換対象のオブジェクト
   * @returns {Object} キーがスネークケースに変換された新しいオブジェクト
   */
  objectKeysToSnake: function(obj) {
    if (typeof obj !== 'object' || obj === null) return obj;
    
    if (Array.isArray(obj)) {
      return obj.map(item => this.objectKeysToSnake(item));
    }
    
    return Object.keys(obj).reduce((acc, key) => {
      const snakeKey = this.toSnakeCase(key);
      acc[snakeKey] = this.objectKeysToSnake(obj[key]);
      return acc;
    }, {});
  }
};

// 使用例
console.log(CaseConverter.camelToSnake('userName'));           // 'user_name'
console.log(CaseConverter.pascalToSnake('UserName'));          // 'user_name'
console.log(CaseConverter.kebabToSnake('user-name'));          // 'user_name'
console.log(CaseConverter.toSnakeCase('userName'));            // 'user_name'
console.log(CaseConverter.toSnakeCase('UserName'));            // 'user_name'
console.log(CaseConverter.toSnakeCase('user-name'));           // 'user_name'

// オブジェクトの変換例
const data = { userName: 'Taro', addressInfo: { zipCode: '123' } };
console.log(CaseConverter.objectKeysToSnake(data));
// { user_name: 'Taro', address_info: { zip_code: '123' } }
JavaScript

オブジェクト化するメリット

関数をオブジェクト(CaseConverter)にまとめることで、以下のような利点があります。

  • 名前空間の汚染を防ぐ:グローバル変数と競合しにくくなります。
  • コードの見通しが良くなる:CaseConverter.camelToSnake()と書くことで、文字列のケース変換であることが明確になります。
  • 保守性が向上する:関連する関数が一箇所に集約されるため、修正や追加が容易になります。
  • thisによる相互参照:objectKeysToSnake内でthis.toSnakeCaseを呼び出すことで、同じオブジェクト内の他のメソッドを利用できます。

まとめ

スネークケースへの変換は、データベース連携やシステム間連携、ファイル名生成など、実務で非常に重要な処理です。今回紹介したように、キャメルケース・パスカルケース・ケバブケースという異なる形式から、一貫したスネークケースへの変換を実現できます。

特に重要なのは、「入力値の検証を必ず行う」こと、「文字列は変更不可であることを理解する」こと、そして「連続した大文字や日本語などの特殊なケースへの配慮」の3点です。これらを押さえておけば、実務で発生する様々なケースに対応できるようになります。

まずは基本形のcamelToSnakeやpascalToSnakeから使い始め、必要に応じて汎用的なtoSnakeCaseやオブジェクト一括変換に拡張していくことをお勧めします。チームで共有するユーティリティとして整備することで、コードの一貫性と生産性が大幅に向上するはずです。

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