JavaScript SetをExcelデータ処理で活用する実践ガイド|重複排除から集合演算まで

未分類

JavaScript Set の実践的な使い方|業務データ処理パターン完全解説

1. Set の基本解説

JavaScriptの Set は、重複のない値を管理するための組み込みオブジェクトです。配列に比べて高速な要素検索が可能で、特にデータ処理業務では非常に便利です。

Set の基本的な特徴:

  • 重複した値を自動的に排除
  • 任意のデータ型の値を格納可能
  • 順序は挿入順を保持
  • has() メソッドで O(1) の高速検索が可能

配列と比較した場合、includes() は O(n) の線形探索であるのに対し、Set の has() は O(1) の定数時間で検索完了します。データ量が多い場合、この差は顕著です。

2. 実務で頻出するユースケース

2-1. 販売データから重複顧客を除外する

営業チームから受け取った日次販売データには、同じ顧客が複数行に渡って記録されることがあります。例えば、顧客IDが重複している場合、ユニークな顧客数を正確に把握する必要があります。

// 販売データのサンプル
const salesData = [
  { date: '2024-01-15', customerId: 'C001', amount: 5000 },
  { date: '2024-01-15', customerId: 'C002', amount: 3000 },
  { date: '2024-01-15', customerId: 'C001', amount: 2000 },
  { date: '2024-01-16', customerId: 'C003', amount: 7000 },
  { date: '2024-01-16', customerId: 'C002', amount: 1500 },
];

// ユニークな顧客IDを取得
const uniqueCustomerIds = new Set(salesData.map(record => record.customerId));
console.log(`本日のユニーク顧客数: ${uniqueCustomerIds.size}`); // 3

// 重複を除いた配列に変換
const uniqueCustomersArray = Array.from(uniqueCustomerIds);
console.log(uniqueCustomersArray); // ['C001', 'C002', 'C003']

2-2. マスタデータとの照合(差分抽出)

在庫管理システムでは、実地棚卸で見つかった商品と、システムに登録されている商品の差分を抽出する必要があります。Set を使うことで、次のような処理が効率化されます。

// システムに登録されている商品マスタ
const masterProductIds = new Set(['P001', 'P002', 'P003', 'P004', 'P005']);

// 実地棚卸で確認された商品
const actualInventory = ['P001', 'P002', 'P004', 'P006', 'P007'];
const actualSet = new Set(actualInventory);

// システムにあるが実際にない商品(廃棄または紛失)
const missing = Array.from(masterProductIds).filter(id => !actualSet.has(id));
console.log('廃棄・紛失商品:', missing); // ['P003', 'P005']

// システムにないが実際にある商品(新規入荷など)
const unexpected = Array.from(actualSet).filter(id => !masterProductIds.has(id));
console.log('予期しない商品:', unexpected); // ['P006', 'P007']

2-3. 複数のリストの共通項を見つける

複数の部門から異なる形式でデータを受け取った場合、共通するユーザーを特定する必要があります。営業リストと問い合わせリストの両方に含まれるメールアドレスを抽出する例:

// 営業部が保有するリード
const salesLeads = ['user01@example.com', 'user02@example.com', 'user03@example.com', 'user04@example.com'];

// カスタマーサポートが受け取った問い合わせメール
const supportInquiries = ['user02@example.com', 'user03@example.com', 'user05@example.com'];

// 両方に含まれるメールアドレス(既にリードでありながら問い合わせもある客)
const salesSet = new Set(salesLeads);
const commonCustomers = supportInquiries.filter(email => salesSet.has(email));
console.log('既存リードからの問い合わせ:', commonCustomers); // ['user02@example.com', 'user03@example.com']

3. 実装コード|実務で即使える関数

3-1. 汎用的な重複排除関数

// TypeScript版
type DataRecord = Record;

/**
 * 配列から指定されたフィールドの重複を排除
 * @param data - 元のデータ配列
 * @param fieldName - 重複チェック対象のフィールド名
 * @returns 重複を排除したデータ配列
 */
function deduplicateByField(
  data: T[],
  fieldName: keyof T
): T[] {
  const seen = new Set();
  return data.filter(record => {
    const value = record[fieldName];
    if (seen.has(value)) {
      return false;
    }
    seen.add(value);
    return true;
  });
}

// 使用例
const orders = [
  { id: 1, customerId: 'C001', amount: 5000 },
  { id: 2, customerId: 'C002', amount: 3000 },
  { id: 3, customerId: 'C001', amount: 2000 },
];

const deduplicatedOrders = deduplicateByField(orders, 'customerId');
console.log(deduplicatedOrders);
// [{ id: 1, customerId: 'C001', amount: 5000 }, { id: 2, customerId: 'C002', amount: 3000 }]

3-2. データの差分比較クラス

/**
 * 2つのデータセット間の差分を管理するクラス
 */
class DatasetDifference {
  private setA: Set;
  private setB: Set;

  constructor(dataA: T[], dataB: T[]) {
    this.setA = new Set(dataA);
    this.setB = new Set(dataB);
  }

  // A にのみ存在
  onlyInA(): T[] {
    return Array.from(this.setA).filter(item => !this.setB.has(item));
  }

  // B にのみ存在
  onlyInB(): T[] {
    return Array.from(this.setB).filter(item => !this.setA.has(item));
  }

  // 両方に存在(共通項)
  intersection(): T[] {
    return Array.from(this.setA).filter(item => this.setB.has(item));
  }

  // どちらかに存在(和集合)
  union(): T[] {
    return Array.from(new Set([...this.setA, ...this.setB]));
  }
}

// 使用例
const before = ['A', 'B', 'C', 'D'];
const after = ['B', 'C', 'E', 'F'];
const diff = new DatasetDifference(before, after);

console.log('削除された項目:', diff.onlyInA()); // ['A', 'D']
console.log('新規追加項目:', diff.onlyInB()); // ['E', 'F']
console.log('共通項目:', diff.intersection()); // ['B', 'C']
console.log('全体:', diff.union()); // ['A', 'B', 'C', 'D', 'E', 'F']

3-3. オブジェクト参照用の Set

複雑なオブジェクトを管理する場合、ID ベースで重複チェックを行う必要があります。

/**
 * IDベースでオブジェクトの重複を排除する関数
 */
function deduplicateObjects(objects, idField = 'id') {
  const idSet = new Set();
  const result = [];

  for (const obj of objects) {
    const id = obj[idField];
    if (!idSet.has(id)) {
      idSet.add(id);
      result.push(obj);
    }
  }

  return result;
}

// 使用例:複数のAPI応答をマージした後、重複を排除
const apiResponse1 = [
  { id: 1, name: '太郎', department: '営業' },
  { id: 2, name: '花子', department: '企画' },
];

const apiResponse2 = [
  { id: 2, name: '花子', department: '企画' }, // 重複
  { id: 3, name: '次郎', department: 'IT' },
];

const merged = [...apiResponse1, ...apiResponse2];
const unique = deduplicateObjects(merged, 'id');
console.log(unique);
// [
//   { id: 1, name: '太郎', department: '営業' },
//   { id: 2, name: '花子', department: '企画' },
//   { id: 3, name: '次郎', department: 'IT' }
// ]

4. よくある応用パターン

4-1. キャッシュの実装

処理済みのユーザーIDをキャッシュして、重複処理を避ける場合に Set が活躍します。

class ProcessingCache {
  constructor(maxSize = 10000) {
    this.cache = new Set();
    this.maxSize = maxSize;
  }

  has(userId) {
    return this.cache.has(userId);
  }

  add(userId) {
    // キャッシュサイズが上限に達した場合、最初の要素を削除
    if (this.cache.size >= this.maxSize) {
      const firstItem = this.cache.values().next().value;
      this.cache.delete(firstItem);
    }
    this.cache.add(userId);
  }

  clear() {
    this.cache.clear();
  }
}

// 使用例:バッチ処理で同じユーザーを何度も処理しない
const cache = new ProcessingCache();
const userIds = [1, 2, 1, 3, 2, 4, 1];

for (const userId of userIds) {
  if (!cache.has(userId)) {
    console.log(`ユーザー ${userId} を処理中`);
    // 実際の処理
    cache.add(userId);
  }
}
// 出力: ユーザー 1 を処理中 / ユーザー 2 を処理中 / ユーザー 3 を処理中 / ユーザー 4 を処理中

4-2. リアルタイムイベント追跡

Web ソケットやストリーミングデータで、アクティブなユーザーセッションを管理する場合:

class ActiveSessionManager {
  constructor() {
    this.activeSessions = new Set();
    this.sessionStartTime = new Map();
  }

  // ユーザーがログイン
  userLoggedIn(sessionId) {
    this.activeSessions.add(sessionId);
    this.sessionStartTime.set(sessionId, Date.now());
    console.log(`セッション ${sessionId} がアクティブ(合計: ${this.activeSessions.size})`);
  }

  // ユーザーがログアウト
  userLoggedOut(sessionId) {
    this.activeSessions.delete(sessionId);
    this.sessionStartTime.delete(sessionId);
    console.log(`セッション ${sessionId} が終了(合計: ${this.activeSessions.size})`);
  }

  // アクティブユーザー数を取得
  getActiveCount() {
    return this.activeSessions.size;
  }

  // タイムアウトしたセッションを自動削除
  cleanupTimeoutSessions(timeoutMs = 30 * 60 * 1000) {
    const now = Date.now();
    const sessionsToDelete = [];

    for (const [sessionId, startTime] of this.sessionStartTime) {
      if (now - startTime > timeoutMs) {
        sessionsToDelete.push(sessionId);
      }
    }

    sessionsToDelete.forEach(sessionId => this.userLoggedOut(sessionId));
    return sessionsToDelete.length;
  }
}

// 使用例
const manager = new ActiveSessionManager();
manager.userLoggedIn('session_001');
manager.userLoggedIn('session_002');
manager.userLoggedIn('session_003');
console.log(`現在のアクティブセッション: ${manager.getActiveCount()}`); // 3
manager.userLoggedOut('session_002');
console.log(`現在のアクティブセッション: ${manager.getActiveCount()}`); // 2

4-3. Excelデータの一括処理

CSV/Excel から読み込んだデータで重複チェックと集計を行う実例:

/**
 * 顧客マスタとインポートデータを比較し、新規顧客を抽出
 */
function extractNewCustomers(masterData, importData) {
  // マスタの顧客IDを Set に変換
  const masterCustomerIds = new Set(masterData.map(row => row.customerId));

  // インポートデータから新規顧客を抽出
  const newCustomers = [];
  const seenInImport = new Set();

  for (const row of importData) {
    const customerId = row.customerId;

    // インポート内での重複チェック
    if (seenInImport.has(customerId)) {
      console.warn(`警告: インポートファイル内に重複があります (ID: ${customerId})`);
      continue;
    }

    // マスタに未登録
    if (!masterCustomerIds.has(customerId)) {
      newCustomers.push(row);
      seenInImport.add(customerId);
    }
  }

  return newCustomers;
}

// 使用例
const masterCustomers = [
  { customerId: 'C001', name: '顧客A' },
  { customerId: 'C002', name: '顧客B' },
];

const importCustomers = [
  { customerId: 'C002', name: '顧客B' }, // 既存
  { customerId: 'C003', name: '顧客C' }, // 新規
  { customerId: 'C004', name: '顧客D' }, // 新規
  { customerId: 'C004', name: '顧客D' }, // インポート内で重複
];

const newCustomers = extractNewCustomers(masterCustomers, importCustomers);
console.log('新規顧客:', newCustomers);
// [{ customerId: 'C003', name: '顧客C' }, { customerId: 'C004', name: '顧客D' }]

5. Python での実装参考

JavaScriptと同じロジックを Python で実装した例も参考に:

from typing import List, Set, Dict, Any

class DataProcessor:
    """データ処理を行うクラス(Python版)"""

    @staticmethod
    def deduplicate_by_field(data: List[Dict], field_name: str) -> List[Dict]:
        """
        指定フィールドで重複排除
        """
        seen = set()
        result = []
        for record in data:
            value = record.get(field_name)
            if value not in seen:
                seen.add(value)
                result.append(record)
        return result

    @staticmethod
    def find_difference(set_a: Set, set_b: Set) -> Dict[str, List]:
        """
        2つのセット間の差分を取得
        """
        return {
            'only_in_a': list(set_a - set_b),
            'only_in_b': list(set_b - set_a),
            'common': list(set_a & set_b),
            'union': list(set_a | set_b)
        }

# 使用例
if __name__ == '__main__':
    # 重複排除の例
    sales_data = [
        {'customer_id': 'C001', 'amount': 5000},
        {'customer_id': 'C002', 'amount': 3000},
        {'customer_id': 'C001', 'amount': 2000},
    ]
    deduplicated = DataProcessor.deduplicate_by_field(sales_data, 'customer_id')
    print('重複排除後:', deduplicated)

    # 差分比較の例
    master_products = {'P001', 'P002', 'P003', 'P004'}
    actual_inventory = {'P001', 'P002', 'P005'}
    diff = DataProcessor.find_difference(master_products, actual_inventory)
    print('差分:', diff)

6. 注意点と落とし穴

6-1. オブジェクトの比較に注意

Set は値の比較に === を使用するため、オブジェクトは参照によって比較されます。内容が同じでも異なるオブジェクトインスタンスは別物として扱われます。

// 注意:以下は期待通りに動きません
const set = new Set();
const obj1 = { id: 1, name: '太郎' };
const obj2 = { id: 1, name: '太郎' }; // 同じ内容だが異なるインスタンス

set.add(obj1);
set.add(obj2);

console.log(set.size); // 2(重複として認識されていない)

// 対策:ID など比較可能な値を使用する
const userIds = new Set();
userIds.add(obj1.id);
userIds.add(obj2.id);
console.log(userIds.size); // 1(正しく重複排除されている)

6-2. パフォーマンス:メモリ使用量

Set は便利ですが、大量のデータを保持する場合、メモリ使用量に注意が必要です。特にバッチ処理では定期的にクリアすることを検討してください。

// 長時間実行するプロセスでメモリリークを防ぐ
class BatchProcessor {
  constructor(batchSize = 10000) {
    this.processedIds = new Set();
    this.batchSize = batchSize;
  }

  process(userId) {
    if (this.processedIds.has(userId)) {
      return; // スキップ
    }

    // 処理実行
    this.doSomething(userId);
    this.processedIds.add(userId);

    // バッチサイズに達したらクリア
    if (this.processedIds.size >= this.batchSize) {
      this.processedIds.clear();
    }
  }

  doSomething(userId) {
    // 実際の処理
  }
}

6-3. JSON シリアライズに非対応

Set は直接 JSON に変換できません。配列に変換してから JSON 化する必要があります。

const set = new Set(['A', 'B', 'C']);

// 誤り
console.log(JSON.stringify(set)); // 結果: {}

// 正し方法
const jsonString = JSON.stringify(Array.from(set));
console.log(jsonString); // ["A","B","C"]

// カスタム replacer を使う方法
const customJson = JSON.stringify(
  { items: set },
  (key, value) => value instanceof Set ? Array.from(value) : value
);
console.log(customJson); // {"items":["A","B","C"]}

6-4. ブラウザ互換性

Set は比較的新しい機能(ES2015)ですが、現在のすべての主要ブラウザで対応しています。ただしレガシーシステムで古いブラウザをサポートする必要がある場合は、ポリフィルの導入を検討してください。

7. まとめ

JavaScript の Set は、業務データ処理において以下のような場面で非常に有効です:

  • 重複排除:配列から一意の値を効率的に抽出
  • 高速検索:大量データの中から特定の値を素早く確認
  • 差分抽出:2つのデータセット間の違いを明確に把握
  • キャッシュ管理:処理済みアイテムの追跡
  • リアルタイムデータ管理:アクティブユーザーなどの動的な集合を管理

注意点としては、オブジェクト参照による比較の仕様を理解し、JSON シリアライズが必要な場合は事前に配列に変換すること、また長時間実行するプロセスではメモリ管理に気を付けることが重要です。

配列の includes() や filter() に比べて処理が速く、コードも簡潔に書けるため、データ量が少ないうちから Set を活用する習慣をつけることをお勧めします。業務システム開発において、Set をうまく活用することで、バグが少なく効率的なコードが実現できます。

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