filterの実務パターン解説|業務システムで実装するデータ絞り込み処理

未分類

filterの実務パターン解説|業務システムで実装するデータ絞り込み処理

1. filterメソッドの基本解説

filterは配列やコレクションから特定の条件を満たす要素のみを抽出するメソッドです。業務システムでは、データベースから取得したレコードのクライアントサイド処理、APIレスポンスのフィルタリング、ログデータの抽出など、あらゆる場面で活躍します。

簡潔な構文で読みやすいコードが書けるため、複数の条件判定が必要な場面でも保守性が高まります。また、関数型プログラミングのアプローチを採用することで、テストしやすい設計になるのも特徴です。

2. 業務でのユースケース

2-1. ユーザー権限に基づくデータフィルタリング

実務では、ユーザーの権限レベルに応じて表示するデータを制限する必要があります。例えば、営業部門は自分の案件のみ表示し、マネージャーは部門全体の案件を表示するといった要件が多くあります。

2-2. ステータス別のタスク抽出

業務管理システムでは、タスクの進捗状況(未着手、進行中、完了など)でフィルタリングして表示することが頻繁に発生します。複数のステータスで同時にフィルタリングすることもあり、複雑な条件判定が必要になります。

2-3. 期間指定での売上データ抽出

財務システムでは、指定された期間内の売上データのみを抽出する処理が必須です。日付範囲、部門、商品カテゴリなど複数の条件を組み合わせることが一般的です。

2-4. 不正データの検出と除外

データ品質管理では、不正な形式のデータやNULL値を含むレコードを除外する必要があります。filterを使うことで、クリーニング処理をシンプルに実装できます。

3. 実装コード

3-1. TypeScriptでの実装パターン

基本的なフィルタリング

// ユーザー型の定義
interface User {
  id: number;
  name: string;
  department: string;
  status: 'active' | 'inactive' | 'suspended';
  joinDate: Date;
  salary: number;
}

// シンプルなフィルタリング:アクティブなユーザーのみ取得
const activeUsers = users.filter(user => user.status === 'active');

// 複数条件でのフィルタリング:営業部で給与が50万以上
const targetUsers = users.filter(user => 
  user.department === '営業部' && user.salary >= 500000
);

// 日付範囲でのフィルタリング
const joinedThisYear = users.filter(user => {
  const currentYear = new Date().getFullYear();
  return user.joinDate.getFullYear() === currentYear;
});

複雑な業務ロジックの実装

// 売上レコードの型定義
interface SalesRecord {
  id: string;
  salesPersonId: number;
  amount: number;
  date: Date;
  status: 'completed' | 'pending' | 'cancelled';
  category: string;
  clientType: 'new' | 'existing';
}

// 複雑な条件:今月の確定売上で金額が100万以上、既存顧客のみ
function getConfirmedMonthlySales(
  records: SalesRecord[],
  minAmount: number = 1000000
): SalesRecord[] {
  const now = new Date();
  const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
  const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);

  return records.filter(record => {
    const isThisMonth = record.date >= firstDay && record.date <= lastDay;
    const isConfirmed = record.status === 'completed';
    const isAboveThreshold = record.amount >= minAmount;
    const isExistingClient = record.clientType === 'existing';

    return isThisMonth && isConfirmed && isAboveThreshold && isExistingClient;
  });
}

// 使用例
const confirmedSales = getConfirmedMonthlySales(salesRecords, 1000000);

フィルタリング条件を外部で管理するパターン

// フィルター条件インターフェース
interface FilterCriteria {
  status?: string[];
  department?: string[];
  salaryRange?: { min: number; max: number };
  joinDateAfter?: Date;
}

// 汎用的なフィルタリング関数
function filterUsers(
  users: User[],
  criteria: FilterCriteria
): User[] {
  return users.filter(user => {
    // ステータス条件
    if (criteria.status && !criteria.status.includes(user.status)) {
      return false;
    }

    // 部門条件
    if (criteria.department && !criteria.department.includes(user.department)) {
      return false;
    }

    // 給与範囲条件
    if (criteria.salaryRange) {
      const { min, max } = criteria.salaryRange;
      if (user.salary < min || user.salary > max) {
        return false;
      }
    }

    // 入社日条件
    if (criteria.joinDateAfter && user.joinDate < criteria.joinDateAfter) {
      return false;
    }

    return true;
  });
}

// 使用例:営業部でアクティブな社員、給与50万以上
const result = filterUsers(users, {
  status: ['active'],
  department: ['営業部'],
  salaryRange: { min: 500000, max: Infinity }
});

3-2. Pythonでの実装パターン

基本的なフィルタリング

from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional

@dataclass
class SalesData:
    transaction_id: str
    amount: float
    date: datetime
    status: str
    region: str
    product_id: str

# リスト内包表記を使ったシンプルなフィルタリング
def get_completed_sales(sales_list: List[SalesData]) -> List[SalesData]:
    return [s for s in sales_list if s.status == 'completed']

# filter関数を使った実装
def get_high_value_sales(sales_list: List[SalesData], threshold: float) -> List[SalesData]:
    return list(filter(lambda s: s.amount >= threshold, sales_list))

# 複数条件でのフィルタリング
def get_regional_sales(
    sales_list: List[SalesData],
    region: str,
    min_amount: float
) -> List[SalesData]:
    return [
        s for s in sales_list
        if s.region == region and s.amount >= min_amount and s.status == 'completed'
    ]

複雑な業務ロジックの実装

from datetime import datetime, timedelta
from enum import Enum

class SalesStatus(Enum):
    COMPLETED = 'completed'
    PENDING = 'pending'
    CANCELLED = 'cancelled'

class Region(Enum):
    KANTO = 'kanto'
    KANSAI = 'kansai'
    TOHOKU = 'tohoku'

# 月間売上分析クラス
class MonthlySalesAnalyzer:
    def __init__(self, sales_data: List[SalesData]):
        self.sales_data = sales_data

    def get_target_sales(
        self,
        target_region: str,
        target_month: int,
        target_year: int,
        min_amount: float = 0,
        exclude_products: Optional[List[str]] = None
    ) -> List[SalesData]:
        \"\"\"特定の地域・月の売上を条件付きで取得\"\"\"
        
        exclude_products = exclude_products or []
        
        def is_target_sales(sale: SalesData) -> bool:
            # 日付チェック
            if sale.date.month != target_month or sale.date.year != target_year:
                return False
            
            # 地域チェック
            if sale.region != target_region:
                return False
            
            # 金額チェック
            if sale.amount < min_amount:
                return False
            
            # ステータスチェック
            if sale.status != SalesStatus.COMPLETED.value:
                return False
            
            # 除外商品チェック
            if sale.product_id in exclude_products:
                return False
            
            return True
        
        return list(filter(is_target_sales, self.sales_data))

    def get_sales_by_multiple_regions(
        self,
        regions: List[str],
        days_back: int = 30
    ) -> List[SalesData]:
        \"\"\"複数地域の過去N日間の売上を取得\"\"\"
        cutoff_date = datetime.now() - timedelta(days=days_back)
        
        return [
            s for s in self.sales_data
            if s.region in regions
            and s.date >= cutoff_date
            and s.status == SalesStatus.COMPLETED.value
        ]

データクリーニングでのフィルタリング

import re
from typing import Callable

class DataValidator:
    @staticmethod
    def is_valid_email(email: str) -> bool:
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
        return re.match(pattern, email) is not None

    @staticmethod
    def is_valid_phone(phone: str) -> bool:
        # 日本の電話番号形式チェック
        cleaned = phone.replace('-', '').replace(' ', '')
        return cleaned.isdigit() and len(cleaned) >= 10

class CustomerRecord:
    def __init__(self, id: str, name: str, email: str, phone: str):
        self.id = id
        self.name = name
        self.email = email
        self.phone = phone

def clean_customer_data(customers: List[CustomerRecord]) -> List[CustomerRecord]:
    \"\"\"不正なデータを除外したクリーンなカスタマーリストを取得\"\"\"
    validator = DataValidator()
    
    return [
        c for c in customers
        if c.name and len(c.name.strip()) > 0  # 名前が空でない
        and validator.is_valid_email(c.email)  # メール形式が正しい
        and validator.is_valid_phone(c.phone)  # 電話形式が正しい
        and not c.email.endswith('@example.com')  # テスト用メールを除外
    ]

# 使用例
cleaned = clean_customer_data(raw_customer_data)
print(f\"元のデータ: {len(raw_customer_data)}件\")\nprint(f\"クリーン後: {len(cleaned)}件\")

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

4-1. チェーン処理(複数のフィルタリングを順序立てて実行)

// TypeScript例:チェーン処理
interface Order {
  id: string;
  customerId: number;
  amount: number;
  status: string;
  createdAt: Date;
  shippingRegion: string;
}

const orders: Order[] = [/* ... */];

// 段階的なフィルタリング
const highValueOrders = orders
  .filter(order => order.status === 'completed')  // ステップ1:完了したオーダーのみ
  .filter(order => order.amount >= 100000)  // ステップ2:高額オーダーのみ
  .filter(order => {  // ステップ3:今月のオーダーのみ
    const now = new Date();
    const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
    return order.createdAt >= firstDay;
  });

// または統合版
const processedOrders = orders
  .filter(order => {
    const isCompleted = order.status === 'completed';
    const isHighValue = order.amount >= 100000;
    const isThisMonth = order.createdAt >= new Date(new Date().getFullYear(), new Date().getMonth(), 1);
    
    return isCompleted && isHighValue && isThisMonth;
  });

4-2. map + filterの組み合わせ

// filterしてからmapで変形
interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
}

interface CartItem {
  productName: string;
  price: number;
}

const products: Product[] = [/* ... */];

// 在庫ありの商品のみをカート用に変形
const availableCartItems: CartItem[] = products
  .filter(product => product.stock > 0)  // 在庫ありのものをフィルタリング
  .map(product => ({  // マッピングで不要なフィールドを除外
    productName: product.name,
    price: product.price
  }));

4-3. 条件分岐との組み合わせ

from typing import List, Dict, Any

def categorize_and_filter_orders(
    orders: List[Dict[str, Any]],
    filter_type: str
) -> List[Dict[str, Any]]:
    \"\"\"フィルタータイプに応じて異なるフィルタリングを実行\"\"\"
    
    filter_conditions = {
        'pending': lambda o: o['status'] == 'pending',
        'recent': lambda o: (datetime.now() - o['created_at']).days <= 7,
        'high_value': lambda o: o['amount'] >= 500000,
        'urgent': lambda o: o['status'] == 'pending' and (datetime.now() - o['created_at']).days > 3,
    }
    
    condition = filter_conditions.get(filter_type, lambda x: True)
    return list(filter(condition, orders))

4-4. キャッシング付きフィルタリング

// パフォーマンス最適化:フィルタリング結果をキャッシュ
class OrderFilterService {
  private cache: Map = new Map();
  private cacheTimeout: number = 5 * 60 * 1000; // 5分

  filterOrders(
    orders: Order[],
    filterKey: string,
    filterFn: (order: Order) => boolean
  ): Order[] {
    const cacheKey = `${filterKey}_${orders.length}`;
    
    // キャッシュがあれば返す
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey)!;
    }
    
    // フィルタリング実行
    const result = orders.filter(filterFn);
    
    // キャッシュに保存
    this.cache.set(cacheKey, result);
    
    // タイムアウト設定
    setTimeout(() => this.cache.delete(cacheKey), this.cacheTimeout);
    
    return result;
  }
}

5. 注意点とベストプラクティス

5-1. パフォーマンス問題

問題点:大規模データセット(10万件以上)に対してfilterを複数回チェーンするとメモリとCPU負荷が増加します。

対策:

  • フィルタリング条件を統合して一度のループで済ませる
  • 大規模データはデータベース側でフィルタリングする(SQLのWHERE句を活用)
  • 必要に応じて結果をページングして取得する
  • 頻繁に使う条件はインデックスを活用
// 非効率な例(複数回のループ)
const result = data
  .filter(item => item.status === 'active')  // ループ1
  .filter(item => item.amount > 1000)  // ループ2
  .filter(item => item.date > cutoff)  // ループ3
  .map(item => ({ id: item.id, name: item.name }));  // ループ4

// 改善した例(1回のループ)
const result = data
  .filter(item => 
    item.status === 'active' && 
    item.amount > 1000 && 
    item.date > cutoff
  )
  .map(item => ({ id: item.id, name: item.name }));

5-2. NULL・未定義値の処理

問題点:filterが予期しないNULLやundefinedを含むデータを処理すると、エラーや想定外の結果が発生します。

対策:

// 安全なフィルタリング
const safeFilter = (items: (Item | null | undefined)[]): Item[] => {
  return items
    .filter((item): item is Item => item !== null && item !== undefined)
    .filter(item => item.amount > 0);
};

// 別のアプローチ:Optional Chainingを活用
const filteredItems = items.filter(item => {
  return item?.status === 'active' && (item?.amount ?? 0) > 1000;
});

5-3. イミュータビリティの考慮

重要:filterは新しい配列を返すため、元の配列は変更されません。これは副作用を避ける点で優れていますが、状態管理ライブラリを使う場合は注意が必要です。

// 良い例:新しい配列を返す
const users = [/* ... */];
const activeUsers = users.filter(u => u.status === 'active');
// users は変更されない

// 悪い例:配列を直接編集してはいけない
users = users.filter(u => u.status === 'active');  // 新しい参照を作成

5-4. 複雑な条件判定は関数に分離する

ベストプラクティス:判定ロジックが複雑な場合は、別関数に分離して可読性とテスタビリティを向上させます。

// 悪い例:判定ロジックがインラインで複雑
const filtered = data.filter(item => {
  return item.status === 'active' && 
         item.amount > 1000 && 
         item.date > cutoff &&\n         !item.isBlacklisted &&\n         item.region.includes('Tokyo') &&\n         (item.type === 'premium' || item.loyaltyPoints > 1000);\n});\n\n// 良い例:判定ロジックを関数化\nfunction isQualifiedForPromotion(item: Item): boolean {\n  const isActive = item.status === 'active';\n  const isHighValue = item.amount > 1000;\n  const isRecent = item.date > cutoff;\n  const isNotBlacklisted = !item.isBlacklisted;\n  const isInTarget = item.region.includes('Tokyo');\n  const isPremium = item.type === 'premium' || item.loyaltyPoints > 1000;\n\n  return isActive && isHighValue && isRecent && isNotBlacklisted && isInTarget && isPremium;\n}\n\nconst filtered = data.filter(isQualifiedForPromotion);

5-5. テストの重要性

// filterを使った関数のテスト例\nimport { describe, it, expect } from '@jest/globals';\n\ndescribe('filterUsers', () => {\n  const users: User[] = [\n    { id: 1, name: 'Alice', department: '営業部', status: 'active', joinDate: new Date('2023-01-01'), salary: 600000 },\n    { id: 2, name: 'Bob', department: '企画部', status: 'active', joinDate: new Date('2024-01-01'), salary: 550000 },\n    { id: 3, name: 'Charlie', department: '営業部', status: 'inactive', joinDate: new Date('2022-01-01'), salary: 500000 },\n  ];\n\n  it('営業部でアクティブなユーザーのみを返す', () => {\n    const result = filterUsers(users, {\n      department: ['営業部'],\n      status: ['active']\n    });\n    expect(result).toHaveLength(1);\n    expect(result[0].name).toBe('Alice');\n  });\n\n  it('複数部門でのフィルタリングに対応', () => {\n    const result = filterUsers(users, {\n      department: ['営業部', '企画部']\n    });\n    expect(result).toHaveLength(3);\n  });\n\n  it('条件がない場合は全件返す', () => {\n    const result = filterUsers(users, {});\n    expect(result).toHaveLength(3);\n  });\n});

まとめ

filterメソッドは、業務システムにおいて最も頻繁に使用される配列操作の一つです。シンプルな条件判定から複雑なビジネスロジックまで、様々な場面で活躍します。

実務で成功するためのポイントは以下の通りです:

  • 可読性優先:複雑な判定は関数に分離する
  • パフォーマンス意識:大規模データはDBで処理、チェーンは統合する
  • 安全性確保:NULL値や型の不一致をハンドルする
  • テスト駆動:エッジケースをカバーしたテストを書く
  • 保守性重視:条件判定の変更に強い設計を心がける

これらのパターンと注意点を実装に活かすことで、堅牢で保守性の高いコードが実現できます。

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