GASで月末日付を計算する方法と注意点

GASで月末日付を計算する方法と注意点

GASで月末日付を計算する方法を紹介します。

GASで日付を扱う場合、いくつか罠がありますので、その罠に、はまらないように気を付けましょう。

標準のDateオブジェクトを使って月末日を取得する

まずは、GAS(JavaScript)標準のDateオブジェクトを使って月末日を取得する方法を2つ紹介します。

ただし、標準のDateオブジェクトを使う方法は、どちらも罠が多いため、あまりおすすめはしません。

基本的には、このページの後半に紹介する「Day.js」を使って加工する方法をおすすめします

ただ、ライブラリを使いたくないなどの理由で、どうしても標準のDateオブジェクトで処理をしたい、という場合には「方法1」を使うことをおすすめします。

方法1:年・月を指定して新しいDateオブジェクトを作る

function endOfMonthTest1() {
  //元の日付
  const d = new Date('2020/01/30');

  //「新たに」月末日のDateオブジェクトを作成
  const d_endOfMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0);

  //月末日をスプレッドシートに出力
  SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue(d_endOfMonth);
}

6行目の右辺で、元々のDateオブジェクトの「年」「月」を使って、月末日を計算しています。

そのとき、日付部分(3つ目の引数)を「0」にしているのがポイントです。「0」を指定すると、「1日の1日前」=「前月末日」の日付を計算することができます

この方法を使うと、変数「d1」、変数「d1_endOfMonth」には、別々のDateオブジェクトが格納されます。そのため、「d1_endOfMonth」の内容を変えても「d1」は連動して変わらず、安心して使えます。

方法2:Dateオブジェクトに「翌月0日」をセットする

function endOfMonthTest2() {
  //元の日付
  const d = new Date('2020/01/30');

  //「元のDateオブジェクト」を「翌月0日」に修正
  d.setMonth(d.getMonth() + 1, 0);

  //月末日をスプレッドシートに出力
  SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue(d);
}

Dateオブジェクトの「setMonth」メソッドを使うと、元々のDateオブジェクトの月と日を変えることができます。

上記プログラムでは、6行目で「setMonth」メソッドを使って、変数dに格納されたDateオブジェクトを「翌月0日」の日付になるように修正しています。

なお、この「方法2」は、気を付けて使わないと誤動作をすることがあります。この方法を使うことはおすすめしませんし、この方法を使わざるを得ない場合には、細心の注意を払いましょう

ダメな例1:元の変数が変わる

function endOfMonthTest2_2() {
  //元の日付
  const d = new Date('2020/01/30');

  //月末日付格納用の変数を準備し、元の変数をコピー
  const d_endOfMonth = d;

  //「元のDateオブジェクト」も「翌月0日」に変更される
  d_endOfMonth.setMonth(d.getMonth() + 1, 0);

  //当初の変数「d」の値は?
  SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue(d);
}

元の日付とは、別の変数に月末日付を格納するために、6行目で変数「d_endOfMonth」を作り、変数「d」の内容をコピーしています。

その後、変数「d_endOfMonth」で、月末日付を計算しています。

一見、これで意図通りに動きそうですが、実際に動かしてみると、元々の変数「d」も、月末の日付に変わってしまいます

原因は「6行目」です。

  //月末日付格納用の変数を準備し、元の変数をコピー
  const d_endOfMonth = d;

今回のように、単に変数を代入すると、変数「d」と変数「d_endOfMonth」は、同じDateオブジェクトが変数に格納された状態になります

この状態で、9行目のように変数「d_endOfMonth」を操作すると、変数「d」の内容も連動して変わってしまうことに注意が必要です。

  //「元のDateオブジェクト」も「翌月0日」に変更される
  d_endOfMonth.setMonth(d.getMonth() + 1, 0);

ダメな例2:setMonth、setDateを別々に実行する

function endOfMonthTest2_3() {
  //元の日付
  const d = new Date('2020/01/30');

  //「元のDateオブジェクト」を「2020/2/30」(=「2020/3/1」)に変更
  d.setMonth(d.getMonth() + 1);

  //「元のDateオブジェクト」を「2020/3/0」(=「2020/2/29」)に変更
  d.setDate(0);

  //月末日をスプレッドシートに出力?
  SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue(d);
}

「方法2」を少し変えて、「setMonth」「setDate」の各メソッドで、月・日を別々に指定しています。

実は、このようにしてしまうと、誤動作の原因となります。

実際、このプログラムを動かしてみると「2020/02/29」と表示され、月末日が正しく計算できていないことがわかります。

うまく動かない原因は、「setMonth」メソッドの挙動にあります。

月以外は、既存のDateオブジェクトの値が使われる
  //「元のDateオブジェクト」を「2020/2/30」(=「2020/3/1」)に変更
  d.setMonth(d.getMonth() + 1);

「setMonth」メソッドで、「月」だけを指定した場合、「日」の部分は、元のDateオブジェクトの値がそのまま使われます。

今回の例であれば、元々、「日」には「30」が指定されていたので、それがそのまま使われます。つまり、元々の日付が「2020/1/30」だったので、その「月」の部分を「2」に変えた「2020/2/30」の日付データが作成されます。

存在しない日付を指定すると、自動的に補正がかかる
  //「元のDateオブジェクト」を「2020/3/0」(=「2020/2/29」)に変更
  d.setDate(0);

そして、このような存在しない日付が指定された場合、自動的に補正がかかります。その結果、「2020/2/30」は「2020/2/29」の1日後ということで、「2020/3/1」のデータが作成されてしまうのです。

日付を「0」にすると「2020/2/29」のデータになってしまう

その後、「setDate」メソッドを実行すると、日付部分が「0」になります。このメソッドを実行する直前の、変数dの値は「2020/3/1」ですから「2020/3/0」の日付を指定したことになります。

これも、存在しない日付ですので、自動的に日付の補正がかかり、「2020/3/1」の1日前である「2020/2/29」の日付データができあがります。

このようにして、本来は「2020/1/31」の日付データが欲しかったのに、実際には「2020/2/29」の日付データができあがってしまいました。

「Day.js」を使って月末日を取得する

JavaScript標準のDateオブジェクトを使って月末日付を計算しようとすると、様々な罠にはまりがちです。

ですから、日付関連の処理を書くときには、基本的には、日付処理用ライブラリを使うことをおすすめします。

ここでは、GASで簡単に使える日付処理ライブラリ「Day.js」を使って、処理を書いていきます。

GASでDay.jsを使う方法は、下記をご覧ください↓

方法3:1か月後の0日の日付を求める

function dayjsTest() {
  //元の日付
  const d = new Date('2020/01/30');

  //元のDateオブジェクトの月末日付を計算
  const d_endOfMonth = dayjs.dayjs(d) //Day.jsオブジェクトを作成
    .add(1, 'month')                  //1か月後の日付「2020/2/29」を作成
    .date(0)                          //0日の日付「2020/2/0」(=「2020/1/31」)を作成
    .toDate();                        //Dateオブジェクトに戻す

  //月末日をスプレッドシートに出力
  SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue(d_endOfMonth);
}

いったん、DateオブジェクトからDayjsオブジェクトを作成し、日付データを加工した後に、最後にDateオブジェクトに戻す処理をしています。

「日付データを加工」する処理では、「2020/1/30の1か月後の日付」を作成し、「日の部分を0にする」ことで、「2020/1/31」の日付を計算しています。

実は、この考え方は、さきほど紹介した「方法2:Dateオブジェクトに「翌月0日」をセットする」の「ダメな例2:setMonth、setDateを別々に実行する」と、ほとんど同じです。なぜ、Dateオブジェクトではうまくいかなかったのに、Day.jsを使う方法でうまくいくのでしょうか?

それは、Day.jsの「add」メソッドの挙動が、Dateオブジェクトの「month」メソッドと違うからです。実は、Day.jsで「add」メソッドで「2020/1/30」の1か月後の日付を計算すると「2020/2/29」の日付が得られます

あとは、日付の部分を「0」にすることで、「2020/2/0」→「2020/1/31」の日付が得られるのです。

方法4:endOfメソッドを使って月末日付を計算する

function endOfMonthTest4() {
  //元の日付
  const d = new Date('2020/01/30');

  //元のDateオブジェクトの月末日付を計算
  const d_endOfMonth = dayjs.dayjs(d) //Day.jsオブジェクトを作成
    .endOf('month')                   //月末日付「2020/1/31 23:59:59.999」を作成
    .millisecond(0)                   //秒未満の端数を0にする「2020/1/31 23:59:59.000」
    .toDate();                        //Dateオブジェクトに戻す

  //月末日をスプレッドシートに出力
  SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue(d_endOfMonth);
}

Day.jsには、endOfメソッドがあり、それを使うと、月末日付を直接計算することができます。

ただし、endOfメソッドを使うと、時間以下が自動的に「23:59:59.999」に設定されてしまうことに注意してください。

上のプログラムでは、millisecondメソッドを使って秒未満を「000」に修正したうえで、Dateオブジェクトを作成しています。

こうすることで、スプレッドシートに「2020/1/31 23:59:59」の日付データとして出力することができます。

秒未満の端数に注意

この方法を使うときには、秒未満の端数に注意してください。

たとえば、下記のプログラムを実行すると、コンソールには「2020/01/31 23:59:59.999」と表示されるにもかかわらず、スプレッドシートには「2020/2/1」と表示されます。

function endOfMonthTest5() {
  //元の日付
  const d = new Date('2020/01/30');

  //元のDateオブジェクトの月末日付を計算
  const dayjs_endOfMonth = dayjs.dayjs(d) //Day.jsオブジェクトを作成
    .endOf('month')                   //月末日付「2020/1/31 23:59:59.999」を作成

  //計算した月末日付をコンソールに出力
  //「2020/01/31 23:59:59.999」
  Logger.log(dayjs_endOfMonth.format('YYYY/MM/DD HH:mm:ss.SSS'));

  //Dateオブジェクトに変換
  const d_endOfMonth = dayjs_endOfMonth.toDate();

  //月末日をスプレッドシートに出力(※秒未満は四捨五入される)
  //「2020/02/01 00:00:00」
  SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue(d_endOfMonth);
}

スプレッドシートにDateオブジェクトを転記するときに、1秒未満の端数は小数第1位で四捨五入されます。

そのため、endOfメソッドで作成した「2020/1/31 23:59:59.999」のDateオブジェクトをスプレッドシートに書き込むと「2020/2/1 00:00:00」に変化してしまいます

これでは、月末日の日付になりませんので、millisecondメソッドを使って秒未満を「000」にしておく必要があることに注意してください。

まとめ

月末日付の計算をするときには、Day.jsを使うと、少し楽ができます。ただしendOfメソッドを使う場合には、millisecondメソッドを使って、millisecondを「000」にしないと、不具合が出ますので気を付けてください。