iso8601 0.8.7 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/CONTRIBUTING.md +58 -0
  4. data/Gemfile +2 -2
  5. data/README.md +28 -102
  6. data/docs/date-time.md +86 -0
  7. data/docs/duration.md +77 -0
  8. data/docs/time-interval.md +120 -0
  9. data/iso8601.gemspec +43 -6
  10. data/lib/iso8601.rb +15 -7
  11. data/lib/iso8601/atomic.rb +78 -0
  12. data/lib/iso8601/date.rb +35 -10
  13. data/lib/iso8601/date_time.rb +14 -3
  14. data/lib/iso8601/days.rb +47 -0
  15. data/lib/iso8601/duration.rb +115 -99
  16. data/lib/iso8601/errors.rb +13 -1
  17. data/lib/iso8601/hours.rb +43 -0
  18. data/lib/iso8601/minutes.rb +43 -0
  19. data/lib/iso8601/months.rb +98 -0
  20. data/lib/iso8601/seconds.rb +47 -0
  21. data/lib/iso8601/time.rb +43 -15
  22. data/lib/iso8601/time_interval.rb +392 -0
  23. data/lib/iso8601/version.rb +1 -1
  24. data/lib/iso8601/weeks.rb +43 -0
  25. data/lib/iso8601/years.rb +80 -0
  26. data/spec/iso8601/date_spec.rb +0 -6
  27. data/spec/iso8601/date_time_spec.rb +0 -8
  28. data/spec/iso8601/days_spec.rb +44 -0
  29. data/spec/iso8601/duration_spec.rb +103 -99
  30. data/spec/iso8601/hours_spec.rb +44 -0
  31. data/spec/iso8601/minutes_spec.rb +44 -0
  32. data/spec/iso8601/months_spec.rb +86 -0
  33. data/spec/iso8601/seconds_spec.rb +44 -0
  34. data/spec/iso8601/time_interval_spec.rb +416 -0
  35. data/spec/iso8601/time_spec.rb +0 -6
  36. data/spec/iso8601/weeks_spec.rb +46 -0
  37. data/spec/iso8601/years_spec.rb +69 -0
  38. metadata +37 -19
  39. data/.dockerignore +0 -7
  40. data/.editorconfig +0 -9
  41. data/.gitignore +0 -19
  42. data/.rubocop.yml +0 -38
  43. data/.travis.yml +0 -19
  44. data/Dockerfile +0 -10
  45. data/Makefile +0 -19
  46. data/circle.yml +0 -13
  47. data/lib/iso8601/atoms.rb +0 -279
  48. data/spec/iso8601/atoms_spec.rb +0 -329
@@ -2,8 +2,11 @@ module ISO8601
2
2
  ##
3
3
  # Contains all ISO8601-specific errors.
4
4
  module Errors
5
+ ##
6
+ # Catch-all exception.
5
7
  class StandardError < ::StandardError
6
8
  end
9
+
7
10
  ##
8
11
  # Raised when the given pattern doesn't fit as ISO 8601 parser.
9
12
  class UnknownPattern < StandardError
@@ -11,6 +14,7 @@ module ISO8601
11
14
  super("Unknown pattern #{pattern}")
12
15
  end
13
16
  end
17
+
14
18
  ##
15
19
  # Raised when the given pattern contains an invalid fraction.
16
20
  class InvalidFractions < StandardError
@@ -18,6 +22,7 @@ module ISO8601
18
22
  super("Fractions are only allowed in the last component")
19
23
  end
20
24
  end
25
+
21
26
  ##
22
27
  # Raised when the given date is valid but out of range.
23
28
  class RangeError < StandardError
@@ -25,10 +30,17 @@ module ISO8601
25
30
  super("#{pattern} is out of range")
26
31
  end
27
32
  end
33
+
28
34
  ##
29
35
  # Raised when the type is unexpected
30
- class TypeError < StandardError
36
+ class TypeError < ::ArgumentError
37
+ end
38
+
39
+ ##
40
+ # Raised when the interval is unexpected
41
+ class IntervalError < StandardError
31
42
  end
43
+
32
44
  ##
33
45
  # Raise when the base is not suitable.
34
46
  class DurationBaseError < StandardError
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ module ISO8601
4
+ ##
5
+ # The Hours atom in a {ISO8601::Duration}
6
+ class Hours
7
+ include Atomic
8
+
9
+ AVERAGE_FACTOR = 3600
10
+
11
+ ##
12
+ # @param [Numeric] atom The atom value
13
+ def initialize(atom)
14
+ valid_atom?(atom)
15
+
16
+ @atom = atom
17
+ end
18
+
19
+ ##
20
+ # The Week factor
21
+ #
22
+ # @return [Numeric]
23
+ def factor
24
+ AVERAGE_FACTOR
25
+ end
26
+
27
+ ##
28
+ # The amount of seconds
29
+ #
30
+ # @return [Numeric]
31
+ def to_seconds
32
+ AVERAGE_FACTOR * atom
33
+ end
34
+
35
+ ##
36
+ # The atom symbol.
37
+ #
38
+ # @return [Symbol]
39
+ def symbol
40
+ :H
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ module ISO8601
4
+ ##
5
+ # The Minutes atom in a {ISO8601::Duration}
6
+ class Minutes
7
+ include Atomic
8
+
9
+ AVERAGE_FACTOR = 60
10
+
11
+ ##
12
+ # @param [Numeric] atom The atom value
13
+ def initialize(atom)
14
+ valid_atom?(atom)
15
+
16
+ @atom = atom
17
+ end
18
+
19
+ ##
20
+ # The Minute factor
21
+ #
22
+ # @return [Numeric]
23
+ def factor
24
+ AVERAGE_FACTOR
25
+ end
26
+
27
+ ##
28
+ # The amount of seconds
29
+ #
30
+ # @return [Numeric]
31
+ def to_seconds
32
+ AVERAGE_FACTOR * atom
33
+ end
34
+
35
+ ##
36
+ # The atom symbol.
37
+ #
38
+ # @return [Symbol]
39
+ def symbol
40
+ :M
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ module ISO8601
4
+ ##
5
+ # A Months atom in a {ISO8601::Duration}
6
+ #
7
+ # A "calendar month" is the time interval resulting from the division of a
8
+ # "calendar year" in 12 time intervals.
9
+ #
10
+ # A "duration month" is the duration of 28, 29, 30 or 31 "calendar days"
11
+ # depending on the start and/or the end of the corresponding time interval
12
+ # within the specific "calendar month".
13
+ class Months
14
+ include Atomic
15
+
16
+ ##
17
+ # The "duration month" average is calculated through time intervals of 400
18
+ # "duration years". Each cycle of 400 "duration years" has 303 "common
19
+ # years" of 365 "calendar days" and 97 "leap years" of 366 "calendar days".
20
+ AVERAGE_FACTOR = Years::AVERAGE_FACTOR / 12
21
+
22
+ ##
23
+ # @param [Numeric] atom The atom value
24
+ def initialize(atom)
25
+ valid_atom?(atom)
26
+
27
+ @atom = atom
28
+ end
29
+
30
+ ##
31
+ # The Month factor
32
+ #
33
+ # @param [ISO8601::DateTime, nil] base (nil) The base datetime to compute
34
+ # the month length.
35
+ #
36
+ # @return [Numeric]
37
+ def factor(base = nil)
38
+ return AVERAGE_FACTOR if base.nil?
39
+ return zero_calculation(base) if atom.zero?
40
+
41
+ calculation(atom, base)
42
+ end
43
+
44
+ ##
45
+ # The amount of seconds
46
+ #
47
+ # @param [ISO8601::DateTime, nil] base (nil) The base datetime to compute
48
+ # the month length.
49
+ #
50
+ # @return [Numeric]
51
+ def to_seconds(base = nil)
52
+ valid_base?(base)
53
+
54
+ return (AVERAGE_FACTOR * atom) if base.nil?
55
+ return zero_calculation(base) if atom.zero?
56
+
57
+ calculation(atom, base) * atom
58
+ end
59
+
60
+ ##
61
+ # The atom symbol.
62
+ #
63
+ # @return [Symbol]
64
+ def symbol
65
+ :M
66
+ end
67
+
68
+ private
69
+
70
+ def zero_calculation(base)
71
+ month = (base.month <= 12) ? base.month : (base.month % 12)
72
+ year = base.year + ((base.month) / 12).to_i
73
+
74
+ (::Time.utc(year, month) - ::Time.utc(base.year, base.month))
75
+ end
76
+
77
+ def calculation(atom, base)
78
+ initial = base.month + atom
79
+ if initial <= 0
80
+ month = base.month + atom
81
+
82
+ if initial % 12 == 0
83
+ year = base.year + (initial / 12) - 1
84
+ month = 12
85
+ else
86
+ year = base.year + (initial / 12).floor
87
+ month = (12 + initial > 0) ? (12 + initial) : (12 + (initial % -12))
88
+ end
89
+ else
90
+ month = (initial <= 12) ? initial : (initial % 12)
91
+ month = 12 if month.zero?
92
+ year = base.year + ((base.month + atom) / 12).to_i
93
+ end
94
+
95
+ (::Time.utc(year, month) - ::Time.utc(base.year, base.month)) / atom
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ module ISO8601
4
+ ##
5
+ # The Seconds atom in a {ISO8601::Duration}
6
+ #
7
+ # The second is the base unit of measurement of time in the International
8
+ # System of Units (SI) as defined by the International Committee of Weights
9
+ # and Measures.
10
+ class Seconds
11
+ include Atomic
12
+
13
+ AVERAGE_FACTOR = 1
14
+
15
+ ##
16
+ # @param [Numeric] atom The atom value
17
+ def initialize(atom)
18
+ valid_atom?(atom)
19
+
20
+ @atom = atom
21
+ end
22
+
23
+ ##
24
+ # The Second factor
25
+ #
26
+ # @return [Numeric]
27
+ def factor
28
+ AVERAGE_FACTOR
29
+ end
30
+
31
+ ##
32
+ # The amount of seconds
33
+ #
34
+ # @return [Numeric]
35
+ def to_seconds
36
+ atom
37
+ end
38
+
39
+ ##
40
+ # The atom symbol.
41
+ #
42
+ # @return [Symbol]
43
+ def symbol
44
+ :S
45
+ end
46
+ end
47
+ end
@@ -17,12 +17,15 @@ module ISO8601
17
17
  :to_time, :to_date, :to_datetime,
18
18
  :hour, :minute, :zone
19
19
  )
20
+
20
21
  ##
21
22
  # The separator used in the original ISO 8601 string.
22
23
  attr_reader :separator
24
+
23
25
  ##
24
26
  # The second atom
25
27
  attr_reader :second
28
+
26
29
  ##
27
30
  # The original atoms
28
31
  attr_reader :atoms
@@ -37,6 +40,7 @@ module ISO8601
37
40
  @time = compose(@atoms, @base)
38
41
  @second = @time.second + @time.second_fraction.to_f.round(1)
39
42
  end
43
+
40
44
  ##
41
45
  # @param [#hash] other The contrast to compare against
42
46
  #
@@ -44,6 +48,7 @@ module ISO8601
44
48
  def ==(other)
45
49
  (hash == other.hash)
46
50
  end
51
+
47
52
  ##
48
53
  # @param [#hash] other The contrast to compare against
49
54
  #
@@ -51,11 +56,13 @@ module ISO8601
51
56
  def eql?(other)
52
57
  (hash == other.hash)
53
58
  end
59
+
54
60
  ##
55
61
  # @return [Fixnum]
56
62
  def hash
57
63
  [atoms, self.class].hash
58
64
  end
65
+
59
66
  ##
60
67
  # Forwards the time the given amount of seconds.
61
68
  #
@@ -68,6 +75,7 @@ module ISO8601
68
75
 
69
76
  self.class.new(moment.strftime('T%H:%M:%S.%L%:z'), base)
70
77
  end
78
+
71
79
  ##
72
80
  # Backwards the date the given amount of seconds.
73
81
  #
@@ -80,13 +88,15 @@ module ISO8601
80
88
 
81
89
  self.class.new(moment.strftime('T%H:%M:%S.%L%:z'), base)
82
90
  end
91
+
83
92
  ##
84
93
  # Converts self to a time component representation.
85
94
  def to_s
86
- second_format = (second % 1).zero? ? '%02d' % second : '%04.1f' % second
95
+ second_format = format((second % 1).zero? ? '%02d' : '%04.1f', second)
87
96
 
88
- "T%02d:%02d:#{second_format}#{zone}" % atoms
97
+ format("T%02d:%02d:#{second_format}#{zone}", *atoms)
89
98
  end
99
+
90
100
  ##
91
101
  # Converts self to an array of atoms.
92
102
  def to_a
@@ -104,30 +114,47 @@ module ISO8601
104
114
  #
105
115
  # @return [Array<Integer, Float>]
106
116
  def atomize(input)
107
- _, time, zone = /^T?(.+?)(Z|[+-].+)?$/.match(input).to_a
108
-
109
- _, hour, separator, minute, second = /^(?:
110
- (\d{2})(:?)(\d{2})\2(\d{2}(?:[.,]\d+)?) |
111
- (\d{2})(:?)(\d{2}) |
112
- (\d{2})
113
- )$/x.match(time).to_a.compact
117
+ _, time, zone = parse_timezone(input)
118
+ _, hour, separator, minute, second = parse_time(time)
114
119
 
115
- fail ISO8601::Errors::UnknownPattern, @original if hour.nil?
120
+ fail ISO8601::Errors::UnknownPattern,
121
+ @original if hour.nil?
116
122
 
117
123
  @separator = separator
118
- require_separator = !minute.nil?
124
+ require_separator = require_separator(minute)
119
125
 
120
126
  hour = hour.to_i
121
127
  minute = minute.to_i
122
- second = second.nil? ? 0.0 : second.tr(',', '.').to_f
128
+ second = parse_second(second)
123
129
 
124
130
  atoms = [hour, minute, second, zone].compact
125
131
 
126
-
127
- fail ISO8601::Errors::UnknownPattern, @original unless valid_zone?(zone, require_separator)
132
+ fail ISO8601::Errors::UnknownPattern,
133
+ @original unless valid_zone?(zone, require_separator)
128
134
 
129
135
  atoms
130
136
  end
137
+
138
+ def require_separator(input)
139
+ !input.nil?
140
+ end
141
+
142
+ def parse_timezone(timezone)
143
+ /^T?(.+?)(Z|[+-].+)?$/.match(timezone).to_a
144
+ end
145
+
146
+ def parse_time(time)
147
+ /^(?:
148
+ (\d{2})(:?)(\d{2})\2(\d{2}(?:[.,]\d+)?) |
149
+ (\d{2})(:?)(\d{2}) |
150
+ (\d{2})
151
+ )$/x.match(time).to_a.compact
152
+ end
153
+
154
+ def parse_second(second)
155
+ second.nil? ? 0.0 : second.tr(',', '.').to_f
156
+ end
157
+
131
158
  ##
132
159
  # @param [String] zone The timezone offset as Z or +-hh[:mm].
133
160
  # @param [Boolean] require_separator Flag to determine if the separator
@@ -138,12 +165,13 @@ module ISO8601
138
165
  _, offset, separator = zone_regexp.match(zone).to_a.compact
139
166
 
140
167
  wrong_pattern = !zone.nil? && offset.nil?
141
- if (require_separator)
168
+ if require_separator
142
169
  invalid_separators = zone.to_s.match(/^[+-]\d{2}:?\d{2}$/) && (@separator != separator)
143
170
  end
144
171
 
145
172
  !(wrong_pattern || invalid_separators)
146
173
  end
174
+
147
175
  ##
148
176
  # Wraps ::DateNew.new to play nice with ArgumentError.
149
177
  #
@@ -0,0 +1,392 @@
1
+ module ISO8601
2
+ ##
3
+ # A Time Interval representation.
4
+ # See https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
5
+ #
6
+ # @example
7
+ # ti = ISO8601::TimeInterval.parse('P1MT2H/2014-05-28T19:53Z')
8
+ # ti.size # => 2635200.0
9
+ # ti2 = ISO8601::TimeInterval.parse('2014-05-28T19:53Z/2014-05-28T20:53Z')
10
+ # ti2.to_f # => 3600.0
11
+ #
12
+ # @example
13
+ # start_time = ISO8601::DateTime.new('2014-05-28T19:53Z')
14
+ # end_time = ISO8601::DateTime.new('2014-05-30T19:53Z')
15
+ # ti = ISO8601::TimeInterval.from_datetimes(start_time, end_time)
16
+ # ti.size # => 172800.0 (Seconds)
17
+ #
18
+ # @example
19
+ # duration = ISO8601::Duration.new('P1MT2H')
20
+ # end_time = ISO8601::DateTime.new('2014-05-30T19:53Z')
21
+ # ti = ISO8601::TimeInterval.from_duration(duration, end_time)
22
+ # ti.size # => 2635200.0 (Seconds)
23
+ #
24
+ # @example
25
+ # start_time = ISO8601::DateTime.new('2014-05-30T19:53Z')
26
+ # duration = ISO8601::Duration.new('P1MT2H', base)
27
+ # ti = ISO8601::TimeInterval.from_duration(start_time, duration)
28
+ # ti.size # => 2635200.0 (Seconds)
29
+ #
30
+ class TimeInterval
31
+ include Comparable
32
+
33
+ ##
34
+ # Initializes a time interval based on two time points.
35
+ #
36
+ # @overload from_datetimes(start_time, end_time)
37
+ # @param [ISO8601::DateTime] start_time The start time point of the
38
+ # interval.
39
+ # @param [ISO8601::DateTime] end_time The end time point of the interaval.
40
+ #
41
+ # @raise [ISO8601::Errors::TypeError] If both params are not instances of
42
+ # `ISO8601::DateTime`.
43
+ #
44
+ # @return [ISO8601::TimeInterval]
45
+ def self.from_datetimes(*atoms)
46
+ guard_from_datetimes(atoms, 'Start and end times must instances of ISO8601::DateTime')
47
+ new(atoms)
48
+ end
49
+
50
+ ##
51
+ # Initializes a TimeInterval based on a `ISO8601::Duration` and a
52
+ # `ISO8601::DateTime`. The order of the params define the strategy to
53
+ # compute the interval.
54
+ #
55
+ # @overload from_duration(start_time, duration)
56
+ # Equivalent to the `<start>/<duration>` pattern.
57
+ # @param [ISO8601::DateTime] start_time The start time point of the
58
+ # interval.
59
+ # @param [ISO8601::Duration] duration The size of the interval.
60
+ #
61
+ # @overload from_duration(duration, end_time)
62
+ # Equivalent to the `<duration>/<end>` pattern.
63
+ # @param [ISO8601::Duration] duration The size of the interval.
64
+ # @param [ISO8601::DateTime] end_time The end time point of the interaval.
65
+ #
66
+ # @raise [ISO8601::Errors::TypeError] If the params aren't a mix of
67
+ # `ISO8601::DateTime` and `ISO8601::Duration`.
68
+ #
69
+ # @return [ISO8601::TimeInterval]
70
+ def self.from_duration(*atoms)
71
+ guard_from_duration(atoms, 'Expected one date time and one duration')
72
+ new(atoms)
73
+ end
74
+
75
+ ##
76
+ # Dispatches the constructor based on the type of the input.
77
+ #
78
+ # @overload new(pattern)
79
+ # Parses a pattern.
80
+ # @param [String] input A time interval pattern.
81
+ #
82
+ # @overload new([start_time, duration])
83
+ # Equivalent to the `<start>/<duration>` pattern.
84
+ # @param [Array<(ISO8601::DateTime, ISO8601::Duration)>] input
85
+ #
86
+ # @overload new([duration, end_time])
87
+ # Equivalent to the `<duration>/<end>` pattern.
88
+ # @param [Array<(ISO8601::Duration, ISO8601::DateTime)>] input
89
+ #
90
+ # @return [ISO8601::TimeInterval]
91
+ def initialize(input)
92
+ case input
93
+ when String
94
+ parse(input)
95
+ when Array
96
+ from_atoms(input)
97
+ else
98
+ fail(ISO8601::Errors::TypeError, 'The pattern must be a String or a Hash')
99
+ end
100
+ end
101
+
102
+ ##
103
+ # Alias of `initialize` to have a closer interface to the core `Time`,
104
+ # `Date` and `DateTime` interfaces.
105
+ def self.parse(pattern)
106
+ new(pattern)
107
+ end
108
+
109
+ ##
110
+ # The start time (first) of the interval.
111
+ #
112
+ # @return [ISO8601::DateTime] start time
113
+ attr_reader :first
114
+ alias_method :start_time, :first
115
+
116
+ ##
117
+ # The end time (last) of the interval.
118
+ #
119
+ # @return [ISO8601::DateTime] end time
120
+ attr_reader :last
121
+ alias_method :end_time, :last
122
+
123
+ ##
124
+ # The pattern for the interval.
125
+ #
126
+ # @return [String] The pattern of this interval
127
+ def pattern
128
+ return @pattern if @pattern
129
+
130
+ "#{@atoms.first}/#{@atoms.last}"
131
+ end
132
+ alias_method :to_s, :pattern
133
+
134
+ ##
135
+ # The size of the interval. If any bound is a Duration, the
136
+ # size of the interval is the number of seconds of the interval.
137
+ #
138
+ # @return [Float] Size of the interval in seconds
139
+ attr_reader :size
140
+ alias_method :to_f, :size
141
+ alias_method :length, :size
142
+
143
+ ##
144
+ # Checks if the interval is empty.
145
+ #
146
+ # @return [Boolean]
147
+ def empty?
148
+ first == last
149
+ end
150
+
151
+ ##
152
+ # Check if a given time is inside the current TimeInterval.
153
+ #
154
+ # @param [#to_time] other DateTime to check if it's
155
+ # inside the current interval.
156
+ #
157
+ # @raise [ISO8601::Errors::TypeError] if time param is not a compatible
158
+ # Object.
159
+ #
160
+ # @return [Boolean]
161
+ def include?(other)
162
+ fail(ISO8601::Errors::TypeError, 'The parameter must respond_to #to_time') \
163
+ unless other.respond_to?(:to_time)
164
+
165
+ (first.to_time <= other.to_time &&
166
+ last.to_time >= other.to_time)
167
+ end
168
+ alias_method :member?, :include?
169
+
170
+ ##
171
+ # Returns true if the interval is a subset of the given interval.
172
+ #
173
+ # @param [ISO8601::TimeInterval] other a time interval.
174
+ #
175
+ # @raise [ISO8601::Errors::TypeError] if time param is not a compatible
176
+ # Object.
177
+ #
178
+ # @return [Boolean]
179
+ def subset?(other)
180
+ fail(ISO8601::Errors::TypeError, "The parameter must be an instance of #{self.class}") \
181
+ unless other.is_a?(self.class)
182
+
183
+ other.include?(first) && other.include?(last)
184
+ end
185
+
186
+ ##
187
+ # Returns true if the interval is a superset of the given interval.
188
+ #
189
+ # @param [ISO8601::TimeInterval] other a time interval.
190
+ #
191
+ # @raise [ISO8601::Errors::TypeError] if time param is not a compatible
192
+ # Object.
193
+ #
194
+ # @return [Boolean]
195
+ def superset?(other)
196
+ fail(ISO8601::Errors::TypeError, "The parameter must be an instance of #{self.class}") \
197
+ unless other.is_a?(self.class)
198
+
199
+ include?(other.first) && include?(other.last)
200
+ end
201
+
202
+ ##
203
+ # Check if two intervarls intersect.
204
+ #
205
+ # @param [ISO8601::TimeInterval] other Another interval to check if they
206
+ # intersect.
207
+ #
208
+ # @raise [ISO8601::Errors::TypeError] if the param is not a TimeInterval.
209
+ #
210
+ # @return [Boolean]
211
+ def intersect?(other)
212
+ fail(ISO8601::Errors::TypeError,
213
+ "The parameter must be an instance of #{self.class}") \
214
+ unless other.is_a?(self.class)
215
+
216
+ include?(other.first) || include?(other.last)
217
+ end
218
+
219
+ ##
220
+ # Return the intersection between two intervals.
221
+ #
222
+ # @param [ISO8601::TimeInterval] other time interval
223
+ #
224
+ # @raise [ISO8601::Errors::TypeError] if the param is not a TimeInterval.
225
+ #
226
+ # @return [Boolean]
227
+ def intersection(other)
228
+ fail(ISO8601::Errors::IntervalError, "The intervals are disjoint") \
229
+ if disjoint?(other) && other.disjoint?(self)
230
+
231
+ return self if subset?(other)
232
+ return other if other.subset?(self)
233
+
234
+ a, b = sort_pair(self, other)
235
+ self.class.from_datetimes(b.first, a.last)
236
+ end
237
+
238
+ ##
239
+ # Check if two intervarls have no element in common. This method is the
240
+ # opposite of `#intersect?`.
241
+ #
242
+ # @param [ISO8601::TimeInterval] other Time interval.
243
+ #
244
+ # @raise [ISO8601::Errors::TypeError] if the param is not a TimeInterval.
245
+ #
246
+ # @return [Boolean]
247
+ def disjoint?(other)
248
+ !intersect?(other)
249
+ end
250
+
251
+ ##
252
+ # @param [ISO8601::TimeInterval] other
253
+ #
254
+ # @return [-1, 0, 1, nil]
255
+ def <=>(other)
256
+ return nil unless other.is_a?(self.class)
257
+
258
+ to_f <=> other.to_f
259
+ end
260
+
261
+ ##
262
+ # Equality by hash.
263
+ #
264
+ # @param [ISO8601::TimeInterval] other
265
+ #
266
+ # @return [Boolean]
267
+ def eql?(other)
268
+ (hash == other.hash)
269
+ end
270
+
271
+ ##
272
+ # @return [Fixnum]
273
+ def hash
274
+ @atoms.hash
275
+ end
276
+
277
+ private
278
+
279
+ # Initialize a TimeInterval ISO8601 by a pattern. If you initialize it with
280
+ # a duration pattern, the second argument is mandatory because you need to
281
+ # specify an start/end point to calculate the interval.
282
+ #
283
+ # @param [String] pattern This parameter define a full time interval. These
284
+ # patterns are defined in the ISO8601:
285
+ # * <start_time>/<end_time>
286
+ # * <start_time>/<duration>
287
+ # * <duration>/<end_time>
288
+ #
289
+ # @raise [ISO8601::Errors::UnknownPattern] If given pattern is not a valid
290
+ # ISO8601 pattern.
291
+ def parse(pattern)
292
+ fail(ISO8601::Errors::UnknownPattern, pattern) unless pattern.include?('/')
293
+
294
+ @pattern = pattern
295
+ subpatterns = pattern.split('/')
296
+
297
+ fail(ISO8601::Errors::UnknownPattern, pattern) if subpatterns.size != 2
298
+
299
+ @atoms = subpatterns.map { |x| parse_subpattern(x) }
300
+ @first, @last, @size = limits(@atoms)
301
+ end
302
+
303
+ def sort_pair(a, b)
304
+ (a.first < b.first) ? [a, b] : [b, a]
305
+ end
306
+
307
+ ##
308
+ # Parses a subpattern to a correct type.
309
+ #
310
+ # @param [String] pattern
311
+ #
312
+ # @return [ISO8601::Duration, ISO8601::DateTime]
313
+ def parse_subpattern(pattern)
314
+ return ISO8601::Duration.new(pattern) if pattern.start_with?('P')
315
+
316
+ ISO8601::DateTime.new(pattern)
317
+ end
318
+
319
+ ##
320
+ # See the constructor methods.
321
+ #
322
+ # @param [Array] atoms
323
+ def from_atoms(atoms)
324
+ @atoms = atoms
325
+ @first, @last, @size = limits(@atoms)
326
+ end
327
+
328
+ ##
329
+ # Calculates the limits (first, last) and the size of the interval.
330
+ #
331
+ # @param [Array] atoms The atoms result of parsing the pattern.
332
+ #
333
+ # @return [Array<(ISO8601::DateTime, ISO8601::DateTime, ISO8601::Duration)>]
334
+ def limits(atoms)
335
+ valid_atoms?(atoms)
336
+
337
+ if atoms.none? { |x| x.is_a?(ISO8601::Duration) }
338
+ return tuple_by_both(atoms)
339
+ elsif atoms.first.is_a?(ISO8601::Duration)
340
+ return tuple_by_end(atoms)
341
+ else
342
+ return tuple_by_start(atoms)
343
+ end
344
+ end
345
+
346
+ def tuple_by_both(atoms)
347
+ [atoms.first,
348
+ atoms.last,
349
+ (atoms.last.to_time - atoms.first.to_time)]
350
+ end
351
+
352
+ def tuple_by_end(atoms)
353
+ seconds = atoms.first.to_seconds(atoms.last)
354
+ [(atoms.last - seconds),
355
+ atoms.last,
356
+ seconds]
357
+ end
358
+
359
+ def tuple_by_start(atoms)
360
+ seconds = atoms.last.to_seconds(atoms.first)
361
+ [atoms.first,
362
+ (atoms.first + seconds),
363
+ seconds]
364
+ end
365
+
366
+ def valid_atoms?(atoms)
367
+ fail(ISO8601::Errors::UnknownPattern,
368
+ "The pattern of a time interval can't be <duration>/<duration>") \
369
+ if atoms.all? { |x| x.is_a?(ISO8601::Duration) }
370
+ end
371
+
372
+ def valid_date_time?(time)
373
+ self.valid_date_time?(time)
374
+ end
375
+
376
+ def self.valid_date_time?(time, message = 'Expected a ISO8601::DateTime')
377
+ return true if time.is_a?(ISO8601::DateTime)
378
+
379
+ fail(ISO8601::Errors::TypeError, message)
380
+ end
381
+
382
+ def self.guard_from_datetimes(atoms, message)
383
+ atoms.all? { |x| valid_date_time?(x, message) }
384
+ end
385
+
386
+ def self.guard_from_duration(atoms, message)
387
+ fail(ISO8601::Errors::TypeError, message) \
388
+ unless atoms.any? { |x| x.is_a?(ISO8601::Duration) } &&
389
+ atoms.any? { |x| x.is_a?(ISO8601::DateTime) }
390
+ end
391
+ end
392
+ end