iso8601 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/CONTRIBUTING.md +58 -0
  4. data/Gemfile +2 -2
  5. data/README.md +28 -102
  6. data/docs/date-time.md +86 -0
  7. data/docs/duration.md +77 -0
  8. data/docs/time-interval.md +120 -0
  9. data/iso8601.gemspec +43 -6
  10. data/lib/iso8601.rb +15 -7
  11. data/lib/iso8601/atomic.rb +78 -0
  12. data/lib/iso8601/date.rb +35 -10
  13. data/lib/iso8601/date_time.rb +14 -3
  14. data/lib/iso8601/days.rb +47 -0
  15. data/lib/iso8601/duration.rb +115 -99
  16. data/lib/iso8601/errors.rb +13 -1
  17. data/lib/iso8601/hours.rb +43 -0
  18. data/lib/iso8601/minutes.rb +43 -0
  19. data/lib/iso8601/months.rb +98 -0
  20. data/lib/iso8601/seconds.rb +47 -0
  21. data/lib/iso8601/time.rb +43 -15
  22. data/lib/iso8601/time_interval.rb +392 -0
  23. data/lib/iso8601/version.rb +1 -1
  24. data/lib/iso8601/weeks.rb +43 -0
  25. data/lib/iso8601/years.rb +80 -0
  26. data/spec/iso8601/date_spec.rb +0 -6
  27. data/spec/iso8601/date_time_spec.rb +0 -8
  28. data/spec/iso8601/days_spec.rb +44 -0
  29. data/spec/iso8601/duration_spec.rb +103 -99
  30. data/spec/iso8601/hours_spec.rb +44 -0
  31. data/spec/iso8601/minutes_spec.rb +44 -0
  32. data/spec/iso8601/months_spec.rb +86 -0
  33. data/spec/iso8601/seconds_spec.rb +44 -0
  34. data/spec/iso8601/time_interval_spec.rb +416 -0
  35. data/spec/iso8601/time_spec.rb +0 -6
  36. data/spec/iso8601/weeks_spec.rb +46 -0
  37. data/spec/iso8601/years_spec.rb +69 -0
  38. metadata +37 -19
  39. data/.dockerignore +0 -7
  40. data/.editorconfig +0 -9
  41. data/.gitignore +0 -19
  42. data/.rubocop.yml +0 -38
  43. data/.travis.yml +0 -19
  44. data/Dockerfile +0 -10
  45. data/Makefile +0 -19
  46. data/circle.yml +0 -13
  47. data/lib/iso8601/atoms.rb +0 -279
  48. data/spec/iso8601/atoms_spec.rb +0 -329
@@ -18,13 +18,50 @@ Gem::Specification.new do |s|
18
18
  EOD
19
19
  s.license = 'MIT'
20
20
  s.rubyforge_project = 'iso8601'
21
-
22
- s.files = `git ls-files`.split("\n")
23
- s.test_files = s.files.grep(%r{^(spec|features)/})
21
+ s.files = %W(CHANGELOG.md
22
+ CONTRIBUTING.md
23
+ Gemfile
24
+ LICENSE
25
+ README.md
26
+ Rakefile
27
+ iso8601.gemspec
28
+ docs/date-time.md
29
+ docs/duration.md
30
+ docs/time-interval.md
31
+ lib/iso8601.rb
32
+ lib/iso8601/atomic.rb
33
+ lib/iso8601/date.rb
34
+ lib/iso8601/date_time.rb
35
+ lib/iso8601/days.rb
36
+ lib/iso8601/duration.rb
37
+ lib/iso8601/errors.rb
38
+ lib/iso8601/hours.rb
39
+ lib/iso8601/minutes.rb
40
+ lib/iso8601/months.rb
41
+ lib/iso8601/seconds.rb
42
+ lib/iso8601/time.rb
43
+ lib/iso8601/time_interval.rb
44
+ lib/iso8601/version.rb
45
+ lib/iso8601/weeks.rb
46
+ lib/iso8601/years.rb
47
+ spec/iso8601/date_spec.rb
48
+ spec/iso8601/date_time_spec.rb
49
+ spec/iso8601/days_spec.rb
50
+ spec/iso8601/duration_spec.rb
51
+ spec/iso8601/hours_spec.rb
52
+ spec/iso8601/minutes_spec.rb
53
+ spec/iso8601/months_spec.rb
54
+ spec/iso8601/seconds_spec.rb
55
+ spec/iso8601/time_interval_spec.rb
56
+ spec/iso8601/time_spec.rb
57
+ spec/iso8601/weeks_spec.rb
58
+ spec/iso8601/years_spec.rb
59
+ spec/spec_helper.rb)
60
+ s.test_files = s.files.grep(%r{^spec/})
24
61
  s.require_paths = ['lib']
25
62
 
26
63
  s.has_rdoc = 'yard'
27
- s.required_ruby_version = '>= 1.9.3'
28
- s.add_development_dependency 'rspec', '~> 3.1'
29
- s.add_development_dependency 'rubocop', '~> 0.26'
64
+ s.required_ruby_version = '>= 2.0.0'
65
+ s.add_development_dependency 'rspec', '~> 3.4'
66
+ s.add_development_dependency 'rubocop', '~> 0.35'
30
67
  end
@@ -3,10 +3,18 @@
3
3
  require 'time'
4
4
  require 'forwardable'
5
5
 
6
- require 'iso8601/version'
7
- require 'iso8601/errors'
8
- require 'iso8601/atoms'
9
- require 'iso8601/date'
10
- require 'iso8601/time'
11
- require 'iso8601/date_time'
12
- require 'iso8601/duration'
6
+ require_relative 'iso8601/version'
7
+ require_relative 'iso8601/errors'
8
+ require_relative 'iso8601/atomic'
9
+ require_relative 'iso8601/years'
10
+ require_relative 'iso8601/months'
11
+ require_relative 'iso8601/weeks'
12
+ require_relative 'iso8601/days'
13
+ require_relative 'iso8601/hours'
14
+ require_relative 'iso8601/minutes'
15
+ require_relative 'iso8601/seconds'
16
+ require_relative 'iso8601/date'
17
+ require_relative 'iso8601/time'
18
+ require_relative 'iso8601/date_time'
19
+ require_relative 'iso8601/duration'
20
+ require_relative 'iso8601/time_interval'
@@ -0,0 +1,78 @@
1
+ # encoding: utf-8
2
+
3
+ module ISO8601
4
+ module Atomic
5
+ include Comparable
6
+
7
+ attr_reader :atom
8
+
9
+ ##
10
+ # The integer representation
11
+ #
12
+ # @return [Integer]
13
+ def to_i
14
+ atom.to_i
15
+ end
16
+
17
+ ##
18
+ # The float representation
19
+ #
20
+ # @return [Float]
21
+ def to_f
22
+ atom.to_f
23
+ end
24
+
25
+ ##
26
+ # Returns the ISO 8601 representation for the atom
27
+ #
28
+ # @return [String]
29
+ def to_s
30
+ (value.zero?) ? '' : "#{value}#{symbol}"
31
+ end
32
+
33
+ ##
34
+ # The simplest numeric representation. If modulo equals 0 returns an
35
+ # integer else a float.
36
+ #
37
+ # @return [Numeric]
38
+ def value
39
+ (atom % 1).zero? ? atom.to_i : atom
40
+ end
41
+
42
+ ##
43
+ # @param [Atom] other The contrast to compare against
44
+ #
45
+ # @return [-1, 0, 1]
46
+ def <=>(other)
47
+ return nil unless other.is_a?(self.class)
48
+
49
+ to_f <=> other.to_f
50
+ end
51
+
52
+ ##
53
+ # @param [#hash] other The contrast to compare against
54
+ #
55
+ # @return [Boolean]
56
+ def eql?(other)
57
+ (hash == other.hash)
58
+ end
59
+
60
+ ##
61
+ # @return [Fixnum]
62
+ def hash
63
+ [atom, self.class].hash
64
+ end
65
+
66
+ ##
67
+ # Validates the atom is a Numeric
68
+ def valid_atom?(atom)
69
+ fail ISO8601::Errors::TypeError,
70
+ "The atom argument for #{self.class} should be a Numeric value." unless atom.is_a?(Numeric)
71
+ end
72
+
73
+ def valid_base?(base)
74
+ fail ISO8601::Errors::TypeError,
75
+ "The base argument for #{self.class} should be a ISO8601::DateTime instance or nil." unless base.is_a?(ISO8601::DateTime) || base.nil?
76
+ end
77
+ end
78
+ end
@@ -20,12 +20,15 @@ module ISO8601
20
20
  :to_s, :to_time, :to_date, :to_datetime,
21
21
  :year, :month, :day, :wday
22
22
  )
23
+
23
24
  ##
24
25
  # The original atoms
25
26
  attr_reader :atoms
27
+
26
28
  ##
27
29
  # The separator used in the original ISO 8601 string.
28
30
  attr_reader :separator
31
+
29
32
  ##
30
33
  # @param [String] input The date pattern
31
34
  def initialize(input)
@@ -33,6 +36,7 @@ module ISO8601
33
36
  @atoms = atomize(input)
34
37
  @date = compose(@atoms)
35
38
  end
39
+
36
40
  ##
37
41
  # The calendar week number (1-53)
38
42
  #
@@ -40,6 +44,7 @@ module ISO8601
40
44
  def week
41
45
  @date.cweek
42
46
  end
47
+
43
48
  ##
44
49
  # Forwards the date the given amount of days.
45
50
  #
@@ -50,6 +55,7 @@ module ISO8601
50
55
  other = other.to_days if other.respond_to?(:to_days)
51
56
  ISO8601::Date.new((@date + other).iso8601)
52
57
  end
58
+
53
59
  ##
54
60
  # Backwards the date the given amount of days.
55
61
  #
@@ -60,11 +66,13 @@ module ISO8601
60
66
  other = other.to_days if other.respond_to?(:to_days)
61
67
  ISO8601::Date.new((@date - other).iso8601)
62
68
  end
69
+
63
70
  ##
64
71
  # Converts self to an array of atoms.
65
72
  def to_a
66
73
  [year, month, day]
67
74
  end
75
+
68
76
  ##
69
77
  # @param [#hash] other The contrast to compare against
70
78
  #
@@ -72,6 +80,7 @@ module ISO8601
72
80
  def ==(other)
73
81
  (hash == other.hash)
74
82
  end
83
+
75
84
  ##
76
85
  # @param [#hash] other The contrast to compare against
77
86
  #
@@ -79,6 +88,7 @@ module ISO8601
79
88
  def eql?(other)
80
89
  (hash == other.hash)
81
90
  end
91
+
82
92
  ##
83
93
  # @return [Fixnum]
84
94
  def hash
@@ -103,14 +113,31 @@ module ISO8601
103
113
  #
104
114
  # @return [Array<Integer>]
105
115
  def atomize(input)
106
- week_date = /^([+-]?)\d{4}(-?)W\d{2}(?:\2\d)?$/.match(input)
116
+ week_date = parse_weekdate(input)
107
117
  return atomize_week_date(input, week_date[2], week_date[1]) unless week_date.nil?
108
118
 
109
- ordinal_regexp = /^([+-]?)(\d{4})(-?)(\d{3})$/
110
- _, sign, year, separator, day = ordinal_regexp.match(input).to_a.compact
119
+ _, sign, year, separator, day = parse_ordinal(input)
111
120
  return atomize_ordinal(year, day, separator, sign) unless year.nil?
112
121
 
113
- _, year, separator, month, day = /^
122
+ _, year, separator, month, day = parse_date(input)
123
+
124
+ fail ISO8601::Errors::UnknownPattern, @original if year.nil?
125
+
126
+ @separator = separator
127
+
128
+ [year, month, day].compact.map(&:to_i)
129
+ end
130
+
131
+ def parse_weekdate(input)
132
+ /^([+-]?)\d{4}(-?)W\d{2}(?:\2\d)?$/.match(input)
133
+ end
134
+
135
+ def parse_ordinal(input)
136
+ /^([+-]?)(\d{4})(-?)(\d{3})$/.match(input).to_a.compact
137
+ end
138
+
139
+ def parse_date(input)
140
+ /^
114
141
  ([+-]?\d{4}) # YYYY
115
142
  (?:
116
143
  (-?)(\d{2}) # YYYY-MM
@@ -119,13 +146,8 @@ module ISO8601
119
146
  )?
120
147
  )?
121
148
  $/x.match(input).to_a.compact
122
-
123
- fail ISO8601::Errors::UnknownPattern, @original if year.nil?
124
-
125
- @separator = separator
126
-
127
- [year, month, day].compact.map(&:to_i)
128
149
  end
150
+
129
151
  ##
130
152
  # Parses a week date (YYYY-Www-D, YYYY-Www) and returns its atoms.
131
153
  #
@@ -141,6 +163,7 @@ module ISO8601
141
163
 
142
164
  [sign * date.year, date.month, date.day]
143
165
  end
166
+
144
167
  ##
145
168
  # Parses an ordinal date (YYYY-DDD) and returns its atoms.
146
169
  #
@@ -157,6 +180,7 @@ module ISO8601
157
180
 
158
181
  [sign * date.year, date.month, date.day]
159
182
  end
183
+
160
184
  ##
161
185
  # Wraps ::Date.parse to play nice with ArgumentError.
162
186
  #
@@ -168,6 +192,7 @@ module ISO8601
168
192
  rescue ArgumentError
169
193
  raise ISO8601::Errors::RangeError, @original
170
194
  end
195
+
171
196
  ##
172
197
  # Wraps ::Date.new to play nice with ArgumentError.
173
198
  #
@@ -25,6 +25,7 @@ module ISO8601
25
25
  @date_time = parse(date_time)
26
26
  @second = @date_time.second + @date_time.second_fraction.to_f.round(1)
27
27
  end
28
+
28
29
  ##
29
30
  # Addition
30
31
  #
@@ -34,6 +35,7 @@ module ISO8601
34
35
 
35
36
  self.class.new(moment.strftime('%Y-%m-%dT%H:%M:%S.%N%:z'))
36
37
  end
38
+
37
39
  ##
38
40
  # Substraction
39
41
  #
@@ -43,24 +45,28 @@ module ISO8601
43
45
 
44
46
  self.class.new(moment.strftime('%Y-%m-%dT%H:%M:%S.%N%:z'))
45
47
  end
48
+
46
49
  ##
47
50
  # Converts DateTime to a formated string
48
51
  def to_s
49
- second_format = (second % 1).zero? ? '%02d' % second : '%04.1f' % second
52
+ second_format = format((second % 1).zero? ? '%02d' : '%04.1f', second)
50
53
 
51
- "%04d-%02d-%02dT%02d:%02d:#{second_format}#{zone}" % atoms
54
+ format("%04d-%02d-%02dT%02d:%02d:#{second_format}#{zone}", *atoms)
52
55
  end
56
+
53
57
  ##
54
58
  # Converts DateTime to an array of atoms.
55
59
  def to_a
56
60
  [year, month, day, hour, minute, second, zone]
57
61
  end
58
62
  alias_method :atoms, :to_a
63
+
59
64
  ##
60
65
  # Converts DateTime to a floating point number of seconds since the Epoch.
61
66
  def to_f
62
67
  to_time.to_f
63
68
  end
69
+
64
70
  ##
65
71
  # @param [#hash] other The contrast to compare against
66
72
  #
@@ -68,6 +74,7 @@ module ISO8601
68
74
  def ==(other)
69
75
  (hash == other.hash)
70
76
  end
77
+
71
78
  ##
72
79
  # @param [#hash] other The contrast to compare against
73
80
  #
@@ -75,6 +82,7 @@ module ISO8601
75
82
  def eql?(other)
76
83
  (hash == other.hash)
77
84
  end
85
+
78
86
  ##
79
87
  # @return [Fixnum]
80
88
  def hash
@@ -106,6 +114,7 @@ module ISO8601
106
114
 
107
115
  ::DateTime.new(*(date_atoms + time_atoms).compact)
108
116
  end
117
+
109
118
  ##
110
119
  # Validates the date has the right pattern.
111
120
  #
@@ -122,6 +131,7 @@ module ISO8601
122
131
 
123
132
  date.atoms << date.separator
124
133
  end
134
+
125
135
  ##
126
136
  # @return [Array<String, nil>]
127
137
  def parse_time(input)
@@ -141,11 +151,12 @@ module ISO8601
141
151
 
142
152
  true
143
153
  end
154
+
144
155
  ##
145
156
  # If time is provided date must use a complete representation
146
157
  def valid_representation?(date, time)
147
158
  year, month, day = date
148
- hour, _ = time
159
+ hour = time.first
149
160
 
150
161
  date.nil? || !(!year.nil? && (month.nil? || day.nil?) && !hour.nil?)
151
162
  end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ module ISO8601
4
+ ##
5
+ # The Days atom in a {ISO8601::Duration}
6
+ #
7
+ # A "calendar day" is the time interval which starts at a certain time of day
8
+ # at a certain "calendar day" and ends at the same time of day at the next
9
+ # "calendar day".
10
+ class Days
11
+ include Atomic
12
+
13
+ AVERAGE_FACTOR = 86400
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 Day 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
+ AVERAGE_FACTOR * atom
37
+ end
38
+
39
+ ##
40
+ # The atom symbol.
41
+ #
42
+ # @return [Symbol]
43
+ def symbol
44
+ :D
45
+ end
46
+ end
47
+ end
@@ -3,148 +3,129 @@
3
3
  module ISO8601
4
4
  ##
5
5
  # A duration representation. When no base is provided, all atoms use an
6
- # average factor which affects the result of any computation like
7
- # `#to_seconds`.
6
+ # average factor to compute the amount of seconds.
8
7
  #
9
8
  # @example
10
9
  # d = ISO8601::Duration.new('P2Y1MT2H')
11
- # d.years # => #<ISO8601::Years:0x000000051adee8 @atom=2.0, @base=nil>
12
- # d.months # => #<ISO8601::Months:0x00000004f230b0 @atom=1.0, @base=nil>
13
- # d.days # => #<ISO8601::Days:0x00000005205468 @atom=0, @base=nil>
14
- # d.hours # => #<ISO8601::Hours:0x000000051e02a8 @atom=2.0, @base=nil>
10
+ # d.years # => #<ISO8601::Years:0x000000051adee8 @atom=2.0>
11
+ # d.months # => #<ISO8601::Months:0x00000004f230b0 @atom=1.0>
12
+ # d.days # => #<ISO8601::Days:0x00000005205468 @atom=0>
13
+ # d.hours # => #<ISO8601::Hours:0x000000051e02a8 @atom=2.0>
15
14
  # d.to_seconds # => 65707200.0
16
15
  #
17
16
  # @example Explicit base date time
18
17
  # base = ISO8601::DateTime.new('2014-08017')
19
- # d = ISO8601::Duration.new('P2Y1MT2H', base)
20
- # d.years # => #<ISO8601::Years:0x000000051adee8 @atom=2.0,
21
- # @base=#<ISO8601::DateTime...>>
22
- # d.months # => #<ISO8601::Months:0x00000004f230b0 @atom=1.0,
23
- # @base=#<ISO8601::DateTime...>>
24
- # d.days # => #<ISO8601::Days:0x00000005205468 @atom=0,
25
- # @base=#<ISO8601::DateTime...>>
26
- # d.hours # => #<ISO8601::Hours:0x000000051e02a8 @atom=2.0,
27
- # @base=#<ISO8601::DateTime...>>
28
- # d.to_seconds # => 65757600.0
18
+ # d.to_seconds(base) # => 65757600.0
29
19
  #
30
20
  # @example Number of seconds versus patterns
31
21
  # di = ISO8601::Duration.new(65707200)
32
- # dp = ISO8601::Duration.new('P2Y1MT2H')
33
22
  # ds = ISO8601::Duration.new('P65707200S')
23
+ # dp = ISO8601::Duration.new('P2Y1MT2H')
34
24
  # di == dp # => true
35
25
  # di == ds # => true
36
26
  #
37
27
  class Duration
38
28
  ##
39
29
  # @param [String, Numeric] input The duration pattern
40
- # @param [ISO8601::DateTime, nil] base (nil) The base datetime to
41
- # calculate the duration against an specific point in time.
42
- def initialize(input, base = nil)
30
+ def initialize(input)
43
31
  @original = input
44
- @pattern = to_pattern
32
+ @pattern = to_pattern(input)
45
33
  @atoms = atomize(@pattern)
46
- @base = validate_base(base)
47
34
  end
35
+
48
36
  ##
49
37
  # Raw atoms result of parsing the given pattern.
50
38
  #
51
39
  # @return [Hash<Float>]
52
40
  attr_reader :atoms
53
- ##
54
- # Datetime base.
55
- #
56
- # @return [ISO8601::DateTime, nil]
57
- attr_reader :base
58
- ##
59
- # Assigns a new base datetime
60
- #
61
- # @return [ISO8601::DateTime, nil]
62
- def base=(value)
63
- @base = validate_base(value)
64
- @base
65
- end
41
+
66
42
  ##
67
43
  # @return [String] The string representation of the duration
68
44
  attr_reader :pattern
69
45
  alias_method :to_s, :pattern
46
+
70
47
  ##
71
48
  # @return [ISO8601::Years] The years of the duration
72
49
  def years
73
- ISO8601::Years.new(atoms[:years], base)
50
+ ISO8601::Years.new(atoms[:years])
74
51
  end
52
+
75
53
  ##
76
54
  # @return [ISO8601::Months] The months of the duration
77
55
  def months
78
- # Changes the base to compute the months for the right base year
79
- month_base = base.nil? ? nil : base + years.to_seconds
80
- ISO8601::Months.new(atoms[:months], month_base)
56
+ ISO8601::Months.new(atoms[:months])
81
57
  end
58
+
82
59
  ##
83
60
  # @return [ISO8601::Weeks] The weeks of the duration
84
61
  def weeks
85
- ISO8601::Weeks.new(atoms[:weeks], base)
62
+ ISO8601::Weeks.new(atoms[:weeks])
86
63
  end
64
+
87
65
  ##
88
66
  # @return [ISO8601::Days] The days of the duration
89
67
  def days
90
- ISO8601::Days.new(atoms[:days], base)
68
+ ISO8601::Days.new(atoms[:days])
91
69
  end
70
+
92
71
  ##
93
72
  # @return [ISO8601::Hours] The hours of the duration
94
73
  def hours
95
- ISO8601::Hours.new(atoms[:hours], base)
74
+ ISO8601::Hours.new(atoms[:hours])
96
75
  end
76
+
97
77
  ##
98
78
  # @return [ISO8601::Minutes] The minutes of the duration
99
79
  def minutes
100
- ISO8601::Minutes.new(atoms[:minutes], base)
80
+ ISO8601::Minutes.new(atoms[:minutes])
101
81
  end
82
+
102
83
  ##
103
84
  # @return [ISO8601::Seconds] The seconds of the duration
104
85
  def seconds
105
- ISO8601::Seconds.new(atoms[:seconds], base)
86
+ ISO8601::Seconds.new(atoms[:seconds])
106
87
  end
88
+
107
89
  ##
108
90
  # The Integer representation of the duration sign.
109
91
  #
110
92
  # @return [Integer]
111
93
  attr_reader :sign
94
+
112
95
  ##
113
96
  # @return [ISO8601::Duration] The absolute representation of the duration
114
97
  def abs
115
- self.class.new(pattern.sub(/^[-+]/, ''), base)
98
+ self.class.new(pattern.sub(/^[-+]/, ''))
116
99
  end
100
+
117
101
  ##
118
102
  # Addition
119
103
  #
120
104
  # @param [ISO8601::Duration] other The duration to add
121
105
  #
122
- # @raise [ISO8601::Errors::DurationBaseError] If bases doesn't match
123
106
  # @return [ISO8601::Duration]
124
107
  def +(other)
125
- compare_bases(other)
126
- seconds_to_iso(to_seconds + other.to_seconds)
108
+ seconds_to_iso(to_seconds + fetch_seconds(other))
127
109
  end
110
+
128
111
  ##
129
112
  # Substraction
130
113
  #
131
114
  # @param [ISO8601::Duration] other The duration to substract
132
115
  #
133
- # @raise [ISO8601::Errors::DurationBaseError] If bases doesn't match
134
116
  # @return [ISO8601::Duration]
135
117
  def -(other)
136
- compare_bases(other)
137
- seconds_to_iso(to_seconds - other.to_seconds)
118
+ seconds_to_iso(to_seconds - fetch_seconds(other))
138
119
  end
120
+
139
121
  ##
140
122
  # @param [ISO8601::Duration] other The duration to compare
141
123
  #
142
- # @raise [ISO8601::Errors::DurationBaseError] If bases doesn't match
143
124
  # @return [Boolean]
144
125
  def ==(other)
145
- compare_bases(other)
146
- (to_seconds == other.to_seconds)
126
+ (to_seconds == fetch_seconds(other))
147
127
  end
128
+
148
129
  ##
149
130
  # @param [ISO8601::Duration] other The duration to compare
150
131
  #
@@ -152,42 +133,40 @@ module ISO8601
152
133
  def eql?(other)
153
134
  (hash == other.hash)
154
135
  end
136
+
155
137
  ##
156
138
  # @return [Fixnum]
157
139
  def hash
158
140
  [atoms.values, self.class].hash
159
141
  end
142
+
160
143
  ##
161
144
  # Converts original input into a valid ISO 8601 duration pattern.
162
145
  #
163
146
  # @return [String]
164
- def to_pattern
165
- (@original.is_a? Numeric) ? "PT#{@original}S" : @original
147
+ def to_pattern(original)
148
+ (original.is_a? Numeric) ? "PT#{original}S" : original
166
149
  end
150
+
167
151
  ##
152
+ # @param [ISO8601::DateTime, nil] base (nil) The base datetime to
153
+ # calculate the duration against an specific point in time.
154
+ #
168
155
  # @return [Numeric] The duration in seconds
169
- def to_seconds
170
- atoms = [years, months, weeks, days, hours, minutes, seconds]
171
- atoms.map(&:to_seconds).reduce(&:+)
172
- end
173
- ##
174
- # @return [Numeric] The duration in days
175
- def to_days
176
- (to_seconds / 86400)
177
- end
178
- ##
179
- # @return [Integer] The integer part of the duration in seconds
180
- def to_i
181
- to_seconds.to_i
182
- end
183
- ##
184
- # @return [Float] The duration in seconds coerced to float
185
- def to_f
186
- to_seconds.to_f
156
+ def to_seconds(base = nil)
157
+ rest = [weeks, days, hours, minutes, seconds].map(&:to_seconds)
158
+
159
+ years.to_seconds(base) + months_to_seconds(base) + rest.reduce(&:+)
187
160
  end
188
161
 
189
162
  private
190
163
 
164
+ # Changes the base to compute the months for the right base year
165
+ def months_to_seconds(base)
166
+ month_base = base.nil? ? nil : base + years.to_seconds(base)
167
+ months.to_seconds(month_base)
168
+ end
169
+
191
170
  ##
192
171
  # Splits a duration pattern into valid atoms.
193
172
  #
@@ -205,7 +184,35 @@ module ISO8601
205
184
  #
206
185
  # @return [Hash<Float>]
207
186
  def atomize(input)
208
- duration = input.match(/^
187
+ duration = parse(input) || fail(ISO8601::Errors::UnknownPattern, input)
188
+
189
+ valid_pattern?(duration)
190
+
191
+ @sign = sign_to_i(duration[:sign])
192
+
193
+ components = parse_tokens(duration)
194
+ components.delete(:time) # clean time capture
195
+
196
+ valid_fractions?(components.values)
197
+
198
+ components
199
+ end
200
+
201
+ def parse_tokens(tokens)
202
+ components = tokens.names.zip(tokens.captures).map! do |k, v|
203
+ value = v.nil? ? v : v.tr(',', '.')
204
+ [k.to_sym, sign * value.to_f]
205
+ end
206
+
207
+ Hash[components]
208
+ end
209
+
210
+ def sign_to_i(sign)
211
+ (sign == '-') ? -1 : 1
212
+ end
213
+
214
+ def parse(input)
215
+ input.match(/^
209
216
  (?<sign>\+|-)?
210
217
  P(?:
211
218
  (?:
@@ -220,23 +227,9 @@ module ISO8601
220
227
  ) |
221
228
  (?<weeks>\d+(?:[.,]\d+)?W)
222
229
  ) # Duration
223
- $/x) || fail(ISO8601::Errors::UnknownPattern, input)
224
-
225
- valid_pattern?(duration)
226
-
227
- @sign = (duration[:sign] == '-') ? -1 : 1
228
-
229
- components = duration.names.zip(duration.captures).map! do |k, v|
230
- value = v.nil? ? v : v.tr(',', '.')
231
- [k.to_sym, sign * value.to_f]
232
- end
233
- components = Hash[components]
234
- components.delete(:time) # clean time capture
235
-
236
- valid_fractions?(components.values)
237
-
238
- components
230
+ $/x)
239
231
  end
232
+
240
233
  ##
241
234
  # @param [Numeric] value The seconds to promote
242
235
  #
@@ -279,18 +272,20 @@ module ISO8601
279
272
  end
280
273
 
281
274
  def valid_pattern?(components)
282
- date = [
283
- components[:years], components[:months], components[:days]
284
- ]
285
- time = [
286
- components[:hours], components[:minutes], components[:seconds]
287
- ].compact
275
+ date = [components[:years],
276
+ components[:months],
277
+ components[:days]]
278
+ time = [components[:hours],
279
+ components[:minutes],
280
+ components[:seconds]].compact
288
281
  weeks = components[:weeks]
289
282
  all = [date, time, weeks].flatten.compact
290
283
 
291
284
  missing_time = (weeks.nil? && !components[:time].nil? && time.empty?)
292
285
  empty = missing_time || all.empty?
293
- fail ISO8601::Errors::UnknownPattern, @pattern if empty
286
+
287
+ fail ISO8601::Errors::UnknownPattern,
288
+ @pattern if empty
294
289
  end
295
290
 
296
291
  def valid_fractions?(values)
@@ -301,8 +296,29 @@ module ISO8601
301
296
  fail ISO8601::Errors::InvalidFractions if fractions.size > 1 || consistent
302
297
  end
303
298
 
304
- def compare_bases(other)
299
+ def compare_bases(other, base)
305
300
  fail ISO8601::Errors::DurationBaseError, other if base != other.base
306
301
  end
302
+
303
+ ##
304
+ # Fetch the number of seconds of another element.
305
+ #
306
+ # @param [ISO8601::Duration, Numeric] other Instance of a class to fetch
307
+ # seconds.
308
+ #
309
+ # @raise [ISO8601::Errors::TypeError] If other param is not an instance of
310
+ # ISO8601::Duration or Numeric classes
311
+ #
312
+ # @return [Float] Number of seconds of other param Object
313
+ def fetch_seconds(other, base = nil)
314
+ case other
315
+ when ISO8601::Duration
316
+ other.to_seconds(base)
317
+ when Numeric
318
+ other.to_f
319
+ else
320
+ fail(ISO8601::Errors::TypeError, other)
321
+ end
322
+ end
307
323
  end
308
324
  end