class Date
PRECISION = [:year, :month, :day].freeze
PRECISIONS = Hash[*PRECISION.map { |p| [p, "#{p}s".to_sym] }.flatten].freeze
FORMATS = %w{ %04d %02d %02d }.freeze
SYMBOLS = {
:uncertain => '?',
:approximate => '~',
:calendar => '^',
:unspecified => 'u'
}.freeze
EXTENDED_ATTRIBUTES = %w{ calendar precision uncertain approximate
unspecified }.map(&:to_sym).freeze
extend Forwardable
class << self
def edtf(input, options = {})
edtf!(input, options)
rescue
nil
end
def edtf!(input, options = {})
::EDTF::Parser.new(options).parse!(input)
end
end
attr_accessor :calendar
PRECISION.each do |p|
define_method("#{p}_precision?") { precision == p }
define_method("#{p}_precision!") do
self.precision = p
self
end
define_method("#{p}_precision") do
change(:precision => p)
end
end
def initialize_copy(other)
super
copy_extended_attributes(other)
end
# Alias advance method from Active Support.
alias original_advance advance
# Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
# any of these keys: :years, :months, :weeks, :days.
def advance(options)
original_advance(options).copy_extended_attributes(self)
end
# Alias change method from Active Support.
alias original_change change
# Returns a new Date where one or more of the elements have been changed according to the +options+ parameter.
def change(options)
d = original_change(options)
EXTENDED_ATTRIBUTES.each do |attribute|
d.send("#{attribute}=", options[attribute] || send(attribute))
end
d
end
# Returns this Date's precision.
def precision
@precision ||= :day
end
# Sets this Date/Time's precision to the passed-in value.
def precision=(precision)
precision = precision.to_sym
raise ArgumentError, "invalid precision #{precision.inspect}" unless PRECISION.include?(precision)
@precision = precision
update_precision_filter[-1]
end
def uncertain
@uncertain ||= EDTF::Uncertainty.new
end
def approximate
@approximate ||= EDTF::Uncertainty.new(nil, nil, nil, 8)
end
def unspecified
@unspecified ||= EDTF::Unspecified.new
end
def_delegators :uncertain, :uncertain?, :certain?
def certain!(arguments = precision_filter)
uncertain.certain!(arguments)
self
end
def uncertain!(arguments = precision_filter)
uncertain.uncertain!(arguments)
self
end
def approximate?(arguments = precision_filter)
approximate.uncertain?(arguments)
end
alias approximately? approximate?
def approximate!(arguments = precision_filter)
approximate.uncertain!(arguments)
self
end
alias approximately! approximate!
def precise?(arguments = precision_filter)
!approximate?(arguments)
end
alias precisely? precise?
def precise!(arguments = precision_filter)
approximate.certain!(arguments)
self
end
alias precisely! precise!
def_delegators :unspecified, :unspecified?, :specified?, :unspecific?, :specific?
def unspecified!(arguments = precision_filter)
unspecified.unspecified!(arguments)
self
end
alias unspecific! unspecified!
def specified!(arguments = precision_filter)
unspecified.specified!(arguments)
self
end
alias specific! specified!
# Returns false for Dates.
def season?; false; end
# Returns true if the Date has an EDTF calendar string attached.
def calendar?; !!@calendar; end
# Converts the Date into a season.
def season
Season.new(self)
end
# Returns the Date's EDTF string.
def edtf
return "y#{year}" if long_year?
s = FORMATS.take(values.length).zip(values).map { |f,v| f % v }
s = unspecified.mask(s)
unless (h = ua_hash).zero?
#
# To efficiently calculate the uncertain/approximate state we use
# the bitmask. The primary flags are:
#
# Uncertain: 1 - year, 2 - month, 4 - day
# Approximate: 8 - year, 16 - month, 32 - day
#
# Invariant: assumes that uncertain/approximate are not set for values
# not covered by precision!
#
y, m, d = s
# ?/~ if true-false or true-true and other false-true
y << SYMBOLS[:uncertain] if 3&h==1 || 27&h==19
y << SYMBOLS[:approximate] if 24&h==8 || 27&h==26
# combine if false-true-true and other m == d
if 7&h==6 && (48&h==48 || 48&h==0) || 56&h==48 && (6&h==6 || 6&h==0)
m[0,0] = '('
d << ')'
else
case
# false-true
when 3&h==2 || 24&h==16
m[0,0] = '('
m << ')'
# *-false-true
when 6&h==4 || 48&h==32
d[0,0] = '('
d << ')'
end
# ?/~ if *-true-false or *-true-true and other m != d
m << SYMBOLS[:uncertain] if h!=31 && (6&h==2 || 6&h==6 && (48&h==16 || 48&h==32))
m << SYMBOLS[:approximate] if h!=59 && (48&h==16 || 48&h==48 && (6&h==2 || 6&h==4))
end
# ?/~ if *-*-true
d << SYMBOLS[:uncertain] if 4&h==4
d << SYMBOLS[:approximate] if 32&h==32
end
s = s.join('-')
s << SYMBOLS[:calendar] << calendar if calendar?
s
end
alias to_edtf edtf
# Returns the Date of the next day, month, or year depending on the
# current Date/Time's precision.
def next(n = 1)
if n > 1
1.upto(n).map { |by| advance(PRECISIONS[precision] => by) }
else
advance(PRECISIONS[precision] => 1)
end
end
alias succ next
# Returns the Date of the previous day, month, or year depending on the
# current Date/Time's precision.
def prev(n = 1)
if n > 1
1.upto(n).map { |by| advance(PRECISIONS[precision] => -by) }
else
advance(PRECISIONS[precision] => -1)
end
end
def <=>(other)
return nil unless other.is_a?(::Date)
values <=> other.values
end
# Returns an array of the current year, month, and day values filtered by
# the Date/Time's precision.
def values
precision_filter.map { |p| send(p) }
end
# Returns the same date but with negated year.
def negate
change(:year => year * -1)
end
alias -@ negate
# Returns true if this Date/Time has year precision and the year exceeds four digits.
def long_year?
year_precision? && year.abs > 9999
end
private
def ua_hash
uncertain.hash + approximate.hash
end
def precision_filter
@precision_filter ||= update_precision_filter
end
def update_precision_filter
case @precision
when :year
[:year]
when :month
[:year,:month]
else
[:year,:month,:day]
end
end
protected
attr_writer :uncertain, :unspecified, :approximate
def copy_extended_attributes(other)
@uncertain = other.uncertain.dup
@approximate = other.approximate.dup
@unspecified = other.unspecified.dup
@calendar = other.calendar.dup if other.calendar?
@precision = other.precision
self
end
end