edtf 0.0.6 → 0.0.7

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.
@@ -12,7 +12,7 @@ Then /^the EDTF String should be "([^"]*)"$/i do |edtf|
12
12
  end
13
13
 
14
14
  When /^I parse the string "([^"]*)"$/ do |string|
15
- @date = EDTF.parse(string)
15
+ @date = EDTF.parse!(string)
16
16
  end
17
17
 
18
18
  Then /^the year should be "([^"]*)"$/ do |year|
@@ -72,6 +72,10 @@ Then /^the interval should include the date "([^"]*)"$/ do |date|
72
72
  @date.should include(Date.parse(date))
73
73
  end
74
74
 
75
+ Then /^the interval should cover the date "([^"]*)"$/ do |date|
76
+ @date.should cover(Date.parse(date))
77
+ end
78
+
75
79
 
76
80
  Then /^the date should be uncertain\? "([^"]*)"$/ do |arg1|
77
81
  @date.uncertain?.should == !!(arg1 =~ /y(es)?/i)
@@ -109,3 +113,33 @@ end
109
113
  Then /^the unspecified string code be "([^"]*)"$/ do |arg1|
110
114
  @date.unspecified.to_s.should == arg1
111
115
  end
116
+
117
+ When /^I parse the following strings an error should be raised:$/ do |table|
118
+ table.raw.each do |row|
119
+ expect { Date.edtf!(row[0]) }.to raise_error(ArgumentError)
120
+ end
121
+ end
122
+
123
+ When /^the year is uncertain: "([^"]*)"$/ do |arg1|
124
+ @date.uncertain!(:year) if arg1 =~ /y(es)?/i
125
+ end
126
+
127
+ When /^the month is uncertain: "([^"]*)"$/ do |arg1|
128
+ @date.uncertain!(:month) if arg1 =~ /y(es)?/i
129
+ end
130
+
131
+ When /^the day is uncertain: "([^"]*)"$/ do |arg1|
132
+ @date.uncertain!(:day) if arg1 =~ /y(es)?/i
133
+ end
134
+
135
+ When /^the year is approximate: "([^"]*)"$/ do |arg1|
136
+ @date.approximate!(:year) if arg1 =~ /y(es)?/i
137
+ end
138
+
139
+ When /^the month is approximate: "([^"]*)"$/ do |arg1|
140
+ @date.approximate!(:month) if arg1 =~ /y(es)?/i
141
+ end
142
+
143
+ When /^the day is approximate "([^"]*)"$/ do |arg1|
144
+ @date.approximate!(:day) if arg1 =~ /y(es)?/i
145
+ end
data/lib/edtf.rb CHANGED
@@ -28,18 +28,23 @@
28
28
  # policies, either expressed or implied, of the copyright holder.
29
29
  #++
30
30
 
31
+ if ENV['DEBUG']
32
+ require 'ruby-debug'
33
+ Debugger.start
34
+ end
35
+
31
36
  require 'date'
32
37
  require 'time'
33
38
 
34
39
  autoload :Rational, 'rational'
35
40
 
36
41
  require 'forwardable'
42
+ require 'enumerator'
37
43
 
38
44
  require 'active_support/core_ext/date/calculations'
39
45
  require 'active_support/core_ext/date_time/calculations'
40
46
  require 'active_support/core_ext/time/calculations'
41
47
 
42
-
43
48
  require 'active_support/core_ext/date/conversions'
44
49
 
45
50
  require 'edtf/compatibility'
@@ -63,11 +68,17 @@ require 'edtf/extensions'
63
68
  # To parse EDTF strings use either `Date.edtf` of `EDTF.parse`.
64
69
  #
65
70
  module EDTF
66
-
71
+
72
+ module_function
73
+
67
74
  def parse(input, options = {})
68
- ::Date.edtf(input, options)
75
+ parse!(input, options)
76
+ rescue
77
+ nil
69
78
  end
70
79
 
71
- module_function :parse
72
-
80
+ def parse!(input, options = {})
81
+ ::Date.edtf!(input, options)
82
+ end
83
+
73
84
  end
@@ -1,19 +1,8 @@
1
1
 
2
- # unless DateTime.respond_to?(:to_time)
3
- # require 'time'
4
- #
5
- # class DateTime
6
- # def to_time
7
- # Time.parse(to_s)
8
- # end
9
- # end
10
- # end
11
-
12
-
13
2
  class DateTime
14
-
15
- def iso8601
16
- to_time.iso8601
17
- end unless method_defined?(:iso8601)
18
-
3
+
4
+ def iso8601
5
+ to_time.iso8601
6
+ end unless method_defined?(:iso8601)
7
+
19
8
  end
data/lib/edtf/date.rb CHANGED
@@ -1,34 +1,66 @@
1
1
  class Date
2
2
 
3
- PRECISIONS = [:year, :month, :day].freeze
3
+ PRECISION = [:year, :month, :day].freeze
4
+ PRECISIONS = Hash[*PRECISION.map { |p| [p, "#{p}s".to_sym] }.flatten].freeze
5
+
4
6
  FORMATS = %w{ %04d %02d %02d }.freeze
5
7
 
8
+ SYMBOLS = {
9
+ :uncertain => '?',
10
+ :approximate => '~',
11
+ :calendar => '^',
12
+ :unspecified => 'u'
13
+ }.freeze
14
+
6
15
  EXTENDED_ATTRIBUTES = %w{ calendar precision uncertain approximate
7
16
  unspecified }.map(&:to_sym).freeze
8
17
 
9
18
  extend Forwardable
10
19
 
11
20
  class << self
21
+
12
22
  def edtf(input, options = {})
13
- ::EDTF::Parser.new(options).parse(input)
23
+ edtf!(input, options)
24
+ rescue
25
+ nil
26
+ end
27
+
28
+ def edtf!(input, options = {})
29
+ ::EDTF::Parser.new(options).parse!(input)
14
30
  end
15
31
  end
16
32
 
17
33
  attr_accessor :calendar
18
34
 
19
- PRECISIONS.each do |p|
20
- define_method("#{p}_precision?") { @precision == p }
21
- define_method("#{p}_precision!") { @precision = p }
35
+ PRECISION.each do |p|
36
+ define_method("#{p}_precision?") { precision == p }
37
+
38
+ define_method("#{p}_precision!") do
39
+ self.precision = p
40
+ self
41
+ end
42
+
43
+ define_method("#{p}_precision") do
44
+ change(:precision => p)
45
+ end
22
46
  end
23
47
 
24
- def dup
25
- d = super
26
- d.uncertain = uncertain.dup
27
- d.approximate = approximate.dup
28
- d.unspecified = unspecified.dup
29
- d
48
+
49
+ def initialize_copy(other)
50
+ super
51
+ copy_extended_attributes(other)
30
52
  end
31
53
 
54
+
55
+ # Alias advance method from Active Support.
56
+ alias original_advance advance
57
+
58
+ # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
59
+ # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
60
+ def advance(options)
61
+ original_advance(options).copy_extended_attributes(self)
62
+ end
63
+
32
64
  # Alias change method from Active Support.
33
65
  alias original_change change
34
66
 
@@ -41,6 +73,7 @@ class Date
41
73
  d
42
74
  end
43
75
 
76
+
44
77
  # Returns this Date's precision.
45
78
  def precision
46
79
  @precision ||= :day
@@ -49,21 +82,17 @@ class Date
49
82
  # Sets this Date/Time's precision to the passed-in value.
50
83
  def precision=(precision)
51
84
  precision = precision.to_sym
52
- raise ArgumentError, "invalid precision #{precision.inspect}" unless PRECISIONS.include?(precision)
85
+ raise ArgumentError, "invalid precision #{precision.inspect}" unless PRECISION.include?(precision)
53
86
  @precision = precision
54
87
  update_precision_filter[-1]
55
88
  end
56
89
 
57
- def self.included(base)
58
- base.extend(ClassMethods)
59
- end
60
-
61
90
  def uncertain
62
91
  @uncertain ||= EDTF::Uncertainty.new
63
92
  end
64
93
 
65
94
  def approximate
66
- @approximate ||= EDTF::Uncertainty.new
95
+ @approximate ||= EDTF::Uncertainty.new(nil, nil, nil, 8)
67
96
  end
68
97
 
69
98
  def unspecified
@@ -108,7 +137,7 @@ class Date
108
137
 
109
138
  alias precisely! precise!
110
139
 
111
- def_delegators :unspecified, :unspecified?, :specified?, :unsepcific?, :specific?
140
+ def_delegators :unspecified, :unspecified?, :specified?, :unspecific?, :specific?
112
141
 
113
142
  def unspecified!(arguments = precision_filter)
114
143
  unspecified.unspecified!(arguments)
@@ -124,35 +153,103 @@ class Date
124
153
 
125
154
  alias specific! specified!
126
155
 
156
+ # Returns false for Dates.
127
157
  def season?; false; end
128
158
 
159
+ # Returns true if the Date has an EDTF calendar string attached.
160
+ def calendar?; !!@calendar; end
161
+
162
+ # Converts the Date into a season.
129
163
  def season
130
164
  Season.new(self)
131
165
  end
132
166
 
167
+ # Returns the Date's EDTF string.
133
168
  def edtf
134
- FORMATS.take(values.length).join('-') % values << (uncertain? ? '?' : '')
169
+ return "y#{year}" if long_year?
170
+
171
+ s = FORMATS.take(values.length).zip(values).map { |f,v| f % v }
172
+ s = unspecified.mask(s)
173
+
174
+ unless (h = ua_hash).zero?
175
+ #
176
+ # To efficiently calculate the uncertain/approximate state we use
177
+ # the bitmask. The primary flags are:
178
+ #
179
+ # Uncertain: 1 - year, 2 - month, 4 - day
180
+ # Approximate: 8 - year, 16 - month, 32 - day
181
+ #
182
+ # Invariant: assumes that uncertain/approximate are not set for values
183
+ # not covered by precision!
184
+ #
185
+ y, m, d = s
186
+
187
+ # ?/~ if true-false or true-true and other false-true
188
+ y << SYMBOLS[:uncertain] if 3&h==1 || 27&h==19
189
+ y << SYMBOLS[:approximate] if 24&h==8 || 27&h==26
190
+
191
+
192
+ # combine if false-true-true and other m == d
193
+ if 7&h==6 && (48&h==48 || 48&h==0) || 56&h==48 && (6&h==6 || 6&h==0)
194
+ m[0,0] = '('
195
+ d << ')'
196
+ else
197
+ case
198
+ # false-true
199
+ when 3&h==2 || 24&h==16
200
+ m[0,0] = '('
201
+ m << ')'
202
+
203
+ # *-false-true
204
+ when 6&h==4 || 48&h==32
205
+ d[0,0] = '('
206
+ d << ')'
207
+ end
208
+
209
+ # ?/~ if *-true-false or *-true-true and other m != d
210
+ m << SYMBOLS[:uncertain] if h!=31 && (6&h==2 || 6&h==6 && (48&h==16 || 48&h==32))
211
+ m << SYMBOLS[:approximate] if h!=59 && (48&h==16 || 48&h==48 && (6&h==2 || 6&h==4))
212
+ end
213
+
214
+ # ?/~ if *-*-true
215
+ d << SYMBOLS[:uncertain] if 4&h==4
216
+ d << SYMBOLS[:approximate] if 32&h==32
217
+ end
218
+
219
+ s = s.join('-')
220
+ s << SYMBOLS[:calendar] << calendar if calendar?
221
+ s
135
222
  end
136
223
 
137
224
  alias to_edtf edtf
138
225
 
139
- # Returns a the Date of the next day, month, or year depending on the
226
+ # Returns the Date of the next day, month, or year depending on the
140
227
  # current Date/Time's precision.
141
- def next
142
- send("next_#{precision}")
228
+ def next(n = 1)
229
+ if n > 1
230
+ 1.upto(n).map { |by| advance(PRECISIONS[precision] => by) }
231
+ else
232
+ advance(PRECISIONS[precision] => 1)
233
+ end
143
234
  end
144
-
145
- # def succ
146
- # end
147
235
 
148
- # def ==(other)
149
- # end
236
+ alias succ next
150
237
 
151
- # def <=>(other)
152
- # end
238
+ # Returns the Date of the previous day, month, or year depending on the
239
+ # current Date/Time's precision.
240
+ def prev(n = 1)
241
+ if n > 1
242
+ 1.upto(n).map { |by| advance(PRECISIONS[precision] => -by) }
243
+ else
244
+ advance(PRECISIONS[precision] => -1)
245
+ end
246
+ end
247
+
248
+ def <=>(other)
249
+ return nil unless other.is_a?(::Date)
250
+ values <=> other.values
251
+ end
153
252
 
154
- # def ===(other)
155
- # end
156
253
 
157
254
  # Returns an array of the current year, month, and day values filtered by
158
255
  # the Date/Time's precision.
@@ -160,16 +257,24 @@ class Date
160
257
  precision_filter.map { |p| send(p) }
161
258
  end
162
259
 
163
- # Returns the same date with negated year
260
+ # Returns the same date but with negated year.
164
261
  def negate
165
262
  change(:year => year * -1)
166
263
  end
167
264
 
168
265
  alias -@ negate
169
266
 
170
-
267
+ # Returns true if this Date/Time has year precision and the year exceeds four digits.
268
+ def long_year?
269
+ precision == :year && year.abs > 9999
270
+ end
271
+
171
272
  private
172
-
273
+
274
+ def ua_hash
275
+ uncertain.hash + approximate.hash
276
+ end
277
+
173
278
  def precision_filter
174
279
  @precision_filter ||= update_precision_filter
175
280
  end
@@ -188,5 +293,16 @@ class Date
188
293
  protected
189
294
 
190
295
  attr_writer :uncertain, :unspecified, :approximate
191
-
296
+
297
+ def copy_extended_attributes(other)
298
+ @uncertain = other.uncertain.dup
299
+ @approximate = other.approximate.dup
300
+ @unspecified = other.unspecified.dup
301
+
302
+ @calendar = other.calendar.dup if other.calendar?
303
+ @precision = other.precision
304
+
305
+ self
306
+ end
307
+
192
308
  end
@@ -1,9 +1,9 @@
1
1
  class DateTime
2
-
3
- alias edtf iso8601
4
- alias to_edtf edtf
5
-
2
+
3
+ alias edtf iso8601
4
+ alias to_edtf edtf
5
+
6
6
  def values
7
- super + [hour,minute,second,offset]
7
+ super().cocat([hour,minute,second,offset])
8
8
  end
9
9
  end
data/lib/edtf/interval.rb CHANGED
@@ -1,86 +1,285 @@
1
1
  module EDTF
2
2
 
3
+ # An interval behaves like a regular Range but is dedicated to EDTF dates.
4
+ # Most importantly, intervals use the date's precision when generating
5
+ # the set of contained values and for membership tests. All tests are
6
+ # implemented without iteration and should therefore be considerably faster
7
+ # than if you were to use a regular Range.
8
+ #
9
+ # For example, the interval "2003/2006" covers the years 2003, 2004, 2005
10
+ # and 2006. Converting the interval to an array would result in a an array
11
+ # containing exactly four dates with year precision. This is also reflected
12
+ # in membership tests.
13
+ #
14
+ # Date.edtf('2003/2006').length -> 4
15
+ #
16
+ # Date.edtf('2003/2006').include? Date.edtf('2004') -> true
17
+ # Date.edtf('2003/2006').include? Date.edtf('2004-03') -> false
18
+ #
19
+ # Date.edtf('2003/2006').cover? Date.edtf('2004-03') -> true
20
+ #
3
21
  class Interval
4
22
 
5
23
  extend Forwardable
6
-
7
- include Enumerable
8
24
 
9
- def_delegators :to_range, *(Range.instance_methods - Enumerable.instance_methods - Object.instance_methods)
25
+ include Comparable
26
+ include Enumerable
10
27
 
11
- attr_reader :from, :to
28
+ # Intervals delegate hash calculation to Ruby Range
29
+ def_delegators :to_range, :eql?, :hash
30
+ def_delegators :to_a, :length, :empty?
12
31
 
13
- def initialize(from = :open, to = :open)
32
+ attr_accessor :from, :to
33
+
34
+ def initialize(from = Date.today, to = :open)
14
35
  @from, @to = from, to
15
36
  end
16
37
 
17
- def from=(from)
18
- @from = from || :open
19
- end
20
-
21
- def to=(to)
22
- @to = to || :open
23
- end
24
-
25
38
  [:open, :unknown].each do |method_name|
26
-
27
- define_method("#{method_name}?") do
28
- @to == method_name || @from == method_name
29
- end
30
-
31
- define_method("#{method_name}!") do
39
+ define_method("#{method_name}_end!") do
32
40
  @to = method_name
41
+ self
33
42
  end
34
43
 
35
- alias_method("#{method_name}_end!", "#{method_name}!")
36
-
37
44
  define_method("#{method_name}_end?") do
38
45
  @to == method_name
39
46
  end
40
-
41
47
  end
42
-
48
+
49
+ alias open! open_end!
50
+ alias open? open_end?
51
+
43
52
  def unknown_start?
44
- @from == :unknown
53
+ from == :unknown
45
54
  end
46
55
 
47
56
  def unknown_start!
48
57
  @from = :unknown
58
+ self
59
+ end
60
+
61
+ def unknown?
62
+ unknown_start? || unknown_end?
63
+ end
64
+
65
+ # Returns the intervals precision. Mixed precisions are currently not
66
+ # supported; in that case, the start date's precision takes precedence.
67
+ def precision
68
+ min.precision || max.precision
69
+ end
70
+
71
+ # Returns true if the precisions of start and end date are not the same.
72
+ def mixed_precision?
73
+ min.precsion != max.precision
49
74
  end
50
75
 
76
+ def each(&block)
77
+ step(1, &block)
78
+ end
79
+
80
+
81
+ # call-seq:
82
+ # interval.step(by=1) { |date| block } -> self
83
+ # interval.step(by=1) -> Enumerator
84
+ #
85
+ # Iterates over the interval by passing by elements at each step and
86
+ # yielding each date to the passed-in block. Note that the semantics
87
+ # of by are precision dependent: e.g., a value of 2 can mean 2 days,
88
+ # 2 months, or 2 years.
89
+ #
90
+ # If not block is given, returns an enumerator instead.
91
+ #
92
+ def step(by = 1)
93
+ raise ArgumentError unless by.respond_to?(:to_i)
94
+
95
+ if block_given?
96
+ f, t, by = min, max, by.to_i
97
+
98
+ unless f.nil? || t.nil? || by < 1
99
+ by = { Date::PRECISIONS[precision] => by }
100
+
101
+ until f > t do
102
+ yield f
103
+ f = f.advance(by)
104
+ end
105
+ end
106
+
107
+ self
108
+ else
109
+ enum_for(:step, by)
110
+ end
111
+ end
112
+
113
+
114
+ # This method always returns false for Range compatibility. EDTF intervals
115
+ # always include the last date.
116
+ def exclude_end?
117
+ false
118
+ end
119
+
120
+
51
121
  # TODO how to handle +/- Infinity for Dates?
122
+ # TODO we can't delegate to Ruby range for mixed precision intervals
52
123
 
124
+ # Returns the Interval as a Range.
53
125
  def to_range
54
126
  case
55
- when open?
56
- nil
57
- when unknown_end?
127
+ when open?, unknown?
58
128
  nil
59
129
  else
60
- Range.new(unknown_start? ? Date.new : @from, bounds)
130
+ Range.new(unknown_start? ? Date.new : @from, max)
61
131
  end
62
132
  end
63
133
 
64
- def bounds
134
+ # Returns true if other is an element of the Interval, false otherwise.
135
+ # Comparision is done according to the Interval's min/max date and
136
+ # precision.
137
+ def include?(other)
138
+ cover?(other) && precision == other.precision
139
+ end
140
+ alias member? include?
141
+
142
+ # Returns true if other is an element of the Interval, false otherwise.
143
+ # In contrast to #include? and #member? this method does not take into
144
+ # account the date's precision.
145
+ def cover?(other)
146
+ return false unless other.is_a?(Date)
147
+
148
+ other = other.day_precision
149
+
65
150
  case
66
- when open_end?, to.day_precision?
67
- to
68
- when to.month_precision?
69
- to.end_of_month
151
+ when unknown_start?
152
+ max.day_precision! == other
153
+ when unknown_end?
154
+ min.day_precision! == other
155
+ when open_end?
156
+ min.day_precision! <= other
157
+ else
158
+ min.day_precision! <= other && other <= max.day_precision!
159
+ end
160
+ end
161
+
162
+ # call-seq:
163
+ # interval.first -> Date or nil
164
+ # interval.first(n) -> Array
165
+ #
166
+ # Returns the first date in the interval, or the first n dates.
167
+ def first(n = 1)
168
+ if n > 1
169
+ (ds = Array(min)).empty? ? ds : ds.concat(ds[0].next(n - 1))
170
+ else
171
+ min
172
+ end
173
+ end
174
+
175
+ # call-seq:
176
+ # interval.last -> Date or nil
177
+ # interval.last(n) -> Array
178
+ #
179
+ # Returns the last date in the interval, or the last n dates.
180
+ def last(n = 1)
181
+ if n > 1
182
+ (ds = Array(max)).empty? ? ds : ds.concat(ds[0].prev(n - 1))
183
+ else
184
+ max
185
+ end
186
+ end
187
+
188
+ # call-seq:
189
+ # interval.min -> Date or nil
190
+ # interval.min { |a,b| block } -> Date or nil
191
+ #
192
+ # Returns the minimum value in the interval. If a block is given, it is
193
+ # used to compare values (slower). Returns nil if the first date of the
194
+ # interval is larger than the last or if the interval has an unknown or
195
+ # open start.
196
+ def min
197
+ if block_given?
198
+ to_a.min(&Proc.new)
199
+ else
200
+ case
201
+ when unknown_start?, !open? && to < from
202
+ nil
203
+ when from.day_precision?
204
+ from
205
+ when from.month_precision?
206
+ from.beginning_of_month
207
+ else
208
+ from.beginning_of_year
209
+ end
210
+ end
211
+ end
212
+
213
+ def begin
214
+ min
215
+ end
216
+
217
+ # call-seq:
218
+ # interval.max -> Date or nil
219
+ # interval.max { |a,b| block } -> Date or nil
220
+ #
221
+ # Returns the maximum value in the interval. If a block is given, it is
222
+ # used to compare values (slower). Returns nil if the first date of the
223
+ # interval is larger than the last or if the interval has an unknown or
224
+ # open end.
225
+ #
226
+ # To calculate the dates, precision is taken into account. Thus, the max
227
+ # Date of "2007/2008" would be 2008-12-31, whilst the max Date of
228
+ # "2007-12/2008-10" would be 2009-10-31.
229
+ def max
230
+ if block_given?
231
+ to_a.max(&Proc.new)
232
+ else
233
+ case
234
+ when open_end?, unknown_end?, !unknown_start? && to < from
235
+ nil
236
+ when to.day_precision?
237
+ to
238
+ when to.month_precision?
239
+ to.end_of_month
240
+ else
241
+ to.end_of_year
242
+ end
243
+ end
244
+ end
245
+
246
+ def end
247
+ max
248
+ end
249
+
250
+ def <=>(other)
251
+ case other
252
+ when Interval
253
+ [min, max] <=> [other.min, other.max]
254
+ when Date
255
+ cover?(other) ? min <=> other : 0
70
256
  else
71
- to.end_of_year
257
+ nil
72
258
  end
73
259
  end
74
-
75
- def edtf
76
- [
77
- @from.send(@from.respond_to?(:edtf) ? :edtf : :to_s),
78
- @to.send(@to.respond_to?(:edtf) ? :edtf : :to_s)
79
- ].join('/')
80
- end
81
-
82
- alias to_s edtf
83
-
260
+
261
+ def ===(other)
262
+ case other
263
+ when Interval
264
+ cover?(other.min) && cover?(other.max)
265
+ when Date
266
+ cover?(other)
267
+ else
268
+ false
269
+ end
270
+ end
271
+
272
+
273
+ # Returns the Interval as an EDTF string.
274
+ def edtf
275
+ [
276
+ from.send(from.respond_to?(:edtf) ? :edtf : :to_s),
277
+ to.send(to.respond_to?(:edtf) ? :edtf : :to_s)
278
+ ] * '/'
279
+ end
280
+
281
+ alias to_s edtf
282
+
84
283
  end
85
284
 
86
285
  end