edtf 0.0.6 → 0.0.7

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