質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.46%
Google カレンダー

Google カレンダーは、Google社が提供する無料のスケジュール管理ツールです。パソコンやスマートフォン、タブレットなどからアクセスし、スケジュールの追加・変更が可能。Googleアカウントがあれば誰でも使用できます。

Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

1回答

1510閲覧

GAS Googleカレンダーのイベント時間を区間別に集計

msmll0

総合スコア1

Google カレンダー

Google カレンダーは、Google社が提供する無料のスケジュール管理ツールです。パソコンやスマートフォン、タブレットなどからアクセスし、スケジュールの追加・変更が可能。Googleアカウントがあれば誰でも使用できます。

Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

0グッド

3クリップ

投稿2021/11/19 15:30

前提・実現したいこと

独学で勉強を始めたばかりで、コードが読みづらかったらすみません。
GASでGoogleカレンダーの一定期間内のイベントをすべて取得し、
時間を区間別6:00~8:00 8:00~18:00 18:00~22:00 22:00~6:00に集計させるシステムを作っています。

下記コードが、イベント一つひとつを設定時間で区切り、配列を組みなおす部分です。
main()startTime endTimeは取り出した一つのイベントの開始時刻、終了時刻です。

ご質問は、

  • separateTime()の簡略化
  • getFirst()の一般化(現在3日以上の長さのイベントが考慮されていない)
  • もっと簡単な集計方法 or 考え方はないか?

です。
お知恵をお貸しいただけたら幸いです。
よろしくお願いいたします。

GAS

1function main() { 2 const startTime = new Date('2021/11/19 23:00') 3 const endTime = new Date('2021/11/20 7:00') 4 5 Logger.log(separateTime(startTime, endTime)) 6 // {blocks=[[Fri Nov 19 23:00:00 GMT+09:00 2021, Sat Nov 20 06:00:00 GMT+09:00 2021], [Sat Nov 20 06:00:00 GMT+09:00 2021, Sat Nov 20 07:00:00 GMT+09:00 2021]], 7 // diff=[7.0, 1.0], 8 // index=4.0} 9}

GAS

1// sepの時間で分割する 2function separateTime(startTime, endTime) { 3 const separatePoints = getFirst(startTime); 4 const sep = separatePoints[0]; 5 const index = separatePoints[1]; 6 7 8 if (!(sep < endTime)) { 9 return { 10 blocks : [[startTime, endTime]], 11 diff : [(endTime - startTime) / 1000 / 3600], 12 index : index 13 }; 14 } 15 16 const sep2 = getFirst(sep); 17 if (!(sep2 < endTime)) { 18 return { 19 blocks : [[startTime, sep], [sep, endTime]], 20 diff : [(sep - startTime) / 1000 / 3600, (endTime - sep) / 1000 / 3600], 21 index : index 22 }; 23 } 24 25 const sep3 = getFirst(sep2); 26 if (!(sep3 < endTime)) { 27 return { 28 blocks : [[startTime, sep], [sep, sep2], [sep2, endTime]], 29 diff : [(sep - startTime) / 1000 / 3600, (sep2 - sep) / 1000 / 3600, (endTime - sep2) / 1000 / 3600], 30 index : index 31 }; 32 } 33 34 const sep4 = getFirst(sep3); 35 if (!(sep4 < endTime)) { 36 return { 37 blocks : [[startTime, sep], [sep, sep2], [sep2, sep3], [sep3, endTime]], 38 diff : [(sep - startTime) / 1000 / 3600, (sep2 - sep) / 1000 / 3600, (sep3 - sep2) / 1000 / 3600, (endTime - sep3) / 1000 / 3600], 39 index : index 40 }; 41 } 42 43 const sep5 = getFirst(sep4); 44 if (!(sep5 < endTime)) { 45 return { 46 blocks : [[startTime, sep], [sep, sep2], [sep2, sep3], [sep3, sep4], [sep4, endTime]], 47 diff : [(sep - startTime) / 1000 / 3600, (sep2 - sep) / 1000 / 3600, (sep3 - sep2) / 1000 / 3600, (sep4 - sep3) / 1000 / 3600, (endTime - sep4) / 1000 / 3600], 48 index : index 49 }; 50 } 51 52 const sep6 = getFirst(sep5); 53 if (!(sep6 < endTime)) { 54 return { 55 blocks : [[startTime, sep], [sep, sep2], [sep2, sep3], [sep3, sep4], [sep4, sep5], [sep5, endTime]], 56 diff : [(sep - startTime) / 1000 / 3600, (sep2 - sep) / 1000 / 3600, (sep3 - sep2) / 1000 / 3600, (sep4 - sep3) / 1000 / 3600, (sep5 - sep4) / 1000 / 3600, (endTime - sep5) / 1000 / 3600], 57 index : index 58 }; 59 } 60 61 const sep7 = getFirst(sep6); 62 if (!(sep7 < endTime)) { 63 return { 64 blocks : [[startTime, sep], [sep, sep2], [sep2, sep3], [sep3, sep4], [sep4, sep5], [sep5, sep6], [sep6, endTime]], 65 diff : [(sep - startTime) / 1000 / 3600, (sep2 - sep) / 1000 / 3600, (sep3 - sep2) / 1000 / 3600, (sep4 - sep3) / 1000 / 3600, (sep5 - sep4) / 1000 / 3600, (sep6 - sep5) / 1000 / 3600, (endTime - sep6) / 1000 / 3600], 66 index : index 67 }; 68 } 69 70 return { 71 blocks : [[startTime, sep], [sep, sep2], [sep2, sep3], [sep3, sep4], [sep4, sep5], [sep5, sep6], [sep6, sep7], [sep7, endTime]], 72 diff : [(sep - startTime) / 1000 / 3600, (sep2 - sep) / 1000 / 3600, (sep3 - sep2) / 1000 / 3600, (sep4 - sep3) / 1000 / 3600, (sep5 - sep4) / 1000 / 3600, (sep6 - sep5) / 1000 / 3600, (sep7 - sep6) / 1000 / 3600, (endTime - sep7) / 1000 / 3600], 73 index : index 74 }; 75}

GAS

1// startTimeに合わせて初期値を取得 2function getFirst(startTime) { 3 const point = ['6:00', '8:00', '18:00', '22:00', '30:00', '32:00', '42:00', '46:00']; 4 5 const start = new Date(startTime); 6 const d = []; 7 8 // startTime以降の最初の時間を取得 9 for (let i of point) { 10 start.setHours(i.match(/.*:/)[0].replace(':', '')); 11 start.setMinutes(i.match(/:.*/)[0].replace(':', '')); 12 d.push(Math.floor((start - startTime) / 1000 / 3600)); 13 } 14 15 const index = d.findIndex(function(element){ 16 return element > 0 && function (a, b) {return Math.min(a, b);}; 17 }); 18 19 const ret = new Date(startTime); 20 ret.setHours(point[index].match(/.*:/)[0].replace(':', '')); 21 ret.setMinutes(point[index].match(/:.*/)[0].replace(':', '')); 22 23 return [ret, index]; 24}

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2021/11/19 22:37 編集

たとえば、main()の中の日時を const startTime = new Date('2021/11/19 3:00') const endTime = new Date('2021/11/19 19:00') に変えて実行したとき、blocksは [[03:00,06:00],[06:00,19:00]]  (日付は省略し、時刻はわかりやすい表記にしています) となりますが、これは期待する動作なのでしょうか? 文面からすると [[03:00,06:00],[06:00,08:00],[08:00,18:00],[18:00,19:00]] となるのが実際に期待する動作である、という理解でよろしいでしょうか?
msmll0

2021/11/19 23:07

ほんとだ..気づかなかったです。 おっしゃる通りです。 下の配列が期待する戻り値です。
guest

回答1

0

ベストアンサー

・考え方
1日の区切り点を持った定規のようなものがあり、
この定規を前後に並べた無限の長さのタイムラインのようなものがあるとみなします。

イメージ説明

そのタイムライン上に、分割対象となるスケジュール(target)がのっかっているとみなします。

これを最終的には下の図の2番目のような形に分割したいということになります。

イメージ説明

分割手順は下記のようになります。

1.targetの開始時刻に一番近いタイムライン(定規の左端の部分)をスタート地点にセットします。
便宜上、この部分の時刻を「edge」(端)と呼ぶことにします。
イメージ説明

2.このedgeを、各区切り時間の分だけ加算していき、その都度edgeとtargetの開始時刻を比較します。
(「edgeを後ろに伸ばしていく」)

「edgeが示す時刻 ≦ targetの開始時刻」である間は、まだ分割を開始せず、次の区切り時間を加算してedgeを右に進めるだけです。
イメージ説明

3.「edgeが示す時刻>targetの開始時刻」 となったら分割処理を開始します。
イメージ説明

edgeを区切り時間分だけ進めながら、その都度edgeのところで分割し、分割した時刻データを格納していきます。

 
4.edgeを進めていき、「edgeが示す時刻 ≧ targetの終了時刻」になったら、最後のデータを格納して分割を終了します。
イメージ説明


参考ソースコードは以下になります。

js

1 2class BreakPointsRuler { 3 4 /** 5 * BreakPointsRulerのインスタンスを生成します. 6 * BreakPointsRulerは、区切り時刻間隔のテーブル `intervals`と 7 * スケジュールの分割基準となる日付時刻 `edge` を保持します。 8 * 9 * @param {Array[Date]} points 区切り時刻(日付オブジェクト)の配列 10 * @param {Date} startDate 分割対象スケジュールの開始時刻 11 * @memberof BreakPointsRuler 12 */ 13 constructor(points, startDate) { 14 15 /** 16 * 分割基準となる現時点の区切り時刻を表します。 17 * getNextEdge()を呼び出すと、内部に保持されている区切り 18 * 時間間隔が加算された値に変わります。 19 * @type {Date} 20 */ 21 this.edge = this.getNearestEdge(points, startDate); 22 23 /** 24 * 区切り時刻の時間間隔の配列です。 25 * @type {Array[Date]} 26 */ 27 this.intervals = this.getIntervals(points); 28 29 /** 30 * intervals内の要素を循環して返すためのインデックスカウンターです。 31 * @type {number} 32 */ 33 this.counter = 0; 34 } 35 36 /** 37 * 現在のedgeを返す関数です。 38 * @return {Date} 現在のedge 39 * @memberof BreakPointsRuler 40 */ 41 getEdge() { 42 return new Date(this.edge.getTime()); 43 } 44 45 /** 46 * 指定された区切り時刻配列の最初の要素を、 47 * 指定されたスケジュールイベントの開始日時を越えない 48 * 最も近い日付に変換して返します。 49 * @param {Array[Date]} points 区切り時刻の配列 50 * @param {Date} startDate 分割対象スケジュールの開始時刻 51 * @return {Date} イベントの開始時刻に最も近い日付時刻 52 * @memberof BreakPointsRuler 53 */ 54 getNearestEdge(points, startDate) { 55 const nearestEdge = new Date(); 56 // スケジュールの開始時刻が、 指定された区切り時刻よりも早い場合は 57 // 開始地点を1日前に設定する。 58 if (startDate.getHours() < points[0].getHours()) { 59 nearestEdge.setDate(startDate.getDate() - 1); 60 } else { 61 nearestEdge.setDate(startDate.getDate()); 62 } 63 64 // 時/分/秒を区切り時刻と同じ値に設定する。ミリ秒はゼロに初期化する。 65 nearestEdge.setHours( 66 points[0].getHours(), 67 points[0].getMinutes(), 68 points[0].getSeconds(), 69 0 70 ); 71 72 return nearestEdge; 73 } 74 75 /** 76 * 指定された区切り時刻配列の各要素の 77 * 間隔(ミリ秒)の配列を返します。 78 * 79 * 与えられる区切り時刻の配列は昇順でなければなりません。 80 * 最後の要素は、最初の要素に24時間を足した時刻と元データ 81 * の最後の要素の時刻の差になります。 82 * 83 * 配列内の要素の合計は86400000(24時間*60分*60秒*1000)となります。 84 * 85 * 例)["1:00","9:00","21:00"] 86 * -> [3600000, 28800000, 43200000, 14400000] 87 * 88 * @param {Array[Date]} points 区切り時刻の配列 89 * @return {Array[number]} 指定された区切り時刻配列の各要素の間隔(ミリ秒) 90 * @memberof BreakPointsRuler 91 */ 92 getIntervals(points) { 93 //最初の区切り時刻を1日加算して、翌日の区切り時刻を作っておく。 94 const next = new Date(points[0].getTime()); 95 next.setDate(points[0].getDate() + 1); 96 97 // 前後の時間差(秒数)を計算し配列に格納 98 const intervals = points.map((e, idx, ary) => { 99 if (idx === points.length - 1) 100 // 翌日の1番目のpointと当日の最後のpointの時間差 101 return next.getTime() - ary[idx].getTime(); 102 else 103 // 次のpointとの時間差 104 return ary[idx + 1].getTime() - e.getTime(); 105 }) 106 return intervals; 107 } 108 109 110 /** 111 * 現在のedgeが示す日付時刻に次の区切り時刻までの時間間隔を加算した 112 * 日付時刻を返す関数です。 113 * 114 * この関数を実行すると、このクラスの内部変数edgeが更新されるとともに、 115 * 更新されたedgeと同じ日付時刻を持つ新しい日付オブジェクトが返されます。 116 * 117 * 加算する時間間隔は、この関数を呼び出すごとに順繰りに変わります。 118 * 119 * @return {Date} 現在のedgeに、次の区切り時間間隔を加算した日付時刻 120 * @memberof BreakPointsRuler 121 */ 122 getNextEdge() { 123 // edgeに区切り時間を加算 124 this.edge = new Date(this.edge.getTime() + this.intervals[this.counter]); 125 // カウンターをインクリメント。 126 // ただし配列長に一致した場合はゼロに戻す。 127 this.counter++; 128 if (this.counter % this.intervals.length === 0) this.counter = 0; 129 130 return new Date(this.edge.getTime()); 131 } 132 133} 134 135/** 136 * 1日内の区切り時刻を表す文字列(HH:mm形式)の配列を 137 * 日付時刻の配列に変換します。 138 * 呼び出し例) 139 * convertPoints(["01:00","09:00","21:00"]) 140 * @param {Array[string]} points 1日内の区切りを表す文字列(HH:mm形式)の配列 141 * @return {Array[Date]} pointsに対応する日付時刻の配列 142 */ 143function convertPoints(points) { 144 return points.map( 145 point => new Date( 146 2021,1,1, 147 parseInt(point.match(/.*:/)[0].replace(':', '')), 148 parseInt(point.match(/:.*/)[0].replace(':', '')), 149 0,0 150 ) 151 ); 152} 153 154function main() { 155 const startTime = new Date('2021/11/19 03:00') 156 const endTime = new Date('2021/11/19 19:00') 157 158 const point = ['6:00', '8:00', '18:00', '22:00']; 159 160 // pointの要素を日付時刻型に変換 161 const points = convertPoints(point); 162 163 const ruler = new BreakPointsRuler(points, startTime) 164 165 let edge = ruler.getEdge(); 166 167 let start = new Date(startTime); 168 169 // 結果格納用の配列 170 const result = []; 171 172 // 2.「edgeが示す時刻 ≦ targetの開始時刻」である間は、 173 // まだ分割を開始せず、次の区切り時間を加算してedgeを右に進めるだけ。 174 while (edge.getTime() <= startTime.getTime()) { 175 edge = ruler.getNextEdge(); 176 } 177 178 // 3.「edgeが示す時刻>targetの開始時刻」 となったら分割処理を開始。 179 180 // edgeが示す時刻<targetの終了時刻の間、通常の分割処理を行う 181 while (edge.getTime() < endTime.getTime()) { 182 // 分割した時刻の端点をresultに格納 183 result.push([start, edge]); 184 185 // startを現在のedgeの時刻に更新する。 186 start = new Date(edge); 187 188 // edgeを区切り時間分だけ進める。 189 edge = ruler.getNextEdge(); 190 } 191 192 // 4.edgeが示す時刻≧targetの終了時刻になったら 193 // 最後のピースを格納して分割を終了 194 result.push([start, endTime]); 195 196 // 結果の表示 197 Logger.log(result.map(e => 198 Utilities.formatDate(e[0],"Asia/Tokyo","MM/dd HH:mm:ss")+ "~" 199 + Utilities.formatDate(e[1],"Asia/Tokyo","MM/dd HH:mm:ss")).join('\n')); 200} 201

#【補足】

現在3日以上の長さのイベントが考慮されていない

御記載の通り、元質問文では、イベントの長さに対応した固定の配列を用意しておき
for文でその配列の要素を順番に呼び出して、スケジュールを分割しようとされています。

js

1 const point = ['6:00', '8:00', '18:00', '22:00', '30:00', '32:00', '42:00', '46:00']; 23 // startTime以降の最初の時間を取得 4 for (let i of point) { 5(以下略)

しかし、無限の長さの固定配列を用意する必要はありません。

区切り時刻のデータは1日分の繰り返しになっているので、24時間ごとに同じ間隔が繰り返されます。

text

1 ['6:00', '8:00', '18:00', '22:00'] 2 3=> 区切り時間の間隔は、「2時間、10時間、4時間、8時間・・・」の繰り返し 4

したがって、1つの区切り時刻のデータから、順番に時間間隔を取り出せる仕組みがあればよいということになります。

これを実現しているのが、BreakPointsRulerクラス内のgetNextEdge()関数になります。

js

1 2 this.counter = 0; 3(略) 4 getNextEdge() { 5 // edgeに区切り時間を加算 6 this.edge = new Date(this.edge.getTime() + this.intervals[this.counter]); 7 // カウンターをインクリメント。 8 // ただし配列長に一致した場合はゼロに戻す。 9 this.counter++; 10 if (this.counter % this.intervals.length === 0) this.counter = 0; 11 12 return new Date(this.edge.getTime()); 13 }

getIntervals関数にて、区切り時刻のデータ ['6:00', '8:00', '18:00', '22:00']を
あらかじめ、時間間隔の配列intervalsに変換して保持しておきます。

上記の例だと

6:00 -> 8:00 : 2h = 7200000 ms (ミリ秒)
8:00 -> 18:00 : 10h = 36000000 ms
18:00 -> 22:00 : 4h = 14400000 ms
22:00 -> 6:00 : 8h = 28800000 ms

以上より、

intervals = [7200000,36000000,14400000,28800000]

に変換されます。

edge += interval[counter] とすることで、edgeを各時間間隔分だけ動かすことができます。

オブジェクト内で counterを保持しておき、getNextEdge()を呼び出すごとに
counter(インデックス) を1加算します。

counter が intervals配列の最大値に達したら、counter をゼロにします。

このようにすることで、たとえばintervalsの要素数が4つの場合、
counterは、0 -> 1 -> 2 -> 3 -> 0 -> 1 -> 2 -> 3...
と無限に繰り返すことになり、順番に区切り時刻の間隔を呼び出せます。

(他にはジェネレータを使う方法もあります)

投稿2021/11/20 08:25

編集2021/11/20 10:00
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

msmll0

2021/11/20 11:19

図までご用意いただいて丁寧にご教授くださり、誠にありがとうございます。 考え方・補足共に初心者でも理解しやすく、感謝の念に堪えません。 特に、 ・2.このedgeを、各区切り時間の分だけ加算していき、その都度edgeとtargetの開始時刻を比較します。 (「edgeを後ろに伸ばしていく」) ・1つの区切り時刻のデータから、順番に時間間隔を取り出せる仕組みがあればよい この2つの考え方とその実装には衝撃が走りました。 時間を点(定刻)ではなく線(間隔)として捉え、それをつなげる(加算する)ことで無限(一般化)を表現できるのですね。 また、オブジェクト指向は今まで便利そうだと思っても設計方法や使い方が分からずすべて配列でごり押ししていました。 オブジェクト指向はより汎用性に富む考え方なのですね。簡単に見やすい仕組みも作れますし.. このご回答により、考え方の幅が倍以上に伸びた所感です。 今後はオブジェクト指向を意識して、頂いた考え方を幅広く実現させられる技術を得ることを目的に経験を積みたいと思います。 ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.46%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問