GASで重複しない8桁の受付番号を自動採番する仕組み|数学的シャッフルとチェックデジットの実装手順を解説

GASで重複しない8桁の受付番号を自動採番する仕組みと実装手順を解説するアイキャッチ画像

Google Apps Script(GAS)とスプレッドシートでWebフォームや予約受付システムを構築する際、送信完了時の控えとして「重複しない8桁の受付番号」を自動発行する仕組みを組み込むことができます。

単純な乱数で8桁を生成すると発行件数が増えるにつれて過去の番号と重複する確率が上がり、反対に「00000001、00000002…」と連番をそのまま出力すると累計の申込件数が外部から推測可能になります。本記事では、内部の連番(1, 2, 3…)を数式でシャッフルし、最大900万件まで重複しない8桁の受付番号(上7桁+チェックデジット1桁)を生成する仕組みと実装コードを解説します。

この記事でわかること

  • 単純な乱数(Math.random)や連番ではなく「連番×数学的シャッフル」を採用する理由
  • 最大900万件まで重複ゼロを実現する「互いに素の掛け算」と「チェックデジット(検証桁)」の仕組み
  • 8桁採番関数(generate8DigitCode)の完成コードと各定数の役割
  • 同時アクセスを防ぐ排他ロック(LockService)と連番保存(ScriptProperties)を組み合わせた実装方法

単純な乱数や連番ではなく「連番×シャッフル」を採用する理由

受付番号を発行する方式として、「乱数(ランダムな数字)で8桁を作る方式」と「1番から順番に00000001、00000002…と連番を振る方式」がありますが、それぞれ以下の技術的な課題が存在します。

単純な乱数・連番の弱点と8桁の自動採番ロジックの違いを比較した図解
図:単純な乱数や連番における課題と、「連番×数学的シャッフル」方式の比較
  • 単純な乱数(Math.random)の課題:独立した乱数生成では、過去の番号と偶然一致する可能性が常に残ります。数学の「誕生日のパラドックス」に基づき、発行件数が増えるほど重複発生確率が上昇します。
  • そのままの連番(00000001, 00000002…)の課題:重複は発生しませんが、番号から全体の申込件数が把握でき、前後の番号(他者の受付番号)も容易に推測できます。また、スプレッドシートの数値フォーマットによっては先頭の「0」が省略され、桁数が変化する(桁落ちする)性質があります。

これらの課題を解消するため、今回のプログラムでは「内部では重複しない連番(1, 2, 3…)を管理しながら、数式による変換を通して、表面上は規則性の見えない8桁の番号へ変換する」方式をとっています。

【コピペでOK】重複しない8桁の受付番号を生成する関数

連番(seqId)を受け取り、重複しない8桁の受付番号を返す関数(generate8DigitCode)のコードは以下の通りです。

/*
 * 連番(1, 2, 3...)から重複しない8桁の受付番号を生成する関数
 * @param {number} seqId - 1以上の連番
 * @return {string} 8桁の受付番号(1〜9から始まる・最大900万件まで重複0%)
 */
function generate8DigitCode(seqId) {
  // 7桁(900万通り)の範囲とシャッフル用の定数
  const RANGE = 9000000n;      // 900万通り
  const MULTIPLIER = 5482937n; // 900万と互いに素な数値
  const OFFSET = 2718281n;     // 初期位置をズラす数値

  const id = BigInt(seqId);
  const scrambled = (id * MULTIPLIER + OFFSET) % RANGE;
  
  // 100万を足して、必ず先頭が1〜9から始まる7桁(1000000〜9999999)にする
  const base7 = (scrambled + 1000000n).toString();

  // 上7桁からチェックデジット(検証用の末尾1桁)を計算する
  let sum = 0;
  for (let i = 0; i < 7; i++) {
    let digit = parseInt(base7.charAt(i), 10);
    if (i % 2 === 0) {
      digit *= 2;
      if (digit > 9) digit -= 9;
    }
    sum += digit;
  }
  const checkDigit = (10 - (sum % 10)) % 10;

  // 7桁+チェックデジット1桁を合体して8桁で返す
  return base7 + checkDigit.toString();
}

重複しない8桁を生み出す「3つのステップ」の仕組み

この関数が連番(seqId)から最大900万件まで重複しない8桁を生成する処理は、3つのステップで構成されています。

重複しない8桁の受付番号が作られる3つの仕組み(シャッフル計算・上7桁の確定・下1桁の合体)の図解
図:連番から「上7桁(本体)」と「下1桁(チェックデジット)」を作って合体する3ステップ

時計の文字盤の原理で連番をシャッフルする(RANGE・MULTIPLIER・OFFSET)

関数の冒頭では、連番に定数を掛けて余りを求めることで、数値の並び順を攪拌(シャッフル)しています(数学における「線形合同法」および「モジュラ演算」の性質を利用した計算です)。

const RANGE = 9000000n;      // 900万通り(7桁の範囲)
const MULTIPLIER = 5482937n; // 900万と「互いに素」な掛け算の数
const OFFSET = 2718281n;     // 初期位置をズラすための足し算の数

const id = BigInt(seqId);
const scrambled = (id * MULTIPLIER + OFFSET) % RANGE;

この計算で番号が重複しない理由は、「時計の文字盤(0〜11の12マス)」に置き換えると確認できます。

掛け算と余りで重複しない理由(12マスの円盤での例)

0から11までの12マスがある時計の針を、毎回「4時間ずつ」進めると、「4 → 8 → 0 → 4 → 8 → 0…」となり、3回周期で同じ数字に戻ります。これは全体のマス数「12」と進み幅「4」が、公約数(2や4)を持っているためです。

一方、12と1以外の公約数を持たない「5時間ずつ」進めると、「5 → 10 → 3 → 8 → 1 → 6 → 11 → 4 → 9 → 2 → 7 → 0」となり、0から11までの12個の数字をすべて1回ずつ通ってから、13回目で初めて元の位置に戻ります。このように「2つの整数の最大公約数が1である(1以外に共通して割り切れる数を持たない)関係」を「互いに素(たがいにそ)」と呼びます。

今回のコードでは、円盤のマス数を RANGE = 9000000n(900万マス)に設定しています。900万を素因数分解すると「2」と「5」の積のみで構成されるため、進み幅(MULTIPLIER)に「2でも5でも割り切れない整数(一の位が1, 3, 7, 9のいずれかの奇数)」を指定すれば、900万と「互いに素」になります。

コード内の 5482937n は一の位が7であるため、900万と互いに素です。したがって、連番(seqId)が1ずつ増加するたびに、数値が548万2937ずつジャンプしながら900万マスの空間を巡回し、1件目から900万件目まで1度も同じ余り(scrambled)が発生しないことが数学的に保証されます。さらに OFFSET = 2718281n を加算することで初期位置をシフトさせています。

なお、数値の末尾にある n(例:9000000n)は、JavaScriptの「BigInt(任意精度整数)」リテラルであり、大きな整数の乗算時にも浮動小数点の丸め誤差を起こさず正確に計算するために使用しています。

加算値(100万)を足して「必ず1〜9から始まる7桁」に揃える

900万で割った余り(scrambled)の取り得る範囲は、最小が 0、最大が 8999999 です。これに 1000000n(100万)を加算します。

// 100万を足して、必ず7桁(1000000〜9999999)にする
const base7 = (scrambled + 1000000n).toString();

0〜8999999の数値に100万を足すことで、最小値は 1000000、最大値は 9999999 となります。これにより、「先頭が必ず1〜9のいずれかで始まる7桁の文字列(base7)」が確定し、スプレッドシートやCSVで先頭の0が消滅する事象を防止できます。

末尾1桁に「チェックデジット(検査用数字)」を合体する

関数の後半では、確定した上7桁(base7)から検証用の末尾1桁(チェックデジット)を算出し、末尾に連結して8桁の文字列を完成させます。

let sum = 0;
for (let i = 0; i < 7; i++) {
  let digit = parseInt(base7.charAt(i), 10);
  if (i % 2 === 0) {
    digit *= 2;
    if (digit > 9) digit -= 9;
  }
  sum += digit;
}
const checkDigit = (10 - (sum % 10)) % 10;

// 7桁+チェックデジット1桁を合体して8桁で返す
return base7 + checkDigit.toString();

このループ処理は、クレジットカード番号や識別コードの検証に用いられる「Luhn(ルーン)アルゴリズム」に基づいています。

  • 上7桁を左から1文字ずつ取り出し、偶数インデックス(0, 2, 4, 6番目)の数字を2倍します(2倍した値が10以上になった場合は、9を引いて2桁の各数字を足し合わせた値に変換します)。
  • 7桁分の合計値(sum)から、「10の倍数にするために必要な1桁の数値(0〜9)」を求めたものが checkDigit です。
  • この1桁を末尾に付与することで、問い合わせ時などに番号の1文字誤入力があった場合でも、上7桁と末尾1桁の整合性チェックによって入力ミスを機械的に判定できます。

定数の役割一覧と生成される8桁番号のシミュレーション

generate8DigitCode 関数内の4つのパラメータ・処理の役割を整理すると、以下の通りです。

generate8DigitCode関数内の4つの定数・計算の役割を示した図解
図:8桁の受付番号を構成する4つのパラメータと計算処理の役割

連番(seqId)に「1〜5」を入力した際に、この関数が出力する8桁番号の計算結果は以下の通りです。

連番(seqId) シャッフル後の余り(scrambled) 100万を足した上7桁(base7) 検証用1桁(checkDigit) 完成する8桁の受付番号
1件目(1) 8201218 9201218 6 92012186
2件目(2) 4684155 5684155 4 56841554
3件目(3) 1167092 2167092 2 21670922
4件目(4) 6650029 7650029 6 76500296
5件目(5) 3132966 4132966 9 41329669

内部の連番は「1 → 2 → 3 → 4 → 5」と1ずつ増加していますが、出力される受付番号は「92012186 → 56841554 → 21670922 → 76500296 → 41329669」へと分散されます。

同時アクセスやシート重複を防ぐ3つの安全機構との連携

Webフォームの運用において、複数の送信アクセスが同時に発生した場合や、スプレッドシート上に既存の番号が存在する場合でも重複を防ぐため、変換関数の外側で以下の3つの安全機構を組み合わせます。

フォーム送信時に番号重複を防ぐ3つの安全機構(LockService・ScriptProperties・generateReceiptNo_)の図解
図:数式変換の外側で採番の安全性を守る3つの仕組み
  • 排他ロック(LockService):複数のフォーム送信が同時に実行された際、後続の処理を待機させて1件ずつ順番に実行させるGASの標準サービスです。同じ連番(seqId)が複数のアクセスに同時発行されるのを防ぎます。
  • 連番の永続保存(ScriptProperties):スクリプトプロパティ領域に最新の連番(LAST_RECEIPT_SEQ)を保存します。スプレッドシート側の行削除や並び替えの影響を受けずに、次回送信時も前回の続きから連番を進められます。
  • スプレッドシートA列との重複照合(generateReceiptNo_):生成した8桁の候補番号がシートのA列に存在しないかを Set オブジェクトで照合し、万が一同一の番号が存在した場合は連番を1つ進めて再生成します。

呼び出し元(generateReceiptNo_)を含めた実装コード

generate8DigitCode 関数と連携して動作する、シート照合・連番保存・排他ロックの実装コードは以下の通りです。

/*
 * スプレッドシートA列と照合しながら重複しない8桁の受付番号を発行する関数
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet - 記録先シート
 * @return {string} 重複確認済みの8桁受付番号
 */
function generateReceiptNo_(sheet) {
  const props = PropertiesService.getScriptProperties();
  let seqId = parseInt(props.getProperty('LAST_RECEIPT_SEQ') || '0', 10);

  // シートA列の既存受付番号を取得してSetに格納(高速照合用)
  const lastRow = sheet.getLastRow();
  const existingSet = new Set();
  if (lastRow >= 2) {
    const values = sheet.getRange(2, 1, lastRow - 1, 1).getDisplayValues();
    for (let i = 0; i < values.length; i++) {
      if (values[i][0]) existingSet.add(String(values[i][0]).trim());
    }
  }

  // 連番を1つ進め、シート上に同じ番号がないことを確認する
  let candidate = '';
  do {
    seqId += 1;
    candidate = generate8DigitCode(seqId);
  } while (existingSet.has(candidate));

  // 最新の連番を保存して8桁の受付番号を返す
  props.setProperty('LAST_RECEIPT_SEQ', String(seqId));
  return candidate;
}

/*
 * フォーム送信時に排他ロックをかけて受付番号を発行・記録する例
 */
function registerFormSubmission(formData) {
  const lock = LockService.getScriptLock();
  lock.waitLock(15000); // 最大15秒間ロック取得を待機

  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('受付一覧');
    const receiptNo = generateReceiptNo_(sheet);

    // A列に8桁の受付番号、B列以降に受信日時やフォーム入力内容を記録
    sheet.appendRow([receiptNo, new Date(), formData.name, formData.email]);
    return receiptNo;
  } finally {
    lock.releaseLock();
  }
}

まとめ:数式シャッフルと検証桁による8桁採番の要点

GASで重複しない8桁の受付番号を自動採番する仕組みと実装手順の要点は以下の通りです。

本記事の要点まとめ

  • 内部では連番(1, 2, 3…)を管理しつつ、数式で変換することで累計件数や前後の番号を推測されない8桁を生成できる
  • 「900万(RANGE)」と「互いに素な乗数(5482937)」を掛けて余りを求めることで、900万件目まで重複なしに連番をシャッフルできる
  • 100万を加算して「先頭1〜9の7桁」を確定させた後、Luhnアルゴリズムによる検証用の1桁(チェックデジット)を末尾に連結して8桁を構成している
  • 連番保存(ScriptProperties)、排他ロック(LockService)、シートA列照合(generateReceiptNo_)を組み合わせることで、同時送信時にも重複を防いで安全に記録できる

eguchi.netをもっと見る

購読すると最新の投稿がメールで送信されます。