|
1 package hirondelle.web4j.model; |
|
2 |
|
3 import java.util.regex.Matcher; |
|
4 import java.util.regex.Pattern; |
|
5 import static hirondelle.web4j.util.Consts.SPACE; |
|
6 |
|
7 /** |
|
8 Convert a date-time from a string into a {@link DateTime}. |
|
9 The primary use case for this class is converting date-times from a database <tt>ResultSet</tt> |
|
10 into a {@link DateTime}. It can also parse an ISO date-time containing a 'T' separator. |
|
11 */ |
|
12 final class DateTimeParser { |
|
13 |
|
14 /** |
|
15 Thrown when the given string cannot be converted into a <tt>DateTime</tt>, since it doesn't |
|
16 have a format allowed by this class. |
|
17 An unchecked exception. |
|
18 */ |
|
19 static final class UnknownDateTimeFormat extends RuntimeException { |
|
20 UnknownDateTimeFormat(String aMessage){ super(aMessage); } |
|
21 UnknownDateTimeFormat(String aMessage, Throwable aEx){ super(aMessage, aEx); } |
|
22 } |
|
23 |
|
24 DateTime parse(String aDateTime) { |
|
25 if(aDateTime == null){ |
|
26 throw new NullPointerException("DateTime string is null"); |
|
27 } |
|
28 String dateTime = aDateTime.trim(); |
|
29 Parts parts = splitIntoDateAndTime(dateTime); |
|
30 if (parts.hasTwoParts()) { |
|
31 parseDate(parts.datePart); |
|
32 parseTime(parts.timePart); |
|
33 } |
|
34 else if (parts.hasDateOnly()){ |
|
35 parseDate(parts.datePart); |
|
36 } |
|
37 else if (parts.hasTimeOnly()){ |
|
38 parseTime(parts.timePart); |
|
39 } |
|
40 DateTime result = new DateTime(fYear, fMonth, fDay, fHour, fMinute, fSecond, fNanosecond); |
|
41 return result; |
|
42 } |
|
43 |
|
44 // PRIVATE |
|
45 |
|
46 /** |
|
47 Gross pattern for dates. |
|
48 Detailed validation is done by DateTime. |
|
49 The Group index VARIES for y-m-d according to which option is selected |
|
50 Year: Group 1, 4, 6 |
|
51 Month: Group 2, 5 |
|
52 Day: Group 3 |
|
53 */ |
|
54 private static final Pattern DATE = Pattern.compile("(\\d{1,4})-(\\d\\d)-(\\d\\d)|(\\d{1,4})-(\\d\\d)|(\\d{1,4})"); |
|
55 |
|
56 /** |
|
57 Gross pattern for times. |
|
58 Detailed validation is done by DateTime. |
|
59 The Group index VARIES for h-m-s-f according to which option is selected |
|
60 Hour: Group 1, 5, 8, 10 |
|
61 Minute: Group 2, 6, 9 |
|
62 Second: Group 3, 7 |
|
63 Microsecond: Group 4 |
|
64 */ |
|
65 private static final String CL = "\\:"; //colon is a special character |
|
66 private static final String TT = "(\\d\\d)"; //colon is a special character |
|
67 private static final String NUM_DIGITS_FOR_FRACTIONAL_SECONDS = "9"; |
|
68 private static final Integer NUM_DIGITS = Integer.valueOf(NUM_DIGITS_FOR_FRACTIONAL_SECONDS); |
|
69 private static final Pattern TIME = Pattern.compile("" + |
|
70 TT+CL+TT+CL+TT+ "\\." + "(\\d{1," + NUM_DIGITS_FOR_FRACTIONAL_SECONDS + "})" + "|" + |
|
71 TT+CL+TT+CL+TT+ "|" + |
|
72 TT+CL+TT+ "|" + |
|
73 TT |
|
74 ); |
|
75 |
|
76 private static final String COLON = ":"; |
|
77 private static final int THIRD_POSITION = 2; |
|
78 |
|
79 private Integer fYear; |
|
80 private Integer fMonth; |
|
81 private Integer fDay; |
|
82 private Integer fHour; |
|
83 private Integer fMinute; |
|
84 private Integer fSecond; |
|
85 private Integer fNanosecond; |
|
86 |
|
87 private class Parts { |
|
88 String datePart; |
|
89 String timePart; |
|
90 boolean hasTwoParts(){ |
|
91 return datePart != null && timePart != null; |
|
92 } |
|
93 boolean hasDateOnly(){ |
|
94 return timePart == null; |
|
95 } |
|
96 boolean hasTimeOnly(){ |
|
97 return datePart == null; |
|
98 } |
|
99 } |
|
100 |
|
101 /** Date and time can be separated with a single space, or with a 'T' character (case-sensitive). */ |
|
102 private Parts splitIntoDateAndTime(String aDateTime){ |
|
103 Parts result = new Parts(); |
|
104 int dateTimeSeparator = getDateTimeSeparator(aDateTime); |
|
105 boolean hasDateTimeSeparator = 0 < dateTimeSeparator && dateTimeSeparator < aDateTime.length(); |
|
106 if (hasDateTimeSeparator){ |
|
107 result.datePart = aDateTime.substring(0, dateTimeSeparator); |
|
108 result.timePart = aDateTime.substring(dateTimeSeparator+1); |
|
109 } |
|
110 else if(hasColonInThirdPlace(aDateTime)){ |
|
111 result.timePart = aDateTime; |
|
112 } |
|
113 else { |
|
114 result.datePart = aDateTime; |
|
115 } |
|
116 return result; |
|
117 } |
|
118 |
|
119 /** Return the index of a space character, or of a 'T' character. If not found, return -1.*/ |
|
120 int getDateTimeSeparator(String aDateTime){ |
|
121 int NOT_FOUND = -1; |
|
122 int result = NOT_FOUND; |
|
123 result = aDateTime.indexOf(SPACE); |
|
124 if(result == NOT_FOUND){ |
|
125 result = aDateTime.indexOf("T"); |
|
126 } |
|
127 return result; |
|
128 } |
|
129 |
|
130 private boolean hasColonInThirdPlace(String aDateTime){ |
|
131 boolean result = false; |
|
132 if(aDateTime.length() >= THIRD_POSITION){ |
|
133 result = COLON.equals(aDateTime.substring(THIRD_POSITION,THIRD_POSITION+1)); |
|
134 } |
|
135 return result; |
|
136 } |
|
137 |
|
138 private void parseDate(String aDate) { |
|
139 Matcher matcher = DATE.matcher(aDate); |
|
140 if (matcher.matches()){ |
|
141 String year = getGroup(matcher, 1, 4, 6); |
|
142 if(year !=null ){ |
|
143 fYear = Integer.valueOf(year); |
|
144 } |
|
145 String month = getGroup(matcher, 2, 5); |
|
146 if(month !=null ){ |
|
147 fMonth = Integer.valueOf(month); |
|
148 } |
|
149 String day = getGroup(matcher, 3); |
|
150 if(day !=null ){ |
|
151 fDay = Integer.valueOf(day); |
|
152 } |
|
153 } |
|
154 else { |
|
155 throw new DateTimeParser.UnknownDateTimeFormat("Unexpected format for date:" + aDate); |
|
156 } |
|
157 } |
|
158 |
|
159 private String getGroup(Matcher aMatcher, int... aGroupIds){ |
|
160 String result = null; |
|
161 for(int id: aGroupIds){ |
|
162 result = aMatcher.group(id); |
|
163 if(result!=null) break; |
|
164 } |
|
165 return result; |
|
166 } |
|
167 |
|
168 private void parseTime(String aTime) { |
|
169 Matcher matcher = TIME.matcher(aTime); |
|
170 if (matcher.matches()){ |
|
171 String hour = getGroup(matcher, 1, 5, 8, 10); |
|
172 if(hour !=null ){ |
|
173 fHour = Integer.valueOf(hour); |
|
174 } |
|
175 String minute = getGroup(matcher, 2, 6, 9); |
|
176 if(minute !=null ){ |
|
177 fMinute = Integer.valueOf(minute); |
|
178 } |
|
179 String second = getGroup(matcher, 3, 7); |
|
180 if(second !=null ){ |
|
181 fSecond = Integer.valueOf(second); |
|
182 } |
|
183 String decimalSeconds = getGroup(matcher, 4); |
|
184 if(decimalSeconds !=null ){ |
|
185 fNanosecond = Integer.valueOf(convertToNanoseconds(decimalSeconds)); |
|
186 } |
|
187 } |
|
188 else { |
|
189 throw new DateTimeParser.UnknownDateTimeFormat("Unexpected format for time:" + aTime); |
|
190 } |
|
191 } |
|
192 |
|
193 /** |
|
194 Convert any number of decimals (1..9) into the form it would have taken if nanos had been used, |
|
195 by adding any 0's to the right side. |
|
196 */ |
|
197 private String convertToNanoseconds(String aDecimalSeconds){ |
|
198 StringBuilder result = new StringBuilder(aDecimalSeconds); |
|
199 while( result.length( ) < NUM_DIGITS ){ |
|
200 result.append("0"); |
|
201 } |
|
202 return result.toString(); |
|
203 } |
|
204 } |