iso8601 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f6495a0f183eb9f89a063e0404b9001fd024b90d
4
- data.tar.gz: c9bedd70b4ecc51ad6c1094a4f714b5d2a17fdcc
3
+ metadata.gz: a046b00234ecd1ead9531234435f3b28bb1e89d3
4
+ data.tar.gz: 53e15b12514e255b2a5aaeb451e041047b2d6ef0
5
5
  SHA512:
6
- metadata.gz: 2dbe7ba46843e37298b2a4766752e74d240546ec6232e9dd1aa0e2449589351d18440ed0bf1b982c39353141d5a91b84b8f031b29697d5f09616bb6eadbfd33d
7
- data.tar.gz: ced9dcc8204013b265f69e6c8a2407247386202026c83e19296acd99ec44b06a4811b7785e9becc1cae7a49c8a4005c64c4e4a519f7f06decb8bdfd727991b0f
6
+ metadata.gz: 4be163f63f8e4d488ecb8d6c997e17031445ab0b124644f8c0b6d33ac60cbc9b9b21f5009ba0a185f8cb08cbc7e4a3de9900c9fdccf3589fd75ea5a6cb8e2d99
7
+ data.tar.gz: a10fa5e91adbd1717954b81809cdf206e34f676f07f27add1cceb1530d782c7a3df5b5e8052265f10f915578538f3417c8f7890d97fe4bceedf11d2b0061ae22
data/CHANGELOG.md CHANGED
@@ -1,22 +1,32 @@
1
- ## Changes for 0.7.0
1
+ ## 0.8.0
2
+
3
+ * `DateTime` has hash identity by value.
4
+ * `Time` has hash identity by value.
5
+ * `Date` has hash identity by value.
6
+ * `Duration` has hash identity by value.
7
+ * `Atom` has hash identity by value.
8
+ * `Atom#value` returns either an integer or a float.
9
+ * `Atom#to_s` returns a valid ISO8601 subpattern.
10
+
11
+ ## 0.7.0
2
12
 
3
13
  * Add decimal fractions for any component in a duration.
4
14
  * Add a catch all `ISO8601::Errors::StandardError`.
5
15
  * Add support for comma (`,`) as a separator for duration decimal fractions.
6
16
 
7
- ## Changes for 0.6.0
17
+ ## 0.6.0
8
18
 
9
19
  * Add `#hash` to `Duration`, `Date`, `Time` and `DateTime`.
10
20
 
11
- ## Changes for 0.5.2
21
+ ## 0.5.2
12
22
 
13
23
  * Fix `DateTime` when handling empty strings.
14
24
 
15
- ## Changes for 0.5.1
25
+ ## 0.5.1
16
26
 
17
27
  * Fix durations with sign.
18
28
 
19
- ## Changes for 0.5
29
+ ## 0.5.0
20
30
 
21
31
  * Drop support for Ruby 1.8.7.
22
32
  * Add support for Rubinius 2.
data/README.md CHANGED
@@ -84,16 +84,16 @@ Week dates raise an error when two digit days provied instead of return monday:
84
84
  DateTime.new('2014-W15-02') # => #<Date: 2014-04-07 ((2456755j,0s,0n),+0s,2299161j)>
85
85
 
86
86
 
87
-
88
- ## TODO
89
-
90
- * Recurring time intervals
91
-
92
-
93
- ## Contributors
87
+ ## Contributing
94
88
 
95
89
  [Contributors](https://github.com/arnau/ISO8601/graphs/contributors)
96
90
 
91
+ 1. Fork it (http://github.com/arnau/ISO8601/fork)
92
+ 2. Create your feature branch (git checkout -b features/xyz)
93
+ 3. Commit your changes (git commit -am 'Add XYZ')
94
+ 4. Push to the branch (git push origin features/xyz)
95
+ 5. Create new Pull Request
96
+
97
97
 
98
98
  ## License
99
99
 
data/iso8601.gemspec CHANGED
@@ -25,6 +25,6 @@ Gem::Specification.new do |s|
25
25
 
26
26
  s.has_rdoc = 'yard'
27
27
 
28
- s.add_development_dependency 'rspec', '~> 2.14'
28
+ s.add_development_dependency 'rspec', '~> 3.1'
29
29
  s.add_development_dependency 'rerun', '~> 0'
30
30
  end
data/lib/iso8601.rb CHANGED
@@ -8,5 +8,5 @@ require 'iso8601/errors'
8
8
  require 'iso8601/atoms'
9
9
  require 'iso8601/date'
10
10
  require 'iso8601/time'
11
- require 'iso8601/dateTime'
11
+ require 'iso8601/date_time'
12
12
  require 'iso8601/duration'
data/lib/iso8601/atoms.rb CHANGED
@@ -8,28 +8,75 @@ module ISO8601
8
8
  class Atom
9
9
  ##
10
10
  # @param [Numeric] atom The atom value
11
- # @param [ISO8601::DateTime, nil] base (nil) The base datetime to
12
- # compute the atom factor.
11
+ # @param [ISO8601::DateTime, nil] base (nil) The base datetime to compute
12
+ # the atom factor.
13
13
  def initialize(atom, base=nil)
14
- raise TypeError, "The atom argument for #{self.inspect} should be a Numeric value." unless atom.kind_of? Numeric
15
- raise TypeError, "The base argument for #{self.inspect} should be a ISO8601::DateTime instance or nil." unless base.kind_of? ISO8601::DateTime or base.nil?
14
+ raise TypeError, "The atom argument for #{self.inspect} should be a Numeric value." unless atom.kind_of?(Numeric)
15
+ raise TypeError, "The base argument for #{self.inspect} should be a ISO8601::DateTime instance or nil." unless base.kind_of?(ISO8601::DateTime) || base.nil?
16
16
  @atom = atom
17
17
  @base = base
18
18
  end
19
+ attr_reader :atom
20
+ attr_reader :base
19
21
  ##
20
- # The integer representation of the atom
22
+ # The integer representation
23
+ #
24
+ # @return [Integer]
21
25
  def to_i
22
- @atom.to_i
26
+ atom.to_i
27
+ end
28
+ ##
29
+ # The float representation
30
+ #
31
+ # @return [Float]
32
+ def to_f
33
+ atom.to_f
23
34
  end
24
35
  ##
25
- # The amount of seconds of the atom
36
+ # Returns the ISO 8601 representation for the atom
37
+ #
38
+ # @return [String]
39
+ def to_s
40
+ (value.zero?) ? '' : "#{value}#{symbol}"
41
+ end
42
+ ##
43
+ # The simplest numeric representation. If modulo equals 0 returns an
44
+ # integer else a float.
45
+ #
46
+ # @return [Numeric]
47
+ def value
48
+ (atom % 1).zero? ? atom.to_i : atom
49
+ end
50
+ ##
51
+ # The amount of seconds
52
+ #
53
+ # @return [Numeric]
26
54
  def to_seconds
27
- @atom * self.factor
55
+ atom * factor
56
+ end
57
+ ##
58
+ # @param [#hash] contrast The contrast to compare against
59
+ #
60
+ # @return [Boolean]
61
+ def ==(contrast)
62
+ (hash == contrast.hash)
63
+ end
64
+ ##
65
+ # @param [#hash] contrast The contrast to compare against
66
+ #
67
+ # @return [Boolean]
68
+ def eql?(contrast)
69
+ (hash == contrast.hash)
70
+ end
71
+ ##
72
+ # @return [Fixnum]
73
+ def hash
74
+ [atom, self.class].hash
28
75
  end
29
76
  ##
30
77
  # The atom factor to compute the amount of seconds for the atom
31
78
  def factor
32
- raise NotImplementedError, "The #factor method should be implemented for each subclass"
79
+ raise NotImplementedError, "The #factor method should be implemented by each subclass"
33
80
  end
34
81
  end
35
82
  ##
@@ -49,17 +96,26 @@ module ISO8601
49
96
  # The “duration year” average is calculated through time intervals of 400
50
97
  # “duration years”. Each cycle of 400 “duration years” has 303 “common
51
98
  # years” of 365 “calendar days” and 97 “leap years” of 366 “calendar days”.
99
+ #
100
+ # @return [Integer]
52
101
  def factor
53
- if @base.nil?
102
+ if base.nil?
54
103
  ((365 * 303 + 366 * 97) / 400) * 86400
55
- elsif @atom == 0
56
- year = (@base.year).to_i
57
- (::Time.utc(year) - ::Time.utc(@base.year))
104
+ elsif atom.zero?
105
+ year = (base.year).to_i
106
+ (::Time.utc(year) - ::Time.utc(base.year))
58
107
  else
59
- year = (@base.year + @atom).to_i
60
- (::Time.utc(year) - ::Time.utc(@base.year)) / @atom
108
+ year = (base.year + atom).to_i
109
+ (::Time.utc(year) - ::Time.utc(base.year)) / atom
61
110
  end
62
111
  end
112
+ ##
113
+ # The atom symbol.
114
+ #
115
+ # @return [Symbol]
116
+ def symbol
117
+ :Y
118
+ end
63
119
  end
64
120
  ##
65
121
  # A Months atom in a {ISO8601::Duration}
@@ -78,14 +134,21 @@ module ISO8601
78
134
  # “duration years”. Each cycle of 400 “duration years” has 303 “common
79
135
  # years” of 365 “calendar days” and 97 “leap years” of 366 “calendar days”.
80
136
  def factor
81
- if @base.nil?
137
+ if base.nil?
82
138
  nobase_calculation
83
- elsif @atom == 0
139
+ elsif atom.zero?
84
140
  zero_calculation
85
141
  else
86
142
  calculation
87
143
  end
88
144
  end
145
+ ##
146
+ # The atom symbol.
147
+ #
148
+ # @return [Symbol]
149
+ def symbol
150
+ :M
151
+ end
89
152
 
90
153
  private
91
154
 
@@ -94,30 +157,30 @@ module ISO8601
94
157
  end
95
158
 
96
159
  def zero_calculation
97
- month = (@base.month <= 12) ? (@base.month) : ((@base.month) % 12)
98
- year = @base.year + ((@base.month) / 12).to_i
160
+ month = (base.month <= 12) ? (base.month) : ((base.month) % 12)
161
+ year = base.year + ((base.month) / 12).to_i
99
162
 
100
- (::Time.utc(year, month) - ::Time.utc(@base.year, @base.month))
163
+ (::Time.utc(year, month) - ::Time.utc(base.year, base.month))
101
164
  end
102
165
 
103
166
  def calculation
104
- if @base.month + @atom <= 0
105
- month = @base.month + @atom
167
+ if base.month + atom <= 0
168
+ month = base.month + atom
106
169
 
107
170
  if month % 12 == 0
108
- year = @base.year + (month / 12) - 1
171
+ year = base.year + (month / 12) - 1
109
172
  month = 12
110
173
  else
111
- year = @base.year + (month / 12).floor
174
+ year = base.year + (month / 12).floor
112
175
  month = (12 + month > 0) ? (12 + month) : (12 + (month % -12))
113
176
  end
114
177
  else
115
- month = (@base.month + @atom <= 12) ? (@base.month + @atom) : ((@base.month + @atom) % 12)
116
- month = 12 if month == 0
117
- year = @base.year + ((@base.month + @atom) / 12).to_i
178
+ month = (base.month + atom <= 12) ? (base.month + atom) : ((base.month + atom) % 12)
179
+ month = 12 if month.zero?
180
+ year = base.year + ((base.month + atom) / 12).to_i
118
181
  end
119
182
 
120
- (::Time.utc(year, month) - ::Time.utc(@base.year, @base.month)) / @atom
183
+ (::Time.utc(year, month) - ::Time.utc(base.year, base.month)) / atom
121
184
  end
122
185
  end
123
186
  ##
@@ -128,6 +191,13 @@ module ISO8601
128
191
  def factor
129
192
  604800
130
193
  end
194
+ ##
195
+ # The atom symbol.
196
+ #
197
+ # @return [Symbol]
198
+ def symbol
199
+ :W
200
+ end
131
201
  end
132
202
  ##
133
203
  # The Days atom in a {ISO8601::Duration}
@@ -141,6 +211,13 @@ module ISO8601
141
211
  def factor
142
212
  86400
143
213
  end
214
+ ##
215
+ # The atom symbol.
216
+ #
217
+ # @return [Symbol]
218
+ def symbol
219
+ :D
220
+ end
144
221
  end
145
222
  ##
146
223
  # The Hours atom in a {ISO8601::Duration}
@@ -150,6 +227,13 @@ module ISO8601
150
227
  def factor
151
228
  3600
152
229
  end
230
+ ##
231
+ # The atom symbol.
232
+ #
233
+ # @return [Symbol]
234
+ def symbol
235
+ :H
236
+ end
153
237
  end
154
238
  ##
155
239
  # The Minutes atom in a {ISO8601::Duration}
@@ -159,6 +243,13 @@ module ISO8601
159
243
  def factor
160
244
  60
161
245
  end
246
+ ##
247
+ # The atom symbol.
248
+ #
249
+ # @return [Symbol]
250
+ def symbol
251
+ :M
252
+ end
162
253
  end
163
254
  ##
164
255
  # The Seconds atom in a {ISO8601::Duration}
@@ -172,5 +263,12 @@ module ISO8601
172
263
  def factor
173
264
  1
174
265
  end
266
+ ##
267
+ # The atom symbol.
268
+ #
269
+ # @return [Symbol]
270
+ def symbol
271
+ :S
272
+ end
175
273
  end
176
274
  end
data/lib/iso8601/date.rb CHANGED
@@ -1,14 +1,14 @@
1
1
  module ISO8601
2
2
  ##
3
- # A Date representation
3
+ # A Date representation.
4
4
  #
5
5
  # @example
6
- # d = Date.new('2014-05-28')
6
+ # d = ISO8601::Date.new('2014-05-28')
7
7
  # d.year # => 2014
8
8
  # d.month # => 5
9
9
  #
10
10
  # @example Week dates
11
- # d = Date.new('2014-W15-2')
11
+ # d = ISO8601::Date.new('2014-W15-2')
12
12
  # d.day # => 27
13
13
  # d.wday # => 2
14
14
  # d.week # => 15
@@ -17,7 +17,7 @@ module ISO8601
17
17
 
18
18
  def_delegators(:@date,
19
19
  :to_s, :to_time, :to_date, :to_datetime,
20
- :year, :month, :day, :wday, :hash)
20
+ :year, :month, :day, :wday)
21
21
  ##
22
22
  # The original atoms
23
23
  attr_reader :atoms
@@ -61,6 +61,25 @@ module ISO8601
61
61
  def to_a
62
62
  [year, month, day]
63
63
  end
64
+ ##
65
+ # @param [#hash] contrast The contrast to compare against
66
+ #
67
+ # @return [Boolean]
68
+ def ==(contrast)
69
+ (hash == contrast.hash)
70
+ end
71
+ ##
72
+ # @param [#hash] contrast The contrast to compare against
73
+ #
74
+ # @return [Boolean]
75
+ def eql?(contrast)
76
+ (hash == contrast.hash)
77
+ end
78
+ ##
79
+ # @return [Fixnum]
80
+ def hash
81
+ [atoms, self.class].hash
82
+ end
64
83
 
65
84
  private
66
85
  ##
@@ -12,7 +12,7 @@ module ISO8601
12
12
 
13
13
  def_delegators(:@date_time,
14
14
  :strftime, :to_time, :to_date, :to_datetime,
15
- :year, :month, :day, :hour, :minute, :zone, :hash)
15
+ :year, :month, :day, :hour, :minute, :zone)
16
16
 
17
17
  attr_reader :second
18
18
 
@@ -57,6 +57,26 @@ module ISO8601
57
57
  def to_a
58
58
  [year, month, day, hour, minute, second, zone]
59
59
  end
60
+ ##
61
+ # @param [#hash] contrast The contrast to compare against
62
+ #
63
+ # @return [Boolean]
64
+ def ==(contrast)
65
+ (hash == contrast.hash)
66
+ end
67
+ ##
68
+ # @param [#hash] contrast The contrast to compare against
69
+ #
70
+ # @return [Boolean]
71
+ def eql?(contrast)
72
+ (hash == contrast.hash)
73
+ end
74
+ ##
75
+ # @return [Fixnum]
76
+ def hash
77
+ [second, self.class].hash
78
+ end
79
+
60
80
 
61
81
  private
62
82
  ##
@@ -2,111 +2,111 @@
2
2
 
3
3
  module ISO8601
4
4
  ##
5
- # Represents a duration in ISO 8601 format
5
+ # A duration representation. When no base is provided, all atoms use an
6
+ # average factor which affects the result of any computation like `#to_seconds`.
7
+ #
8
+ # @example
9
+ # d = ISO8601::Duration.new('P2Y1MT2H')
10
+ # d.years # => #<ISO8601::Years:0x000000051adee8 @atom=2.0, @base=nil>
11
+ # d.months # => #<ISO8601::Months:0x00000004f230b0 @atom=1.0, @base=nil>
12
+ # d.days # => #<ISO8601::Days:0x00000005205468 @atom=0, @base=nil>
13
+ # d.hours # => #<ISO8601::Hours:0x000000051e02a8 @atom=2.0, @base=nil>
14
+ # d.to_seconds # => 65707200.0
15
+ #
16
+ # @example Explicit base date time
17
+ # d = ISO8601::Duration.new('P2Y1MT2H', ISO8601::DateTime.new('2014-08017'))
18
+ # d.years # => #<ISO8601::Years:0x000000051adee8 @atom=2.0, @base=#<ISO8601::DateTime...>>
19
+ # d.months # => #<ISO8601::Months:0x00000004f230b0 @atom=1.0, @base=#<ISO8601::DateTime...>>
20
+ # d.days # => #<ISO8601::Days:0x00000005205468 @atom=0, @base=#<ISO8601::DateTime...>>
21
+ # d.hours # => #<ISO8601::Hours:0x000000051e02a8 @atom=2.0, @base=#<ISO8601::DateTime...>>
22
+ # d.to_seconds # => 65757600.0
23
+ #
24
+ # @example Number of seconds versus patterns
25
+ # di = ISO8601::Duration.new(65707200)
26
+ # dp = ISO8601::Duration.new('P2Y1MT2H')
27
+ # ds = ISO8601::Duration.new('P65707200S')
28
+ # di == dp # => true
29
+ # di == ds # => true
6
30
  #
7
- # @todo Support fraction values for years, months, days, weeks, hours
8
- # and minutes
9
31
  class Duration
10
- attr_reader :base, :atoms
11
32
  ##
12
- # @param [String, Numeric] pattern The duration pattern
33
+ # @param [String, Numeric] input The duration pattern
13
34
  # @param [ISO8601::DateTime, nil] base (nil) The base datetime to
14
- # calculate the duration properly
15
- def initialize(pattern, base = nil)
16
- # we got seconds instead of an ISO8601 duration
17
- pattern = "PT#{pattern}S" if (pattern.kind_of? Numeric)
18
- @duration = /^(\+|-)? # Sign
19
- P(
20
- (
21
- (\d+(?:[,.]\d+)?Y)? # Years
22
- (\d+(?:[.,]\d+)?M)? # Months
23
- (\d+(?:[.,]\d+)?D)? # Days
24
- (T
25
- (\d+(?:[.,]\d+)?H)? # Hours
26
- (\d+(?:[.,]\d+)?M)? # Minutes
27
- (\d+(?:[.,]\d+)?S)? # Seconds
28
- )? # Time
29
- )
30
- |(\d+(?:[.,]\d+)?W) # Weeks
31
- ) # Duration
32
- $/x.match(pattern) or raise ISO8601::Errors::UnknownPattern.new(pattern)
33
-
34
- @base = base
35
- valid_pattern?
36
- valid_base?
37
- @atoms = {
38
- :years => @duration[4].nil? ? 0 : @duration[4].chop.to_f * sign,
39
- :months => @duration[5].nil? ? 0 : @duration[5].chop.to_f * sign,
40
- :weeks => @duration[11].nil? ? 0 : @duration[11].chop.to_f * sign,
41
- :days => @duration[6].nil? ? 0 : @duration[6].chop.to_f * sign,
42
- :hours => @duration[8].nil? ? 0 : @duration[8].chop.to_f * sign,
43
- :minutes => @duration[9].nil? ? 0 : @duration[9].chop.to_f * sign,
44
- :seconds => @duration[10].nil? ? 0 : @duration[10].chop.to_f * sign
45
- }
46
- valid_fractions?
35
+ # calculate the duration against an specific point in time.
36
+ def initialize(input, base = nil)
37
+ @original = input
38
+ @pattern = to_pattern
39
+ @atoms = atomize(@pattern)
40
+ @base = validate_base(base)
47
41
  end
48
42
  ##
43
+ # Raw atoms result of parsing the given pattern.
44
+ #
45
+ # @return [Hash<Float>]
46
+ attr_reader :atoms
47
+ ##
48
+ # Datetime base.
49
+ #
50
+ # @return [ISO8601::DateTime, nil]
51
+ attr_reader :base
52
+ ##
49
53
  # Assigns a new base datetime
50
54
  #
51
55
  # @return [ISO8601::DateTime, nil]
52
56
  def base=(value)
53
- @base = value
54
- valid_base?
55
- return @base
57
+ @base = validate_base(value)
58
+ @base
56
59
  end
57
60
  ##
58
61
  # @return [String] The string representation of the duration
59
- def to_s
60
- @duration[0]
61
- end
62
+ attr_reader :pattern
63
+ alias_method :to_s, :pattern
62
64
  ##
63
65
  # @return [ISO8601::Years] The years of the duration
64
66
  def years
65
- ISO8601::Years.new(@atoms[:years], @base)
67
+ ISO8601::Years.new(atoms[:years], base)
66
68
  end
67
69
  ##
68
70
  # @return [ISO8601::Months] The months of the duration
69
71
  def months
70
72
  # Changes the base to compute the months for the right base year
71
- base = @base.nil? ? nil : @base + self.years.to_seconds
72
- ISO8601::Months.new(@atoms[:months], base)
73
+ month_base = base.nil? ? nil : base + years.to_seconds
74
+ ISO8601::Months.new(atoms[:months], month_base)
73
75
  end
74
76
  ##
75
77
  # @return [ISO8601::Weeks] The weeks of the duration
76
78
  def weeks
77
- ISO8601::Weeks.new(@atoms[:weeks], @base)
79
+ ISO8601::Weeks.new(atoms[:weeks], base)
78
80
  end
79
81
  ##
80
82
  # @return [ISO8601::Days] The days of the duration
81
83
  def days
82
- ISO8601::Days.new(@atoms[:days], @base)
84
+ ISO8601::Days.new(atoms[:days], base)
83
85
  end
84
86
  ##
85
87
  # @return [ISO8601::Hours] The hours of the duration
86
88
  def hours
87
- ISO8601::Hours.new(@atoms[:hours], @base)
89
+ ISO8601::Hours.new(atoms[:hours], base)
88
90
  end
89
91
  ##
90
92
  # @return [ISO8601::Minutes] The minutes of the duration
91
93
  def minutes
92
- ISO8601::Minutes.new(@atoms[:minutes], @base)
94
+ ISO8601::Minutes.new(atoms[:minutes], base)
93
95
  end
94
96
  ##
95
97
  # @return [ISO8601::Seconds] The seconds of the duration
96
98
  def seconds
97
- ISO8601::Seconds.new(@atoms[:seconds], @base)
99
+ ISO8601::Seconds.new(atoms[:seconds], base)
98
100
  end
99
101
  ##
100
- # @return [Numeric] The duration in seconds
101
- def to_seconds
102
- years, months, weeks, days, hours, minutes, seconds = self.years.to_seconds, self.months.to_seconds, self.weeks.to_seconds, self.days.to_seconds, self.hours.to_seconds, self.minutes.to_seconds, self.seconds.to_seconds
103
- return years + months + weeks + days + hours + minutes + seconds
104
- end
102
+ # The Integer representation of the duration sign.
103
+ #
104
+ # @return [Integer]
105
+ attr_reader :sign
105
106
  ##
106
107
  # @return [ISO8601::Duration] The absolute representation of the duration
107
108
  def abs
108
- absolute = self.to_s.sub(/^[-+]/, '')
109
- return ISO8601::Duration.new(absolute)
109
+ self.class.new(pattern.sub(/^[-+]/, ''), base)
110
110
  end
111
111
  ##
112
112
  # Addition
@@ -116,10 +116,12 @@ module ISO8601
116
116
  # @raise [ISO8601::Errors::DurationBaseError] If bases doesn't match
117
117
  # @return [ISO8601::Duration]
118
118
  def +(duration)
119
- raise ISO8601::Errors::DurationBaseError.new(duration) if @base.to_s != duration.base.to_s
119
+ compare_bases(duration)
120
+
120
121
  d1 = to_seconds
121
122
  d2 = duration.to_seconds
122
- return seconds_to_iso(d1 + d2)
123
+
124
+ seconds_to_iso(d1 + d2)
123
125
  end
124
126
  ##
125
127
  # Substraction
@@ -129,15 +131,12 @@ module ISO8601
129
131
  # @raise [ISO8601::Errors::DurationBaseError] If bases doesn't match
130
132
  # @return [ISO8601::Duration]
131
133
  def -(duration)
132
- raise ISO8601::Errors::DurationBaseError.new(duration) if @base.to_s != duration.base.to_s
134
+ compare_bases(duration)
135
+
133
136
  d1 = to_seconds
134
137
  d2 = duration.to_seconds
135
- duration = d1 - d2
136
- if duration == 0
137
- return ISO8601::Duration.new('PT0S')
138
- else
139
- return seconds_to_iso(duration)
140
- end
138
+
139
+ seconds_to_iso(d1 - d2)
141
140
  end
142
141
  ##
143
142
  # @param [ISO8601::Duration] duration The duration to compare
@@ -145,67 +144,145 @@ module ISO8601
145
144
  # @raise [ISO8601::Errors::DurationBaseError] If bases doesn't match
146
145
  # @return [Boolean]
147
146
  def ==(duration)
148
- raise ISO8601::Errors::DurationBaseError.new(duration) if @base.to_s != duration.base.to_s
149
- (self.to_seconds == duration.to_seconds)
147
+ compare_bases(duration)
148
+
149
+ (to_seconds == duration.to_seconds)
150
+ end
151
+ ##
152
+ # @param [ISO8601::Duration] duration The duration to compare
153
+ #
154
+ # @return [Boolean]
155
+ def eql?(duration)
156
+ (hash == duration.hash)
150
157
  end
151
158
  ##
152
159
  # @return [Fixnum]
153
160
  def hash
154
- @atoms.hash
161
+ [atoms.values, self.class].hash
162
+ end
163
+ ##
164
+ # Converts original input into a valid ISO 8601 duration pattern.
165
+ #
166
+ # @return [String]
167
+ def to_pattern
168
+ (@original.kind_of? Numeric) ? "PT#{@original}S" : @original
169
+ end
170
+ ##
171
+ # @return [Numeric] The duration in seconds
172
+ def to_seconds
173
+ [years, months, weeks, days, hours, minutes, seconds].map(&:to_seconds).reduce(&:+)
155
174
  end
156
175
 
157
176
 
158
177
  private
159
178
  ##
160
- # @param [Numeric] duration The seconds to promote
179
+ # Splits a duration pattern into valid atoms.
180
+ #
181
+ # Acceptable patterns:
182
+ #
183
+ # * PnYnMnD
184
+ # * PTnHnMnS
185
+ # * PnYnMnDTnHnMnS
186
+ # * PnW
187
+ #
188
+ # Where `n` is any number. If it contains a decimal fraction, a dot (`.`) or
189
+ # comma (`,`) can be used.
190
+ #
191
+ # @param [String] input
192
+ #
193
+ # @return [Hash<Float>]
194
+ def atomize(input)
195
+ duration = input.match(/^
196
+ (?<sign>\+|-)?
197
+ P(?:
198
+ (?:
199
+ (?:(?<years>\d+(?:[,.]\d+)?)Y)?
200
+ (?:(?<months>\d+(?:[.,]\d+)?)M)?
201
+ (?:(?<days>\d+(?:[.,]\d+)?)D)?
202
+ (?<time>T
203
+ (?:(?<hours>\d+(?:[.,]\d+)?)H)?
204
+ (?:(?<minutes>\d+(?:[.,]\d+)?)M)?
205
+ (?:(?<seconds>\d+(?:[.,]\d+)?)S)?
206
+ )?
207
+ ) |
208
+ (?<weeks>\d+(?:[.,]\d+)?W)
209
+ ) # Duration
210
+ $/x) or raise ISO8601::Errors::UnknownPattern.new(input)
211
+
212
+ valid_pattern?(duration)
213
+
214
+ @sign = (duration[:sign].nil? || duration[:sign] == '+') ? 1 : -1
215
+
216
+ keys = duration.names.map(&:to_sym)
217
+ values = duration.captures.map { |v| v.to_f * sign }
218
+ components = Hash[keys.zip(values)]
219
+ components.delete(:time) # clean time capture
220
+
221
+ valid_fractions?(components.values)
222
+
223
+ components
224
+ end
225
+ ##
226
+ # @param [Numeric] value The seconds to promote
161
227
  #
162
228
  # @return [ISO8601::Duration]
163
- def seconds_to_iso(duration)
164
- sign = '-' if (duration < 0)
165
- duration = duration.abs
166
- years, y_mod = (duration / self.years.factor).to_i, (duration % self.years.factor)
167
- months, m_mod = (y_mod / self.months.factor).to_i, (y_mod % self.months.factor)
168
- days, d_mod = (m_mod / self.days.factor).to_i, (m_mod % self.days.factor)
169
- hours, h_mod = (d_mod / self.hours.factor).to_i, (d_mod % self.hours.factor)
170
- minutes, mi_mod = (h_mod / self.minutes.factor).to_i, (h_mod % self.minutes.factor)
171
- seconds = mi_mod.div(1) == mi_mod ? mi_mod.to_i : mi_mod.to_f # Coerce to Integer when needed (`PT1S` instead of `PT1.0S`)
172
-
173
- seconds = (seconds != 0 or (years == 0 and months == 0 and days == 0 and hours == 0 and minutes == 0)) ? "#{seconds}S" : ""
174
- minutes = (minutes != 0) ? "#{minutes}M" : ""
175
- hours = (hours != 0) ? "#{hours}H" : ""
176
- days = (days != 0) ? "#{days}D" : ""
177
- months = (months != 0) ? "#{months}M" : ""
178
- years = (years != 0) ? "#{years}Y" : ""
179
-
180
- date = %[#{sign}P#{years}#{months}#{days}]
181
- time = (hours != "" or minutes != "" or seconds != "") ? %[T#{hours}#{minutes}#{seconds}] : ""
182
- date_time = date + time
183
- return ISO8601::Duration.new(date_time)
184
- end
185
-
186
- def sign
187
- (@duration[1].nil? or @duration[1] == "+") ? 1 : -1
188
- end
189
- def valid_base?
190
- if !(@base.nil? or @base.kind_of? ISO8601::DateTime)
191
- raise TypeError
192
- end
229
+ def seconds_to_iso(value)
230
+ return self.class.new('PT0S') if value.zero?
231
+
232
+ sign_str = (value < 0) ? '-' : ''
233
+ value = value.abs
234
+
235
+ y, y_mod = decompose_atom(value, years)
236
+ m, m_mod = decompose_atom(y_mod, months)
237
+ d, d_mod = decompose_atom(m_mod, days)
238
+ h, h_mod = decompose_atom(d_mod, hours)
239
+ mi, mi_mod = decompose_atom(h_mod, minutes)
240
+ s = Seconds.new(mi_mod)
241
+
242
+ date = to_date_s(sign_str, y, m, d)
243
+ time = to_time_s(h, mi, s)
244
+
245
+ self.class.new(date + time)
246
+ end
247
+
248
+ def decompose_atom(value, atom)
249
+ [atom.class.new((value / atom.factor).to_i), (value % atom.factor)]
250
+ end
251
+
252
+ def to_date_s(sign, *args)
253
+ "#{sign}P#{args.map(&:to_s).join('')}"
193
254
  end
194
- def valid_pattern?
195
- if @duration.nil? or
196
- (@duration[4].nil? and @duration[5].nil? and @duration[6].nil? and @duration[7].nil? and @duration[11].nil?) or
197
- (!@duration[7].nil? and @duration[8].nil? and @duration[9].nil? and @duration[10].nil? and @duration[11].nil?)
198
255
 
199
- raise ISO8601::Errors::UnknownPattern.new(@duration)
256
+ def to_time_s(*args)
257
+ (args.map(&:value).reduce(&:+) > 0) ? "T#{args.map(&:to_s).join('')}" : ''
258
+ end
259
+
260
+ def validate_base(input)
261
+ raise ISO8601::Errors::TypeError if !(input.nil? or input.kind_of? ISO8601::DateTime)
262
+
263
+ input
264
+ end
265
+ def valid_pattern?(components)
266
+ date = [components[:years], components[:months], components[:days]].compact
267
+ time = [components[:hours], components[:minutes], components[:seconds]].compact
268
+ weeks = components[:weeks]
269
+ all = [date, time, weeks].flatten.compact
270
+
271
+ if all.empty? || (!components[:time].nil? && time.empty? && weeks.nil?)
272
+ raise ISO8601::Errors::UnknownPattern.new(@pattern)
200
273
  end
201
274
  end
202
275
 
203
- def valid_fractions?
204
- values = @atoms.values.reject(&:zero?)
276
+ def valid_fractions?(values)
277
+ values = values.reject(&:zero?)
205
278
  fractions = values.select { |a| (a % 1) != 0 }
206
279
  if fractions.size > 1 || (fractions.size == 1 && fractions.last != values.last)
207
- raise ISO8601::Errors::InvalidFractions.new(@duration)
280
+ raise ISO8601::Errors::InvalidFractions.new(@pattern)
208
281
  end
209
282
  end
283
+
284
+ def compare_bases(duration)
285
+ raise ISO8601::Errors::DurationBaseError.new(duration) if base.to_s != duration.base.to_s
286
+ end
210
287
  end
211
288
  end