タスク表の余分な空白を集計前に発見|GASで文字列をdry-run監査
ツールおじ|GoogleアカウントをAIエージェント化
タスク名の末尾に空白が1文字入っただけで、同じ作業が別項目として集計されたり、担当者が検索できなかったりします。見た目では気づきにくい全角空白や改行を、更新前に見つけておくと、朝の確認表や期限集計の崩れを減らせます。
この記事では、元のタスク表を一切書き換えず、問題のあるセルの行番号・列名・理由だけを実行ログへ出す無料GASを作ります。タスク名や担当者名そのものはログへ残しません。
先に知っておきたいこと
- 対象はGoogleスプレッドシートのTasksシートです。
- 初期設定はdryRunをtrueに固定し、セルの修正、行削除、メール送信、ファイル作成を行いません。
- 検査対象はタスクID、タスク名、区分、状態の4列です。期限や数値は変更しません。
- ログへ出すのは行番号、列名、検出理由だけです。セル値や個人情報は出しません。
- 余分な空白を検出しても自動修正しません。意図した空白かどうかを人が確認してください。
準備するシート
コピーしたスプレッドシートにTasksという名前のシートを作り、1行目へ次の見出しを入れます。
- A列: タスクID。空欄にせず、行を識別できる値
- B列: タスク名。実行する作業
- C列: 区分。制作、確認、連絡など
- D列: 状態。未着手、進行中、完了など
- E列: 期限。このコードでは読み取りません
テスト用として2〜5行だけ入力してください。先頭・末尾の半角空白、全角空白、連続空白、改行を含むサンプルを1つずつ作ると、検出結果を確認しやすくなります。
コピーして使える最小コード
const TASK_TEXT_AUDIT_CONFIG = Object.freeze({
sheetName: 'Tasks',
headerRow: 1,
maxRows: 100,
dryRun: true,
expectedHeaders: ['タスクID', 'タスク名', '区分', '状態']
});
function auditTaskTextWhitespaceFree() {
const config = TASK_TEXT_AUDIT_CONFIG;
validateTaskTextAuditConfig_(config);
if (config.dryRun !== true) {
throw new Error('安全のためdryRun=trueで実行してください。');
}
const sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(config.sheetName);
if (!sheet) {
throw new Error('Tasksシートが見つかりません。');
}
const headerValues = sheet
.getRange(config.headerRow, 1, 1, config.expectedHeaders.length)
.getDisplayValues()[0];
const missingHeaders = config.expectedHeaders.filter((name, index) => {
return String(headerValues[index] || '').trim() !== name;
});
if (missingHeaders.length > 0) {
throw new Error('見出しが一致しません: ' + missingHeaders.join(', '));
}
const lastRow = sheet.getLastRow();
if (lastRow <= config.headerRow) {
console.log('対象0件です。');
return;
}
const rowCount = Math.min(
lastRow - config.headerRow,
config.maxRows
);
const rows = sheet
.getRange(config.headerRow + 1, 1, rowCount, 4)
.getDisplayValues();
const issues = [];
const seenTaskIds = new Set();
rows.forEach((row, offset) => {
const sheetRow = config.headerRow + 1 + offset;
const taskId = String(row[0] || '');
if (!taskId.trim()) {
issues.push({
row: sheetRow,
column: 'タスクID',
reason: '空欄'
});
} else {
const normalizedId = normalizeForCompare_(taskId);
if (seenTaskIds.has(normalizedId)) {
issues.push({
row: sheetRow,
column: 'タスクID',
reason: '正規化後に重複'
});
}
seenTaskIds.add(normalizedId);
}
config.expectedHeaders.forEach((header, columnIndex) => {
const value = String(row[columnIndex] || '');
detectTextIssues_(value).forEach(reason => {
issues.push({
row: sheetRow,
column: header,
reason
});
});
});
});
const summary = issues.reduce((counts, issue) => {
counts[issue.reason] = (counts[issue.reason] || 0) + 1;
return counts;
}, {});
issues.slice(0, 100).forEach(issue => {
console.log(
`行${issue.row} / ${issue.column} / ${issue.reason}`
);
});
console.log(JSON.stringify({
checkedRows: rowCount,
issueCount: issues.length,
summary
}));
console.log('セル値・担当者名・タスク名はログへ出していません。');
}
function validateTaskTextAuditConfig_(config) {
if (config.dryRun !== true) {
throw new Error('dryRunはtrue固定です。');
}
if (!Number.isInteger(config.maxRows) ||
config.maxRows < 1 ||
config.maxRows > 100) {
throw new Error('maxRowsは1〜100の整数にしてください。');
}
if (!Array.isArray(config.expectedHeaders) ||
config.expectedHeaders.length !== 4) {
throw new Error('expectedHeadersは4列にしてください。');
}
}
function detectTextIssues_(value) {
const reasons = [];
if (!value) return reasons;
if (/^[ \t]|[ \t]$/.test(value)) {
reasons.push('先頭または末尾に半角空白');
}
if (/^ | $/.test(value)) {
reasons.push('先頭または末尾に全角空白');
}
if (/ {2,}/.test(value)) {
reasons.push('半角空白が連続');
}
if (/ {2,}/.test(value)) {
reasons.push('全角空白が連続');
}
if (/[\r\n]/.test(value)) {
reasons.push('改行を含む');
}
if (/[\u200B-\u200D\uFEFF]/.test(value)) {
reasons.push('ゼロ幅文字を含む');
}
return reasons;
}
function normalizeForCompare_(value) {
return String(value || '')
.normalize('NFKC')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}このコードが確認すること
- 先頭・末尾の半角空白
- 先頭・末尾の全角空白
- 2文字以上続く半角空白または全角空白
- セル内の改行
- コピー時に混ざることがあるゼロ幅文字
- NFKC正規化と空白整理後に同じになるタスクID
値の自動置換は行いません。「区切りとして必要な空白」「意図した改行」もあり得るため、検出後に対象セルを目視します。
実行手順
- 元データではなく、スプレッドシートをコピーして検証用にします。
- 拡張機能からApps Scriptを開き、コードを貼り付けて保存します。
- TASK_TEXT_AUDIT_CONFIGのsheetNameがTasks、dryRunがtrue、maxRowsが100以下であることを確認します。
- auditTaskTextWhitespaceFreeを選び、初回だけ必要なスプレッドシート権限を確認して実行します。
- 実行ログで、行番号・列名・理由を確認します。
- 該当セルをシート上で開き、意図しない空白だけを人が修正します。
- もう一度実行し、issueCountが減ったことを確認します。
失敗しやすい点と制限
- 見出し名や列順が違うと安全のため停止します。コード側かシート側を安易に合わせず、コピー環境で確認してください。
- getDisplayValuesを使うため、数式の計算結果を文字列として検査します。数式そのものは変更しません。
- 全角・半角の表記統一や誤字判定は行いません。この記事の無料範囲は空白・改行・ゼロ幅文字の監査です。
- 100行を超えるデータは先頭100行だけ確認します。範囲を広げる前に少量で実行時間とログ量を確認してください。
- 個人名や顧客情報をタスク名へ含めない運用を推奨します。コードは値をログへ出しませんが、シート自体の共有範囲は別途確認が必要です。
- 実行回数や処理時間にはGoogle Apps Script側の上限があります。
公開前チェックリスト
- dryRunはtrueのままか
- 元のTasksシートを更新する処理が入っていないか
- タスク名や担当者名をログへ出していないか
- maxRowsは100以下か
- 見出し4列が期待どおりか
- 自動修正ではなく、人が対象セルを確認する運用か
- コピー環境で再実行し、検出理由が理解できたか
まとめ
タスク表の集計が崩れる原因は、大きな設定ミスだけではありません。見えにくい空白や改行を「更新前に、値を出さず、少量で確認する」だけでも、朝の確認や期限別集計を整えやすくなります。
期限・区分・優先度を使って朝のToDo確認まで整えたい方へ

GasおじのTips商品一覧を見る

※学習・検証用の例です。実データへ適用する前にコピー環境で確認してください。Google公式・Google公認の商品ではありません。
