lib/edtf/interval.rb in edtf-0.0.6 vs lib/edtf/interval.rb in edtf-0.0.7
- old
+ new
@@ -1,86 +1,285 @@
module EDTF
+ # An interval behaves like a regular Range but is dedicated to EDTF dates.
+ # Most importantly, intervals use the date's precision when generating
+ # the set of contained values and for membership tests. All tests are
+ # implemented without iteration and should therefore be considerably faster
+ # than if you were to use a regular Range.
+ #
+ # For example, the interval "2003/2006" covers the years 2003, 2004, 2005
+ # and 2006. Converting the interval to an array would result in a an array
+ # containing exactly four dates with year precision. This is also reflected
+ # in membership tests.
+ #
+ # Date.edtf('2003/2006').length -> 4
+ #
+ # Date.edtf('2003/2006').include? Date.edtf('2004') -> true
+ # Date.edtf('2003/2006').include? Date.edtf('2004-03') -> false
+ #
+ # Date.edtf('2003/2006').cover? Date.edtf('2004-03') -> true
+ #
class Interval
extend Forwardable
-
- include Enumerable
- def_delegators :to_range, *(Range.instance_methods - Enumerable.instance_methods - Object.instance_methods)
+ include Comparable
+ include Enumerable
- attr_reader :from, :to
+ # Intervals delegate hash calculation to Ruby Range
+ def_delegators :to_range, :eql?, :hash
+ def_delegators :to_a, :length, :empty?
- def initialize(from = :open, to = :open)
+ attr_accessor :from, :to
+
+ def initialize(from = Date.today, to = :open)
@from, @to = from, to
end
- def from=(from)
- @from = from || :open
- end
-
- def to=(to)
- @to = to || :open
- end
-
[:open, :unknown].each do |method_name|
-
- define_method("#{method_name}?") do
- @to == method_name || @from == method_name
- end
-
- define_method("#{method_name}!") do
+ define_method("#{method_name}_end!") do
@to = method_name
+ self
end
- alias_method("#{method_name}_end!", "#{method_name}!")
-
define_method("#{method_name}_end?") do
@to == method_name
end
-
end
-
+
+ alias open! open_end!
+ alias open? open_end?
+
def unknown_start?
- @from == :unknown
+ from == :unknown
end
def unknown_start!
@from = :unknown
+ self
end
+ def unknown?
+ unknown_start? || unknown_end?
+ end
+
+ # Returns the intervals precision. Mixed precisions are currently not
+ # supported; in that case, the start date's precision takes precedence.
+ def precision
+ min.precision || max.precision
+ end
+
+ # Returns true if the precisions of start and end date are not the same.
+ def mixed_precision?
+ min.precsion != max.precision
+ end
+
+ def each(&block)
+ step(1, &block)
+ end
+
+
+ # call-seq:
+ # interval.step(by=1) { |date| block } -> self
+ # interval.step(by=1) -> Enumerator
+ #
+ # Iterates over the interval by passing by elements at each step and
+ # yielding each date to the passed-in block. Note that the semantics
+ # of by are precision dependent: e.g., a value of 2 can mean 2 days,
+ # 2 months, or 2 years.
+ #
+ # If not block is given, returns an enumerator instead.
+ #
+ def step(by = 1)
+ raise ArgumentError unless by.respond_to?(:to_i)
+
+ if block_given?
+ f, t, by = min, max, by.to_i
+
+ unless f.nil? || t.nil? || by < 1
+ by = { Date::PRECISIONS[precision] => by }
+
+ until f > t do
+ yield f
+ f = f.advance(by)
+ end
+ end
+
+ self
+ else
+ enum_for(:step, by)
+ end
+ end
+
+
+ # This method always returns false for Range compatibility. EDTF intervals
+ # always include the last date.
+ def exclude_end?
+ false
+ end
+
+
# TODO how to handle +/- Infinity for Dates?
+ # TODO we can't delegate to Ruby range for mixed precision intervals
+ # Returns the Interval as a Range.
def to_range
case
- when open?
+ when open?, unknown?
nil
- when unknown_end?
- nil
else
- Range.new(unknown_start? ? Date.new : @from, bounds)
+ Range.new(unknown_start? ? Date.new : @from, max)
end
end
- def bounds
+ # Returns true if other is an element of the Interval, false otherwise.
+ # Comparision is done according to the Interval's min/max date and
+ # precision.
+ def include?(other)
+ cover?(other) && precision == other.precision
+ end
+ alias member? include?
+
+ # Returns true if other is an element of the Interval, false otherwise.
+ # In contrast to #include? and #member? this method does not take into
+ # account the date's precision.
+ def cover?(other)
+ return false unless other.is_a?(Date)
+
+ other = other.day_precision
+
case
- when open_end?, to.day_precision?
- to
- when to.month_precision?
- to.end_of_month
+ when unknown_start?
+ max.day_precision! == other
+ when unknown_end?
+ min.day_precision! == other
+ when open_end?
+ min.day_precision! <= other
else
- to.end_of_year
+ min.day_precision! <= other && other <= max.day_precision!
end
end
-
- def edtf
- [
- @from.send(@from.respond_to?(:edtf) ? :edtf : :to_s),
- @to.send(@to.respond_to?(:edtf) ? :edtf : :to_s)
- ].join('/')
- end
-
- alias to_s edtf
-
+
+ # call-seq:
+ # interval.first -> Date or nil
+ # interval.first(n) -> Array
+ #
+ # Returns the first date in the interval, or the first n dates.
+ def first(n = 1)
+ if n > 1
+ (ds = Array(min)).empty? ? ds : ds.concat(ds[0].next(n - 1))
+ else
+ min
+ end
+ end
+
+ # call-seq:
+ # interval.last -> Date or nil
+ # interval.last(n) -> Array
+ #
+ # Returns the last date in the interval, or the last n dates.
+ def last(n = 1)
+ if n > 1
+ (ds = Array(max)).empty? ? ds : ds.concat(ds[0].prev(n - 1))
+ else
+ max
+ end
+ end
+
+ # call-seq:
+ # interval.min -> Date or nil
+ # interval.min { |a,b| block } -> Date or nil
+ #
+ # Returns the minimum value in the interval. If a block is given, it is
+ # used to compare values (slower). Returns nil if the first date of the
+ # interval is larger than the last or if the interval has an unknown or
+ # open start.
+ def min
+ if block_given?
+ to_a.min(&Proc.new)
+ else
+ case
+ when unknown_start?, !open? && to < from
+ nil
+ when from.day_precision?
+ from
+ when from.month_precision?
+ from.beginning_of_month
+ else
+ from.beginning_of_year
+ end
+ end
+ end
+
+ def begin
+ min
+ end
+
+ # call-seq:
+ # interval.max -> Date or nil
+ # interval.max { |a,b| block } -> Date or nil
+ #
+ # Returns the maximum value in the interval. If a block is given, it is
+ # used to compare values (slower). Returns nil if the first date of the
+ # interval is larger than the last or if the interval has an unknown or
+ # open end.
+ #
+ # To calculate the dates, precision is taken into account. Thus, the max
+ # Date of "2007/2008" would be 2008-12-31, whilst the max Date of
+ # "2007-12/2008-10" would be 2009-10-31.
+ def max
+ if block_given?
+ to_a.max(&Proc.new)
+ else
+ case
+ when open_end?, unknown_end?, !unknown_start? && to < from
+ nil
+ when to.day_precision?
+ to
+ when to.month_precision?
+ to.end_of_month
+ else
+ to.end_of_year
+ end
+ end
+ end
+
+ def end
+ max
+ end
+
+ def <=>(other)
+ case other
+ when Interval
+ [min, max] <=> [other.min, other.max]
+ when Date
+ cover?(other) ? min <=> other : 0
+ else
+ nil
+ end
+ end
+
+ def ===(other)
+ case other
+ when Interval
+ cover?(other.min) && cover?(other.max)
+ when Date
+ cover?(other)
+ else
+ false
+ end
+ end
+
+
+ # Returns the Interval as an EDTF string.
+ def edtf
+ [
+ from.send(from.respond_to?(:edtf) ? :edtf : :to_s),
+ to.send(to.respond_to?(:edtf) ? :edtf : :to_s)
+ ] * '/'
+ end
+
+ alias to_s edtf
+
end
end
\ No newline at end of file