【JavaScript】Date型のオブジェクト生成、文字列変換時の罠を避ける
JavaScriptでDate型の罠を避ける
JavaScript(TypeScript)のDate型には、様々なトラップがあります。
気を付けて使わないと、簡単にバグが発生するので気を付けて使いましょう。
以下、Date型を使ううえでポイントになりそうな点をまとめます
- Date型の内部実装
- タイムゾーンの扱い
- 有効な日付の判定
- ローカルタイムゾーンによる、Date型と他の型の相互変換
- 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型として有効かどうかを判定するためには、次のように判定しましょう。
- Date型であることを確かめる
- 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月が
0
〜11
に対応する - 指定した日時は、ローカル時間で生成される
また、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月が
0
〜11
に対応する 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型データを指定すると、文字列に変換することができます。