SafariにおけるAngularのDatePipeでISO8601がパースできない問題をカスタムパイプで解決する

## SafariでAngularのDatePipe使えない問題

Angularにおいて、ISO8601の文字列を表示する場合を考えます。

```javascript
@Component({
  template:`<p>{{post.created_at | Date:'short'}}`
})
export class DemoComponent{
  public post = {
    title: 'タイトル',
    created_at: '2017-01-03 23:15:27'
  }
}
```

https://angular.io/docs/ts/latest/api/common/index/DatePipe-pipe.html

DatePipeは文字列もパースできるので、このようにすれば動くように思います。実際、ChromeやFirefoxではなんの問題もなく動きます。

しかし、Safariで実行すると

```
[Error] EXCEPTION: Error in ./TestComponent class TestComponent - inline template:30:38 caused by: Invalid argument '2017-01-25 21:00:10' for pipe 'DatePipe'
	handleError (main.bundle.js:61490)
	next (main.bundle.js:43128:94)
	(anonymous function) (main.bundle.js:45175)
	__tryOrUnsub (main.bundle.js:4737)
	next (main.bundle.js:4686)
	_next (main.bundle.js:4639)
	next (main.bundle.js:4603)
	next (main.bundle.js:10467)
	emit (main.bundle.js:45161)
	triggerError (main.bundle.js:32869)
	onHandleError (main.bundle.js:32830)
	runTask (main.bundle.js:128472)
	drainMicroTaskQueue (main.bundle.js:128723)
	invoke (main.bundle.js:128654)
[Error] ORIGINAL EXCEPTION: Invalid argument '2017-01-25 21:00:10' for pipe 'DatePipe'
	handleError (main.bundle.js:61492)
	next (main.bundle.js:43128:94)
	(anonymous function) (main.bundle.js:45175)
	__tryOrUnsub (main.bundle.js:4737)
	next (main.bundle.js:4686)
	_next (main.bundle.js:4639)
	next (main.bundle.js:4603)
	next (main.bundle.js:10467)
	emit (main.bundle.js:45161)
	triggerError (main.bundle.js:32869)
	onHandleError (main.bundle.js:32830)
	runTask (main.bundle.js:128472)
	drainMicroTaskQueue (main.bundle.js:128723)
	invoke (main.bundle.js:128654)
```

のようなエラーが発生します。

<img width="1208" alt="Screen_Shot_2017-01-29_at_11_37_46.png" src="https://qiita-image-store.s3.amazonaws.com/0/15238/a065533f-82b1-4826-fe9c-75aca94b3b57.png">


https://github.com/angular/angular/blob/4.0.0-beta.2/modules/%40angular/common/src/pipes/date_pipe.ts#L14-L138

DatePipeは渡された形式に応じてDateオブジェクトを作成しようとするのですが、Safariだとパースに失敗します。そのため、exceptionが投げられるようになっています。

## dateParseパイプを作って解決する

macOSのSafariだけならシカトしても良い（？）のですが、iOSでも同じ問題が発生するため、放置することはなかなか難しいです。そこで、`Date`に渡す前に`dateParse`という独自のパイプを噛ますことで解決しました。

こんなパイプを定義します。

```javascript
import { Pipe, PipeTransform } from '@angular/core';

export function parseIsoDatetime(dtstr: string): Date {
  const dt = dtstr.split(/[: T-]/).map(parseFloat);
  return new Date(dt[0], dt[1] - 1, dt[2], dt[3] || 0, dt[4] || 0, dt[5] || 0, 0);
}

@Pipe({
  name: 'dateParse'
})
export class DateParsePipe implements PipeTransform {
  transform(value: any | Date): Date {
    if (value instanceof Date) {
      // Dateはそのまま返す
      return value;
    }
    if (typeof value === 'number') {
      return new Date(value);
    }
    if (typeof value === 'string') {
      const parsed = Date.parse(value);
      if (Number.isNaN(parsed)) {
        const challenge = parseIsoDatetime(value);
        if (Number.isNaN(challenge.getTime())) {
          return null;
        } else {
          return challenge;
        }
      }
      return new Date(value);
    }
    return null;
  }
}
```

http://stackoverflow.com/questions/4829569/help-parsing-iso-8601-date-in-javascript

冒頭の`parseIsoDatetime`で文字列をパースしています。自分の環境ではタイムゾーンを扱うことがなかったので一番シンプルなこの関数を使用していますが、それが必要な場合でもこの部分を置き換えれば動くはずです。Moment.jsを入れても良いと思います。

後は、ngModulesから読み込み、テンプレートを

```html
<p>{{post.created_at | dateParse | Date:'short'}}</html>
```

のように書き換えてあげれば、Safariでも他のブラウザを同じように使用することが可能になります。

## さいごに

今回はISO8601以外の文字列が返ってくることを想定していないためエラーハンドリングをほとんどしていません。状況によっては、dateParseでエラーを発生させるなども必要かなと考えます。

ブラウザによる差異、最悪ですね...