1: <?php
2: /*
3: * Copyright (c) 2014 @trashtoy
4: * https://github.com/trashtoy/
5: *
6: * Permission is hereby granted, free of charge, to any person obtaining a copy of
7: * this software and associated documentation files (the "Software"), to deal in
8: * the Software without restriction, including without limitation the rights to use,
9: * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
10: * Software, and to permit persons to whom the Software is furnished to do so,
11: * subject to the following conditions:
12: *
13: * The above copyright notice and this permission notice shall be included in all
14: * copies or substantial portions of the Software.
15: *
16: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22: */
23: /**
24: * PHP class file.
25: * @auhtor trashtoy
26: * @since 2.0.0
27: */
28: namespace Peach\DT;
29:
30: /**
31: * HTTP-Date の書式を扱うクラスです.
32: * parse 系メソッドは, 以下の 3 種類のフォーマットを解釈することが出来ます.
33: *
34: * <code>
35: * "Fri, 13 Feb 2009 23:31:30 GMT" #RFC 822, updated by RFC 1123
36: * "Friday, 13-Feb-09 23:31:30 GMT" #RFC 850, obsoleted by RFC 1036
37: * "Fri Feb 13 23:31:30 2009" #ANSI C's asctime() format
38: * </code>
39: *
40: * format 系メソッドは, 一番上の RFC 1123 形式のフォーマットで文字列を生成します.
41: *
42: * このクラスは parse および format を行う際に, 内部時刻と GMT の自動変換を行います.
43: *
44: * 参考文献:
45: * {@link http://www.arielworks.net/articles/2004/0125a モジュール版PHPで「If-Modified-Since」に対応する}
46: */
47: class HttpDateFormat implements Format
48: {
49: /**
50: * システム時刻の時差です (単位は分)
51: * @var int
52: */
53: private $internalOffset;
54:
55: /**
56: * 新しいフォーマットを生成します.
57: * 引数 $offset を指定した場合, parse または format を行った際に,
58: * 指定された時差の量だけ時刻に補正がかかります.
59: * もしも時差に応じた自動変換が必要ない場合は $offset に 0 を指定してください.
60: *
61: * 引数を省略した場合は, システム時刻の時差 ({@link Util::getTimeZoneOffset()} の返り値と等価)
62: * を使用します. 特に必要がなければ引数なしのコンストラクタを使う代わりに
63: * {@link HttpDateFormat::getInstance()} を使ってください.
64: *
65: * @param int $offset 時間オブジェクトの時差 (単位は分, 省略した場合はシステム設定の値を使用)
66: */
67: public function __construct($offset = null)
68: {
69: $this->internalOffset = Util::cleanTimeZoneOffset($offset);
70: }
71:
72: /**
73: * デフォルトのインスタンスを返します.
74: * このメソッドは引数なしでコンストラクタを呼び出した場合と同じ結果を返しますが,
75: * 返り値がキャッシュされるため, 複数の場所で同じインスタンスを使いまわすことができます.
76: *
77: * システムのタイムゾーン設定を動的に変更した場合は, 変更前のキャッシュを破棄する必要があるため,
78: * 引数 $clearCache に TRUE を指定して実行してください.
79: *
80: * @param bool $clearCache キャッシュを破棄してインスタンスを再生成する場合は TRUE
81: * @return HttpDateFormat デフォルトのインスタンス
82: */
83: public static function getInstance($clearCache = false)
84: {
85: static $instance = null;
86: if (!isset($instance) || $clearCache) {
87: $instance = new self();
88: }
89: return $instance;
90: }
91:
92: /**
93: * {@link HttpDateFormat::parseTimestamp()} の実行結果を Date にキャストします.
94: *
95: * @param string $format HTTP-date 形式の文字列
96: * @return Date 変換結果
97: * @throws \InvalidArgumentException フォーマットが不正な場合
98: */
99: public function parseDate($format)
100: {
101: return $this->parseTimestamp($format)->toDate();
102: }
103:
104: /**
105: * {@link HttpDateFormat::parseTimestamp()} の実行結果を Datetime にキャストします.
106: *
107: * @param string $format HTTP-date 形式の文字列
108: * @return Datetime 変換結果
109: * @throws \InvalidArgumentException フォーマットが不正な場合
110: */
111: public function parseDatetime($format)
112: {
113: return $this->parseTimestamp($format)->toDatetime();
114: }
115:
116: /**
117: * HTTP-date 形式のフォーマットを Timestamp に変換します.
118: *
119: * @param string $format HTTP-date 形式の文字列
120: * @return Timestamp 変換結果
121: * @throws \InvalidArgumentException フォーマットが不正な場合
122: */
123: public function parseTimestamp($format)
124: {
125: $temp_date = array();
126: if (preg_match("/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-3][0-9]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-9]{4}) ([0-2][0-9]):([0-5][0-9]):([0-5][0-9]) GMT$/", $format, $temp_date)) {
127: $date["hour"] = $temp_date[5];
128: $date["minute"] = $temp_date[6];
129: $date["second"] = $temp_date[7];
130: $date["month"] = $this->parseMonthDescription($temp_date[3]);
131: $date["day"] = $temp_date[2];
132: $date["year"] = $temp_date[4];
133: } else if (preg_match("/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), ([0-3][0-9])-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-([0-9]{2}) ([0-2][0-9]):([0-5][0-9]):([0-5][0-9]) GMT$/", $format, $temp_date)) {
134: $date["hour"] = $temp_date[5];
135: $date["minute"] = $temp_date[6];
136: $date["second"] = $temp_date[7];
137: $date["month"] = $this->parseMonthDescription($temp_date[3]);
138: $date["day"] = $temp_date[2];
139: $date["year"] = $this->getFullYear($temp_date[4]);
140: } else if (preg_match("/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-3 ][0-9]) ([0-2][0-9]):([0-5][0-9]):([0-5][0-9]) ([0-9]{4})$/", $format, $temp_date)) {
141: $date["hour"] = $temp_date[4];
142: $date["minute"] = $temp_date[5];
143: $date["second"] = $temp_date[6];
144: $date["month"] = $this->parseMonthDescription($temp_date[2]);
145: // 日が1桁の場合先、半角スペースを0に置換
146: $date["day"] = str_replace(" ", "0", $temp_date[3]);
147: // 定義済みの月の名前を数字に変換する
148: $date["year"] = $temp_date[7];
149: } else {
150: $this->throwFormatException($format);
151: }
152:
153: $parsed = new Timestamp($date["year"], $date["month"], $date["day"], $date["hour"], $date["minute"], $date["second"]);
154: return $parsed->add("minute", - $this->internalOffset);
155: }
156:
157: /**
158: * この日付の 00:00 の時刻を GMT に変換した結果を Http-date にして返します.
159: * 例えばシステム時刻の時差が UTC+9 だった場合, 前日の 15:00 の HTTP-date 表現を返り値とします.
160: *
161: * @param Date $d 書式化対象の時間オブジェクト
162: * @return string この日付の HTTP-date 表現
163: */
164: public function formatDate(Date $d)
165: {
166: return $this->formatDatetime($d->toDatetime());
167: }
168:
169: /**
170: * この時刻の HTTP-date 表現を返します.
171: *
172: * @param Datetime $d 書式化対象の時間オブジェクト
173: * @return string この時刻の HTTP-date 表現
174: */
175: public function formatDatetime(Datetime $d)
176: {
177: $d = $d->add("minute", $this->internalOffset);
178:
179: $format = '';
180: $format .= $this->getDayDescription($d->getDay()).', ';
181: $format .= str_pad($d->get('date'), 2, '0', STR_PAD_LEFT) . ' ';
182: $format .= $this->getMonthDescription($d->get('month')) . ' ';
183: $format .= str_pad($d->get('year'), 4, '0', STR_PAD_LEFT) . ' ';
184: $format .= $d->formatTime() . ' ';
185: $format .= 'GMT';
186: return $format;
187: }
188:
189: /**
190: * この時刻の HTTP-date 表現を返します.
191: *
192: * @param Timestamp $d 書式化対象の時間オブジェクト
193: * @return string この時刻の HTTP-date 表現
194: */
195: public function formatTimestamp(Timestamp $d)
196: {
197: return $this->formatDatetime($d);
198: }
199:
200: /**
201: * parse に失敗した場合に呼び出されます.
202: *
203: * @param string $format parse に失敗した文字列
204: * @throws \InvalidArgumentException
205: */
206: private function throwFormatException($format)
207: {
208: throw new \InvalidArgumentException("Illegal format({$format}). HTTP-date format required.");
209: }
210:
211: /**
212: * 月の略称一覧です
213: * @return array "Jan" から "Dec" までの月の略称を持つ配列
214: * @codeCoverageIgnore
215: */
216: private function getMonthMapping()
217: {
218: static $mapping = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
219: return $mapping;
220: }
221:
222: /**
223: * 月の略称を書式化します.
224: * @param int $month 月
225: * @return string 引数の月の略称
226: */
227: private function getMonthDescription($month)
228: {
229: $mapping = $this->getMonthMapping();
230: return $mapping[$month - 1];
231: }
232:
233: /**
234: * 月の略称を月に変換します.
235: * @param string $mon 月の略称
236: * @return int 引数の略称の数値表現 (1 から 12)
237: */
238: private function parseMonthDescription($mon)
239: {
240: $mapping = $this->getMonthMapping();
241: $month = array_search($mon, $mapping) + 1;
242: return str_pad($month, 2, '0', STR_PAD_LEFT);
243: }
244:
245: /**
246: * 2 桁の年表示を 4 桁に変換します.
247: * もしも引数の 2 桁年が, 現在の 2 桁年よりも大きい場合,
248: * 前世紀の年と判断します.
249: *
250: * @param int $y 変換対象の 2 桁年
251: * @return int 4 桁の年
252: */
253: private function getFullYear($y)
254: {
255: $currentYear = intval(date("Y"));
256: $century = intval($currentYear / 100);
257: $smallY = $currentYear % 100;
258: $resultC = ($smallY < $y) ? $century - 1 : $century;
259: return $resultC * 100 + $y;
260: }
261:
262: /**
263: * 曜日の略称を書式化します.
264: * @param int $day 曜日の値
265: * @return string 曜日の略称
266: */
267: private function getDayDescription($day)
268: {
269: switch ($day) {
270: case 0: return "Sun";
271: case 1: return "Mon";
272: case 2: return "Tue";
273: case 3: return "Wed";
274: case 4: return "Thu";
275: case 5: return "Fri";
276: case 6: return "Sat";
277: }
278: // @codeCoverageIgnoreStart
279: throw new \Exception("day: Out of range");
280: // @codeCoverageIgnoreEnd
281: }
282: }
283: