まえおき
Hamee Advent Calendar 24日目の記事です。
サンタさんには今流行のホットサンドメーカーをお願いしました。
(昨年サンタさんにお願いした2016ピースパズルは6ヶ月かけて完成させました)
さて、うちの会社では週次の定例MTGがあり、部内の各プロジェクトから代表者が参加します。
議事録は毎回決まったフォーマットで、毎回、前回分をコピーして作成します。そして日付等を書き換えます。
これが結構な手間だったので、毎週MTGが終わった後に自動コピーしていい感じに手直しするGASを先輩と後輩が組んでくれました。
それをちょっとずつメンテしてもらいながら、かれこれ1年以上運用しています。超ありがたい。
最近困っている問題
プロジェクトのかたちや会議体の見直しなどの度に、MTGのルールもいろいろ変わっていくのですが
いつからか、MTGの司会進行は参加者が持ち回りで行うことになりました。
今やってる運用方法
議事録の冒頭に参加者名を並べておき、今回司会の人の名前を赤で表すことにしています。
つまり、毎週の議事録自動作成後にそれを1つずつズラす(前回司会をした人の名前を黒に戻し、その次の人の名前を赤にする)作業が発生します。
…めんどうですね。なのでこれを完全自動化したいと思います。
展望
司会者の行の下にプロジェクト一覧表があるので、これを利用できそうです。
(これもMTGのルールが変わっていく中で、最近追加されました。)
要件
- MTG参加者一覧から毎週一人を司会として選出する
- 順番はなんでもいい
- MTG参加者は各プロジェクトから代表者1名なので、プロジェクト掛け持ちで参加者が重複することもある
- 一時的プロジェクトや現在活動停止中のプロジェクトなど、一覧には名前があるが参加停止中の人もいる(一覧の記載がグレーになっている)
- 別営業所勤務でビデオチャットで参加する人もいて、そういう人は司会をスキップしたい
対応方法案1:プロジェクト一覧に司会列を追加
運用ルール
- 司会列を追加して、司会者に◯つける
-
参加者名が重複している場合、2回目以降はスキップ(ex. No.11 双葉さん, No.1で司会担当済み)- 後述の通り、重複の自動スキップはデメリットになるのでやめた
- 一覧の記載がグレーになっているプロジェクトの参加者はスキップ(ex. No.12 多田さん)
- 司会列に任意の文字が記載されている参加者もスキップ(ex. No.15 神崎さん)
→ 例えば、上の表で今週の司会者が No.9 渋谷さんだった場合、
No.11 双葉さんは来ない可能性があるためスキップ、
No.12 多田さんもにわかロックは一夜限りプロジェクトなのでスキップ、
No.13 アナスタシアさんにバトンタッチしたい。
メリット
- 常にビデオチャットの人や、来週は休みなのでスキップ!というのも司会欄に文字を設定することで対応可
- しかも文字はなんでもいい
- MTG参加者が増えたときに、司会者枠に追加する必要がない(今まで結構忘れてた)
デメリット
- 次の司会者の人がプロジェクト重複でスキップ対象というのがパッと表を見てわからない
- → 重複があればスキップする処理はやめて、重複の場合は手動で「ー」なり「skip」なりを入れてもらう
- 2プロジェクトで参加の場合、2回司会やりたければやることも可能
- → 重複があればスキップする処理はやめて、重複の場合は手動で「ー」なり「skip」なりを入れてもらう
- 今の運用を変更しなければならない
対応方法案2:司会一覧を横並びでテーブル化
枠線を白にしてテーブルで並べてみる(下:わかりにくいので枠黒バージョン)
メリット
- 今の運用を変えなくて良い
- 重複抜きで参加者とは別に表示できるという点でわかりやすい
デメリット
- 余白の扱いが難しくてどうしても不格好になる
- 人が多いかったり名前が長い人がいると1列に入らない可能性がある
- 参加者が増えたり変わったときに司会一覧に追加する処理が必要
- そこまで込みで自動化やるならありだが、この人は常にビデオチャット!などの情報を持てないため、意図的に司会者一覧から除いておくという運用とは併用できない
- このデメリットが大きいので、テーブルにせずに今の文字列の書式のままで正規表現使う案もあるが、同じデメリットを抱えるので却下
ソース
ということで、今回は案1でやってみたいと思います。
前回の議事録のコピーが作成されている前提で、手直し処理にこのロジックを組み込みます。
解説用に丁寧めのコメントを入れました。
function updateDuty() {
const COL_INDEX_NAME = 5; // 参加者名列のindex
const COL_INDEX_DUTY = 6; // 司会当番列のindex
const IS_DUTY_TEXT = '◯'; // 当番を表す文字列
var doc = DocumentApp.openById('1234567890asdfghjklzxcvbnmqwertyuiop'); // コピーされたドキュメント(dummy)
var body = doc.getBody();
var text = body.editAsText();
// 参加者一覧のテーブルを取得する(テーブルタイトルの次の要素としている)
var paragraph_start = text.findText('<プロジェクト一覧>').getElement().getParent();
var table = paragraph_start.getNextSibling();
var table_count = table.getNumRows();
// 今週の当番を上から順に探す
for (i = 1; i < table_count; i++) { // 1行目はタイトル行なので2行目(i = 1)から始める
var duty_cell = table.getCell(i, COL_INDEX_DUTY);
var duty = duty_cell.getText();
var name = table.getCell(i, COL_INDEX_NAME).getText();
if (duty !== IS_DUTY_TEXT) {
continue;
}
Logger.log("今週の当番: " + name);
var is_found = false;
var last_check_i = table_count - 1; // 値を書き換えるので別の変数として宣言
// 今週当番だった人より下の行の参加者で、最初に条件に合致する人を見つける
for (next_i = i + 1; next_i <= last_check_i; next_i++) {
var next_name_as_text = table.getCell(next_i, COL_INDEX_NAME).editAsText();
var next_name = next_name_as_text.getText();
var next_name_color = next_name_as_text.getForegroundColor();
var next_duty_cell = table.getCell(next_i, COL_INDEX_DUTY);
var next_duty = next_duty_cell.getText();
// 参加者として名前が有効かどうかを確認
// 条件:参加者なし(セル結合含む)じゃない & 文字色が設定されてない & 当番の欄が空
if (next_name !== '' && next_name_color === null && next_duty === '') {
is_found = true;
break;
}
// 最後の行まで行って見つからなかった場合、カウンターを先頭に戻し、今週の当番の1つ上までチェックを繰り返す
if (next_i === last_check_i) {
Logger.log("先頭に戻ります: " + next_i);
next_i = 0;
last_check_i = i;
}
}
// 次の当番が見つかったら、今週の当番の ◯ を消して、次の当番に ◯ を入れる
if (is_found) {
next_duty_cell.setText(IS_DUTY_TEXT);
duty_cell.setText('');
Logger.log("当番をバトンタッチしました: " + name + "→" + next_name_as_text.getText());
break;
} else {
throw new Error("次の当番をバトンタッチできる参加者がいませんでした…このままだと来週も同じ司会者です");
}
}
}
ポイント
文字色の判定
- セル内テキストの文字列を取得するだけなら、
TableCell
オブジェクトに対して.getText()
だけでOKなのですが、文字色判定するには.editAsText()
をつけてリッチテキスト形式を取得する必要があります - 背景色などは
getbackgroundColor()
などとHTMLと同じ名称でわかりやすいですが、文字色はgetForegroundColor()
で取れるようでした。リファレンスをfontcolor
で探しに行ったので迷った。 - 書式が設定されていないと
null
になる- 見た目黒だからと言って、
#000000
が入っている訳ではない
- 見た目黒だからと言って、
当番表の下まで行ったら最初に戻る
- 例の表で言うと、今週の当番がNo.18 二宮さんだった場合や、二宮さんがスキップされた場合など、次の人が見つからないまま表が終わってしまうケースがある
- 最初に戻す場合、タイトル行を除く最初の行(2行目)〜今週当番だった人の行の一つ前までを検索対象にしなければならないので、カウンターをそれらに合わせ、再度ループが回るようにする
- 最初の行だけを最後に個別でチェックする方法も考えたが、最初の人が必ず居るとも限らない問題(ex. No.1 双葉さん)があるので再度ループを回したい
- maxの指定を誤ると無限ループになってしまうので注意
セルの結合
想定通りなので書くほどのことでもないがメモとして。
- 結合されたセルは結合の最初の行のセルとして扱われる
- 結合されたセルの2行目以降のセルインデックスを指定して
getText()
した結果は空文字になる
その他
当たり前のことだけど留意したこと
- 行数などは定数に切り出したので、表に列が追加されても冒頭の定数を変えるだけでOK
- skip要件にも種類があるので、なるべくログを残すようにしてバグの原因に後で気づけるように
- 最終的に失敗した場合にthrow()しとくと、定期実行でエラーメール飛ばしてくれるので動いてないことに気付ける
「MTGのルールは変わる」と冒頭で言ったように、この制度もいつまで続くか、いつ形式がどのように変わるかわからないです。
どう変わるかを見越すのは難しいですが、業務ツールの自動化をする際には可能な限り柔軟性とメンテ性を考えておきたいところです。
気になる点
- この処理だと、「参加者名の文字に色が設定されていない」ことを司会になり得る条件にしているので、例えばグレーアウトから黒に戻すのに、書式クリアを使わないとスキップになってしまう
- もっと厳密にやるなら、グレーとして設定するであろう、パレットに入ってる数色を対象カラーとして定義し、そのいずれかならskipとかしてあげるとより良い
- 最初の要件に入れてた重複スキップの処理は、やりたければ配列に値を格納するようにしてチェックすれば良いだけなので運用してニーズがあれば対応
- もし黒◯がわかりにくければ、これまでと同じように赤◯に設定することも可。文字色判定と同じで、
setForegroundColor()
すれば良い。 - 当番ってdutyって変数名でいいのだろうか…
あとがき
今回、自分が司会を書き換えることになり、「え、ここだけ手動?めんどくさ」と言ったことに端を欲して、まわりがワイワイ盛り上がった勢いでやることにしました。
今のチームの若手(+おじさん1人含む)のみなさんは、こういうお題解決系話題が出たら、「あ、これ◯◯でできそう」「あーでも△△をどうにかしないと」などと自分の業務をほっぽり出して非常に親切に前のめりに話に乗っかってきてくれます。
まったく、いいチームワークです。
今回やりたいことは司会者の交代でしたが、
司会に限らずドキュメント内セルの当番表としても流用できると思います。
GASでやったことなかったことをいくつかやったので勉強になりました。
では良いクリスマスを\(^o^)/