Class: Philiprehberger::Interval::Range

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/philiprehberger/interval/range.rb

Overview

Represents an interval over any Comparable type. Supports closed [a, b], open (a, b), left-open (a, b], and right-open [a, b) boundaries.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(start, finish, type: :closed) ⇒ Range

Create a new interval.

Parameters:

  • start (Comparable)

    the start value

  • finish (Comparable)

    the end value

  • type (Symbol) (defaults to: :closed)

    boundary type (:closed, :open, :left_open, :right_open)

Raises:

  • (Error)

    if start > finish

  • (Error)

    if type is invalid



28
29
30
31
32
33
34
35
# File 'lib/philiprehberger/interval/range.rb', line 28

def initialize(start, finish, type: :closed)
  raise Error, 'start must be <= finish' if start > finish
  raise Error, "invalid interval type: #{type}" unless VALID_TYPES.include?(type)

  @start = start
  @finish = finish
  @type = type
end

Instance Attribute Details

#finishComparable (readonly)

Returns the end of the interval.

Returns:

  • (Comparable)

    the end of the interval



16
17
18
# File 'lib/philiprehberger/interval/range.rb', line 16

def finish
  @finish
end

#startComparable (readonly)

Returns the start of the interval.

Returns:

  • (Comparable)

    the start of the interval



13
14
15
# File 'lib/philiprehberger/interval/range.rb', line 13

def start
  @start
end

#typeSymbol (readonly)

Returns the interval type (:closed, :open, :left_open, :right_open).

Returns:

  • (Symbol)

    the interval type (:closed, :open, :left_open, :right_open)



19
20
21
# File 'lib/philiprehberger/interval/range.rb', line 19

def type
  @type
end

Instance Method Details

#<=>(other) ⇒ Integer

Compare intervals by start then finish.

Parameters:

Returns:

  • (Integer)


204
205
206
207
# File 'lib/philiprehberger/interval/range.rb', line 204

def <=>(other)
  result = @start <=> other.start
  result.zero? ? @finish <=> other.finish : result
end

#==(other) ⇒ Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/philiprehberger/interval/range.rb', line 210

def ==(other)
  other.is_a?(self.class) && @start == other.start && @finish == other.finish && @type == other.type
end

#adjacent?(other) ⇒ Boolean

Check if this interval is adjacent to another (touching but not overlapping).

Parameters:

  • other (Range)

    the other interval

Returns:

  • (Boolean)


71
72
73
# File 'lib/philiprehberger/interval/range.rb', line 71

def adjacent?(other)
  @finish == other.start || other.finish == @start
end

#clamp(value) ⇒ Comparable

Clamp a value to the interval bounds.

Parameters:

  • value (Comparable)

    the value to clamp

Returns:

  • (Comparable)

    the clamped value



193
194
195
196
197
198
# File 'lib/philiprehberger/interval/range.rb', line 193

def clamp(value)
  return @start if value < @start
  return @finish if value > @finish

  value
end

#contains?(other) ⇒ Boolean

Check if this interval fully contains another.

Parameters:

  • other (Range)

    the other interval

Returns:

  • (Boolean)


63
64
65
# File 'lib/philiprehberger/interval/range.rb', line 63

def contains?(other)
  @start <= other.start && @finish >= other.finish
end

#include?(point) ⇒ Boolean

Check if a point is within the interval.

Parameters:

  • point (Comparable)

    the point to check

Returns:

  • (Boolean)


137
138
139
140
141
# File 'lib/philiprehberger/interval/range.rb', line 137

def include?(point)
  left = left_closed? ? point >= @start : point > @start
  right = right_closed? ? point <= @finish : point < @finish
  left && right
end

#inspectString

Returns:

  • (String)


222
223
224
# File 'lib/philiprehberger/interval/range.rb', line 222

def inspect
  "#<#{self.class} #{self}>"
end

#intersect(other) ⇒ Range?

Return the intersection of two overlapping intervals.

Parameters:

  • other (Range)

    the other interval

Returns:

  • (Range, nil)

    the intersection, or nil if no overlap



79
80
81
82
83
# File 'lib/philiprehberger/interval/range.rb', line 79

def intersect(other)
  return nil unless overlaps?(other)

  self.class.new([@start, other.start].max, [@finish, other.finish].min)
end

#left_closed?Boolean

Returns true if the left endpoint is included.

Returns:

  • (Boolean)

    true if the left endpoint is included



227
228
229
# File 'lib/philiprehberger/interval/range.rb', line 227

def left_closed?
  @type == :closed || @type == :right_open
end

#overlap_ratio(other) ⇒ Float

Return the fraction of self that is covered by another interval.

Returns a Float in 0.0..1.0. Returns 0.0 if disjoint, 1.0 if self is fully contained in other. If self has zero length (a point), returns 1.0 when the point is within other, else 0.0.

Parameters:

  • other (Range)

    the other interval

Returns:

  • (Float)

    the fraction of self covered by other



124
125
126
127
128
129
130
131
# File 'lib/philiprehberger/interval/range.rb', line 124

def overlap_ratio(other)
  return other.include?(@start) ? 1.0 : 0.0 if size.zero?

  overlap = intersect(other)
  return 0.0 if overlap.nil?

  overlap.size.to_f / size
end

#overlaps?(other) ⇒ Boolean

Check if this interval overlaps with another.

Parameters:

  • other (Range)

    the other interval

Returns:

  • (Boolean)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/philiprehberger/interval/range.rb', line 41

def overlaps?(other)
  return false if empty? || other.empty?

  left_ok = if right_closed? && other.left_closed?
              @finish >= other.start
            else
              @finish > other.start
            end

  right_ok = if other.right_closed? && left_closed?
               other.finish >= @start
             else
               other.finish > @start
             end

  left_ok && right_ok
end

#right_closed?Boolean

Returns true if the right endpoint is included.

Returns:

  • (Boolean)

    true if the right endpoint is included



232
233
234
# File 'lib/philiprehberger/interval/range.rb', line 232

def right_closed?
  @type == :closed || @type == :left_open
end

#scale(factor, anchor: :center) ⇒ Range

Scale interval width around an anchor point, preserving type.

Parameters:

  • factor (Numeric)

    the scale factor

  • anchor (Symbol) (defaults to: :center)

    the anchor point (:center, :left, :right)

Returns:

  • (Range)

    a new scaled interval



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/philiprehberger/interval/range.rb', line 156

def scale(factor, anchor: :center)
  current_size = size
  new_size = current_size * factor

  case anchor
  when :left
    self.class.new(@start, @start + new_size, type: @type)
  when :right
    self.class.new(@finish - new_size, @finish, type: @type)
  when :center
    center = @start + (current_size / 2.0)
    half = new_size / 2.0
    self.class.new(center - half, center + half, type: @type)
  else
    raise Error, "invalid anchor: #{anchor}"
  end
end

#shift(delta) ⇒ Range

Return a new interval shifted by delta, preserving type.

Parameters:

  • delta (Numeric)

    the amount to shift

Returns:

  • (Range)

    a new shifted interval



147
148
149
# File 'lib/philiprehberger/interval/range.rb', line 147

def shift(delta)
  self.class.new(@start + delta, @finish + delta, type: @type)
end

#sizeNumeric

Return the size (length) of the interval.

Returns:

  • (Numeric)

    the difference between finish and start



112
113
114
# File 'lib/philiprehberger/interval/range.rb', line 112

def size
  @finish - @start
end

#split(n) ⇒ Array<Range>

Split interval into n equal sub-intervals, preserving type.

Parameters:

  • n (Integer)

    number of sub-intervals

Returns:

  • (Array<Range>)

    array of n equal sub-intervals

Raises:



178
179
180
181
182
183
184
185
186
187
# File 'lib/philiprehberger/interval/range.rb', line 178

def split(n)
  raise Error, 'n must be positive' if n < 1

  step = size.to_f / n
  Array.new(n) do |i|
    sub_start = @start + (step * i)
    sub_finish = @start + (step * (i + 1))
    self.class.new(sub_start, sub_finish, type: @type)
  end
end

#subtract(other) ⇒ Array<Range>

Subtract another interval from this one.

Parameters:

  • other (Range)

    the interval to subtract

Returns:

  • (Array<Range>)

    zero, one, or two remaining intervals



99
100
101
102
103
104
105
106
107
# File 'lib/philiprehberger/interval/range.rb', line 99

def subtract(other)
  return [self.class.new(@start, @finish)] unless overlaps?(other)
  return [] if other.contains?(self)

  result = []
  result << self.class.new(@start, other.start) if @start < other.start
  result << self.class.new(other.finish, @finish) if other.finish < @finish
  result
end

#to_sString

Returns:

  • (String)


215
216
217
218
219
# File 'lib/philiprehberger/interval/range.rb', line 215

def to_s
  left_bracket = left_closed? ? '[' : '('
  right_bracket = right_closed? ? ']' : ')'
  "#{left_bracket}#{@start}, #{@finish}#{right_bracket}"
end

#union(other) ⇒ Range?

Return the union of two overlapping or adjacent intervals.

Parameters:

  • other (Range)

    the other interval

Returns:

  • (Range, nil)

    the union, or nil if not overlapping/adjacent



89
90
91
92
93
# File 'lib/philiprehberger/interval/range.rb', line 89

def union(other)
  return nil unless overlaps?(other) || adjacent?(other)

  self.class.new([@start, other.start].min, [@finish, other.finish].max)
end