每週一早上,要把「這週誰值班、誰備援」寄給同事。每次手動寄會忘,所以交給試算表:到了週一,它自己讀值班表、組出信、寄出去。
這件事要解決三個問題:什麼時候執行、怎麼決定「這週」是哪一週、重複安裝觸發器會不會寄兩次。
先看流程
觸發器只負責叫醒函式。要寄給誰、寄哪一週,都由函式自己判斷。
先準備
- 試算表建一個「值班表」分頁,第 1 列是表頭,之後每週一列:A 欄是該週週一的日期(要是真的日期,不是文字)、B 欄值班人、C 欄備援。
- 在「擴充功能 → Apps Script」把四個區塊依序貼進去並儲存。
- 把
TEST_RECIPIENT換成你自己的信箱,先寄給自己測試。 - 函式選單選
sendWeeklyRoster按「執行」。這支程式會寄信、也會建觸發器,Google 會分別要求授權;授權畫面列出的權限請自己看清楚再同意。 - 專案的時區與試算表的時區,建議設成一樣:觸發器的「8 點」依專案時區(官方文件寫預設是腳本的時區),值班表的日期依試算表時區。
區塊一:算出這一週的週一
/**
* @OnlyCurrentDoc
*/
const SHEET_NAME = "值班表";
const HANDLER = "sendWeeklyRoster";
const TEST_RECIPIENT = "you@example.com"; // 換成你要收信的信箱;收件者寫在程式裡,不從儲存格讀
function isDate(value) {
return Object.prototype.toString.call(value) === "[object Date]";
}
// 這一天所屬那一週的週一 00:00(週日算前一個週一)
function mondayOf(date) {
const monday = new Date(date.getFullYear(), date.getMonth(), date.getDate());
monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7));
return monday;
}
function formatDay(date) {
const pad = (n) => (n < 10 ? "0" + n : String(n));
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
}
// 把某個時間點換成指定時區看到的年月日(本地午夜),腳本時區與試算表時區不同時才不會差一天
function dayInZone(date, timeZone) {
const [year, month, day] = Utilities.formatDate(date, timeZone, "yyyy-MM-dd").split("-").map(Number);
return new Date(year, month - 1, day);
}mondayOf回傳某一天所屬那一週的週一 00:00。週日算前一個週一,所以週日、週六執行都會對到同一週。- 它只用年月日欄位計算,不改傳進去的日期。
HANDLER是觸發器要叫醒的函式名稱,後面的清理也靠它。formatDay把日期寫成2026-10-12這種樣子,主旨與記錄都用它。dayInZone把某個時間點換成指定時區看到的年月日。專案時區與試算表時區不同時,儲存格的日期與「現在」才會對到同一天。
區塊二:找本週那一列、組出信
// rows 每列:[週一日期, 值班人, 備援];只比年月日,找不到回傳 null
function pickWeekRow(rows, monday) {
const found = rows.find((row) => isDate(row[0]) && mondayOf(row[0]).getTime() === monday.getTime());
return found || null;
}
function buildRosterMessage(row, monday) {
return {
subject: "本週值班表(" + formatDay(monday) + " 起)",
body: "值班人:" + row[1] + "\n備援:" + row[2],
};
}pickWeekRow只比年月日,所以儲存格裡的日期帶了時間也找得到;空白列與非日期的列會被跳過;找不到回傳null。buildRosterMessage只組信,不寄。主旨含該週週一,內文是值班人與備援。
區塊三:觸發器叫醒的函式
// 觸發器要執行的函式:用「現在」算出這週的週一,找到那一列才寄
function sendWeeklyRoster() {
const book = SpreadsheetApp.getActiveSpreadsheet();
const sheet = book.getSheetByName(SHEET_NAME);
if (!sheet) {
throw new Error("找不到分頁「" + SHEET_NAME + "」");
}
const timeZone = book.getSpreadsheetTimeZone();
const monday = mondayOf(dayInZone(new Date(), timeZone));
const last = sheet.getLastRow();
const rows = (last < 2 ? [] : sheet.getRange(2, 1, last - 1, 3).getValues()).map((row) =>
isDate(row[0]) ? [dayInZone(row[0], timeZone), row[1], row[2]] : row
);
const row = pickWeekRow(rows, monday);
if (row === null) {
Logger.log("值班表沒有 " + formatDay(monday) + " 這週,不寄信");
return;
}
const message = buildRosterMessage(row, monday);
MailApp.sendEmail(TEST_RECIPIENT, message.subject, message.body);
Logger.log("已寄出:" + message.subject);
}- 「業務日」就是這一週的週一,用現在的時間算出來,不是寫死的日期。
- 「現在」與值班表的日期,都先換成試算表時區的年月日再比。
- 值班表找不到這一週就寫記錄、結束,不寄信;整個分頁不存在則丟錯。
- 收件者是
TEST_RECIPIENT,寫在程式裡。就算有人在儲存格填了別的信箱,信也不會寄過去。
區塊四:安裝與清理觸發器
// 只刪自己這個函式的觸發器,回傳刪了幾個
function removeWeeklyTrigger() {
let removed = 0;
ScriptApp.getProjectTriggers().forEach((trigger) => {
if (trigger.getHandlerFunction() === HANDLER) {
ScriptApp.deleteTrigger(trigger);
removed++;
}
});
return removed;
}
// 先清掉舊的再建,連按幾次都只會留一個
function installWeeklyTrigger() {
removeWeeklyTrigger();
ScriptApp.newTrigger(HANDLER).timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(8).create();
}
function listTriggers() {
const triggers = ScriptApp.getProjectTriggers();
if (triggers.length === 0) {
Logger.log("沒有觸發器");
return;
}
triggers.forEach((trigger) => {
Logger.log(trigger.getHandlerFunction() + " / " + trigger.getEventType());
});
}installWeeklyTrigger建立「每週一、早上 8 點那個小時」的時間觸發器。atHour(8)不是 8:00 整,分鐘不固定。- 建立前先呼叫
removeWeeklyTrigger,只刪這個函式自己的觸發器,其他函式的不動。所以重複執行安裝,只會留一個。 listTriggers把現有觸發器列進執行記錄,確認有沒有裝好。- 安裝只要執行一次。想停止每週寄信,執行
removeWeeklyTrigger。 - 官方文件寫
getProjectTriggers只回傳目前使用者在這個專案的觸發器。所以清理與安裝只看得到你自己帳號建的;共用試算表時如果兩個人各自執行安裝,可能各建一個,每週一就會寄兩封。
執行結果
下面用範例資料示範。「值班表」是:
| 列 / 欄 | A | B | C |
|---|---|---|---|
| 1 | 週一日期 | 值班人 | 備援 |
| 2 | 2026-10-12 | 小美 | 阿明 |
| 3 | 2026-10-19 | 阿明 | 小華 |
| 4 | 2026-10-26 | 小華 | 小美 |
| 5 | 2026-11-02 | 小美 | 阿明 |
示範試算表
當天是週六,執行 sendWeeklyRoster 後,執行記錄是「已寄出:本週值班表(2026-10-12 起)」,程式回推到前一個週一 10 月 12 日,找到第一列。信的主旨與內文如下(信箱換成範例,內容是同一段程式組出的):
| 列 / 欄 | A | B | C | D |
|---|---|---|---|---|
| 1 | 收件者 | 主旨 | 值班人 | 備援 |
| 2 | you@example.com | 本週值班表(2026-10-12 起) | 小美 | 阿明 |
示範試算表
接著把 installWeeklyTrigger 執行兩次,再執行 listTriggers,執行記錄只有一行 sendWeeklyRoster / CLOCK。最後執行 removeWeeklyTrigger,回傳 1,再列一次是「沒有觸發器」。這兩個表是預先寫好的結果,不會自己執行程式。
這支程式以誰的身分執行
| 項目 | 說明 |
|---|---|
| 以誰的身分執行 | 官方文件寫:安裝式觸發器以建立它的人的帳號執行 |
| 需要哪些授權 | 寄信與管理觸發器。要求的權限是 script.send_mail 與 script.scriptapp |
| 收件者從哪來 | 寫在程式的 TEST_RECIPIENT,不從儲存格讀 |
| 觸發時間 | 不保證整點;官方文件說明沒呼叫 nearMinute 時,分鐘是隨機的 |
| 每日用量 | 寄信與觸發器執行時間都有每日配額,數字以官方配額頁為準 |
換成你的情境
- 週幾、幾點:
onWeekDay與atHour。 - 分頁與欄位:
SHEET_NAME,以及區塊二的欄位順序。 - 收件者:
TEST_RECIPIENT;要寄給多人,可以改成固定的一串信箱,不要從儲存格讀。 - 信的內容:
buildRosterMessage。
改成每月 1 日寄上個月的彙總
如果要改成「每月 1 日早上寄上個月的彙總」,「業務日」要怎麼算?觸發器要改哪裡?
延伸閱讀
接下來可以
相關文章
查看所有文章星期幾與第幾週:WEEKDAY、WEEKNUM、ISOWEEKNUM 的 type 怎麼選
整理 WEEKDAY 的 10 種 type、WEEKNUM 的週起始日與 21(ISO),以及 ISOWEEKNUM 在跨年那幾天的行為。附對照表、週末判斷、這個月第幾週,以及 ISO 年週代碼的寫法。
Gmail 合併郵件:用 Google 試算表的名單一次寄出個人化信件
說明 Gmail 內建的合併郵件(Mail Merge)怎麼連結 Google 試算表:名單要怎麼排、連結步驟、用 @ 插入合併欄位、每日 1,500 人的限制、不能用在主旨與連結的規則,以及適用方案。依官方公告與說明中心整理。
日期與時間入門:日期序號、DATE、TODAY 與文字日期
從日期序號開始,認識 DATE、TODAY、NOW、YEAR、MONTH、DAY,學會日期加減、分辨文字日期和真日期,並搞清楚顯示格式和儲存格的值是兩回事。
留言討論
共 0 則留言
