🙆♀️
Array.reduce()を使った重複排除と最新レコード抽出のテクニック
はじめに
データ処理でよくある課題として、「同じIDを持つレコードが複数存在する場合、最新のもののみを取得したい」というケースがあります。今回は、Array.reduce()を使った効率的な解決方法を紹介します。
実際のケース:リトライ処理での最新レコード抽出
問題設定
以下のような「商品の処理履歴データ」があるとします:
interface ProcessedItem {
productId: string;
retrySequence: number; // 0=初回, 1=1回目再処理, 2=2回目再処理...
status: 'COMPLETED' | 'FAILED';
processedAt: Date;
}
const items: ProcessedItem[] = [
{ productId: "PROD001", retrySequence: 0, status: "FAILED", processedAt: new Date('2024-01-01') },
{ productId: "PROD002", retrySequence: 0, status: "COMPLETED", processedAt: new Date('2024-01-01') },
{ productId: "PROD001", retrySequence: 1, status: "FAILED", processedAt: new Date('2024-01-02') },
{ productId: "PROD001", retrySequence: 2, status: "COMPLETED", processedAt: new Date('2024-01-03') },
{ productId: "PROD002", retrySequence: 1, status: "COMPLETED", processedAt: new Date('2024-01-02') }
];
欲しい結果:各productIdについて、最新のretrySequenceを持つレコードのみを取得
解決方法:reduceを使った重複排除
const latestItems = items.reduce(
(acc, item) => {
const key = item.productId;
const existing = acc[key];
// 既存レコードがないか、現在のアイテムの方が新しい場合は更新
if (!existing || item.retrySequence > existing.retrySequence) {
acc[key] = item;
}
return acc;
},
{} as Record<string, ProcessedItem>
);
// オブジェクトから配列に変換
const latestItemsArray = Object.values(latestItems);
コードの詳細解説
1. 初期値の設定
{} as Record<string, ProcessedItem>
- 空のオブジェクトを初期値として設定
-
Record<string, ProcessedItem>
は「文字列をキーとし、ProcessedItemを値とするオブジェクト」の型定義
2. キーと既存レコードの取得
const key = item.productId;
const existing = acc[key];
- key:グループ化の基準となるID
- existing:既にアキュムレータに保存されている同じIDのレコード
3. 最新判定ロジック
if (!existing || item.retrySequence > existing.retrySequence) {
acc[key] = item;
}
- 条件1:
!existing
→ 初回の場合(そのIDのレコードが初めて処理される) - 条件2:
item.retrySequence > existing.retrySequence
→ 現在のレコードの方が新しい場合
実行結果
処理過程の可視化
処理順 | ID | retrySequence | 判定 | アキュムレータの状態 |
---|---|---|---|---|
1 | "PROD001" | 0 | 初回 | {"PROD001": item1} |
2 | "PROD002" | 0 | 初回 | {"PROD001": item1, "PROD002": item2} |
3 | "PROD001" | 1 | 1 > 0 | {"PROD001": item3, "PROD002": item2} |
4 | "PROD001" | 2 | 2 > 1 | {"PROD001": item4, "PROD002": item2} |
5 | "PROD002" | 1 | 1 > 0 | {"PROD001": item4, "PROD002": item5} |
最終結果
// latestItems の内容
{
"PROD001": { productId: "PROD001", retrySequence: 2, status: "COMPLETED" },
"PROD002": { productId: "PROD002", retrySequence: 1, status: "COMPLETED" }
}
他の解決方法との比較
Map + forEach を使った場合
const latestMap = new Map<string, ProcessedItem>();
items.forEach(item => {
const existing = latestMap.get(item.productId);
if (!existing || item.retrySequence > existing.retrySequence) {
latestMap.set(item.productId, item);
}
});
const latestItems = Array.from(latestMap.values());
filter + sort を使った場合(非効率)
const uniqueIds = [...new Set(items.map(item => item.productId))];
const latestItems = uniqueIds.map(id =>
items
.filter(item => item.productId === id)
.sort((a, b) => b.retrySequence - a.retrySequence)[0]
);
比較結果:
- reduce: 一度のループで処理完了、メモリ効率良好
- Map + forEach: 読みやすいが、reduceと性能はほぼ同等
- filter + sort: 読みやすいが、O(n²)で非効率
応用パターン
1. 日付での最新レコード抽出
const latestByDate = items.reduce(
(acc, item) => {
const key = item.productId;
const existing = acc[key];
if (!existing || item.processedAt > existing.processedAt) {
acc[key] = item;
}
return acc;
},
{} as Record<string, ProcessedItem>
);
2. 複数条件での最新レコード抽出
const latestByMultipleCriteria = items.reduce(
(acc, item) => {
const key = item.productId;
const existing = acc[key];
if (!existing ||
item.retrySequence > existing.retrySequence ||
(item.retrySequence === existing.retrySequence &&
item.processedAt > existing.processedAt)) {
acc[key] = item;
}
return acc;
},
{} as Record<string, ProcessedItem>
);
3. より実用的な例:注文履歴の最新ステータス取得
interface OrderStatusLog {
orderId: string;
status: 'PENDING' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED';
updatedAt: Date;
version: number;
}
const orderLogs: OrderStatusLog[] = [
{ orderId: "ORD001", status: "PENDING", updatedAt: new Date('2024-01-01'), version: 1 },
{ orderId: "ORD002", status: "PENDING", updatedAt: new Date('2024-01-01'), version: 1 },
{ orderId: "ORD001", status: "PROCESSING", updatedAt: new Date('2024-01-02'), version: 2 },
{ orderId: "ORD001", status: "SHIPPED", updatedAt: new Date('2024-01-03'), version: 3 },
{ orderId: "ORD002", status: "DELIVERED", updatedAt: new Date('2024-01-02'), version: 2 }
];
const latestOrderStatus = orderLogs.reduce(
(acc, log) => {
const key = log.orderId;
const existing = acc[key];
if (!existing || log.version > existing.version) {
acc[key] = log;
}
return acc;
},
{} as Record<string, OrderStatusLog>
);
4. 型安全性の向上
// より型安全なバージョン
const getLatestItems = <T extends { [K in keyof T]: any }>(
items: T[],
keySelector: (item: T) => string,
compareSelector: (item: T) => number
): T[] => {
const latest = items.reduce(
(acc, item) => {
const key = keySelector(item);
const existing = acc[key];
if (!existing || compareSelector(item) > compareSelector(existing)) {
acc[key] = item;
}
return acc;
},
{} as Record<string, T>
);
return Object.values(latest);
};
// 使用例
const result = getLatestItems(
items,
item => item.productId,
item => item.retrySequence
);
まとめ
Array.reduce()を使った重複排除と最新レコード抽出は、以下の特徴があります:
メリット:
- 一度のループで処理完了(O(n))
- メモリ効率が良い
- 関数型プログラミングのパラダイムに適合
注意点:
- 初見では理解しにくい場合がある
- デバッグがやや困難
使用場面:
- データベースから取得したレコードの重複排除
- APIレスポンスの最新データ抽出
- ログデータの集計処理
- 注文履歴やステータス更新の最新状態取得
この手法は実際のWebアプリケーション開発でよく使われるパターンなので、ぜひ参考にしてください。
Discussion