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: * {@link http://www.w3.org/TR/NOTE-datetime W3CDTF} と時間オブジェクトの相互変換を行うフォーマットです.
32: * 本来の W3CDTF は日付と時刻の間に "T" が入りますが (例: "YYYY-MM-DDThh:mm:ss")
33: * このクラスは SQL などで用いられる慣用表現 (例: "YYYY-MM-DD hh:mm:ss") のパースもできます.
34: *
35: * parse メソッドは, 文字列の先頭が W3CDTF の書式にマッチするかどうかで妥当性をチェックします.
36: * "2012-05-21T07:30:00" のような文字列は
37: * parseDate, parseDatetime, parseTimestamp のすべてのメソッドで受け付けますが,
38: * "2012-05-21" のような文字列は parseDate だけしか受け付けません.
39: *
40: * このクラスはタイムゾーンを適切に扱うことが出来ます.
41: * parse 系メソッドでは, 時間オブジェクトに変換する際に内部のタイムゾーンに合わせて時刻を調整します.
42: * 例えばシステム時刻が UTC+9 として, GMT の時間文字列 "2012-05-20T22:30Z" を parse した場合
43: * 9 時間進ませた時間オブジェクト (new Datetime(2012, 5, 21, 7, 30) に等価) を生成します.
44: *
45: * 時間オブジェクトを書式化する際は, 出力用の時差に合わせた文字列を生成します.
46: * 例えば内部のタイムゾーンが UTC+9, 出力用のタイムゾーンが UTC+5 に設定されているものとします.
47: * この場合, 内部のタイムゾーンと出力用のタイムゾーンの時差が -4 時間となるため,
48: * new Datetime(2012, 5, 21, 7, 30) の時間オブジェクトを書式化した結果は
49: * "2012-05-21T03:30+5:00" となります.
50: */
51: class W3cDatetimeFormat implements Format
52: {
53: /**
54: * タイムゾーンを扱うのみ TRUE となります
55: * @var bool
56: */
57: private $usingTz;
58:
59: /**
60: * システム時刻の時差です (単位は分)
61: * @var int
62: */
63: private $internalOffset;
64:
65: /**
66: * フォーマットの時差です (単位は分)
67: * @var int
68: */
69: private $externalOffset;
70:
71: /**
72: * 日付文字列のパターンです.
73: * "YYYY-MM-DD" のような形式にマッチします.
74: *
75: * @var string
76: */
77: private static $datePattern = "[0-9]{4}[^0-9][0-9]{2}[^0-9][0-9]{2}";
78:
79: /**
80: * タイムゾーンの文字列のパターンです. 以下の形式にマッチします.
81: *
82: * - "Z"
83: * - "-hh:mm"
84: * - "+hh:mm"
85: * - 空文字列
86: *
87: * @var string
88: */
89: private static $timeZonePattern = "(Z|[\\-\\+][0-9]{2}:[0-9]{2})?";
90:
91: /**
92: * 指定されたタイムゾーンの条件で, 新しいフォーマットを構築します.
93: * タイムゾーンを扱わない場合は, コンストラクタの代わりに
94: * {@link W3cDatetimeFormat::getInstance} を使って下さい.
95: *
96: * 引数 $externalOffset を指定することで,
97: * 書式化する際のタイムゾーンの値を設定することが出来ます.
98: * (デフォルトはシステム時刻のタイムゾーンとなります)
99: * さらに $internalOffset を指定することで,
100: * システム時刻のタイムゾーンとして任意の値を指定することも出来ます.
101: *
102: * $externalOffset と $internalOffset を省略した場合, デフォルト値としてシステム時刻の設定
103: * ({@link Util::getTimeZoneOffset}) と同じ値が指定されます.
104: * UTC+1 以上の時差を設定する場合は負の値,
105: * UTC-1 以下の時差を設定する場合は正の値を指定してください.
106: *
107: * 以下に, $externalOffset と $internalOffset の組み合わせによる動作例を示します.
108: *
109: * <code>
110: * // 書式・システム時刻共に UTC+9
111: * $f1 = new W3cDatetimeFormat(-540, -540);
112: *
113: * // 書式は GMT, システム時刻は UTC+9
114: * $f2 = new W3cDatetimeFormat(0, -540);
115: *
116: * // 書式は UTC+9, システム時刻は GMT
117: * $f3 = new W3cDatetimeFormat(-540, 0);
118: *
119: * // 2012-05-21T07:30 の時間オブジェクトを生成
120: * $d = new Datetime(2012, 5, 21, 7, 30);
121: * var_dump($d->format($f1)); // "2012-05-21T07:30+09:00"
122: * var_dump($d->format($f2)); // "2012-05-20T22:30Z" (UTC+9 の時刻を GMT として書式化)
123: * var_dump($d->format($f3)); // "2012-05-21T16:30+09:00" (GMT の時刻を UTC+9 として書式化)
124: * </code>
125: *
126: * @param int $externalOffset フォーマットの時差 (単位は分, 省略した場合はシステム設定の値を使用)
127: * @param int $internalOffset システム時刻の時差 (単位は分, 省略した場合はシステム設定の値を使用)
128: */
129: public function __construct($externalOffset = null, $internalOffset = null)
130: {
131: // @codeCoverageIgnoreStart
132: if ($externalOffset === false) {
133: $this->usingTz = false;
134: $this->externalOffset = null;
135: $this->internalOffset = null;
136: return;
137: }
138: // @codeCoverageIgnoreEnd
139:
140: $this->usingTz = true;
141: $this->externalOffset = Util::cleanTimeZoneOffset($externalOffset);
142: $this->internalOffset = Util::cleanTimeZoneOffset($internalOffset);
143: }
144:
145: /**
146: * デフォルトのインスタンスを返します.
147: * このメソッドから生成されたオブジェクトは, parse の際にタイムゾーンを一切考慮しません.
148: * また, 書式化する際にタイムゾーン文字列を付与しません.
149: *
150: * @return W3cDatetimeFormat タイムゾーンに対応しないインスタンス
151: * @codeCoverageIgnore
152: */
153: public static function getInstance()
154: {
155: static $instance = null;
156: if ($instance === null) {
157: $instance = new self(false);
158: }
159: return $instance;
160: }
161:
162: /**
163: * "YYYY-MM-DD" 形式の文字列を解析します.
164: *
165: * @param string $format 解析対象の文字列
166: * @return Date 解析結果
167: * @see Format::parseDate()
168: */
169: public function parseDate($format)
170: {
171: $dp = self::$datePattern;
172: if (preg_match("/^{$dp}/", $format)) {
173: $year = substr($format, 0, 4);
174: $month = substr($format, 5, 2);
175: $date = substr($format, 8, 2);
176: return new Date($year, $month, $date);
177: } else {
178: throw $this->createFormatException($format, "YYYY-MM-DD");
179: }
180: }
181:
182: /**
183: * "YYYY-MM-DDThh:mm" 形式の文字列を解析します.
184: * 文字列の末尾にタイムゾーン (+hh:mm や -hh:mm など) を含む場合は, システム時刻への変換を行います.
185: *
186: * @param string $format 解析対象の文字列
187: * @return Datetime 解析結果
188: * @see Format::parseDatetime()
189: */
190: public function parseDatetime($format)
191: {
192: $dp = self::$datePattern;
193: $tzp = self::$timeZonePattern;
194: $result = null;
195: if (!preg_match("/^{$dp}[^0-9][0-9]{2}:[0-9]{2}{$tzp}/", $format, $result)) {
196: throw $this->createFormatException($format, "YYYY-MM-DD hh:mm[timezone]");
197: }
198:
199: $year = substr($format, 0, 4);
200: $month = substr($format, 5, 2);
201: $date = substr($format, 8, 2);
202: $hour = substr($format, 11, 2);
203: $min = substr($format, 14, 2);
204: $obj = new Datetime($year, $month, $date, $hour, $min);
205: return ($this->usingTz && isset($result[1])) ?
206: $this->adjustFromParse($obj, $result[1]) : $obj;
207: }
208:
209: /**
210: * "YYYY-MM-DDThh:mm:ss" 形式の文字列を解析します.
211: *
212: * @param string $format 解析対象の文字列
213: * @return Timestamp 解析結果
214: * @see Format::parseTimestamp()
215: */
216: public function parseTimestamp($format)
217: {
218: $dp = self::$datePattern;
219: $tzp = self::$timeZonePattern;
220: $result = null;
221: if (!preg_match("/^{$dp}[^0-9][0-9]{2}:[0-9]{2}:[0-9]{2}{$tzp}/", $format, $result)) {
222: throw $this->createFormatException($format, "YYYY-MM-DD hh:mm:ss");
223: }
224:
225: $year = substr($format, 0, 4);
226: $month = substr($format, 5, 2);
227: $date = substr($format, 8, 2);
228: $hour = substr($format, 11, 2);
229: $min = substr($format, 14, 2);
230: $sec = substr($format, 17, 2);
231: $obj = new Timestamp($year, $month, $date, $hour, $min, $sec);
232: return ($this->usingTz && isset($result[1])) ?
233: $this->adjustFromParse($obj, $result[1]) : $obj;
234: }
235:
236: /**
237: * 指定された時間オブジェクトを "YYYY-MM-DD" 形式の文字列に変換します.
238: *
239: * @param Date $d 解析対象の Date オブジェクト
240: * @return string "YYYY-MM-DD" 形式の文字列
241: * @see Format::formatDate()
242: */
243: public function formatDate(Date $d)
244: {
245: return $d->__toString();
246: }
247:
248: /**
249: * 指定された時間オブジェクトを "YYYY-MM-DDThh:mm" 形式の文字列に変換します.
250: * このインスタンスがタイムゾーンに対応している場合, 末尾にタイムゾーン文字列も付加します.
251: *
252: * @param Datetime $d 変換対象の Datetime オブジェクト
253: * @return string "YYYY-MM-DDThh:mm" 形式の文字列
254: * @see Format::formatDatetime()
255: */
256: public function formatDatetime(Datetime $d)
257: {
258: $a = $this->adjustFromFormat($d, false);
259: return $this->formatDate($a->toDate()) . "T" . $a->formatTime() . $this->formatTimezone();
260: }
261:
262: /**
263: * 指定された時間オブジェクトを "YYYY-MM-DDThh:mm:ss" 形式の文字列に変換します.
264: * このインスタンスがタイムゾーンに対応している場合, 末尾にタイムゾーン文字列も付加します.
265: *
266: * @param Timestamp $d 変換対象の Timestamp オブジェクト
267: * @return string "YYYY-MM-DDThh:mm:ss" 形式の文字列
268: * @see Format::formatTimestamp()
269: */
270: public function formatTimestamp(Timestamp $d)
271: {
272: return $this->formatDatetime($d);
273: }
274:
275: /**
276: * 指定された文字列が想定されたフォーマットでないことをあらわす
277: * InvalidArgumentException を生成します.
278: *
279: * @param string $format
280: * @param string $expected
281: * @return \InvalidArgumentException
282: */
283: private function createFormatException($format, $expected)
284: {
285: return new \InvalidArgumentException("Illegal format({$format}). Expected: {$expected}");
286: }
287:
288: /**
289: * タイムゾーン文字列をパースして, 時差を分単位で返します.
290: * NULL が指定された場合 (タイムゾーン文字列がなかった場合),
291: * このオブジェクトに設定されている externalOffset の値を返します.
292: *
293: * @param string $tz NULL, "Z", または "+h:mm", "-h:mm" 形式の文字列
294: * @return int
295: */
296: private function parseTimezone($tz)
297: {
298: if ($tz === "Z") {
299: return 0;
300: }
301:
302: $coronIndex = strpos($tz, ":");
303: $sign = substr($tz, 0, 1);
304: $hour = substr($tz, 1, $coronIndex - 1);
305: $minute = substr($tz, $coronIndex + 1);
306: $offset = $hour * 60 + $minute;
307: return ($sign === "-") ? $offset : - $offset;
308: }
309:
310: /**
311: * フォーマットのタイムゾーンと, 時間オブジェクトのタイムゾーンを元に
312: * 指定された時間オブジェクトの補正処理を行います.
313: *
314: * @param Datetime $d 補正対象の時間オブジェクト
315: * @param string $tz タイムゾーン文字列
316: * @return Datetime 補正結果の時間オブジェクト
317: */
318: private function adjustFromParse(Datetime $d, $tz)
319: {
320: $externalOffset = $this->parseTimezone($tz);
321: return $d->add("minute", $externalOffset - $this->internalOffset);
322: }
323:
324: /**
325: * このオブジェクトがタイムゾーンを利用する場合は,
326: * 引数の時間オブジェクトを補正した結果を返します.
327: * タイムゾーンを利用しない場合は引数をそのまま返します.
328: * このメソッドは各種 format メソッドから呼ばれます.
329: *
330: * @param Datetime $d 補正対象の時間オブジェクト
331: * @return Datetime 補正結果
332: */
333: private function adjustFromFormat(Datetime $d)
334: {
335: return $this->usingTz ? $d->add("minute", $this->internalOffset - $this->externalOffset) : $d;
336: }
337:
338: /**
339: * タイムゾーンを書式化します.
340: * @return string
341: */
342: private function formatTimezone()
343: {
344: if (!$this->usingTz) {
345: return "";
346: }
347: if ($this->externalOffset === 0) {
348: return "Z";
349: }
350:
351: $ext = $this->externalOffset;
352: $format = (0 < $ext) ? "-" : "+";
353: $offset = abs($ext);
354: $hour = intval($offset / 60);
355: $minute = intval($offset % 60);
356: $format .= str_pad($hour, 2, '0', STR_PAD_LEFT);
357: $format .= ':';
358: $format .= str_pad($minute, 2, '0', STR_PAD_LEFT);
359: return $format;
360: }
361: }
362: