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.
- 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
|