rubinius/rubinius

View on GitHub
core/range.rb

Summary

Maintainability
D
2 days
Test Coverage
class Range
  include Enumerable

  def initialize(first, last, exclude_end = false)
    raise NameError, "`initialize' called twice" if @begin

    unless first.kind_of?(Fixnum) && last.kind_of?(Fixnum)
      begin
        raise ArgumentError, "bad value for range" unless first <=> last
      rescue
        raise ArgumentError, "bad value for range"
      end
    end

    @begin = first
    @end = last
    @excl = exclude_end
  end
  private :initialize

  def ==(other)
    return true if equal? other

    other.kind_of?(Range) and
      self.first == other.first and
      self.last == other.last and
      self.exclude_end? == other.exclude_end?
  end

  alias_method :eql?, :==

  attr_reader_specific :excl, :exclude_end?

  attr_reader :begin
  attr_reader :end

  def bsearch
    return to_enum :bsearch unless block_given?

    unless @begin.kind_of? Numeric and @end.kind_of? Numeric
      raise TypeError, "bsearch is not available for #{@begin.class}"
    end

    min = @begin
    max = @end

    max -= 1 if max.kind_of? Integer and @excl

    start = min = Rubinius::Type.coerce_to min, Integer, :to_int
    total = max = Rubinius::Type.coerce_to max, Integer, :to_int

    last_true = nil

    seeker = Proc.new do |current|
      x = yield current

      return current if x == 0

      case x
      when Numeric
        if x > 0
          min = current + 1
        else
          max = current
        end
      when true
        last_true = current
        max = current
      when false, nil
        min = current + 1
      else
        raise TypeError, "Range#bsearch block must return Numeric or boolean"
      end
    end

    while min < max
      if max < 0 and min < 0
        mid = min + (max - min) / 2
      elsif min < -max
        mid = -((-1 - min - max) / 2 + 1)
      else
        mid = (min + max) / 2
      end      
      
      seeker.call mid
    end
    
    if min == max
      seeker.call min
    end

    if min < max
      return @begin if mid == start
      return @begin.kind_of?(Float) ? mid.to_f : mid
    end

    if last_true
      return @begin if last_true == start
      return @begin.kind_of?(Float) ? last_true.to_f : last_true
    end

    nil
  end

  def each
    return to_enum { size } unless block_given?
    first, last = @begin, @end

    unless first.respond_to?(:succ) && !first.kind_of?(Time)
      raise TypeError, "can't iterate from #{first.class}"
    end

    case first
    when Fixnum
      last -= 1 if @excl

      i = first
      while i <= last
        yield i
        i += 1
      end
    when String
      first.upto(last, @excl) do |str|
        yield str
      end
    when Symbol
      first.to_s.upto(last.to_s, @excl) do |str|
        yield str.to_sym
      end
    else
      current = first
      if @excl
        while (current <=> last) < 0
          yield current
          current = current.succ
        end
      else
        while (c = current <=> last) && c <= 0
          yield current
          break if c == 0
          current = current.succ
        end
      end
    end

    self
  end

  def first(n=undefined)
    return @begin if undefined.equal? n

    super
  end

  def hash
    excl = @excl ? 1 : 0
    hash = excl
    hash ^= @begin.hash << 1
    hash ^= @end.hash << 9
    hash ^= excl << 24;
    # Are we throwing away too much here for a good hash value distribution?
    return hash & Fixnum::MAX
  end

  def include?(value)
    if @begin.respond_to?(:to_int) ||
       @end.respond_to?(:to_int) ||
       @begin.kind_of?(Numeric) ||
       @end.kind_of?(Numeric)
      cover? value
    else
      super
    end
  end

  alias_method :member?, :include?

  def ===(value)
    include?(value)
  end

  def inspect
    "#{@begin.inspect}#{@excl ? "..." : ".."}#{@end.inspect}"
  end

  def last(n=undefined)
    return @end if undefined.equal? n

    to_a.last(n)
  end

  def max
    return super if block_given? || (@excl && !@end.kind_of?(Numeric))
    return nil if @end < @begin || (@excl && @end == @begin)
    return @end unless @excl

    unless @end.kind_of?(Integer)
      raise TypeError, "cannot exclude non Integer end value"
    end

    unless @begin.kind_of?(Integer)
      raise TypeError, "cannot exclude end value with non Integer begin value"
    end

    @end - 1
  end

  def min
    return super if block_given?
    return nil if @end < @begin || (@excl && @end == @begin)

    @begin
  end

  def step(step_size=1) # :yields: object
    return to_enum(:step, step_size) do
      m = Rubinius::Mirror::Range.reflect(self)
      m.step_iterations_size(*m.validate_step_size(@begin, @end, step_size))
    end unless block_given?

    m = Rubinius::Mirror::Range.reflect(self)
    values = m.validate_step_size(@begin, @end, step_size)
    first = values[0]
    last = values[1]
    step_size = values[2]

    case first
    when Float
      iterations = m.step_float_iterations_size(first, last, step_size)

      i = 0
      while i < iterations
        curr = i * step_size + first
        curr = last if last < curr
        yield curr
        i += 1
      end
    when Numeric
      curr = first
      last -= 1 if @excl

      while curr <= last
        yield curr
        curr += step_size
      end
    else
      i = 0
      each do |curr|
        yield curr if i % step_size == 0
        i += 1
      end
    end

    self
  end

  def to_s
    "#{@begin}#{@excl ? "..." : ".."}#{@end}"
  end

  def to_a
    return super unless @begin.kind_of? Fixnum and @end.kind_of? Fixnum

    fin = @end
    fin += 1 unless @excl

    size = fin - @begin
    return [] if size <= 0

    ary = Array.new(size)

    i = 0
    while i < size
      ary[i] = @begin + i
      i += 1
    end

    ary
  end

  def cover?(value)
    # MRI uses <=> to compare, so must we.

    beg_compare = (@begin <=> value)
    return false unless beg_compare

    if Comparable.compare_int(beg_compare) <= 0
      end_compare = (value <=> @end)

      if @excl
        return true if Comparable.compare_int(end_compare) < 0
      else
        return true if Comparable.compare_int(end_compare) <= 0
      end
    end

    false
  end

  def size
    return nil unless @begin.kind_of?(Numeric)

    delta = @end - @begin
    return 0 if delta < 0

    if @begin.kind_of?(Float) || @end.kind_of?(Float)
      return delta if delta == Float::INFINITY

      err = (@begin.abs + @end.abs + delta.abs) * Float::EPSILON
      err = 0.5 if err > 0.5

      (@excl ? delta - err : delta + err).floor + 1
    else
      delta += 1 unless @excl
      delta
    end
  end
end