citeproc 0.0.8 → 0.0.9

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.
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