v / vlib / time / date_time_parser.v
403 lines · 386 sloc · 10.65 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1module time
2
3struct DateTimeParser {
4 datetime string
5 format string
6mut:
7 current_pos_datetime int
8}
9
10@[inline]
11fn (p &DateTimeParser) matches_at(chars string) bool {
12 end := p.current_pos_datetime + chars.len
13 if end > p.datetime.len {
14 return false
15 }
16 for i in 0 .. chars.len {
17 if p.datetime[p.current_pos_datetime + i] != chars[i] {
18 return false
19 }
20 }
21 return true
22}
23
24fn (mut p DateTimeParser) next(length int) !string {
25 if p.current_pos_datetime + length > p.datetime.len {
26 return error('end of string')
27 }
28 val := p.datetime[p.current_pos_datetime..p.current_pos_datetime + length]
29 p.current_pos_datetime += length
30 return val
31}
32
33fn (mut p DateTimeParser) peek(length int) !string {
34 if p.current_pos_datetime + length > p.datetime.len {
35 return error('end of string')
36 }
37 return p.datetime[p.current_pos_datetime..p.current_pos_datetime + length]
38}
39
40fn (mut p DateTimeParser) must_be_int(length int) !int {
41 end := p.current_pos_datetime + length
42 if end > p.datetime.len {
43 return error('end of string')
44 }
45 mut val := 0
46 for i in p.current_pos_datetime .. end {
47 ch := p.datetime[i]
48 if ch < `0` || ch > `9` {
49 return error('expected int, found: ${p.datetime[p.current_pos_datetime..end]}')
50 }
51 val = val * 10 + int(ch - `0`)
52 }
53 p.current_pos_datetime = end
54 return val
55}
56
57fn (mut p DateTimeParser) must_be_int_with_minimum_length(min int, max int, allow_leading_zero bool) !int {
58 max_len := max + 1 - min
59 start := p.current_pos_datetime
60 mut end := start
61 for _ in 0 .. max_len {
62 if end >= p.datetime.len {
63 break
64 }
65 ch := p.datetime[end]
66 if ch < `0` || ch > `9` {
67 break
68 }
69 end++
70 }
71 if end - start < min {
72 return error('expected int with a minimum length of ${min}, found: ${end - start}')
73 }
74 if !allow_leading_zero && p.datetime[start] == `0` {
75 return error('0 is not allowed for this format')
76 }
77 mut val := 0
78 for i in start .. end {
79 val = val * 10 + int(p.datetime[i] - `0`)
80 }
81 p.current_pos_datetime = end
82 return val
83}
84
85fn (mut p DateTimeParser) must_be_string(must string) ! {
86 start := p.current_pos_datetime
87 end := p.current_pos_datetime + must.len
88 if end > p.datetime.len {
89 return error('end of string')
90 }
91 if !p.matches_at(must) {
92 p.current_pos_datetime = end
93 return error('invalid string: "${p.datetime[start..end]}"!="${must}" at: ${p.current_pos_datetime}')
94 }
95 p.current_pos_datetime = end
96}
97
98fn (mut p DateTimeParser) must_be_string_one_of(oneof []string) !string {
99 for must in oneof {
100 val := p.peek(must.len) or { continue }
101 if val == must {
102 return must
103 }
104 }
105 return error('invalid string: must be one of ${oneof}, at: ${p.current_pos_datetime}')
106}
107
108fn (mut p DateTimeParser) must_be_valid_month() !int {
109 for v in long_months {
110 if p.current_pos_datetime + v.len < p.datetime.len && p.matches_at(v) {
111 p.current_pos_datetime += v.len
112 return long_months.index(v) + 1
113 }
114 }
115 return error_invalid_time(0, 'invalid month name, at: ${p.current_pos_datetime}')
116}
117
118fn (mut p DateTimeParser) must_be_valid_three_letter_month() !int {
119 if p.current_pos_datetime + 3 < p.datetime.len {
120 for m := 1; m <= long_months.len; m++ {
121 token := months_string[(m - 1) * 3..m * 3]
122 if p.matches_at(token) {
123 p.current_pos_datetime += 3
124 return m
125 }
126 }
127 }
128 return error_invalid_time(0, 'invalid three letter month, at: ${p.current_pos_datetime}')
129}
130
131fn (mut p DateTimeParser) must_be_valid_week_day() !string {
132 for v in long_days {
133 if p.current_pos_datetime + v.len < p.datetime.len && p.matches_at(v) {
134 p.current_pos_datetime += v.len
135 return v
136 }
137 }
138 return error_invalid_time(0, 'invalid weekday, at: ${p.current_pos_datetime}')
139}
140
141fn (mut p DateTimeParser) must_be_valid_two_letter_week_day() !int {
142 if p.current_pos_datetime + 2 < p.datetime.len {
143 for d := 1; d <= long_days.len; d++ {
144 token := days_string[(d - 1) * 3..d * 3 - 1]
145 if p.matches_at(token) {
146 p.current_pos_datetime += 2
147 return d
148 }
149 }
150 }
151 return error_invalid_time(0, 'invalid two letter weekday, at: ${p.current_pos_datetime}')
152}
153
154fn (mut p DateTimeParser) must_be_valid_three_letter_week_day() !int {
155 if p.current_pos_datetime + 3 < p.datetime.len {
156 for d := 1; d <= long_days.len; d++ {
157 token := days_string[(d - 1) * 3..d * 3]
158 if p.matches_at(token) {
159 p.current_pos_datetime += 3
160 return d
161 }
162 }
163 }
164 return error_invalid_time(0, 'invalid three letter weekday, at: ${p.current_pos_datetime}')
165}
166
167fn extract_tokens(s string) ![]string {
168 mut tokens := []string{}
169 if s.len == 0 {
170 return tokens
171 }
172 mut start := 0
173 for i := 1; i < s.len; i++ {
174 if s[i] != s[i - 1] {
175 tokens << s[start..i]
176 start = i
177 }
178 }
179 tokens << s[start..s.len]
180 return tokens
181}
182
183// parse_format parses the string `s`, as a custom `format`, containing the following specifiers:
184// YYYY - 4 digit year, 0000..9999
185// YY - 2 digit year, 00..99
186// M - month, 1..12
187// MM - month, 2 digits, 01..12
188// MMM - month, three letters, Jan..Dec
189// MMMM - name of month
190// D - day of the month, 1..31
191// DD - day of the month, 01..31
192// d - day of week, 0..6
193// c - day of week, 1..7
194// dd - day of week, Su..Sa
195// ddd - day of week, Sun..Sat
196// dddd - day of week, Sunday..Saturday
197// H - hour, 0..23
198// HH - hour, 00..23
199// h - hour, 0..23
200// hh - hour, 0..23
201// k - hour, 0..23
202// kk - hour, 0..23
203// m - minute, 0..59
204// mm - minute, 0..59
205// s - second, 0..59
206// ss - second, 0..59
207fn (mut p DateTimeParser) parse() !Time {
208 mut year_ := 0
209 mut month_ := 0
210 mut day_in_month := 0
211 mut hour_ := 0
212 mut minute_ := 0
213 mut second_ := 0
214 tokens := extract_tokens(p.format) or {
215 return error_invalid_time(0, 'malformed format string: ${err}')
216 }
217 for token in tokens {
218 match token {
219 'YYYY' {
220 year_ = p.must_be_int(4) or {
221 return error_invalid_time(0,
222 'end of string reached before the full year was specified')
223 }
224 }
225 'YY' {
226 year_ = now().year / 100 * 100 + p.must_be_int(2) or {
227 return error_invalid_time(0, 'end of string reached before the full year was specified')
228 }
229 }
230 'M' {
231 month_ = p.must_be_int_with_minimum_length(1, 2, false) or {
232 return error_invalid_time(0,
233 'end of string reached before the month was specified')
234 }
235 if month_ < 1 || month_ > 12 {
236 return error_invalid_time(0, 'month must be between 1 and 12')
237 }
238 }
239 'MM' {
240 month_ = p.must_be_int(2) or {
241 return error_invalid_time(0,
242 'end of string reached before the month was specified')
243 }
244 if month_ < 1 || month_ > 12 {
245 return error_invalid_time(0, 'month must be between 01 and 12')
246 }
247 }
248 'MMM' {
249 month_ = p.must_be_valid_three_letter_month() or { return err }
250 }
251 'MMMM' {
252 month_ = p.must_be_valid_month() or { return err }
253 }
254 'D' {
255 day_in_month = p.must_be_int_with_minimum_length(1, 2, false) or {
256 return error_invalid_time(0,
257 'end of string reached before the day was specified')
258 }
259 if day_in_month < 1 || day_in_month > 31 {
260 return error_invalid_time(0, 'day must be between 1 and 31')
261 }
262 }
263 'DD' {
264 day_in_month = p.must_be_int(2) or {
265 return error_invalid_time(0,
266 'end of string reached before the month was specified')
267 }
268 if day_in_month < 1 || day_in_month > 31 {
269 return error_invalid_time(0, 'day must be between 01 and 31')
270 }
271 }
272 'd' {
273 p.must_be_int(1) or { return err }
274 }
275 'c' {
276 p.must_be_int(1) or { return err }
277 }
278 'dd' {
279 p.must_be_valid_two_letter_week_day() or { return err }
280 }
281 'ddd' {
282 p.must_be_valid_three_letter_week_day() or { return err }
283 }
284 'dddd' {
285 p.must_be_valid_week_day() or { return err }
286 }
287 'H' {
288 hour_ = p.must_be_int_with_minimum_length(1, 2, true) or {
289 return error_invalid_time(0,
290 'end of string reached before hours where specified')
291 }
292 if hour_ < 0 || hour_ > 23 {
293 return error_invalid_time(0, 'hour must be between 0 and 23')
294 }
295 }
296 'HH' {
297 hour_ = p.must_be_int(2) or {
298 return error_invalid_time(0,
299 'end of string reached before hours where specified')
300 }
301 if hour_ < 0 || hour_ > 23 {
302 return error_invalid_time(0, 'hour must be between 00 and 23')
303 }
304 }
305 'h' {
306 hour_ = p.must_be_int_with_minimum_length(1, 2, true) or {
307 return error_invalid_time(0,
308 'end of string reached before hours where specified')
309 }
310 if hour_ < 0 || hour_ > 23 {
311 return error_invalid_time(0, 'hour must be between 0 and 23')
312 }
313 }
314 'hh' {
315 hour_ = p.must_be_int(2) or {
316 return error_invalid_time(0,
317 'end of string reached before hours where specified')
318 }
319 if hour_ < 0 || hour_ > 23 {
320 return error_invalid_time(0, 'hour must be between 00 and 23')
321 }
322 }
323 'k' {
324 hour_ = p.must_be_int(1) or {
325 return error_invalid_time(0,
326 'end of string reached before hours where specified')
327 }
328 if hour_ < 0 || hour_ > 23 {
329 return error_invalid_time(0, 'hour must be between 0 and 23')
330 }
331 }
332 'kk' {
333 hour_ = p.must_be_int(2) or {
334 return error_invalid_time(0,
335 'end of string reached before hours where specified')
336 }
337 if hour_ < 0 || hour_ > 23 {
338 return error_invalid_time(0, 'hour must be between 00 and 23')
339 }
340 }
341 'm' {
342 minute_ = p.must_be_int(1) or {
343 return error_invalid_time(0,
344 'end of string reached before minutes where specified')
345 }
346 if minute_ < 0 || minute_ > 59 {
347 return error_invalid_time(0, 'minute must be between 0 and 59')
348 }
349 }
350 'mm' {
351 minute_ = p.must_be_int(2) or {
352 return error_invalid_time(0,
353 'end of string reached before minutes where specified')
354 }
355 if minute_ < 0 || minute_ > 59 {
356 return error_invalid_time(0, 'minute must be between 00 and 59')
357 }
358 }
359 's' {
360 second_ = p.must_be_int(1) or {
361 return error_invalid_time(0,
362 'end of string reached before seconds where specified')
363 }
364 if second_ < 0 || second_ > 59 {
365 return error_invalid_time(0, 'second must be between 0 and 59')
366 }
367 }
368 'ss' {
369 second_ = p.must_be_int(2) or {
370 return error_invalid_time(0,
371 'end of string reached before seconds where specified')
372 }
373 if second_ < 0 || second_ > 59 {
374 return error_invalid_time(0, 'second must be between 00 and 59')
375 }
376 }
377 else {
378 p.must_be_string(token) or { return error_invalid_time(0, '${err}') }
379 }
380 }
381 }
382
383 if month_ == 2 {
384 feb_days_in_year := if is_leap_year(year_) { 29 } else { 28 }
385 if day_in_month > feb_days_in_year {
386 return error_invalid_time(0, 'February has only 28 days in the given year')
387 }
388 } else if day_in_month == 31 && month_ !in [1, 3, 5, 7, 8, 10, 12] {
389 month_name := Time{
390 month: month_
391 }.custom_format('MMMM')
392 return error_invalid_time(0, '${month_name} has only 30 days')
393 }
394
395 return new(
396 year: year_
397 month: month_
398 day: day_in_month
399 hour: hour_
400 minute: minute_
401 second: second_
402 )
403}
404