Last active
February 1, 2021 01:39
-
-
Save omas-public/0623403c8d2b7866330a81129e5e9b06 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* checkAbsence.js | |
# 欠席者メール送信プログラム | |
Googleフォームと連携して欠席者のリストを送信するプログラム,Googleフォームのデータから | |
土日はメールの送信をrejectしています,祝祭日は未対応です(gas.sendEmail -> isWeekDay) | |
日別,週別,月別,あるいはすべてのデータを送信します(自動化は別途トリガーで)。 | |
## 使い方 | |
- フォームデータのシート名を data に rename してください | |
- [メイルアドレス,名前] のsheetを作り user に rename してください(一行目はヘッダーにすること) | |
- 本使用の際は settings.sendMailの値を gas.dummyMail から gas.sendEmail に置換してください | |
## トリガー | |
- notificationMail -> 講義中の時間にトリガーをセット(学生に連絡メールが行きます) | |
- dailyMail -> 講義終了後にトリガーをセット (登録者に報告が行きます) | |
- mailWeekly -> 週明けにトリガーをセット (登録者に報告が行きます) | |
- mailMonthly -> 月初めにトリガーをセット (登録者に報告が行きます) | |
講義中に出席フォームの記入をさせてください,忘れている方には報告のメールが届きます | |
日別のメールが送られるまでにフォームの記入をさせてください | |
## Update | |
- 2020/01/19 | |
- ソートのミスを修正 | |
- 欠席者へのメール送信を分離 | |
- 2020/01/06 | |
- 日本時間との時差による不具合を修正しました | |
- dailyメールに累積の欠席情報を追加しました | |
- 土日はメールが配信されないようにしました(トリガー) | |
- 欠席者に欠席報告メールを送るようにしました | |
- ライブラリを整理して依存関係をまとめました | |
## パラメータ(settings) | |
- mailAddress: 送りたいメールアドレス | |
- mailaddress = 'mailaddress' | |
- mailaddress = 'mailaddress1, mailaddress2, ...' // 複数のメールアドレスに送りたい場合 | |
- dailyKey: 日付のデータを送る | |
- dailyKey = '2020/05/23' // 特定の日のデータを送る(YYYY/MM/DD) | |
- dailyKey = '' // 本日のデータを送る | |
- nWeeksBefore: 週ごとのデータを送る | |
- nWeeksBefore = 0 // 今週のデータを送る | |
- nWeeksBefore = 1 // 先週のデータを送る | |
- nMonthsBefore: 月ごとのデータを送る | |
- nMonthsBefore = 0 // 今月のデータを送る | |
- nMonthsBefore = 1 // 先月のデータを送る | |
- datasheet: 出席データシート | |
- usersheet: メールアドレス,名前のシート | |
*/ | |
const settings = { | |
mailAddress: '[email protected], [email protected], [email protected]', | |
dailyKey: '', | |
nWeeksBefore: 1, | |
nMonthsBefore: 1, | |
datasheet: 'data', | |
usersheet: 'user', | |
sendMail: gas.dummyMail | |
} | |
function notificationMail () { | |
const date = settings.dailyKey || gas.toJST(gas.today()) | |
const data = absentDaysMap.has(date) ? absentDaysMap.get(date) : [] | |
data.forEach(id => { // 欠席者にメールを送付 | |
settings.sendMail(id, `欠席 ${date}日`, `${userMap.get(id)} さんの ${date}日の欠席をお知らせします`) | |
}) | |
} | |
function mailDaily() { | |
const exception = '全員出席あるいは講義がない日' | |
const date = settings.dailyKey || gas.toJST(gas.today()) | |
const data = absentDaysMap.has(date) ? absentDaysMap.get(date) : [] | |
const title = `${date}日の欠席 [ ${data.length || 0} / ${userMap.size} 人 ]` | |
const daily = Array.from(data, id => userMap.get(id)).join('\n') || exception | |
const contents = [daily, getStatistics()].join('\n'.repeat(2)) | |
settings.sendMail(settings.mailAddress, title, contents) // 登録メールに欠席者を配信 | |
} | |
function mailAll() { | |
const title = `欠席カウント(Total) ${gas.toJST(gas.today())}現在` | |
const contents = getStatistics() | |
settings.sendMail( | |
settings.mailAddress, title, contents | |
) | |
} | |
function mailWeekly() { | |
const [title, contents] = createMailContents('Weekly', jst.getWeekDates(-settings.nWeeksBefore).map(gas.toJST)) | |
settings.sendMail( | |
settings.mailAddress, title, contents | |
) | |
} | |
function mailMonthly() { | |
const [title, contents] = createMailContents('Monthly', jst.getMonthDates(-settings.nMonthsBefore).map(gas.toJST)) | |
settings.sendMail( | |
settings.mailAddress, title, contents | |
) | |
} | |
const createMailContents = (title, dates) => { | |
const _title = `欠席リスト(${title})[ ${dates[0]} - ${dates[dates.length - 1]} ]` | |
const _data = cvfun.countMapValues(utils.reverseMap(new Map(cvfun.filterKeys(absentDaysMap, dates)))) | |
return [ | |
_title, cvfun.shapefn(userMap)(_data.sort((a, b) => b[0] - a[0])) | |
] | |
} | |
const getStatistics = () => { | |
const separator = '-'.repeat(80) | |
const _data = cvfun.countMapValues(absentUserMap) | |
return [ | |
`TotalCount: ${gas.toJST(gas.today())}現在`, | |
`${separator}`, | |
cvfun.shapefn(userMap)(_data.sort((a, b) => b[0] - a[0])) | |
].join('\n') | |
} | |
/** | |
* GASに依存するライブラリ | |
*/ | |
const gas = { | |
/** | |
* シートから指定された範囲のデータを配列で取得する | |
* @param {String} sheet | |
* @param {number} rowIndex | |
* @param {number} colIndex | |
* @param {number} colNum | |
* @return {Array} | |
*/ | |
_getRangeValues: sheet => (rowIndex, colIndex, colNum) => sheet | |
.getRange(rowIndex, colIndex, sheet.getLastRow() - 1, colNum) | |
.getValues(), | |
/** | |
* sheet上の指定範囲のデータを配列で返す | |
* @param {String} sheetname | |
* @param {Array<Number, Number>} [rowIndex, colIndex] | |
* @param {Number} colNum | |
* @param {Function} formatfn | |
* @return Array<String, String> | |
*/ | |
createMatrix: (app => sheetname => ( | |
[rowIndex, colIndex], colNum) => convertfn => { | |
const rangeValues = gas._getRangeValues(app.getSheetByName(sheetname))( | |
rowIndex, | |
colIndex, | |
colNum | |
) | |
return Array.from(rangeValues, convertfn) | |
})(SpreadsheetApp.getActiveSpreadsheet()), | |
/** | |
* GMTをYYYY/MM/dd形式のJSTに変換 | |
* @param {String} format | |
* @param {String} date | |
* @return String | |
*/ | |
toJST: (format => d => Utilities.formatDate(new Date(d), 'JST', format))('YYYY/MM/dd'), | |
/** | |
* 本日のJST(00:00:00)の日付を返す | |
* @return Date | |
*/ | |
today: (_) => new Date(gas.toJST(new Date())), | |
/** | |
* メイルを送る | |
* @param {String} mailAddress | |
* @param {String} title | |
* @param {String} contents | |
* @return void | |
*/ | |
sendEmail: (mailAddress, title, contents) => { | |
const separator = '-'.repeat(80) | |
const isWeekDay = date => { | |
const day = new Date(date).getDay() | |
return day > 0 && day < 6 | |
} | |
if (isWeekDay(gas.today())) { | |
MailApp.sendEmail( | |
mailAddress, title, [title, separator, contents].join('\n') | |
) | |
} | |
return | |
}, | |
/** | |
* ダミーのメイルを送る | |
* @param {String} mailAddress | |
* @param {String} title | |
* @param {String} contents | |
* @return void | |
*/ | |
dummyMail: (mailAddress, title, contents) => { | |
console.log([ | |
`mailAddress: ${mailAddress}`, | |
`title: ${title}`, | |
`contents: ${contents}` | |
].join('\n')) | |
} | |
} | |
/** | |
* 汎用ライブラリ | |
*/ | |
const utils = { | |
/** | |
* Arrayの先頭を返す | |
* @param {Array<any>} matrix | |
* @return any | |
*/ | |
head: matrix => matrix[0], | |
/** | |
* Arrayの先頭以外を返す | |
* @param {Array<any>} matrix | |
* @return Array<any> | |
*/ | |
tail: matrix => matrix.slice(1), | |
/** | |
* swap | |
* @param {Array<String,String>} matrix | |
* @return Map<String, Array> | |
*/ | |
swap: ([k, v]) => [v, k], | |
/** | |
* ArrayをMapに変換 | |
* @param {Array<any,any>} matrix | |
* @return Map<any, Array<any>> | |
*/ | |
makeMap: matrix => | |
matrix.reduce((cons, [key, ...value]) => { | |
if (!cons.has(key)) cons.set(key, []) | |
return cons.set(key, [...cons.get(key), ...value]) | |
}, new Map()), | |
/** | |
* MapのValuesのそれぞれをKeyとしたMapを返す | |
* @param {Map<any, Array<any>>} map | |
* @return Map<any, Array<any>> | |
*/ | |
reverseMap: map => { | |
const _map = new Map() | |
map.forEach((values, key) => { | |
for (let v of values) { | |
if (!_map.has(v)) _map.set(v, []) | |
_map.set(v, [..._map.get(v), key]) | |
} | |
}) | |
return _map | |
}, | |
} | |
/** | |
* 日付に関するライブラリ(GAS依存) | |
*/ | |
const jst = { | |
/** | |
* dateの週の日曜日を返す | |
* @param {String} date | |
* @return Date | |
*/ | |
getSunday: d => (d => new Date(d.setDate(d.getDate() - d.getDay())))(new Date(d)), | |
/** | |
* | |
* @param {String} d | |
* @param {Number} n | |
* @return Date | |
*/ | |
addnDays: d => (d => n => new Date(d.setDate(d.getDate() + n)))(new Date(d)), | |
/** | |
* n週間を足した日付を返す | |
* @param {Number} n | |
* @return Date | |
*/ | |
addnWeeks: d => n => jst.addnDays(new Date(d))(n * 7), | |
/** | |
* nヶ月を足した日付を返す | |
* @param {String} d | |
* @param {Number} n | |
* @return Date | |
*/ | |
addnMonths: d => (d => n => new Date(d.setMonth(d.getMonth() + n)))(new Date(d)), | |
/** | |
* 期間内の日付を返す | |
* @param {Number} startTime | |
* @param {Number} endTime | |
* @return Array<String> | |
*/ | |
getDays: (startTime, endTime) => { | |
const DAY = 86400000 | |
const result = [] | |
for (let time = startTime; time < endTime; time += DAY) { | |
result.push(time) | |
} | |
return result | |
}, | |
/** | |
* 現在にnヶ月を足した週のリストを返す | |
* @param {Number} n | |
* @return Array<String> | |
*/ | |
getWeekDates: (n = 0) => { | |
const WEEK = 7 | |
const date = jst.addnWeeks(gas.today())(n) | |
const startTime = jst.getSunday(date).valueOf() | |
const endTime = jst.addnDays(startTime)(WEEK).valueOf() | |
return jst.getDays(startTime, endTime) | |
}, | |
/** | |
* 現在にnヶ月を足した月の日付をリストで返す | |
* @param {Number} n | |
* @return Array<String> | |
*/ | |
getMonthDates: (n = 0) => { | |
const FIRST_DAY = 1 | |
const NEXT_MONTH = 1 | |
const startTime = jst.addnMonths(gas.today())(n).setDate(FIRST_DAY) | |
const endTime = jst.addnMonths(startTime)(NEXT_MONTH).valueOf() | |
return jst.getDays(startTime, endTime) | |
}, | |
} | |
/** | |
* このアプリのデータ変換ライブラリ(convert function) | |
*/ | |
const cvfun = { | |
/** | |
* 引数をそのまま返す | |
* @param {any} value | |
* @return any | |
* @example const val = cvfun.identity(42) // val => 42 | |
*/ | |
identity: value => value, | |
/** | |
* Mailアドレスからユーザー名を取り出す | |
* @param {String} mail | |
* @return String | |
*/ | |
userName: mail => mail.split('@')[0], | |
/** | |
* GMT時間を JST時間に変換 | |
* @param {String} date | |
* @param {String} mail | |
* @return Array<String, String> | |
*/ | |
convertData: ([date, mail]) => [gas.toJST(date), mail], | |
/** | |
* 学籍番号,名前を連結する | |
* @param {String} mail | |
* @param {String} name | |
* @return Array<String, String> | |
*/ | |
convertUser: ([mail, name]) => [mail, `${cvfun.userName(mail)} (${name})`], | |
/** | |
* 欠席者のデータをシェーピング | |
* @param {Map<String, String>} userMap | |
* @param {Array<>} data | |
* @return string | |
*/ | |
shapefn: userMap => data => Array.from(data, (([n, id, ...rest]) => `${n}回 ${userMap.get(id)} [${rest}]`)).join('\n'), | |
/** | |
* userListからdataMapに存在しないuserデータを日付とuserのMapにして返す | |
* @param {Array<String>} users | |
* @param {Map} dataMap | |
* @return Map<String, Array<String>> | |
*/ | |
deltaMap: userList => dataMap => { | |
const _map = new Map() | |
for (const [date, users] of dataMap) { | |
_map.set( | |
date, | |
userList.filter(user => !users.includes(user)) | |
) | |
} | |
return _map | |
}, | |
/** | |
* keysにマッチするデータを返す | |
* @param {Map} dataMap | |
* @param {Array} keys | |
* @return Array<any> | |
*/ | |
filterKeys: (dataMap, keys) => [...dataMap.entries()].filter(v => keys.includes(utils.head(v))), | |
/** | |
* mapのvaluesの数を付け加えたListを返す | |
* @param {Map} map | |
* @return Array<Number, String, Array<String>> | |
*/ | |
countMapValues: map => Array.from(map.entries(), ([key, values]) => [values.length, key, values]), | |
} | |
/** | |
* 初期化プログラム | |
* | |
*/ | |
const [userMap, absentDaysMap, absentUserMap] = (function initialize() { | |
// spreadsheet(user)のデータをマップ化 | |
const _userMap = utils.makeMap( | |
gas.createMatrix(settings.usersheet)([2, 1], 2)(cvfun.convertUser) | |
) | |
// spreadsheet(data)のデータをマップ化 | |
const _dataMap = utils.makeMap( | |
gas.createMatrix(settings.datasheet)([2, 1], 2)(cvfun.convertData) | |
) | |
// 日毎の欠席 | |
const _absentDaysMap = cvfun.deltaMap([..._userMap.keys()])(_dataMap) | |
return [ | |
_userMap, _absentDaysMap, utils.reverseMap(_absentDaysMap) | |
] | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment