iso8601 0.8.7 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/CONTRIBUTING.md +58 -0
- data/Gemfile +2 -2
- data/README.md +28 -102
- data/docs/date-time.md +86 -0
- data/docs/duration.md +77 -0
- data/docs/time-interval.md +120 -0
- data/iso8601.gemspec +43 -6
- data/lib/iso8601.rb +15 -7
- data/lib/iso8601/atomic.rb +78 -0
- data/lib/iso8601/date.rb +35 -10
- data/lib/iso8601/date_time.rb +14 -3
- data/lib/iso8601/days.rb +47 -0
- data/lib/iso8601/duration.rb +115 -99
- data/lib/iso8601/errors.rb +13 -1
- data/lib/iso8601/hours.rb +43 -0
- data/lib/iso8601/minutes.rb +43 -0
- data/lib/iso8601/months.rb +98 -0
- data/lib/iso8601/seconds.rb +47 -0
- data/lib/iso8601/time.rb +43 -15
- data/lib/iso8601/time_interval.rb +392 -0
- data/lib/iso8601/version.rb +1 -1
- data/lib/iso8601/weeks.rb +43 -0
- data/lib/iso8601/years.rb +80 -0
- data/spec/iso8601/date_spec.rb +0 -6
- data/spec/iso8601/date_time_spec.rb +0 -8
- data/spec/iso8601/days_spec.rb +44 -0
- data/spec/iso8601/duration_spec.rb +103 -99
- data/spec/iso8601/hours_spec.rb +44 -0
- data/spec/iso8601/minutes_spec.rb +44 -0
- data/spec/iso8601/months_spec.rb +86 -0
- data/spec/iso8601/seconds_spec.rb +44 -0
- data/spec/iso8601/time_interval_spec.rb +416 -0
- data/spec/iso8601/time_spec.rb +0 -6
- data/spec/iso8601/weeks_spec.rb +46 -0
- data/spec/iso8601/years_spec.rb +69 -0
- metadata +37 -19
- data/.dockerignore +0 -7
- data/.editorconfig +0 -9
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -38
- data/.travis.yml +0 -19
- data/Dockerfile +0 -10
- data/Makefile +0 -19
- data/circle.yml +0 -13
- data/lib/iso8601/atoms.rb +0 -279
- data/spec/iso8601/atoms_spec.rb +0 -329
data/lib/iso8601/errors.rb
CHANGED
@@ -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 <
|
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
|
data/lib/iso8601/time.rb
CHANGED
@@ -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'
|
95
|
+
second_format = format((second % 1).zero? ? '%02d' : '%04.1f', second)
|
87
96
|
|
88
|
-
"T%02d:%02d:#{second_format}#{zone}"
|
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 =
|
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,
|
120
|
+
fail ISO8601::Errors::UnknownPattern,
|
121
|
+
@original if hour.nil?
|
116
122
|
|
117
123
|
@separator = separator
|
118
|
-
require_separator =
|
124
|
+
require_separator = require_separator(minute)
|
119
125
|
|
120
126
|
hour = hour.to_i
|
121
127
|
minute = minute.to_i
|
122
|
-
second = second
|
128
|
+
second = parse_second(second)
|
123
129
|
|
124
130
|
atoms = [hour, minute, second, zone].compact
|
125
131
|
|
126
|
-
|
127
|
-
|
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
|
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
|