【JavaScript】DateDelta クラスを自作してみた

# 1. どんなことができるクラスか
次のコードペンのようなことができます。
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="VwdObLL" data-user="hiratatomotaka" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/hiratatomotaka/pen/VwdObLL">
  日付比較</a> by hiratatomotaka (<a href="https://codepen.io/hiratatomotaka">@hiratatomotaka</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

- 2つの日時の間の差分を 年、月、週、日、秒単位で表すことができます。
- 2つの日時の間の差分 を「A年間 Bか月間 C日間 D時間 E分間 F秒間」というような形式で取得できます。
- `Date`オブジェクトとの間で 足し引き算も可能です。


# 2. 仕様
## 2-1. コンストラクタ
コンストラクタでは、2つの `Date` オブジェクト `dateA`, `dateB`をこの順で受け取り、これをもとに、次のプライベートフィールドを作ります。

- `#newer_is_A`: `dateA` が `dateB` よりも新しい日付であることの真偽値
- `#newer`: `dateA`, `dateB` のうち、より新しいほうの `Date` オブジェクト
- `#older`: `dateA`, `dateB` のうち、より古いほうの `Date` オブジェクト
- `#answer`: `dateA` から `dateB` を引き算※した結果を表す オブジェクト。
`negative`, `year`, `month`, `day`, `hour`, `minute`, `second` をキーとするオブジェクト。
  - `#answer.negative`: 引き算の結果が 0 より小さいか否か。
`#newer_is_A` の否定となる。
  - `#answer.year`: 引き算の絶対値における年情報。
  - `#answer.month`: 月情報。0から11の間の整数となる。
  - `#answer.day` : 日。0～30
  - `#answer.hour`: 時。0～23
  - `#answer.minute`: 分。0～59
  - `#answer.second`: 秒。0～59

※  `dateA` から `dateB` への 引き算は次のルールで行います。

1.  符号と絶対値は別の工程で求めます。
    1. 新しい日時から古い日時を引き算することで絶対値をまず求めます。
    2. `dataA`と`dataB`の新旧関係に基づいて符号を決めます。
2.  新しい日時から古い日時への引き算は、「時間の筆算」により求めます。
というのは、
`unit` = (秒, 分, 時, 日, 月) および
`next_unit` = (分, 時, 日, 月, 年) について、
この順で次の処理をし、最後に年情報を引き算することを意味します。 
    1. まず 新しい日時の`unit` (例えば 秒) から 古い日時の`unit` を引き算します。
    2. その結果 (ここでは仮に`x`) により、処理は場合分けされます。
        1. `x` が負でない場合、筆算の答えにおける`unit` として `x` を採用します。
        2. `x` が負であった場合、次の通りとします。
            1. 新しい日時の`next_unit` (例えば 分) を 1だけ減算します。
            2. 新しい日時の`unit` を `borrow` (例えば 60) だけ加算します。
            3. 再度 `x` を求めると必ず 非負となりますので、これを筆算の答えにおける `unit` として採用します。
3. `borrow` は、古い日時の`next_unit`を、`unit` で表現した値とします。
例えば 
新しい日時が `2022/3/1 0:00:00`、 
古い日時が `2022/2/28 0:00:00` で、
`next_unit` が 月、 
`unit` が 日
の場合を考えましょう。
`x` は 1 - 28 = -27 なので負となります。
そのため、新しい日付の 月 を 1だけ減算して、
日 は `borrow` だけ加算します。
`borrow` は 古い日時の 1ヶ月 を 日 で表現したものとなります。
古い日時は 2022年2月... なので、 1ヶ月は 28 日となります。
よって、新しい日付は `2022/(3-1)/(1+28) 0:00:00` 
つまり `2022/2/29 0:00:00` とみなされます。
そして再度 日の引き算を行いますから、 筆算の答えにおける日 は 29 - 28 = 1 となります。

## 2-2. メソッド

### 2-2-1. `get_as_year(mode="up")`
`dataA` から `dataB` を引いた結果を年単位で取得するメソッドです。
`#answer.year` は 整数ですが、 `get_as_year` の返却値は小数部分を含む実数となります。

小数部分は、数え方によって2通りの値となる場合があります。
例えば、2022年3月21日11:23:02 と 2028年2月17日08:56:11 の時間差は、
数え方によって -5.9095...年となったり、 -5.9120...年となったりします。
この違いは、小数部分を計算する際、うるう日が含まれるか含まれないかによって生じます。


そこで、2通りの数え方のうちのどちらを採用するのかを、`mode` 引数で指示します。
この引数は `"up"`, `"down"`, `"AtoB"`, `"BtoA"`, `"nonleap"`, `"leap"` の6通りが許され、省略された場合や無効な値が代入された場合は`"up"` が指定されたものとみなされます。

小数点以下は、次の方法で数えます。
`mode="up"` の場合、 古い日付から数えて求めます。
`mode="down"` の場合、新しい日付から数えて求めます。
`mode="AtoB"` の場合、`dateA`から数えて求めます。
`mode="BtoA"` の場合、`dateB`から数えて求めます。
`mode="nonleap"` の場合、うるう年を無視して数えます。
`mode="leap"` の場合、うるう年があるものとみなして数えます。

次の2例では、
A: 2022年3月21日11:23:02 と 
B: 2028年2月17日08:56:11を比較する場合を考えます。

#### 2-2-1-1. `mode = "up"` の例
Bのほうが新しい日付であるため、`mode="AtoB"` は、`mode="up"` と同じ効果をもたらします。

1. 古い日付 2022年3月21日11:23:02 を `older`, 
新しい日付 2028年2月17日08:56:11 を `newer` とします。
2. 整数部分は `#answer.year` から求められます。5 です。
3. `older` に対して、年情報を 整数部分 だけ加算して得られる `older_dush` を考えます。
`older_dush` は 2027年3月21日11:23:02 です。 
(`newer` と `older_dush` の時間差を、(`older_dush`にとっての1年の長さ) で割ったものこそが、求めたい小数部分です)
4. `newer` と `older_dush` の時間差 を 秒 単位で表します。
今回は `28762389` となります。
5. `older_dush` にとっての1年の長さを 秒 単位で表します。
つまり、次のようにします。
    1. `older_dush` に対して、年情報を 1 だけ加算して得られる `older_dush2` を考えます。
`older_dush2` は 2028年3月21日11:23:02 です。
    2. `older_dush2` と `older_dush` の時間差 を秒 単位で表します。
今回は `31622400` となります。(うるう日である2028年2月29日をまたぐので、日で表すと366日となっています。つまり、`mode="leap"` と同様の結果となります)
6. 小数部分は 28762389 / 31622400 = 0.9095... です。
7. A - B が 負であることから、負号が必要です。
8. 手順2, 6, 7 より、このメソッドが返却すべき値は -5.9095... となります。


#### 2-2-1-2. `mode = "down"` の例
Bのほうが新しい日付であるため、`mode="BtoA"` は、`mode="down"` と同じ効果をもたらします。

1. 古い日付 2022年3月21日11:23:02 を `older`, 
新しい日付 2028年2月17日08:56:11 を `newer` とします。
2. 整数部分は `#answer.year` から求められます。5 です。
3. `newer` に対して、年情報を 整数部分 だけ減算して得られる `newer_dush` を考えます。
`newer_dush` は 2023年2月17日08:56:11 です。 
(`newer_dush` と `older` の時間差を、(`newer_dush`にとっての1年の長さ) で割ったものこそが、求めたい小数部分です)
4. `newer_dush` と `older` の時間差 を 秒 単位で表します。
今回は `28762389` となります。
5. `newer_dush` にとっての1年の長さを 秒 単位で表します。
つまり、次のようにします。
    1. `newer_dush` に対して、年情報を 1 だけ減算して得られる `newer_dush2` を考えます。
`newer_dush2` は 2022年2月17日08:56:11 です。
    2. `newer_dush` と `newer_dush2` の時間差 を秒 単位で表します。
今回は `31536000` となります。(うるう日をまたがないので、日で表すと365日となっています。つまり、`mode="nonleap"` と同様の結果となります)
6. 小数部分は 28762389 / 31536000 = 0.9120... です。
7. A - B が 負であることから、負号が必要です。
8. 手順2, 6, 7 より、このメソッドが返却すべき値は -5.9120... となります。



### 2-2-2. `get_as_month(mode="up")`
`dataA` から `dataB` を引いた結果を月単位で取得するメソッドです。
整数部分は `#answer.year` の12 倍 に `#answer.month` を加えて求めます。

`get_as_year` メソッド同様、小数部分は数え方によって2通りに分かれますので、`mode` 引数で数え方を指定します。
このメソッドの `mode` 引数は `"up"`, `"down"`, `"AtoB"`, `"BtoA"` の4通りから選びます。

### 2-2-3. `get_as_week()`
`dataA` から `dataB` を引いた結果を週単位で取得するメソッドです。
引数は不要です。

### 2-2-4. `get_as_day()`
`dataA` から `dataB` を引いた結果を日単位で取得するメソッドです。
引数は不要です。

### 2-2-5. `get_as_second()`
`dataA` から `dataB` を引いた結果を秒単位で取得するメソッドです。
引数は不要です。

### 2-2-6. `get_as_object()`
`dataA` から `dataB` を引いた結果としてのプライベートフィールド `#answer` のコピーを取得するメソッドです。
引数は不要です。

### 2-2-7. `added_by(date)`
`date + (dateA - dateB)` というような日付を求めるメソッドです。
引数 `date` は `Date` オブジェクトです。

### 2-2-8. `subted_by(date)`
`date - (dateA - dateB)` というような日付を求めるメソッドです。
引数 `date` は `Date` オブジェクトです。


# 3. 使い方
第1章に掲げたコードペンを参考にお使いください。