iso8601 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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