データ入力規則の弱点 — コピペすり抜け問題を解決する

実際のビジネスシーンで使える自動化100例をGASで実装するシリーズ「GAS自働化100本ノック」。その9本目

コピペすり抜け問題を検証

スプレッドシートのデータ入力規則、便利ですね。
使っている人も多いと思います。

しかし、規則によっては、手入力では有効でも、データをコピペした時はすり抜けてしまうって、知ってました?

検証してみましょう!

受注データの例

上の受注データのD列数量には「0~1000の間に含まれる値」の規則を、F列メアドには「有効なメールアドレスであるテキスト」の規則を設定してあります。

この規則に違反する値を手入力とコピペした時の結果が以下です。

入力規則のすり抜け

D列に -3と15000を手入力した時には、入力規則が働きセルの右上に警告の赤い▲が表示されました。
しかし、下の行にコピーで貼り付けた時には▲が出ません。規則をすり抜けてしまいました(分かりやすいようにグレー背景に加工)。

同様に、F列に無効なメアドを手入力した時には、▲警告がでましたが、コピペした時には、すり抜けています。

入力規則のコピペすり抜け問題、どうやら本当のようです。

コピペで問題が起きるのは不便ですね。
どうしましょう?

GASならコピペもすり抜けさせない

GASならコピペで入力しても、規則違反をしっかりガードします!

GASでできること

  1. 入力ルールを「入力ルール」シートで管理

  2. ルールに違反する入力が起きたら、コピペや範囲ドラッグも含めて即座に検出

  3. 違反セルを赤くハイライト+トーストで通知

スプレッドシートの「警告」や「拒否」モードでは止められない貼り付けすり抜けを、シートを開いている間ずっと監視します。

そのコードがこちら。

コード.gs

// コード.gs
/**
 * 入力ルールシートで定義したルールに違反する入力(直接入力・コピペ問わず)を、
 * シートが編集された瞬間に検出し、違反セルを赤くハイライトする。
 *   - シンプルトリガー onEdit(e) を使う。コードを保存してシートをリロードすればすぐ有効。
 *   - 範囲貼り付け(複数セル)にも対応。e.range 全体を1セルずつ走査する。
 *   - スプレッドシートの「データの入力規則」と違い、貼り付けですり抜けない。
 */

const RULES_SHEET = '入力ルール';
const HIGHLIGHT_BG = '#ffcccc';
const RESET_BG = '#ffffff';

function onEdit(e) {
  if (!e || !e.range) return;

  const range = e.range;
  const sheet = range.getSheet();
  const ss = sheet.getParent();

  if (sheet.getName() === RULES_SHEET) return;

  const rulesSheet = ss.getSheetByName(RULES_SHEET);
  if (!rulesSheet) return;

  const rules = loadRules_(rulesSheet);
  if (rules.length === 0) return;

  const startRow = range.getRow();
  const startCol = range.getColumn();
  const numRows = range.getNumRows();
  const numCols = range.getNumColumns();

  // ヘッダー行(1行目)に重なる部分は検査対象外
  const dataStartRow = Math.max(2, startRow);
  const dataEndRow = startRow + numRows - 1;
  if (dataEndRow < 2) return;

  const lastCol = Math.max(sheet.getLastColumn(), startCol + numCols - 1);
  const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0]
    .map(function(h) { return String(h).trim(); });

  const checkRows = dataEndRow - dataStartRow + 1;
  const checkRange = sheet.getRange(dataStartRow, startCol, checkRows, numCols);
  const values = checkRange.getValues();
  const backgrounds = checkRange.getBackgrounds();

  let violations = 0;
  for (let i = 0; i < checkRows; i++) {
    for (let j = 0; j < numCols; j++) {
      const colIndex = startCol - 1 + j;
      const colName = headers[colIndex] || '';
      if (!colName) continue;

      const colRules = rules.filter(function(r) { return r.colName === colName; });
      if (colRules.length === 0) continue;

      const cellValue = values[i][j];
      let hasError = false;
      for (let r = 0; r < colRules.length; r++) {
        if (checkRule_(colRules[r], cellValue)) {
          hasError = true;
          break;
        }
      }

      backgrounds[i][j] = hasError ? HIGHLIGHT_BG : RESET_BG;
      if (hasError) violations++;
    }
  }

  checkRange.setBackgrounds(backgrounds);

  if (violations > 0) {
    ss.toast(violations + ' 件のルール違反を検出(赤いセルを確認)', '入力ガード', 5);
  }
}

function checkRule_(rule, cellValue) {
  if (rule.ruleType === '必須') {
    return cellValue === '' || cellValue === null || cellValue === undefined;
  }
  // 必須以外は空欄スルー
  if (cellValue === '' || cellValue === null || cellValue === undefined) return false;

  if (rule.ruleType === '最小値') {
    return typeof cellValue === 'number' && cellValue < Number(rule.ruleValue);
  }
  if (rule.ruleType === '最大値') {
    return typeof cellValue === 'number' && cellValue > Number(rule.ruleValue);
  }
  if (rule.ruleType === 'リスト') {
    const allowed = String(rule.ruleValue).split(',').map(function(v) { return v.trim(); });
    return allowed.indexOf(String(cellValue).trim()) === -1;
  }
  if (rule.ruleType === '形式') {
    const pattern = formatPattern_(String(rule.ruleValue));
    return pattern && !pattern.test(String(cellValue));
  }
  return false;
}

/** 形式名を正規表現に変換。プリセット未対応の指定は正規表現として直接解釈する。 */
function formatPattern_(spec) {
  const s = String(spec).trim();
  if (s === 'メール' || s === 'メアド') return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
  if (s === '電話' || s === '電話番号') return /^0\d{1,4}-\d{1,4}-\d{4}$/;
  if (s === '半角英数') return /^[A-Za-z0-9]+$/;
  if (s === '半角数字') return /^\d+$/;
  try { return new RegExp(s); } catch (e) { return null; }
}

function loadRules_(rulesSheet) {
  const last = rulesSheet.getLastRow();
  if (last < 2) return [];
  const data = rulesSheet.getRange(2, 1, last - 1, 3).getValues();
  const rules = [];
  for (let i = 0; i < data.length; i++) {
    const colName = String(data[i][0]).trim();
    const ruleType = String(data[i][1]).trim();
    if (!colName || !ruleType) continue;
    rules.push({ colName: colName, ruleType: ruleType, ruleValue: data[i][2] });
  }
  return rules;
}

function clearGuardHighlights() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getActiveSheet();
  if (sheet.getName() === RULES_SHEET) {
    SpreadsheetApp.getUi().alert('「' + RULES_SHEET + '」シート自体は対象外です。');
    return;
  }
  const last = sheet.getLastRow();
  const lastCol = sheet.getLastColumn();
  if (last < 2 || lastCol < 1) {
    SpreadsheetApp.getUi().alert('クリア対象のデータがありません。');
    return;
  }
  const range = sheet.getRange(2, 1, last - 1, lastCol);
  const backgrounds = [];
  for (let i = 0; i < last - 1; i++) {
    const row = [];
    for (let j = 0; j < lastCol; j++) row.push(RESET_BG);
    backgrounds.push(row);
  }
  range.setBackgrounds(backgrounds);
  SpreadsheetApp.flush();
  SpreadsheetApp.getUi().alert('ハイライトをクリアしました。');
}

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('自動ボタン')
    .addItem('ハイライトをクリア', 'clearGuardHighlights')
    .addToUi();
}

使い方

① スプレッドシートを用意
チェック対象にしたいスプレッドシートを開きます。
新規スプレッドシートで動作確認してから本番に導入するのがおすすめです。

② 「入力ルール」シートを作成
シートを1枚追加して名前を「入力ルール」にし、A〜C列にルールを記入します。

入力ルールの例

ルールは以下の5種類から選べます。

  • 必須 … 空欄ならエラー(C列:不要)

  • 最小値 … 数値がC列より小さければエラー(C列:下限値)

  • 最大値 … 数値がC列より大きければエラー(C列:上限値)

  • リスト … カンマ区切りの許可値に含まれなければエラー(C列例:田中,佐藤,鈴木,山本)

  • 形式 … 指定形式と一致しなければエラー(C列:メール/電話/半角英数/半角数字)

※ 同じ列にルールを複数、設定できます(`数量` の `必須`+`最小値`+`最大値` のように)
※ ルール名は変えられませんが、列名は対象シートに合わせて自由に変更可能です

③ 拡張機能 → Apps Script にコード.gsを貼り付けて保存

④ スプレッドシートをリロード
これでガードが有効になります。トリガー登録などの設定は不要です。
初めて違反を検出したときに承認ダイアログが表示されます。
承認手順は第1回と同じです。

⑤ 試しに違反する値を貼り付けてみる

ルール違反を含むデータの例

上図のような入力ルール違反を含むデータをまとめて貼り付けてみた結果がこちら ↓

コピペすり抜けを防止

貼り付けた瞬間、`取引先`列の空欄、`数量` 列の `-3` `15000`、`メアド` 列の `  Tanaka@Example.com`、`yamamoto[at]example` がそれぞれ赤くなり、画面右下に「5 件のルール違反を検出」のトーストが出ます。

⑥ 自動ボタン → ハイライトをクリア
シート上部のメニュー「 自動ボタン → ハイライトをクリア」 で、適宜、開いているシートの赤いマークをまとめて消せます。

「データの入力規則」では止められない貼り付け経由の不正データが、入力された瞬間に分かる のがGASのリアルタイムガードです。

ワンポイント・レッスン

onEdit(e) — シートが編集された瞬間に呼ばれる「シンプルトリガー」

今回の貼り付けガードを支えているのは `onEdit` です。

① `onEdit` は予約名で自動的に呼ばれる

スクリプトに `onEdit` という名前の関数を書いておくと、シートのセルが編集されるたび、自動でこの関数が呼ばれる—これがGASの「シンプルトリガー」です。
トリガー設定の手作業は不要で、保存してシートをリロードすれば即有効になります。

function onEdit(e) {
  if (!e || !e.range) return;
  const range = e.range;
  ...
}

② 引数 `e` に「何が編集されたか」が入っている

呼ばれた関数は、第1引数 `e`(編集イベントオブジェクト)から編集情報を受け取ります。

`e.range` — 編集された範囲(Rangeオブジェクト)
`e.value` — 単一セル編集の場合の新しい値
`e.oldValue` — 単一セル編集の場合の以前の値
`e.source` — スプレッドシート全体(`SpreadsheetApp.getActiveSpreadsheet()` 相当)

注目してほしいのは `e.value` の 「単一セル編集の場合」 というただし書きです。貼り付けや範囲ドラッグでは `e.value` は `undefined` になり、値が取れません。
一方の `e.range` は1セルでも複数セルでも、常に「今編集された範囲」を返します。貼り付け対応で頼れるのは `e.range` の方、というわけです。

③ 貼り付けの肝:範囲を「位置+サイズ」で押さえて、`getValues()` で一気に走査する

コピペや範囲ドラッグも `onEdit` を発火させますが、10行×3列の貼り付けは30回ではなく一度のイベントで届きます(`e.range` に10行3列の範囲がまるごと入る)。なので、`e.range` から「どこから始まる、何行×何列か」を取り出して全セルを順に見ていく、という処理が必要になります。

その「位置+サイズ」を取り出すのがこの4行です。

const startRow = range.getRow();      // 範囲左上の行番号
const startCol = range.getColumn();   // 範囲左上の列番号
const numRows = range.getNumRows();   // 範囲の行数
const numCols = range.getNumColumns();// 範囲の列数

D2に10行×3列を貼ったなら `startRow=2, startCol=4, numRows=10, numCols=3` が入り、これで貼り付け範囲が完全に特定できます。
あとは `getValues()` で その範囲の値を2次元配列としてまとめて取得し、サイズをループ上限に使ってセルを1つずつ見ていけばOKです。

const values = range.getValues();    // numRows × numCols の2次元配列
for (let i = 0; i < numRows; i++) {
  for (let j = 0; j < numCols; j++) {
    const cellValue = values[i][j];           // 範囲内 (i,j) の値
    const absRow = startRow + i, absCol = startCol + j; // シート上の絶対座標
    // …ここでルール違反チェック
  }
}

まとめ

  • 入力ルールを「入力ルール」シートで管理

  • ルールに違反する入力が起きたら、コピペや範囲ドラッグも含めて即座に検出

  • 違反セルを赤くハイライト+トーストで通知


コピペで入力しても規則すり抜けをガードして、違反が分かるようになったことは一歩前進です。

しかし、違反が分かったらそれでOKなのではなく、それを違反しない、正常な値に直してはじめて作業完了です。

違反を赤く表示するだけでなく、正しいかたちに整形まで自動でやってくれたら、さらに便利ですね。

拡張コードでそれを実現しましょう!

拡張コードでできること

  1. 自動整形 — 末尾空白の除去、全角→半角、メアド小文字化を入力時に自動で実施。整形済みセルは黄色でマーク

    この記事の続きはnoteで公開しています。

    noteで続きを読む

GAS,自動化自動化

Posted by botw