opal/corelib/range.rb

Summary

Maintainability
D
1 day
Test Coverage
# backtick_javascript: true
# use_strict: true

require 'corelib/enumerable'

class ::Range
  include ::Enumerable

  `self.$$prototype.$$is_range = true`

  attr_reader :begin, :end

  def initialize(first, last, exclude = false)
    ::Kernel.raise ::NameError, "'initialize' called twice" if @begin
    ::Kernel.raise ::ArgumentError, 'bad value for range' unless first <=> last || first.nil? || last.nil?

    @begin = first
    @end   = last
    @excl  = exclude
  end

  def ===(value)
    return false if `value.$$is_range`
    cover? value
  end

  %x{
    function is_infinite(self) {
      if (self.begin === nil || self.end === nil ||
          self.begin === -Infinity || self.end === Infinity ||
          self.begin === Infinity || self.end === -Infinity) return true;
      return false;
    }
  }

  def count(&block)
    if !block_given? && `is_infinite(self)`
      return ::Float::INFINITY
    end
    super
  end

  def to_a
    ::Kernel.raise ::TypeError, 'cannot convert endless range to an array' if `is_infinite(self)`
    super
  end

  def cover?(value)
    compare = ->(a, b) {
      a <=> b || 1
    }

    if `value.$$is_range`
      val_begin = value.begin
      val_end = value.end
      val_excl = value.exclude_end?
      if (@begin && val_begin.nil?) ||
         (@end && val_end.nil?) ||
         (val_begin && val_end && compare.call(val_begin, val_end).then { |c| val_excl ? c >= 0 : c > 0 }) ||
         (val_begin && !cover?(val_begin))
        return false
      end

      cmp = compare.call(@end, val_end)
      return cmp >= 0 if @excl == val_excl
      return cmp > 0 if @excl
      return true if cmp >= 0

      val_max = value.max
      return !val_max.nil? && compare.call(val_max, @end) <= 0
    end

    return false if @begin && compare.call(@begin, value) > 0
    return true if @end.nil?
    end_cmp = compare.call(value, @end)
    @excl ? end_cmp < 0 : end_cmp <= 0
  end

  def each(&block)
    return enum_for(:each) { size } unless block_given?

    %x{
      var i, limit;

      if (#{@begin}.$$is_number && #{@end}.$$is_number) {
        if (#{@begin} % 1 !== 0 || #{@end} % 1 !== 0) {
          #{::Kernel.raise ::TypeError, "can't iterate from Float"}
        }

        for (i = #{@begin}, limit = #{@end} + #{@excl ? 0 : 1}; i < limit; i++) {
          block(i);
        }

        return self;
      }

      if (#{@begin}.$$is_string && #{@end}.$$is_string) {
        #{@begin.upto(@end, @excl, &block)}
        return self;
      }
    }

    current = @begin
    last    = @end

    unless current.respond_to?(:succ)
      ::Kernel.raise ::TypeError, "can't iterate from #{current.class}"
    end

    while @end.nil? || (current <=> last) < 0
      yield current

      current = current.succ
    end

    yield current if !@excl && current == last

    self
  end

  def eql?(other)
    return false unless ::Range === other

    @excl === other.exclude_end? &&
      @begin.eql?(other.begin) &&
      @end.eql?(other.end)
  end

  def exclude_end?
    @excl
  end

  def first(n = undefined)
    ::Kernel.raise ::RangeError, 'cannot get the minimum of beginless range' if @begin.nil?
    return @begin if `n == null`
    super
  end

  def include?(val)
    if `self.begin.$$is_number || self.end.$$is_number` ||
       @begin.is_a?(::Time) || @end.is_a?(::Time) ||
       ::Integer.try_convert(@begin) || ::Integer.try_convert(@end)
      return cover?(val)
    end

    if `self.begin.$$is_string || self.end.$$is_string`
      if `self.begin.$$is_string && self.end.$$is_string`
        return @begin.upto(@end, @excl).any? { |s| s == val }
      elsif @begin.nil?
        cmp = val <=> @end
        return !cmp.nil? && (@excl ? cmp < 0 : cmp <= 0)
      elsif @end.nil?
        cmp = @begin <=> val
        return !cmp.nil? && cmp <= 0
      end
    end

    # invoke Enumerable#include?
    super
  end

  def last(n = undefined)
    ::Kernel.raise ::RangeError, 'cannot get the maximum of endless range' if @end.nil?
    return @end if `n == null`
    to_a.last(n)
  end

  # FIXME: currently hardcoded to assume range holds numerics
  def max
    if @end.nil?
      ::Kernel.raise ::RangeError, 'cannot get the maximum of endless range'
    elsif block_given?
      super
    elsif !@begin.nil? && (@begin > @end ||
                           @excl && @begin == @end)
      nil
    else
      `#{@excl} ? #{@end} - 1 : #{@end}`
    end
  end

  def min
    if @begin.nil?
      ::Kernel.raise ::RangeError, 'cannot get the minimum of beginless range'
    elsif block_given?
      super
    elsif !@end.nil? && (@begin > @end ||
                         @excl && @begin == @end)
      nil
    else
      @begin
    end
  end

  def size
    %x{
      var b = this.begin, e = this.end;

      // If begin is Numeric
      if (#{::Numeric === `b`}) {
        // If end is Numeric
        if (#{::Numeric === `e`}) {
          // Calculating size based on whether range is exclusive or inclusive
          var size = #{`e` - `b`};
          if (size < 0) {
            return 0;
          }
          if (!this.excl) {
            size += 1;
          }
          return (#{::Float === `b`} || #{::Float === `e`}) ? Math.floor(size) : size;
        }
        // If end is nil
        else if (e === nil) {
          return Infinity;
        }
      }
      // If begin is nil
      else if (b === nil) {
        // If end is Numeric
        if (#{::Numeric === `e`}) {
          return Infinity;
        }
      }

      // If neither begin nor end is Numeric
      return nil;
    }
  end

  def step(n = undefined)
    %x{
      function coerceStepSize() {
        if (n == null) {
          n = 1;
        }
        else if (!n.$$is_number) {
          n = #{::Opal.coerce_to!(n, ::Integer, :to_int)}
        }

        if (n < 0) {
          #{::Kernel.raise ::ArgumentError, "step can't be negative"}
        } else if (n === 0) {
          #{::Kernel.raise ::ArgumentError, "step can't be 0"}
        }
      }

      function enumeratorSize() {
        if (!#{@begin.respond_to?(:succ)}) {
          return nil;
        }

        if (#{@begin}.$$is_string && #{@end}.$$is_string) {
          return nil;
        }

        if (n % 1 === 0) {
          return #{(size / n).ceil};
        } else {
          // n is a float
          var begin = self.begin, end = self.end,
              abs = Math.abs, floor = Math.floor,
              err = (abs(begin) + abs(end) + abs(end - begin)) / abs(n) * #{::Float::EPSILON},
              size;

          if (err > 0.5) {
            err = 0.5;
          }

          if (self.excl) {
            size = floor((end - begin) / n - err);
            if (size * n + begin < end) {
              size++;
            }
          } else {
            size = floor((end - begin) / n + err) + 1
          }

          return size;
        }
      }
    }

    unless block_given?
      if (@begin.is_a?(Numeric) || @begin.nil?) &&
         (@end.is_a?(Numeric) || @end.nil?) &&
         !(@begin.nil? && @end.nil?)

        return ::Enumerator::ArithmeticSequence.new(self, n, :step)
      else
        return enum_for(:step, n) do
          %x{
            coerceStepSize();
            return enumeratorSize();
          }
        end
      end
    end

    `coerceStepSize()`

    if `self.begin.$$is_number && self.end.$$is_number`
      i = 0
      loop do
        current = @begin + i * n
        if @excl
          break if current >= @end
        elsif current > @end
          break
        end
        yield(current)
        i += 1
      end
    else
      %x{
        if (#{@begin}.$$is_string && #{@end}.$$is_string && n % 1 !== 0) {
          #{::Kernel.raise ::TypeError, 'no implicit conversion to float from string'}
        }
      }
      each_with_index do |value, idx|
        yield(value) if idx % n == 0
      end
    end
    self
  end

  def %(n)
    if @begin.is_a?(Numeric) && @end.is_a?(Numeric)
      ::Enumerator::ArithmeticSequence.new(self, n, :%)
    else
      step(n)
    end
  end

  def bsearch(&block)
    return enum_for(:bsearch) unless block_given?

    if `is_infinite(self) && (self.begin.$$is_number || self.end.$$is_number)`
      ::Kernel.raise ::NotImplementedError, "Can't #bsearch an infinite range"
    end

    unless `self.begin.$$is_number && self.end.$$is_number`
      ::Kernel.raise ::TypeError, "can't do binary search for #{@begin.class}"
    end

    to_a.bsearch(&block)
  end

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

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

  def marshal_load(args)
    @begin = args[:begin]
    @end = args[:end]
    @excl = args[:excl]
  end

  def hash
    [::Range, @begin, @end, @excl].hash
  end

  alias == eql?
  alias member? include?
end