OSDN Git Service

Merge branch 'release/v4.101.2'
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / data / Anchor.java
1 /*
2  * anchor
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.data;
9
10 import java.util.LinkedList;
11 import java.util.List;
12 import java.util.regex.Matcher;
13 import java.util.regex.Pattern;
14
15 /**
16  * 発言アンカー。
17  */
18 public final class Anchor{
19
20     private static final int EPILOGUEDAY = 99;
21     private static final Pattern ANCHOR_PATTERN;
22
23     static{
24         String spchar = "\u0020\u3000\\t";
25         String sp = "[" +spchar+ "]";
26         String sp_n = "(?:" + sp + "|" + "(?:\\Q \\E)" + ")*?";
27
28         String day =   // TODO 「昨日」なども含めるか?
29                 "("
30                     +"(?:"
31                         +    "(プロ(?:ローグ)?)"
32                         +"|"+"(エピ(?:ローグ)?)"
33                         +"|"+"(?:"
34                                 + "([1-91-9]?[0-90-9])"
35                                 +sp_n+ "(?:[dDdD]|(?:日目?))"
36                             +")"
37                     +")" +"[\\-\\[\\(/_-ー―[_]?" +sp_n
38                 +")?";
39         String ampm =
40                 "("
41                     +"(?:"
42                         +    "((?:[aAaA][\\..]?[mMmM][\\..]?)|(?:午前))"
43                         +"|"+"((?:[pPpP][\\..]?[mMmM][\\..]?)|(?:午後))"
44                     +")" +sp_n
45                 +")?";
46         String hhmm =
47                 "(?:"
48                     +"("
49                         +"([0-20-2]?[0-90-9])"
50                             +sp_n+ "[:;:;]?" +sp_n
51                         +"([0-50-5][0-90-9])"
52                     +")"
53                         +"|"
54                     +"("
55                         +"([0-20-2]?[0-90-9])"
56                             +sp_n+ "時" +sp_n
57                         +"([0-50-5]?[0-90-9])"
58                             +sp_n+ "分"
59                     +")"
60                 +")";
61
62         String talkNum =
63                 "(?:>>([1-9][0-9]{0,8}))";
64
65         ANCHOR_PATTERN = Pattern.compile(day + ampm + hhmm +"|"+ talkNum,
66                                          Pattern.DOTALL);
67     }
68
69
70     private final CharSequence source;
71     private final int startPos;
72     private final int endPos;
73     private final int day;
74     private final int hour;
75     private final int minute;
76     private final int talkNo;
77
78
79     /**
80      * アンカーのコンストラクタ。
81      * @param source アンカーが含まれる文字列
82      * @param startPos アンカーの始まる位置
83      * @param endPos アンカーの終わる位置
84      * @param talkNo 公開発言番号
85      */
86     private Anchor(CharSequence source, int startPos, int endPos,
87                     int talkNo ){
88         super();
89
90         if(talkNo <= 0) throw new IllegalArgumentException();
91
92         this.source = source;
93         this.startPos = startPos;
94         this.endPos = endPos;
95         this.day = -1;
96         this.hour = -1;
97         this.minute = -1;
98         this.talkNo = talkNo;
99
100         return;
101     }
102
103     /**
104      * アンカーのコンストラクタ。
105      * @param source アンカーが含まれる文字列
106      * @param startPos アンカーの始まる位置
107      * @param endPos アンカーの終わる位置
108      * @param day 日
109      * @param hour 時間(0-23)
110      * @param minute 分(0-59)
111      */
112     private Anchor(CharSequence source, int startPos, int endPos,
113                     int day, int hour, int minute                 ){
114         super();
115
116         this.source = source;
117         this.startPos = startPos;
118         this.endPos = endPos;
119         this.day = day;
120         this.hour = hour;
121         this.minute = minute;
122         this.talkNo = -1;
123
124         return;
125     }
126
127
128     /**
129      * 与えられた範囲指定文字列からアンカーを抽出する。
130      * @param source 検索対象文字列
131      * @param regionStart 範囲開始位置
132      * @param regionEnd 範囲終了位置
133      * @param currentDay 相対日付の基本となる日
134      * @return アンカー
135      */
136     public static Anchor getAnchor(CharSequence source,
137                                     int regionStart,
138                                     int regionEnd,
139                                     int currentDay      ){
140         Matcher matcher = ANCHOR_PATTERN.matcher(source);
141         matcher.region(regionStart, regionEnd);
142
143         if( ! matcher.find() ) return null;
144
145         Anchor anchor = getAnchorFromMatched(source, matcher, currentDay);
146
147         return anchor;
148     }
149
150     /**
151      * 与えられた文字列から全アンカーを抽出する。
152      * @param source 検索対象文字列
153      * @param currentDay 相対日付の基本となる日
154      * @return アンカーのリスト(出現順)
155      */
156     public static List<Anchor> getAnchorList(CharSequence source,
157                                                int currentDay      ){
158         List<Anchor> result = new LinkedList<>();
159
160         Matcher matcher = ANCHOR_PATTERN.matcher(source);
161         int regionEnd = source.length();
162
163         while(matcher.find()){
164             Anchor anchor = getAnchorFromMatched(source, matcher, currentDay);
165             result.add(anchor);
166             int regionStart = matcher.end();
167             matcher.region(regionStart, regionEnd);
168         }
169
170         return result;
171     }
172
173     /**
174      * 文字列とそのMatcherからアンカーを抽出する。
175      * @param source 検索対象文字列
176      * @param matcher Matcher
177      * @param currentDay 相対日付の基本となる日
178      * @return アンカー
179      */
180     private static Anchor getAnchorFromMatched(CharSequence source,
181                                                   Matcher matcher,
182                                                   int currentDay){
183         int startPos = matcher.start();
184         int endPos   = matcher.end();
185
186         /* G国アンカー */
187         if(matcher.start(14) < matcher.end(14)){
188             int talkNo = parseInt(source, matcher, 14);
189             Anchor anchor = new Anchor(source, startPos, endPos, talkNo);
190             return anchor;
191         }
192
193         int day = currentDay;
194         if(matcher.start(1) < matcher.end(1)){
195             if(matcher.start(2) < matcher.end(2)){ // prologue
196                 day = 0;
197             }else if(matcher.start(3) < matcher.end(3)){ // epilogue
198                 day = EPILOGUEDAY;
199             }else if(matcher.start(4) < matcher.end(4)){  // etc) "6d"
200                 day = parseInt(source, matcher, 4);
201             }else{
202                 assert false;
203                 return null;
204             }
205         }
206
207         boolean isPM = false;
208         if(matcher.start(5) < matcher.end(5)){
209             if(matcher.start(6) < matcher.end(6)){        // AM
210                 isPM = false;
211             }else if(matcher.start(7) < matcher.end(7)){  // PM
212                 isPM = true;
213             }else{
214                 assert false;
215                 return null;
216             }
217         }
218
219         int hourGroup;
220         int minuteGroup;
221         if(matcher.start(8) < matcher.end(8)){   // hhmm hmm hh:mm
222             hourGroup = 9;
223             minuteGroup = 10;
224         }else if(matcher.start(11) < matcher.end(11)){   // h時m分
225             hourGroup = 12;
226             minuteGroup = 13;
227         }else{
228             assert false;
229             return null;
230         }
231         int hour   = parseInt(source, matcher, hourGroup);
232         int minute = parseInt(source, matcher, minuteGroup);
233
234         if(isPM && hour < 12) hour += 12;
235         hour %= 24;
236         // 午後12:34は午後00:34になる
237
238         // TODO 3d25:30 は 3d01:30 か 4d01:30 どちらにすべきか?
239         // とりあえず前者
240
241         Anchor anchor = new Anchor(source, startPos, endPos,
242                                    day, hour, minute);
243
244         return anchor;
245     }
246
247     /**
248      * 正規表現にマッチした領域を数値化する。
249      *
250      * @param seq 文字列
251      * @param matcher Matcher
252      * @param groupIndex 前方指定グループ番号
253      * @return 数値
254      * @throws IndexOutOfBoundsException 不正なグループ番号
255      */
256     static int parseInt(CharSequence seq,
257                         Matcher matcher,
258                         int groupIndex )
259             throws IndexOutOfBoundsException {
260         int startPos = matcher.start(groupIndex);
261         int endPos   = matcher.end(groupIndex);
262         return parseInt(seq, startPos, endPos);
263     }
264
265     /**
266      * 部分文字列を数値化する。
267      *
268      * @param seq 文字列
269      * @param startPos 範囲開始位置
270      * @param endPos 範囲終了位置
271      * @return パースした数値
272      * @throws IndexOutOfBoundsException 不正な位置指定
273      */
274     static int parseInt(CharSequence seq, int startPos, int endPos)
275             throws IndexOutOfBoundsException{
276         int result = 0;
277
278         for(int pos = startPos; pos < endPos; pos++){
279             char ch = seq.charAt(pos);
280             int digit = Character.digit(ch, 10);
281             if(digit < 0) break;
282             result *= 10;
283             result += digit;
284         }
285
286         return result;
287     }
288
289
290     /**
291      * アンカーの含まれる文字列を返す。
292      * @return アンカーの含まれる文字列
293      */
294     public CharSequence getSource(){
295         return this.source;
296     }
297
298     /**
299      * アンカーの開始位置を返す。
300      * @return アンカー開始位置
301      */
302     public int getStartPos(){
303         return this.startPos;
304     }
305
306     /**
307      * アンカーの終了位置を返す。
308      * @return アンカー終了位置
309      */
310     public int getEndPos(){
311         return this.endPos;
312     }
313
314     /**
315      * アンカーの示す日付を返す。
316      * @return 日付
317      */
318     public int getDay(){
319         return this.day;
320     }
321
322     /**
323      * アンカーの示す時刻を返す。
324      * @return 時刻(0-23)
325      */
326     public int getHour(){
327         return this.hour;
328     }
329
330     /**
331      * アンカーの示す分を返す。
332      * @return 分(0-59)
333      */
334     public int getMinute(){
335         return this.minute;
336     }
337
338     /**
339      * アンカーの示す公開発言番号を返す。
340      * @return 公開発言番号。公開発言番号でない場合は0以下の値。
341      */
342     public int getTalkNo(){
343         return this.talkNo;
344     }
345
346     /**
347      * このアンカーが公開発言番号による物か判定する。
348      * @return 公開発言番号由来であるならtrue
349      */
350     public boolean hasTalkNo(){
351         return 0 < this.talkNo;
352     }
353
354     /**
355      * 明示的なエピローグへのアンカーか判定する。
356      * @return 明示的なエピローグへのアンカーならtrue
357      */
358     public boolean isEpilogueDay(){
359         if(this.day >= EPILOGUEDAY) return true;
360         return false;
361     }
362
363     /**
364      * アンカーの文字列表記を返す。
365      * 出典:まとめサイトの用語集
366      * @return アンカーの文字列表記
367      */
368     @Override
369     public String toString(){
370         /* G国表記 */
371         if(hasTalkNo()){
372             return ">>" + this.talkNo;
373         }
374
375         StringBuilder result = new StringBuilder();
376
377         result.append(getDay()).append('d');
378
379         int anchorHour = getHour();
380         if(anchorHour < 10) result.append('0');
381         result.append(anchorHour).append(':');
382
383         int anchorMinute = getMinute();
384         if(anchorMinute < 10) result.append('0');
385         result.append(anchorMinute);
386
387         return result.toString();
388     }
389
390 }