タスク表の余分な空白を集計前に発見|GASで文字列をdry-run監査

タスク表の余分な空白を集計前に発見|GASで文字列をdry-run監査

ツールおじ|GoogleアカウントをAIエージェント化

ツールおじ|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

値の自動置換は行いません。「区切りとして必要な空白」「意図した改行」もあり得るため、検出後に対象セルを目視します。

実行手順

  1. 元データではなく、スプレッドシートをコピーして検証用にします。
  2. 拡張機能からApps Scriptを開き、コードを貼り付けて保存します。
  3. TASK_TEXT_AUDIT_CONFIGのsheetNameがTasks、dryRunがtrue、maxRowsが100以下であることを確認します。
  4. auditTaskTextWhitespaceFreeを選び、初回だけ必要なスプレッドシート権限を確認して実行します。
  5. 実行ログで、行番号・列名・理由を確認します。
  6. 該当セルをシート上で開き、意図しない空白だけを人が修正します。
  7. もう一度実行し、issueCountが減ったことを確認します。

失敗しやすい点と制限

  • 見出し名や列順が違うと安全のため停止します。コード側かシート側を安易に合わせず、コピー環境で確認してください。
  • getDisplayValuesを使うため、数式の計算結果を文字列として検査します。数式そのものは変更しません。
  • 全角・半角の表記統一や誤字判定は行いません。この記事の無料範囲は空白・改行・ゼロ幅文字の監査です。
  • 100行を超えるデータは先頭100行だけ確認します。範囲を広げる前に少量で実行時間とログ量を確認してください。
  • 個人名や顧客情報をタスク名へ含めない運用を推奨します。コードは値をログへ出しませんが、シート自体の共有範囲は別途確認が必要です。
  • 実行回数や処理時間にはGoogle Apps Script側の上限があります。

公開前チェックリスト

  • dryRunはtrueのままか
  • 元のTasksシートを更新する処理が入っていないか
  • タスク名や担当者名をログへ出していないか
  • maxRowsは100以下か
  • 見出し4列が期待どおりか
  • 自動修正ではなく、人が対象セルを確認する運用か
  • コピー環境で再実行し、検出理由が理解できたか

まとめ

タスク表の集計が崩れる原因は、大きな設定ミスだけではありません。見えにくい空白や改行を「更新前に、値を出さず、少量で確認する」だけでも、朝の確認や期限別集計を整えやすくなります。

期限・区分・優先度を使って朝のToDo確認まで整えたい方へ

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

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


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事の販売者

ツールおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

この販売者が書いた他の記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
  • Gmailの自動ラベルが競合する前に確認|GASで仕分けルールをdry-run監査

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)

関連のおすすめ記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化