積読リストの見直し期限切れを放置する前に|GASで登録日と次回確認日をdry-run監査
ツールおじ|GoogleアカウントをAIエージェント化
「あとで読む」に追加した本や動画が増え、何から見直せばよいか分からなくなっていませんか。登録日は残っていても、次回確認日が空欄だったり、確認期限を過ぎた項目が埋もれたりすると、リストは少しずつ使いにくくなります。
この記事では、スプレッドシートの積読・動画リストを変更せずに、見直し期限切れ・日付不備・長期放置の候補だけをGASで確認します。初期設定はdry-runで、元データのセル、ステータス、URLを更新しません。
先に知っておきたいこと
- できること:登録日と次回確認日を検査し、要確認の行番号と理由をログへ出します。
- できること:次回確認日が空欄でも、登録日から一定日数を超えた行を長期放置候補として見つけます。
- できないこと:記事や動画を自動で開く、視聴済みにする、行を削除する、外部へ送信する処理は含みません。
- 安全設定:`dryRun: true` と `writeReport: false` が初期値です。元の一覧は常に読み取り専用です。
- ログへ出すのは行番号、区分、経過日数、判定理由だけです。タイトル、URL、メモ本文は出しません。
準備するシート
シート名を `Watchlist` にし、1行目へ次の見出しを入れます。
- A列:タイトル
- B列:区分(本、動画、記事など)
- C列:登録日
- D列:次回確認日
- E列:状態(未着手、進行中、完了、保留など)
2行目以降へ確認したい項目を入れてください。URLや個人的なメモが別の列にあっても、このコードは読みません。
判定日は実行日の午前0時です。状態が `完了` または `視聴済み` の行は対象外にします。次回確認日が過ぎた行に加え、次回確認日が空欄で登録から30日を超えた行も確認候補になります。
コピーして使える最小コード
function auditWatchlistReviewDatesFree() {
const config = {
sourceSheetName: 'Watchlist',
reportSheetName: 'ReviewAudit',
staleAfterDays: 30,
maxRows: 1000,
dryRun: true,
writeReport: false,
completedStatuses: ['完了', '視聴済み']
};
validateWatchlistAuditConfig_(config);
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const source = spreadsheet.getSheetByName(config.sourceSheetName);
if (!source) {
throw new Error('対象シートが見つかりません: ' + config.sourceSheetName);
}
const lastRow = source.getLastRow();
if (lastRow < 2) {
console.log('監査対象は0件です。見出しの下へデータを入力してください。');
return;
}
const targetRows = lastRow - 1;
if (targetRows > config.maxRows) {
throw new Error(
'対象行が上限を超えています。少量で確認してから maxRows を調整してください。' +
' rows=' + targetRows
);
}
const headers = source.getRange(1, 1, 1, 5).getDisplayValues()[0];
const expectedHeaders = ['タイトル', '区分', '登録日', '次回確認日', '状態'];
expectedHeaders.forEach(function(expected, index) {
if (normalizeWatchlistText_(headers[index]) !== expected) {
throw new Error(
'見出しが一致しません。' + (index + 1) + '列目は「' + expected + '」にしてください。'
);
}
});
const values = source.getRange(2, 1, targetRows, 5).getValues();
const today = startOfLocalDay_(new Date());
const findings = [];
const summary = {
checked: 0,
skippedCompleted: 0,
invalid: 0,
overdue: 0,
staleWithoutReviewDate: 0,
ok: 0
};
values.forEach(function(row, offset) {
const rowNumber = offset + 2;
const title = normalizeWatchlistText_(row[0]);
const itemType = normalizeWatchlistText_(row[1]) || '未設定';
const registeredAt = parseWatchlistDate_(row[2]);
const reviewAt = parseWatchlistDate_(row[3]);
const status = normalizeWatchlistText_(row[4]);
if (!title && !row[2] && !row[3] && !status) return;
summary.checked += 1;
if (config.completedStatuses.includes(status)) {
summary.skippedCompleted += 1;
return;
}
const reasons = [];
let elapsedDays = '';
if (!title) reasons.push('タイトル未入力');
if (row[2] && !registeredAt) reasons.push('登録日の形式不正');
if (row[3] && !reviewAt) reasons.push('次回確認日の形式不正');
if (registeredAt) {
elapsedDays = diffDays_(registeredAt, today);
if (elapsedDays < 0) reasons.push('登録日が未来');
}
if (reviewAt) {
const overdueDays = diffDays_(reviewAt, today);
if (overdueDays > 0) {
reasons.push('次回確認日を' + overdueDays + '日超過');
summary.overdue += 1;
}
} else if (registeredAt && elapsedDays > config.staleAfterDays) {
reasons.push('次回確認日なしで登録から' + elapsedDays + '日経過');
summary.staleWithoutReviewDate += 1;
}
if (reasons.length === 0) {
summary.ok += 1;
return;
}
if (reasons.some(function(reason) { return reason.includes('未入力') || reason.includes('形式不正') || reason.includes('未来'); })) {
summary.invalid += 1;
}
findings.push([
rowNumber,
itemType,
elapsedDays,
reasons.join(' / ')
]);
});
console.log(JSON.stringify({
mode: config.dryRun ? 'DRY_RUN' : 'REPORT_ONLY',
summary: summary,
findingRows: findings.map(function(item) { return item[0]; })
}));
console.log('タイトル・URL・メモ本文はログへ出していません。');
if (config.dryRun === true || config.writeReport !== true) {
console.log('dry-runのためシートへ書き込みません。要確認件数=' + findings.length);
return;
}
writeWatchlistAuditReport_(spreadsheet, config.reportSheetName, findings, today);
}
function validateWatchlistAuditConfig_(config) {
if (!config || typeof config !== 'object') {
throw new Error('configがありません。');
}
if (!config.sourceSheetName || !config.reportSheetName) {
throw new Error('sourceSheetNameとreportSheetNameを指定してください。');
}
if (config.sourceSheetName === config.reportSheetName) {
throw new Error('元シートとレポートシートは別名にしてください。');
}
if (!Number.isInteger(config.staleAfterDays) || config.staleAfterDays < 1 || config.staleAfterDays > 3650) {
throw new Error('staleAfterDaysは1〜3650の整数にしてください。');
}
if (!Number.isInteger(config.maxRows) || config.maxRows < 1 || config.maxRows > 5000) {
throw new Error('maxRowsは1〜5000の整数にしてください。');
}
if (!Array.isArray(config.completedStatuses) || config.completedStatuses.length === 0) {
throw new Error('completedStatusesを1件以上指定してください。');
}
}
function parseWatchlistDate_(value) {
if (value instanceof Date && !isNaN(value.getTime())) {
return startOfLocalDay_(value);
}
if (typeof value !== 'string' || !value.trim()) return null;
const matched = value.trim().match(/^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$/);
if (!matched) return null;
const parsed = new Date(Number(matched[1]), Number(matched[2]) - 1, Number(matched[3]));
if (
parsed.getFullYear() !== Number(matched[1]) ||
parsed.getMonth() !== Number(matched[2]) - 1 ||
parsed.getDate() !== Number(matched[3])
) return null;
return startOfLocalDay_(parsed);
}
function startOfLocalDay_(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
function diffDays_(fromDate, toDate) {
const oneDay = 24 * 60 * 60 * 1000;
return Math.floor((toDate.getTime() - fromDate.getTime()) / oneDay);
}
function normalizeWatchlistText_(value) {
return String(value == null ? '' : value).trim();
}
function writeWatchlistAuditReport_(spreadsheet, reportSheetName, findings, checkedAt) {
let report = spreadsheet.getSheetByName(reportSheetName);
if (!report) report = spreadsheet.insertSheet(reportSheetName);
report.clearContents();
report.getRange(1, 1, 1, 5).setValues([[
'確認日', '元の行番号', '区分', '登録からの日数', '判定理由'
]]);
if (findings.length === 0) return;
const rows = findings.map(function(item) {
return [checkedAt, item[0], item[1], item[2], item[3]];
});
report.getRange(2, 1, rows.length, 5).setValues(rows);
}実行手順
- 元のスプレッドシートをコピーし、検証用ファイルで試します。
- `拡張機能` から `Apps Script` を開き、コードを貼り付けます。
- シート名と1行目の見出しが説明どおりか確認します。
- `dryRun: true` と `writeReport: false` のまま `auditWatchlistReviewDatesFree` を実行します。
- 初回だけ、スプレッドシートを読むための権限を確認します。想定外の権限が表示されたら承認せず停止してください。
- 実行ログで確認件数と要確認の行番号を見ます。タイトルやURLがログへ出ていないことも確認します。
- 少量のサンプルで判定が合うことを確認してから、必要に応じて対象行上限や経過日数を調整します。
レポートシートへ出したい場合
dry-runで結果を確認した後だけ、`dryRun: false` と `writeReport: true` に変更すると、別シート `ReviewAudit` に行番号、区分、経過日数、理由を書き出します。
この処理でも `Watchlist` は変更しません。ただし、同名の `ReviewAudit` がすでにある場合は内容を消して作り直します。必要な既存レポートがある場合は、先にシート名を変更するか、コピーを保存してください。
失敗しやすい点と制限
- 見出し違い:全角・半角を含め、1行目の5項目が一致しない場合は停止します。
- 日付形式:スプレッドシートの日付値、または `YYYY-MM-DD`、`YYYY/MM/DD` を対象にします。曖昧な文字列は不正として扱います。
- 時刻とタイムゾーン:日単位の簡易判定です。実行環境やシートのタイムゾーンが異なる場合は境界日をサンプルで確認してください。
- 対象件数:初期値は1000行までです。大量データでは実行時間の上限に達する可能性があります。
- 完了状態:`完了` と `視聴済み` だけを除外します。自分の運用に合わせて一覧を調整してください。
- 自動判断ではありません。期限切れ候補が、読むべき優先順位や内容の価値を決めるわけではありません。
- URLの到達確認は行いません。リンク切れ確認とは別の監査です。
公開前チェックリスト
- 元ファイルではなくコピーで少量テストした
- `dryRun: true` と `writeReport: false` を確認した
- 実行ログにタイトル、URL、メモ本文が出ていない
- 見出し、日付、完了状態の定義を自分のシートに合わせた
- 要確認行を人が見直してから次の操作を判断する
- 想定外の権限、行数、日付形式を見つけたら実行を止める
まとめ
積読・動画リストは、登録するだけでなく「いつ見直すか」を確認できると整理しやすくなります。まずは元データを変えないdry-runで、期限切れ、日付不備、長期放置の行だけを安全に絞り込んでください。
読書・動画ウォッチリスト整理GAS Proを見る

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

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