0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASのトリガー残り時間を制御・配分・管理する汎用フレームワーク「GTRM (=GAS トリガー・リソース・マネージャー)」。わずか2ステップで利用開始する方法

0
Posted at

Googleスプレッドシート(GAS)でバッチ処理を組む場合,残り時間の制約 が気になりますよね?

  • 1回のトリガ起動あたり6分までしか動かせない。
  • 1日(24時間)あたり90分以内しか動かせない。

でも,その制約をうまく検知・管理してくれるフレームワークがあるんです。

GAS Trigger Resource Manager,略して GTRMというオープンソースのライブラリです。
(ガス トリガー  リソース   マネージャー)

MITライセンスで商用利用も可能。

この記事に記載の,わずか2ステップで利用開始できます。
その2ステップとは,

  1. 本記事に掲載されているコードをコピペ。
  2. トリガを1つ設定。

これだけで導入完了です。簡単ですね!

GTRMを使うと,どんな事ができるか?

このフレームワークを使うと,概念としてはGASコード内で下記のようなプログラムを書けます。


while( トリガ内の残り時間が十分にある ){ // ★

  バッチ処理を継続;

}

トリガ内残り時間が減ってきたので今回の処理を打ち切り;

上記の概念コード中で,「★」マークを付けた部分で「残り時間は十分にあるか?」を検査していますよね。

なんと,GASにはそのようなAPIが存在しない のです。
そのような「トリガ内の残り時間の管理の仕組み」は,自前で作らないといけません。

それを自前で作ったのが,下記のライブラリです。


さらに,トリガ内の時間を管理するだけでなく

  • 1時間ごとにバッチ処理でどれだけの仕事をなし終えたか?
  • 1時間ごとにトリガ内で何秒の時間が経過したか?

を,全自動で棒グラフで描画してくれます。
(※1時間おきだけでなく,2時間おき,3時間おきなどの集計も可能)

下記のスクショをご覧ください。

可視化シート2.png

左側の黒い四角の棒グラフが,「バッチ処理によってGASが成し遂げた仕事の量」。
また,右側の白い四角の棒グラフは「トリガ内で消費・経過した時間の長さ」。

上の写真の右側を見ると,だいたい毎時間ごとに200秒ぐらい消費してますね。
(200秒×24時間=4800秒=80分 なので,1日あたり90分以内に収まるようにGTRMフレームワークが時間をペース配分してくれています。)

なお,黒い四角 ■ や白い四角 □ のかわりに,任意の文字で棒グラフを作れます。
見せ方はコード内の設定項目を変更すれば調節できるのです。
このシートは自動生成されますので,手動でシートを作る必要はありません。


上記のような1時間ごとの棒グラフが,毎日・1日おきに描画 されます。
古い情報ほど,シートの右側にずれてゆきます。
下記スクショをご覧ください。
(7日分を超えた分は自動的に削除されます。
何日ぶんを取っておきたいか?は自由に設定できます。)

可視化シート1.png


また,このフレームワークは棒グラフを描画するだけでなく,専用のシート上に詳しいログを記録 してくれます。
下記スクショをご覧ください。
(このログ用シートも自動生成されます。手動では何もシートを作る必要はありません。)

シートログ.png

上の写真を見ると,各行のログが出力される際に

  • 「トリガ内」という欄に,今回のトリガ内の経過時間 が表示される。(30秒など)
  • 「日内」という欄に,本日のトリガ内実行時間の累計値 が表示される。(50分など)
  • 「ダイジェストレート」という欄に,リアルタイムで「現時点での 残り時間を消費しきった割合」が表示される。これが100%に達していないという事は,そのトリガ内でまだ残り時間が十分にあるということ。(100%に達したら,その回のトリガ起動は「6分に達するよりも前に」フレームワーク側が打ち切る。)

上記のシート上では,新しいログほど一番上に記録されます。
古くなったログは,5千行を超えたぶんは自動的に削除されます。
(何行を取っておきたいか?は自由に設定できます。)


これらの説明から,GASでトリガーを使ってバッチ処理を組むための
とても汎用的なフレームワーク」である事がお分かり頂けたと思います。

細かい機能は他にもいろいろあるのですが,下記でさっそく導入してみましょう。

GTRMを導入してみよう

前述の通り,このフレームワークの導入はわずか2ステップです。

  1. 本記事の下側にあるコードをGASエディタにコピペしてください。
  2. GASエディタ上でトリガ設定してください。

トリガ設定する関数は,下記の通りです。

トリガ設定画面.png

  • 「実行する関数を選択」の欄: my_triggeredMainFunction を選ぶ。
  • 「イベントのソースを選択」の欄: 時間主導型。
  • 「時間ベースのトリガーのタイプ」の欄: 分ベースのタイマー。
  • 「時間の感覚を選択」の欄: 5分おき。

これで,OKを押せばトリガ設定完了です。

すると,5分おきにトリガが起動され,「シートログ用」というシートにリアルタイムで進捗がログ表示されてゆきます。

ワンポイントアドバイス:
どうして5分おきに設定しているの?

→保険をかける意味で,そうしています。
GASのトリガは,Googleのサーバ側の都合で「トリガ起動に失敗」することもあります。
そのような事態が起きても,バッチ処理が動き続けるように念を入れているんです。

かりに,バッチ処理を1時間に6回,10分おきに実行させたいと思っているとしましょう。
その状況でトリガの間隔を「10分おき」にセットしてしまうと・・・?
トリガ起動失敗した時に,バッチ処理が行われないブランクの時間帯が20分間も生じてしまいますよね。

そのような事態を避けるために,「バッチ処理したい頻度よりも多い頻度でトリガ設定する」事が必要なのです。
10分おきに確実にバッチ処理させたいなら,トリガ設定は5分おきとします。

「でも,それだと5分おきにバッチ処理されちゃうんじゃないの?」と,疑問に思われるかもしれませんね。
ご安心ください!
そういう心配をしなくてすむように,このGTRMフレームワークがうまく調整してくれます。

5分おきにトリガ起動されても,「5分前にバッチ処理を実行したばかりだ」という状況をフレームワークが検知したら,「今回は何もしません」という判断を下します。
そしてさらに5分後,次回トリガが呼ばれた時に「前回のバッチ処理の実行から10分が経過しているので,今回は処理を実行しよう」とフレームワークが判断します。

こうして,「10分おきのバッチ処理実行」が確実に果たされるように担保される。というわけです。

なお,上記のような理由でトリガ起動間隔は5分おきにセットしてはいますが,バッチ処理の間隔は10分おきではなく,15分おき・30分おきなどGTRMのコード内で自由に変えられます。

GTRMおよび動作サンプルのソースコード(コピペ用)

お待たせしました。下記のソースコードをGASディタ上に丸ごとコピペしてください。

(コードの詳しい説明や,より細かいカスタマイズの方法などは,またQiita上で別記事でご説明しますので。)


//
// 「Googleスプレッドシート上で,トリガ定期実行によるバッチ処理を
//  毎日・毎時,安定稼働させるためのリソース管理フレームワーク」
//
//  "GAS Trigger Resource Manager" (略称: GTRM)
//   ガス  トリガー   リソース      マネージャー         ジーティーアールエム
// 
// 内容:
//  (1) トリガが重複実行・処理時間超過しないように制御する汎用的な機構。
//      1トリガあたり6分,1日あたり90分の処理リソースをうまくモニタ・配分し,
//      1日の各時間帯を通じ安定稼働させ,バッチ処理の結果と統計情報を記録,グラフ描画図示。
//  (2) シート内の最上部にログ記録し,最下部でログ行数が増えたら自動ローテート。
// GASエディタ上でトリガとして(1)を呼び出し,次いで(1)が(2)を呼び出します。
// (1)の部分は,GASでバッチ処理を組む際の共通ロジックとして汎用的に使いまわせます。
//
// 使い方:
// ・ my_triggeredMainFunction() を,GASでトリガ設定してください。
// ・ my_mainRoutineWhileLockedSection() 内に,トリガ内で実行したい任意の処理を記述してください。
//
// 注意点:
//  このフレームワークでは,情報を管理するために下記の名称の3つのシートを自動的に生成します。
//  (1) 「トリガ結果図示」→ バッチ処理によって達成された仕事数を,日別・時間帯別に棒グラフで可視化。
//  (2) 「シートログ用」→ ログを記録。
//  (3) 「トリガ制御用」→ GASのトリガの状態を制御。
//
// ver1.0: 2026.5.14. @rwanda_go_tan
//         MIT License
//


// コードの解説:
// このコード内で定義しているオブジェクトは下記の通りです。
(function(){

  const definedObjectsExplained = [

    // GASエディタ上で各オブジェクト名を右クリック→「定義へ移動」を選べば,該当コードを見れます。
    [ "(1)", "定数宣言部分",                 "大文字でconstを宣言。400行ぐらい。" ],
    [ "(2)", "地の関数",                    "GASのトリガに設定する関数,およびメイン処理記述部。100行ぐらい。" ],
    [ "(3)", MyTriggerManager,             "トリガのロック管理や主要な処理の分岐,リソース管理を行なう。1300行ぐらい。" ],
    [ "(4)", TriggerInfoSheetDomainLogics, "トリガ制御情報のシート読み書きに関するドメインロジック。700行ぐらい。" ],
    [ "(5)", TriggerInfoSheetDAO,          "トリガ制御情報のシート読み書きを行なう。1000行ぐらい。" ],
    [ "(6)", TriggerInfoUtil,              "トリガ情報読み書き等の便利関数集。300行ぐらい。" ],
    [ "(7)", MySheetLogger,                "シート上にログを記録する。400行ぐらい。" ],
    [ "(8)", TriggerStatVisualizer,        "トリガ実行結果を視覚的にグラフを使って記録する。1400行ぐらい。" ],
    [ "(9)", "補足情報",                    "コードの末尾に補足情報や設計思想が掲載されています。100行ぐらい。"  ]

  ];
  return;

  // ※上記constをfunctionでラップしてある理由は,地のコードだと未定義オブジェクトの名前が参照エラーになるから。
  //   (下のほうのコードで宣言されるオブジェクトを,上のほうの地のコードで先回りして呼び出し・名前参照する事はできない)

});



// ------------------------ 下記は設定項目・定数宣言部分 -----------------------------




// ------- 全体の動作にかかわる設定項目 -------

// トリガが起動されても,処理を開始せずトリガをキャンセルすべき状態か? (コードのメンテナンス中など)
const DISABLE_ALL_TRIGGERS_FOR_MAINTENANCE = false;

// デバッグログをコンソール表示するかどうかのフラグ
const ENABLE_CONSOLE_DEBUG_LOG_FLAG = false;
  // 実運用時の処理ステップや処理時間を少しでも減らすため,
  // デバッグログ出力を抑制可能にしてある。
  // リリース時はfalseとし,デバッグログ出力を抑制する。



// ------- GAS上での処理時間に関する設定項目 -------

// 新規トリガ起動時に,ロック取得成功まで最大でどれだけ待つか(ミリ秒)
const WAIT_MS_FOR_LOCK = 10 * 1000;

// (トリガ起動頻度とは別の概念として) トリガマネージャには正味の処理(メイン処理,MAIN_PROC)を1時間に何度実行してほしいか。
// 1以上60以下の整数を指定する。
// 前回のトリガ起動成功から次回の処理実行までの時間間隔の目安を指定する。
// 例: たとえば6を指定した場合,一時間に必ず6回実行されるということではなく,
//     前回のトリガ起動成功から次回のトリガ内処理まで10分ほど時間をあけるということ。
const FREQUENCY_PACE_IN_ONE_HOUR_FOR_TRIGGER_TO_START_MAIN_PROC = 6;

// 1トリガ内の処理時間の上限値は何分か
const LIMIT_MINUTES_OF_ONE_TRIGGER = 6; // 一回の処理は6分以内

// 1トリガあたりの処理時間にかける安全係数
const SAFETY_COEFFICIENT_FOR_ONE_TRIGGER = 5/6;
  // 安全余裕を持たせ,1トリガあたり「安全に使用可能」なのは6分のうち5分だけとする

// 全プロジェクトを合わせた仕様として,1日の累積処理時間の上限値は何分か
// (現時刻までの使用率などを計算するために必要な情報)
const LIMIT_MINUTES_OF_ALL_TRIGGERS_ONE_DAY_ALL_PROJECTS = 90; // GAS無料版の1日の処理時間の合計は90分

// 本プロジェクトでは,1日の累積処理時間の上限値は何分か
// (1つのGoogleアカウントで複数のGASプロジェクトを保有している場合,
//  本プロジェクトで消費してよい全体のうちの割合はどれくらいか)
const LIMIT_MINUTES_OF_ALL_TRIGGERS_ONE_DAY_THIS_PROJECT =
  LIMIT_MINUTES_OF_ALL_TRIGGERS_ONE_DAY_ALL_PROJECTS * 1.0; // 割合を減らしたい場合は1.0という係数を小さい値に変更する

// 1日全体はフルで何ミリ秒か
const HOW_MANY_MS_IN_ONE_DAY = 24 * 60 * 60 * 1000; // 毎回掛け算しなくて済むように定数として保持

// ほかに,定数としての宣言ではないが,安全係数を1日の中で時間帯別に変化させる関数がある。
// 下記から定義にジャンプすれば,関数の内容をメンテし,係数を微調整することができる。
(function(){ MyTriggerManager.getCurrentSafetyCoefficientOnedayAsRatioNumber; });


// ------- トリガ管理情報のシート記録についての設定項目 -------

// トリガ情報が記載されているシート名
const SHEET_NAME_TRIGGER_INFO = "トリガ制御用";

// トリガステータス文字列として,「トリガ起動に成功」を表すもの
const TRIGGER_STATUS_START_SUCCESS = "START SUCCESS";

// トリガステータス文字列として,「トリガ終了に成功」を表すもの
const TRIGGER_STATUS_END_SUCCESS = "END SUCCESS";

// トリガIDとして許容できる上限値
const MAX_TRIGGER_ID_AS_INTEGER = 100000000000000; // 15ケタになったら1に巻き戻す。
  // ・GASで扱える最大の整数値はJavaScriptのNumber型(64ビット浮動小数点数)の規格に準拠しており
  //   2^53 - 1 = 9007199254740991 (16ケタの整数)。
  // ・Googleスプレッドシートで扱える最大の整数値は,
  //   数値として正確に認識されるのは最大15桁までであり,16桁以上の数値を入力すると近似値に置き換わる。
  // ・トリガIDの桁数の現実的な見積もりは,1日24時間で1分おきにトリガ起動したとしても毎日24*60=1440。
  //   それが1年間続くと 1440*365=525600 でわずか6桁。よって普通の運用ではトリガIDの桁あふれは起きない。
  //   誤ってシート上に手動で大きな整数を入力してしまった場合などに誤動作を回避するため,この上限値がある。


// トリガ系の設定項目の項目名ラベル部分を何列目に記載するか(共通値とする場合用)
const COL_NUM_TRIGGER_PROPERTY_LABELS = 1;

// トリガ系の設定項目の値部分を何列目に記載するか(共通値とする場合用)
const COL_NUM_TRIGGER_PROPERTY_VAUES = 2;

// MEMO:
// いつか実装してみたいアイデアとして。
// 下記のようなシート上の大量の設定項目値の位置定義,名称定義,およびgetter/setterの定義をactiverecordみたいに自動化したい。


// トリガ情報をシート上で何行目から記載し始めるか
// (コード内で項目ごとにインクリメントしてゆく。シート上で1行ずつ下にずれるということ。)
let row_num_counter_trigger_info_sheet = 1;

// 「トリガ制御用シートを手動で編集しないように」という注意書きはシート行で何行目か
const LABEL_TRIGGER_INFO_ATTENTION_FOR_MANUAL_EDIT = "(※このシートは手動で編集しないこと)";
const ROW_NUM_TRIGGER_INFO_ATTENTION_FOR_MANUAL_EDIT = row_num_counter_trigger_info_sheet++;

// シート上で1行あける
row_num_counter_trigger_info_sheet++; 

// 最終トリガ起動日時(ms)はシート上で何行目か
const LABEL_LAST_TRIGGER_START_MS = "最終トリガ発動日時(タイムスタンプ)";
const ROW_NUM_LAST_TRIGGER_START_MS = row_num_counter_trigger_info_sheet++;

// 最終トリガ起動時刻(フォーマット済み文字列)はシート上で何行目か
const LABEL_LAST_TRIGGER_START_FORMATTED_TIME = "最終トリガ発動日時(年月日時分秒)";
const ROW_NUM_LAST_TRIGGER_START_FORMATTED_TIME = row_num_counter_trigger_info_sheet++;

// 最終トリガIDはシート上で何行目か (これまでの全ての日にわたるトリガ起動回数の累積合計値)
const LABEL_LAST_TRIGGER_ID = "最終トリガID(=トリガ累計発動回数)";
const ROW_NUM_LAST_TRIGGER_ID = row_num_counter_trigger_info_sheet++;
  // トリガIDが発行されるのは,トリガ起動が確立された場合のみ。
  // もしトリガ起動が確立せずキャンセルされた場合は,新規トリガIDは発行されない。

// 最終トリガ・ステータスはシート上で何行目か
const LABEL_LAST_TRIGGER_STATUS = "最終トリガ・ステータス"; 
const ROW_NUM_LAST_TRIGGER_STATUS = row_num_counter_trigger_info_sheet++; 
  // この欄から前回に正常終了していなかったと分かった場合,前回は6分の時間制限を超過したとみなす。

// 最終トリガ・処理内の消費時間(ミリ秒)はシート上で何行目か
const LABEL_LAST_TRIGGER_CONSUMED_TIME_MILLISEC = "最終トリガ内・処理時間(ミリ秒)"; 
const ROW_NUM_LAST_TRIGGER_CONSUMED_TIME_MILLISEC = row_num_counter_trigger_info_sheet++; 

// 最終トリガ・処理内の消費時間(分秒形式)はシート上で何行目か
const LABEL_LAST_TRIGGER_CONSUMED_TIME_READABLE = "最終トリガ内・処理時間(分秒)"; 
const ROW_NUM_LAST_TRIGGER_CONSUMED_TIME_READABLE = row_num_counter_trigger_info_sheet++; 

// 最終トリガ内・仕事完了数はシート上で何行目か
const LABEL_LAST_TRIGGER_ACHIEVED_USER_TASKS = "最終トリガ内・仕事完了数"; 
const ROW_NUM_LAST_TRIGGER_ACHIEVED_USER_TASKS = row_num_counter_trigger_info_sheet++; 

// 最終トリガ内・1仕事あたり平均時間はシート上で何行目か
const LABEL_LAST_TRIGGER_AVERAGE_USER_TASK_TIME = "最終トリガ内・1仕事あたり平均時間"; 
const ROW_NUM_LAST_TRIGGER_AVERAGE_USER_TASK_TIME = row_num_counter_trigger_info_sheet++; 

// シート上で1行あける
row_num_counter_trigger_info_sheet++; 

// 本日のトリガ起動回数の合計値はシート上で何行目か
const LABEL_ALL_TRIGGERS_TODAY_COUNT = "本日のトリガ起動回数";
const ROW_NUM_ALL_TRIGGERS_TODAY_COUNT = row_num_counter_trigger_info_sheet++;

// 本日の全トリガ内の処理に要した時間(ミリ秒)はシート上で何行目か
const LABEL_ALL_TRIGGERS_TODAY_CONSUMED_TIME_MILLISEC = "本日のトリガ内・累積処理時間(ミリ秒)";
const ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_MILLISEC = row_num_counter_trigger_info_sheet++;

// 本日の全トリガ内の処理に要した時間(分秒形式)はシート上で何行目か
const LABEL_ALL_TRIGGERS_TODAY_CONSUMED_TIME_READABLE = "本日のトリガ内・累積処理時間(分秒)"; 
const ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_READABLE = row_num_counter_trigger_info_sheet++; 
  // 無料版は90分が上限。

// 本日の全トリガ内の処理に要した時間の枠内使用率はシート上で何行目か
const LABEL_CONSUMED_TIME_PERCENTAGE_TODAY = "本日のトリガ内・累積処理時間の枠内使用率";
const ROW_NUM_CONSUMED_TIME_PERCENTAGE_TODAY = row_num_counter_trigger_info_sheet++;
  // ここでの計算は安全係数を考慮しない。

// 本日の最終トリガ終了時の日内時間経過率はシート上で何行目か
const LABEL_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_TODAY = "本日の最終トリガ終了時の日内時間の経過率";  
const ROW_NUM_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_TODAY = row_num_counter_trigger_info_sheet++;  

// 「日内全処理時間の使用率を日内時間経過率で割った値」
// (つまり,本日の全トリガのリアルタイム時間消化率)はシート上で何行目か
const LABEL_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_TODAY = "上記の2つの%の比(本日のダイジェストレート)";  
const ROW_NUM_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_TODAY = row_num_counter_trigger_info_sheet++;
  // この値は,最終トリガ終了時点でのその時間の「1日の安全係数」に近い値になる。

// 本日の全トリガ内の1回あたり平均処理時間はシート上で何行目か
const LABEL_AVERAGE_CONSUMED_TIME_READABLE_TODAY = "本日のトリガ内・平均処理時間(分秒)";
const ROW_NUM_AVERAGE_CONSUMED_TIME_READABLE_TODAY = row_num_counter_trigger_info_sheet++;
  // 6分が上限。

// 本日の全トリガ内の1回あたり平均処理時間の枠内使用率はシート上で何行目か
const LABEL_AVERAGE_CONSUMED_TIME_PERCENTAGE_TODAY = "本日のトリガ内・平均処理時間の枠内使用率";
const ROW_NUM_AVERAGE_CONSUMED_TIME_PERCENTAGE_TODAY = row_num_counter_trigger_info_sheet++;
  // ここでの計算は安全係数を考慮しない。

// 本日の仕事完了数はシート上で何行目か
const LABEL_ACHIEVED_USER_TASKS_TODAY = "本日の仕事完了数"; 
const ROW_NUM_ACHIEVED_USER_TASKS_TODAY = row_num_counter_trigger_info_sheet++; 

// 本日の1トリガあたり平均仕事数はシート上で何行目か
const LABEL_AVERAGE_USER_TASK_PER_ONE_TRIGGER_TODAY = "本日の1トリガあたりの平均仕事数"; 
const ROW_NUM_AVERAGE_USER_TASK_PER_ONE_TRIGGER_TODAY = row_num_counter_trigger_info_sheet++; 

// 本日の1仕事あたり平均時間はシート上で何行目か
const LABEL_AVERAGE_USER_TASK_TIME_TODAY = "本日の1仕事あたり平均時間"; 
const ROW_NUM_AVERAGE_USER_TASK_TIME_TODAY = row_num_counter_trigger_info_sheet++;

// シート上で1行あける
row_num_counter_trigger_info_sheet++; 

// 前日のトリガ起動回数の合計値はシート上で何行目か
const LABEL_ALL_TRIGGERS_PREVDAY_COUNT = "前日のトリガ起動回数";
const ROW_NUM_ALL_TRIGGERS_PREVDAY_COUNT = row_num_counter_trigger_info_sheet++;

// 前日の全トリガ内の処理に要した時間(ミリ秒)はシート上で何行目か
const LABEL_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_MILLISEC = "前日のトリガ内・累積処理時間(ミリ秒)";
const ROW_NUM_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_MILLISEC = row_num_counter_trigger_info_sheet++;

// 前日の全トリガ内の処理に要した時間(分秒形式)はシート上で何行目か
const LABEL_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_READABLE = "前日のトリガ内・累積処理時間(分秒)"; 
const ROW_NUM_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_READABLE = row_num_counter_trigger_info_sheet++; 
  // 無料版は90分が上限。

// 前日の全トリガ内の処理に要した時間の枠内使用率はシート上で何行目か
const LABEL_CONSUMED_TIME_PERCENTAGE_PREVDAY = "前日のトリガ内・累積処理時間の枠内使用率"; 
const ROW_NUM_CONSUMED_TIME_PERCENTAGE_PREVDAY = row_num_counter_trigger_info_sheet++; 
  // ここでの計算は安全係数を考慮しない。

// 前日の最終トリガ終了時の日内時間経過率はシート上で何行目か
const LABEL_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_PREVDAY = "前日の最終トリガ終了時の日内時間の経過率";  
const ROW_NUM_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_PREVDAY = row_num_counter_trigger_info_sheet++;  

// 「日内全処理時間の使用率を日内時間経過率で割った値」
// (つまり,前日の全トリガのリアルタイム時間消化率)はシート上で何行目か
const LABEL_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_PREVDAY = "上記の2つの%の比(前日のダイジェストレート)";  
const ROW_NUM_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_PREVDAY = row_num_counter_trigger_info_sheet++;
  // この値は,最終トリガ終了時点でのその時間の「1日の安全係数」に近い値になる。

// 前日の全トリガ内の1回あたり平均処理時間はシート上で何行目か
const LABEL_AVERAGE_CONSUMED_TIME_READABLE_PREVDAY = "前日のトリガ内・平均処理時間(分秒)";
const ROW_NUM_AVERAGE_CONSUMED_TIME_READABLE_PREVDAY = row_num_counter_trigger_info_sheet++;
  // 6分が上限。

// 前日の全トリガ内の1回あたり平均処理時間の枠内使用率はシート上で何行目か
const LABEL_AVERAGE_CONSUMED_TIME_PERCENTAGE_PREVDAY = "前日のトリガ内・平均処理時間の枠内使用率";
const ROW_NUM_AVERAGE_CONSUMED_TIME_PERCENTAGE_PREVDAY = row_num_counter_trigger_info_sheet++;
  // ここでの計算は安全係数を考慮しない。

// 前日の仕事完了数はシート上で何行目か
const LABEL_ACHIEVED_USER_TASKS_PREVDAY = "前日の仕事完了数"; 
const ROW_NUM_ACHIEVED_USER_TASKS_PREVDAY = row_num_counter_trigger_info_sheet++; 

// 前日の1トリガあたり平均仕事数はシート上で何行目か
const LABEL_AVERAGE_USER_TASK_PER_ONE_TRIGGER_PREVDAY = "前日の1トリガあたりの平均仕事数"; 
const ROW_NUM_AVERAGE_USER_TASK_PER_ONE_TRIGGER_PREVDAY = row_num_counter_trigger_info_sheet++; 

// 前日の1仕事あたり平均時間はシート上で何行目か
const LABEL_AVERAGE_USER_TASK_TIME_PREVDAY = "前日の1仕事あたり平均時間"; 
const ROW_NUM_AVERAGE_USER_TASK_TIME_PREVDAY = row_num_counter_trigger_info_sheet++; 



// ------- シート上のログ記録(シートログ,SheetLogging)についての設定項目 -------

// シートログ記録用のシート名
const SHEET_NAME_FOR_SHEET_LOGGING = "シートログ用";

// シートログの項目名ラベルはシート上で何行目に記載するか
const ROW_NUM_SHEET_LOGGING_LABELS = 1;

// シートログの記録日時欄はシート上で何列目か
const LABEL_SHEET_LOGGING_DATETIME = "ログ記録日時";
const COL_NUM_SHEET_LOGGING_DATETIME = 1;

// シートログのカテゴリ欄はシート上で何列目か
const LABEL_SHEET_LOGGING_CATEGORY = "ログのカテゴリ";
const COL_NUM_SHEET_LOGGING_CATEGORY = 2;

// シートログのトリガ内経過時間の欄はシート上で何列目か
const LABEL_SHEET_LOGGING_CONSUMED_TIME_IN_TRIGGER = "トリガ内";
const COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_TRIGGER = 3;

// シートログの日内処理の累積時間の欄はシート上で何列目か
const LABEL_SHEET_LOGGING_CONSUMED_TIME_IN_ONEDAY = "日内";
const COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_ONEDAY = 4;

// シートログのダイジェストレートの欄はシート上で何列目か
const LABEL_SHEET_LOGGING_DIGEST_RATE = "ダイジェストレート";
const COL_NUM_SHEET_LOGGING_DIGEST_RATE = 5;

// シートログの本文欄はシート上で何列目か
const LABEL_SHEET_LOGGING_CONTENT = "ログ内容";
const COL_NUM_SHEET_LOGGING_CONTENT = 6;

// シートログ記録はシート上で何行目から開始するか
const ROW_NUM_SHEET_LOGGING_START = 2;

// シートログが何件まで増えたら,古い情報のカット処理を行なうか
const MAX_ROWS_COUNT_FOR_SHEET_LOGGIONG = 5500;

// シートログが想定の最大件数を超過したら,最も古いほうから何行ぶんをカット処理するか
const HOW_MANY_LOGS_TO_CUT_OFF_FOR_SHEET_LOGGING = 500;
  // ※動作テストする場合は,10行を上限,カット数を3行などにします。



// ------- トリガ実行結果の可視化についての設定項目 -------

// 可視化用シートのシート名
const SHEET_NAME_FOR_TRIGGER_VISUALIZATION = "トリガ結果図示";

// 「可視化用のシートを手動で編集しないように」という注意書きはシート行で何行何列目か
const ROW_NUM_TRIGGER_VISUALIZATION_ATTENTION_FOR_MANUAL_EDIT = 1;
const COL_NUM_TRIGGER_VISUALIZATION_ATTENTION_FOR_MANUAL_EDIT = 1;

// 可視化用シート上で,本日の日付を新たに記載する場所は何行何列目か
const ROW_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY = 2;
const COL_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY = 2;

// 可視化用シートの項目名ラベルはシート上で何行目に記載するか
const ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS = 3;

// 可視化用シート上で現在記録中の時間帯は,何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE = 2;

// 可視化用シート上で最新の日付のトリガ起動確立数は何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_TRIGGERS_COUNT = COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE + 1;
// 「トリガ起動確立数」の列番号をシート上でアルファベットにしたもの
const COL_ALPHABET_TRIGGER_COUNT_ON_TRIGGER_VISUALIZATION_SHEET = "C";

// 可視化用シート上で時間帯ごとのエラー数は何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_ERRORS_COUNT = COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE + 2;

// 可視化用シート上で最新の日付の仕事完了数は何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS = COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE + 3;
// 「仕事完了数」の列番号をシート上でアルファベットにしたもの
const COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET = "E";

// 可視化用シート上で最新の日付の仕事完了数のAAグラフは何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH = COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE + 4;
// 仕事完了数のAAグラフの列番号をシート上でアルファベットにしたもの
const COL_ALPHABET_ACHIEVED_TASKS_AA_GRAPH_ON_TRIGGER_VISUALIZATION_SHEET = "F";

// 可視化用シート上で時間帯ごとのトリガ内経過時間(秒形式)は何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_SECONDS = COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE + 5;
// 「トリガ内経過時間(秒形式)」の列番号をシート上でアルファベットにしたもの
const COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET = "G";

// 可視化用シート上で時間帯ごとのトリガ内経過時間のAAグラフは何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH = COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE + 6;
// トリガ内経過時間のAAグラフの列番号をシート上でアルファベットにしたもの
const COL_ALPHABET_CONSUMED_TIMES_AA_GRAPH_ON_TRIGGER_VISUALIZATION_SHEET = "H";

// 可視化用シート上で日付ごとの区切りの空白列は,最新の日付の右側だと何列目に記載するか
const COL_NUM_TRIGGER_VISUALIZATION_SHEET_SHEET_BLANK_MARGIN = COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE + 7;

// 可視化用シート上で各日付ごとのデータは1日分の列数として何列分を使用するか
const COLS_NUM_FOR_ONE_DAY_ON_TRIGGER_VISUALIZATION_SHEET = 8;

// 可視化用シート上で現在記録中の時間帯は,何行目から記載するか
const ROW_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE_START = 4;

// 可視化用シート上で時間帯は何時間おきに記載するか (整数値のみ。1なら1時間ずつが時間帯となる)
const NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT = 1;

// 可視化用シート上で,仕事完了数のAAグラフは最大で何文字か
const MAX_CHAR_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH = 15;

// 可視化用シート上で,仕事完了数のAAグラフを何という文字の繰り返しで描画するか (素材文字の定義)
const MATERIAL_CHAR_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH = "";

// 可視化用シート上で,トリガ内経過時間のAAグラフは最大で何文字か
const MAX_CHAR_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH = 15;

// 可視化用シート上で,トリガ内経過時点のAAグラフを何という文字の繰り返しで描画するか (素材文字の定義)
const MATERIAL_CHAR_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH = "";

// 可視化用シート上で,日付ごとの区切りに存在する空白列の幅は何ピクセルか
const COL_WIDTH_TRIGGER_VISUALIZATION_SHEET_BLANK_MARGIN = 20;

// 可視化シート上で,日付ごとのデータは最新の日を含めて合計で何日分までを保管しておくか
const MAX_DATE_NUM_TRIGGER_VISUALIZATION_SHEET_TO_KEEP_OLD_DATA = 7;


// ------- ブック内のシート並び順についての設定項目 -------


// 各シートの望ましい並び順を表す配列
const ARR_ORDERED_SHEET_NAMES = [

  // トリガ実行結果の可視化用シート
  SHEET_NAME_FOR_TRIGGER_VISUALIZATION,

  // シートログ用のシート
  SHEET_NAME_FOR_SHEET_LOGGING,

  // トリガ制御用のシート
  SHEET_NAME_TRIGGER_INFO

];
  // 上記配列内の定数はそれぞれ,該当シート名として定義されている。


// ------------------------ 下記はメイン処理 -----------------------------




// GASエディタ上で定期的なトリガを設定する関数。
//
// 注意: この関数内には具体的な処理詳細は記載しない。
//
function my_triggeredMainFunction(){

  // 新規トリガの排他制御と,トリガ起動確立を試みる
  MyTriggerManager.tryLockForNewTriggerAndMainProc();

}


// GASエディタ上で手動でテスト実行するための関数。
// 前回のトリガ起動時間から十分な時間間隔があいていなくても,手動でメイン処理を呼び出せる。
// ただし複数のトリガが並列して重複実行されないように,排他制御はしている。
//
// 注意: この関数内には具体的な処理詳細は記載しない。
//
function my_manualStartedMainFunction(){

  // 新規トリガの排他制御と,トリガ起動確立を試みる
  MyTriggerManager.tryLockForNewTriggerAndMainProc( { manual_start : true } );

}



// ロック保持中に呼び出されるメイン処理。実行したい具体的な処理内容はここに書く。
// (すでにロック保持し排他制御中なので,ここに記載した処理が同時に別トリガから重複実行される心配は不要)
function my_mainRoutineWhileLockedSection(){


  // 動作テスト用なので,ちょっと多めに呼び出して負荷をかけてみるか。
  for( var i = 0; i < 500; i ++ ){  
    
    // トリガ内に残り時間が十分にあるか?
    if( ! MyTriggerManager.isCurrentTriggerRemainingTimeSafe() ){

      MySheetLogger.controlInfo(
        "現在のトリガ残り時間は安全でないため,トリガ内の処理を打ち切ります。" 
          + "処理打ち切りに関わる残り時間についての説明: " + MyTriggerManager.getRemainingTimeDescriptionOnBreakingByTimeout()
      );

      // for文を抜ける
      break;

    }
    // 以下は,トリガ内処理を継続できる場合


    // ここにユーザー定義タスクを記述する。
    // テスト動作として,トリガ情報のリアルタイム値を調べる自作APIを呼び出し,シートログに記録する。
    MySheetLogger.testLog(
      "テスト動作用にトリガ起動。ロック保持中にログ記録。(" + i + ") "
        + "現在のトリガ情報の説明: " + MyTriggerManager.getAllCurrentStateDescriptions()
    );

    // ユーザー定義タスクが1つ完了したことを知らせる (※ここからの通知は棒グラフ描画に反映される)
    MyTriggerManager.onSingleUserTaskFinished();


    // エラー発生数を時間帯別に記録することもできる
    //if( i % 20 == 0 ){
    //  MyTriggerManager.recordOneErrorInCurrentHourZone();
    //}


  } // for文の終わり


  // テスト動作として,トリガ情報のリアルタイム値を調べる自作APIを呼び出し,シートログに記録する。
  MySheetLogger.testLog( 
    "今回のトリガ内で完了したタスクの個数:"
      + MyTriggerManager.getCurrentTriggerAchievedUserTasksCountAsCashedValue()
  );

  return;

}
// MEMO:
// 上記のforループ自体をトリガマネージャ内に組み込んだほうがよいかどうか,について。
// ユーザー定義タスクを複数セットしたい場合,タスクそのものをクラスで抽象化するという設計もありうるだろう。
// でも,それをやっちゃうと「シート上をスキャンしながらタスクを順番に消化」という実装がしづらくなる。
// 「botデータをシート上に列挙しておいて順番につぶやく」という用途を想定すると,forループ的なコード構造を隠ぺいせず残したほうがよい。
// そのため,上記では明示的なforループにしてある。(while文でもよい)




// ------------------------ 自前でのトリガ管理 -----------------------------



// トリガマネージャ。トリガ情報を取得・記録する。
// トリガごとのコンテキスト内で常に存在するglobal変数のような存在であり,newする必要はない。
let MyTriggerManager = {


  // ----------- 下記はこのオブジェクトの外部から呼び出される関数 ----------


  // 新規トリガ起動のために,排他制御のロック確保を試みる。
  // ロック取得できれば,トリガ起動の確立を試みて,具体的なメイン処理を呼び出す。
  // (この関数内には具体的なメイン処理は書かない)
  tryLockForNewTriggerAndMainProc : function( option_obj ){

    // ドキュメントロック取得を試みる
    var docLocker = LockService.getDocumentLock();
      my_debug_log( "ロック取得を試みます。最大待機時間は " + WAIT_MS_FOR_LOCK + "ms" );
    if (docLocker.tryLock( WAIT_MS_FOR_LOCK )) {

      // メイン処理を呼び出してからロック開放する。
      try {
        my_debug_log( "ロック取得に成功しました。" );

        // ここにロック保持中のメイン処理

        // トリガの起動確立を試みる
        this.tryToEstablishNewTrigger( option_obj );

      } catch(e) {
          
          // メイン処理中のエラー分岐があればここに記載
          my_err_log( "ロック保持中のメイン処理内でエラー発生。", e );

          // 可能ならシートログも残す
          MySheetLogger.err( "ロック保持中のメイン処理内でエラーが発生しました。", e );

          // ユーザー定義タスクの実行中にエラーが1つ発生したことを時間帯レポートに記録する
          MyTriggerManager.recordOneErrorInCurrentHourZone();

      } finally {
          
          // メイン処理の実行成否に関わりなく,最後にロック解放
          docLocker.releaseLock();
            my_debug_log( "ロック解放しました。" );

      }
    }else{

      // 指定秒数だけ待ってもロック取得できなかった場合   
      my_err_log("ロック取得に失敗しました。");
        // この場合,シートログは記載しない。
        // 並行して動いているプロセス同士で,シートへの書き込みの順序が前後・衝突してしまう恐れがあるから。

    }

    return;


    // ※トリガのロック取得に関する説明用の記事:
    // 「たった200行で,GASのトリガ重複実行を検出・回避し,シート末尾に定期的にログ記録するサンプルコード
    //  (排他制御しつつ,Googleスプレッドシート内でデータが存在する最終行に情報記録) 」
    //  https://qiita.com/rwanda_go_tan/items/e6a8bae04fdd2d1ba9a6


    // ※ちなみに,GASではコード中で定義されたオブジェクトのメソッドを
    //   直接トリガに指定することはできない。指定可能なのは「地の関数」のみ。

  },
    

  // 日内とトリガ内を組み合わせた正味の計算からして,
  // 現在のトリガ内処理を継続してよいだけの残り時間の余裕があるかどうか(その意味で「安全」かどうか)を返す。
  isCurrentTriggerRemainingTimeSafe : function(){

    // まず,安全係数を加味したうえで
    // 「現在のトリガを含め1日の全トリガの処理時間の上限値に対する割合」が
    // 「現時点での日内時間の経過率」を超過していないか?

    // 比を取得
    const digest_rate_safe_hedged = this.getSafeHedgedRealTimeConsumedRateOnedayAsRatio();
    if( digest_rate_safe_hedged >= 1 ){
      
      // 1未満じゃないと「余裕なし。1日のペースから見て多く動きすぎ」という判定を返す
      my_debug_log( "digest_rate_safe_hedgedが1以上です。" + digest_rate_safe_hedged );

      // 安全ではない,と返す
      return false;

    }
    else
    {
      my_debug_log( "digest_rate_safe_hedgedは1未満です。" + digest_rate_safe_hedged );
    }


    // そして,安全係数を加味した正味の残り時間は正か?

    // ミリ秒を取得
    const net_remaining_time_safe_hedged_millisec = this.getNetRemainingTimeSafeHedgedMillisec();
    if( net_remaining_time_safe_hedged_millisec <= 0 ){

      // 安全係数を加味したうえで残り時間が負だったら,「余裕なし。ここで処理を打ち切れ」という判定を返す
      my_debug_log( "安全係数を加味した残り時間が0以下です。" + net_remaining_time_safe_hedged_millisec );

      // 安全ではない,と返す
      return false;

    }
    else
    {
      my_debug_log( "安全係数を加味した残り時間は正です。" + net_remaining_time_safe_hedged_millisec );
    }


    // 上記の全てのチェックを通過できれば「安全」とみなす
    return true;

  },


  // 現在のトリガ状態に関する統計情報すべてを文字列として返す。
  // この文字列をログ出力すれば,デバッグに役立つ。
  getAllCurrentStateDescriptions : function(){

    // いろんな統計情報を組み合わせて,文字列を構築する
    const s = ""
      + "現在のトリガIDは"
      + this.getCurrentTriggerIdAsCashedValue()

      // 1日全体の残り時間に関する情報
      + ",現時刻における日内時間の経過率は"
      + this.getCurrentPassedTimePercentageTodayAsRealTimeValue()
      + ",現時刻における日内処理の安全係数は"
      + this.getCurrentSafetyCoefficientOnedayAsPercentageString()
      + ",日内処理のみの残り時間(分秒形式)は"
      + this.getAllTriggersOnedayRemainingTimeReadableAsRealTimeValue()
      + ",日内処理のみの残り時間(RAW%)は"
      + this.getAllTriggersOnedayRemainingTimePercentageAsRealTimeValue()
      + ",日内処理のみの残り時間(安全係数込みの%形式文字列)は"
      + this.getAllTriggersOnedayRemainingTimeSafeHedgedPercentageAsRealTimeValue()
      + ",日内処理のみの累積処理時間(安全係数込みの%形式文字列)は"
      + this.getAllTriggersConsumedTimeOnedaySafeHedgedPercentageAsRealTimeValue()
      + ",日内時間の経過率(安全係数込みの%形式文字列)は"
      + this.getCurrentPassedTimePercentageSafeHedgedTodayAsRealTimeValue()
      + ",日内処理の累積処理時間(安全係数込み)を日内時間の経過率(安全係数込み)で割った%形式文字列は"
      + this.getSafeHedgedRealTimeDigestRateOnedayAsPercentage()
      
      // 1トリガ内の残り時間に関する情報
      + ",トリガ内処理のみの残り時間(分秒形式)は"
      + this.getCurrentTriggerRemainingTimeReadableAsRealTimeValue()
      + ",トリガ内処理のみの残り時間(RAW%)は"
      + this.getCurrentTriggerRemainingTimePercentageAsRealTimeValue()
      + ",トリガ内処理のみの残り時間(安全係数込みの%文字列形式)は"
      + this.getCurrentTriggerRemainingTimeSafeHedgedPercentageAsRealTimeValue()

      // 1日と1トリガ内の残り時間を組み合わせた情報
      // (通常は1トリガ内の残り時間を気にすればよいのだが,トリガ内時間上限が来るよりも前に
      // 1日の残り時間上限がやってくる場合もあるので,この2つを組み合わせて残り時間を考慮する必要がある。)
      + ",日内とトリガ内を組み合わせた正味の処理残り時間(分秒形式,安全係数を無視)は"
      + this.getNetRemainingTimeRawReadable()
      + ",日内とトリガ内を組み合わせた正味の処理残り時間(分秒形式,安全係数を加味)は"
      + this.getNetRemainingTimeSafeHedgedReadable()
      + ",日内とトリガ内を組み合わせた正味の計算からしてトリガ内処理を継続してよいかどうかのフラグは"
      + this.isCurrentTriggerRemainingTimeSafe()
    ;

    return s;
  },


  // 現在のトリガ状態に関する統計情報のうち,処理打ち切りに関わる残り時間の情報を文字列として返す。
  // この文字列をログ出力すれば,デバッグに役立つ。
  getRemainingTimeDescriptionOnBreakingByTimeout : function(){

    // なぜ処理を打ち切らなければいけなかったか,数値的な記録を残す
    const s = ""
      + "日内処理のみの累積処理時間(安全係数込み)を日内時間の経過率(安全係数込み)で割った%形式文字列は"
      + this.getSafeHedgedRealTimeDigestRateOnedayAsPercentage()
      + ",日内とトリガ内を組み合わせた正味の処理残り時間(分秒形式,安全係数を加味)は"
      + this.getNetRemainingTimeSafeHedgedReadable()
    ;

    return s;
  },


  // ユーザー定義タスクが1つ完了したことを知らされる
  onSingleUserTaskFinished : function(){

    // 「現トリガ内でのユーザー定義タスクの完了数」のキャッシュ値を1だけ増やす。
    this.incrementCurrentTriggerAchevedUserTasksCountAsCashedValue();

  },


  // ユーザー定義タスクの実行中にエラーが1つ発生したことを時間帯レポートに記録する
  recordOneErrorInCurrentHourZone : function(){
    
    // トリガ起動日時(キャッシュ値から取得)
    const trigger_start_dt = this.getCurrentTriggerStartDateObjAsCashedValue();
      // エラー発生時点での時間帯を特定するために,
      // トリガ起動時点を表すDateオブジェクトが必要

    // 即座にビジュアライザに渡す (今すぐ可視化シート上に記録・反映)
    TriggerStatVisualizer.onReceivedOneErrorWhileCurrentTrigger({
      trigger_start_dt : trigger_start_dt,
      error_count      : 1 // 今回のエラー発生数は1個分と数える
    });

  },


  // ----------- オブジェクトの外側から呼び出される関数はここまで。 ----------
  // ----------- 以降はオブジェクトの内部処理のうち,トリガ管理のメインの流れに関するもの。 ----------


  // ロック保持中に,前回のトリガ起動時刻などと照らし合わせて,新規トリガ起動の確立を試みる。
  // 新規トリガ起動が許可・確立されれば,具体的なメイン処理を呼び出す。
  // (この関数内には具体的なメイン処理は書かない)
  tryToEstablishNewTrigger : function( option_obj ){

    // まず真っ先に,現在日時(トリガ起動タイミング)を控えておく。
    // (トリガ内の累計処理時間を漏れなく数えるために必要だから,できるだけ早めにカウントし始める必要がある)
    const trigger_start_dt = new Date();
    this.setCurrentTriggerStartDateObjAsCashedValue( trigger_start_dt ); // キャッシュ値としてオブジェクト内に保管
  

    // メンテナンス中のため,全てのトリガ起動をキャンセルすべきか?
    if( this.mustDisableAllTriggersForMaintenance() ){

      my_debug_log("コードメンテナンス中のため,トリガ起動をキャンセルします。シートログも記録しません。");

      // 処理を中断する。
      return;

    }


    // 手動で起動された場合は,前回のトリガ起動からの時間間隔をチェックしない。
    if( option_obj && option_obj.manual_start ){

      // 手動起動された場合
      my_debug_log( "今回は手動起動されたケースなので,前回のトリガ起動から時間があいているかチェックしません。" );

    }
    else
    {
      // 手動起動ではなく,トリガが自動起動された場合
      my_debug_log( "今回はトリガが自動起動されたケースなので,前回のトリガ起動から時間があいているかチェックします。" );

      // 前回のトリガから見て,新規トリガ起動間隔が短ければ処理を中断
      if( TriggerInfoSheetDomainLogics.isNewTriggerStartedTooShortSinceLastTrigger() ){
        
        MySheetLogger.controlInfo(
          "前回のトリガ起動時刻から十分な時間が経過していないため,今回のトリガ起動はキャンセルします。",
          { disable_digest_rates : true }
        );

        // 処理を中断する。
        return;
      }
      else
      {

        MySheetLogger.controlInfo(
          "前回のトリガ起動時刻から十分な時間が経過しているため,今回のトリガ起動を確立します。",
          { disable_digest_rates : true }
        );

      }
    } // 時間間隔チェック終わり


    // ここまでで,「新規トリガ起動を中断しなくてもよい事」が確定した。

    // 新規トリガの起動確定についてシート上に記録し,メイン処理を実行する
    this.onNewTriggerStartEstablished();

    return;

  },


  // 新規トリガの起動が確定した時に,そのことを記録するために呼び出される。
  onNewTriggerStartEstablished : function(){
      my_debug_log( "MyTriggerManager.onNewTriggerStartEstablished()が呼ばれました。" );


    // 新規トリガ情報の様々な項目をシート上に記録
    const trigger_start_dt = this.getCurrentTriggerStartDateObjAsCashedValue();
    this.recordNewTriggerInfoOnSheet( trigger_start_dt );

    // トリガ実行結果の可視化用のシートを準備しておく
    TriggerStatVisualizer.setupVisSheet( trigger_start_dt );
  

    // ロック保持中に呼び出されるメイン処理は下記の関数の中に書く
    my_mainRoutineWhileLockedSection();


    // 今回のトリガによる処理の完了をシート上に記録する
    this.onCurrentTriggerFinished();

    return;

  },


  // メンテナンスモードのため,全てのトリガ起動をキャンセルすべきかどうかを判定する。
  mustDisableAllTriggersForMaintenance : function(){

    // 定数により状態を定義する
    return DISABLE_ALL_TRIGGERS_FOR_MAINTENANCE;

  },


  // 新規トリガ起動時に,トリガ情報の様々な項目をシート上に記録
  recordNewTriggerInfoOnSheet : function( trigger_start_dt ){

    // トリガ情報を記録するためのシートを(必要なら)初期化
    TriggerInfoSheetDomainLogics.setupTriggerInfoSheet();

    // 前回のトリガが時間切れで異常終了していないかチェック・正常化しておく。
    TriggerInfoSheetDomainLogics.checkLastTriggerEndStatus();

    // 今回のトリガ起動時刻 (必要なら,前日と本日の情報をローテーションする)
    TriggerInfoSheetDomainLogics.setCurrentTriggerStartTime( trigger_start_dt );

    // 新規トリガIDを生成・取得
    const new_trigger_id = TriggerInfoSheetDomainLogics.getNewTriggerIdValidInt();
    TriggerInfoSheetDomainLogics.setLastTriggerId( new_trigger_id );
    this.setCurrentTriggerIdAsCashedValue( new_trigger_id ); // シート上だけでなく,オブジェクト内にもキャッシュしておく

    // 今回のトリガ内の処理に要した時間の記録表示をシート上でいったんクリア
    TriggerInfoSheetDomainLogics.clearConsumedTimeWhileCurrentTrigger();

    // 本日のトリガ起動回数に+1
    TriggerInfoSheetDomainLogics.incrementAllTriggersCountToday();

    // 前回のトリガまでに本日起動された全トリガが消費し終えた合計時間を
    // シート上からミリ秒として取得。(空欄の場合は0が返る)
    // その値をすぐ参照できるようにするため,オブジェクト内にもキャッシュしておく。
    const consumed_time_past_triggers_millisec = TriggerInfoSheetDomainLogics.getConsumedTimeMilliSecWhileAllTriggersToday();
    this.setConsumesTimePassedTriggersTodayAsCashedValue( consumed_time_past_triggers_millisec );

    // 「新規トリガ起動に成功した」とトリガステータス記録
    TriggerInfoSheetDomainLogics.setLastTriggerStatusStartSuccess();


    return;

  },
  
  
  // 今回のトリガによる処理が完了した時に,そのことを記録するために呼び出される
  onCurrentTriggerFinished : function(){
      my_debug_log( "MyTriggerManager.onCurrentTriggerFinished()が呼ばれました。" );

    // 今の時点を,今回のトリガ内処理の終了時刻とする
    const trigger_end_dt = new Date();
    
    // トリガ起動日時(キャッシュ値から取得)
    const trigger_start_dt = this.getCurrentTriggerStartDateObjAsCashedValue();


    // ---------- 今回のトリガについて記録 ---------- 

    // 今回のトリガ内の処理に要した時間を記録
    const consumed_time_millisec = TriggerInfoSheetDomainLogics.setConsumedTimeWhileCurrentTrigger( trigger_end_dt );

    // 今回のトリガ内で果たしたユーザ定義タスクについて記録
    const achieved_user_tasks_one_trigger = this.getCurrentTriggerAchievedUserTasksCountAsCashedValue();
    TriggerInfoSheetDomainLogics.updateAchievedUserTasksWhileCurrentTrigger( consumed_time_millisec, achieved_user_tasks_one_trigger );


    // ---------- 本日の全トリガについて記録 ---------- 

    // 本日の全トリガ内の処理に要した時間に1トリガ分を加算
    const all_triggers_stat = this.addConsumedTimeWhileAllTriggersToday( consumed_time_millisec, trigger_end_dt );
    const all_triggers_count_oneday = all_triggers_stat.all_triggers_count_oneday;
    const all_triggers_consumed_time_millisec = all_triggers_stat.all_triggers_consumed_time_millisec;

    // 本日の全トリガ内で果たしたユーザ定義タスクについて記録
    TriggerInfoSheetDomainLogics.updateAchievedUserTasksWhileAllTriggersToday({
      all_triggers_consumed_time_millisec : all_triggers_consumed_time_millisec, 
      all_triggers_count_oneday           : all_triggers_count_oneday,
      achieved_user_tasks_one_trigger     : achieved_user_tasks_one_trigger
    });


    // ---------- 可視化用に記録 ---------- 

    // 1回分のトリガ実行結果を受け取って,可視化(ビジュアライズ)用にシート上に記録
    TriggerStatVisualizer.onReceivedOneTriggerResult({
      trigger_start_dt                   : trigger_start_dt,
      achieved_user_tasks_one_trigger    : achieved_user_tasks_one_trigger,
      consumed_time_millisec_one_trigger : consumed_time_millisec
    });



    // ---------- トリガ終了を確定 ---------- 

    // 「トリガ終了に成功した」というステータスを記録
    TriggerInfoSheetDomainLogics.setLastTriggerStatusEndSuccess();

    return;

  },

  
  // 本日の全トリガ内の処理に要した時間に,1トリガ分を加算して記録する。
  // 日内の合計時間(ミリ秒および分秒形式)だけでなく,日内の使用率や1トリガ当たり平均時間もあわせて更新する。
  addConsumedTimeWhileAllTriggersToday : function( one_trigger_consumed_time_millisec, trigger_end_dt ){

    // 少しでもシート参照の回数を減らして高速化するために,ここでは
    // 最終トリガ内の所要時間をシート参照ではなく引数受け取りとした。


    // 本日の全トリガ内の所要時間をミリ秒で(シートではなくオブジェクト内の)キャッシュ値から)取得し,最終トリガのぶんを加算する。
    let all_triggers_consumed_time_millisec = this.getConsumedTimePassedTriggersTodayAsCashedValue();
    all_triggers_consumed_time_millisec += one_trigger_consumed_time_millisec;


    // 本日の全トリガ内の所要時間(ミリ秒,および分秒形式)をシート上に記録
    TriggerInfoSheetDomainLogics.updateConsumedTimesWhileAllTriggersToday( all_triggers_consumed_time_millisec );

    // 本日の全トリガの累積処理時間の枠内使用率をシート上に記録
    const consumed_time_ratio = TriggerInfoSheetDomainLogics.setConsumedTimePercentageAllTriggersTodayByMillisec( all_triggers_consumed_time_millisec );

    // 本日の最終トリガ終了時点での日内時間経過率をシート上に記録
    const trigger_start_dt = this.getCurrentTriggerStartDateObjAsCashedValue();
    const passed_time_ratio = TriggerInfoSheetDomainLogics.setPassedTimePercentageOnLastTriggerFinishedTodayAsDateObj( trigger_end_dt, trigger_start_dt );

    // 本日の最終トリガ終了時点でのダイジェストレートをシート上に記録
    TriggerInfoSheetDomainLogics.updateDigestRatePercentageOnLastTriggerFinishedToday( consumed_time_ratio, passed_time_ratio );

    // 本日の全トリガ内の平均処理時間を,分秒形式および枠内使用率の形式でシート上に記録
    const all_triggers_count_oneday = TriggerInfoSheetDomainLogics.analyzeAverageConsumedTimeAllTriggersToday( all_triggers_consumed_time_millisec );


    // 返り値
    const ret = {
      all_triggers_count_oneday : all_triggers_count_oneday,
      all_triggers_consumed_time_millisec : all_triggers_consumed_time_millisec
    };
    return ret;


    // NOTE: なお,このメソッドの「前回のトリガが異常終了した時用」のバージョンが下記にあるので
    // 2つのメソッドの内容に相互に抜け漏れがないように注意。
    (function(){
      // GASエディタ上でコードを相互参照しやすくしてある。
      TriggerInfoSheetDomainLogics.addConsumedTimeWhileAllTriggersTodayOnLastTriggerFailedToFinishNormally();
    });

  },


  // ----------- オブジェクトの内部処理のうち,トリガ管理のメインの流れに関するものはここまで。 ----------
  // ----------- 以降はオブジェクトの内部処理のうち,とくにシートアクセスが発生しないリアルタイム処理に関するもの。 ----------


  // ----------- 安全係数 ----------


  // 現時刻における1日の中での安全係数を0以上1以下の実数で返す。
  // 安全係数は1日の中の時間帯に応じて変化し,1日の初めほど低く,1日の終わりほど高い。
  // こうすることで,1日の後半や終盤に残り処理時間が尽きてしまわないようにし,
  // また1日を終えるタイミングまでに利用可能枠をできるだけ使い切るようにする。
  getCurrentSafetyCoefficientOnedayAsRatioNumber : function(){

    // 現時刻の日内時間の経過率
    const current_passed_time_ratio = this.getCurrentPassedTimeRatioTodayAsRealTimeValue();

    // 日内時間の経過率が0から1まで変化するにつれて,時間帯が遅くなる。
    // 時間帯に応じて,返却する安全係数を返す。
    let safety_coeff = 1;
    if(
      // 夜0時から1時まで
      current_passed_time_ratio <= (1/24 + 1/100)
    ){

      // ここだけは,安全係数を十分に高く(つまり,遠慮せずたくさん動作できるように)しておく。
      safety_coeff = 1.0;
        // NOTE:
        // ・日付が変わった直後は,「日内時間の経過率」がとても少ないので
        //   日内時間経過率を基準に実行時間を決めようとすると,実行可能な処理時間はすごく少なくなってしまう。
        //   そのため,日が変わった直後は多めに余裕を生み出しておく。
        // ・なお安全係数を1より大きくすると様々な不具合が出るので,最大で1.0である。
        //   1より大きい値を返してしまうと,1トリガ内で6分までという枠をはみでてタイムアウトエラーで打ち切りになったりする。

    }
    /*
    else if(
      // 夜0:30から2時過ぎまで
      current_passed_time_ratio <= (1/12 + 1/100)
    ){
      // 少し控えめに
      safety_coeff = 0.93;
        // NOTE:
        // 不等式の条件を2時ちょうどまでにせず,1/100という微小量を足してある理由は,
        // 2時ちょうどの時点で時報が動作したい可能性があるから。
        // この部分は連日の実運用を通し,試行錯誤を繰り返してパラメータ調整してある。
    }
    else if(
      // 夜2時から朝4時まで
      current_passed_time_ratio <= (1/6 - 1/100)
    ){
      // やや減らす
      safety_coeff = 0.92;
    }
*/
    else if(
      // 朝5時まで
      current_passed_time_ratio <= 5/24
    ){
      // 少し控えめに
      safety_coeff = 0.93;
    }
    else if(
      // 朝9時まで
      current_passed_time_ratio <= (9/24 - 1/100)
    ){
      // ちょっと増やす
      safety_coeff = 0.94;
    }
    else if(
      // 朝9時からお昼の正午まで
      current_passed_time_ratio <= (1/2 - 1/100)
    ){
      // ちょっと増やす
      safety_coeff = 0.95;
        // 正午から微小量を引いてある理由は,
        // 正午のタイミングで時報を動かしたい人が多いと思うから。
        // そして正午からは一般的にお昼休みで,その時間中にTLは加速してほしいから。
    }
    else if(
      // お昼の正午から17時まで
      current_passed_time_ratio <= ( 17/24 - 1/100 )
    ){
      // 稼働をもっとちょっと増やす
      safety_coeff = 0.96;
        // 18時ではなく17時とした理由は,たとえばbot投稿のバッチ処理で
        // 一般的な人々が帰りの通勤電車の中で読むbotツイートが増えてほしいから。
    }
    else if(
      // 17時から21時まで
      current_passed_time_ratio <= 7/8
    ){
      // 稼働をさらにちょっと増やす
      safety_coeff = 0.97;
    }
    else
    {
      // 21時から24時まで

      // ほぼフルで稼働させて,1日の終わりまでに残り時間を「ほぼ完全に」使い切らせる。
      safety_coeff = 0.98;
        // ※1.0にして「完全に」使い切らせる,というのは良くない。
        //   1日の終わりの時点で,ごくわずかでも余地を残したほうがよい。
        //   たとえば「残り時間が安全でない」と判断してトリガ内処理を打ち切った後などに,
        //   トリガ情報を記録するなどの事後処理のために若干の時間を要するから。
    }

    return safety_coeff;

    // NOTE:
    // より完璧を求めるなら,時間帯だけで決め打ちで係数を変えるのではなく,
    // その時間までにどのようなペースで処理を完了してこれたか?など実績に基づいて
    // 1日の残りの時間についてもリアルタイムで予測を立て直し,安全係数をきめ細やかに柔軟に変化させるという方法もある。
    // つまり,安全係数の変動率を変動させるということ。(係数曲線の形状をリアルタイム変化させるということ。)
    // がしかし,そこまで制御工学っぽい事をする必要は今は無いと思う。

  },

  // 現時刻における安全係数を%形式の文字列で返す。
  getCurrentSafetyCoefficientOnedayAsPercentageString : function(){
    
    // 安全係数を0以上1以下の数値として取得 
    const safety_coeff_ratio = this.getCurrentSafetyCoefficientOnedayAsRatioNumber();

    // %形式の文字列に変換
    const safety_coeff_percentage_string = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace( safety_coeff_ratio );

    return safety_coeff_percentage_string;

  },

  
  // ----------- 上限値 ----------


  // 1トリガ内の理論上の処理時間の上限値ミリ秒を返す。
  // 安全係数は考慮しないし,1日のトータルの処理時間も考慮しない。
  getLimitMillisecOneTrigger : function(){
    
    // この計算はマネージャクラスからも使うが,おもにドメインロジック内で多用したいので
    // マネージャクラス上ではなくドメインロジック内に実装しておく。
    return TriggerInfoSheetDomainLogics.getLimitMillisecOneTrigger();
  
  },

  // 1トリガ内で使用できる処理時間の上限値を,安全係数をかけた値としてミリ秒で返す。
  getSafeHedgedLimitMillisecOneTrigger : function(){
    
    // 1トリガ内の処理時間の上限値(ミリ秒)
    const limit_time_millisec_raw = this.getLimitMillisecOneTrigger();

    // 1トリガあたりの安全係数をかける
    const limit_time_millisec_safety_hedged = Math.floor( limit_time_millisec_raw * SAFETY_COEFFICIENT_FOR_ONE_TRIGGER );

    return limit_time_millisec_safety_hedged;
  
  },

  // 1日の枠で決められた全トリガ分の合計処理時間の上限値をミリ秒で返す。
  // 安全係数は考慮しない。
  // また,1つのGoogleアカウント内で複数のGASプロジェクトを保有・運営する場合もあるので
  // ここではあくまでも本プロジェクト内に限った場合の値とする。
  getLimitMillisecAllTriggersOneDay : function(){

    // マネージャ側だけでなくドメインロジック側でよく使うので,
    // ドメインロジック側に実装する。
    return TriggerInfoSheetDomainLogics.getLimitMillisecAllTriggersOneDay();

  },

  // 1日の枠で決められた処理時間の上限値に,現時点での安全係数をかけた値をミリ秒で返す。
  getLimitMillisecAllTriggersOneDayAsSafeHedgedValue : function(){

    // 1日の枠で決められた処理時間の上限値をミリ秒で返す
    const limit_millisec_raw = this.getLimitMillisecAllTriggersOneDay();

    // 現時点での安全係数をかけて整数にする
    const safety_coeff_ratio = this.getCurrentSafetyCoefficientOnedayAsRatioNumber();
    const limit_millisec_safe_hedged = Math.floor( limit_millisec_raw * safety_coeff_ratio );

    return limit_millisec_safe_hedged;

  },


  // ----------- 日内の経過時間 ----------


  // 現時点までに1日の中で日内時間が何ミリ秒経過したかを返す。
  getCurrentPassedTimeMillisecToday : function(){

    // 現時点のDateオブジェクト
    const dt_now = new Date();

    // 本日の0時から数えて,現時点まで何ミリ秒が経過しているか
    const current_passed_time_millisec_today = TriggerInfoUtil.transformDateObjToPassedTimeMillisec( dt_now, dt_now );

    return current_passed_time_millisec_today;
  },

  // 現時点までの1日の中での経過時間を,0以上1以下の比の実数で返す。(日内時間のリアルタイム経過率)
  getCurrentPassedTimeRatioTodayAsRealTimeValue : function(){

    // 現時点までに1日の中で何ミリ秒が経過したか?
    const current_passed_time_millisec_today = this.getCurrentPassedTimeMillisecToday();

    // 1日の全ミリ秒で割った比率
    const current_passed_time_ratio = current_passed_time_millisec_today / HOW_MANY_MS_IN_ONE_DAY;

    return current_passed_time_ratio;

  },

  // 現時点までの1日の中での経過時間を,%形式の文字列で返す。
  getCurrentPassedTimePercentageTodayAsRealTimeValue : function(){
    
    // 日内経過時間の経過率を0以上1以下の数値として取得 
    const passed_time_ratio = this.getCurrentPassedTimeRatioTodayAsRealTimeValue();

    // %形式の文字列に変換
    const passed_time_percentage_string = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace( passed_time_ratio );

    return passed_time_percentage_string;

  },

  // 日内時間の経過率に,現時点での安全係数をかけた値を返す。(0以上1以下の実数)
  getCurrentPassedTimeRatioSafeHedgedTodayAsRealTimeValue : function(){

    // 現時点での日内時間の経過率
    const passed_time_ratio = this.getCurrentPassedTimeRatioTodayAsRealTimeValue();

    // 現時点での安全係数をかける
    const safety_coeff = this.getCurrentSafetyCoefficientOnedayAsRatioNumber();
    const passed_time_ratio_safety_hedged = passed_time_ratio * safety_coeff;

    return passed_time_ratio_safety_hedged;

  },

  // 日内時間の経過率(安全係数込み)を,%形式の文字列で返す。
  getCurrentPassedTimePercentageSafeHedgedTodayAsRealTimeValue : function(){

    // 比を取得
    const passed_time_ratio_safe_hedged = this.getCurrentPassedTimeRatioSafeHedgedTodayAsRealTimeValue();

    // %形式の文字列にする
    const passed_time_percentage_safe_hedged = TriggerInfoUtil
      .transformRatioToPercentageStringToFirstDecimalPlace( passed_time_ratio_safe_hedged );

    return passed_time_percentage_safe_hedged;

  },


  // ----------- 現在のトリガ処理のみの消費済み時間 ----------


  // 現在起動中のトリガがトリガ内で消費した時間をリアルタイム値として返す(ミリ秒)
  getCurrentTriggerConsumedTimeMilliSecAsRealTimeValue : function(){
  
    // 現時点
    const now_dt = new Date();

    // トリガ起動時点
    const trigger_start_dt = this.getCurrentTriggerStartDateObjAsCashedValue();

    // 起動時点とUNIXミリ秒で差を取る
    const consumed_time_millisec = now_dt.getTime() - trigger_start_dt.getTime();

    return consumed_time_millisec;

  },

  // トリガ内経過時間をコロン形式で返す。(シートログ用)
  getCurrentTriggerConsumedTimeAsColonForm : function(){

    // 現在起動中のトリガがトリガ内で消費した時間をミリ秒として取得
    const consumed_time_millisec = this.getCurrentTriggerConsumedTimeMilliSecAsRealTimeValue();

    // コロン形式に直す
    const consumed_time_colon_form = TriggerInfoUtil.transformMillisecToColonFormString( consumed_time_millisec );

    return consumed_time_colon_form;
  },


  // ----------- 現在のトリガ処理のみの残り時間 ----------


  // 現在起動中のトリガについて,トリガ内処理のみの残り時間(ミリ秒形式)を返す。
  // 1日のトータルの処理時間は考慮しない。
  getCurrentTriggerRemainingTimeMillisecAsRealTimeValue : function(){

    // 現在起動中のトリガがトリガ内で消費した時間(ミリ秒)
    const consumed_time_millisec = this.getCurrentTriggerConsumedTimeMilliSecAsRealTimeValue();

    // 1トリガの理論上の上限値ミリ秒
    const limit_millisec = this.getLimitMillisecOneTrigger();

    // ミリ秒どうしで差を取る
    let remaining_millisec = limit_millisec - consumed_time_millisec;

    return remaining_millisec;

  },

  // 現在起動中のトリガについて,トリガ内処理のみの残り時間(0以上1以下の比形式)を返す。
  // トリガ内経過時間のみをもとに算出するので,1日の残り時間は考慮しない。
  // 安全係数も考慮しない。
  getCurrentTriggerRemainingTimeRatioAsRealTimeValue : function(){
    
    // 現在起動中のトリガがトリガ内で残している時間(ミリ秒)
    const remaining_time_millisec = this.getCurrentTriggerRemainingTimeMillisecAsRealTimeValue();

    // 1トリガの理論上の上限値ミリ秒
    const limit_millisec = this.getLimitMillisecOneTrigger();

    // ミリ秒どうしで差を取る
    const remaining_time_ratio = remaining_time_millisec / limit_millisec;

    return remaining_time_ratio;

  },

  // 現在起動中のトリガについて,トリガ内処理のみの残り時間(%形式)を返す。
  // トリガ内経過時間のみをもとに算出するので,1日の残り時間は考慮しない。安全係数も考慮しない。
  getCurrentTriggerRemainingTimePercentageAsRealTimeValue : function(){
    
    // 残り時間の上限に対する比率を求める
    const remaining_time_ratio = this.getCurrentTriggerRemainingTimeRatioAsRealTimeValue();

    // %形式に直す
    const remaining_time_percentage = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace( remaining_time_ratio );

    return remaining_time_percentage;

  },

  // 現在起動中のトリガについて,トリガ内処理のみの残り時間(分秒形式)を返す。
  // トリガ内経過時間のみをもとに算出するので,1日の残り時間は考慮しない。安全係数も考慮しない。
  getCurrentTriggerRemainingTimeReadableAsRealTimeValue : function(){
    
    // 現在起動中のトリガがトリガ内で残している時間(ミリ秒)
    const remaining_time_millisec = this.getCurrentTriggerRemainingTimeMillisecAsRealTimeValue();

    // 分秒形式に直す
    const remaining_time_readable = TriggerInfoUtil.transformMillisecToReadableString( remaining_time_millisec );

    return remaining_time_readable;

  },

  // 現在起動中のトリガについて,トリガ内処理のみの残り時間を安全係数込みでのミリ秒として返す。
  // なお,トリガ用の安全係数は日内用の安全係数とは異なる。
  // 上限を超過している場合は0を返す。
  getCurrentTriggerRemainingTimeSafeHedgedMillisecAsRealTimeValue : function(){

    // 現在起動中のトリガがトリガ内で消費した時間(ミリ秒)
    const consumed_millisec = this.getCurrentTriggerConsumedTimeMilliSecAsRealTimeValue();

    // 1トリガ内で使用できる処理時間の上限値に,安全係数をかけた値(ミリ秒)
    const limit_millisec_safety_hedged = this.getSafeHedgedLimitMillisecOneTrigger();

    // ミリ秒どうしで引き算する
    const remaining_millisec_safety_hedged = limit_millisec_safety_hedged - consumed_millisec;

    // 上限を超過?
    if( remaining_millisec_safety_hedged < 0 ){
      // 残り時間の余裕なしという意味で0を返す
      return 0;
    }

    // 上限を超過していない場合
    return remaining_millisec_safety_hedged;
  
  },

  // 現在起動中のトリガについて,トリガ内処理のみの残り時間を
  // 安全係数込みでの比形式(0以上1以下の実数)として返す。
  // なお,トリガ用の安全係数は日内用の安全係数とは異なる。
  getCurrentTriggerRemainingTimeSafeHedgedRatioAsRealTimeValue : function(){

    // 現在起動中のトリガがトリガ内であと残り使用できる時間(ミリ秒,安全係数込み)
    const remaining_millisec_safety_hedged = this.getCurrentTriggerRemainingTimeSafeHedgedMillisecAsRealTimeValue();

    // 1トリガ内で使用できる処理時間の上限値に,安全係数をかけた値(ミリ秒)
    const limit_millisec_safety_hedged = this.getSafeHedgedLimitMillisecOneTrigger();

    // 安全係数込みのミリ秒どうしで,比を求める
    const remaining_ratio_safety_hedged = remaining_millisec_safety_hedged / limit_millisec_safety_hedged;
  
    return remaining_ratio_safety_hedged;

  },

  // 現在起動中のトリガについて,トリガ内処理のみの残り時間を
  // 安全係数込みでの%文字列形式として返す。
  // なお,トリガ用の安全係数は日内用の安全係数とは異なる。
  getCurrentTriggerRemainingTimeSafeHedgedPercentageAsRealTimeValue : function(){

    // 安全係数込みでの比を求める
    const remaining_ratio_safety_hedged = this.getCurrentTriggerRemainingTimeSafeHedgedRatioAsRealTimeValue();
  
    // %形式に直す
    const remaining_percentage_safety_hedged = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace( remaining_ratio_safety_hedged );

    return remaining_percentage_safety_hedged;

  },
  

  // ----------- 日内処理のみの消費済み時間 ----------


  
  // 前回のトリガまでに本日起動された全トリガが消費し終えた合計時間(0以上のミリ秒)
  // をオブジェクト内にキャッシュしておく
  consumed_time_past_triggers_today_millisec : null,


  // 前回のトリガまでに本日起動された全トリガが消費し終えた合計時間(0以上のミリ秒)
  // をキャッシュ値として返す (シート読み取りは発生しない)
  getConsumedTimePassedTriggersTodayAsCashedValue : function(){
    return this.consumed_time_past_triggers_today_millisec;
  },


  // 前回のトリガまでに本日起動された全トリガが消費し終えた合計時間(0以上のミリ秒)
  // をキャッシュ値として保持する (シート読み取りは発生しない)
  setConsumesTimePassedTriggersTodayAsCashedValue : function( ms_value ){
    this.consumed_time_past_triggers_today_millisec = ms_value;
  },


  // 現在起動中のトリガを含め,本日起動した全てのトリガが消費した時間をリアルタイム値として返す。(ミリ秒)
  getAllTriggersOnedayConsumedTimeMilliSecAsRealTimeValue : function(){

    // 前回のトリガまでに本日起動された全トリガが消費し終えた合計時間を取得
    // ※キャッシュ値として取得するので,シート読み取りは発生しない。
    const consumed_time_past_triggers_millisec = this.getConsumedTimePassedTriggersTodayAsCashedValue();

    // 現在起動中のトリガがトリガ内で消費した時間をミリ秒として取得
    const consumed_time_current_trigger_millisec = this.getCurrentTriggerConsumedTimeMilliSecAsRealTimeValue();

    // 上記2つを合算して経過時間とする(ミリ秒)
    const consumed_time_millisec = consumed_time_past_triggers_millisec + consumed_time_current_trigger_millisec;

    return consumed_time_millisec;

    
    // NOTE:
    // ちなみに,この関数で自前で実装しているような機能は,GAS上に公式APIは存在しない。
    // 「GAS 1日に処理可能な残り時間を取得」などのキーワードでググると
    // 「GAS(Google Apps Script)で1日の残り処理時間を直接取得するAPIは存在しません」と出てくる。
    // 公式APIがあれば一番いいんですけどね。

  },

  // 全トリガの日内処理時間をコロン形式で返す。(シートログ用)
  // コロン形式とは,80分2秒を80:02のような文字列として表すこと。(分と秒単位ではカウント表示するが,1時間単位ではカウントを表示しない。)
  getAllTriggersOnedayConsumedTimeAsColonForm : function(){

    // 現在起動中のトリガを含め,本日起動した全てのトリガが消費した時間(ミリ秒)
    const consumed_time_millisec = this.getAllTriggersOnedayConsumedTimeMilliSecAsRealTimeValue();

    // コロン形式に直す
    const consumed_time_colon_form = TriggerInfoUtil.transformMillisecToColonFormString( consumed_time_millisec );

    return consumed_time_colon_form;

  },

  // 日内処理のみの累積処理時間(安全係数込みの%)を,日内時間の経過率で割った比を返す。
  getSafeHedgedRealTimeConsumedRateOnedayAsRatio : function(){

    // 日内処理のみの消費済み時間(安全係数込み%)を,比形式で取得 (基本的に0以上1以下だが,上限超過時は1を超える)
    const consumed_time_ratio_safe_hedged = this.getAllTriggersOnedayConsumedTimeSafeHedgedRatioAsRealTimeValue();

    // 日内時間の経過率(安全係数込み%)
    const passed_time_ratio_safe_hedged = this.getCurrentPassedTimeRatioSafeHedgedTodayAsRealTimeValue();

    // 万が一,分母が0の場合は「残り余裕が十分にある」とみなして,比は0を返す
    if( passed_time_ratio_safe_hedged == 0 ){
      return 0;
    }

    // 分母が0でない場合,比どうしで比を取る。分母も分子も安全係数込み。
    const consumed_rate_safe_hedged = consumed_time_ratio_safe_hedged / passed_time_ratio_safe_hedged;

    return consumed_rate_safe_hedged;
  },

  // 日内処理のみの消費済み時間時間(※上限は安全係数込み)を,0以上1以下の実数(比形式)で返す。
  // ただし上限を超過した場合には,返却する比が1を超える。
  getAllTriggersOnedayConsumedTimeSafeHedgedRatioAsRealTimeValue : function(){

    // 1日の枠で決められた処理時間の上限値に,現時点での安全係数をかけた値(ミリ秒)
    const limit_millisec_safe_hedged = this.getLimitMillisecAllTriggersOneDayAsSafeHedgedValue();

    // 本日の消費済み時間を,0以上のミリ秒で取得
    const consumed_time_millisec_safe_hedged = this.getAllTriggersOnedayConsumedTimeMilliSecAsRealTimeValue();

    // 上限に対して比を取る
    const consumed_time_ratio_safe_hedged = consumed_time_millisec_safe_hedged / limit_millisec_safe_hedged;

    return consumed_time_ratio_safe_hedged;
  },

  // 日内処理のみの累積処理時間(安全係数込み)を,%文字列形式で返す。
  // 日内処理のみで考えるので,現在のトリガ内の残り時間に関する事情は考慮しない。
  getAllTriggersConsumedTimeOnedaySafeHedgedPercentageAsRealTimeValue : function(){
    
    // 日内処理のみの消費済み時間(安全係数込み%)を,比形式で取得 (基本的に0以上1以下だが,上限超過時は1を超える)
    const consumed_time_ratio_safe_hedged = this.getAllTriggersOnedayConsumedTimeSafeHedgedRatioAsRealTimeValue();

    // %形式の文字列にする
    const consumed_time_percentage_safe_hedged = TriggerInfoUtil
      .transformRatioToPercentageStringToFirstDecimalPlace( consumed_time_ratio_safe_hedged );

    return consumed_time_percentage_safe_hedged;

  },

  // 日内処理のみの累積処理時間(安全係数込みの%)を,日内時間の経過率で割った%形式文字列を返す。
  getSafeHedgedRealTimeDigestRateOnedayAsPercentage : function(){

    // 比を取得
    const consumed_rate_oneday_safe_hedged = this.getSafeHedgedRealTimeConsumedRateOnedayAsRatio();

    // %形式の文字列にする
    const digest_rate_percentage_safe_hedged = TriggerInfoUtil
      .transformRatioToPercentageStringToFirstDecimalPlace( consumed_rate_oneday_safe_hedged );

    return digest_rate_percentage_safe_hedged;

  },


  // ----------- 日内処理のみの残り時間 ----------


  // 現在起動中のトリガを含め,本日起動した全てのトリガが消費した時間を考慮したうえで,
  // 本日消費できる全残り時間をリアルタイム値として返す。(0以上のミリ秒)
  // 安全係数は考慮しない。
  getAllTriggersOnedayRemainingTimeMilliSecAsRealTimeValue : function(){
  
    // 現在起動中のトリガを含め,本日起動した全てのトリガが消費した時間(ミリ秒)
    const consumed_time_millisec = this.getAllTriggersOnedayConsumedTimeMilliSecAsRealTimeValue();

    // 1日の枠で決められた上限値ミリ秒
    const limit_millisec = this.getLimitMillisecAllTriggersOneDay();

    // ミリ秒どうしで差を取る
    let remaining_millisec = limit_millisec - consumed_time_millisec;

    // 消費時間が上限を超過している場合もありうる。
    // その場合はマイナスではなく,「残り余裕なし」という意味で0を返す。
    if( remaining_millisec < 0 ){
        my_debug_log("getAllTriggersOnedayRemainingTimeMilliSecAsRealTimeValueでremaining_millisecが負です。" + remaining_millisec);
        my_debug_log("負値のかわりに,0を返却します。");
      remaining_millisec = 0;
    }

    return remaining_millisec;

  },

  // 現在起動中のトリガを含め,本日起動した全てのトリガが消費した時間を考慮したうえで,
  // 本日消費できる全残り時間をリアルタイム値として返す。(分秒形式)
  // ただし1日の枠内ベースのみで計算し,1トリガの枠内では計算評価しない。
  // 安全係数も考慮しない。
  getAllTriggersOnedayRemainingTimeReadableAsRealTimeValue : function(){
  
    // 本日消費できる全残り時間(ミリ秒)
    const remaining_time_millisec = this.getAllTriggersOnedayRemainingTimeMilliSecAsRealTimeValue();

    // 分秒形式に直す
    const remaining_time_readable = TriggerInfoUtil.transformMillisecToReadableString( remaining_time_millisec );

    return remaining_time_readable;

  },

  // 現在起動中のトリガを含め,本日起動した全てのトリガが消費できる残り時間の
  // 日内上限値に対する割合をリアルタイム値として返す。(%形式の文字列)
  // 安全係数は考慮しない。
  getAllTriggersOnedayRemainingTimePercentageAsRealTimeValue : function(){

    // 現在起動中のトリガを含め,本日起動した全てのトリガが消費できる残り時間(ミリ秒)
    const remaining_time_millisec = this.getAllTriggersOnedayRemainingTimeMilliSecAsRealTimeValue();

    // 1日の枠で決められた処理にあてられる上限値ミリ秒
    const limit_millisec = this.getLimitMillisecAllTriggersOneDay();

    // 比を取って%形式文字列にする
    const remaining_time_percentage = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace(
      remaining_time_millisec / limit_millisec
    );

    return remaining_time_percentage;

  },

  // 日内処理のみの残り時間(※上限は安全係数込み)を,0以上1以下の実数(比形式)で返す。
  getAllTriggersOnedayRemainingTimeSafeHedgedRatioAsRealTimeValue : function(){

    // 1日の枠で決められた処理時間の上限値に,現時点での安全係数をかけた値(ミリ秒)
    const limit_millisec_safe_hedged = this.getLimitMillisecAllTriggersOneDayAsSafeHedgedValue();

    // 日内処理のみの上限値に対する残り時間(上限値は安全係数込み)を,0以上のミリ秒で返す。
    const remaining_time_millisec_safe_hedged = this.getAllTriggersOnedayRemainingTimeSafeHedgedMillisecAsRealTimeValue(); 

    // NOTE: 上記は2つの値のどちらも,上限値に対して安全係数をかけてSafe-Hedgedな値を取得している。


    // 上限に対して比を取る
    const remaining_time_ratio_safe_hedged = remaining_time_millisec_safe_hedged / limit_millisec_safe_hedged;

    return remaining_time_ratio_safe_hedged;
  },

  // 日内処理のみの上限値に対する残り時間(※上限値は安全係数込み)を,0以上のミリ秒で返す。
  getAllTriggersOnedayRemainingTimeSafeHedgedMillisecAsRealTimeValue : function(){

    // 1日の枠で決められた処理時間の上限値に,現時点での安全係数をかけた値(ミリ秒)
    const limit_millisec_safe_hedged = this.getLimitMillisecAllTriggersOneDayAsSafeHedgedValue();

    // 現在起動中のトリガを含め,本日起動した全てのトリガが消費した時間(ミリ秒)
    const consumed_time_millisec = this.getAllTriggersOnedayConsumedTimeMilliSecAsRealTimeValue();

    // 安全係数を加味したうえでの,本日消費可能な残り時間(0以上の値)
    let remaining_time_millisec_safe_hedged = limit_millisec_safe_hedged - consumed_time_millisec;

    // 消費時間が上限を超過している場合もありうる。
    // その場合はマイナスではなく,「残り余裕なし」という意味で0を返す。
    if( remaining_time_millisec_safe_hedged < 0 ){
        my_debug_log("remaining_time_millisec_safe_hedgedが負です。" + remaining_time_millisec_safe_hedged);
        my_debug_log("負値のかわりに,0を返却します。");
      remaining_time_millisec_safe_hedged = 0;
    }

    return remaining_time_millisec_safe_hedged;
  },

  // 日内処理のみの残り時間(安全係数込み)を,%文字列形式で返す。
  // 日内処理のみで考えるので,現在のトリガ内の残り時間に関する事情は考慮しない。
  getAllTriggersOnedayRemainingTimeSafeHedgedPercentageAsRealTimeValue : function(){

    // 日内処理のみの残り時間(安全係数込み%)を,0以上1以下の実数(比形式)で取得
    const remaining_time_ratio_safe_hedged = this.getAllTriggersOnedayRemainingTimeSafeHedgedRatioAsRealTimeValue();

    // %形式の文字列にする
    const remaining_time_percentage_safe_hedged = TriggerInfoUtil
      .transformRatioToPercentageStringToFirstDecimalPlace( remaining_time_ratio_safe_hedged );

    return remaining_time_percentage_safe_hedged;

  },


  // ----------- 日内とトリガ内を組み合わせた正味の処理残り時間 ----------


  // 日内とトリガ内を組み合わせた正味の処理残り時間を,ミリ秒形式で返す。
  // (例: トリガが起動してから1分しか経っていないのでトリガ内余裕が残り5分あるとしても,
  //      その日にすでに全体で89分を消費していたら残り1分しかないので,正味では1分というひっ迫した値を返す。)
  // なお安全係数は加味しない。
  getNetRemainingTimeRawMillisec : function(){

    // トリガ内残り時間をミリ秒で取得 (安全係数は加味しない)
    const trigger_remaining_millisec = this.getCurrentTriggerRemainingTimeMillisecAsRealTimeValue();
    let net_remaining_millisec = trigger_remaining_millisec;
      // 基本的にはこの値を返せばよいのだが,これよりも使える時間が短い場合があるので下記で考慮する

    // 1日の残り時間をミリ秒で取得 (安全係数は加味しない)
    const oneday_remaining_millisec = this.getAllTriggersOnedayRemainingTimeMilliSecAsRealTimeValue();

    // トリガ内残り時間は,正味の値はもっと小さいか?
    if( trigger_remaining_millisec > oneday_remaining_millisec ){

      // 小さいほうの値を採用する
      net_remaining_millisec = oneday_remaining_millisec;

    }

    return net_remaining_millisec;

  },

  // 日内とトリガ内を組み合わせた正味の処理残り時間を,分秒形式で返す。
  // (通常は1トリガ内の残り時間を気にすればよいのだが,トリガ内時間上限が来るよりも前に
  // 1日の残り時間上限がやってくる場合もあるので,この2つを組み合わせて残り時間を考慮する必要がある。)
  // なお安全係数は加味しない。
  getNetRemainingTimeRawReadable : function(){

    // ミリ秒として取得
    const net_remaining_time_raw_millisec = this.getNetRemainingTimeRawMillisec();

    // 分秒形式に直す
    const net_remaining_time_raw_readable = TriggerInfoUtil.transformMillisecToReadableString( net_remaining_time_raw_millisec );

    return net_remaining_time_raw_readable;

  },

  // 日内とトリガ内を組み合わせた正味の処理残り時間を,安全係数を加味したミリ秒形式で返す。
  getNetRemainingTimeSafeHedgedMillisec : function(){

    // トリガ内残り時間をミリ秒で取得 (安全係数も加味)
    const trigger_remaining_millisec = this.getCurrentTriggerRemainingTimeSafeHedgedMillisecAsRealTimeValue();
    let net_remaining_millisec = trigger_remaining_millisec;
      // 基本的にはこの値を返せばよいのだが,これよりも使える時間が短い場合があるので下記で考慮する

    // 1日の残り時間をミリ秒で取得 (安全係数も加味)
    const oneday_remaining_millisec = this.getAllTriggersOnedayRemainingTimeSafeHedgedMillisecAsRealTimeValue();

    // トリガ内残り時間は,正味の値はもっと小さいか?
    if( trigger_remaining_millisec > oneday_remaining_millisec ){

      // 小さいほうの値を採用する
      net_remaining_millisec = oneday_remaining_millisec;

    }

    return net_remaining_millisec;
  },

  // 日内とトリガ内を組み合わせた正味の処理残り時間を,安全係数を加味した分秒形式で返す。
  getNetRemainingTimeSafeHedgedReadable : function(){

    // ミリ秒として取得
    const net_remaining_time_safe_hedged_millisec = this.getNetRemainingTimeSafeHedgedMillisec();

    // 分秒形式に直す
    const net_remaining_time_safe_hedged_readable = TriggerInfoUtil.transformMillisecToReadableString( net_remaining_time_safe_hedged_millisec );

    return net_remaining_time_safe_hedged_readable;

  },


  // ----------- リアルタイム値取得に関するその他の内部処理 ----------


  // 現在のトリガ起動日時を表すDateオブジェクト
  trigger_start_dt : null,

  // 現在のトリガ起動日時を表すDateオブジェクトをキャッシュ値として取得
  getCurrentTriggerStartDateObjAsCashedValue : function(){
    return this.trigger_start_dt;
  },

  // 現在のトリガ起動日時を表すDateオブジェクトをキャッシュ値として保持
  setCurrentTriggerStartDateObjAsCashedValue : function( dt_obj ){
    this.trigger_start_dt = dt_obj;
  },


  // トリガIDのキャッシュ
  trigger_id : null,

  // 現在実行中のトリガIDのキャッシュ値を返す。
  // (オブジェクト内にキャッシュされた値を使うので,シート読み取りは発生しない)
  getCurrentTriggerIdAsCashedValue : function(){
    return this.trigger_id;
  },

  // 現在実行中のトリガIDのキャッシュ値をセットする。
  setCurrentTriggerIdAsCashedValue : function( trigger_id ){
    this.trigger_id = trigger_id;
  },


  // 現トリガ内でのユーザー定義タスクの完了数のキャッシュ
  achieved_user_tasks_count : 0,

  // 現トリガ内でのユーザー定義タスクの完了数のキャッシュ値を返す。
  getCurrentTriggerAchievedUserTasksCountAsCashedValue : function(){
    return this.achieved_user_tasks_count;
  },

  // 現トリガ内でのユーザー定義タスクの完了数のキャッシュ値をセットする。
  setCurrentTriggerAchevedUserTasksCountAsCashedValue : function( i ){
    this.achieved_user_tasks_count = i;
  },

  // 現トリガ内でのユーザー定義タスクの完了数のキャッシュ値を1だけ増やす。
  incrementCurrentTriggerAchevedUserTasksCountAsCashedValue : function(){

    // 愚直な書き方だが,GASでは「this.プロパティ値」という記法を極力避けて
    // getterとsetterを介することが望ましいので,これでよい。

    let acheved_user_tasks_count = this.getCurrentTriggerAchievedUserTasksCountAsCashedValue();
    acheved_user_tasks_count ++;
    this.setCurrentTriggerAchevedUserTasksCountAsCashedValue( acheved_user_tasks_count );

      my_debug_log( "acheved_user_tasks_countは" + acheved_user_tasks_count );

  }


  // ----------- リアルタイム値取得に関する内部処理はここまで。 ----------

  
};
// トリガマネージャの定義終わり




// ------------------------ 以下はトリガ情報を記録するシートに対するドメインロジック -----------------------------




// トリガ情報を記録しておくシート上のドメインロジックを抽象化したオブジェクト。
// ※なぜドメインロジックという名前にしたか。昔はこういう物をビジネスロジックと呼んで,BLと略していた。
//   しかし今現在,そのような略し方は別の良くない意味を持つようになったので,その呼び方をもうしたくない。
//   それで代わりに,対象となるデータモデル内でのデータ処理の中核的な部分という意味で,ドメインロジックと呼んでいる。
let TriggerInfoSheetDomainLogics = {


  // ----------- DAOを呼び出すだけの単純なプロキシメソッド ----------
  // ※こうしておくことで,マネージャクラスからDAOを直接呼ばなくて済む。



  // トリガ情報を記録するためのシートを(必要なら)初期化する。
  setupTriggerInfoSheet : function(){
    
    TriggerInfoSheetDAO.setupTriggerInfoSheet();

  },


  // 最終トリガIDを記録する。
  setLastTriggerId : function( trigger_id ){

    TriggerInfoSheetDAO.setLastTriggerId( trigger_id );

  },


  // 本日の全トリガ内の処理に要した時間をミリ秒数値でシートから取得する。
  // (現在起動中のトリガ内の実行時間は除外する。)
  // シート上の記録値が空欄の場合は0を返す。
  getConsumedTimeMilliSecWhileAllTriggersToday : function(){

    return TriggerInfoSheetDAO.getConsumedTimeMilliSecWhileAllTriggersToday();

  },


  // ----------- DAOを呼び出すだけの単純なプロキシメソッドはここまで。 ----------
  // ----------- 以下は,DAOの呼び出しを組み合わせた処理 ----------


  // ----------- トリガステータス関連 ----------


  // 「新規トリガ起動に成功した」というステータスをシート上に記録する
  setLastTriggerStatusStartSuccess : function(){

    // トリガステータスを記録
    TriggerInfoSheetDAO.setLastTriggerStatus( TRIGGER_STATUS_START_SUCCESS );
  
  },


  // 「トリガ終了に成功した」というステータスをシート上に記録する
  setLastTriggerStatusEndSuccess : function(){

    // トリガステータスを記録
    TriggerInfoSheetDAO.setLastTriggerStatus( TRIGGER_STATUS_END_SUCCESS );
      // NOTE:
      // 逆にもし,シート上にこのステータスが記録されていなかったら
      // 「前回のトリガは時間切れで異常終了したんだな」とわかる。

  },


  // 前回終了時のトリガ・ステータスが正常終了ではないかどうか判定 
  isLastTriggerStatusNotNormalEnd : function(){

    // 最終ステータスを取得して比較
    if( TriggerInfoSheetDAO.getLastTriggerStatus() == TRIGGER_STATUS_END_SUCCESS ){

      my_debug_log( "前回のトリガは正常終了しています。" );
      return false;
    
    }
    else
    {

      my_debug_log( "前回のトリガは異常終了しています。" );
      return true;

    }

  },


  // 前回のトリガが時間切れで異常終了していないかチェックし,
  // もし異常を検出したら正常化しておく。
  checkLastTriggerEndStatus : function(){

    // 正常終了のステータスが残っていなければ,前回(本日)のトリガ実行時間に上限値(6分)を追記する。
    // なぜ「本日」の累計時間の欄にプラスするかというと,この時点ではまだローテーションをしていないから。

    // 前回終了時のトリガ・ステータスが正常終了ではない場合 
    if( TriggerInfoSheetDomainLogics.isLastTriggerStatusNotNormalEnd() ){
      
      // 前回のトリガは時間切れで強制終了されたので,
      // トリガ終了時に呼び出されるはずの onCurrentTriggerFinished による集計処理が
      // 行われていない,と考える。
      // そのぶんの集計処理を今ここで済ませてしまう。
      TriggerInfoSheetDomainLogics.addConsumedTimeWhileAllTriggersTodayOnLastTriggerFailedToFinishNormally();

    }

    return;

  },


  // 前回のトリガが時間切れで強制終了されたため,
  // トリガ終了時に呼び出されるはずのシート上集計更新処理が行われていない,と考え,
  // そのぶんの集計処理を今ここで済ませてしまう。
  addConsumedTimeWhileAllTriggersTodayOnLastTriggerFailedToFinishNormally : function(){

    // 前回はトリガ内の時間をMAXまで使い果たしたとみなす
    const last_trigger_consumed_time_millisec = TriggerInfoSheetDomainLogics.getLimitMillisecOneTrigger();
      // NOTE:
      // ちなみにこのような異常系の対処を行なうと,
      // 前回のトリガが必ずしも6分という枠を使い切った場合ではなくても
      // 何らかの例外をthrowして処理が停まった場合でも「6分消費した」という仮の数え方をすることになる。
      // そのため,プログラムコードがどこか間違っているせいでその部分でエラーが毎回出てたりすると,
      // エラー発生による異常終了のたびに消費時間に6分が加算される。
      // これはけっこうなペナルティとなる。1トリガあたり30秒ぐらいの動作を10分おきとかで小刻みに繰り返して
      // ダイジェストレートがその都度100%に達してメインループを打ち切るような挙動をしている際,
      // 6分のペナルティが発生すると,30秒のトリガ動作が12回ぶん動けたはずの動作枠を手持ち残り時間から差し引くことになり
      // 結果として,その時間帯にトリガ起動可能な回数は大幅に減ることになる。
      // (つまり,「6分ペナルティ」の発生直後はしばらくメインループを動かせない回が続く。)
      // だから,その対策としては自分で実装するコード内で「catchされない例外を生まないように実装する」という対処をすればよい。
      // プログラムエラーによってメインループの動作が部分的に停まる箇所があるとしても,
      // トリガ終了ステータスを「正常終了」に書き換えてトリガ終了すれば「余分の6分消費というペナルティ」を回避できる。

    // 「本日」の累積実行時間にプラスする
    let all_triggers_consumed_time_millisec = TriggerInfoSheetDAO.getConsumedTimeMilliSecWhileAllTriggersToday();
    all_triggers_consumed_time_millisec += last_trigger_consumed_time_millisec;

    // 「本日」の全トリガ内の所要時間(ミリ秒,および分秒形式)をシート上に記録
    TriggerInfoSheetDAO.setConsumedTimeMilliSecWhileAllTriggersToday( all_triggers_consumed_time_millisec );
    TriggerInfoSheetDAO.setConsumedTimeReadableWhileAllTriggersToday( all_triggers_consumed_time_millisec );

    // 「本日」の全トリガの累積処理時間の枠内使用率をシート上に記録
    const consumed_time_ratio = TriggerInfoSheetDomainLogics.setConsumedTimePercentageAllTriggersTodayByMillisec( all_triggers_consumed_time_millisec );

    // 「本日」の最終トリガ終了時点での日内時間経過率を,「MAXまで到達した」とみなしてシート上に記録
    TriggerInfoSheetDAO.setPassedTimePercentageOnLastTriggerFinishedTodayAsString( "100 %" );
    const passed_time_ratio = 1.0

    // 本日の最終トリガ終了時点でのダイジェストレートをシート上に記録
    TriggerInfoSheetDomainLogics.updateDigestRatePercentageOnLastTriggerFinishedToday( consumed_time_ratio, passed_time_ratio );

    // 本日の全トリガ内の平均処理時間を,分秒形式および枠内使用率の形式でシート上に記録
    TriggerInfoSheetDomainLogics.analyzeAverageConsumedTimeAllTriggersToday( all_triggers_consumed_time_millisec );


    // ※仕事数については,トリガ異常終了時は集計対象外とする。(正常終了した場合のみを集計対象とする。)
    //   前回のトリガが異常終了した場合,その分のユーザー定義タスク数をシート上で加算する必要はない。


    return;


    // NOTE: なお,このメソッドの「トリガが正常終了した時用」のバージョンが下記にあるので
    // 2つのメソッドの内容に相互に抜け漏れがないように注意。
    (function(){
      // GASエディタ上でコードを相互参照しやすくしてある。
      MyTriggerManager.addConsumedTimeWhileAllTriggersToday();
    });

  },


  // ----------- 上限値 関連 ----------


  // 1トリガ内の理論上の処理時間の上限値ミリ秒を返す。
  // 安全係数は考慮しないし,1日のトータルの処理時間も考慮しない。
  getLimitMillisecOneTrigger : function(){
    
    // 1トリガ内の処理時間の上限値(ミリ秒)
    const limit_time_one_trigger_millisec = LIMIT_MINUTES_OF_ONE_TRIGGER * 60 * 1000;

    return limit_time_one_trigger_millisec;
  
  },


  // 1日の枠で決められた全トリガ分の合計処理時間の上限値をミリ秒で返す。
  // 安全係数は考慮しない。
  // また,1つのGoogleアカウント内で複数のGASプロジェクトを保有・運営する場合もあるので
  // ここではあくまでも本プロジェクト内に限った場合の値とする。
  getLimitMillisecAllTriggersOneDay : function(){

    // 分をミリ秒に直す
    const limit_millisec = LIMIT_MINUTES_OF_ALL_TRIGGERS_ONE_DAY_THIS_PROJECT * 60 * 1000;

    return limit_millisec;

  },


  // ----------- 前日と本日のローテーション関連 ----------


  // 前日と本日でトリガ情報のローテーションが必要か?
  requiresTriggerInfoRotation : function( new_trigger_start_dt ){

    // 前日と本日の情報をきっちり区別する。
    // 今回のトリガ起動時に,前回と今回でトリガ同士が(トリガ起動時刻が)日をまたいでいたら,
    // すでに記載済みの情報は「前日の分」とみなして記載欄をローテートすべき。

    // シート上にある最終トリガの起動日時を取得    
    const last_trigger_start_unix_ms = TriggerInfoSheetDAO.getLastTriggerStartUnixMs();
    const last_trigger_start_dt = new Date( last_trigger_start_unix_ms );

    // 前回と今回で,年月日が一致しているか?
    if(
      // 年が一致?
      ( new_trigger_start_dt.getFullYear() == last_trigger_start_dt.getFullYear() )
      &&
      // 月が一致?
      ( new_trigger_start_dt.getMonth() == last_trigger_start_dt.getMonth() )
      &&
      // 日が一致?
      ( new_trigger_start_dt.getDate() == last_trigger_start_dt.getDate() )
    ){
        my_debug_log("同一年月日の出来事なので,ローテーションは不要。")
      return false;
    }else{
        my_debug_log("年・月・日のいずれかが異なるので,ローテーションが必要。")
      return true;
    }

  },

  
  // 前日と本日のトリガ情報のローテーションを実施
  rotateTriggerInfoOneDay : function(){
    
    // なお,シート上に記載されている前回のトリガ情報がたとえ1日以上前の物であっても
    // ローテーション先として「前日の分」という欄を使う。
    // (2日以上前の情報でも,ローテーション先としては「前日」とみなす。)

    // 「本日」の情報を確保
    const all_triggers_count_oneday          = TriggerInfoSheetDAO.getAllTriggersCountToday();
    const total_consumed_time_millisec       = TriggerInfoSheetDAO.getConsumedTimeMilliSecWhileAllTriggersToday();
    const total_consumed_time_readable       = TriggerInfoSheetDAO.getConsumedTimeReadableWhileAllTriggersToday();
    const total_consumed_time_percentage     = TriggerInfoSheetDAO.getConsumedTimePerLimitPercentageAllTriggersToday();
    const total_passed_time_percentage       = TriggerInfoSheetDAO.getPassedTimePercentageOnLastTriggerFinishedToday();
    const digest_rate_percentage             = TriggerInfoSheetDAO.getDigestRatePercentageOnLastTriggerFinishedToday();
    const average_consumed_time_readable     = TriggerInfoSheetDAO.getAverageConsumedTimeReadableAllTriggersToday();
    const average_consumed_time_percentage   = TriggerInfoSheetDAO.getAverageConsumedTimePercentageAllTriggersToday();
    const achieved_user_tasks                = TriggerInfoSheetDAO.getAchievedUserTasksToday();
    const average_user_tasks_per_one_trigger = TriggerInfoSheetDAO.getAverageUserTasksPerOneTriggerToday();
    const average_user_task_time             = TriggerInfoSheetDAO.getAverageUserTaskTimeToday();

    // 「前日」の情報として記録し直す
    TriggerInfoSheetDAO.setAllTriggersCountPrevDay( all_triggers_count_oneday );
    TriggerInfoSheetDAO.setConsumedTimeMilliSecWhileAllTriggersPrevDay( total_consumed_time_millisec );
    TriggerInfoSheetDAO.setConsumedTimeReadableWhileAllTriggersPrevDay( total_consumed_time_readable );
    TriggerInfoSheetDAO.setConsumedTimePerLimitPercentagePrevDay( total_consumed_time_percentage );
    TriggerInfoSheetDAO.setPassedTimePercentageOnLastTriggerFinishedPrevday( total_passed_time_percentage );
    TriggerInfoSheetDAO.setDigestRatePercentageOnLastTriggerFinishedPrevday( digest_rate_percentage );
    TriggerInfoSheetDAO.setAverageConsumedTimeReadableAllTriggersPrevDay( average_consumed_time_readable );
    TriggerInfoSheetDAO.setAverageConsumedTimePercentageAllTriggersPrevDay( average_consumed_time_percentage );
    TriggerInfoSheetDAO.setAchievedUserTasksPrevday( achieved_user_tasks );
    TriggerInfoSheetDAO.setAverageUserTasksPerOneTriggerPrevday( average_user_tasks_per_one_trigger );
    TriggerInfoSheetDAO.setAverageUserTaskTimePrevday( average_user_task_time );

    // 「本日」の欄をクリアする
    TriggerInfoSheetDAO.setAllTriggersCountToday( 0 );
    TriggerInfoSheetDAO.setConsumedTimeMilliSecWhileAllTriggersToday( 0 );
    TriggerInfoSheetDAO.setConsumedTimeReadableWhileAllTriggersToday( 0 ); // "0 分 0 秒" になる
    TriggerInfoSheetDomainLogics.setConsumedTimePercentageAllTriggersTodayByMillisec( 0 ); // "0 %" になる
    TriggerInfoSheetDAO.setPassedTimePercentageOnLastTriggerFinishedTodayAsString( "" ); // 空欄になる
    TriggerInfoSheetDAO.setDigestRatePercentageOnLastTriggerFinishedToday( "" ); // 空欄になる
    TriggerInfoSheetDAO.setAverageConsumedTimeReadableAllTriggersToday( "" ); // 空欄になる
    TriggerInfoSheetDAO.setAverageConsumedTimePercentageAllTriggersToday( "" ); // 空欄になる
    TriggerInfoSheetDAO.setAchievedUserTasksToday( 0 );
    TriggerInfoSheetDAO.setAverageUserTasksPerOneTriggerToday( "" ); // 空欄になる
    TriggerInfoSheetDAO.setAverageUserTaskTimeToday( "" ); // 空欄になる

    return;

  },


  // ----------- 今回起動中のトリガについて読み書き ----------


  // 今回起動中のトリガ内の処理に要した時間をシートに記録する。
  // (ミリ秒形式での記録と,分秒形式での記録をセットで実施)
  // ミリ秒値を返却する。
  setConsumedTimeWhileCurrentTrigger : function( trigger_end_dt ){
    
    // 開始日時を取得
    const trigger_start_dt = TriggerInfoSheetDomainLogics.getCurrentTriggerStartDateObj();

    // 開始から終了までミリ秒で差を取って記録
    const consumed_time_millisec = trigger_end_dt.getTime() - trigger_start_dt.getTime();
    TriggerInfoSheetDAO.setConsumedTimeMilliSecWhileCurrentTrigger( consumed_time_millisec );

    // ミリ秒を分秒形式に変換して記録
    const consumed_time_readable = TriggerInfoUtil.transformMillisecToReadableString( consumed_time_millisec );
    TriggerInfoSheetDAO.setConsumedTimeReadableWhileCurrentTrigger( consumed_time_readable );

    // ミリ秒値を返す
    return consumed_time_millisec;

  },


  // 今回のトリガ内で果たしたユーザ定義タスクについてシート上に記録
  updateAchievedUserTasksWhileCurrentTrigger : function( consumed_time_millisec, achieved_user_tasks ){

    // 仕事完了数をシート上に記録
    TriggerInfoSheetDAO.setAchievedUserTasksWhileCurrentTrigger( achieved_user_tasks );


    // 1仕事あたり平均時間を求める
    let average_task_time_millisec = -1;
    if( achieved_user_tasks > 0 ){

      // 仕事の個数が有効な正の整数なら,割り算が成立する
      average_task_time_millisec = consumed_time_millisec / achieved_user_tasks;

    }
    else
    {
      // 割り算が成立しない場合は,平均時間を0とする
      average_task_time_millisec = 0;
    }

    // 秒数形式(DetailSecond形式)に直し,シート上に記録する
    const average_task_time_seconds = TriggerInfoUtil.transformMillisecToDetailedSeconds( average_task_time_millisec );
    TriggerInfoSheetDAO.setAverageUserTaskTimeWhileCurrentTrigger( average_task_time_seconds );
    
    return;

  },


  // 今回起動中のトリガの起動時刻をDateオブジェクトとして取得
  // (シート上のミリ秒情報をもとに構築)
  getCurrentTriggerStartDateObj : function(){

    // UNIXミリ秒を取得    
    const trigger_start_unix_ms = TriggerInfoSheetDAO.getLastTriggerStartUnixMs();

    // UNIXミリ秒をDateに変換
    const trigger_start_dt = new Date( trigger_start_unix_ms );
    
    return trigger_start_dt;

  },


  // 今回のトリガ内の処理に要した時間をシート上でいったんクリア
  clearConsumedTimeWhileCurrentTrigger : function(){

    // 累積時間,ミリ秒形式
    TriggerInfoSheetDAO.setConsumedTimeMilliSecWhileCurrentTrigger( "" ); // 0を渡すのではなく,空欄とする。

    // 累積時間,分秒形式
    TriggerInfoSheetDAO.setConsumedTimeReadableWhileCurrentTrigger( "" );

    // トリガ内仕事数
    TriggerInfoSheetDAO.setAchievedUserTasksWhileCurrentTrigger( "" ); // 0を渡すのではなく,空欄とする。

    // トリガ内の1仕事あたり平均時間
    TriggerInfoSheetDAO.setAverageUserTaskTimeWhileCurrentTrigger( "" );

  },


  // 今回起動中のトリガの起動時刻を記録する。
  // (ミリ秒形式での起動時刻と,分秒形式での起動時刻をセットで実施)
  // 必要なら前日と本日の情報のローテーションも行なう。
  setCurrentTriggerStartTime : function( trigger_start_dt ){

    // 前日と本日でトリガ情報のローテーションが必要か?
    if( TriggerInfoSheetDomainLogics.requiresTriggerInfoRotation( trigger_start_dt ) ){

      // トリガ情報のローテーションを実施
      TriggerInfoSheetDomainLogics.rotateTriggerInfoOneDay();

    }

    // シート上への記録の準備が整ったので,新しく起動したトリガの情報をシート上に記録する。
    // トリガ起動時刻,ミリ秒形式で
    TriggerInfoSheetDAO.setTriggerStartUnixMs( trigger_start_dt );
    // トリガ起動時刻,分秒形式で
    TriggerInfoSheetDAO.setTriggerStartFormattedString( trigger_start_dt );

    return;

  },


  // 前回のトリガ起動時刻から見て,新規トリガ起動間隔が短かすぎるかどうかを判定する。
  isNewTriggerStartedTooShortSinceLastTrigger : function(){

    // トリガ情報を記録するためのシートを(必要なら)初期化
    TriggerInfoSheetDAO.setupTriggerInfoSheet();


    // 現在日時(※これを新しいトリガの起動日時とみなす)のDateオブジェクト
    const new_trigger_dt = new Date();

    // 前回のトリガ起動日時をUNIXミリ秒としてシートから取得し,Dateオブジェクトにする
    const old_trigger_unix_ms = TriggerInfoSheetDAO.getLastTriggerStartUnixMs();
    const old_trigger_dt = new Date( old_trigger_unix_ms );


    // 2つのDateオブジェクトが,トリガ起動間隔の観点から見て
    // 異なる「分ゾーン」に属するかどうかを判定
    const two_triggers_too_close_flag = TriggerInfoUtil.judgeTwoTriggersTooClose( old_trigger_dt, new_trigger_dt );
      my_debug_log( "isNewTriggerStartedTooShortSinceLastTriggerの返却値: " + two_triggers_too_close_flag );

    return two_triggers_too_close_flag;

  },


  // ----------- 本日分の累積情報や平均情報の読み書き ----------


  // 本日の全トリガ内の所要時間(ミリ秒,および分秒形式)をシート上に記録
  updateConsumedTimesWhileAllTriggersToday : function( all_triggers_consumed_time_millisec ){

    // ミリ秒形式で
    TriggerInfoSheetDAO.setConsumedTimeMilliSecWhileAllTriggersToday( all_triggers_consumed_time_millisec );

    // 分秒形式で
    TriggerInfoSheetDAO.setConsumedTimeReadableWhileAllTriggersToday( all_triggers_consumed_time_millisec );
  
  },


  // 本日の全トリガ内の平均処理時間を,分秒形式および枠内使用率の形式でシート上に記録する。
  // 返却値は,本日の全トリガ起動回数。
  analyzeAverageConsumedTimeAllTriggersToday : function( all_triggers_consumed_time_millisec ){

    // まず,分秒形式を求める。

    // 本日何回起動したか?
    const all_triggers_count_oneday = TriggerInfoSheetDAO.getAllTriggersCountToday();
      // なおトリガ起動時に起動回数が+1されているので,0割りは生じない。(処理の進行中にシート上の値を手動で書き換えない限りは…。) 

    // 1トリガあたり何ミリ秒か?
    let average_consumed_time_millisec = 0;
    if( all_triggers_count_oneday > 0 ){ // 0割りエラーを防止する。

      // ミリ秒を回数で割る。
      average_consumed_time_millisec = all_triggers_consumed_time_millisec / all_triggers_count_oneday;

    }else{

      // 回数が0なら,平均時間も0とする。
      average_consumed_time_millisec = 0;

    }

    // 1トリガあたりの平均処理時間を分秒形式に変換し,シート上に記録
    const average_consumed_time_readable = TriggerInfoUtil.transformMillisecToReadableString( average_consumed_time_millisec );
    TriggerInfoSheetDAO.setAverageConsumedTimeReadableAllTriggersToday( average_consumed_time_readable );


    // 次に,%形式を求める。

    // 1トリガ内の処理時間の上限値(ミリ秒)
    const limit_time_one_trigger_millisec = TriggerInfoSheetDomainLogics.getLimitMillisecOneTrigger();

    // %形式とし,シート上に記録 (1トリガあたりの平均処理時間の枠内使用率)
    const average_consumed_time_percentage = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace(
      average_consumed_time_millisec / limit_time_one_trigger_millisec
    );
    TriggerInfoSheetDAO.setAverageConsumedTimePercentageAllTriggersToday( average_consumed_time_percentage );


    return all_triggers_count_oneday;
  },


  // 本日の全トリガの累積処理時間の枠内使用率を,ミリ秒値をもとに計算して記録。
  // 返却値は,該当値の比としての数値。
  setConsumedTimePercentageAllTriggersTodayByMillisec : function( consumed_millisec ){

    // 1日の枠で決められた上限値ミリ秒
    const limit_millisec = this.getLimitMillisecAllTriggersOneDay();

    // 上限に対して比を取る
    const consumed_time_ratio = consumed_millisec / limit_millisec;

    // パーセント形式の文字列にする
    const consumed_time_percentage = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace( consumed_time_ratio )

    // シート上に記録
    TriggerInfoSheetDAO.setConsumedTimePercentageAllTriggersToday( consumed_time_percentage );

    return consumed_time_ratio;

  },


  // 本日の全トリガ内で果たしたユーザ定義タスクについて,シート上の記録を更新する。
  updateAchievedUserTasksWhileAllTriggersToday : function( arg_obj ){
    
    // 引数を解釈
    const all_triggers_consumed_time_millisec = arg_obj.all_triggers_consumed_time_millisec; 
    const all_triggers_count_oneday           = arg_obj.all_triggers_count_oneday;
    const achieved_user_tasks_one_trigger     = arg_obj.achieved_user_tasks_one_trigger;


    // 本日の仕事完了数を更新
    const achieved_user_tasks_today = this.addAchievedUserTasksToday( achieved_user_tasks_one_trigger );

    // 本日の1トリガあたりの平均仕事数を更新
    this.updateAverageUserTasksCountPerTriggerToday( achieved_user_tasks_today, all_triggers_count_oneday );

    // 本日の1仕事あたり平均時間を求めて記録
    this.updateAverageUserTaskTimeWhileAllTriggersToday( achieved_user_tasks_today, all_triggers_consumed_time_millisec );


    return;

  },


  // 本日の仕事完了数について,1回分のトリガで果たした仕事数を加算してシート上で更新する。
  addAchievedUserTasksToday : function( achieved_user_tasks_one_trigger ){

    // 既存の値をシートから読み取る
    let achieved_user_tasks_today = TriggerInfoSheetDAO.getAchievedUserTasksToday();

    // 0以上の有効な整数か?
    if( achieved_user_tasks_today > 0 ){

      // 1トリガぶんを加算
      achieved_user_tasks_today += achieved_user_tasks_one_trigger;

    }
    else
    {

      // 有効な整数が記載されていなかった場合は,「0が記載されていた」とみなす。
      achieved_user_tasks_today = 0 + achieved_user_tasks_one_trigger;

    }

    // シートに記録し直す
    TriggerInfoSheetDAO.setAchievedUserTasksToday( achieved_user_tasks_today );


    // 新しい累積値を返却
    return achieved_user_tasks_today;

  },


  // 本日の全トリガについて,1仕事あたりの平均時間を求めて記録する
  updateAverageUserTaskTimeWhileAllTriggersToday : function( achieved_user_tasks_today, all_triggers_consumed_time_millisec ){

    // 本日の全経過時間を,本日の全ての仕事完了数で割る
    let average_task_time_millisec = -1;
    if( achieved_user_tasks_today > 0 ){
      
      // 有効な正の整数で割れる場合は,割り算を実行
      average_task_time_millisec = all_triggers_consumed_time_millisec / achieved_user_tasks_today;

    }
    else
    {
      // 有効な整数で割れない場合は,平均時間は0とする
      average_task_time_millisec = 0;
    }


    // 秒数形式(DetailSecond形式)に直し,シート上に記録する
    const average_task_time_seconds = TriggerInfoUtil.transformMillisecToDetailedSeconds( average_task_time_millisec );
    TriggerInfoSheetDAO.setAverageUserTaskTimeToday( average_task_time_seconds );

    return;

  },


  // 本日の1トリガあたりの平均仕事数をシート上で更新
  updateAverageUserTasksCountPerTriggerToday : function( achieved_user_tasks_today, all_triggers_count_oneday ){

    // 本日の全仕事数を,本日の全トリガ数で割る
    let average_tasks_count = -1;
    if( all_triggers_count_oneday > 0 ){
      
      // 有効な正の整数で割れる場合は,割り算を実行
      average_tasks_count = achieved_user_tasks_today / all_triggers_count_oneday;

    }
    else
    {
      // 有効な整数で割れない場合は,平均タスク数は0とする
      average_tasks_count = 0;
    }

    // 小数第1位までで値を打ち切り,シート上に記録
    const average_tasks_count_for_sheet = Math.floor( average_tasks_count * 10 ) / 10;
    TriggerInfoSheetDAO.setAverageUserTasksPerOneTriggerToday( average_tasks_count_for_sheet );

    return;

  },


  // ----------- 本日の最終トリガ終了時点での日内時間経過率やダイジェストレートについて ----------


  // 本日の最終トリガ終了時の日内時間経過率を,Dateオブジェクトから算出してシートに記録。
  // なお,トリガ内の処理が日をまたいだ場合は
  // 「最終トリガ終了時刻における日内時間経過率」は100%を超える。(24時をもって100%になるから)
  // 返却値は,該当値の比としての数値。
  setPassedTimePercentageOnLastTriggerFinishedTodayAsDateObj : function( trigger_end_dt, trigger_start_dt ){

    my_debug_log("setPassedTimePercentageOnLastTriggerFinishedTodayAsDateObjが呼ばれました。")  


    // トリガ終了時刻のDateオブジェクト① は,
    // トリガ開始時刻のDateオブジェクト② の日付開始時点から数えて
    // 何ミリ秒が経過しているか?
    // (トリガ内の処理が日をまたいだ場合,①と②の日付は異なりうるのでそれも加味して整合性が取れるようにする)
    const passed_time_ms = TriggerInfoUtil.transformDateObjToPassedTimeMillisec( trigger_end_dt, trigger_start_dt );

    // 上限に対して比にする
    const passed_time_ratio = passed_time_ms / HOW_MANY_MS_IN_ONE_DAY;

    // 1日の中で占める割合を%にして,シートに記録
    const passed_time_percentage = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace( passed_time_ratio );
    TriggerInfoSheetDAO.setPassedTimePercentageOnLastTriggerFinishedTodayAsString( passed_time_percentage );

    return passed_time_ratio;

  },


  // 本日のダイジェストレートを計算し,%形式の文字列として記録
  updateDigestRatePercentageOnLastTriggerFinishedToday : function( consumed_time_ratio, passed_time_ratio ){

    my_debug_log("updateDigestRatePercentageOnLastTriggerFinishedTodayが呼ばれました。");


    // 2つの%の間で比を取る
    let digest_rate_ratio = -1;

    // 分母が0より大きな有効な値の場合
    if( passed_time_ratio > 0 ){
    
      // 分母が0ではないので,割り算ができる場合
      digest_rate_ratio = consumed_time_ratio / passed_time_ratio;
    
    }   
    else
    {
      // 有効な割り算ができない場合は,比を0とする
      digest_rate_ratio = 0;
    }

    // %形式にし,シート上に記録
    const digest_rate_percentage = TriggerInfoUtil.transformRatioToPercentageStringToFirstDecimalPlace( digest_rate_ratio );
    TriggerInfoSheetDAO.setDigestRatePercentageOnLastTriggerFinishedToday( digest_rate_percentage );

  },


  // ----------- トリガIDおよびトリガ起動回数について ----------


  // 最終トリガIDをシートから調べて+1し,有効な1以上の整数値として新規トリガIDを取得する。
  // トリガIDがシステム上で扱える数値として大きすぎる場合,新規IDとして1を返す。
  getNewTriggerIdValidInt : function(){
    
    // 新規トリガIDを作りたい
    let new_trigger_id = 0;

    // 最終トリガIDを文字列として取得
    const old_trigger_id_str = TriggerInfoSheetDAO.getLastTriggerIdString();

    // 整数値として解釈が可能か
    const old_trigger_id_num = parseInt( old_trigger_id_str, 10 );
      my_debug_log( "前回のトリガID(整数として解釈):" + old_trigger_id_num );

    // 前回の有効なIDが記録されているか?
    if( old_trigger_id_num > 0 ){ 

      // 前回の起動回数に+1する
      new_trigger_id = old_trigger_id_num + 1;

      // トリガIDとして許容できる上限値を超えてしまっているか?
      if( new_trigger_id > MAX_TRIGGER_ID_AS_INTEGER ){

        // 1に巻き戻す
        new_trigger_id = 1;

      }

    }else{ // シート上に有効なID値が記録されていなかった場合

      // 今回を初回起動とみなす
      new_trigger_id = 1;

    }
      my_debug_log( "getNewTriggerIdValidIntの返り値:" + new_trigger_id );
    
    return new_trigger_id;

  },


  // 本日のトリガ起動回数に+1して記録する
  incrementAllTriggersCountToday : function(){

    // シート上から取得
    let all_triggers_count_oneday = parseInt( TriggerInfoSheetDAO.getAllTriggersCountToday(), 10 );

    // 有効な数値として解釈可能できなかったら
    if( !( all_triggers_count_oneday > -1 ) ){
      // 0に初期化する
      all_triggers_count_oneday = 0;
        my_debug_log( "all_triggers_count_onedayを有効な数値として認識できなかったため,0に初期化しました。" );
    }

    // 1足して記録しなおす
    TriggerInfoSheetDAO.setAllTriggersCountToday( all_triggers_count_oneday + 1 );

  }
  

};




// ------------------------ 以下はトリガ情報を記録するシート(を抽象化したもの) -----------------------------




// トリガ情報を記録しておくシート(への読み書き)を抽象化したオブジェクト。
//
// ※ MyTriggerManager から,このオブジェクトを直接呼び出してはいけない。
//    MyTriggerManager から呼び出したい場合は,必ず TriggerInfoSheetDomainLogics を経由すること。
//    コードのレイヤが高い← MyTriggerManager > TriggerInfoSheetDomainLogics > TriggerInfoSheetDAO →コードのレイヤが低い
// ※ DAOからDomainLogicsを呼び出してもいけない。逆に,DomainLogicsからDAOを呼び出すべき。
let TriggerInfoSheetDAO = {

  
  // ----------- シートそのものに関わる情報 ----------


  // トリガ情報等を記録するためのシート
  _info_sheet : null,


  // 記録対象のシートを返す
  getInfoSheet : function(){
    return this._info_sheet;
  },


  // 記録対象のシートをセットする
  setInfoSheet : function( sheet_obj ){
    this._info_sheet = sheet_obj;
  },


  // トリガ情報を記録するためのシートを初期化
  setupTriggerInfoSheet : function(){

    // すでに設定済みなら何もしない
    // (オブジェクト内にプロパティとしてセットされていればよい)
    if( this.getInfoSheet() ) return;


    // オブジェクト内にまだプロパティとして保管していない場合,
    // ブック内から取得する。

    // シート名でシート取得
    let info_sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .getSheetByName( SHEET_NAME_TRIGGER_INFO )
    ;

    // ブック内にシートが無い場合は自動生成する
    if( ! info_sheet ){

      // ※シートの存在判定について
      // https://auto-worker.com/blog/?p=4934
      // getSheetByNameメソッドで存在しないシート名を読み込んだ場合,
      // エラー等は起きず,変数はnullとなる。
      // nullになっていればシートが存在しない,それ以外はシートが存在すると判定できる。

      // ラベルなどが整ったシートを生み出して取得
      info_sheet = this.createNewTriggerInfoSheet();

      // オブジェクト内にプロパティ値としてシートをキャッシュしておく
      this.setInfoSheet( info_sheet );
        // ※シート上に初期値をセットするためには,まず先に setInfoSheet() が済んでいる必要があるため。

      // シート上に最低限の初期値をセットしておく
      this.setLastTriggerStatus( TRIGGER_STATUS_END_SUCCESS );
        // ※これがセットされていないと,前回のトリガが異常終了したという誤検知が作動してしまうため。
  
    }
    else
    {

      // オブジェクト内にプロパティ値としてシートをキャッシュしておく
      this.setInfoSheet( info_sheet );

    }


    return;
  },


  // ブック内にシートを新規作成する。
  createNewTriggerInfoSheet : function(){

    // 空白のシートを生み出す
    const info_sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .insertSheet(
        SHEET_NAME_TRIGGER_INFO, 
        0 // とりあえず先頭に配置 
      )
      // 第二引数は0始まりで,左から数えたシートの一番号を指す。
      // https://caymezon.com/gas-sheet-ins-cp-mv-del/#toc7
    ;
    // シートの並び順を調整
    TriggerInfoUtil.adjustSystemSheetOrders();


    // シート内にラベルを作成する

    // 「トリガ制御用シートを手動で編集しないように」という注意書き
    info_sheet.getRange(
      ROW_NUM_TRIGGER_INFO_ATTENTION_FOR_MANUAL_EDIT, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_TRIGGER_INFO_ATTENTION_FOR_MANUAL_EDIT );

    // 1行あく

    // 最終トリガ起動日時(ms)
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_START_MS, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_START_MS );

    // 最終トリガ起動時刻(フォーマット済み文字列)
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_START_FORMATTED_TIME, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_START_FORMATTED_TIME );

    // 最終トリガID
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_ID, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_ID );

    // 最終トリガ・ステータス
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_STATUS, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_STATUS );

    // 最終トリガ・処理内の消費時間(ミリ秒)
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_CONSUMED_TIME_MILLISEC );

    // 最終トリガ・処理内の消費時間(分秒形式)
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_CONSUMED_TIME_READABLE, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_CONSUMED_TIME_READABLE );


    // 最終トリガ内・仕事完了数
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_ACHIEVED_USER_TASKS, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_ACHIEVED_USER_TASKS );

    // 最終トリガ内・1仕事あたり平均時間
    info_sheet.getRange(
      ROW_NUM_LAST_TRIGGER_AVERAGE_USER_TASK_TIME, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_LAST_TRIGGER_AVERAGE_USER_TASK_TIME );

    // 1行あく

    // 本日のトリガ起動回数の合計値
    info_sheet.getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_COUNT, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ALL_TRIGGERS_TODAY_COUNT );

    // 本日の全トリガ内の処理に要した時間(ミリ秒)
    info_sheet.getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ALL_TRIGGERS_TODAY_CONSUMED_TIME_MILLISEC );

    // 本日の全トリガ内の処理に要した時間(分秒形式)
    info_sheet.getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_READABLE, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ALL_TRIGGERS_TODAY_CONSUMED_TIME_READABLE );

    // 本日の全トリガ内の処理に要した時間の枠内使用率
    info_sheet.getRange(
      ROW_NUM_CONSUMED_TIME_PERCENTAGE_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_CONSUMED_TIME_PERCENTAGE_TODAY );

    // 本日の最終トリガ終了時の日内時間経過率
    info_sheet.getRange(
      ROW_NUM_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_TODAY );

    // 日内全処理時間の使用率を日内時間経過率で割った値 (本日)
    info_sheet.getRange(
      ROW_NUM_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_TODAY );

    // 本日の全トリガ内の1回あたり平均処理時間
    info_sheet.getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_READABLE_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_CONSUMED_TIME_READABLE_TODAY );

    // 本日の全トリガ内の1回あたり平均処理時間の枠内使用率
    info_sheet.getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_PERCENTAGE_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_CONSUMED_TIME_PERCENTAGE_TODAY );

    // 本日の仕事完了数
    info_sheet.getRange(
      ROW_NUM_ACHIEVED_USER_TASKS_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ACHIEVED_USER_TASKS_TODAY );

    // 本日の1トリガあたり平均仕事数
    info_sheet.getRange(
      ROW_NUM_AVERAGE_USER_TASK_PER_ONE_TRIGGER_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_USER_TASK_PER_ONE_TRIGGER_TODAY );

    // 本日の1仕事あたり平均時間
    info_sheet.getRange(
      ROW_NUM_AVERAGE_USER_TASK_TIME_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_USER_TASK_TIME_TODAY );

    // 1行あく

    // 前日のトリガ起動回数の合計値
    info_sheet.getRange(
      ROW_NUM_ALL_TRIGGERS_PREVDAY_COUNT, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ALL_TRIGGERS_PREVDAY_COUNT );

    // 前日の全トリガ内の処理に要した時間(ミリ秒)
    info_sheet.getRange(
      ROW_NUM_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_MILLISEC );

    // 前日の全トリガ内の処理に要した時間(分秒形式)
    info_sheet.getRange(
      ROW_NUM_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_READABLE, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_READABLE );

    // 前日の全トリガ内の処理に要した時間の枠内使用率
    info_sheet.getRange(
      ROW_NUM_CONSUMED_TIME_PERCENTAGE_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_CONSUMED_TIME_PERCENTAGE_PREVDAY );

    // 前日の最終トリガ終了時の日内時間経過率
    info_sheet.getRange(
      ROW_NUM_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_PREVDAY );

    // 日内全処理時間の使用率を日内時間経過率で割った値 (前日)
    info_sheet.getRange(
      ROW_NUM_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_PREVDAY );

    // 前日の全トリガ内の1回あたり平均処理時間
    info_sheet.getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_READABLE_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_CONSUMED_TIME_READABLE_PREVDAY );

    // 前日の全トリガ内の1回あたり平均処理時間の枠内使用率
    info_sheet.getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_PERCENTAGE_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_CONSUMED_TIME_PERCENTAGE_PREVDAY );

    // 前日の仕事完了数
    info_sheet.getRange(
      ROW_NUM_ACHIEVED_USER_TASKS_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_ACHIEVED_USER_TASKS_PREVDAY );

    // 前日の1トリガあたり平均仕事数
    info_sheet.getRange(
      ROW_NUM_AVERAGE_USER_TASK_PER_ONE_TRIGGER_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_USER_TASK_PER_ONE_TRIGGER_PREVDAY );

    // 前日の1仕事あたり平均時間
    info_sheet.getRange(
      ROW_NUM_AVERAGE_USER_TASK_TIME_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_LABELS
    ).setValue( LABEL_AVERAGE_USER_TASK_TIME_PREVDAY );


    // 列の幅を調節

    // ※列幅変更
    // https://auto-worker.com/blog/?p=4980

    // ラベル用の列
    info_sheet.setColumnWidth( COL_NUM_TRIGGER_PROPERTY_LABELS, 290 );

    // バリュー用の列
    info_sheet.setColumnWidth( COL_NUM_TRIGGER_PROPERTY_VAUES, 180 );


    // セル値の寄せ方を調節 (左寄せ)

    // ラベル用の列
    info_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_PROPERTY_LABELS,
      // 最後の行
      info_sheet.getMaxRows(), 1
    ).setHorizontalAlignment("left");

    // バリュー用の列
    info_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_PROPERTY_VAUES,
      // 最後の行
      info_sheet.getMaxRows(), 1
    ).setHorizontalAlignment("left");


    // できたてホヤホヤのシートを返却する
    return info_sheet;
  },


  // ----------- シート上の各種記載値に関わる情報 ----------


  // ----------- 最終起動トリガ,ないし今回起動中のトリガについて ----------


  // 最終トリガ起動日時をUNIXミリ秒として取得
  getLastTriggerStartUnixMs : function(){

    // UNIXミリ秒を取得    
    const last_trigger_start_unix_ms = this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_START_MS, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "最終トリガ起動日時(ms)を取得。" + last_trigger_start_unix_ms );
    
    return last_trigger_start_unix_ms;

  },

  // 最終トリガ起動時刻(UNIXミリ秒形式)を記録
  setTriggerStartUnixMs : function( trigger_start_dt ){
    
    // DateをUNIXミリ秒に変換
    const trigger_start_unix_ms = trigger_start_dt.getTime();
    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_START_MS, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( trigger_start_unix_ms );
      my_debug_log( "最終トリガ起動日時(ms)を記録。" + trigger_start_unix_ms );

  },


  // 最終トリガ起動時刻(フォーマット済み文字列)を記録
  setTriggerStartFormattedString : function( trigger_start_dt ){
    
    // Dateを文字列フォーマット
    const trigger_start_formatted_str = Utilities.formatDate(
      trigger_start_dt, 
      "JST", 
      "yyyy-MM-dd (E) HH:mm:ss"
    );
    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_START_FORMATTED_TIME, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( trigger_start_formatted_str );
      my_debug_log( "最終トリガ起動日時(文字列)を記録。" + trigger_start_formatted_str );

  },


  // 最終トリガIDを文字列として取得する
  getLastTriggerIdString : function(){

    const old_trigger_id_str = this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_ID, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "前回のトリガID(文字列):" + old_trigger_id_str );

    return old_trigger_id_str;

  },

  // 最終トリガIDを記録する
  setLastTriggerId : function( trigger_id ){

    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_ID, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( trigger_id );
      my_debug_log( "新規トリガIDを記録。" + trigger_id );
  
  },


  // 最終トリガステータスを文字列として取得する
  getLastTriggerStatus : function(){

    const last_trigger_status = this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_STATUS, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "最終トリガ・ステータスを取得。" + last_trigger_status );

    return last_trigger_status;
  },

  // 最終トリガステータスを記録する
  setLastTriggerStatus : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_STATUS, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "最終トリガ・ステータスを記録。" + s );

  },


  // 今回起動中のトリガ内の処理に要した時間(ミリ秒)を記録する
  setConsumedTimeMilliSecWhileCurrentTrigger : function( consumed_time_millisec ){

    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( consumed_time_millisec );
      my_debug_log( "最終トリガの消費時間(ミリ秒)を記録。" + consumed_time_millisec );

  },


  // 今回起動中のトリガ内の処理に要した時間(分秒形式)を記録する
  setConsumedTimeReadableWhileCurrentTrigger : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_CONSUMED_TIME_READABLE, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "最終トリガの消費時間(分秒形式)を記録。" + s );

  },


  // 最終トリガ内の処理に要した時間をミリ秒数値で取得する
  getConsumedTimeMilliSecWhileCurrentTrigger : function(){
    
    const consumed_time_millisec = this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "最終トリガの消費時間(ミリ秒)を取得:" + consumed_time_millisec );

    return consumed_time_millisec;

  },


  // 最終トリガ内の仕事完了数を取得
  getAchievedUserTasksWhileCurrentTrigger : function(){
    
    const achieved_user_tasks = this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_ACHIEVED_USER_TASKS, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "最終トリガの仕事完了数を取得:" + achieved_user_tasks );

    return achieved_user_tasks;

  },

  // 最終トリガ内の仕事完了数を記録
  setAchievedUserTasksWhileCurrentTrigger : function( achieved_user_tasks ){
    
    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_ACHIEVED_USER_TASKS, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( achieved_user_tasks );
      my_debug_log( "最終トリガの仕事完了数を記録:" + achieved_user_tasks );

  },


  // 最終トリガ内の1仕事あたり平均時間を取得
  getAverageUserTaskTimeWhileCurrentTrigger : function(){
    
    const average_task_time = this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_AVERAGE_USER_TASK_TIME, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "最終トリガ内の1仕事あたり平均時間を取得:" + average_task_time );

    return average_task_time;

  },
  
  // 最終トリガ内の1仕事あたり平均時間を文字列として記録
  setAverageUserTaskTimeWhileCurrentTrigger : function( s ){
    
    this.getInfoSheet().getRange(
      ROW_NUM_LAST_TRIGGER_AVERAGE_USER_TASK_TIME, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "最終トリガ内の1仕事あたり平均時間を記録:" + s );

  },
  

  // ----------- 本日起動した全トリガの累計情報について ----------


  // 本日トリガ起動に成功した回数を取得
  getAllTriggersCountToday : function(){

    const all_triggers_count_oneday = this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_COUNT, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日トリガ起動に成功した回数を取得:" + all_triggers_count_oneday );

    return all_triggers_count_oneday;

  },

  // 本日トリガ起動に成功した回数を記録
  setAllTriggersCountToday : function( all_triggers_count_oneday ){

    this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_COUNT, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( all_triggers_count_oneday );
      my_debug_log( "本日トリガ起動に成功した回数を記録:" + all_triggers_count_oneday );

  },


  // 本日の全トリガ内の処理に要した時間をミリ秒数値でシートから取得する。
  // (現在起動中のトリガ内の実行時間は除外する。)
  // シート上の記録値が空欄の場合は0を返す。
  getConsumedTimeMilliSecWhileAllTriggersToday : function(){

    // シート上から取得
    let consumed_time_millisec = this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).getValue();
      my_debug_log( "本日の全トリガが要した処理時間(ミリ秒)を取得:" + consumed_time_millisec );
    
    // 空なら0に初期化
    if(
      ( ! consumed_time_millisec )
      ||
      ( consumed_time_millisec.length < 1 )
    ){
      consumed_time_millisec = 0;
        my_debug_log( "consumed_time_millisecの欄が空であったため,0と認識しました。" );
    }

    return consumed_time_millisec;

  },

  // 本日の全トリガ内の所要時間(ミリ秒)を記録
  setConsumedTimeMilliSecWhileAllTriggersToday : function( consumed_time_millisec ){

    this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).setValue( consumed_time_millisec );
      my_debug_log( "本日の全トリガが要した処理時間(ミリ秒)を記録:" + consumed_time_millisec );

  },


  // 本日の全トリガ内の所要時間(分秒形式)を取得
  getConsumedTimeReadableWhileAllTriggersToday : function(){

    const consumed_time_readable = this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_READABLE, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).getValue();
      my_debug_log( "本日の全トリガが要した処理時間(分秒形式)をを取得:" + consumed_time_readable );

    return consumed_time_readable;

  },

  // 本日の全トリガ内の所要時間(分秒形式)を記録
  setConsumedTimeReadableWhileAllTriggersToday : function( consumed_time_millisec ){

    // ミリ秒を分秒形式に変換
    const consumed_time_readable = TriggerInfoUtil.transformMillisecToReadableString( consumed_time_millisec );

    // シート上に記録
    this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_TODAY_CONSUMED_TIME_READABLE, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).setValue( consumed_time_readable );
      my_debug_log( "本日の全トリガが要した処理時間(分秒形式)を記録:" + consumed_time_readable );

  },


  // 本日の全トリガの累積処理時間の枠内使用率を取得
  getConsumedTimePerLimitPercentageAllTriggersToday : function(){

    const consumed_time_percentage = this.getInfoSheet().getRange(
      ROW_NUM_CONSUMED_TIME_PERCENTAGE_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).getValue();
      my_debug_log( "本日の全トリガが要した処理時間の枠内使用率を取得:" + consumed_time_percentage );

    return consumed_time_percentage;

  },

  // 本日の全トリガの累積処理時間の枠内使用率を,文字列として記録
  setConsumedTimePercentageAllTriggersToday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_CONSUMED_TIME_PERCENTAGE_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).setValue( s );
      my_debug_log( "本日の全トリガが要した処理時間の枠内使用率を記録:" + s );

  },


  // 本日の最終トリガ終了時の日内時間経過率を取得
  getPassedTimePercentageOnLastTriggerFinishedToday : function(){

    const passed_time_percentage = this.getInfoSheet().getRange(
      ROW_NUM_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日の最終トリガ終了時の日内時間経過率を取得:" + passed_time_percentage );

    return passed_time_percentage;

  },

  // 本日の最終トリガ終了時の日内時間経過率を,文字列として記録
  setPassedTimePercentageOnLastTriggerFinishedTodayAsString : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_TODAY, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "本日の最終トリガ終了時の日内時間経過率を記録:" + s );

  },


  // 本日のダイジェストレートを,%形式の文字列として取得
  getDigestRatePercentageOnLastTriggerFinishedToday : function(){

    const digest_rate_percentage = this.getInfoSheet().getRange(
      ROW_NUM_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日のダイジェストレートを取得:" + digest_rate_percentage );

    return digest_rate_percentage;

  },

  // 本日のダイジェストレートを,文字列として記録
  setDigestRatePercentageOnLastTriggerFinishedToday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "本日のダイジェストレートを記録:" + s );

  },


  // 本日の全トリガ内の平均処理時間を,分秒形式の文字列として取得
  getAverageConsumedTimeReadableAllTriggersToday : function(){

    const average_consumed_time_readable = this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_READABLE_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日の全トリガの平均処理時間を取得:" + average_consumed_time_readable );

    return average_consumed_time_readable;

  },

  // 本日の全トリガ内の平均処理時間を,文字列として記録
  setAverageConsumedTimeReadableAllTriggersToday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_READABLE_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "本日の全トリガの平均処理時間を記録:" + s );

  },


  // 本日の全トリガ内の平均処理時間の枠内使用率を,文字列として取得
  getAverageConsumedTimePercentageAllTriggersToday : function( s ){

    const average_consumed_time_percentage = this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_PERCENTAGE_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日の全トリガの平均処理時間の枠内使用率を取得:" + s );

    return average_consumed_time_percentage;

  },

  // 本日の全トリガ内の平均処理時間の枠内使用率を,文字列として記録
  setAverageConsumedTimePercentageAllTriggersToday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_PERCENTAGE_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "本日の全トリガの平均処理時間の枠内使用率を記録:" + s );

  },


  // 本日の仕事完了数を取得
  getAchievedUserTasksToday : function(){

    const acheved_user_tasks = this.getInfoSheet().getRange(
      ROW_NUM_ACHIEVED_USER_TASKS_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日の仕事完了数を取得:" + acheved_user_tasks );

    return acheved_user_tasks;

  },

  // 本日の仕事完了数を記録
  setAchievedUserTasksToday : function( i ){

    this.getInfoSheet().getRange(
      ROW_NUM_ACHIEVED_USER_TASKS_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( i );
      my_debug_log( "本日の仕事完了数を記録:" + i );

  },


  // 本日の1トリガあたり平均仕事数を取得
  getAverageUserTasksPerOneTriggerToday : function(){

    const average_user_tasks = this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_USER_TASK_PER_ONE_TRIGGER_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日の1トリガあたり平均仕事数を取得:" + average_user_tasks );

    return average_user_tasks;

  },

  // 本日の1トリガあたり平均仕事数を記録
  setAverageUserTasksPerOneTriggerToday : function( average_user_tasks ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_USER_TASK_PER_ONE_TRIGGER_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( average_user_tasks );
      my_debug_log( "本日の1トリガあたり平均仕事数を記録:" + average_user_tasks );

  },


  // 本日の1仕事あたり平均時間を,文字列として取得
  getAverageUserTaskTimeToday : function( s ){

    const average_task_time = this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_USER_TASK_TIME_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).getValue();
      my_debug_log( "本日の1仕事あたり平均時間を取得:" + average_task_time );

    return average_task_time;

  },

  // 本日の1仕事あたり平均時間を,文字列として記録
  setAverageUserTaskTimeToday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_USER_TASK_TIME_TODAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "本日の1仕事あたり平均時間を記録:" + s );

  },


  // ----------- 前日起動した全トリガの累計情報について ----------


  // 前日トリガ起動に成功した回数を記録
  setAllTriggersCountPrevDay : function( all_triggers_count_oneday ){

    this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_PREVDAY_COUNT, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( all_triggers_count_oneday );
      my_debug_log( "前日トリガ起動に成功した回数を記録:" + all_triggers_count_oneday );

  },


  // 前日の全トリガ内の所要時間(ミリ秒)を記録
  setConsumedTimeMilliSecWhileAllTriggersPrevDay : function( consumed_time_millisec ){

    this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_MILLISEC, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).setValue( consumed_time_millisec );
      my_debug_log( "前日の全トリガが要した処理時間(ミリ秒)を記録:" + consumed_time_millisec );

  },


  // 前日の全トリガ内の所要時間(分秒形式)を記録
  setConsumedTimeReadableWhileAllTriggersPrevDay : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_ALL_TRIGGERS_PREVDAY_CONSUMED_TIME_READABLE, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).setValue( s );
      my_debug_log( "前日の全トリガが要した処理時間(分秒形式)を記録:" + s );

  },


  // 前日の全トリガ内の処理時間の枠内使用率を記録
  setConsumedTimePerLimitPercentagePrevDay : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_CONSUMED_TIME_PERCENTAGE_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_VAUES 
    ).setValue( s );
      my_debug_log( "前日の全トリガが要した処理時間の枠内使用率を記録:" + s );

  },


  // 前日の最終トリガ終了時の日内時間経過率を,文字列として記録
  setPassedTimePercentageOnLastTriggerFinishedPrevday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_PASSED_TIME_PERCENTAGE_ON_LAST_TRIGGER_FINISHED_PREVDAY, 
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "前日の最終トリガ終了時の日内時間経過率を記録:" + s );

  },


  // 前日の全トリガ内の平均処理時間を,文字列として取得
  setAverageConsumedTimeReadableAllTriggersPrevDay : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_READABLE_PREVDAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "前日の全トリガの平均処理時間を記録:" + s );

  },


  // 前日の全トリガ内の平均処理時間の枠内使用率を,文字列として取得
  setAverageConsumedTimePercentageAllTriggersPrevDay : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_CONSUMED_TIME_PERCENTAGE_PREVDAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "前日の全トリガの平均処理時間の枠内使用率を記録:" + s );

  },


  // 前日のダイジェストレートを,文字列として記録
  setDigestRatePercentageOnLastTriggerFinishedPrevday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_DIGEST_TIME_RATE_ON_LAST_TRIGGER_FINISHED_PREVDAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "前日のダイジェストレートを記録:" + s );

  },


  // 前日の仕事完了数を記録
  setAchievedUserTasksPrevday : function( i ){

    this.getInfoSheet().getRange(
      ROW_NUM_ACHIEVED_USER_TASKS_PREVDAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( i );
      my_debug_log( "前日の仕事完了数を記録:" + i );

  },

  
  // 前日の1トリガあたり平均仕事数を記録
  setAverageUserTasksPerOneTriggerPrevday : function( average_user_tasks ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_USER_TASK_PER_ONE_TRIGGER_PREVDAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( average_user_tasks );
      my_debug_log( "前日の1トリガあたり平均仕事数を記録:" + average_user_tasks );

  },

  
  // 前日の1仕事あたり平均時間を,文字列として記録
  setAverageUserTaskTimePrevday : function( s ){

    this.getInfoSheet().getRange(
      ROW_NUM_AVERAGE_USER_TASK_TIME_PREVDAY,
      COL_NUM_TRIGGER_PROPERTY_VAUES
    ).setValue( s );
      my_debug_log( "前日の1仕事あたり平均時間を記録:" + s );

  }

};
// トリガ情報シートのDAO終わり。




// ------------------------ 以下はトリガ情報シートに関連する便利メソッド集 -----------------------------




// トリガ情報のシート上での読み書きに関わる便利メソッドを集めたオブジェクト。
// データ形式の変換など。
let TriggerInfoUtil = {


  // ----------- ミリ秒からの変換について ----------


  // ミリ秒の数値を分秒形式の文字列に変換。
  // (これは人間にとって読みやすい形式なので,readableと名付けてある。)
  // ちなみに時間単位ではカウント表示しない。「80分1秒」は「1時間20分1秒」にはならない。
  transformMillisecToReadableString : function( time_millisec ){

    // ミリ秒から分と秒を算出
    const time_minutes = Math.floor( time_millisec / 1000 / 60 );
    const time_seconds = Math.floor( time_millisec / 1000 ) % 60;
    const time_readable = time_minutes + "" + time_seconds + "";

    return time_readable;
    
  },


  // ミリ秒をコロン形式に直す。
  // コロン形式とは,1分2秒を1:02のような文字列として表すこと。
  // なお時間単位でのカウント表示はしない。"80:01" は "1:20:01" とはならない。
  transformMillisecToColonFormString : function( time_millisec ){

    // コロン形式に直す
    const time_minutes = Math.floor( time_millisec / 1000 / 60 );
    const time_seconds = Math.floor( time_millisec / 1000) % 60;
    const time_colon_form = time_minutes 
      + ":"
      + ( ( "" + time_seconds ).padStart( 2, "0" ) )
    ;
      // GASには0埋め用にString.padStartがある
      // https://itsakura.com/gas-padstart

    return time_colon_form;

  },


  // ミリ秒を秒数形式(DetailSecond形式)に直す。
  // この形式は,ユーザー定義タスクの消費時間を表すために用いる。
  transformMillisecToDetailedSeconds : function( user_task_time_millisec ){

    // ミリ秒を秒に直す
    let user_task_time_seconds = user_task_time_millisec / 1000;

    // 小数第二位までとする
    user_task_time_seconds = Math.floor( user_task_time_seconds * 100 ) / 100;

    // 読みやすい単位を付ける
    user_task_time_seconds = user_task_time_seconds + "";

    return user_task_time_seconds;

  },


  // ----------- 比からの変換について ----------


  // 0以上1以下の数値(比)を受け取り,”XX.XX %” というパーセント形式の文字列として返す。
  transformRatioToPercentageStringToFirstDecimalPlace : function( num_ratio ){

    // 比をパーセントの実数にする
    const num_percentage = num_ratio * 100;

    // 小数第2位までで値を打ち切る
    const percentage_to_first_decimal_place = Math.floor( num_percentage * 100 ) / 100;

    // 末尾に%記号を付け,文字列にする
    const percentage_string = percentage_to_first_decimal_place + " %";

    return percentage_string;

  },


  // ----------- Dateオブジェクトの取り扱いについて ----------


  // Dateオブジェクトを受け取り,その表す日付の日付開始時刻(0時0分)のUNIXミリ秒を返す。
  transformDateObjToBeginningOfDayUnixMs : function( original_dt ){

    // 時分秒などの情報を取り出す
    const original_dt_hours = original_dt.getHours();
    const original_dt_minutes = original_dt.getMinutes();
    const original_dt_seconds = original_dt.getSeconds();
    const original_dt_milliseconds = original_dt.getMilliseconds();
  
    // その日の初めから何ミリ秒が経過しているか?
    const passed_time_ms = 0
      + ( original_dt_hours * 60 * 60 * 1000 ) // 時のぶん
      + ( original_dt_minutes * 60 * 1000 ) // 分のぶん
      + ( original_dt_seconds * 1000 ) // 秒のぶん
      + original_dt_milliseconds // ミリ秒のぶん
    ;

    // 経過した分のミリ秒を引き算し,0時時点にする
    const original_dt_unix_ms = original_dt.getTime();
    const beginning_dt_unix_ms = original_dt_unix_ms - passed_time_ms;

    return beginning_dt_unix_ms;
  },


  // 2つのDateオブジェクトを受け取り,起点となる日付の0時時点から数えて,終点となる日時まで経過したミリ秒を返す。
  // 終了時刻のDateオブジェクト① が,
  // 開始時刻のDateオブジェクト② の日付開始時点(0時)から数えて
  // 何ミリ秒が経過しているか,を計算するが,日をまたいだ場合は①と②の日付は異なりうる。
  transformDateObjToPassedTimeMillisec : function( dt_end, dt_start ){

    // 起点となるDateオブジェクトの0時時点のUNIXミリ秒
    const start_time_unix_ms = this.transformDateObjToBeginningOfDayUnixMs( dt_start );

    // 終点となるDateオブジェクトのUNIXミリ秒
    const end_time_unix_ms = dt_end.getTime();

    // 起点から終点まで何ミリ秒か
    const passed_time_ms = end_time_unix_ms - start_time_unix_ms;

    return passed_time_ms;

  },


  // 2つのDateオブジェクトが,トリガ起動間隔の設定値から見て
  // 異なる「分ゾーン」に属するかどうかを判定
  judgeTwoTriggersTooClose : function( old_trigger_dt, new_trigger_dt ){

    // 新旧のDateオブジェクトで,時が異なっていれば「異なるゾーンに属する」とみなす。
    if(
      // 年が一致しない?
      ( old_trigger_dt.getFullYear() != new_trigger_dt.getFullYear() )
      ||
      // 月が一致しない?
      ( old_trigger_dt.getMonth() != new_trigger_dt.getMonth() )
      ||
      // 日が一致しない?
      ( old_trigger_dt.getDate() != new_trigger_dt.getDate() )
      ||
      // 時が一致しない?
      ( old_trigger_dt.getHours() != new_trigger_dt.getHours() )
    ){

      // 別の「分ゾーン」に属する,とみなす (2つのトリガ間には十分な時間が確保できているとみなす)
      my_debug_log("2つのDateは別個の「時」に属するため,トリガ起動間隔は十分にあいているとみなします。");

      // 近くないと判定
      return false;

    }
    else
    {

      my_debug_log("2つのDateは同一の「時」に属します。トリガ起動間隔が十分にあいているか,分ゾーンの検査が必要です。");

    }
    // 下記は新旧のDateオブジェクトで「時」が一致する場合のゾーン判定


    // 新旧のDateオブジェクト(トリガ起動日時)の,1時間の中での分数を取得
    const old_trigger_minutes = old_trigger_dt.getMinutes();
    const new_trigger_minutes = new_trigger_dt.getMinutes();

    // トリガ間隔の設定値は何分おきか?
    const trigger_pace_minutes = 60 / FREQUENCY_PACE_IN_ONE_HOUR_FOR_TRIGGER_TO_START_MAIN_PROC; // 60分を分割する
      my_debug_log( "トリガ間隔の設定値 trigger_pace_minutes : " + trigger_pace_minutes );
      // この間隔値を使って1時間のうち「0分以上60分未満」をいくつかの「分ゾーン」(※時間帯のようなもの)に分割し,
      // 前回のトリガ起動と今回のトリガ起動が異なる「分ゾーン」に属するなら新規起動を許可する。
      // 1つの「分ゾーン」は始点を「その時刻以上」として含み,終点を「その時刻未満」として含まない。
    
    // トリガ間隔1つあたり1ゾーンと数えることにして,新旧それぞれでゾーンはいくつあるかを比較する。

    // 古いほうのDateオブジェクトは,1時間の中で1番目から数えて何番目のゾーンに属しているか?
    const old_trigger_zone_num = Math.floor( old_trigger_minutes / trigger_pace_minutes );
    const old_trigger_zone_index = old_trigger_zone_num + 1;
      my_debug_log(
        "古いほうのDateオブジェクトの情報:"
        + "old_trigger_minutes = " + old_trigger_minutes
        + ", old_trigger_zone_num = " + old_trigger_zone_num
        + ", old_trigger_zone_index = " + old_trigger_zone_index
      );

    // 新しいほうのDateオブジェクトは,1時間の中で1番目から数えて何番目のゾーンに属しているか?
    const new_trigger_zone_num = Math.floor( new_trigger_minutes / trigger_pace_minutes );
    const new_trigger_zone_index = new_trigger_zone_num + 1;
      my_debug_log(
        "新しいほうのDateオブジェクトの情報:"
        + "new_trigger_minutes = " + new_trigger_minutes
        + ", new_trigger_zone_num = " + new_trigger_zone_num
        + ", new_trigger_zone_index = " + new_trigger_zone_index
      );
    // 所属する分ゾーン番号が一致?
    if( old_trigger_zone_index == new_trigger_zone_index ){

      // 同一の「分ゾーン」に属する (2つのトリガ間には十分な時間が確保できていない,とみなす)
      my_debug_log("2つのDateは同一の「分ゾーン」に属します。トリガ起動間隔が十分にあいていません。");

      // 近いと判定
      return true;

    }
    else
    {

      // 別の「分ゾーン」に属する (2つのトリガ間には十分な時間が確保できているとみなす)
      my_debug_log("2つのDateは同一の「分ゾーン」に属しません。トリガ起動間隔が十分にあいています。");

      // 近くないと判定
      return false;

    }

    return false;

  },


  // ----------- 複数シートの整理について ----------


  // ブック内で使用する複数シートの並び順を調整する。
  // トリガ情報だけでなくシートログなど,いろんな機能のシートにまたがった処理なので
  // Util系のオブジェクト内に関数を置いてある。
  // ユーザが自由に作成できるシートではなく,システム的に使用するシート(SystemSheet)が対象。
  // ブック上にシステムシートが自動生成される際に,この関数が呼び出されるようにしてシートの並び順を担保する。
  // insertSheetの直後に呼び出すこと。
  adjustSystemSheetOrders : function(){

    my_debug_log( "シートの並び順を調整開始します。" );


    // 対象となる各シートが存在しているかどうかの配列
    const arr_sheet_exist_flags = ARR_ORDERED_SHEET_NAMES.map(function( sheet_name ){
      
      // シート名からシートオブジェクトを取得
      const sheet_obj = SpreadsheetApp
        .getActiveSpreadsheet()
        .getSheetByName( sheet_name )
      ;

      // シートオブジェクトがnullではないか?boolに変換
      const sheet_exist_flag = !! sheet_obj;
        my_debug_log( "シート存在判定。シート名:" + sheet_name + ", フラグ:" + sheet_exist_flag );

      return sheet_exist_flag;

    });

    // 各シートの望ましい並び順を表す配列のうち,実際に存在しているシートだけを残す
    const arr_existing_sheet_names = ARR_ORDERED_SHEET_NAMES.filter(function( sheet_name, arr_index ){

      // このシートが存在するかどうかのフラグ
      const sheet_exist_flag = arr_sheet_exist_flags[ arr_index ];

      // 存在判定フラグを返す (falseな要素は配列からカットされる)
      return sheet_exist_flag;

    });
      my_debug_log( "整列対象のシートのうち,存在するシート数は" + arr_existing_sheet_names.length );

    // 各シートの望ましい並び順を,先頭から順番に実現してゆく
    arr_existing_sheet_names.forEach(function( sheet_name, arr_index ){
      
      // シートの並び順は一番左から数えて1始まり
      const expected_sheet_index = arr_index + 1;
        // NOTE:
        // ネット上では,moveActiveSheetによるシート移動先が
        // 0始まりなのか1始まりなのか情報がはっきりしない。
        // 実際の動作では,1始まりで指定しなければいけない。
        // これを間違えて0始まりでコーディングすると正常に動作しないので注意。

      // シート名からシートオブジェクトを取得
      const sheet_obj = SpreadsheetApp
        .getActiveSpreadsheet()
        .getSheetByName( sheet_name )
      ;

      // シートを指定位置に移動
      sheet_obj.activate();
      SpreadsheetApp.getActiveSpreadsheet().moveActiveSheet( expected_sheet_index );
        // シート位置変更の例
        // https://auto-worker.com/blog/?p=3211

      my_debug_log( "シート位置を変更。シート名:" + sheet_name + ", 位置:" + expected_sheet_index );

    });


    my_debug_log( "シートの並び順を調整終了。" );
    return;

  }

};




// ------------------------ 以下はコンソール上でのログ用 -----------------------------




// 開発中のデバッグ用にコンソールログ表示
function my_debug_log( s ){

  // 実運用時の処理ステップを少しでも減らすために,
  // デバッグログの出力を抑制制御するフラグを設けてある。
  if( ENABLE_CONSOLE_DEBUG_LOG_FLAG ){
    console.log( "デバッグ情報: " + s );
  }

}


// エラー発生時用にコンソールログ表示
function my_err_log( s, error ){
  console.error( "エラー情報: " + s, error );
}




// ------------------------ 以下はシート上でのログ用 -----------------------------




// シート上にログを記録するオブジェクト。
// ・シート内の先頭に新しい情報が来るようにする。
// ・シート内で行数が増えすぎたら,古い内容は自動的に削除してゆく。
let MySheetLogger = {


  // ----------- 下記はオブジェクトの外部から呼び出される関数 ----------


  // シート上に1行ぶんのログを記録する。


  // テスト動作用のログ
  testLog : function( log_content ){
    this.logWithCategory( "TEST", log_content );
  },


  // カテゴリーを指定しない通常のログ
  log : function( log_content ){
    this.logWithCategory( "GENERAL", log_content );
  },


  // 制御情報を記録するためのログ
  controlInfo : function( log_content, option_obj ){
    this.logWithCategory( "CONTROL_INFO", log_content, option_obj );
  },


  // エラーを記録するためのログ。
  // もし記録に失敗した場合は,コンソールログのみを残す。
  err : function( log_content, err_obj ){

    // NOTE:
    // エラー発生時は,シートログすら残せない場合もある。
    // そういう場合,シートログ記録しようとしてエラーを多重に発生させてしまうと
    // 呼び出し元でエラーキャッチしきれなくなり,ロック解放に失敗する恐れもある。
    // そのため,エラー発生時のシートログ記録操作は無理をせず,シートログを残せなければコンソールログのみにとどめる。

    try{
      
      // エラーメッセージを構築
      const err_msg = log_content
        + ", "
        + err_obj.toString()
        + ", "
        + err_obj.message
        + ", "
        + err_obj.stack
      ;

      // 可能ならシートログを記録する
      this.logWithCategory( "ERROR", err_msg );

    }
    catch( e_onSheetLog ){
      
      // シートログ記録時に例外が発生したら,コンソールログのみとする
      my_err_log( "エラー用のシートログを記録できません。", e_onSheetLog );
      my_debug_log( "シートログに記録できなかった情報:" + log_content );

    }

    return;
    
  },


  // カテゴリを自由に指定して,シート上の先頭に1行ぶんのログを追記
  // ・シート内の先頭に新しい情報が来るようにする。
  // ・シート内で行数が増えすぎたら,古い内容は自動的に削除してゆく。
  logWithCategory : function( log_category, log_content, option_obj ){

    // シートを確保しておく
    this.setupLogSheet();

    // シート上の先頭に1行を追記
    this.writeOneRecordAtTopLine( log_category, log_content, option_obj );

    // ログの行数が増え過ぎていたら古い情報をカットする
    this.deleteOldLogsIfExceeded();

    return;
  },


  // ----------- 下記は内部処理 ----------


  // 記録対象となるシート
  _log_sheet : null,


  // 記録対象のシートを返す
  getLogSheet : function(){
    return this._log_sheet;
  },


  // 記録対象のシートをセットする
  setLogSheet : function( sheet_obj ){
    this._log_sheet = sheet_obj;
  },


  // 記録対象となるシートをセットアップ(初回のみ)
  setupLogSheet : function(){

    // すでに設定済みなら何もしない
    if( this.getLogSheet() ) return;

    // オブジェクト内にまだキャッシュされていない場合は
    // プロパティ値として保持する

    // シート名を指定してシート取得
    let log_sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .getSheetByName( SHEET_NAME_FOR_SHEET_LOGGING )
    ;

    // ブック内にシートが無い場合は自動生成する
    if( ! log_sheet ){

      // ラベルなどが整ったシートを生み出して取得
      log_sheet = this.createNewLogSheet();

    }

    // オブジェクト内にキャッシュする
    this.setLogSheet( log_sheet );

    return;

  },

  
  // ブック内にシートログ記録用のシートを新規作成する。
  createNewLogSheet : function(){

      my_debug_log("createNewLogSheetを開始します。");


    // 空白のシートを生み出す
    SpreadsheetApp.flush();
    const log_sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .insertSheet(
        SHEET_NAME_FOR_SHEET_LOGGING, 
        0 // とりあえず先頭に配置
      )
    ;
    SpreadsheetApp.flush();
      my_debug_log("空白のシートをinsertSheetしました。");

      // NOTE:
      // 上記で作成したばかりの空白シートに対して,間を置かずに下記コードでgetRangeしようとすると
      // 「Exception: Service Spreadsheets timed out while accessing document with id ~」
      // のエラーが出る事がよくあった。
      // スプレッドシートの負荷が高い状態でシート内容にアクセスしようとすると起きるエラーらしい。
      // 解決策として,insertSheetの前後で SpreadsheetApp.flush() を呼び出すとエラーが起きなくなった。
      // 下記リンクを参照。
      // https://stackoverflow.com/a/68634508


    // シートの並び順を調整
    TriggerInfoUtil.adjustSystemSheetOrders();


    // シート内にラベルを作成する

    // ログ記録日時
    log_sheet.getRange(
      ROW_NUM_SHEET_LOGGING_LABELS, 
      COL_NUM_SHEET_LOGGING_DATETIME
    ).setValue( LABEL_SHEET_LOGGING_DATETIME );

    // ログのカテゴリー
    log_sheet.getRange(
      ROW_NUM_SHEET_LOGGING_LABELS, 
      COL_NUM_SHEET_LOGGING_CATEGORY
    ).setValue( LABEL_SHEET_LOGGING_CATEGORY );

    // トリガ内経過時間
    log_sheet.getRange(
      ROW_NUM_SHEET_LOGGING_LABELS, 
      COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_TRIGGER
    ).setValue( LABEL_SHEET_LOGGING_CONSUMED_TIME_IN_TRIGGER );

    // 日内処理の累積時間
    log_sheet.getRange(
      ROW_NUM_SHEET_LOGGING_LABELS, 
      COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_ONEDAY
    ).setValue( LABEL_SHEET_LOGGING_CONSUMED_TIME_IN_ONEDAY );

    // ダイジェストレート
    log_sheet.getRange(
      ROW_NUM_SHEET_LOGGING_LABELS, 
      COL_NUM_SHEET_LOGGING_DIGEST_RATE
    ).setValue( LABEL_SHEET_LOGGING_DIGEST_RATE );

    // ログ本文
    log_sheet.getRange(
      ROW_NUM_SHEET_LOGGING_LABELS, 
      COL_NUM_SHEET_LOGGING_CONTENT
    ).setValue( LABEL_SHEET_LOGGING_CONTENT );

      my_debug_log("シート内にラベルを作成完了しました。");


    // 列の幅を調節。
    // (※このあたりの設定値は,わざわざ定数として持っておくほどでは無いと思われる)

    // ログ記録日時
    log_sheet.setColumnWidth( COL_NUM_SHEET_LOGGING_DATETIME, 170 );

    // ログのカテゴリー
    log_sheet.setColumnWidth( COL_NUM_SHEET_LOGGING_CATEGORY, 115 );

    // トリガ内経過時間
    log_sheet.setColumnWidth( COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_TRIGGER, 70 );

    // 日内処理の累積時間
    log_sheet.setColumnWidth( COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_ONEDAY, 55 );

    // ダイジェストレート
    log_sheet.setColumnWidth( COL_NUM_SHEET_LOGGING_DIGEST_RATE, 100 );

    // ログ本文
    log_sheet.setColumnWidth( COL_NUM_SHEET_LOGGING_CONTENT, 610 );

      my_debug_log("シート内で列の幅を調節完了しました。");


    // 右側に表示される列数を増やしておく。
    // (ログ本文が横に長くなりうるため)
    log_sheet.insertColumnsAfter( COL_NUM_SHEET_LOGGING_CONTENT, 40 );
      // シートを自動生成すると,初期状態ではZ列(0始まりで25番目)までしか表示されない。
      // これをBM列やBN列あたりまで表示されるようにしておくとあとあと便利。(Z列から数えて40列ほどを追加することになる)
    log_sheet.setColumnWidths( COL_NUM_SHEET_LOGGING_CONTENT + 1, 40, 100 );
      my_debug_log("シート内の右側に余白列を確保しました。");


    // セル値の寄せ方を調節 (左寄せ・上寄せ)

    // シートログに関係する全ての列
    log_sheet.getRange(
      // 最初の行
      1, COL_NUM_SHEET_LOGGING_DATETIME,
      // 最後の行
      log_sheet.getMaxRows(), COL_NUM_SHEET_LOGGING_CONTENT - COL_NUM_SHEET_LOGGING_DATETIME + 1
    )
    .setHorizontalAlignment("left")
    .setVerticalAlignment("top")
    ;
      my_debug_log("セル値の寄せ方を調節しました。");


    // オートフィルタを設定

    // シートログに関係する全ての列
    log_sheet.getRange(
      // 最初の行
      1, COL_NUM_SHEET_LOGGING_DATETIME,
      // 最後の行
      log_sheet.getLastRow(), COL_NUM_SHEET_LOGGING_CONTENT
    ).createFilter();
      my_debug_log("オートフィルタを設定しました。");


      my_debug_log("createNewLogSheetを終了します。");

    // できたてホヤホヤのシートを返却する
    return log_sheet;

  },


  // シート上の先頭に1行ぶんのログを追記
  // (追記するだけで,全件数の超過チェック処理などはここでは行わない)
  writeOneRecordAtTopLine : function( log_category, log_content, option_obj ){

    // 先頭行に記録用のスペースを確保(空白行を1行挿入)
    this.getLogSheet().insertRowBefore( ROW_NUM_SHEET_LOGGING_START );

      // NOTE:
      // 「空行を確保した後でログを記録することなくエラーで停まったりした場合,
      //  その場合は例外をキャッチし,確保した空行を消したほうが良いだろうか。」
      // などと考えたこともあったが,それはしなくてよいだろう。
      // 下記はそこまでの異常系が発生するような複雑な処理じゃないから。
      // 唯一エラーになるのは,下記のコードに手を加えた直後に変数の定義漏れが起きるとかに限られる。


    // 現在の日時を文字列としてフォーマット
    const current_datetime = Utilities.formatDate(
      new Date(), 
      "JST", 
      "yyyy-MM-dd (E) HH:mm:ss"
    );
      // GASでのDate型データのフォーマット
      // https://jp.tdsynnex.com/blog/google/gas-data-format/

    // トリガ関連の情報
    let consumed_time_in_trigger = "";
    let consumed_time_in_oneday = "";
    let digest_rate_percentage = "";
    if( option_obj && option_obj.disable_digest_rates ){
      // 記録不要の場合は空欄とする
      consumed_time_in_trigger = "";
      consumed_time_in_oneday = "";
      digest_rate_percentage = "";
    }else{
      consumed_time_in_trigger = MyTriggerManager.getCurrentTriggerConsumedTimeAsColonForm();
      consumed_time_in_oneday = MyTriggerManager.getAllTriggersOnedayConsumedTimeAsColonForm();
      digest_rate_percentage = MyTriggerManager.getSafeHedgedRealTimeDigestRateOnedayAsPercentage();
    }
      // ↑これらの値を生成する際に,毎回シート読み取りの負荷がかからないように工夫してある。

    // ログ内容をセルに記録
    this.getLogSheet().getRange( // ログ記録日時
      ROW_NUM_SHEET_LOGGING_START, 
      COL_NUM_SHEET_LOGGING_DATETIME
    ).setValue( current_datetime );
    this.getLogSheet().getRange( // ログのカテゴリー
      ROW_NUM_SHEET_LOGGING_START, 
      COL_NUM_SHEET_LOGGING_CATEGORY
    ).setValue( log_category );
    this.getLogSheet().getRange( // 1トリガ内での経過時間
      ROW_NUM_SHEET_LOGGING_START, 
      COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_TRIGGER
    ).setValue( "'" + consumed_time_in_trigger );
    this.getLogSheet().getRange( // 1日の全トリガの累積処理時間
      ROW_NUM_SHEET_LOGGING_START, 
      COL_NUM_SHEET_LOGGING_CONSUMED_TIME_IN_ONEDAY
    ).setValue( "'" + consumed_time_in_oneday );
    this.getLogSheet().getRange( // ダイジェストレート
      ROW_NUM_SHEET_LOGGING_START, 
      COL_NUM_SHEET_LOGGING_DIGEST_RATE
    ).setValue( digest_rate_percentage );
    this.getLogSheet().getRange( // ログ本文
      ROW_NUM_SHEET_LOGGING_START, 
      COL_NUM_SHEET_LOGGING_CONTENT
    ).setValue( log_content );
      my_debug_log(
        "シート上に新規ログを記録しました。"
        + current_datetime 
        + ", "
        + log_category
        + ", "
        + consumed_time_in_trigger
        + ", "
        + consumed_time_in_oneday
        + ", "
        + digest_rate_percentage
        + ", "
        + log_content
      );
  
    return;
  },


  // シート内のログ最終行の行番号を求める
  getRowNumLastLogRecord : function(){

    let row_num_last_record = -1;
    
    // ログは1行だけか?
    const emptytest_str = this.getLogSheet()
      .getRange(
        ROW_NUM_SHEET_LOGGING_START + 1, 
        COL_NUM_SHEET_LOGGING_DATETIME
    ).getValue();
    if( ( ! emptytest_str ) || ( emptytest_str.length < 1 ) ){ // 2行目が空?

      // 1行目を最終行とみなす
      row_num_last_record = ROW_NUM_SHEET_LOGGING_START;

    }
    else
    {

      // 列内で値がある最後のセルを求める。
      // 下方向(DOWN)のデータがある最終セルを取得
      const lastCell = this.getLogSheet().getRange(
        ROW_NUM_SHEET_LOGGING_START, 
        COL_NUM_SHEET_LOGGING_DATETIME
      ).getNextDataCell( SpreadsheetApp.Direction.DOWN );
        // NOTE: 上記のコードは,ログ総数が0行の場合と1行の場合には正しく動作しない。
        // ログが2行以上ある場合は正しく動作する。
        // ログ総数が0行の場合と1行の場合には,シート内の「値の有無にかかわらず」最終行に飛んでしまう。
        // そのため,ログ総数が0行の場合と1行の場合は事前に分岐して別処理とする。

      // 最終セルの行番号を取得
      row_num_last_record = lastCell.getRow();

      // NOTE: ログの途中に一回でも空行があると,その行から下は消えずに残ってしまう。

    }
      my_debug_log( "ログの最終行は" + row_num_last_record );

    return row_num_last_record;

  },


  // ログの行数が増え過ぎていたら古い情報をカットする
  deleteOldLogsIfExceeded : function(){

    // シート内のログ最終行の行番号を求める。
    const row_num_last_record = this.getRowNumLastLogRecord();

    // もし行数が増え過ぎていないか検査
    const current_logs_count = row_num_last_record - ROW_NUM_SHEET_LOGGING_START + 1;
      my_debug_log( "現在のログ行数は current_logs_count = " + current_logs_count );
      my_debug_log( "ログ行数の上限値は max_logs_count = " + MAX_ROWS_COUNT_FOR_SHEET_LOGGIONG );
    // 上限を超過?
    if( MAX_ROWS_COUNT_FOR_SHEET_LOGGIONG < current_logs_count ){

      my_debug_log( "古いほうのログをカット(削除)します。");

      // 何行目から削除開始するか
      const row_num_to_start_cut_logs_off = row_num_last_record - HOW_MANY_LOGS_TO_CUT_OFF_FOR_SHEET_LOGGING;
        my_debug_log( "ログカット開始の行番号: " + row_num_to_start_cut_logs_off);
      

      // 合計で何行をログ削除するか
      const delete_rows_count = row_num_last_record - row_num_to_start_cut_logs_off + 1;
        // ログデータが存在する最も下側まで削除する。
        my_debug_log( "ログカット開始の行を含めて削除する行数: " + delete_rows_count);

        //const delete_rows_count = HOW_MANY_LOGS_TO_CUT_OFF + 1;
        // これだと設定値通りの行数を削除するが,もっと古いデータが下側に余分に残っていた場合に消しきれず残ってしまう。
        // なので,ここで削除対象の行範囲を選ぶ際に,終端は「データが存在する末尾」にした。


      // 古いほうのログをカット(削除)する。行ごと消して詰める。
      this._log_sheet.deleteRows( row_num_to_start_cut_logs_off, delete_rows_count );
        // deleteRowsについて
        // https://auto-worker.com/blog/?p=4621
        my_debug_log( "古いログの削除が完了。" );

    }

    return;

  }

};
// シートロガーの定義終わり




// ------------------------ 以下はトリガ実行結果の視覚化用 -----------------------------



// トリガ実行結果をシート上に視覚的にわかりやすく表現・記録するオブジェクト。
// 1時間おき,2時間おきなどの棒グラフによる視覚化方法を選べる。
let TriggerStatVisualizer = {


  // ----------- 下記はオブジェクトの外部から呼び出される関数 ----------


  // 一回分のトリガ実行結果を受け取って,可視化用にシート上に記録
  onReceivedOneTriggerResult : function( arg_obj ){
    
    // 引数を解釈
    const trigger_start_dt                   = arg_obj.trigger_start_dt;
    const achieved_user_tasks_one_trigger    = arg_obj.achieved_user_tasks_one_trigger;
    const consumed_time_millisec_one_trigger = arg_obj.consumed_time_millisec_one_trigger;

    // シート上で記載すべき行と列を確保し,書き込み対象の行番号を特定
    const expected_row_num = this.determineDateAndHourZoneOnSheetForRecording( trigger_start_dt );

    // 完了した仕事数をシート上に追記
    this.recordOneTriggerResultInfosAtExpectedPosition({
      expected_row_num                   : expected_row_num,
      achieved_user_tasks_one_trigger    : achieved_user_tasks_one_trigger,
      consumed_time_millisec_one_trigger : consumed_time_millisec_one_trigger,
      finished_triggers_count            : 1 // トリガ数1個分
    });

    my_debug_log( "トリガ実行結果の図示が完了。" );  

    return;

  },


  // トリガ内でユーザ定義タスクを実行中に発生したエラー数情報を受け取り,
  // 即座に可視化シート上に記録・反映する。
  onReceivedOneErrorWhileCurrentTrigger : function( arg_obj ){
    
    // 引数を解釈
    const trigger_start_dt = arg_obj.trigger_start_dt;
    const error_count      = arg_obj.error_count;

    // シート上で記載すべき行と列を確保し,書き込み対象の行番号を特定
    const expected_row_num = this.determineDateAndHourZoneOnSheetForRecording( trigger_start_dt );

    // エラー数をシート上に追記
    this.recordOneErrorInfoAtExpectedPosition({
      expected_row_num : expected_row_num,
      error_count      : error_count
    });
      my_debug_log( "エラー1個分の図示が完了。" );  

    return;
    
  },


  // ----------- 下記は内部処理 ----------


  // 記録対象となるシート (visはビジュアライズ=可視化の意)
  _vis_sheet : null,


  // 記録対象のシートを,オブジェクト内のキャッシュ値から返す
  getVisSheet : function(){
    return this._vis_sheet;
  },


  // 記録対象のシートを,オブジェクト内のキャッシュ値にセットする
  setVisSheet : function( sheet_obj ){
    this._vis_sheet = sheet_obj;
  },


  // ----------- 下記は新規シート生成について ----------


  // 記録対象となるシートを生成(初回のみ)
  setupVisSheet : function( trigger_start_dt ){

    // すでに設定済みなら何もしない
    if( this.getVisSheet() ) return;

    // オブジェクト内にまだキャッシュされていない場合は
    // プロパティ値として保持する

    // シート名を指定してシート取得
    let vis_sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .getSheetByName( SHEET_NAME_FOR_TRIGGER_VISUALIZATION )
    ;

    // ブック内にシートが無い場合は自動生成する
    if( ! vis_sheet ){

      // 新規シートを生み出して取得
      vis_sheet = this.createNewVisSheet();

      // オブジェクト内にキャッシュする
      this.setVisSheet( vis_sheet );

      // トリガ開始日時の日付と,時間帯ラベルをセットする
      this.createNewDateFormatAndHourZonesFormatOnSheet( trigger_start_dt );

    }else{

      // オブジェクト内にキャッシュする
      this.setVisSheet( vis_sheet );

    }

    return;

  },

  
  // ブック内にシートを新規作成する。
  createNewVisSheet : function(){

    // 空白のシートを生み出す。
    // (insertSheetは,安全のために前後をflushではさんでおく)
    SpreadsheetApp.flush();
    const vis_sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .insertSheet(
        SHEET_NAME_FOR_TRIGGER_VISUALIZATION, 
        0 // とりあえず先頭に配置
      )
    ;
    SpreadsheetApp.flush();

    // シートの並び順を調整
    TriggerInfoUtil.adjustSystemSheetOrders();


    // 編集禁止の文言

    // シート内にラベルを作成
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_ATTENTION_FOR_MANUAL_EDIT, 
      COL_NUM_TRIGGER_VISUALIZATION_ATTENTION_FOR_MANUAL_EDIT
    )
      .setValue( "(※手動編集禁止)" )
      .setHorizontalAlignment("left")
      .setVerticalAlignment("top")
    ;
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_ATTENTION_FOR_MANUAL_EDIT,
      COL_WIDTH_TRIGGER_VISUALIZATION_SHEET_BLANK_MARGIN
    );


    // NOTE:
    // 日付や時間帯などは,ここでは記載しない。
    // それは1日おきに新たに記載し直すものだから。


    // できたてホヤホヤのシートを返却する
    return vis_sheet;

  },


  // ----------- 下記は日付ラベルについて ----------


  // 新しい日付と,時間帯ラベルをシート上にセットする。
  // (生み出したい日付・年月日は引数で指定できるようになっている)
  createNewDateFormatAndHourZonesFormatOnSheet : function( date_obj_today ){

    my_debug_log("図示シート上に,新規日付と時間帯ラベルを記入します。");

    // シートを取得
    const vis_sheet = this.getVisSheet();


    // 本日の日付をシート記載用に文字列として整形
    const date_label_today = this.transformDateObjToReadableLabel( date_obj_today );
    this.setNewestDateLabel( date_label_today );


    // 項目名を記入

    // 「時間帯」のラベルの列
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE  
    ).setValue( "トリガ\n開始の\n時間帯" );
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE,
      95
    );
    vis_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE,
      // 最後の行
      vis_sheet.getMaxRows(), 1
    )
    .setHorizontalAlignment("right")
    ;


    // 「トリガ起動確立数」のラベルの列
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_TRIGGERS_COUNT
    ).setValue( "トリガ\n起動の\n確立数" );
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_TRIGGERS_COUNT,
      50
    );
    vis_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_VISUALIZATION_SHEET_TRIGGERS_COUNT,
      // 最後の行
      vis_sheet.getMaxRows(), 1
    )
    .setHorizontalAlignment("right")
    ;


    // 「エラー数」のラベルの列
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ERRORS_COUNT
    ).setValue( "検知\nした\nエラー" );
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ERRORS_COUNT,
      35
    );
    vis_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_VISUALIZATION_SHEET_ERRORS_COUNT,
      // 最後の行
      vis_sheet.getMaxRows(), 1
    )
    .setHorizontalAlignment("right")
    ;


    // 「仕事完了数」のラベルの列
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS
    ).setValue( "仕事\n完了\n" );
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS,
      35
    );
    vis_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS,
      // 最後の行
      vis_sheet.getMaxRows(), 1
    )
    .setHorizontalAlignment("right")
    ;


    // AAグラフによる「仕事完了数の図示」のラベルの列
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH
    ).setValue( "時間帯中の\n仕事完了数を\n棒グラフで図示" );
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH,
      130
    );
    vis_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH,
      // 最後の行
      vis_sheet.getMaxRows(), 1
    )
    .setHorizontalAlignment("left")
    ;


    // 「経過時間(分秒)」のラベルの列
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_SECONDS
    ).setValue( "トリガ内\n処理時間\n(秒)" );
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_SECONDS,
      60
    );
    vis_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_SECONDS,
      // 最後の行
      vis_sheet.getMaxRows(), 1
    )
    .setHorizontalAlignment("right")
    ;


    // AAグラフによる「処理時間の図示」のラベルの列
    vis_sheet.getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_SHEET_LABELS,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH
    ).setValue( "時間帯中の\nトリガ内処理時間を\n棒グラフで図示" );
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH,
      130
    );
    vis_sheet.getRange(
      // 最初の行
      1, COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH,
      // 最後の行
      vis_sheet.getMaxRows(), 1
    )
    .setHorizontalAlignment("left")
    ;


    // 日付ごとの区切りの空白列 (最新の日付の右側にある分)
    vis_sheet.setColumnWidth(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_SHEET_BLANK_MARGIN,
      COL_WIDTH_TRIGGER_VISUALIZATION_SHEET_BLANK_MARGIN
    );


    // 時間帯ラベル等をシート上にセットする。
    this.createAllHourZoneFormatsOfOneDayOnSheet();


    return;

  },


  // 可視化シート上に情報を1つ追記したい時に,書き込み対象となる行と列をシート上で確保する。
  // 行番号を特定して返す。
  // 書き込み対象となる列とは,正しい日付を指す。
  // また書き込み対象となる行とは,正しい時間帯を指す。
  determineDateAndHourZoneOnSheetForRecording : function( trigger_start_dt ){

    // 必要なら記録用のシートを生成
    this.setupVisSheet( trigger_start_dt );


    // 現時点での時間帯の設定に応じて,シート上で記入すべき行番号を求める
    const expected_row_num = this.getExpectedRowNumForOneTriggerResult( trigger_start_dt );

    // シート上で予期した通りの場所に記入が可能な状態になっているか?
    if( ! this.checkPositionOnSheetReadyForRecording( expected_row_num, trigger_start_dt ) ){

      // もしシート上の状態が整っていなければ,本日の欄を新たに生み出し,古い日付の分をローテーション削除
      this.rotateDateFormatColumnOnSheet( trigger_start_dt );

    }
    // ここまでで,シート上で予期した通りの場所に記入が可能な状態になっている事が保証されている。


    return expected_row_num;

  },


  // トリガ開始時刻を表すDateオブジェクトを受け取り,
  // シート上で該当する日付の列が記載用に整っているかどうかを返す。
  checkDateColumnExistsOnSheet : function( trigger_start_dt ){

    // 記載されている事を期待すべき文字列
    const expected_date_label = this.transformDateObjToReadableLabel( trigger_start_dt );

    // 実際のセル上の文字列
    const real_date_label = this.getNewestDateLabel();
      my_debug_log( 
        "expected_date_label = " 
          + expected_date_label 
          + ", real_date_label = "
          + real_date_label
      );

    // 一致しない場合はNGとする
    if( expected_date_label != real_date_label ){

      my_debug_log( "図示シート上で記載位置の日付が整っていません。" );

      // 該当する日付の記載列が整っていない
      return false;

    }

    return true;

  },


  // 最新の日付ラベルのセルの記載内容を取得
  getNewestDateLabel : function(){

    const date_label = this.getVisSheet().getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY,
      COL_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY  
    ).getValue();
      my_debug_log( "図示シート上で最新の日付を取得。" + date_label );

    return date_label;

  },

  // 最新の日付ラベルのセルに,太字で文字列を記載
  setNewestDateLabel : function( s ){

    this.getVisSheet().getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY,
      COL_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY  
    )
    .setValue( s )
    .setFontWeight("bold") // 太字にする
    ;
      my_debug_log( "図示シート上に最新の日付を記入しました。" + s );

  },


  // Dateオブジェクトり受け取り,シート記載用に年月日系形式の文字列として整形
  transformDateObjToReadableLabel : function( date_obj ){

    // 年月日と曜日とする
    const date_label = Utilities.formatDate(
      new Date(), 
      "JST", 
      "yyyy-MM-dd (E)"
    );

    return date_label;

  },


  // シート上に該当日付の記載欄を列として新たに生み出し,必要なら古い日付の分をローテーション削除
  rotateDateFormatColumnOnSheet : function( trigger_start_dt ){

    // 空白列を生み出す
    this.getVisSheet().insertColumnsBefore(
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE, // 最新の日付の時間帯ラベルが記載されている列
      COLS_NUM_FOR_ONE_DAY_ON_TRIGGER_VISUALIZATION_SHEET // 1日分の列数
    );

    // トリガ開始の日付及び時間帯等のフォーマットを整える
    this.createNewDateFormatAndHourZonesFormatOnSheet( trigger_start_dt );
  
    // 必要なら,古いデータを削除する
    if( this.tooOldDataExistsOnSheet() ){
      this.deleteOldDataColumnsOnSheet();  
    }

    return;

  },


  // 削除すべき古すぎるデータのうち,最も日付が新しい物の日付欄の列番号を返す。
  getNewestDateColumnOfTooOldData : function(){

    // 削除すべき古すぎるデータの日付欄の列番号     
    const col_num_old_data = COL_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY // 最新の日付
      + (
        // 「最新の日付を含めて許容される日数」を1日分こえた日付の欄
        ( MAX_DATE_NUM_TRIGGER_VISUALIZATION_SHEET_TO_KEEP_OLD_DATA + 0 )
        *
        COLS_NUM_FOR_ONE_DAY_ON_TRIGGER_VISUALIZATION_SHEET // 1日分の列幅
      )
    ;

    return col_num_old_data;

  },


  // 古すぎる日付のデータがシート上に残ったままになっているか?
  tooOldDataExistsOnSheet : function(){

    // 削除すべき古すぎるデータの存在チェック列番号     
    const col_num_check_old_data = this.getNewestDateColumnOfTooOldData();

    // セル値を取得
    const old_date_label = this.getVisSheet().getRange(
      ROW_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY,
      col_num_check_old_data  
    ).getValue();
      my_debug_log(
        "図示シート上で古い日付の日付ラベルを取得:" 
        + old_date_label 
        + ", 行番号は"
        + ROW_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY
        + ", 列番号は"
        + col_num_check_old_data
      );

    // 有効な値が書き込まれている?
    if(
      ( old_date_label )
      && 
      ( old_date_label.length > 0 )
    ){
      
      my_debug_log(
        "古い日付のデータが残ったままです。列番号は" 
          + col_num_check_old_data 
          + ", 日付ラベルは"
          + old_date_label
      );

      // 残ったままになっているフラグを返す
      return true;

    }
    else
    {

      // 古いデータが存在しない場合
      return false;
    
    }

  },


  // 古い日付のデータを列ごと削除する。
  deleteOldDataColumnsOnSheet : function(){

    // 削除すべき古すぎるデータの日付が記載されている列番号     
    const col_num_old_data_start = this.getNewestDateColumnOfTooOldData();

    // そこから右に何日さかのぼれるか?
    let col_num_old_data_check = col_num_old_data_start;
    let continue_flag = true;
    let col_num_old_data_end = -1;
    while( continue_flag ){
      
      // シート上で右に1日分さかのぼる。(1日分の列幅を足している)
      col_num_old_data_check += COLS_NUM_FOR_ONE_DAY_ON_TRIGGER_VISUALIZATION_SHEET;

      // セル値を取得
      const check_date_label = this.getVisSheet().getRange(
        ROW_NUM_TRIGGER_VISUALIZATION_DATE_NEWEST_DAY,
        col_num_old_data_check  
      ).getValue();

      // 有効な値が書き込まれている?
      if(
        ( check_date_label )
        && 
        ( check_date_label.length > 0 )
      ){
        
        // さらに右側の検査も次ループで続ける
        continue_flag = true;

      }else{

        // これ以上日付データが無い場合
        // 現在の検査範囲の直前までを削除対象の列とする
        col_num_old_data_end = col_num_old_data_check - 2;

        // ループを終了する
        continue_flag = false;

      }

    } // whileの終わり

    // 何列分を削除すべきか
    const col_num_how_many_to_delete = col_num_old_data_end - col_num_old_data_start + 1;

    // 複数列を削除する
    this.getVisSheet().deleteColumns(
      col_num_old_data_start, // 削除開始列
      col_num_how_many_to_delete // 削除列数
    );
      my_debug_log(
        "古い日付情報を削除しました。削除開始列は" 
          + col_num_old_data_start 
          + ", 削除列数は"
          + col_num_how_many_to_delete
      );

    return;

  },


  // ----------- 下記は時間帯ラベルについて ----------


  // 1日分の時間帯ラベルや,その下部の統計情報のフォーマット等をシート上にセットする。
  createAllHourZoneFormatsOfOneDayOnSheet : function(){

    my_debug_log("図示シート上に,時間帯ラベルを記入します。");
  
    // シートを取得
    const vis_sheet = this.getVisSheet();


    // 設定値にある時間間隔にしたがって,時間帯ラベル列を記載する。
    if( NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT >= 1 ){

      // 刻み幅が1以上なら,while文が正常に終了できる保証となる。
      my_debug_log( "時間帯ラベルを記載します。刻み幅は" + NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT );

    }else{

      // while文が無限ループになるのを回避する
      my_err_log( "時間帯ラベルを記載できません。刻み幅は" + NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT );

      // ここで処理を打ち切り,while文に入らせない
      throw new Exception("トリガ実行結果の図示シート更新中のエラーです。");

    }

    // 0時からスタート
    let hour_num_start = 0;
    let row_num_hour = ROW_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE_START;
    let continue_flag = true;
    while( continue_flag ){
      
      // NOTE:
      // このwhileループは,時間帯を「起点:00~終点:59」の形式で行ごとに記録する。
      // 起点か終点のどちらかが24に到達したら,ループは完了する。
      // その結果,0:00から23:59までの時間が時間帯の刻み幅ごとに小分けになる。


      // 1日の中での1行分の時間帯ラベル等をシート上にセットする。
      const ret = this.createOneHourZoneFormatOnSheet({
        hour_num_start : hour_num_start,
        continue_flag  : continue_flag,
        vis_sheet      : vis_sheet,
        row_num_hour   : row_num_hour
      });
      // 継続可能フラグを更新 (終端時刻が24時に到達した場合などに継続ストップする)
      continue_flag = ret.continue_flag;


      // 次の行に進むための準備をする

      // 起点となる時刻を増加させる
      hour_num_start += NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT; // 設定した刻み幅の分だけ増える
      // 起点時刻が24時以降になってしまったら
      if( hour_num_start >= 24 ){

        // 今回でループを抜ける (※24時以降の開始分は,次の日の分として記録するから)
        continue_flag = false;

      }
      // 記載対象となる行番号を1だけ増加させる
      row_num_hour ++;

    } // whileループの終わり
    my_debug_log( "図示シート上に時間帯ラベルを記入しました。刻み幅は" + NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT );


    // 集計上の注意点をシート上に記録
    vis_sheet.getRange(
      row_num_hour,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH  
    ).setValue( "※仕事完了数と処理時間は正常終了したトリガのみ集計" );


    // 時間帯ラベルの下部に,仕事完了数に関する統計情報の表示フォーマットを整える
    const stat_formats_achievd_tasks = this.createDailyStatFormatOfAchievedTasksOnSheet({ blank_row_num : row_num_hour });


    // 時間帯ラベルの下部に,時間帯別のトリガ内処理時間数に関する統計情報の表示フォーマットを整える
    this.createDailyStatFormatOfConsumedTimesOnSheet({
      blank_row_num              : row_num_hour,
      stat_formats_achievd_tasks : stat_formats_achievd_tasks
    });


    return;

  },


  // 1日の中での1行分の時間帯ラベルや,シート関数を使った表示フォーマット等をシート上にセットする。
  createOneHourZoneFormatOnSheet : function( arg_obj ){
    
    // 引数を解釈
    const hour_num_start = arg_obj.hour_num_start;
    const continue_flag  = arg_obj.continue_flag;
    const vis_sheet      = arg_obj.vis_sheet;
    const row_num_hour   = arg_obj.row_num_hour;


    // 時間帯ラベルを構築する
    let hour_zone_str = "";

    // 起点となる時刻
    hour_zone_str = ""
      + hour_num_start
      + ":00~"
    ;

    // 終点となる時刻(のHH部分)
    let hour_num_end = -1;
    hour_num_end = hour_num_start
      + NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT // 刻み幅
      - 1 // 刻み幅だけ進んだあとで1分だけ戻るので
    ;
    // 終点が24時以降になってしまったら
    if( hour_num_end >= 24 ){

      // 23時台の末尾を終点とする
      hour_num_end = 23;
        // NOTE:
        // このコードの仕様として,トリガ開始が23時台であれば
        // たとえそのトリガの終了が日をまたいで24時以降になったとしても,
        // そのトリガ内の仕事完了数は23時台の分として記録・集計する。

      // 今回でループを抜ける
      continue_flag = false;

    }
    hour_zone_str = hour_zone_str
      + hour_num_end
      + ":59"
    ;

    // 1行分の時間帯ラベルをシート上に記録
    vis_sheet.getRange(
      row_num_hour,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE  
    ).setValue( hour_zone_str );


    // 1行分の仕事完了数グラフの関数をシート上に記録
    vis_sheet.getRange(
      row_num_hour,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH
    ).setFormula( // その時間帯の仕事完了数に応じた長さの棒グラフをアスキーアートで描画 
      '=IFERROR(REPT("'
        + MATERIAL_CHAR_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH // 棒グラフの素材文字
        + '",CEILING(' // floorではなくceilingにしておけば,その時間帯の仕事数が0から1に増えただけで1マス表示される
        + MAX_CHAR_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH // 棒グラフは最大で何文字か
        + '*'
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + row_num_hour
        + '/MAX('
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + ':'
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + '))),"")'
    );
      // NOTE:
      // ・上記のシート関数は,「列内の最大値で割る」という計算を含んでいる。
      //   0割りが発生した場合のエラー表示は,IFERROR関数によってシート上で抑制している。
      //   したがって,仕事数の列に0しか記入されていないような場合はセルに空白が描画される。
      // ・棒グラフ描画は,REPT関数で文字列を繰り返し表示している。
      //   こうやってシート関数を使用することによって,該当箇所をGASコードで書き換える必要がなくなる。


    // 1行分のトリガ内の処理時間を棒グラフで描画する関数をシートに記録
    vis_sheet.getRange(
      row_num_hour,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH
    ).setFormula( // その時間帯のトリガ内処理時間に応じた長さの棒グラフをアスキーアートで描画 
      '=IFERROR(REPT("'
        + MATERIAL_CHAR_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH // 棒グラフの素材文字
        + '",CEILING('
        + MAX_CHAR_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH // 棒グラフは最大で何文字か
        + '*'
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + row_num_hour
        + '/MAX('
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + ':'
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + '))),"")'
    );


    // この関数を呼び出したループに対し,ループ継続可否フラグを返す
    const ret = {
      continue_flag : continue_flag
    };
    return ret;

  },


  // 時間帯ラベルの下部に,仕事完了数に関する統計情報の表示フォーマットを整える
  createDailyStatFormatOfAchievedTasksOnSheet : function( arg_obj ){

    // 引数を解釈
    const blank_row_num = arg_obj.blank_row_num;
      // 時間帯ラベルを書き終えた直下の空白行の行番号を指す。
    my_debug_log( "図示シート上に仕事完了数の統計情報のフォーマットを作成します。空白行番号は" + blank_row_num );


    // シートを取得
    const vis_sheet = this.getVisSheet();

    // 何行何列目に記載してゆくか
    let current_row_index = blank_row_num + 2;
    const stat_col_index_achieved_tasks = COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS_AA_GRAPH;
    const stat_col_alphabet_achieved_tasks = COL_ALPHABET_ACHIEVED_TASKS_AA_GRAPH_ON_TRIGGER_VISUALIZATION_SHEET;


    // 日内の仕事完了数の合計
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "仕事完了数の合計は" );
    const row_index_achieved_tasks_today = current_row_index; // 行番号を保管
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      "=SUM("
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + ":"
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + ")"
    );
    current_row_index ++ ; // 1行あける


    // 日内のトリガ数の合計
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "トリガ数の合計は" );
    const row_index_total_triggers_today = current_row_index; // 行番号を保管
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      "=SUM("
        + COL_ALPHABET_TRIGGER_COUNT_ON_TRIGGER_VISUALIZATION_SHEET
        + ":"
        + COL_ALPHABET_TRIGGER_COUNT_ON_TRIGGER_VISUALIZATION_SHEET
        + ")"
    );
    current_row_index ++ ; // 1行あける


    // 1トリガあたりの仕事完了数の平均
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "1トリガあたりの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "仕事完了数の平均は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      '=IFERROR(ROUND('
        + stat_col_alphabet_achieved_tasks
        + row_index_achieved_tasks_today
        + '/'
        + stat_col_alphabet_achieved_tasks
        + row_index_total_triggers_today
        + ',1),"")'
    );
    current_row_index ++ ; // 1行あける


    // 時間帯数の合計
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "時間帯数の合計は" );
    const row_index_total_hour_zones_today = current_row_index; // 行番号を保管
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      "=COUNTA("
        + COL_ALPHABET_TRIGGER_COUNT_ON_TRIGGER_VISUALIZATION_SHEET
        + ":"
        + COL_ALPHABET_TRIGGER_COUNT_ON_TRIGGER_VISUALIZATION_SHEET
        + ")-1"
    );
    current_row_index ++ ; // 1行あける


    // 1つの時間帯あたりの平均トリガ数
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "1つの時間帯" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "あたりの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "平均トリガ数は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      '=IFERROR(ROUND('
        + stat_col_alphabet_achieved_tasks
        + row_index_total_triggers_today
        + '/'
        + stat_col_alphabet_achieved_tasks
        + row_index_total_hour_zones_today
        + ',1),"")'
    );
    current_row_index ++ ; // 1行あける


    // 1つの時間帯あたりの平均仕事完了数
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "1つの時間帯" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "あたりの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "平均仕事完了数は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      '=IFERROR(ROUND('
        + stat_col_alphabet_achieved_tasks
        + row_index_achieved_tasks_today
        + '/'
        + stat_col_alphabet_achieved_tasks
        + row_index_total_hour_zones_today
        + ',1),"")'
    );
    current_row_index ++ ; // 1行あける


    // 時間帯ごとの仕事完了数の標準偏差
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "時間帯ごとの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "仕事完了数の" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "標準偏差は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      '=IFERROR(ROUND(STDEV('
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + ':'
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + '),1),"")'
    );
    current_row_index ++ ; // 1行あける


    // 時間帯ごとの仕事完了数の分散
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "時間帯ごとの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "仕事完了数の" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setValue( "分散は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_achieved_tasks ).setFormula(
      '=IFERROR(ROUND(VAR('
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + ':'
        + COL_ALPHABET_ACHIEVED_TASKS_ON_TRIGGER_VISUALIZATION_SHEET
        + '),1),"")'
    );
    current_row_index ++ ; // 1行あける


    // 以降の処理で活用するための情報を引き渡す
    const stat_formats_achievd_tasks = {
      row_index_total_triggers_today   : row_index_total_triggers_today, // 全トリガ数は何行目か
      row_index_total_hour_zones_today : row_index_total_hour_zones_today, // 時間帯の個数は何行目か 
      stat_col_alphabet_achieved_tasks : stat_col_alphabet_achieved_tasks // 仕事完了数の統計情報の列名称アルファベット
    };

    return stat_formats_achievd_tasks;

  },


  // 時間帯ラベルの下部に,時間帯別のトリガ内処理時間数に関する統計情報の表示フォーマットを整える
  createDailyStatFormatOfConsumedTimesOnSheet : function( arg_obj ){

    // 引数を解釈
    const blank_row_num              = arg_obj.blank_row_num;
      // 時間帯ラベルを書き終えた直下の空白行の行番号を指す。
      my_debug_log( "図示シート上にトリガ内処理時間の統計情報のフォーマットを作成します。空白行番号は" + blank_row_num );
    const stat_formats_achievd_tasks = arg_obj.stat_formats_achievd_tasks;

    // 左隣(すでにシート上に関数を書き込み済み)の列から統計情報を参照する
    const row_index_total_triggers_today   = stat_formats_achievd_tasks.row_index_total_triggers_today;
    const row_index_total_hour_zones_today = stat_formats_achievd_tasks.row_index_total_hour_zones_today;
    const stat_col_alphabet_achieved_tasks = stat_formats_achievd_tasks.stat_col_alphabet_achieved_tasks;


    // シートを取得
    const vis_sheet = this.getVisSheet();

    // 何行何列目に記載してゆくか
    let current_row_index = blank_row_num + 2;
    const stat_col_index_consumed_times = COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_AA_GRAPH;
    const stat_col_alphabet_consumed_times = COL_ALPHABET_CONSUMED_TIMES_AA_GRAPH_ON_TRIGGER_VISUALIZATION_SHEET;


    // 日内のトリガ内処理時間の合計時間(秒形式)
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "トリガ内処理時間の" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "合計秒数は" );
    const row_index_total_seconds_today = current_row_index; // 行番号を保管
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setFormula(
      "=SUM("
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + ":"
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + ")"
    );
      // ※このセルの値はのちのち,別セルから計算のために呼び出したいので
      // セルの末尾に「秒」という単位を書き込まないでおく。
    current_row_index ++ ; // 1行あける

    
    // 日内のトリガ内処理時間の合計時間(分秒形式)
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "合計秒数を" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "分秒形式にすると" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setFormula(
      '=FLOOR('
        + stat_col_alphabet_consumed_times
        + row_index_total_seconds_today
        + '/60)&" 分 "&MOD('
        + stat_col_alphabet_consumed_times
        + row_index_total_seconds_today
        + ',60)&" 秒"'
    );
    current_row_index ++ ; // 1行あける

    
    // 1トリガあたりの平均時間の秒数
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "1トリガあたりの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "処理時間の" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "平均秒数は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setFormula(
      '=ROUND('
        + stat_col_alphabet_consumed_times
        + row_index_total_seconds_today
        + '/'
        + stat_col_alphabet_achieved_tasks
        + row_index_total_triggers_today
        + ',1)'
    );
    current_row_index ++ ; // 1行あける


    // 1つの時間帯あたりの平均時間(秒形式)
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "1つの時間帯" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "あたりの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "平均秒数は" );
    const row_index_average_seconds_per_one_hourzone = current_row_index; // 行番号を保管
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setFormula(
      '=ROUND('
        + stat_col_alphabet_consumed_times
        + row_index_total_seconds_today
        + '/'
        + stat_col_alphabet_achieved_tasks
        + row_index_total_hour_zones_today
        + ',1)'
    );
    current_row_index ++ ; // 1行あける


    // 1つの時間帯あたりの平均時間(分秒形式)
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "1つの時間帯" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "あたり平均秒数を" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "分秒形式にすると" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setFormula(
      '=FLOOR('
        + stat_col_alphabet_consumed_times
        + row_index_average_seconds_per_one_hourzone
        + '/60)&" 分 "&MOD(FLOOR('
        + stat_col_alphabet_consumed_times
        + row_index_average_seconds_per_one_hourzone
        + '),60)&" 秒"'
    );
    current_row_index ++ ; // 1行あける


    // 時間帯ごとのトリガ内秒数の標準偏差
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "時間帯ごとの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "トリガ内秒数の" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "標準偏差は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setFormula(
      '=IFERROR(ROUND(STDEV('
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + ':'
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + '),1),"")'
    );
    current_row_index ++ ; // 1行あける


    // 時間帯ごとのトリガ内秒数の分散
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "時間帯ごとの" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "トリガ内秒数の" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setValue( "分散は" );
    vis_sheet.getRange( current_row_index ++, stat_col_index_consumed_times ).setFormula(
      '=IFERROR(ROUND(VAR('
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + ':'
        + COL_ALPHABET_CONSUMED_TIME_ON_TRIGGER_VISUALIZATION_SHEET
        + '),1),"")'
    );
    current_row_index ++ ; // 1行あける


    return;

  },


  // トリガの実行開始時間に応じて時間帯を区分し,その情報を記入すべきシート上の行番号を求める
  getExpectedRowNumForOneTriggerResult : function( trigger_start_dt ){

    // Dateオブジェクトから時情報を取り出す
    const started_hour = trigger_start_dt.getHours();

    // 時情報を,刻み幅となる時間数で割る (0以上の整数となる)
    const expected_row_index = Math.floor( started_hour / NUM_TRIGGER_VISUALIZATION_HOUR_ZONE_UNIT );

    // 開始行からオフセットを数える
    expected_row_num = ROW_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE_START + expected_row_index;

    return expected_row_num;

  },


  // トリガ開始時刻を表すDateオブジェクトを受け取り,
  // シート上で該当する時間帯の行が記載用に整っているかどうかを返す。
  checkHourZoneRowExistsOnSheet : function( expected_row_num, trigger_start_dt ){
    
    // Dateオブジェクトから時情報を取り出す
    const trigger_start_hour = trigger_start_dt.getHours();


    // 指定された行の実際の時間帯ラベルを取得し,開始及び終了時間帯を割り出す
    const real_hour_zone_label = this.getVisSheet().getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_HOUR_ZONE  
    ).getValue();
    const zone_label_matches = real_hour_zone_label.match( /^(\d+):00[^0-9]*(\d+):59$/ ); // コロンより前の数値を2つ取り出す
    // 正規表現のマッチングに失敗?
    if( ! zone_label_matches ){
      
      my_err_log( "正規表現のマッチングに失敗。real_hour_zone_label=" + real_hour_zone_label );
      
      // 正しい時間帯ラベルが記載されていない。時間帯ラベルを作り直す必要がある
      my_debug_log( "図示シート上で記載位置の時間帯ラベルが整っていません。" );

      return false;

    }
    // 下記は正規表現のマッチングに成功した場合
    const zone_label_start_hour = parseInt( zone_label_matches[1], 10 );
    const zone_label_end_hour = parseInt( zone_label_matches[2], 10 );


    // 時間帯ラベルの範囲に収まっているか?    
    if(
      // 時間帯の開始時
      ( zone_label_start_hour <= trigger_start_hour )
      &&
      // 時間帯の終了時
      ( trigger_start_hour <= zone_label_end_hour )
    ){

      // 正しい時間帯ラベルが記載されている
      return true;

    }
    else
    {

      // 正しい時間帯ラベルが記載されていない。時間帯ラベルを作り直す必要がある
      my_debug_log( "図示シート上で記載位置の時間帯ラベルが整っていません。" );

      return false;

    }

    return false;

  },
 

  // ----------- 下記は図示情報の追記・更新操作について ----------


  // 完了した仕事数,トリガ内の経過時間数,およびトリガ数などをシート上に追記する1回分の操作。
  // ただし,本日の欄は列としてすでに存在しており,
  // また,今回の時間帯の欄は行番号として記入すべき位置に存在しているものとする。
  recordOneTriggerResultInfosAtExpectedPosition : function( arg_obj ){

    // 引数を解釈
    const expected_row_num                   = arg_obj.expected_row_num;
    const achieved_user_tasks_one_trigger    = arg_obj.achieved_user_tasks_one_trigger;
    const consumed_time_millisec_one_trigger = arg_obj.consumed_time_millisec_one_trigger;
    const finished_triggers_count            = arg_obj.finished_triggers_count;
      my_debug_log(
        "図示シート上で,第"
          + expected_row_num
          + "行に"
          + achieved_user_tasks_one_trigger
          + "件の仕事完了を追記します。"
          + "今回の経過時間は"
          + consumed_time_millisec_one_trigger
      );
    

    // シートを取得
    const vis_sheet = this.getVisSheet();

    // 完了した仕事数をシート上に追記
    this.recordAchievedUserTasksAtExpectedPosition({
      expected_row_num                : expected_row_num,
      achieved_user_tasks_one_trigger : achieved_user_tasks_one_trigger,
      vis_sheet                       : vis_sheet
    });

    // トリガ数をシート上に追記
    this.recordTriggerCountAtExpectedPosition({
      expected_row_num        : expected_row_num,
      finished_triggers_count : finished_triggers_count,
      vis_sheet               : vis_sheet
    });

    // トリガ内の経過時間をシート上に記録
    this.recordTriggerConsumedTimeAtExpectedPosition({
      expected_row_num                   : expected_row_num,
      consumed_time_millisec_one_trigger : consumed_time_millisec_one_trigger,
      vis_sheet                          : vis_sheet
    });


    return;

  },


  // 完了した仕事数をシート上に追記する1回分の操作。
  recordAchievedUserTasksAtExpectedPosition : function( arg_obj ){

    // 引数を解釈
    const expected_row_num                = arg_obj.expected_row_num;
    const achieved_user_tasks_one_trigger = arg_obj.achieved_user_tasks_one_trigger;
    const vis_sheet                       = arg_obj.vis_sheet;


    // シートから値を取得
    const current_achieved_tasks = vis_sheet.getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS  
    ).getValue();

    // 0以上の有効な値か?
    let new_achieved_tasks = -1;
    if( current_achieved_tasks >= 0 ){

      // 既存の値に1トリガ分を加算する
      new_achieved_tasks = current_achieved_tasks + achieved_user_tasks_one_trigger;

    }else{

      // 既存の値が有効な値でなかった場合は,0と記載されていたとみなす
      new_achieved_tasks = 0 + achieved_user_tasks_one_trigger;

    }

    // シート上に記録
    vis_sheet.getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ACHIEVED_TASKS  
    ).setValue( new_achieved_tasks );
      my_debug_log( "図示シート上で仕事完了数を加算しました。加算結果:" + new_achieved_tasks );

    return;

  },


  // 完了したトリガ数をシート上に追記する1回分の操作。
  recordTriggerCountAtExpectedPosition : function( arg_obj ){

    // 引数を解釈
    const expected_row_num        = arg_obj.expected_row_num;
    const finished_triggers_count = arg_obj.finished_triggers_count
    const vis_sheet               = arg_obj.vis_sheet;


    // シートから値を取得
    const current_triggers_count = vis_sheet.getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_TRIGGERS_COUNT  
    ).getValue();

    // 0以上の有効な値か?
    let new_triggers_count = -1;
    if( current_triggers_count >= 0 ){

      // 既存の値に1トリガ分を加算する
      new_triggers_count = current_triggers_count + finished_triggers_count;

    }else{

      // 既存の値が有効な値でなかった場合は,0と記載されていたとみなす
      new_triggers_count = 0 + finished_triggers_count;

    }

    // シート上に記録
    vis_sheet.getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_TRIGGERS_COUNT  
    ).setValue( new_triggers_count );
      my_debug_log( "図示シート上でトリガ起動確立数を加算しました。加算結果:" + new_triggers_count );

    return;

  },


  // トリガ内で経過した時間をシート上に追記する1回分の操作。
  recordTriggerConsumedTimeAtExpectedPosition : function( arg_obj ){

    // 引数を解釈
    const expected_row_num                   = arg_obj.expected_row_num;
    const consumed_time_millisec_one_trigger = arg_obj.consumed_time_millisec_one_trigger
    const vis_sheet                          = arg_obj.vis_sheet;


    // 加算したい分をミリ秒から秒単位に直す
    const consumed_time_seconds_one_trigger = Math.ceil( consumed_time_millisec_one_trigger / 1000 );

    // シートから既存の値を整数値として取得
    let current_consumed_time = vis_sheet.getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_SECONDS  
    ).getValue();

    // 0以上の有効な値か?
    let new_consumed_time = -1;
    if( current_consumed_time >= 0 ){

      // 既存の値に1トリガ分を加算する
      new_consumed_time = current_consumed_time + consumed_time_seconds_one_trigger;

    }else{

      // 既存の値が有効な値でなかった場合は,0と記載されていたとみなす
      new_consumed_time = 0 + consumed_time_seconds_one_trigger;

    }

    // シート上に記録
    vis_sheet.getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_CONSUMED_TIME_SECONDS  
    ).setValue( new_consumed_time );
      my_debug_log( "図示シート上でトリガ内消費時間を加算しました。加算結果:" + new_consumed_time );

    return;

  },


  // 1回分のエラー数をシート上に追記
  // (※これはトリガ完了時にまとめて呼び出されるのではない。エラー発生のたびに随時呼び出される。)
  recordOneErrorInfoAtExpectedPosition : function( arg_obj ){
  
    // 引数を解釈
    const expected_row_num = arg_obj.expected_row_num;
    const error_count      = arg_obj.error_count;

    // シートから値を取得
    const current_error_count = this.getVisSheet().getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ERRORS_COUNT  
    ).getValue();

    // 0以上の有効な値か?
    let new_error_count = -1;
    if( current_error_count >= 0 ){

      // 既存の値に1回分のエラー数を加算する
      new_error_count = current_error_count + error_count;

    }else{

      // 既存の値が有効な値でなかった場合は,0と記載されていたとみなす
      new_error_count = 0 + error_count;

    }

    // シート上に記録
    this.getVisSheet().getRange(
      expected_row_num,
      COL_NUM_TRIGGER_VISUALIZATION_SHEET_ERRORS_COUNT  
    ).setValue( new_error_count );
      my_debug_log( "図示シート上でエラー数を加算しました。加算結果:" + new_error_count );

    return;

  },


  // シート上で予期した通りの場所に記入が可能な状態になっているか?
  // 列および行の観点からチェックする。
  checkPositionOnSheetReadyForRecording : function( expected_row_num, trigger_start_dt ){

    // 本日の欄は列としてすでにあるか?
    if( ! this.checkDateColumnExistsOnSheet( trigger_start_dt ) ){

      // 「シート上で予期した通りの場所に記入が可能な状態」ではない。
      return false;

    }

    // 今回の時間帯の欄は,行として記入すべき位置に存在しているか?(時間帯の刻み幅が変わってしまっていないかなど)
    if( ! this.checkHourZoneRowExistsOnSheet( expected_row_num, trigger_start_dt ) ){

      // 「シート上で予期した通りの場所に記入が可能な状態」ではない。
      return false;

    }

    // 「シート上で予期した通りの場所に記入が可能な状態」になっている。
    return true;

  }
 

};


//
//  "GAS Trigger Resource Manager" (略称: GTRM)
//  のソースコードは以上です。
//


/*

  ◆補足情報1: ライブラリ化について。

  上記のコードをライブラリ化すべきかどうか?
  現時点ではその必要はないと考えます。下記資料を参考に。


  Google Apps Scriptライブラリの作り方 (2021年)
  https://qiita.com/shikumiya_tech/items/0aed6d0c67ee365d9161#-global
  ・ライブラリ内に記載したコードは,ライブラリ外から参照可能になるために
   「super global領域へのひもづけ」が必要となる。(this.プロパティ名 構文を使用)
   ライブラリ内のclass定義,const変数,let変数などが対象。

  
  GAS(Google Apps Script)のライブラリーコードを隠蔽する (2024年)
  https://qiita.com/minmin68/items/a2bdcfdd961d7715dd86#1-3-%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%83%BC%E3%81%A8%E3%81%97%E3%81%A6%E5%A4%96%E9%83%A8%E3%81%AB%E5%85%AC%E9%96%8B
  ・GASコードをデプロイする際,公開範囲を「リンクを知っている全員」とすることで
    一般に利用可能なライブラリーとして外部に公開される。
    この際,権限を「閲覧者」とすることで「利用できるが変更はできない」という権限になる。
    共同編集者などに設定しないよう注意。
  ・CryptoJSを使ってライブラリー内のコードを暗号化する方法もある。


  知らないと怖い、GASのセキュリティリスク。情シスが押さえるべきポイント (2025年)
  https://www.yoshidumi.co.jp/collaboration-lab/gas-security-risks-it-admin-guide#toc-4
  ・「外部ライブラリと依存関係のリスク:
    Googleの公式ドキュメントにおいても、外部ライブラリの読み込みは極力避けることが推奨されています。」


  Google Apps Script用のライブラリを作ろう  作成や導入上の注意点 (2026年)
  https://note.com/gicloud/n/n714f4a8517e8#6f5351c4-5657-4b5a-8120-5d41bd13c130
  ・(ライブラリを使うと)「ネイティブで書いているコードよりも、プログラムの実行速度が遅くなります。」


  Google Apps Script の公開(閲覧権限でどこまで公開される?) (2022年の情報)
  https://zenn.dev/hankei6km/scraps/88b6ab63fa6307
  ・「ロールが閲覧者でもトリガーを設定できる。実行するデプロイに HEAD を選択できる。」
  ・「思っていたよりも公開されてしまう部分:
    以下の点が予想していたよりも公開されてしまうと感じた。
    閲覧者が作成したトリガーでも HEAD としてプッシュされているコードを利用できる。
    これは開発中のものをうかつにプッシュできないということになる。」
  
 */


/*

  ◆補足情報2: 技術選定,設計思想,および制御方式について。

  本フレームワークを開発するにあたっての技術選定の根拠や経過を記録しておきます。
  バッチ処理を実現するためのプラットフォームとして,なぜあえてGoogleスプレッドシート(GAS)を選んだのか。


  「GoogleスプレッドシートでBlueskyに投稿する,という暫定的な方針」
  https://posfie.com/@ouen_suru_tan/p/iT3HTHp?page=6
   2026年に,X(旧Twitter)上で無料でのbot開発が全面的に廃止され,移行先が必要となりました。
  Xに自動ツイート投稿するようなツールとして,これまではBotbirdやTwittbotがありましたが,同じように
  X以外のSNSプラットフォーム(例:Bluesky)向けにも自動投稿するような類似ツールを今後,誰かが生み出してゆくかもしれません。
  それほどまでに,「X以外のSNSプラットフォームへの無料bot作成」というニーズが世界的に生じるだろうと思いました。
   そうすると,わざわざ自前でVPSやサーバを立てドメインを取り,PHPとかでWebアプリを作り込んで…という手間をかけているうちに
  自分がやろうとしていたことを,他の人が先に実現してしまう可能性もあります。
  その場合,botプラットフォーム立ち上げのために自分がかけた費用は丸ごと無駄になってしまうかもしれません。
   だとすると,方針を少し変えて,「費用を無駄にしないため,無料で実現できないだろうか?」というコンセプト案が生じてきます。
  RDBMS上にbotデータを保管するのではなく,Googleスプレッドシート上にbotデータを列挙・保管すれば費用はタダです。
  また,LinuxのレンタルサーバーやVPSを有償契約してCRONで定期処理するのではなく,GASのトリガを使えばやはりタダです。
  そのようにGASを使って,「無償でbotプラットフォームを誰もが自分用に保有・運用できる」という状況を作り出しておけば,
  今後どのような競合および類似ツールがWebアプリの形で世の中に現れたとしても,独自の価値を維持できるのでは…と思いました。
    bot運営者はよく,自身の保有するbotのつぶやきデータをExcelなどのスプレッドシート上に保管しています。
  そのため,クラウド上の無料スプレッドシートにbotデータを保管する方針というのは,既存のbot運営者たちにとって受け入れやすい事です。
  このような経緯で,「X以外のSNSでbotを無料で作成・運用するための手段」としてGoogleスプレッドシート(GAS)を選ぶことになり,
  またGASで「1日のあらゆる時間帯に均一のペースでbot投稿できるようなリソース管理ツール」が必要となったため,
  本フレームワーク(GTRM)が生み出されました。
  「bot投稿API操作」のロジックと「GASの生身のトリガ機能」の間を橋渡しする中間的なレイヤがGTRMだという位置づけです。
  とはいえ,本フレームワークはSNSへのbot投稿と関係なくとも,ほかのあらゆる用途のためにも汎用的に使用できます。


  「Googleスプレッドシートにbotを本気で移植してゆきましょう。トリガ機能を使用する」
  https://posfie.com/@ouen_suru_tan/p/wxlcpz6?page=3#h51_0
  ・「SNSにbot投稿するプラットフォーム」をGASで開発するために,1トリガあたり6分の枠を気にしなければいけない理由は何かというと,
    botアカウントの個数が1つや2つではなく,非常に大量のアカウント数を想定しているからです。(※多言語の語学など勉強用のbotです。)
    botアカウント数が多いので,1トリガぶんの制限時間である6分以内に所望の処理を全て行ないきることは必ずしも期待できず,
    むしろ次回のトリガ起動時に向けていつでも処理を途中で中断・再開できるような仕組みが必要です。
  ・また,1日の制限時間である90分を24時間に対して均等に割り当てて,1日のうちのどの時間帯にも
    いつもだいたい同じようなペースでbotが(たとえ細々とでも)稼働し続けるような「定常性」も実現する必要があります。
    その事について上記URLからの引用文:
    「botというのは,feedを途切れさせないことが重要なのであって,特定の時間帯だけ動かないというのは困る。
    1日のどの時間帯をとっても,平均的に一定したtimelineのflow(流量)を確保する必要がある。
    これは,水道水とかを供給するインフラ施設の考え方にも似ている。」
    この結果,90分という上限を24時間に均等に割り当てると90/24=3.75つまり3分45秒,すなわち225秒が1時間あたりMAXとなります。
    このペースを毎時間,24時間ずっと維持するような仕組みが必要です。
  ・加えて,1トリガ内の時間をフル活用するので,あるていど長い処理時間を見越しておく場合,
    今回のトリガ内での処理と次回のトリガ内の処理とが時間的にかち合ってしまわないように排他制御(ロック機構)が必要です。
  ・ScriptPropertiesは使用しません。それを使うとシステムの挙動が隠ぺいされ,シート上で目視で確認できません。
    むしろ,今起きている事や現在のシステムの状態を,シート上で見て確認・リアルタイム把握できるようにします。
    そして万一,必要ならシート上で手動でセル値を書き換えて,システムの挙動を制御できるようにしておきます。
    そのために「トリガ制御用」というシートを設けてあります。


  『botシステムが「24時間動き続ける」ようにするため,本気で設計してゆく』
  https://posfie.com/@ouen_suru_tan/p/wxlcpz6?page=4#h89_0
  ・1日の合計の処理時間が90分を超えてはいけませんが,逆に90分を大幅に下回るのも機会損失となります。
    それで,「安全余裕を考慮しつつ,可能な限り90分という資源(リソース)を毎日使い切る」ように最適化します。
    なお,その90分という上限もコード内で設定可能にしてあり,複数プロジェクトを同時で走らせる場合は
    90分の代わりに半分の45分にする…などの調節が可能です。
  ・リソースを24時間ごとに「均等に割り振る」とは言っても,完全な均等ではなく,時間帯ごとに自分で増減可能にしておきます。
    時間帯ごとに「安全係数」を調節可能にしておき,たくさん動作する時間帯もあれば,動作を減らす時間帯も作れるという事です。
  ・制御の方式として,1日の中で何もしなくても時計の針が進んでゆき,これを1日の経過時間と呼ぶことにします。
   理想的なのは,1日の経過時間が0時から24時に増えてゆく際に,それと同じペースでトリガ内消費分数が0分から90分へ増えてゆくことです。
    そうすると,「1日の経過時間が0時から24時に増えるペース」と「トリガ内消費分数が0分から90分へ増えるペース」
    という2つの値を同時に監視することになり,この2つの値をそれぞれ分母,分子にとった比を
    「ダイジェストレート」(消化率)と呼ぶことにしています。
    このダイジェストレートが1(=100%)よりもちょっと少ない状態をいつも保つことが,効率よく安定的で安全なバッチ処理の秘訣です。
    安全係数を加味したうえでダイジェストレートが100%を超えた場合,トリガ内で処理を打ち切るべきタイミングの合図となります。
    そのような「トリガ内で処理を打ち切るべきタイミングが来ているか?」を関数1個で即,計算・回答してくれるのが
    本フレームワークというわけです。そのサンプルコードとして,
    forループの中で MyTriggerManager.isCurrentTriggerRemainingTimeSafe() を呼んでいる箇所をご参照ください。


  「1日に利用可能な無料枠を,いつからいつまでカウントするのか」
  https://posfie.com/@ouen_suru_tan/p/wxlcpz6?page=7#h165_0
  ・GASの1日の処理時間の上限が90分とはいうものの,その分のカウントはいつリセットされるのか?  
    日本時間の0時にいっせいにリセットされるわけではありません(0時から24時までで90分,という数え方ではない)。
    さらに,「トリガ内残り時間を知るためのGASの公式API」は用意されていません。
    しかしGAS利用者の側としては,1日のうちでどこかの時点で「ここからここまでの時間内で90分」という枠の目安が必要です。
    それで,本フレームワークでは「0時から24時までで90分」という仮の数え方や集計方法をすることにしています。

 */

ちなみに6千行ぐらいあります。
このフレームワークの開発にかかった期間は,1か月ほどです。

どうしてこのようなフレームワークが必要になったのか,経緯などについては下記をご参照ください。

X(旧Twitter)上で,もう無料でbot作成できない。2026年2~3月にAPI完全有料化。価格は100ツイート1ドル。10ドル分クレジットを消費し次第botつぶやきを強制停止。有力な移行先・代替手段はBlueskyか
https://posfie.com/@ouen_suru_tan/p/iT3HTHp

作業ログ:【パート2】「X(旧Twitter)上で,もう無料でbot作成できない」の件の続き。Blueskyへのbot移行に向けGASで開発作業を進めるも,次々にアカウントが停められてゆく…
https://posfie.com/@ouen_suru_tan/p/iT3HTHp

たった100行で,Blueskyにbot投稿するGASサンプルコード (GoogleスプレッドシートからブルースカイAPIを無料で使う方法を理解する)
https://qiita.com/rwanda_go_tan/items/c2a28e21b9db3ec05004

たった200行で,GASのトリガ重複実行を検出・回避し,シート末尾に定期的にログ記録するサンプルコード (排他制御しつつ,Googleスプレッドシート内でデータが存在する最終行に情報記録)
https://qiita.com/rwanda_go_tan/items/e6a8bae04fdd2d1ba9a6

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?