JavaScript/TypeScriptメモ

【JavaScript】Date型のオブジェクト生成、文字列変換時の罠を避ける

JavaScriptでDate型の罠を避ける

JavaScript(TypeScript)のDate型には、様々なトラップがあります。

気を付けて使わないと、簡単にバグが発生するので気を付けて使いましょう。

以下、Date型を使ううえでポイントになりそうな点をまとめます

  1. Date型の内部実装
  2. タイムゾーンの扱い
  3. 有効な日付の判定
  4. ローカルタイムゾーンによる、Date型と他の型の相互変換
  5. UTCによる、Date型と他の型の相互変換

なお、このページのソースは、すべてTypeScriptで書いています。JavaScriptで使うときには、型の部分を削除して使ってください

1. Date型の内部実装:内部にNumber型を持つ

Date型変数は、内部にNumber型の値を持っています。

そのNumber型には、UTCの1970年1月1日からのミリ秒数が格納されており、これで、日付・時刻を管理しています。

たとえば、UTCの「2020年1月1日0時0分0秒」は「1580515200000」という数値に置き換えて管理されることになります。

この、内部のNumber型には、-8,640,000,000,000,000〜8,640,000,000,000,000と「NaN」が入る可能性があります。特に、「NaN」が格納される場合もあることに注意が必要です

また、1日は厳格に86,400,000ミリ秒で換算されます(うるう秒は考慮されません)。

2. タイムゾーンの扱い

Date型変数の内部では、タイムゾーンの情報は持ちません。

つまりDate型変数単体では、次の2つを区別することはできません。

  • 2020年1月1日0時0分0秒(UTC)
  • 2020年1月1日9時0分0秒(東京時間)

3. 有効な日付の判定

ある変数に格納されているデータが、Date型として有効かどうかを判定するためには、次のように判定しましょう。

  1. Date型であることを確かめる
  2. Date型で管理している日付の内部値が「NaN」でないことを確かめる
const isValidDate = (date: unknown): boolean =>
  toString.call(date) === `[object Date]` &&
  (date as Date).toString() !== 'Invalid Date';

const d1 = new Date();
const d2 = new Date('invalid date sample');
const d3 = '2020-02-03';

console.log(isValidDate(d1)); //true
console.log(isValidDate(d2)); //false
console.log(isValidDate(d3)); //false

日付の内部値が「NaN」になっていると、後で紹介する「date-fns」の「format」関数使用時に実行時エラーが表示されるので、注意が必要です。

4. ローカルタイムゾーンによる、Date型と他の型の相互変換

以下、console.logの結果は、私の手元のNode.jsで実行した場合のものです。タイムゾーンは東京(UTC+9)に設定されています。

A. 指定した日時に対応するDate型を作成する

const dateFromYMDHMS = new Date(2020, 1, 3, 4, 5, 6);

console.log(dateFromYMDHMS);
// 2020-02-02T19:05:06.000Z

下記の点に注意が必要です。

  • 月は、1月〜12月が011に対応する
  • 指定した日時は、ローカル時間で生成される

また、Node.js環境では、console.logで、Date型の値を表示すると、UTCでの日付が表示されます。

日本時間の「2020年2月3日4時5分6秒」を、UTCに直すと「2020年2月2日19時5分6秒」になります。ですから、今回、console.logで時間を表示すると、2020-02-02T19:05:06.000Zと表示されます。

B. 文字列からDate型を作成する

import { parse } from 'date-fns/fp';

//parse用の関数を準備
const parseString = parse(new Date(), 'yyyy-MM-dd HH:mm:ss');

//parse実行
const dateFromString = parseString('2020-02-03 04:05:06');

console.log(dateFromString);
// 2020-02-02T19:05:06.000Z

文字列からDate型のデータを作成する場合、JavaScript標準の関数だと、動きが読めない場合があるためdate-fnsを使います。

今回は、date-fnsを関数型プログラミング的に使えるdate-fns/fpを使っています。

date-fns/fpのparse関数は、機能的には、通常のdate-fnsのparse関数と同じです。ただし、引数の並び順が逆順になっていて、一部の引数だけを部分適用することができます。

そこで、parseをする準備として、parse関数にReference Dateとしてnew Date()、format stringとして'yyyy-MM-dd HH:mm:ss'を部分適用したparseString関数を作りましょう。

あとは、parseString関数にparseしたい文字列を指定すると、Date型オブジェクトを得ることができます。

C. Date型のデータを文字列に変換する

import { format } from 'date-fns/fp';

//format用の関数を準備
const formatDate = format('yyyy-MM-dd HH:mm:ss');

//日付データ
const d = new Date(2020, 1, 3, 4, 5, 6);

//文字列化
const dateString = formatDate(d);

console.log(dateString);
// 2020-02-03 04:05:06

Date型のデータを文字列に加工する場合も、JavaScript標準の関数だと、動きが読めない場合があるためdate-fnsを使います。

先ほどと同様、date-fnsを関数型プログラミング的に使えるdate-fns/fpを使っています。

まず、準備として、format関数に、出力形式:'yyyy-MM-dd HH:mm:ss'を指定したformatDate関数を準備しましょう。

あとは、formatDate関数に、文字列化したいDate型データを指定すると、文字列に変換することができます。

D. 指定したtimeに対応するDate型を作成する

const time = 1580670306000; // 1970年1月1日からの経過時間(ミリ秒単位)
const dateFromTime = new Date(time);

console.log(dateFromTime);
// 2020-02-02T19:05:06.000Z

経過時間はUTCベースで表されるので、タイムゾーンは関係ありません。

E. 指定したUNIX timeに対応するDate型を作成する

UNIX timeは、1970年1月1日午前0時0分0秒からの経過秒数で表されます。

そのため、UNIX timeを1,000倍すると、JavaScriptのDate型の内部管理で使っている数値に変換ができます。

const unixTime = 1580670306; // 1970年1月1日からの経過時間(秒単位)
const dateFromUnixTime = new Date(unixTime * 1000);

console.log(dateFromUnixTime);
// 2020-02-02T19:05:06.000Z

経過時間はUTCベースで表されるので、タイムゾーンは関係ありません。

5. UTCによる、Date型と他の型の相互変換

A. 指定した日時に対応するDate型を作成する

const dateFromYMDHMS = new Date(Date.UTC(2020, 1, 3, 4, 5, 6));

console.log(dateFromYMDHMS);
// 2020-02-03T04:05:06.000Z

あらかじめ、Date.UTCメソッドを使って、指定した(UTCでの)日付・時刻に応じた「1970年1月1日からの経過秒数(ミリ秒)」が得られます。

その数値を、Dateのコンストラクタに渡すことで、指定したUTCでの日付・時刻に応じたDate型オブジェクトを作成しています。

注意点は、ローカルタイムベースの生成時とまったく同じです。

  • 月は、1月〜12月が011に対応する
  • new Date( ... )を使うと、指定した日時は、ローカル時間で生成される

実際、console.logで時間を表示すると2020-02-03T04:05:06.000Zと表示されます。

B. 文字列からDate型を作成する

import { parse, addMinutes } from 'date-fns/fp';

//ローカルタイムゾーンからUTCにDateオブジェクトを調整する関数を準備
const AdjustLocalToUTC = addMinutes(-new Date().getTimezoneOffset());
//ローカルタイムゾーンにparseする関数を準備
const parseString = parse(new Date(), 'yyyy-MM-dd HH:mm:ss');

//parse後ローカルタイムゾーン分時刻を調整する
const parseUTCString = (dateString: string): Date =>  AdjustLocalToUTC(parseString(dateString));
//parseし、DateオブジェクトをUTCに調整
const dateFromString = parseUTCString('2020-02-03 04:05:06');

console.log(dateFromString);
// 2020-02-03T04:05:06.000Z

ローカルタイムベースのときと同じようにdate-fnsを使います。

単にparseをするだけだと、ローカルタイムでのDateオブジェクトしか得られません。そこで、さらに、parseして得られたDateオブジェクトを、タイムゾーンのずれ分だけ時刻を調整します

まず、タイムゾーンを補正する用の関数AdjustLocalToUTCを作ります。

new Date().getTimezoneOffset()で、UTCとローカルタイムの間の時間差を「分」単位で取得することができます。そこで、その結果(の符号を反転させたもの)をAddminutes関数に渡して、タイムゾーンを補正するAdjustLocalToUTC関数を作ります。

次に、ローカルベースのときとまったく同じ方法でparseString関数を作ります。

最後に、この2つの関数を適用するparseUTCString関数を作れば、準備完了です。

あとは、parseUTCString関数にparseしたい文字列を指定すると、UTCベースでのDate型オブジェクトを得ることができます。

C. Date型のデータを文字列に変換する

import { format, addMinutes } from 'date-fns/fp';

//ローカルタイムゾーンからUTCにDateオブジェクトを調整する関数を準備
const AdjustUTCToLocal = addMinutes(new Date().getTimezoneOffset());
//format用の関数を準備
const formatDate = format('yyyy-MM-dd HH:mm:ss');

//タイムゾーン調整後format
const formatUTCDate = (date: Date): string =>  formatDate(AdjustUTCToLocal(date));
//日付データ(UTCで2020年2月3日4時5分6秒)
const d = new Date(Date.UTC(2020, 1, 3, 4, 5, 6));

//DateオブジェクトをUTCの日時として文字列化
const dateString = formatUTCDate(d);

console.log(dateString);
// 2020-02-03 04:05:06

考え方は、先ほどとほとんど同じです。今回は、タイムゾーン分補正を入れた後にformatを使ってstring化します。

まず、UTCベースのDate型の値をローカルベースに調整するための関数AdjustUTCToLocalを作成します。さきほどと違い、今回は、マイナス符号を付けずに、new Date().getTimezoneOffset()の結果をそのまま、addMinutes関数に渡しています。

formatDate関数は、ローカルタイムのときとまったく同じように準備しましょう。

あとは、AdjustUTCToLocal関数と、format関数を連続して摘要するformatUTCDate関数を作れば準備完了です。

formatDate関数に、文字列化したいDate型データを指定すると、文字列に変換することができます。