Class: Tilia::VObject::DateTimeParser

Inherits:
Object
  • Object
show all
Defined in:
lib/tilia/v_object/date_time_parser.rb

Overview

DateTimeParser.

This class is responsible for parsing the several different date and time formats iCalendar and vCards have.

Class Method Summary collapse

Class Method Details

.parse(date, reference_tz = nil) ⇒ DateTimeImmutable|DateInterval

Parses either a Date or DateTime, or Duration value.

Parameters:

  • date (String)
  • reference_tz (ActiveSupport::TimeZone|string) (defaults to: nil)

Returns:

  • (DateTimeImmutable|DateInterval)


129
130
131
132
133
134
135
136
137
# File 'lib/tilia/v_object/date_time_parser.rb', line 129

def self.parse(date, reference_tz = nil)
  if date[0] == 'P' || (date[0] == '-' && date[1] == 'P')
    parse_duration(date)
  elsif date.length == 8
    parse_date(date, reference_tz)
  else
    parse_date_time(date, reference_tz)
  end
end

.parse_date(date, tz = nil) ⇒ Time

Parses an iCalendar (rfc5545) formatted date and returns a DateTimeImmutable object.

Parameters:

  • date (String)
  • tz (ActiveSupport::TimeZone) (defaults to: nil)

Returns:

  • (Time)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/tilia/v_object/date_time_parser.rb', line 39

def self.parse_date(date, tz = nil)
  # Format is YYYYMMDD
  matches = /^([0-9]{4})([0-1][0-9])([0-3][0-9])$/.match(date)

  unless matches
    fail InvalidDataException, "The supplied iCalendar date value is incorrect: #{date}"
  end

  tz = ActiveSupport::TimeZone.new('UTC') if tz.nil?

  date = tz.parse("#{matches[1]}-#{matches[2]}-#{matches[3]}")

  date
end

.parse_date_time(dt, tz = nil) ⇒ Time

Parses an iCalendar (rfc5545) formatted datetime and returns a DateTimeImmutable object.

Specifying a reference timezone is optional. It will only be used if the non-UTC format is used. The argument is used as a reference, the returned DateTimeImmutable object will still be in the UTC timezone.

Parameters:

  • dt (String)
  • tz (ActiveSupport::TimeZone) (defaults to: nil)

Returns:

  • (Time)


19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/tilia/v_object/date_time_parser.rb', line 19

def self.parse_date_time(dt, tz = nil)
  # Format is YYYYMMDD + "T" + hhmmss
  matches = /^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/.match(dt)

  unless matches
    fail InvalidDataException, "The supplied iCalendar datetime value is incorrect: #{dt}"
  end

  tz = ActiveSupport::TimeZone.new('UTC') if matches[7] == 'Z' || tz.nil?
  date = tz.parse("#{matches[1]}-#{matches[2]}-#{matches[3]} #{matches[4]}:#{matches[5]}:#{matches[6]}")

  date
end

.parse_duration(duration, as_string = false) ⇒ DateInterval|string

Parses an iCalendar (RFC5545) formatted duration value.

This method will either return a DateTimeInterval object, or a string suitable for strtotime or DateTime::modify.

Parameters:

  • duration (String)
  • as_string (Boolean) (defaults to: false)

Returns:

  • (DateInterval|string)


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/tilia/v_object/date_time_parser.rb', line 63

def self.parse_duration(duration, as_string = false)
  matches = /^(?<plusminus>\+|-)?P((?<week>\d+)W)?((?<day>\d+)D)?(T((?<hour>\d+)H)?((?<minute>\d+)M)?((?<second>\d+)S)?)?$/.match(duration.to_s)

  unless matches
    fail InvalidDataException, "The supplied iCalendar duration value is incorrect: #{duration}"
  end

  unless as_string
    invert = false

    invert = true if matches['plusminus'] == '-'

    parts = [
      'week',
      'day',
      'hour',
      'minute',
      'second'
    ]

    new_matches = {}
    parts.each do |part|
      new_matches[part] = matches[part].to_i
    end
    matches = new_matches

    # We need to re-construct the duration string, because weeks and
    # days are not supported by DateInterval in the same string.
    duration = matches['week'].weeks +
               matches['day'].days +
               matches['hour'].hours +
               matches['minute'].minutes +
               matches['second'].seconds

    duration = -duration if invert

    return duration
  end

  parts = [
    'week',
    'day',
    'hour',
    'minute',
    'second'
  ]

  new_dur = ''

  parts.each do |part|
    new_dur += " #{matches[part]} #{part}s" if matches[part].to_i > 0
  end

  new_dur = (matches['plusminus'] == '-' ? '-' : '+') + new_dur.strip

  new_dur = '+0 seconds' if new_dur == '+'

  new_dur
end

.parse_v_card_date_and_or_time(date) ⇒ array

This method parses a vCard date and or time value.

This can be used for the DATE, DATE-TIME and DATE-AND-OR-TIME value.

This method returns an array, not a DateTime value. The elements in the array are in the following order:

year, month, date, hour, minute, second, timezone

Almost any part of the string may be omitted. It’s for example legal to just specify seconds, leave out the year, etc.

Timezone is either returned as ‘Z’ or as ‘+0800’

For any non-specified values null is returned.

List of date formats that are supported:

20150128
2015-01
--01
--0128
---28

List of supported time formats:

13
1353
135301
-53
-5301
--01 (unreachable, see the tests)
--01Z
--01+1234

List of supported date-time formats:

20150128T13
--0128T13
---28T13
---28T1353
---28T135301
---28T13Z
---28T13+1234

See the regular expressions for all the possible patterns.

Times may be postfixed by a timezone offset. This can be either ‘Z’ for UTC, or a string like -0500 or +1100.

Parameters:

  • date (String)

Returns:

  • (array)


422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/tilia/v_object/date_time_parser.rb', line 422

def self.parse_v_card_date_and_or_time(date)
  # \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d
  value_date     = /^(?:
                    (?<year>\d{4})(?<month>\d\d)(?<date>\d\d)
                    |(?<year0>\d{4})-(?<month0>\d\d)
                    |--(?<month1>\d\d)(?<date0>\d\d)?
                    |---(?<date1>\d\d)
                    )$/x

  # (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)?
  value_time     = /^(?:
                    ((?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?
                    |-(?<minute0>\d\d)(?<second0>\d\d)?
                    |--(?<second1>\d\d))
                    (?<timezone>(Z|[+\-]\d\d(\d\d)?))?
                    )$/x

  # (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)?
  value_date_time = /^(?:
                    ((?<year>\d{4})(?<month>\d\d)(?<date>\d\d)
                    |--(?<month0>\d\d)(?<date0>\d\d)
                    |---(?<date1>\d\d))
                    T
                    (?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?
                    (?<timezone>(Z|[+\-]\d\d(\d\d?)))?
                    )$/x

  # date-and-or-time is date | date-time | time
  # in this strict order.
  matches = value_date.match(date)
  matches = value_date_time.match(date) unless matches
  matches = value_time.match(date) unless matches
  unless matches
    fail InvalidDataException, "Invalid vCard date-time string: #{date}"
  end

  map = {
    'year'     => 'year',
    'year0'    => 'year',
    'month'    => 'month',
    'month0'   => 'month',
    'month1'   => 'month',
    'date'     => 'date',
    'date0'    => 'date',
    'date1'    => 'date',
    'hour'     => 'hour',
    'minute'   => 'minute',
    'minute0'  => 'minute',
    'second'   => 'second',
    'second0'  => 'second',
    'second1'  => 'second',
    'timezone' => 'timezone'
  }

  parts = {}
  map.each do |key, real_key|
    parts[real_key] ||= nil
    parts[real_key] = matches[key] if matches.names.include?(key) && matches[key]
  end

  parts
end

.parse_v_card_date_time(date) ⇒ array

This method parses a vCard date and or time value.

This can be used for the DATE, DATE-TIME, TIMESTAMP and DATE-AND-OR-TIME value.

This method returns an array, not a DateTime value.

The elements in the array are in the following order: year, month, date, hour, minute, second, timezone

Almost any part of the string may be omitted. It’s for example legal to just specify seconds, leave out the year, etc.

Timezone is either returned as ‘Z’ or as ‘+0800’

For any non-specified values null is returned.

List of date formats that are supported: YYYY YYYY-MM YYYYMMDD –MMDD —DD

YYYY-MM-DD –MM-DD —DD

List of supported time formats:

HH HHMM HHMMSS -MMSS –SS

HH HH:MM HH:MM:SS -MM:SS –SS

A full basic-format date-time string looks like : 20130603T133901

A full extended-format date-time string looks like : 2013-06-03T13:39:01

Times may be postfixed by a timezone offset. This can be either ‘Z’ for UTC, or a string like -0500 or +1100.

Parameters:

  • date (String)

Returns:

  • (array)


193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/tilia/v_object/date_time_parser.rb', line 193

def self.parse_v_card_date_time(date)
  regex = /^
      (?:  # date part
          (?:
              (?: (?<year> [0-9]{4}) (?: -)?| --)
              (?<month> [0-9]{2})?
          |---)
          (?<date> [0-9]{2})?
      )?
      (?:T  # time part
          (?<hour> [0-9]{2} | -)
          (?<minute> [0-9]{2} | -)?
          (?<second> [0-9]{2})?

          (?: \.[0-9]{3})? # milliseconds
          (?<timezone> # timezone offset

              Z | (?: \+|-)(?: [0-9]{4})

          )?

      )?
      $/x

  matches = regex.match(date)
  unless matches
    # Attempting to parse the extended format.
    regex = /^
        (?: # date part
            (?: (?<year> [0-9]{4}) - | -- )
            (?<month> [0-9]{2}) -
            (?<date> [0-9]{2})
        )?
        (?:T # time part

            (?: (?<hour> [0-9]{2}) : | -)
            (?: (?<minute> [0-9]{2}) : | -)?
            (?<second> [0-9]{2})?

            (?: \.[0-9]{3})? # milliseconds
            (?<timezone> # timezone offset

                Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})

            )?

        )?
        $/x

    matches = regex.match(date)
    unless matches
      fail InvalidDataException, "Invalid vCard date-time string: #{date}"
    end
  end

  parts = [
    'year',
    'month',
    'date',
    'hour',
    'minute',
    'second',
    'timezone'
  ]

  result = {}
  parts.each do |part|
    if matches[part].blank?
      result[part] = nil
    elsif matches[part] == '-' || matches[part] == '--'
      result[part] = nil
    else
      if part == 'timezone'
        result[part] = matches[part]
      else
        result[part] = matches[part].to_i
      end
    end
  end

  result
end

.parse_v_card_time(date) ⇒ array

This method parses a vCard TIME value.

This method returns an array, not a DateTime value.

The elements in the array are in the following order: hour, minute, second, timezone

Almost any part of the string may be omitted. It’s for example legal to just specify seconds, leave out the hour etc.

Timezone is either returned as ‘Z’ or as ‘+08:00’

For any non-specified values null is returned.

List of supported time formats:

HH HHMM HHMMSS -MMSS –SS

HH HH:MM HH:MM:SS -MM:SS –SS

A full basic-format time string looks like : 133901

A full extended-format time string looks like : 13:39:01

Times may be postfixed by a timezone offset. This can be either ‘Z’ for UTC, or a string like -0500 or +11:00.

Parameters:

  • date (String)

Returns:

  • (array)


316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/tilia/v_object/date_time_parser.rb', line 316

def self.parse_v_card_time(date)
  regex = /^
      (?<hour> [0-9]{2} | -)
      (?<minute> [0-9]{2} | -)?
      (?<second> [0-9]{2})?

      (?: \.[0-9]{3})? # milliseconds
      (?<timezone> # timezone offset

          Z | (?: \+|-)(?: [0-9]{4})

      )?
      $/x

  matches = regex.match(date)
  unless matches
    # Attempting to parse the extended format.
    regex = /^
        (?: (?<hour> [0-9]{2}) : | -)
        (?: (?<minute> [0-9]{2}) : | -)?
        (?<second> [0-9]{2})?

        (?: \.[0-9]{3})? # milliseconds
        (?<timezone> # timezone offset

            Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})

        )?
        $/x

    matches = regex.match(date)
    unless matches
      fail InvalidDataException, "Invalid vCard time string: #{date}"
    end
  end

  parts = [
    'hour',
    'minute',
    'second',
    'timezone'
  ]

  result = {}
  parts.each do |part|
    if matches[part].blank?
      result[part] = nil
    elsif matches[part] == '-' || matches[part] == '--'
      result[part] = nil
    else
      result[part] = matches[part]
    end
  end

  result
end