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: use Peach\Util\ArrayMap;
31: use Peach\DT\SimpleFormat\Pattern;
32: use Peach\DT\SimpleFormat\Numbers;
33: use Peach\DT\SimpleFormat\Raw;
34:
35: /**
36: * Java の
37: * {@link http://docs.oracle.com/javase/jp/7/api/java/text/SimpleDateFormat.html SimpleDateFormat}
38: * と同じような使い勝手で, ユーザー定義の書式を扱うことができるクラスです.
39: * 日付・時刻のパターンは {@link www.php.net/manual/function.date.php date()} の一部を採用しています.
40: *
41: * - Y: 年 (4桁固定)
42: * - m: 月 (2桁固定)
43: * - n: 月 (1~2桁)
44: * - d: 日 (2桁固定)
45: * - j: 日 (1~2桁)
46: * - H: 時 (2桁固定)
47: * - G: 時 (1~2桁)
48: * - i: 分 (2桁固定)
49: * - s: 秒 (2桁固定)
50: *
51: * 以下は, このクラス独自の拡張パターンです.
52: *
53: * - f: 分 (1~2桁)
54: * - b: 秒 (1~2桁)
55: * - E: 曜日 (後述)
56: *
57: * "2012年5月21日(月)" のように曜日を含む書式の入出力を行う場合は,
58: * コンストラクタの第 2 引数に曜日文字列の一覧を指定してください.
59: * 以下に例を示します.
60: * <code>
61: * $format = new SimpleFormat("Y年n月j日(E)", array("日", "月", "火", "水", "木", "金", "土"));
62: * </code>
63: * 第 2 引数を省略した場合はデフォルト値として array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
64: * が適用されます.
65: *
66: * パターンを繋げて記述する場合は (例: "Ymd_His" など)
67: * 必ず固定長のパターンを使用してください.
68: * 可変長 (n, j, G など) のパターンは常に最長一致でマッチングするため,
69: * 繋げて記述した際にパースに失敗する可能性があります.
70: *
71: * このクラスは, 月の文字列表記 (例えば "Jan", "Feb" など) のためのパターンをサポートしません.
72: * そのようなフォーマットが必要となった場合は, 独自の Format クラスを定義する必要があります.
73: *
74: * パースまたは書式化を行う際, 情報が足りない場合はデフォルト値が指定されます.
75: * 年・月・日については現在の日付, 時・分・秒については 0 が適用されます.
76: * 具体的には, 以下のような状況が相当します.
77: *
78: * 1. パースする際, オブジェクトを構成するために必要な情報をパターン文字列が網羅していなかった.
79: * (例: "m/d" というパターンをパースした場合,
80: * 「年」の情報がパターン文字列に含まれていないため, 現在の年が適用されます)
81: * 2. 時間オブジェクトを書式化する際に, パターン文字列に含まれるフィールドを
82: * そのオブジェクトが持っていなかった.
83: * (例: "Y/m/d H:i:s" というパターンで Date オブジェクトを書式化した場合,
84: * Date は時刻の情報を持たないため, 時刻部分は 00:00:00 となります.)
85: *
86: * PHP の date() 関数の実装と同様に,
87: * バックスラッシュをつけることでパターン文字列が展開されるのを抑制することができます.
88: *
89: * このクラスはイミュータブルです. 一つのオブジェクトを複数の箇所で使いまわすことが出来ます.
90: */
91: class SimpleFormat implements Format
92: {
93: /**
94: * parse または format に使うパターン文字列です.
95: * @var string
96: */
97: private $format;
98:
99: /**
100: * 曜日文字列の一覧を表す, 長さ 7 の配列です.
101: * @var array
102: */
103: private $dayList;
104:
105: /**
106: * Pattern オブジェクトの配列です.
107: * キーが "Y", "n" などのパターン文字, 値がその文字に該当する
108: * Pattern オブジェクトとなります.
109: *
110: * @var array
111: */
112: private $patternList;
113:
114: /**
115: * パターン文字列を分解した結果をあらわします.
116: *
117: * @var array
118: */
119: private $context;
120:
121: /**
122: * 指定されたパターン文字列で SimpleFormat を初期化します.
123: *
124: * @param string $pattern パターン文字列
125: * @param array $dayList 曜日文字列の配列. デフォルトは array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
126: */
127: public function __construct($pattern, array $dayList = array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"))
128: {
129: $format = strval($pattern);
130: $this->format = $format;
131: $this->dayList = $this->initDayList($dayList);
132: $this->patternList = $this->initPatternList($this->dayList);
133: $this->context = $this->createContext($format);
134: }
135:
136: /**
137: * 曜日文字列を初期化します.
138: * もしも配列の長さが 7 より大きかった場合, 8 個目以降の要素は無視されます.
139: *
140: * @param array $dayList 引数
141: * @return array 曜日文字列の配列
142: * @throws \InvalidArgumentException 配列の長さが 7 未満であるか, または空文字列が含まれている場合
143: */
144: private function initDayList(array $dayList)
145: {
146: $values = array_slice(array_values($dayList), 0, 7);
147: $count = count($values);
148: if ($count !== 7) {
149: throw new \InvalidArgumentException("Invalid array count({$count}). Expected: 7");
150: }
151: for ($i = 0; $i < 7; $i++) {
152: $value = $values[$i];
153: if (!strlen($value)) {
154: throw new \InvalidArgumentException("Daystring is empty at index {$i}");
155: }
156: }
157: return $values;
158: }
159:
160: /**
161: * パターン文字の一覧を作成します.
162: * @param array $dayList 曜日文字列の配列
163: * @return array Pattern オブジェクトの配列
164: */
165: private function initPatternList(array $dayList)
166: {
167: $patternList = $this->getDefaultPatternList();
168: $patternList["E"] = new Raw($dayList);
169: return $patternList;
170: }
171:
172: /**
173: * このオブジェクトのパターン文字列を返します.
174: * @return string パターン文字列
175: */
176: public function getFormat()
177: {
178: return $this->format;
179: }
180:
181: /**
182: * 指定された文字列を解析し, Date に変換します.
183: * @param string $format 解析対象の文字列
184: * @return Date 解析結果
185: */
186: public function parseDate($format)
187: {
188: $d = Date::now();
189: return $d->setAll($this->interpret($format));
190: }
191:
192: /**
193: * 指定された文字列を解析し, Datetime に変換します.
194: * @param string $format 解析対象の文字列
195: * @return Datetime 解析結果
196: */
197: public function parseDatetime($format)
198: {
199: $d = Date::now();
200: return $d->toDatetime()->setAll($this->interpret($format));
201: }
202:
203: /**
204: * 指定された文字列を解析し, Timestamp に変換します.
205: * @param string $format 解析対象の文字列
206: * @return Date 解析結果
207: */
208: public function parseTimestamp($format)
209: {
210: $d = Date::now();
211: return $d->toTimestamp()->setAll($this->interpret($format));
212: }
213:
214: /**
215: * 指定された Date オブジェクトを書式化します.
216: * @param Date $d 書式化対象の時間オブジェクト
217: * @return string このフォーマットによる文字列表現
218: */
219: public function formatDate(Date $d)
220: {
221: return $this->formatTimestamp($d->toTimestamp());
222: }
223:
224: /**
225: * 指定された Datetime オブジェクトを書式化します.
226: * @param Datetime $d 書式化対象の時間オブジェクト
227: * @return string このフォーマットによる文字列表現
228: */
229: public function formatDatetime(Datetime $d)
230: {
231: return $this->formatTimestamp($d->toTimestamp());
232: }
233:
234: /**
235: * 指定された Timestamp オブジェクトを書式化します.
236: * @param Timestamp $d 書式化対象の時間オブジェクト
237: * @return string このフォーマットによる文字列表現
238: */
239: public function formatTimestamp(Timestamp $d)
240: {
241: $patternList = $this->patternList;
242: $result = "";
243: foreach ($this->context as $part) {
244: $buf = array_key_exists($part, $patternList) ? $this->formatKey($d, $part) : stripslashes($part);
245: $result .= $buf;
246: }
247: return $result;
248: }
249:
250: /**
251: * パターン一覧を返します.
252: * キーが変換文字, 値がその文字に対応する Pattern オブジェクトとなります.
253: *
254: * @return array
255: * @codeCoverageIgnore
256: */
257: private function getDefaultPatternList()
258: {
259: static $patterns = null;
260: if (!isset($patterns)) {
261: $fixed4 = "\\d{4}";
262: $fixed2 = "\\d{2}";
263: $var2 = "[1-5][0-9]|[0-9]";
264: $varM = "1[0-2]|[1-9]";
265: $varD = "3[0-1]|[1-2][0-9]|[0-9]";
266: $varH = "2[0-4]|1[0-9]|[0-9]";
267: $patterns = array(
268: "Y" => new Numbers("year", $fixed4),
269: "m" => new Numbers("month", $fixed2),
270: "n" => new Numbers("month", $varM),
271: "d" => new Numbers("date", $fixed2),
272: "j" => new Numbers("date", $varD),
273: "H" => new Numbers("hour", $fixed2),
274: "G" => new Numbers("hour", $varH),
275: "i" => new Numbers("minute", $fixed2),
276: "f" => new Numbers("minute", $var2),
277: "s" => new Numbers("second", $fixed2),
278: "b" => new Numbers("second", $var2),
279: );
280: }
281: return $patterns;
282: }
283:
284: /**
285: * 指定された文字列に相当する Pattern オブジェクトを返します.
286: *
287: * @param string $part
288: * @return Pattern
289: * @codeCoverageIgnore
290: */
291: private function getPatternByPart($part)
292: {
293: $patterns = $this->patternList;
294: return array_key_exists($part, $patterns) ? $patterns[$part] : new Raw(array(stripslashes($part)));
295: }
296:
297: /**
298: * 指定されたパターン文字を, 対応するフィールドの値に変換します.
299: *
300: * @param Time $d 変換対象の時間オブジェクト
301: * @param string $key パターン文字 ("Y", "m", "d" など)
302: * @return int 変換結果
303: * @throws \Exception 不正なパターン文字が指定された場合
304: */
305: private function formatKey(Time $d, $key)
306: {
307: $year = $d->get("year");
308: $month = $d->get("month");
309: $date = $d->get("date");
310: $hour = $d->get("hour");
311: $min = $d->get("minute");
312: $sec = $d->get("second");
313:
314: switch ($key) {
315: case "Y":
316: return str_pad($year, 4, "0", STR_PAD_LEFT);
317: case "m":
318: return str_pad($month, 2, "0", STR_PAD_LEFT);
319: case "n":
320: return $month;
321: case "d":
322: return str_pad($date, 2, "0", STR_PAD_LEFT);
323: case "j":
324: return $date;
325: case "H":
326: return str_pad($hour, 2, "0", STR_PAD_LEFT);
327: case "G":
328: return $hour;
329: case "i":
330: return str_pad($min, 2, "0", STR_PAD_LEFT);
331: case "f":
332: return $min;
333: case "s":
334: return str_pad($sec, 2, "0", STR_PAD_LEFT);
335: case "b":
336: return $sec;
337: case "E":
338: return $this->dayList[$d->getDay()];
339: }
340:
341: // @codeCoverageIgnoreStart
342: throw new \Exception("Illegal pattern: " . $key);
343: // @codeCoverageIgnoreEnd
344: }
345:
346: /**
347: * 指定されたパターン文字列 (例えば "Y/m/d" など) を構文解析します.
348: *
349: * @param string $format パターン文字列
350: * @return array 解析結果
351: */
352: private function createContext($format)
353: {
354: $patternList = $this->patternList;
355: $result = array();
356: $current = "";
357: $escaped = false;
358: for ($i = 0, $length = strlen($format); $i < $length; $i ++) {
359: $chr = substr($format, $i, 1);
360: if ($escaped) {
361: $current .= $chr;
362: $escaped = false;
363: } else if ($chr === "\\") {
364: $current .= $chr;
365: $escaped = true;
366: } else if (array_key_exists($chr, $patternList)) {
367: if (strlen($current)) {
368: $result[] = $current;
369: $current = "";
370: }
371: $result[] = $chr;
372: } else {
373: $current .= $chr;
374: }
375: }
376: if (strlen($current)) {
377: $result[] = $current;
378: }
379: return $result;
380: }
381:
382: /**
383: * 指定されたテキストを構文解析します.
384: *
385: * @param string $text 解析対象の文字列
386: * @return ArrayMap 構文解析した結果
387: */
388: private function interpret($text)
389: {
390: $input = $text;
391: $result = new ArrayMap();
392: $matched = null;
393: foreach ($this->context as $part) {
394: $pattern = $this->getPatternByPart($part);
395: $matched = $pattern->match($input);
396: if ($matched === null) {
397: throw $this->createFormatException($input, $this->format);
398: }
399: $pattern->apply($result, $matched);
400: $input = substr($input, strlen($matched));
401: }
402: return $result;
403: }
404:
405: /**
406: * 文字列を時間オブジェクトに変換する際に, 不正な文字列が指定されたことをあらわす例外を生成します.
407: *
408: * @param string $format 指定された文字列
409: * @param string $expected 想定されるパターン文字列
410: * @return \InvalidArgumentException
411: */
412: private function createFormatException($format, $expected)
413: {
414: return new \InvalidArgumentException("Illegal format({$format}). Expected: {$expected}");
415: }
416: }
417: