[GAS][JS] Momentライブラリのdiffでの日付差分


(追記 2020/09/19) Moment.js の開発が終了したようです。

↓ ここ参照
https://momentjs.com/docs/

今すぐライブラリが使えなくなることは無いと思いますが、この先、積極的にこのライブラリを選択するかどうかは考えものですね。

あと、GAS版のライブラリがどうなるのか、については情報が見つかりませんでした。(どなたか知ってたら教えてほしい)


GASで話をしますが、Javascriptでも同じです。

GAS版 MomentライブラリのIDは下記です。

MHMchiX6c1bwSqGM1PZiW_PxhMjh3Sh48

やりたかったこと

こんな表を作って、毎朝8時台にGASが起動して、「締め切りまであと3日」以内になったら「締め切り近いけど大丈夫?」というリマインドをしてくれる機能を作りたい。

そのため、Momentライブラリをつかって「日付の差分」を取りたかった。

ダメだった例

簡略化するためにA2セルの日付だけを対象にします。

始めはこう書きました。

function sample1() {
  // 日付のセルからgetValue()したら Dateオブジェクト が取れるのでこう書いてしまいます
  const deadline_date = new Date("2020-09-10");

  const deadline_moment = Moment.moment(deadline_date);
  const now_moment      = Moment.moment();

  const diff = deadline_moment.diff(now_moment, "days");
  console.log(diff);
}

そして「今日は 2020/9/9 だとする」じゃないですか。
A2セルは 2020/9/10 なので 1 が出力されることを期待するじゃないですか。
だけど 出力結果は「0」

ダメだった理由

上記で 「今日は 2020/9/9 だとする」 と書いたのですが、
実際はこのGASが実行される時刻は 2020/09/09 12:34:56 だったりするわけです。

そして deadline として指定しているのは 「2020-09-10 00:00:00」 なのです。

実験してみるとこうなりました↓

function sample2() {
  const deadline_date = new Date("2020-09-10 00:00:00");
  const deadline_moment = Moment.moment(deadline_date);

  const now_moment1 = Moment.moment("2020-09-09 00:00:00");
  const diff1 = deadline_moment.diff(now_moment1, "days");
  console.log(diff1); //=> 1 

  const now_moment2 = Moment.moment("2020-09-09 00:00:01");
  const diff2 = deadline_moment.diff(now_moment2, "days");
  console.log(diff2); //=> 0
}

now_moment1 と now_moment2では「1秒」異なります。

diff1 では deadlineである 2020-09-10 00:00:00 と 2020-09-09 00:00:00 の差分が 24:00:00 なので 1日。
diff2 では deadlineである 2020-09-10 00:00:00 と 2020-09-09 00:00:01 の差分が 23:59:59 なので 0日。

という動きのようだ。

どうすればいいか

「日付」で比較したいのだからこうしたらいいのでは。

「時分秒ミリ秒にゼロをセットしちゃう」作戦

function sample3() {
  const deadline_date = new Date("2020-09-10 00:00:00");
  const deadline_moment = Moment.moment(deadline_date);

  // 時分秒ミリ秒にゼロをセットしちゃう
  const today_moment = Moment.moment().hour(0).minutes(0).second(0).millisecond(0);
  const diff = deadline_moment.diff(today_moment, "days");

  console.log(diff);
}

これで、実行時刻が「2020-09-09 11:11:11」であっても「1」が出力されます。

↓ 念のため実験

function sample4() {
  const deadline_date = new Date("2020-09-10 00:00:00");
  const deadline_moment = Moment.moment(deadline_date);

  const now = new Date("2020-09-09 11:11:11")
  const today_moment = Moment.moment(now).hour(0).minutes(0).second(0).millisecond(0);

  const diff = deadline_moment.diff(today_moment, "days");
  console.log(diff); //=> 1
}

予想通り!

本当にそうなの?

"days" で比較するときに、「24時間未満かどうかで判定している」というのは、動作させた結果を見て私が推測しているところなのですが、実際どういう実装になってるのか momentライブラリのdiff.jsソースコードを見てみました。(これはmoment.jsのソースコードであってGASのライブラリのコードではないので、GASでは違う実装かもしれませんが、きっと同じであろう)

export function diff(input, units, asFloat) {
    var that, zoneDelta, output;

    if (!this.isValid()) {
        return NaN;
    }

    that = cloneWithOffset(input, this);

    if (!that.isValid()) {
        return NaN;
    }

    zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4;

    units = normalizeUnits(units);

    switch (units) {
// 略
        case 'day':
            output = (this - that - zoneDelta) / 864e5;
            break; // 1000 * 60 * 60 * 24, negate dst
// 略
    }

    return asFloat ? output : absFloor(output);
}

864e5 とは 1日をミリ秒にしたときの 86400000 の指数表記。
よって 時刻の差分を 1日を表すミリ秒で割ってる のね。

diff関数の第3引数である asFloat は指定していないので、

return asFloat ? output : absFloor(output);

では absFloor(output) が return される。

absFloor() 関数の定義は こちら

export default function absFloor(number) {
    if (number < 0) {
        // -0 -> 0
        return Math.ceil(number) || 0;
    } else {
        return Math.floor(number);
    }
}

Math.ceil() : 引数として与えた数以上の最小の整数を返します。
Math.floor() : 引数として与えた数以下の最大の整数を返します。

ということなので、output(時刻の差分を 1日を表すミリ秒で割った数値) が

  • 0.999 であれば 0 を返すし、
  • 1.234 なら 1 を返すし、
  • -3.55 なら -3 を返す。

私の推測はあってたようです!(...というここまでの流れが間違ってたら教えてほしいです)

結論

ということで 「日にち」を比較したいのであれば、日にち(date)より小さい単位(hour, minutes, second, millisecond)はゼロで埋めてあげる

補足ですが、ミリ秒がでてくるなら

const deadline_date = new Date("2020-09-10 00:00:00.000");

としたほうが粒度は揃いますね。
(ミリ秒まで指定してDateオブジェクトをnewしたことが無いのでへんな感じする)