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)


245
246
247
248
# File 'lib/philiprehberger/interval/range.rb', line 245

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

#==(other) ⇒ Boolean

Returns:

  • (Boolean)


251
252
253
# File 'lib/philiprehberger/interval/range.rb', line 251

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



234
235
236
237
238
239
# File 'lib/philiprehberger/interval/range.rb', line 234

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)


158
159
160
161
162
# File 'lib/philiprehberger/interval/range.rb', line 158

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

#inspectString

Returns:

  • (String)


263
264
265
# File 'lib/philiprehberger/interval/range.rb', line 263

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



268
269
270
# File 'lib/philiprehberger/interval/range.rb', line 268

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



145
146
147
148
149
150
151
152
# File 'lib/philiprehberger/interval/range.rb', line 145

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



273
274
275
# File 'lib/philiprehberger/interval/range.rb', line 273

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

#sample(n = nil) ⇒ Float+

Return one or more random Floats within the interval.

Without an argument, returns a single random Float. With an integer argument, returns an array of that many random Floats. Respects boundary types: open boundaries are excluded via rejection sampling. Returns start immediately for point intervals (start == finish, closed).

Parameters:

  • n (Integer, nil) (defaults to: nil)

    number of samples, or nil for a single value

Returns:

  • (Float, Array<Float>)

Raises:

  • (Error)

    if the interval is empty



220
221
222
223
224
225
226
227
228
# File 'lib/philiprehberger/interval/range.rb', line 220

def sample(n = nil)
  raise Error, 'cannot sample an empty interval' if empty?

  if n.nil?
    sample_one
  else
    Array.new(n) { sample_one }
  end
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



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/philiprehberger/interval/range.rb', line 177

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



168
169
170
# File 'lib/philiprehberger/interval/range.rb', line 168

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

#sizeNumeric Also known as: length

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:



199
200
201
202
203
204
205
206
207
208
# File 'lib/philiprehberger/interval/range.rb', line 199

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)


256
257
258
259
260
# File 'lib/philiprehberger/interval/range.rb', line 256

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

#touching?(other) ⇒ Boolean

Check if this interval strictly touches another at a single endpoint.

Returns true iff the two intervals share exactly one endpoint value and exactly one of the two boundaries at that point is closed — so the meeting point is covered exactly once, with no gap and no overlap.

Parameters:

  • other (Range)

    the other interval

Returns:

  • (Boolean)


125
126
127
128
129
130
131
132
133
134
135
# File 'lib/philiprehberger/interval/range.rb', line 125

def touching?(other)
  return false if overlaps?(other)

  if @finish == other.start
    right_closed? ^ other.left_closed?
  elsif other.finish == @start
    other.right_closed? ^ left_closed?
  else
    false
  end
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