0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのDateオブジェクトと他のオブジェクト同士の四則演算を可能にするには?

Last updated at Posted at 2022-06-11

こんにちは

初投稿です。
おかしな点があるかもしれませんが生暖かい目で見てやってください……

今回は、JavaScriptのDateオブジェクトと他のオブジェクトと四則演算をできるようにしたいと思います。

前置き

ここでは、以下の四則演算を扱います。

  • +(加算)
  • -(減算)
  • *(乗算)
  • /(除算)

減算は日付の差を求めるために使うのでDateオブジェクトのみでの演算でも一番使用頻度が高いかもしれません。

そもそもDateオブジェクトは四則演算が行えるのでしょうか?
少し例を示してみます。

JavaScript
let date1 = new Date('1985/09/13'); //スーパーマリオブラザーズ(初代)が発売された日。
let date2 = new Date('2017/10/27'); //スーパーマリオオデッセイが発売された日。

console.log(date2 + date1);
console.log(date2 - date1);
console.log(date2 * date1);
console.log(date2 / date1);
実行結果
Fri Oct 27 2017 00:00:00 GMT+0900 (日本標準時)Fri Sep 13 1985 00:00:00 GMT+0900 (日本標準時)
1013644800000
7.47551128356e+23
3.0461749765636923

様々な結果が出ていることがわかります。
オブジェクト同士のまま演算がなされているわけではなく、何らかの操作を施して演算できるようにしているようです。

加算される場合には文字列の連結として扱われています。これは、toString()メソッドが呼び出されている為です。

toString() メソッドは、日付がテキスト値で表現されるとき、例えば console.log(new Date())、または日付が文字列に強制変換されるとき、例えば var today = 'Today is ' + new Date() などで自動的に呼び出されます。
Date.prototype.toString() | MDN

四則演算が行われるとき、DateObjectは加算を除いて全て数値に直されています。
これは、valueOf()メソッドが呼び出されている為です。
では、この変換される数値は何なのでしょうか。

返値
協定世界時 (UTC) 1970 年 1 月 1 日 00:00:00 から指定された日時までの間のミリ秒単位の数値。
Date.prototype.valueOf() | MDN

となっています。
比較演算をする場合にもこれが適応されるので、時刻別の分岐処理は楽に実装できます。

余談
コンピュータの時間にまつわる問題の中に2038年問題というものがあります。

2038年問題(にせんさんじゅうはちねんもんだい)は、2038年1月19日3時14分7秒(UTC、以下同様)を過ぎると、コンピュータが誤動作する可能性があるとされる年問題。
2038年問題 | Wikipedia

JSのDateオブジェクトにこの時刻以降を設定したらどうなっちゃうのでしょうか?ミリ秒なので桁あふれが結構心配です。
実は意外と大丈夫です。

Date オブジェクトの設定可能範囲は 1970 年 1 月 1 日 UTC 時間 に対し -100,000,000 日から 100,000,000 日までです。
数値と日付 | MDN

ざっと計算してみると1970年から273,785年前後は大丈夫そうですね。
4で割り切れる年がうるう年なので、その周期である4年間の平均をとって1年を365.25日として計算しました。
約23万年前にネアンデルタール人が出現したと言われているので、ネアンデルタール人が絶滅しないようにJSを授けに行っても時間を扱う際には大丈夫そうですね。

本題

本題です。いままでDateオブジェクト同士について話しましたが、他のオブジェクトでも可能なのでしょうか?
もしかしたら、ここまでに話した内容でピンときた方もおられるかもしれません。

今回はMyDaysクラスを実装して他のオブジェクトと演算する方法を考えてみます。

どんなクラスかというと、

  • 1つの時間の長さを格納する。
  • 日と時間の文字列('DDD HH:MM'の形式。日の部分は可変)で宣言可能。
  • ミリ秒でも宣言可能。
    • Dateオブジェクト同士の差分を格納できる。
  • Dateオブジェクトと四則演算可能。
    • すなわち、任意の時間だけ前後させたり、割り算でその時間を期間中(Dateオブジェクト - Dateオブジェクトで求める)何回分取れるかなどを計算できる。
  • 引数なしで宣言すると0秒が格納される。

ざっとこんな感じです。

早速、実装を見ていきましょう。

JavaScript
class MyDays {
    constructor(days, hours, minutes) {
        if (days === undefined) {
            let that = new MyDays(0, 0, 0);

            this.days = that.days;
            this.hours = that.hours;
            this.minutes = that.minutes;
        } else if (hours === undefined) {
            let type = typeof days;

            if (type == 'number') {
                let that = MyDays.milliSecondsToDaysObj(days);

                this.days = that.days;
                this.hours = that.hours;
                this.minutes = that.minutes;
            } else if (type == 'string') {
                let that = MyDays.daysTimeStrToDaysObj(days);

                this.days = that.days;
                this.hours = that.hours;
                this.minutes = that.minutes;
            }
        } else {
            this.days = days;
            this.hours = hours;
            this.minutes = minutes;
        }
    }

    //日と時間の文字列で宣言可能。
    static daysTimeStrToDaysObj(daysTimeStr) {
        let days = Number(daysTimeStr.substr(0, daysTimeStr.length - 6));
        let hours = Number(daysTimeStr.substr(-5, 2));
        let minutes = Number(daysTimeStr.substr(-2, 2));
        
        return new MyDays(days, hours, minutes);
    }

    //ミリ秒でも宣言可能。
    //1day=86400000ms 1hour=3600000ms 1minute=60000ms
    static milliSecondsToDaysObj(milliSeconds) {
        let days = Math.floor(milliSeconds / 86400000);
        let hours = Math.floor((milliSeconds - days * 86400000) / 3600000);
        let minutes = Math.floor((milliSeconds - days * 86400000 - hours * 3600000) / 60000);

        return new MyDays(days, hours, minutes);
    }

    //愉快なgetterたち。
    getDays() {
        return this.days;
    }

    getHours() {
        return this.hours;
    }

    getMinutes() {
        return this.minutes;
    }

    //ミリ秒をだしてくれるやつ。
    //DateオブジェクトもgetTimeというメソッド名で同様の機能なので、この名前にしました。
    getTime() {
        return this.days * 86400000 + this.hours * 3600000 + this.minutes * 60000;
    }

    //ここがみそ!
    //これによって演算時にミリ秒をだしてくれる。
    valueOf() {
        return this.getTime();
    }
}

コメントのほうで雑に説明されていますので、詳しい中身に興味のある方はじっくりなめ回してみてください。

ここで重要なのは、valueOf()メソッドはDateオブジェクトに限った話ではなく、全てのオブジェクトにおいて同様の役割を果たすということです。
実際に見てみましょう。

JavaScript
let date = new Date('1582/06/02'); //本能寺の変が起きたとされる日。
let days = new MyDays('13 00:00'); //明智光秀が天下を執っていたとされる期間。実は3日ではないらしい……

console.log(date + days);
console.log(date - days);
console.log(date * days);
console.log(date / days);
実行結果
Wed Jun 02 1582 00:00:00 GMT+0918 (日本標準時)1123200000
-12232113539000
-1.37378483487648e+22
-10889.414475605414

ミリ秒ではわかりづらいですね。
1970年が基準になっているので、掛け算割り算はあまり意味をなしていません。
足し算においては、MyDaysオブジェクトではvalueOf()メソッドが呼び出されて、ミリ秒になっています。しかしながら、Dateオブジェクトでは、またしても文字列の連結と見做されているようです。うんち仕様
引き算をわかりやすいようにDateオブジェクトで表してみましょう。

JavaScript
let date = new Date('1582/06/02');
let days = new MyDays('13 00:00');

console.log(new Date(date - days));
実行結果
Thu May 20 1582 00:00:00 GMT+0918

うまくいってそうですね!!

しかしながら、足し算が出来ないのは悲しいので手を施してあげます。
Dateクラスの代わりにDateクラスを受け継いだMyDateクラスを作って解決します。
(以降の情報は追記前の情報です。おすすめはしません。)

JavaScript
class MyDate extends Date {
    toString() {
        return this.valueOf();
    }
}

足し算の際にtoString()メソッドが呼び出されるので、オーバーライドをすることで解決します。オーバーライドとは継承したクラス内でメソッドを上書きすることです。

JavaScript
let date = new MyDate('1582/06/02');
let days = new MyDays('13 00:00');

console.log(new Date(date + days));//明智光秀、敗れたり……
console.log(date);
実行結果
Tue Jun 15 1582 00:00:00 GMT+0918 (日本標準時)
-12230990339000

なんとかなりました。

ただし、console.log()にそのままオブジェクトを置いた場合、このオーバーライドしたtoString()メソッドが呼び出されます。普通に文字列の結合をしたい場合にも同様です。
なので、デバッグなどでオブジェクトの内容が知りたい場合には、上記のコードのようにDateオブジェクトを宣言するか、自前でメソッドを作って明示的に呼び出す必要が出てきます。オーバーライドされていないtoUTCString()メソッドを使っても良いかもしれません。
Date.prototype.toUTCString() | MDN

もし、toString()メソッドをオーバーライドすることなく、ミリ秒の足し算を他の演算と同様に行えるような方法がございましたら、教えていただけると幸いです。

ここでは、四則演算について主にやりました。
副産物として、MyDaysオブジェクトはこの実装のままでも比較演算が行えるようになっています。結構便利です。

追記

足し算についてもっといい方法がないかと探していたところ、ありました!!

まず、Dateクラスの代わりにDateクラスを受け継いだMyDateクラスを作ります。
ただし、toString()メソッドにオーバーライドはしません。

JavaScript
class MyDate extends Date {
    [Symbol.toPrimitive](hint) {
        if (hint == 'number') { 
            return this.getTime();
        } else if (hint == 'string') {
            return this.toString();
        } else if (hint == 'default') {
            return this.getTime();
        }
    }
}

こうすることによって、足し算の時はvalueOf()を、そのままconsole.log()したときにはtoString()を呼び出してくれます。

JavaScript
let date = new MyDate('1582/06/02');
let days = new MyDays('13 00:00');

console.log(date + days);
console.log(new MyDate(date + days));
console.log(date);
実行結果
-12229867139000
Tue Jun 15 1582 00:00:00 GMT+0918 (日本標準時)
Wed Jun 02 1582 00:00:00 GMT+0918 (日本標準時)

うれしい。

まとめ

いかがでしたか?
JSでは素のままでは演算子オーバーロードができないらしいのでこういった方法での解決となりました。
自分自身、勉強になってよかったです。

実は、これに関連したChrome拡張機能(指定した期間中、Twitterを見れなくするやつ)を作っている(2022/06/11現在)ので、もしよかったらTwitterのほうで進捗だけでも見に来てくれると嬉しいです。

では、皆さん良いJSライフを!!

参考

果たして JavaScript で演算子オーバーロードは可能なのか | けんごのお屋敷

追記の参考

JavaScriptのプリミティブへの変換を完全に理解する

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?