citeproc 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/lib/citeproc/date.rb CHANGED
@@ -1,229 +1,521 @@
1
-
2
1
  module CiteProc
3
2
 
4
- class Date < Variable
5
-
6
- include Attributes
7
-
8
- alias attributes value
9
- private :attributes, :value=
10
-
11
-
12
- # Date parsers (must respond to :parse)
13
- @parsers = []
14
-
15
- require 'date'
16
- @parsers << ::Date
17
-
18
- begin
19
- require 'chronic'
20
- @parsers << Chronic
21
- rescue LoadError
22
- # warn 'failed to load chronic gem'
23
- end
24
-
25
- # Format string used for sorting dates
26
- @sort_order = "%04d%02d%02d-%04d%02d%02d".freeze
27
-
28
- class << self
29
-
30
- attr_reader :parsers, :sort_order
31
-
32
- # Parses the passed-in string with all available date parsers. Creates
33
- # a new CiteProc Date from the first valid date returned by a parser;
34
- # returns nil if no parser was able to parse the string successfully.
35
- #
36
- # For an equivalent method that raises an error on invalid input
37
- # @see #parse!
38
- def parse(date_string)
39
- parse!(date_string)
40
- rescue ParseError
41
- nil
42
- end
43
-
44
- # Like #parse but raises a ParseError if the input failed to be parsed.
45
- def parse!(date_string)
46
- @parsers.each do |p|
47
- d = p.parse(date_string) rescue nil
48
- return new(d) unless d.nil?
49
- end
50
-
51
- # if we get here, all parsers failed
52
- raise ParseError, "failed to parse #{date_string.inspect}"
53
- end
54
-
55
- def today
56
- new(::Date.today)
57
- end
58
-
59
- alias now today
60
-
61
- end
62
-
63
- attr_predicates :circa, :season, :literal, :'date-parts'
64
-
65
- # Make Date behave like a regular Ruby Date
66
- def_delegators :to_ruby,
67
- *::Date.instance_methods(false).reject { |m| m.to_s =~ /^to_s$|^inspect$|start$|^\W/ }
68
-
69
-
70
- def initialize(value = ::Date.today)
71
- super
72
- end
73
-
74
- def initialize_copy(other)
75
- @value = other.value.deep_copy
76
- end
77
-
78
- def replace(value)
79
- case
80
- when value.is_a?(CiteProc::Date)
81
- initialize_copy(value)
82
-
83
- when value.is_a?(Numeric)
84
- @value = { :'date-parts' => [[value.to_i]] }
85
-
86
- when value.is_a?(Hash)
87
- attributes = value.symbolize_keys
88
-
89
- if attributes.has_key?(:raw)
90
- @value = Date.parse(attributes.delete(:raw)).value
91
- @value.merge!(attributes)
92
- else
93
- @value = attributes.deep_copy
94
- end
95
- to_i!
96
-
97
- when value.respond_to?(:strftime)
98
- @value = { :'date-parts' => [value.strftime('%Y-%m-%d').split(/-/).map(&:to_i)] }
99
-
100
- when value.is_a?(Array)
101
- @value = { :'date-parts' => value[0].is_a?(Array) ? value : [value] }
102
- to_i!
103
-
104
- when value.respond_to?(:to_s)
105
- @value = Date.parse(value.to_s).value
106
-
107
- else
108
- raise TypeError, "failed to create date from #{value.inspect}"
109
- end
110
-
111
- self
112
- end
113
-
114
- # TODO replace the date parts by two proper dates or structs
115
-
116
- def date_parts
117
- @value[:'date-parts'] ||= [[]]
118
- end
119
-
120
- alias parts date_parts
121
- alias parts= date_parts=
122
-
123
- def empty?
124
- parts.flatten.compact.empty?
125
- end
126
-
127
- %w{ year month day }.each_with_index do |m,i|
128
- define_method(m) { parts[0][i] }
129
- define_method("#{m}=") { |v| parts[0][i] = v.to_i }
130
- end
131
-
132
- def -@
133
- d = dup
134
- d.year = -1 * year
135
- d
136
- end
137
-
138
- def start_date
139
- ::Date.new(*parts[0])
140
- end
141
-
142
- def start_date=(date)
143
- parts[0] = date.strftime('%Y-%m-%d').split(/-/).map(&:to_i)
144
- end
145
-
146
- def end_date=(date)
147
- parts[1] = date.nil? ? [0,0,0] : date.strftime('%Y-%m-%d').split(/-/).map(&:to_i)
148
- end
149
-
150
- # Returns a Ruby date object for this instance, or Range object if this
151
- # instance is closed range
152
- def to_ruby
153
- closed_range? ? start_date ... end_date : start_date
154
- end
155
-
156
- def end_date
157
- closed_range? ? ::Date.new(*parts[1]) : nil
158
- end
159
-
160
- def has_end_date?
161
- parts[1] && !parts[1].empty?
162
- end
163
-
164
- # Returns true if this date is a range
165
- alias range? has_end_date?
166
-
167
- def open_range?
168
- range? && parts[1].uniq == [0]
169
- end
170
-
171
- alias open? open_range?
172
-
173
- def closed_range?
174
- range? && !open_range?
175
- end
176
-
177
- alias closed? closed_range?
178
-
179
- alias uncertain? circa?
180
-
181
- # Marks the date as uncertain
182
- def uncertain!
183
- @value[:circa] = true
184
- end
185
-
186
- # Marks the date as a certain date
187
- def certain!
188
- @value[:circa] = false
189
- end
190
-
191
- def certain?
192
- !uncertain?
193
- end
194
-
195
- def numeric?
196
- false
197
- end
198
-
199
- def bc?; year and year < 0; end
200
- def ad?; not bc? and year < 1000; end
201
-
202
- def to_citeproc
203
- cp = @value.stringify_keys
204
- cp.delete('date-parts') if empty?
205
- cp
206
- end
207
-
208
- def to_s
209
- literal? ? literal : to_ruby.to_s
210
- end
211
-
212
- def sort_order
213
- Date.sort_order % ((parts[0] + [0,0,0])[0,3] + ((parts[1] || []) + [0,0,0])[0,3])
214
- end
215
-
216
- def <=>(other)
217
- return nil unless other.is_a?(Date)
218
- [year, sort_order] <=> [other.year, other.sort_order]
219
- end
220
-
221
- private
222
-
223
- def to_i!
224
- parts.each { |p| p.map!(&:to_i) }
225
- end
226
-
227
- end
3
+ # Represents a {Variable} wrapping a date value. A date value is a hybrid
4
+ # object in that it can represent either an atomic date or a date range,
5
+ # depending on whether or not the 'date-parts' attribute contains one
6
+ # or two lists of date parts.
7
+ #
8
+ # {Date Dates} can be constructed from a wide range of input values,
9
+ # including Ruby date objects, integers, date ranges, ISO 8601 and
10
+ # CiteProc JSON strings or hashes, and - provided you have the respective
11
+ # gems installed - EDTF strings all strings supported by Chronic.
12
+ #
13
+ # @example Initialization
14
+ # CiteProc::Date.new
15
+ # #-> #<CiteProc::Date "[]">
16
+ #
17
+ # CiteProc::Date.today
18
+ # #-> #<CiteProc::Date "[2012, 6, 10]">
19
+ #
20
+ # CiteProc::Date.new('Yesterday')
21
+ # #-> #<CiteProc::Date "[[2012, 6, 9]]">
22
+ #
23
+ # CiteProc::Date.new(1966)
24
+ # #-> #<CiteProc::Date "[1966]">
25
+ #
26
+ # CiteProc::Date.new(1999..2003)
27
+ # #-> #<CiteProc::Date "[[1999], [2003]]">
28
+ #
29
+ # CiteProc::Date.new(Date.new(1900)...Date.new(2000))
30
+ # #-> #<CiteProc::Date "[[1900, 1, 1], [1999, 12, 31]]">
31
+ #
32
+ # CiteProc::Date.new('2009-03?')
33
+ # #-> #<CiteProc::Date "[[2009, 3]]">
34
+ #
35
+ # CiteProc::Date.new('2001-02/2007')
36
+ # #-> #<CiteProc::Date "[[2001, 2, 1], [2007, 12, 31]]">
37
+ #
38
+ # {Date} instances are typically manipulated by a cite processor. Therefore,
39
+ # the API is optimized for easy information extraction and formatting.
40
+ # Additionally, {Date Dates} can be serialized as CiteProc JSON data.
41
+ #
42
+ # @example Serialization
43
+ # CiteProc::Date.new('2009-03?').to_citeproc
44
+ # #-> {"date-parts"=>[[2009, 3]], "circa"=>true}
45
+ #
46
+ # CiteProc::Date.new(1999..2003).to_json
47
+ # #-> '{"date-parts":[[1999],[2003]]}'
48
+ #
49
+ class Date < Variable
50
+
51
+
52
+ # Represents the individual parts of a date (i.e., year, month, day).
53
+ # There is a sublte difference between CiteProc dates (and date parts)
54
+ # and regular Ruby dates, because a Ruby date will always contain valid
55
+ # year, month and date values, whereas CiteProc dates may leave the month
56
+ # and day parts empty. That is to say, CiteProc distinguishes between
57
+ # the first of May 1955 and the month of May 1955 - a distinction that
58
+ # is not supported by regular Ruby dates.
59
+ #
60
+ # may_1955 = CiteProc::Date::DateParts.new(1955, 5)
61
+ # first_of_may_1955 = CiteProc::Date::DateParts.new(1955, 5, 1)
62
+ #
63
+ # may_1955 < first_of_may_1955
64
+ # #-> true
65
+ #
66
+ # Date.new(1955, 5) < Date.new(1955, 5, 1)
67
+ # #-> false
68
+ #
69
+ # The above example shows that a month's sort order is less than a day
70
+ # in that month, whereas, with Ruby date's there is no such distinction.
71
+ #
72
+ # The {DateParts} class encapsulates the year, month and day parts of a
73
+ # date; it is used internally by {Date} variables and not supposed to
74
+ # be used in an external context.
75
+ class DateParts < Struct.new(:year, :month, :day)
76
+ include Comparable
77
+
78
+ def initialize(*arguments)
79
+ if arguments.length == 1 && arguments[0].is_a?(::Date)
80
+ d = arguments[0]
81
+ super(d.year, d.month, d.day)
82
+ else
83
+ super(*arguments.map(&:to_i))
84
+ end
85
+ end
86
+
87
+ def initialize_copy(other)
88
+ update(other)
89
+ end
90
+
91
+ # Update the date parts with the passed-in values.
92
+ # @param parts [Array, #each_pair] an ordered list of date parts (year,
93
+ # month, day) or a Hash containing the mapping
94
+ # @return [self]
95
+ def update(parts)
96
+ unless parts.respond_to?(:each_pair)
97
+ parts = Hash[DateParts.members.zip(parts)]
98
+ end
99
+
100
+ parts.each_pair do |part, value|
101
+ self[part] = value.nil? ? nil : value.to_i
102
+ end
103
+
104
+ self
105
+ end
106
+
107
+ # @return [Boolean] whether or not the date parts are unset
108
+ def empty?
109
+ to_citeproc.empty?
110
+ end
111
+
112
+ # In the current CiteProc specification, date parts consisting of
113
+ # zeroes are used to designate open ranges.
114
+ # @return [Boolean] whether or not the this is an open-end date
115
+ def open?
116
+ to_citeproc.include?(0)
117
+ end
118
+
119
+ # A date is said to be BC when the year is defined and less than zero.
120
+ # @return [Boolean] whether or not the date is BC
121
+ def bc?
122
+ !!(year && year < 0)
123
+ end
124
+
125
+ # A date is said to be AD when it is in the first millennium, i.e.,
126
+ # between 1 and 1000 AD
127
+ # @return [Boolean] whether or not the date is AD
128
+ def ad?
129
+ !bc? && year < 1000
130
+ end
131
+
132
+ # Formats the date parts according to the passed-in format string.
133
+ # @param format [String] a format string
134
+ # @return [String,nil] the formatted date string; nil if the date
135
+ # parts are not a valid date.
136
+ def strftime(format = '%F')
137
+ d = to_date
138
+
139
+ if d.nil?
140
+ nil
141
+ else
142
+ d.strftime(format)
143
+ end
144
+ end
145
+
146
+ # Compares the date parts with the passed-in date.
147
+ # @param other [DateParts, #to_date] the other date
148
+ # @return [Fixnum,nil] the result of the comparison (-1, 0, 1 or nil)
149
+ def <=>(other)
150
+ case
151
+ when other.is_a?(DateParts)
152
+ to_citeproc <=> other.to_citeproc
153
+ when other.respond_to?(:to_date)
154
+ to_date <=> other.to_date
155
+ else
156
+ nil
157
+ end
158
+ end
159
+
160
+ # Convert the date parts into a proper Ruby date object; if the date
161
+ # parts are empty, contain zero or are otherwise invalid, nil will
162
+ # be returned instead.
163
+ # @return [::Date,nil] the date parts as a Ruby date object
164
+ def to_date
165
+ parts = to_citeproc
166
+
167
+ if parts.empty? || parts.include?(0)
168
+ nil
169
+ else
170
+ begin
171
+ ::Date.new(*parts)
172
+ rescue
173
+ # Catch invalid dates (e.g., if the month is 13).
174
+ nil
175
+ end
176
+ end
177
+ end
178
+
179
+ alias to_ruby to_date
180
+
181
+ # @return [Array<Fixnum>] the list of date parts
182
+ def to_citeproc
183
+ take_while { |p| !p.nil? }
184
+ end
185
+
186
+ # @return [String] the date parts as a string
187
+ def to_s
188
+ to_citeproc.inspect
189
+ end
190
+
191
+ # @return [String] a human-readable representation of the object
192
+ def inspect
193
+ "#<DateParts #{to_s}>"
194
+ end
195
+ end
196
+
197
+
198
+ include Attributes
199
+
200
+ alias attributes value
201
+ protected :value, :attributes
202
+
203
+ undef_method :value=
204
+
205
+
206
+ # List of date parsers (must respond to #parse)
207
+ @parsers = []
208
+
209
+ [%w{ edtf EDTF }, %w{ chronic Chronic }].each do |date_parser, module_id|
210
+ begin
211
+ require date_parser
212
+ @parsers << ::Object.const_get(module_id)
213
+ rescue LoadError
214
+ warn "failed to load `#{date_parser}' gem"
215
+ end
216
+ end
217
+
218
+ @parsers << ::Date
219
+
220
+
221
+ class << self
222
+
223
+ # @!attribute [r] parsers
224
+ #
225
+ # A list of available date parsers. Each parser must respond to a
226
+ # #parse method that converts a date string into a Ruby date object.
227
+ # By default, the list will include Ruby's date parser from the
228
+ # standard library, as well as the parsers of the Chronic and EDTF
229
+ # gems if they are available; to install the latter on your system
230
+ # make sure to `gem install chronic edtf`.
231
+ #
232
+ # @return [Array] the available date parsers
233
+ attr_reader :parsers
234
+
235
+ # Parses the passed-in string with all available date parsers. Creates
236
+ # a new CiteProc Date from the first valid date returned by a parser;
237
+ # returns nil if no parser was able to parse the string successfully.
238
+ #
239
+ # For an equivalent method that raises an error on invalid input
240
+ # @see #parse!
241
+ #
242
+ # @param date_string [String] the date to be parsed
243
+ # @return [CiteProc::Date,nil] the parsed date or nil
244
+ def parse(date_string)
245
+ parse!(date_string)
246
+ rescue ParseError
247
+ nil
248
+ end
249
+
250
+ # Like #parse but raises a ParseError if the input failed to be parsed.
251
+ #
252
+ # @param date_string [String] the date to be parsed
253
+ # @return [CiteProc::Date] the parsed date
254
+ #
255
+ # @raise [ParseError] when the string cannot be parsed
256
+ def parse!(date_string)
257
+ @parsers.each do |p|
258
+ date = p.parse(date_string) rescue nil
259
+ return new(date) unless date.nil?
260
+ end
261
+
262
+ # Subtle: if we get here it means all parsers failed to create a date
263
+ raise ParseError, "failed to parse #{date_string.inspect}"
264
+ end
265
+
266
+ # @return [CiteProc::Date] a date object for the current day
267
+ def today
268
+ new(::Date.today)
269
+ end
270
+
271
+ alias now today
272
+
273
+ end
274
+
275
+
276
+ attr_predicates :circa, :season, :literal, :'date-parts'
277
+
278
+ # Make Date behave like a regular Ruby Date
279
+ def_delegators :to_ruby,
280
+ *::Date.instance_methods(false).reject { |m| m.to_s =~ /^to_s$|^inspect$|start$|^\W|uncertain|season/ }
281
+
282
+
283
+ def initialize(value = {})
284
+ super
285
+ yield self if block_given?
286
+ end
287
+
288
+ def initialize_copy(other)
289
+ @value = other.value.deep_copy
290
+ end
291
+
292
+ def merge(other)
293
+ super
294
+ convert_parts!
295
+ end
296
+
297
+ # Replaces the date's value. Typically called by the constructor, this
298
+ # method intelligently converts various input values.
299
+ def replace(value)
300
+ case
301
+ when value.is_a?(CiteProc::Date)
302
+ initialize_copy(value)
303
+ when value.is_a?(::Date) && Object.const_defined?(:EDTF)
304
+ @value = { :'date-parts' => [DateParts.new(*value.values)] }
305
+ uncertain! if value.uncertain?
306
+ when value.respond_to?(:strftime)
307
+ @value = { :'date-parts' => [DateParts.new(*value.strftime('%Y-%m-%d').split(/-/))] }
308
+ when value.is_a?(Numeric)
309
+ @value = { :'date-parts' => [DateParts.new(value)] }
310
+ when value.is_a?(Hash)
311
+ attributes = value.symbolize_keys
312
+
313
+ if attributes.has_key?(:raw)
314
+ @value = Date.parse(attributes.delete(:raw)).value
315
+ @value.merge!(attributes)
316
+ else
317
+ @value = attributes.deep_copy
318
+ end
319
+ convert_parts!
320
+
321
+ when value.is_a?(Array)
322
+ @value = { :'date-parts' => value[0].is_a?(Array) ? value : [value] }
323
+ convert_parts!
324
+ when !value.is_a?(String) && value.respond_to?(:min) && value.respond_to?(:max)
325
+ @value = { :'date-parts' => [
326
+ DateParts.new(value.min),
327
+ DateParts.new(value.max)
328
+ ]}
329
+ when value.is_a?(String) && /^\s*\{/ =~ value
330
+ return replace(MultiJson.decode(value, :symbolize_keys => true))
331
+ when value.respond_to?(:to_s)
332
+ @value = Date.parse!(value.to_s).value
333
+ else
334
+ raise TypeError, "failed to create date from #{value.inspect}"
335
+ end
336
+
337
+ self
338
+ end
339
+
340
+ # @return [Array<DateParts>]
341
+ def date_parts
342
+ @value[:'date-parts'] ||= []
343
+ end
344
+
345
+ alias parts date_parts
346
+ alias parts= date_parts=
347
+
348
+ # @return [Boolean] whether or not the date parts' are empty and the
349
+ # date is neither literal nor a season
350
+ def empty?
351
+ parts.all?(&:empty?) && !literal? && !season?
352
+ end
353
+
354
+ # @!attribute year
355
+ # @return [Fixnum] the year (of the start date for ranges)
356
+
357
+ # @!attribute month
358
+ # @return [Fixnum] the month (of the start date for ranges)
359
+
360
+ # @!attribute day
361
+ # @return [Fixnum] the day (of the start date for ranges)
362
+ [:year, :month, :day].each do |reader|
363
+ writer = "#{reader}="
364
+
365
+ define_method(reader) do
366
+ d = parts[0] and d.send(reader)
367
+ end
368
+
369
+ define_method(writer) do |v|
370
+ parts[0] ||= DateParts.new
371
+ parts[0].send(writer, v.to_i)
372
+ end
373
+ end
374
+
375
+ # @return [Date] a copy of the date with an inverted year
376
+ def -@
377
+ d = dup
378
+ d.year = -1 * year
379
+ d
380
+ end
381
+
382
+ # @return [::Date,nil] the date (start date if this instance is a range); or nil
383
+ def start_date
384
+ d = parts[0] and d.to_date
385
+ end
386
+
387
+ def start_date=(date)
388
+ parts[0] = DateParts.new(date.strftime('%Y-%m-%d').split(/-/))
389
+ end
390
+
391
+ def end_date=(date)
392
+ parts[1] = DateParts.new(date.nil? ? 0 : date.strftime('%Y-%m-%d').split(/-/))
393
+ end
394
+
395
+ # @return [Date,Range] the date as a Ruby date object or as a Range if
396
+ # this instance is closed range
397
+ def to_ruby
398
+ if closed_range?
399
+ start_date..end_date
400
+ else
401
+ start_date
402
+ end
403
+ end
404
+
405
+ # @return [::Date,nil] the range's end date; or nil
406
+ def end_date
407
+ d = parts[1] and d.to_date
408
+ end
409
+
410
+ # @return [Boolean] whether or not the date-parts contain an end date
411
+ def has_end_date?
412
+ parts[1] && !parts[1].empty?
413
+ end
414
+
415
+ # Returns true if this date is a range
416
+ alias range? has_end_date?
417
+
418
+ # @return [Boolean] whether or not this date is an open range
419
+ def open_range?
420
+ range? && parts[1].open?
421
+ end
422
+
423
+ alias open? open_range?
424
+
425
+ # @return [Boolean] whether or not this date is a closed range
426
+ def closed_range?
427
+ range? && !open_range?
428
+ end
429
+
430
+ alias closed? closed_range?
431
+
432
+ alias uncertain? circa?
433
+
434
+ # Marks the date as uncertain
435
+ # @return [self]
436
+ def uncertain!
437
+ @value[:circa] = true
438
+ self
439
+ end
440
+
441
+ # Marks the date as a certain date
442
+ # @return [self]
443
+ def certain!
444
+ @value[:circa] = false
445
+ self
446
+ end
447
+
448
+ def certain?
449
+ !uncertain?
450
+ end
451
+
452
+ # @return false
453
+ def numeric?
454
+ false
455
+ end
456
+
457
+ # A date is said to be BC when the year is defined and less than zero.
458
+ # @return [Boolean, nil] whether or not the date is BC; nil if there is
459
+ # no start date set
460
+ def bc?
461
+ date = parts[0] and date.bc?
462
+ end
463
+
464
+ # A date is said to be AD when it is in the first millennium, i.e.,
465
+ # between 1 and 1000 AD
466
+ # @return [Boolean, nil] whether or not the date is AD; nil if there is
467
+ # no start date set
468
+ def ad?
469
+ date = parts[0] and date.ad?
470
+ end
471
+
472
+ # @return [Hash] a hash representation of the date.
473
+ def to_citeproc
474
+ cp = @value.stringify_keys
475
+
476
+ # Convert (or suppress empty) date-parts
477
+ if parts.all?(&:empty?)
478
+ cp.delete('date-parts')
479
+ else
480
+ cp['date-parts'] = cp['date-parts'].map(&:to_citeproc)
481
+ end
482
+
483
+ cp
484
+ end
485
+
486
+ # @return [String] the date as a string
487
+ def to_s
488
+ case
489
+ when literal?
490
+ literal
491
+ when season?
492
+ season
493
+ else
494
+ parts.map(&:to_citeproc).inspect
495
+ end
496
+ end
497
+
498
+ def <=>(other)
499
+ case other
500
+ when Date
501
+ parts <=> other.parts
502
+ when ::Date
503
+ parts <=> [other]
504
+ else
505
+ nil
506
+ end
507
+ end
508
+
509
+ private
510
+
511
+ def convert_parts!
512
+ parts.map! do |part|
513
+ part.is_a?(DateParts) ? part : DateParts.new(*part)
514
+ end
515
+
516
+ self
517
+ end
518
+
519
+ end
228
520
 
229
521
  end