見出し画像

【GAS】Google Apps Script 活用事例 シートを経由して特定の参加者を選定してカレンダーに登録するスクリプト

職種ごとに面接官が決まっており、シートを経由して取得して面接官も一緒にカレンダーに登録するスクリプトを書きました。

メインスクリプトでやっている事

  1. 入力画面に学生の名前を入力

  2. Gmailを検索、HRMOS URLを取得

  3. 学生の希望職種を入力

  4. 特定のシートから希望職種の担当面接官を取得

  5. 面接官をランダムに選ぶために乱数を使用する

  6. カレンダーのテンプレートを取得

  7. HRMOS URLや名前などの情報を流し込む

2と3については、学生管理をスプレッドシートでは行わないというお達しで情報がスプシから取得できなくなってしまい、その回避策として考えた苦肉の策であり、本来は必要ありません。

書ききった達成感はあったものの、汎用的な自動化ではありません。全てを掲載すると、あまりにも長くなってしまうため、今回は一部のみ掲載することにしました。

サンプルコード(正規表現)

function extractReplacedWords(){

  const text  = '<p>JP2024ああ23いる345おちつ</p>';
  const reg   = /[A-Z]{2}[0-9]{4}/;
  const array = ['<p>', '</p>'];

  console.log(`元の文字列:${text}`);
  console.log(`正規表現:${reg}`);

  let string  = '';
  if(text.match(reg) !== null){
    string = text.match(reg)[0];

    //配列に格納されている置換対象の文字列で置換していく
    for(const targetWord of array){
      console.log(`削除対象の文字列: ${targetWord}`);
      string = string.replace(targetWord, '');
    }

    console.log(`抽出された文字列: ${string}`);
    return string

  }else{
    console.log(`matchの結果:${text.match(reg)}`);
    console.warn(text);
    return ''
  }
}

実行ログ

ログの出力結果1

サンプルコード(COUNTIF的な挙動)

function myFunction(){
  const values = [
    ['職種', '名前', '職責区分'],
    ['ML', '野比のび太', 'トレーナー'],
    ['FE', 'しずか', '新任'],
    ['BE', 'スネ夫', '新任'],
    ['SRE', 'ジャイアン', 'エキスパート'],
  ];
  const header = values.shift();
  const column = {
    job:    header.indexOf('職種'),
    name:   header.indexOf('名前'),
    status: header.indexOf('職責区分'),
  }
  
  //上の表で言うところの職責区分をカウントする
 //動きとしては、COUNTIFのような感じ
  const numbers = getJobRolesLength_(values, column.status);

}




//2次元配列内に登場する特定の単語を数える
function getJobRolesLength_(values, column){
  
  const jobRolesInfo = new Object;
  const roles        = values.map(record => record[column]).filter(value => value);

  //全ての変数に0を代入する
  trainer = expert = newComer = 0;

  for(const role of roles){
    switch(role){
      case 'トレーナー':
        trainer += 1;
        break;
      case 'エキスパート':
        expert += 1;
        break;
      case '新任':
        newComer += 1;
        break;
    }
  }//for

  //空のオブジェクトにプロパティを追加する
  jobRolesInfo['trainer']  = trainer;
  jobRolesInfo['expert']   = expert;
  jobRolesInfo['newComer'] = newComer;
  
  console.log(jobRolesInfo);
  return jobRolesInfo;
}

後述のメインスクリプトで使用している2次元配列を必要な列のみ取得する関数はこちらで解説しています。

実行ログ

ログの出力結果2

2次元配列から必要な行のみを残し、面接官をランダムに取得

function myFunction() {
  const object = { trainer: 3, expert: 1, newComer: 2 };
  const values = [
    ['職種', '名前', '職責区分'],
    ['ML', '野比のび太', 'トレーナー'],
    ['ML', 'ケイタ', 'トレーナー'],
    ['FE', 'ジバニャン', 'トレーナー'],
    ['FE', 'しずか', '新任'],
    ['BE', 'スネ夫', '新任'],
    ['SRE', 'ジャイアン', 'エキスパート'],
  ];

  //トレーナーという単語が含まれている行のみを残した2次元配列を作成する
  const newValues = values.filter(array => array.indexOf('トレーナー') !== -1);
  console.log(newValues);

  //乱数を使ってランダムに面接官を取得
  const random = getRandomNumber_(object.trainer);
  console.log(`newValues[random][1]: ${newValues[random][1]}`);
}


function getRandomNumber_(length) {
  const random =  Math.floor(Math.random() * length);
  console.log(`引数:${length} 乱数生成の結果:${random}`);
  return random
}

実行ログ

1回目の実行結果
2回目の実行結果
3回目の実行結果

乱数生成では、生成結果に偏りがあります。3回目でジバニャンが出て欲しいのですが、これがなかなか難しい….。
今までの実行結果を配列で貯めておいて、機械学習で乱数生成の精度を補正するみたいな事が出来ればいいのになぁ….。

メインスクリプト(一部抜粋)

/**
 * 面接共有カレンダーに本選考の1次面接の予定を登録する。
 * 
 */
function registerFirstInterview(){
  //入力させる必要がある
  const applicantName = showPrompt_('学生の名前を入力してください', '(例) 野比のび太', 'WithSpace');
  let array, query;

  //応募日、応募者名、HRMOSのURLをGmailから取得する。
 //スプレッドシートでの学生管理を止めてしまったが故の対処法
  try{
    query = /2024新卒採用.*への応募がありました/;
    array = getApplicantHrmosUrl_(applicantName, query);
    console.log(array[0].hrmos);

  }catch{
    console.warn('新着応募時のメールが見つかりませんでした。CSVでインポートされた可能性があります。');
    query = /メールが届きました.*/
    array = getApplicantHrmosUrl_(applicantName, query);
  }

  //職種を入力させる 入力2回目(メールやシートから職種情報の取得が出来ないため)
  //面接官を乱数を使ってランダムに取得

  const jobName = showPrompt_('職種名を入力してください', '(例) FE, BE, SRE, ML, iOS, Android');
  const object  = getInterviewAttendees_(jobName);
  console.log(object);

  SpreadsheetApp.getUi().alert(`面接官の取得に乱数を使用していますが偏りがあります。\n今回の担当者:${object.attendees}\n\n${jobName}職の全メンバーはこちら\n\n${object.allMembers}`);

  //名前をメールアドレスに変換する
  mailAddress = attendees = '';
  object.attendees.map(name => mailAddress += `${personName_(name, 0, 2)},`);
  object.attendees.map(name => attendees   += `${name.slice(0, 2)}さん、`);
  
  
  const calInfo = {
    templateNumber: 0,  //getEvents()で取得したイベントをindexで指定する
    minutes:        60, //面接時間
    lastName:       applicantName.slice(0, 2),//学生の苗字、高橋、鈴木など
    fullName:       applicantName,
    hrmos:          array[0].hrmos,
    interviewers:   attendees,
    mailAddress:    mailAddress
  }

  console.log(calInfo);

  
  //引数に指定した日時に登録された予定をテンプレートとして読み込む
  loadCalTemplate2_(calInfo, '2022/10/01')
  
}




/**
 * 学生の名前とHRMOS URLをGmailを検索して取得する
 * 
 * @param  {string} applicantName - 応募者の名前
 * @param  {string} query - メールを検索する際の文字列
 * 
 * 
 */
function getApplicantHrmosUrl_(applicantName, query) {
  const threads = GmailApp.search(query, 0, 100);

  let array = [];

  for (const thread of threads){
    const messages = thread.getMessages();

    for(const message of messages){
      const body      = message.getBody();
      const subject   = message.getSubject();
      const plainText = message.getPlainBody();

      if(!subject.includes('プレエントリー') && plainText.includes(applicantName)){

        const info = {
          date:       Utilities.formatDate(message.getDate(), 'JST', 'yyyy/MM/dd'),
          name:       extractReplacedWords_(plainText, /候補者名.*/, [/候補者名 /, /.$/, /\s/]),
          hrmos:      extractReplacedWords_(body, /https:\/\/n-ats.hrmos.co.*/, [/候補者名:\r\n/, '"']),
        }

        array.push(info);
        
        console.log(`件名: ${subject}`);
        console.log(array);

        return array;
      }
    }//for
  }//for
}



/**
 * 捜査対象のテキストから、文字列を消去して欲しい文字列を取得するための関数
 * 
 * 
 * @param  {string} text - 操作対象のテキスト
 * @param  {string} reg - 正規表現
 * @param  {Object}  array - 置換対象が格納された配列
 * @return {string} 修正後のテキスト
 */

function extractReplacedWords_(text, reg, array){
  let string = '';
  if(text.match(reg) !== null){
    string = text.match(reg)[0];

    //配列に格納されている置換対象の文字列で置換していく
    for(const targetWord of array){
      string = string.replace(targetWord, '');
    }

    console.log(`抽出された文字列: ${string}`);
    return string

  }else{
    console.log(`matchの結果:${text.match(reg)}`);
    console.warn(text);
    return ''
  }
}



/**
 * 乱数を使い2人の選考官をシートから取得する
 * 
 * @param  {string} jobName - 職種名 (例)BE, FE,iOSなどのように指定
 * @return {Object.<string>}  attendees:['面接官A', '面接官B'], allMembers: '全員の名前'
 * 
 */
function getInterviewAttendees_(jobName){
  const url   = '*****************';
  const sheet = getSheetByUrl_(url);
  
  //シートの作りが悪いために使用している関数
 //1行目に見出し行が存在するシートならば不要
  const targetRow = getHeaderRow_(sheet, '選考官リスト');
  const headerRow = targetRow + 1;

  //面接官はこれ以上増えない見込みのため
  const range  = sheet.getRange(headerRow, 2, 50, 3);
  const values = range.getValues();
  const header = values[0];

  console.log(`取得対象範囲:${range.getA1Notation()}`);
  console.log(values);

  const column = {
    job:  header.indexOf('職種'),
    name: header.indexOf('氏名'),
    role: header.indexOf('ステータス'),
  }
  console.log(column);

  //職種ごとの面接官を取得
  //トレーナーから1人、新任担当者から1人決める
  const newValues    = selectColumn_(values, column, jobName);
  const jobRolesInfo = getJobRolesLength_(newValues, column);
  let random = 0;

  //各ステータスに該当する人のみを残した2次元配列を作成する
  const trainerValues  = newValues.filter(array => array.indexOf('トレーナー') !== -1);
  const newComerValues = newValues.filter(array => array.indexOf('新任担当者') !== -1);

  //トレーナー、新任担当者から、それぞれ一人ずつ選出する
  const trainer  = trainerValues[getRandomNumber_(jobRolesInfo.trainer)][1].replace(/\s/, '');
  const newComer = newComerValues[getRandomNumber_(jobRolesInfo.newComer)][1].replace(/\s/, '');
  
  console.log(`トレーナー:${trainer}`);
  console.log(`新任担当者: ${newComer}`);

  //乱数生成には偏りがあるため一覧で表示させる
  let string = '';
  let allInterviewers = generateArray_(newValues, column.name);
  allInterviewers.map(value => string += value.replace(/\s/, '') + '\n');

  console.log(string);

  const interviewAttendees = {
    attendees: [trainer, newComer],
    allMembers: string
  }

  return interviewAttendees
}





/**
 * 引数に指定された数値より下の数を乱数を使用して返す
 * getJobRolesLength_により、面接官の数を調べている
 * 
 * @param  {number} length - 数値
 * @return {number} 
 */
function getRandomNumber_(length) {
  const random = Math.floor(Math.random() * length);
  console.log(`引数:${length} 乱数生成の結果:${random}`);
  return random
}



/**
 * 2次元配列内に指定した文字列が何回登場するかを調べる
 * 
 * @param  {Array.<Array.<string>>} values - 2次元配列 [['SRE', '面接官A', 'トレーナー']]
 * @param  {Object.<number>} column - 見出し列の情報
 */
function getJobRolesLength_(values, column){
  
  const jobRolesInfo = new Object;
  const roles        = generateArray_(values, column.role);

  trainer = expert = newComer = 0;

  for(const role of roles){
    switch(role){
      case 'トレーナー':
        trainer += 1;
        break;
      case 'エキスパート':
        expert += 1;
        break;
      case '新任':
        newComer += 1;
        break;
    }
  }//for
 
  //空のオブジェクトにプロパティを追加する
  jobRolesInfo['trainer']  = trainer;
  jobRolesInfo['expert']   = expert;
  jobRolesInfo['newComer'] = newComer;
  
  console.log(jobRolesInfo);
  return jobRolesInfo;
}



/**
 * 引数に指定した日付に保存したテンプレート内容を取得し、予定を登録する。
 * 
 * @param  {Object} calInfo - メールアドレスや面接官の名前、HRMOS URLなどの情報
 * @param  {string} targetDate - '2022/10/22' のように文字列で日付を指定
 * 
 */
function loadCalTemplate2_(calInfo, targetDate) {

  //テンプレートから取得した詳細欄に変数を挿入する。
  const template = getDescriptionTemplate_(calInfo.templateNumber, targetDate);
  const newDescription = template.description
  .replace('{name}', calInfo.lastName) //鈴木さんのように苗字で書き換える
  .replace('{hrmos}', calInfo.hrmos)
  .replace('{interviewers}', calInfo.interviewers);
  
  console.log(`生成後の新しい詳細欄`);
  console.log(newDescription);
  
  const title   = template.title;
  const options = {
    description: newDescription,
    guests: calInfo.mailAddress
  }
  
  //チーム共有のカレンダーの、8時間前にイベントを登録する。
  registerEvent8hoursAgo_(title, options, calInfo.minutes);

  //登録が完了した事をUIで表示
  SpreadsheetApp.getUi().alert('予定は、8時間前のチーム共有のカレンダーに作成されました。日程調整を必ず行って、作業を完了させてください。');
}//end


2次元配列で特定の文字の登場回数を数える、COUNTIF的な動きをするスクリプトや乱数を実務へ応用するスクリプトは残しておくと、今後役に立つかなと思い、noteに書き留めています。


この記事が気に入ったらサポートをしてみませんか?