iso8601 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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