配列を循環して要素を取り出すジェネリック関数(TypeScript)

世の中には、循環するモノがあります。例えば、暦の月は1月に始まり12月で終わり、また1月に戻って循環します。このような循環するモノを配列で表現する場合、配列に要素を無限に詰め込むことはできないので、配列の個数は有限になります。

例えば暦の月を数値の配列で表すと、[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 , 11, 12]になります。この配列の10月から先の6ヶ月分の要素を取り出したい場合、[10, 11, 12, 1, 2, 3]というように、配列の最初に戻って要素を取り出す必要があります。

上のような単純な処理をJavaScriptで行うのであれば、Arrayオブジェクトのslice()メソッドを2回呼べば事足りますが、取り出す個数が2巡目、3巡目までというようにどんどん深くなっていくほど、面倒になります。既にこのような関数がJavaScriptかTypeScriptに存在すると思って探したのですが、見当たりませんでした。ただ探し方が甘かっただけかもしれませんが。こういう場合は、後々のためにもできるだけ汎用的な関数を作成したほうがいいでしょう。

ということで、配列を循環して要素を取り出す関数をTypeScriptで作成してみました。最初の引数arrが要素を取り出す配列、二番目の引数startIndexが要素を取り出す開始位置、三番目の引数numが取り出したい要素の個数です。戻り値は結果となる配列です。シャローコピーなので、元の配列は変更されません。

ロジックは再帰を利用しています。つまり内部に関数をもう一つ定義して、その内部関数を呼び出す形を取っています。なお再帰を使用する際は、無限ループに注意する必要があります。終了条件が十分でないと、無限ループを抜けることができなくなるからです。

// TypeScriptバージョン
function getItemsFromArrayCycle<T> (
  arr: T[],
  startIndex: number,
  num: number,
): T[] {
  const resultArray = [];
  if (num <= 0) return resultArray;

  // 実質的な処理を行う内部関数(無限ループに注意)
  const innerFunc = (index: number, num: number) => {
    if (num <= 0 || index < 0) return;

    // 配列の長さを超えた場合は最後まで
    const slicedArray = arr.slice(index, index + num);
    if (slicedArray.length <= 0) return;
    
    resultArray.push(...slicedArray);
    
    num -= slicedArray.length;
    if (num <= 0) return;
    
    innerFunc(0, num); // 再帰呼び出し(位置は最初から)
  };

  innerFunc(startIndex, num);
  return resultArray;
}

TypeScriptならこのようにジェネリックを利用して、より汎用的に処理を記述することができます。上の例はTypeScriptですが、以下のように関数シグネチャの部分を変更することで、JavaScriptでも使用することができます。

// JavaScriptバージョン
function getItemsFromArrayCycle (
  arr,
  startIndex,
  num,
) {
  const resultArray = [];
  if (num <= 0) return resultArray;

  // 実質的な処理を行う内部関数(無限ループに注意)
  const innerFunc = (index, num) => {
    if (num <= 0 || index < 0) return;

    // 配列の長さを超えた場合は最後まで
    const slicedArray = arr.slice(index, index + num);
    if (slicedArray.length <= 0) return;
    
    resultArray.push(...slicedArray);
    
    num -= slicedArray.length;
    if (num <= 0) return;
    
    innerFunc(0, num); // 再帰呼び出し(位置は最初から)
  };

  innerFunc(startIndex, num);
  return resultArray;
}

この関数の書き方に関しては、上の方法が唯一の正解ではないです。改善の余地もあると思いますので、余裕があればもっといい方法について調べてみたいと思います。