0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ページ指定の「1」と配列の「0」を混同しないための入力境界

0
Posted at

ページ選択欄に「1」と入力した利用者が期待するのは、先頭ページです。一方、処理側の配列では先頭がインデックス 0 かもしれません。この変換があいまいだと、正しく入力されても隣のページを選んでしまいます。ここでは、表示上の番号は1始まり、返り値は0始まりと決めておきます。

受け付ける書式を先に決める

例の parsePages が読むのは、ASCIIの正の整数と範囲を半角カンマで区切った文字列です。1-3,5 は [0,1,2,4] になります。4-2 のような逆順も受け付け、重複したページは最初に現れた位置だけを残します。トークンの前後の空白は許しますが、1 - 2 のように範囲の内側に入った空白は許しません。

日本語の環境では、IMEで入力した数字や区切り文字が全角になることがあります。この関数では、全角数字の 1 は null です。読点や全角カンマで区切った 1、2 や 1,2 も null になります。トークンを分けるのはASCIIの , だけだからです。数字の間に全角スペース(U+3000)が入った 1 2 も null です。一方、前後にある全角スペースは String.prototype.trim() で取り除かれるので、' 1,2 ' は [0,1] として読めます。

ほかに、空欄、末尾のカンマ、2junk、0、先頭ゼロ付きの 01、セミコロンも拒否します。これは教材用に狭く決めた方針であり、すべての製品に共通する書式ではありません。実際の画面で使うなら、受け付ける入力例を入力欄の近くに示しておくと、利用者が自分で直しやすくなります。

長さにも上限があります。文字列は120 UTF-16コード単位まで、トークンは20個まで、ページ数は1〜10,000、範囲を展開した回数は重複を含めて1,000までです。1-1000,1 は重複を除けば1,000ページですが、展開回数が上限を超えるので拒否されます。

export function parsePages(text, pageCount) {
  if (typeof text !== 'string' || text.length > 120 ||
      !Number.isSafeInteger(pageCount) || pageCount < 1 || pageCount > 10000) return null;
  const tokens = text.trim().split(',');
  if (tokens.length > 20) return null;
  const pages = [], seen = new Set();
  let expanded = 0;
  for (const token of tokens) {
    const match = /^([1-9]\d*)(?:-([1-9]\d*))?$/.exec(token.trim());
    if (!match) return null;
    const from = Number(match[1]), to = Number(match[2] ?? match[1]);
    if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) ||
        from > pageCount || to > pageCount) return null;
    expanded += Math.abs(to - from) + 1;
    if (expanded > 1000) return null;
    const step = to >= from ? 1 : -1;
    for (let page = from; page !== to + step; page += step) {
      if (!seen.has(page)) { seen.add(page); pages.push(page - 1); }
    }
  }
  return pages;
}

拒否の理由を画面でどう伝えるか

この関数は、不正な入力をすべて null で返します。呼び出し側はこの返り値だけでは「全角数字」「範囲外」「展開回数の超過」を区別できません。理由ごとにメッセージを出したいなら、その診断の仕組みを別に設計する必要があります。黙って読めた部分だけを採用する仕様にはしていません。

次のアサーションは、関数の後ろに貼り付けて同じ .mjs ファイルにまとめます。配列全体を比較するので、ページ番号の変換と、重複があるときの順序を確かめられます。

import assert from 'node:assert/strict';
const valid = [
 ['1-3,5',6,[0,1,2,4]],
 ['4-2,2,6',6,[3,2,1,5]],
 [' 1 , 3-2 ',3,[0,2,1]],
 ['2,2',3,[1]],
 ['1-1000',1000,Array.from({length:1000},(_,i)=>i)]
];
for (const [text,count,expected] of valid) assert.deepEqual(parsePages(text,count),expected);
const invalid = [
 ['',3],['1,',3],['2junk',3],['0',3],['4',3],['01',3],['1',3],
 ['1;2',3],['1 - 2',3],['1-1001',1001],['1-1000,1',1000],
 ['1',0],['1',2.5],['1',10001],[null,3],['1'.repeat(121),3],
 [Array(21).fill('1').join(','),3]
];
for (const [text,count] of invalid) assert.equal(parsePages(text,count),null);
console.log(JSON.stringify({valid:valid.length,rejected:invalid.length,boundary:'synthetic text and page indices only'}));

11月1日の保存済み Node v22.23.4 実行記録では、有効例5件と拒否例17件を扱い、スタブは終了コード1、実装は0で終了しました。対象は合成した文字列とインデックスだけです。PDFのデコード・書き込み、ZIP作成、ブラウザーや端末での動作、変換はテストしていません。

開示:GPT-6.1 Terra を使って Yan Wang / LensUp のために執筆しました。Yan Wang は LensUp の関係者です。本文は独立した編集AI(lensup 主编)が推敲し、Codex(AI)が事実確認と、記録済みの合成入力によるコード実行結果との照合を行いました。人間の編集者やオーナーによる確認・テストは行っていません。根拠は11月1日のソースと公開ページの確認、および上記の保存済み合成Node実行記録です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?