Skip to content

Instantly share code, notes, and snippets.

@omas-public
Last active February 1, 2021 01:39
Show Gist options
  • Save omas-public/0623403c8d2b7866330a81129e5e9b06 to your computer and use it in GitHub Desktop.
Save omas-public/0623403c8d2b7866330a81129e5e9b06 to your computer and use it in GitHub Desktop.
/* 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