見出し画像

Googleスプレッドシート GASを使って、あしたへジャンプ! (GASで日付を扱うポイント他)

前回の XMATCH 超応用例 から派生した小ネタです。

ちなみに「あしたへジャンプ」は 1990年代前半に NHKの教育テレビで放送されていた  小学校高学年向けの道徳ドラマです。

ただ単に 今日以降のセルにジャンプするという動作のお題から、思い出した(思いついた)だけなので、今回の記事とは一切関係ありません。そして年代がバレるww

前回の平日更新記事



スプレッドシートで 直近の予定の日付に飛びたい

前回は関数での解決策を検証した、こちらのあるあるなお題。

今回は、GAS(GoogleAppsScript)で シートを開いた時、もしくはボタンを押したら 直近の予定の行(セル)へ飛べるようにしたい!

を解決する GASを検証したいと思います。



「今日の日付へ飛びたい」は GASをお勧めしない

ただ、前回も書いた通り 本来この処理は GASはお勧めしません!

  1. GAS(Javascript)は日付の扱いがちょっと面倒

  2. スマホ(アプリ版)で利用できない

  3. GAS の activateは 省エネな動き

理由としては この3つをあげました。

唯一解決できない スマホ利用問題は除外するとして、今回はあえて「お勧めしない」GASに手をだしてみましょう。

女子に「先生~、〇〇くんが GASを使おうとしています」って言われそうで、まさに道徳番組でありそうな展開ですねw



GASを実行して A列の直近の予定の日付にジャンプしたい

今日(画像の当日)は 2/21

前提条件としては以下の通り

・A列の A4以降に 2023年の日付が 1月から 昇順で入ってる
・途中にA列が空白の行はない
・同じ日付に複数の依頼(複数行に連続して同じ日付)というケースもある
・依頼がない日付は 入っていない (とばされている)
・直近の予定日とは 今日以降の 一番近い日付の予定を指す。
 (今日に2/21 が存在しない場合は 今日から一番近い日付)

・GASはボタンから、もしくは スプレッドシートを開いたら実行したい
・できれば ジャンプ先のセルが一番上に表示されるようにしたい

上の画像の動きは 最後の要件 「ジャンプ先のセルが一番上に表示されるようにしたい」が満たせてませんが、まずはこれを作っていきましょう。

今回はハマりやすいポイントも抑えていきます!



GASで日付を扱う

早速コードを書いてもいいんですが、せっかくなんで 面倒なGASにおける日付の扱いを少し解説しておきましょう。

GASでの日付の扱いは、基本的には Javascriptと一緒ですが、一部 GASならではのメソッドもあります。



new Date() は シート関数でいうところの NOW()

今日の日付を得る為には

new Date()

と記述します。

中身をログに出力して確認すると

このように 、この関数が実行された 日付けと時間が返っているのがわかります。暦の表記が英語仕様でわかりづらいですが、

2023年2月26日 日曜日 14:29:59

となってます。

つまり、ここで得られる 日付は今日という日付ではなく、〇時〇分〇秒という情報も含む 今の日時 ってことです。(ただしくは 日付オブジェクトです)

シート関数でいうと、TODAY() ではなく NOW() と考えると わかりやすいかも。 

ハマりやすいPoint1
new Date() は 今日ではなく、今(時刻含む)である

もし、ここでログに出力された時間が ズレている、もしくは 「Japan Standard Time」となっていない場合は、このプロジェクト(GAS)のタイムゾーン設定が日本以外になっている可能性があります。

プロジェクトの設定(歯車アイコン)から、タイムゾーンを確認して、日本時間になっていない場合は修正しましょう。

ハマりやすいPoint2
GASのタイムゾーンはスプレッドシートと別。ズレてたら要確認



Utilties.formatDate で日付の表示形式を整える

これは Javascriptにはなくて GASオリジナル です。

GASには日付オブジェクトを文字列に変換する専用のユーティリティメソッドが用意されてます。

これはシート関数だと (日時専用の)TEXT関数みたいな感じですかね。
日付を自分の欲しい表示形式にできます。

formatDate(date, timeZone, format)

注意点
・date は 日付オブジェクトを入れる必要がある(日付文字列は✖)
・timeZone は日本の場合は "JST" または "Asia/Tokyo" を指定
・format で 月は M 時間は m と区分けされてる。大文字小文字に注意

今回、日付のみをスラッシュ区切りで取得するために

Utilities.formatDate(date,"Asia/Tokyo","yyyy/MM/dd");

このようにしましたが、結果として得られる 2023/02/26 は文字列 Stringという扱いです。今日の日付の Dateオブジェクトではありません。

ハマりやすいPoint3
Utilties.formatDateでの M(月)とm(分)は別モノ
yyyy/mm/dd は誤り。yyyy/MM/dd とする。



今日の 「日付」だけが欲しい時

今回は、後の処理で使う用に Dateオブジェクトが欲しいのですが、

new Date() ・・・現在時刻を含む 今を表す 日付オブジェクト
Utilties.formatDate 後 ・・・ 文字列

となると、どうすればいいのか?

シート関数ならシリアル値という考え方があるので INT関数で小数部分カットという技が使えるんですがw

実はUtilities.formatDateで 日付 のみにした文字列を 再度 new Date() に与えることで 本日の日付のみの Dateオブジェクトを生成できます。

function myFunction() {
  let date = Utilities.formatDate(new Date(),"Asia/Tokyo","yyyy/MM/dd");
  const today = new Date(date);
  
  console.log(today);
}

これは、Utilites.formatDateが使える GASならではの書き方で、Javascriptの場合は、

  let today = new Date();
  today = new Date(today.getFullYear(),today.getMonth(),today.getDate());

こんな書き方をすることが多いかと思います。

上のように new Date() は 日付っぽい文字列をカッコ内に入れる以外に、カンマ区切りで 年、月、日 を数値で入れて 日付オブジェクトを生成できます。

これは、シート関数の DATE(年,月,日) っぽいですね。

ただし、直接 new Date() に数値を入れる場合は注意点があります。
ここも月が注意ポイントなんですが、

2023年2月27日に実施。

このように 2023年2月27日 に 本日の日付オブジェクトから

today.getMonth()

で月の数値を得ると 2月なのに 1と表示されます。

逆に 2023年2月27日 の日付オブジェクトを得ようと

new Date(2023,2,27)

とすると、Mar(マーチ)つまり、 2023年3月27日 となってしまいます。

これは Javascript の 月の数値 は 0から11までと、日本人的感覚からすると-1ズレているからです。

0が January ・・・1月
11が December ・・・ 12月

うーん、わかりづらい。

ハマりやすいPoint4
Javascript・GAS の 月 は 0スタート
getMonth() した 月の数値 +1が 〇月と覚える

とりあえず、本日の日付のみの Dateオブジェクトが作れました。



getValue(getValues)で取得した セルの日付は Dateオブジェクトになってる

それでは スプレッドシートのセルに入っている 日付データは、同じように getValueで取得した後、 new Date() に渡して日付オブジェクトにすればよいのか?

実はそんな必要はなく、セルの日付はそのままで Dateオブジェクトになっています。

このように 「今日の日付」の方の型は strig、2023/2/26 の方の型は object となっているのがわかります。

要は取得してそのまんま日付オブジェクトとして使えるってことです。



余談:セルの日付を文字列として使いたい場合は getDisplayValueが便利

余談ですが、取得した日付を Gmailの本文に使ったり、他の文字列と連結させたり、文字列として比較して一致を判定したい場合は、getValueで Dateオブジェクトになってしまうと不便だったりします。

そんな時は getDisplayValue(getDisplayValues) で取得すると便利です。

getDisplayValueは シート上で 表示形式を反映させた 表示そのままを 文字列として取得できるメソッドです。

特に日付を 文字列として取得したい時は便利です。

ハマりやすいPoint5
スプレッドシートから getValue(s)で取得した日付は 日付オブジェクト
日付を 文字列として扱いたい場合は getDisplayValue(s) が便利。


日付オブジェクトは 一致は判定できないが 大小比較はできる

function myFunction() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const values = sheet.getRange(1,1,1,2).getValues();

  console.log(values[0][0] == values[0][1]); //false
  console.log(values[0][0] <= values[0][1]); //true
  console.log(values[0][0] >= values[0][1]); //true
}

同じ日付を表す 日付オブジェクト同士を 等価演算子 == で一致するか判定しようとしても false となってしまいます。つまり日付オブジェクトは、そのままでは 一致を判定できないってことです。

日付同士の一致を判定したい場合は、Utilites.formatDate で文字列にしたり、最初から getDisplayValueで取得し 文字列同士で判定するか、getTime()などのメソッドで ミリ秒(数値)にして 判定する必要があります。

一方 < 、 <= 、> 、 >= といった比較演算子は使えます。意外かもしれませんが、日付オブジェクトのまま 大小の比較判定は 出来るってことです。

わざわざ比較する際に ミリ秒に変換する必要はありません。

ハマりやすいPoint6
日付オブジェクト は、そのままでは一致は判定できない。
比較演算子による 大小の比較は 日付オブジェクト のまま判定できる。

今回のお題が 「本日以降の直近の日付」なので、比較演算子による処理が使えそうですよね?

これでGASにおける「日付」の基本理解と準備は整いました。



Javascript における XMATCHの上位互換 検索メソッド findIndex

Googleスプレッドシートの 最強関数の一つ FILTER関数は、なぜ検索・抽出において 圧倒的に便利なのか?

もちろん配列処理し、複数の結果を配列で返せるという点も魅力ですが、検索条件に 式が使えるという点も大きいんじゃないでしょうか?

FILTER関数は 条件式が TRUEを返した要素を抽出するという動きをします。

XLOOKUPやXMATCHは、ワイルドカードや近似値一致など単純一致以外の検索もできる 便利な検索関数ではありますが、

もし、XLOOKUPや XMATCH の一致条件が 条件式で書けたら?

って思うことがあります。

これできたら XLOOUP、XMATCHは 真の最強検索関数なんだけどなー。

実は GAS(Javascript)の配列メソッドには、このXMATCH の一致条件が条件式で書けたら? という、あんなこといいな、出来たらいいなを実現する ドラえもんの秘密道具のような強力な検索メソッドがあります。

それが findIndex() です。

説明をみると 

findIndex() メソッドは、配列内の指定されたテスト関数に合格する最初の要素の位置を返します。

とあります。まさに今回の「本日以降の直近の日付」の位置(行番号)を得るのに最適なメソッドですね。

function sample(){
  const array = [
    ["田中",23],
    ["山田",26],
    ["鈴木",28],
    ["木内",30],
    ["鮫島",33],
    ["宮内",37],
    ["三田",42]
    ]; //6人の名前と年齢

    //31歳以上を探す
    const index = array.findIndex(v => v[1] >= 31); 

    console.log(index); //4 ・・・ 0スタートなので 5番目の鮫島

}

Googleスプレッドシートで使えそうな サンプルコードはこんな感じ。

スプレッドシートからgetValuesで取得したデータを想定して、名前と年齢が 入った 二次元配列で 31歳以上の年齢 という条件を満たす一人目の位置を取得しています。

二次元配列なので 各要素 v (配列)の 2つ目 (0始まりなので v[1])を取り出して比較しています。

ハマりやすいPoint7
スプレッドシートから getValuesで取得したデータは 二次元配列
二次元配列に対して 配列メソッドを使う場合は、個々の要素が配列であることに注意

そして結果は 0始まりの配列の indexなので、 4ということは人間目線では +1して 5番目であるという点も注意です。

ハマりやすいPoint8
配列は 0はじまり
なので、行番号として使う場合は +1する必要がある

Array.prototype (配列メソッド)は、アロー関数でシンプルに書けるとクールです。何度も使って慣れましょう。

日付の扱いを踏まえ、findIndexを使って 「直近の予定の日付に飛ぶ」コードを書いてみましょう。



A列の 直近の予定の日付にジャンプするコード

function toNextSchedule() {
  const sheet = SpreadsheetApp.getActiveSheet(); //開いているシート

  const date = Utilities.formatDate(new Date(),"Asia/Tokyo","yyyy/MM/dd");
  const today = new Date(date); //今日の日付

  const range = sheet.getRange("A:A");
  const days = range.getValues(); 

  const index = days.findIndex(day => day[0] >= today);
  sheet.getRange(index+1,1).activate(); //行番号なので配列の位置に +1
}

上で 日付の扱いと findIndex には触れたので、解説は不要ですよね? 面倒なんで A列を値を全部ぶっこ抜きしてます。

お目当てのセルにジャンプする処理は、最後の

 sheet.getRange(index+1,1).activate()

ここです。activate メソッドを使います。

思ったよりは シンプルなコードじゃないでしょうか?



GASの動きを確認

ボタンに割りあてて動かしてみましょう。

ボタンを作成し、GASの関数を割り当てる流れは スピンボタンの回で丁寧めに書いています。

今日 2/26の日付が A列にはないので 直近の日付である2/27 がアクティブになりましたね。

ただし、前回も触れましたが GASの省エネな動き、アクティブなセルが画面の一番下で終了してしまう問題 が残っています。

ハマりやすいPoint9
GASの activate() は、見つかった段階(画面にギリ表示されるの アクティブなセルが 表示画面の 下、もしくは右端)で止まる。
気を利かせて左上をアクティブにしてはくれない。

コードを加筆して、これを解決しましょう!



スプレッドシートで直近の予定の日付に飛びたい(アクティブセルが左上に表示される状態にしたい)

この問題を解決するに、上に 16行表示されてるから 本来の行番号 + 16して、16行下をアクティブにすれば・・・なんてことを考える人がいますが、完全に見誤ってます。

Gmailで ワードの 送り状みたいな 日付を右に寄せたり、レイアウトに凝りすぎた本文を スペースキーで作って、相手の環境で見たらガタガタってパターンと一緒です。

そもそも 直近じゃない日がアクティブになったら意味ないですし、パソコンのモニターやその人が使ってる表示倍率によって 1画面に何行表示されるかは変わってきます

じゃあどうするか?

上から下へという動きで アクティブセルが見つかった(画面に入った)段階で止まるので一番下に表示されるなら、表の一番下から 上へという動きにすれば、アクティブセルが見つかった(画面に入った)段階で止まると 一番上に表示されるのでは?

って考えるとよさげじゃないでしょうか?

この動きは GASを使わずに検証できます。やってみましょう!



下から上へ activate をテストしてみる

GAS実行前に A1アクティブ状態から手動Ctrl + ↓ を何度か押して、一番したの A1000セルをアクティブにし 表示させた状態とします。

この状態で GASを実行すると、予想通り 見つかった セル (今回の場合は 2/27)が左上の状態で アクティブとなりました。

このGAS実行前の準備を コードに追記すればいけそうですね!



シートの最終行を取得する

では、GASでアクティブなシートの最終行を取得するにはどうすればよいか?

最終行と言えば、 getLastRow()というメソッドがありますが。

でも、実はこれ sheet(シート) に対して使う時と、range(セル範囲)に対して使う時で挙動が違います。

sheet.getLastRow()
シートのデータが存在する最終行を返す

range.getLastRow()
指定したセル範囲内の最終行を返す

今回の場合は123行目までデータがある

シートに対して getLastRowを使った場合は、そのシートのデータが存在する最終行の行番号、つまり getDataRangeの最終行番号を返します。

一方、sheet.getRange でシートからセル範囲を取得した場合は、そのセル範囲の最終行の行番号を返します。

A:Aとした場合はシートの最終行(今回の場合は1000)となりますし、A10:A100とすれば最終行は100となります。

ハマりやすいPoint10
GASの getLastRow()は、対象によって挙動が違う。

シートを対象とした場合は データが存在する最終行を返す。
・range(セル範囲)
を対象とした場合は、範囲内の最終行を返す。
※範囲を A:A と列指定した場合は、シートの最終行を返す

今回は シートの最終行をまずはアクティブにしたいので、列指定したrangeに対して getLastRow() で良さそうです。



そこまでの処理を反映させる魔法の言葉 flush

flash(閃光のフラッシュ) じゃなくて flush(あふれ出る)という意味の方の単語です。

シートの最終行を1度アクティブにしてから、ヒットした 今日以降の直近の日付をアクティブにする「下から 上」の動きをさせる為に、

このように12行目に 

sheet.getRange(range.getLastRow(),1).activate();

としただけではうまくいきません。

あれれーおかしいぞー?、じゃあちょっと待てばよいのかな?と、

Utilities.sleep() というミリ秒 処理を待たせる方法を使ってもダメです。

GASは処理負荷を軽減する為か、コードの処理が全て終わってからでないと画面上反映されない という特徴があります。

これを知らないと、ここでかなりハマります。

例えば GASで 数式を入れて その計算結果を getValue() しようとしても、
計算結果が取得出来なかったり、setValue()で色々入力してから スプレッドシートをPDF化したはずなのに、入れたはずの値が入ってなかったり。

こんなお悩みは、ほぼ全て SpreadsheetApp.flush() で解決します。

SpreadsheetApp.flush()

と書いて使います。

これを入れることで、「ここまでの処理を反映させる」ことができます。今回の場合は、最終行をアクティブにした状態を反映させて、その後 今日以降の直帰の日付に飛べば 良いですね!

ハマりやすいPoint11
GASの処理は途中では反映されていない。
そこまでの処理を一旦 反映させたい時は
SpreadsheetApp.flush()

ドキュメントやスライドだとこれに該当するメソッド名がバラバラなのよね・・・

最後に 完成版のコードと 検証です。



A列の 直近の予定の日付にジャンプし (上部)に表示するコード

function toNextSchedule() {
  const sheet = SpreadsheetApp.getActiveSheet(); //開いているシート

  const date = Utilities.formatDate(new Date(),"Asia/Tokyo","yyyy/MM/dd");
  const today = new Date(date); //今日の日付

  const range = sheet.getRange("A:A");
  const days = range.getValues();

  const index = days.findIndex(day => day[0] >= today);

  sheet.getRange(range.getLastRow(),1).activate(); //一度最終行をアクティブに
  SpreadsheetApp.flush(); //画面更新

  sheet.getRange(index+1,1).activate(); //行番号なので配列の位置に +1
}
3/2収録

1度 一番下の行を表示してから、直近の日付がアクティブになる動きが 目で追えるので、なんかパッパッといった2段階の動きに若干違和感はあるものの、とりあえず 完成しました。

もちろん この関数名を onOpen() にすれば、開いた時に直帰の日付がアクティブ(左上)になるシートとなります。



次回は また関数の単発ネタを

前回、XMATCH関数 の近似値一致 大 と HYPERLINK関数で 実装した 

「A列の今日以降の直帰の予定に飛ぶ」

という処理を 今回は GASで実装してみました。

どうでしょうか、別にGAS使わなくてもよくない?って思いませんか?


個人的には 関数・数式、標準機能で出来ることは、そっちで処理して GASは最後の手段って考えなんで、これは自分では使わないかなー。

でも、GASでの日付の扱い、使い方によっては便利な getDisplayValue、結構好きな findIndex、処理を反映させる flush と、この辺りを説明するのに良い題材だったと思います。

次回の平日更新は、また関数小ネタを予定。





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