JavaScript Tips | 配列ユーティリティ:分割

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「配列の分割」

ここでの「分割」は、1つの配列を「小さな配列のかたまり」に切り分ける処理のことです。
英語だと「chunk(チャンク)」と呼ばれることが多いです。

例えば、こんな場面で使います。

大量データを 100 件ずつに分けて、順番に API に送信したい。
メール送信対象を 50 件ずつに分けて、バッチ処理したい。
画面表示用に、「1 行あたり 3 件」でカードを並べたい。

「配列の分割ユーティリティ」を持っておくと、こういう処理を毎回 for 文で書かずに済みます。


基本イメージ:「N 個ずつに切り分ける」

まずは結果の形をイメージする

例えば、次の配列があります。

const data = [1, 2, 3, 4, 5, 6, 7];
JavaScript

これを「3 個ずつ」に分割したいとします。
欲しい結果はこうです。

[
  [1, 2, 3],
  [4, 5, 6],
  [7],
]
JavaScript

つまり、「元の配列」 → 「配列の配列」に変換するイメージです。
この「小さな配列 1 つ 1 つ」が「チャンク(かたまり)」です。


一番基本:固定サイズで分割する chunk 関数

シンプルな実装

まずは、「配列を size 個ずつに分割する」基本のユーティリティを書いてみます。

function chunk(array, size) {
  if (!Array.isArray(array)) {
    return [];
  }

  const result = [];

  const chunkSize = size > 0 ? size : 1;

  for (let i = 0; i < array.length; i += chunkSize) {
    const part = array.slice(i, i + chunkSize);
    result.push(part);
  }

  return result;
}
JavaScript

重要なポイントをかみ砕いて説明する

chunkSize で「実際に使うサイズ」を決めています。
size が 0 やマイナスでも、最低 1 にはしておかないと無限ループになるからです。

ループの i は、「チャンクの先頭インデックス」です。
i += chunkSize とすることで、「0 → chunkSize → 2*chunkSize → …」と、チャンクごとの先頭にジャンプしていきます。

array.slice(i, i + chunkSize) で、「i から i + chunkSize まで」を切り出しています。
最後のチャンクは、要素数が chunkSize 未満でもそのまま返ってきます(これがちょうど欲しい挙動です)。

実際の動き

chunk([1, 2, 3, 4, 5, 6, 7], 3);
// [[1, 2, 3], [4, 5, 6], [7]]

chunk([1, 2, 3, 4], 2);
// [[1, 2], [3, 4]]

chunk([1, 2, 3], 10);
// [[1, 2, 3]]

chunk([1, 2, 3], 0);
// size が 0 でも最低 1 になるので [[1], [2], [3]]
JavaScript

このパターンを覚えておけば、「N 個ずつに分ける」はいつでも書けます。


実務でよくある使い方の例

例1:API に送るデータを分割する

例えば、API が「一度に 100 件までしか受け付けない」とします。
1000 件のデータを送るには、「100 件ずつに分割して、10 回に分けて送る」必要があります。

const records = /* 1000件の配列 */;
const chunks = chunk(records, 100);

for (const part of chunks) {
  await sendToApi(part);
}
JavaScript

ここでのポイントは、「分割のロジック」と「送信のロジック」を分離できていることです。
chunk が「分けること」だけに集中してくれるので、sendToApi 側は「渡された配列を送る」だけで済みます。

例2:画面でカードを「1 行 3 件」で並べる

UI で「カードを 1 行 3 件で並べたい」ときにも、分割は便利です。

const items = [
  /* 商品や記事などの配列 */
];

const rows = chunk(items, 3);
JavaScript

テンプレート側では、「rows をループして、その中の item をさらにループする」という形で、
「行」と「列」の構造を自然に表現できます。


条件で分割する:predicate での分割

「サイズ」ではなく「条件」で分けたい場合

ときどき、「N 個ずつ」ではなく、「条件が変わるところで分割したい」ことがあります。

例えば、ログを「日付が変わるごと」に分割したい。
ステータスが変わるところで区切りたい。

この場合は、「前の要素と比べて条件が変わったら新しいグループを始める」という発想で書きます。

function splitWhen(array, isBoundary) {
  if (!Array.isArray(array)) {
    return [];
  }

  const result = [];
  let current = [];

  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    const prev = i > 0 ? array[i - 1] : undefined;

    if (i > 0 && isBoundary(prev, item)) {
      result.push(current);
      current = [];
    }

    current.push(item);
  }

  if (current.length > 0) {
    result.push(current);
  }

  return result;
}
JavaScript

isBoundary(prev, item) が「区切りかどうか」を判定する関数です。
true を返したら「ここで一度グループを閉じて、新しいグループを始める」という動きになります。

実際の使い方:日付が変わるところで分割

const logs = [
  { at: "2024-01-01T10:00:00Z", message: "A" },
  { at: "2024-01-01T12:00:00Z", message: "B" },
  { at: "2024-01-02T09:00:00Z", message: "C" },
  { at: "2024-01-02T18:00:00Z", message: "D" },
];

function toDateString(log) {
  return log.at.slice(0, 10); // "YYYY-MM-DD"
}

const groups = splitWhen(logs, (prev, curr) => {
  return toDateString(prev) !== toDateString(curr);
});
JavaScript

結果はこうなります。

[
  [
    { at: "2024-01-01T10:00:00Z", message: "A" },
    { at: "2024-01-01T12:00:00Z", message: "B" },
  ],
  [
    { at: "2024-01-02T09:00:00Z", message: "C" },
    { at: "2024-01-02T18:00:00Z", message: "D" },
  ],
]
JavaScript

「日付が変わるところで区切る」というルールを isBoundary に閉じ込めているのがポイントです。


実務で意識してほしい設計のポイント

「分割サイズが 0 やマイナス」のときの扱い

chunk のような関数では、size が 0 やマイナスだと危険です。
for (let i = 0; i < length; i += size) が無限ループになってしまうからです。

ユーティリティ側で、

size <= 0 のときは 1 に丸める。
または、空配列を返す。

などのポリシーを決めておくと、安全に使えます。
今回の実装では「最低 1」に丸める方式を採用しました。

「元の配列を壊さない」ことを前提にする

chunksplitWhen は、基本的に「元の配列をそのままにして、新しい配列を返す」設計にしておくのがおすすめです。
業務コードでは、「元データはそのまま」「変換結果だけを別で持つ」ほうがバグを減らせます。

「分割」と「ページング」の違いを意識する

ページングと分割は似ていますが、目的が少し違います。

ページング
「ページ番号で一部分だけを取り出す」
「1ページ目だけ」「3ページ目だけ」といった使い方。

分割(chunk)
「最初から全部を小分けにしておく」
「全チャンクを順番に処理する」「行ごとに表示する」といった使い方。

内部的にはどちらも slice を使いますが、
「ページ番号でアクセスするか」「配列の配列として全部持つか」という違いを意識しておくと、設計がクリアになります。


少し手を動かして感覚をつかむ

コンソールで、次のようなコードを実際に打ってみてください。

chunk([1, 2, 3, 4, 5, 6, 7], 3);
chunk([1, 2, 3, 4, 5], 2);
chunk([1, 2, 3], 10);
chunk([1, 2, 3], 0);

const logs = [
  { at: "2024-01-01T10:00:00Z", message: "A" },
  { at: "2024-01-01T12:00:00Z", message: "B" },
  { at: "2024-01-02T09:00:00Z", message: "C" },
  { at: "2024-01-02T18:00:00Z", message: "D" },
];

splitWhen(logs, (prev, curr) => prev.at.slice(0, 10) !== curr.at.slice(0, 10));
JavaScript

「どういう単位で分割されているか」「最後のかたまりがどう扱われているか」「条件での分割がどう効いているか」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function chunk(...) { ... }
export function splitWhen(...) { ... }
JavaScript

のような関数を置き、

「配列を小分けにしたくなったら、必ずこの“分割ユーティリティ”を通す」

というルールを作ってみてください。
そうすると、あなたのコードは、「場当たり的な for 文」から、「意図がはっきりしたデータ分割」に一段ステップアップしていきます。

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