データ入力規則の弱点 — コピペすり抜け問題を解決する
実際のビジネスシーンで使える自動化100例をGASで実装するシリーズ「GAS自働化100本ノック」。その9本目
コピペすり抜け問題を検証
スプレッドシートのデータ入力規則、便利ですね。
使っている人も多いと思います。
しかし、規則によっては、手入力では有効でも、データをコピペした時はすり抜けてしまうって、知ってました?
検証してみましょう!

上の受注データのD列数量には「0~1000の間に含まれる値」の規則を、F列メアドには「有効なメールアドレスであるテキスト」の規則を設定してあります。
この規則に違反する値を手入力とコピペした時の結果が以下です。
D列に -3と15000を手入力した時には、入力規則が働きセルの右上に警告の赤い▲が表示されました。
しかし、下の行にコピーで貼り付けた時には▲が出ません。規則をすり抜けてしまいました(分かりやすいようにグレー背景に加工)。
同様に、F列に無効なメアドを手入力した時には、▲警告がでましたが、コピペした時には、すり抜けています。
入力規則のコピペすり抜け問題、どうやら本当のようです。
コピペで問題が起きるのは不便ですね。
どうしましょう?
GASならコピペもすり抜けさせない
GASならコピペで入力しても、規則違反をしっかりガードします!
GASでできること
-
入力ルールを「入力ルール」シートで管理
-
ルールに違反する入力が起きたら、コピペや範囲ドラッグも含めて即座に検出
-
違反セルを赤くハイライト+トーストで通知
スプレッドシートの「警告」や「拒否」モードでは止められない貼り付けすり抜けを、シートを開いている間ずっと監視します。
そのコードがこちら。
コード.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なのではなく、それを違反しない、正常な値に直してはじめて作業完了です。
違反を赤く表示するだけでなく、正しいかたちに整形まで自動でやってくれたら、さらに便利ですね。
拡張コードでそれを実現しましょう!
拡張コードでできること
-
自動整形 — 末尾空白の除去、全角→半角、メアド小文字化を入力時に自動で実施。整形済みセルは黄色でマーク
この記事の続きはnoteで公開しています。
