chunkWhile — 条件が続く間で隣接要素を塊に分割

collection
  • カテゴリ: collection
  • 対応バージョン: Laravel 12・PHP 8.2
  • 名前空間 / FQCN / コマンド: Illuminate\Support\Collection::chunkWhile
  • 関連: chunk / split / groupBy / partition / sliding
  • 変更履歴: —

要点(TL;DR)

  • 隣接する要素を「条件が崩れるまで」まとめて小さなコレクションに分割する。
  • collect($xs)->chunkWhile(fn($v,$k,$chunk) => $chunk->isEmpty() || $v - $chunk->last() <= 1)
  • : 初回要素で $chunk->isEmpty() を考慮する/未ソート入力だと期待と異なる/chunk($size)(固定サイズ)やDBのquery()->chunk()と混同しない。

概要

chunkWhile は、連続する要素に対して与えた条件が 真のあいだ 同じグループ(チャンク)へ詰め、偽になった時点で新しいチャンクを開始します。時系列データの連続区間、同一カテゴリが連続するレコード、値の変化点検出などに有効です。結果は「コレクションのコレクション」になります。

構文 / シグネチャ

use Illuminate\Support\Collection;

/**
 * @param  callable(mixed $value, mixed $key, Collection $chunk): bool  $callback
 * @return Collection<int, Collection>  // 外側: チャンク配列、内側: 各チャンク
 */
public function chunkWhile(callable $callback): Collection;

引数(表)

引数必須既定値説明
$callbackcallable(mixed $value, mixed $key, Collection $chunk): boolなし「現在の要素を今のチャンクに追加するか」を返すコールバック。false を返すと新しいチャンクを開始$chunk は「現在構築中のチャンク」。
  • 戻り値Collection<int, Collection>(外側はチャンクの配列、内側は各チャンク)。内側は元のキーを保持
  • 例外/副作用:コールバック内の例外はそのまま伝播。メモリは全要素+分割結果を保持(eager)。

コレクション特性(collectionカテゴリ規約)

  • チェーン可否: 可能(->map(), ->flatten() 等と併用)
  • 破壊性: 非破壊(元コレクションは変更しない)
  • キー保持: 内側チャンクで保持(必要に応じて ->values() で詰め直す)
  • LazyCollection: 対応collect()->lazy() / LazyCollection でも利用可、ストリーム処理で省メモリ)
  • 計算量: O(n)(コールバックは各要素1回実行)

入出力イメージ

collect([1,2,3,10,11,20])
  ->chunkWhile(/* 連続値の差が1以下 */)
==>
[
  [1,2,3],      // 1,2,3 は近接
  [10,11],      // 10,11 は近接
  [20],         // 単独
]

使用例

最小例(連続増加の塊に分割)

<?php

use Illuminate\Support\Collection;

$numbers = collect([1, 2, 3, 10, 11, 20]);

$chunks = $numbers->chunkWhile(function ($v, $k, Collection $chunk) {
    // 先頭要素は常に開始
    if ($chunk->isEmpty()) return true;
    // 「前要素との差が 1 以下なら同じ塊」にする例
    return ($v - $chunk->last()) <= 1;
});

// 出力確認([[1,2,3],[10,11],[20]])
print_r($chunks->map->values()->values()->toArray());

実務例(時系列ログを「同じ時刻帯で連続」ごとに処理)

<?php

use Illuminate\Support\Collection;
use Carbon\CarbonImmutable;

$logs = collect([
    ['ts' => CarbonImmutable::parse('2025-09-01 10:00:10'), 'level' => 'info',  'msg' => 'A'],
    ['ts' => CarbonImmutable::parse('2025-09-01 10:15:02'), 'level' => 'warn',  'msg' => 'B'],
    ['ts' => CarbonImmutable::parse('2025-09-01 10:50:21'), 'level' => 'info',  'msg' => 'C'],
    ['ts' => CarbonImmutable::parse('2025-09-01 11:01:03'), 'level' => 'error', 'msg' => 'D'],
    ['ts' => CarbonImmutable::parse('2025-09-01 11:20:00'), 'level' => 'info',  'msg' => 'E'],
]);

// 同一「時」内の連続部分で塊化し、塊ごとに集計する例
$hourlyChunks = $logs->sortBy('ts')->chunkWhile(function ($row, $k, Collection $chunk) {
    if ($chunk->isEmpty()) return true;
    return $row['ts']->isSameHour($chunk->last()['ts']);
});

$summary = $hourlyChunks->map(function (Collection $chunk) {
    $hour = $chunk->first()['ts']->format('Y-m-d H:00');
    return [
        'hour' => $hour,
        'count' => $chunk->count(),
        'levels' => $chunk->groupBy('level')->map->count()->all(),
    ];
});

print_r($summary->values()->toArray());
// 例:
// [
//   ['hour' => '2025-09-01 10:00', 'count' => 3, 'levels' => ['info'=>2,'warn'=>1]],
//   ['hour' => '2025-09-01 11:00', 'count' => 2, 'levels' => ['error'=>1,'info'=>1]],
// ]

よくある落とし穴・注意

  • 初回要素の扱い$chunk->isEmpty() を明示しないと、最初の要素が除外されるロジックになりがち。
  • 未ソート入力chunkWhile隣接にのみ作用。ソートされていないと期待と異なる塊になる。必要なら事前に ->sortBy()
  • chunk($size)との差chunk固定サイズで分割、chunkWhile条件が切り替わる点で分割。目的に応じて選択。
  • DBクエリの chunk() と混同注意:Eloquent/QueryBuilder の chunk($size) はDBから分割取得する仕組み。chunkWhileメモリ上のコレクションに対する操作。
  • メモリ:大きな配列を丸ごと扱うとメモリ圧迫。ストリーム処理が必要なら LazyCollection を検討。

代替・関連APIとの比較

  • chunk($size):固定サイズで等分割。レコード数ごとにバッチ処理したいとき。
  • groupBy($key|$callback):同じキーの要素を全体からまとめる(隣接関係は無視)。カテゴリ集計に最適。
  • partition($callback):条件に合う/合わないで2分割
  • split($number):指定数のグループへ均等に分割。
  • sliding($size, $step=1):移動窓の切り出し(オーバーラップあり)。時系列の局所計算に。

テスト例(Pest)

<?php

use Illuminate\Support\Collection;

it('chunks consecutive numbers into groups', function () {
    $xs = collect([1,2,3,10,11,20]);

    $chunks = $xs->chunkWhile(function ($v, $k, Collection $chunk) {
        if ($chunk->isEmpty()) return true;
        return ($v - $chunk->last()) <= 1;
    });

    expect($chunks->count())->toBe(3)
        ->and($chunks->values()->get(0)->values()->all())->toBe([1,2,3])
        ->and($chunks->values()->get(1)->values()->all())->toBe([10,11])
        ->and($chunks->values()->get(2)->values()->all())->toBe([20]);
});

トラブルシュート(エラー別)

症状/エラー原因対処
Call to undefined method ... chunkWhile()Laravel が古い/コレクションでない配列に対して呼び出しフレームワークを更新する/collect($array) で包む
期待通りに分割されない入力が未ソート、条件が連続性を正しく表していない->sortBy() を挟む/$chunk->isEmpty() と「直前値/先頭値」を使って条件を見直す
メモリ不足大規模データを全件保持LazyCollection でストリーム処理/DB側 chunk() で分割取得して都度処理

参考リンク

レン (Wren)

こんにちは。レンです。

Laravelのコードの森に住んでいる、小さな案内役です。
ルーティングの枝やクラスの影を歩きながら、コードの流れや仕組みを眺めています。

このサイトでは、Laravelの基本から実装のコツまで、開発で役立つポイントを静かに整理しています。
難しいことを増やすのではなく、コードの見通しが少し良くなるヒントを届けるのが役目です。

「この処理はどこに書くのがいいのか」
「Laravelではどう考えると整理できるのか」

そんな疑問に、小さなメモを残すような気持ちで記事を書いています。

コードを書いている途中で迷ったとき、
このサイトが少し立ち止まって整理できる場所になればうれしいです。

レン (Wren)をフォローする