PDFを複数のファイルに分割する前に、指定したパートが元の資料の全ページを一度ずつ含んでいるかを確かめたいことがあります。4ページの資料を2つに分ける指定 [[0,1],[1,3]] は、要素数こそ4ですが、1 が重複し、2 が欠けています。画面上の番号でいえば、この指定のまま分割すると2ページ目が両方のファイルに入り、3ページ目はどちらにも入りません。
もう一つ、逆順の例があります。0始まりのインデックスで [[3,2],[1,0]] と指定すると、0〜3はすべて一度ずつ現れます。しかし、資料を先頭から順番に渡したい場合、この並びは期待と逆です。「全ページを網羅できた」と「順番が正しい」は、別々に確認する必要があります。
まず網羅だけを検査する
次の関数は、各ページのインデックスがちょうど一度ずつ現れるかを調べます。欠けた番号を missing、繰り返された番号を duplicates に返し、両方が空なら complete を true にします。返す番号は画面上のページ番号ではなく、0始まりのインデックスです。利用者に見せるときは、表示用の番号に直してから伝えると誤解が減ります。
逆順の例は、この関数の契約では complete: true です。順番まで正しいという意味ではありません。連続した範囲を元の順序で渡したいなら、その要件に合わせた別の検査が必要です。
入力として不正なものは網羅不足として扱わず、例外にします。空のパート、負の値、範囲外の値、小数、文字列などが該当します。教材上の上限はページ数1〜1,000、パート数1〜100、出現回数の合計2,000です。入力配列は変更しません。
export function partitionCoverage(parts, pageCount) {
if (!Number.isSafeInteger(pageCount) || pageCount < 1 || pageCount > 1000 ||
!Array.isArray(parts) || parts.length < 1 || parts.length > 100) {
throw new TypeError('invalid partition input');
}
const seen = new Set(), repeated = new Set();
let occurrences = 0;
for (const part of parts) {
if (!Array.isArray(part) || !part.length || (occurrences += part.length) > 2000) {
throw new TypeError('invalid part');
}
for (const page of part) {
if (!Number.isSafeInteger(page) || page < 0 || page >= pageCount) {
throw new TypeError('invalid zero-based page index');
}
if (seen.has(page)) repeated.add(page);
seen.add(page);
}
}
const missing = Array.from({length: pageCount}, (_, i) => i).filter(i => !seen.has(i));
const duplicates = [...repeated].sort((a, b) => a - b);
return {complete: missing.length === 0 && duplicates.length === 0, missing, duplicates};
}
テストで逆順も残す
以下を関数の後ろに置き、同じ .mjs ファイルにまとめます。valid は「検査できる入力」という意味で、網羅が不完全な例も含みます。逆順の例が true になることをテストに明記しておくと、後からこの関数を順序の検査と取り違えにくくなります。入力が変更されていないことも確認します。
import assert from 'node:assert/strict';
const valid = [
[[[0,1],[2,3]],4,{complete:true,missing:[],duplicates:[]}],
[[[0,1],[1,3]],4,{complete:false,missing:[2],duplicates:[1]}],
[[[3,2],[1,0]],4,{complete:true,missing:[],duplicates:[]}],
[[[0,0]],1,{complete:false,missing:[],duplicates:[0]}],
[[[0]],3,{complete:false,missing:[1,2],duplicates:[]}]
];
for (const [parts,count,expected] of valid) {
const before=JSON.stringify(parts);assert.deepEqual(partitionCoverage(parts,count),expected);
assert.equal(JSON.stringify(parts),before);
}
const invalid = [[[],3],[[[]],3],[[[3]],3],[[[-1]],3],[[['1']],3],[[[0.5]],3],[[[0]],0],[[[0]],1001],[[Array(2001).fill(0)],1],[Array(101).fill([0]),1]];
for(const [parts,count] of invalid) assert.throws(()=>partitionCoverage(parts,count));
console.log(JSON.stringify({valid:valid.length,rejected:invalid.length,boundary:'coverage only, not order or real PDF output'}));
11月1日の保存済み Node v22.23.4 実行記録では、検査可能な入力5件と拒否入力10件を扱い、スタブは終了コード1、実装は0でした。この結果は、ページの実体、印字番号、表示やナビゲーションまでは確かめていません。PDFのデコード・書き込み、ZIP作成、ブラウザーや端末での動作、変換もテストしていません。
開示:GPT-6.1 Terra を使って Yan Wang / LensUp のために執筆しました。Yan Wang は LensUp の関係者です。本文は独立した編集AI(lensup 主编)が推敲し、Codex(AI)が事実確認と、記録済みの合成入力によるコード実行結果との照合を行いました。人間の編集者やオーナーによる確認・テストは行っていません。根拠は11月1日のソースと公開ページの確認、および上記の保存済み合成Node実行記録です。