Class: Philiprehberger::Interval::Range
- Inherits:
-
Object
- Object
- Philiprehberger::Interval::Range
- 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
-
#finish ⇒ Comparable
readonly
The end of the interval.
-
#start ⇒ Comparable
readonly
The start of the interval.
-
#type ⇒ Symbol
readonly
The interval type (:closed, :open, :left_open, :right_open).
Instance Method Summary collapse
-
#<=>(other) ⇒ Integer
Compare intervals by start then finish.
- #==(other) ⇒ Boolean
-
#adjacent?(other) ⇒ Boolean
Check if this interval is adjacent to another (touching but not overlapping).
-
#clamp(value) ⇒ Comparable
Clamp a value to the interval bounds.
-
#contains?(other) ⇒ Boolean
Check if this interval fully contains another.
-
#include?(point) ⇒ Boolean
Check if a point is within the interval.
-
#initialize(start, finish, type: :closed) ⇒ Range
constructor
Create a new interval.
- #inspect ⇒ String
-
#intersect(other) ⇒ Range?
Return the intersection of two overlapping intervals.
-
#left_closed? ⇒ Boolean
True if the left endpoint is included.
-
#overlap_ratio(other) ⇒ Float
Return the fraction of self that is covered by another interval.
-
#overlaps?(other) ⇒ Boolean
Check if this interval overlaps with another.
-
#right_closed? ⇒ Boolean
True if the right endpoint is included.
-
#sample(n = nil) ⇒ Float+
Return one or more random Floats within the interval.
-
#scale(factor, anchor: :center) ⇒ Range
Scale interval width around an anchor point, preserving type.
-
#shift(delta) ⇒ Range
Return a new interval shifted by delta, preserving type.
-
#size ⇒ Numeric
(also: #length)
Return the size (length) of the interval.
-
#split(n) ⇒ Array<Range>
Split interval into n equal sub-intervals, preserving type.
-
#subtract(other) ⇒ Array<Range>
Subtract another interval from this one.
- #to_s ⇒ String
-
#touching?(other) ⇒ Boolean
Check if this interval strictly touches another at a single endpoint.
-
#union(other) ⇒ Range?
Return the union of two overlapping or adjacent intervals.
Constructor Details
#initialize(start, finish, type: :closed) ⇒ Range
Create a new interval.
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
#finish ⇒ Comparable (readonly)
Returns the end of the interval.
16 17 18 |
# File 'lib/philiprehberger/interval/range.rb', line 16 def finish @finish end |
#start ⇒ Comparable (readonly)
Returns the start of the interval.
13 14 15 |
# File 'lib/philiprehberger/interval/range.rb', line 13 def start @start end |
#type ⇒ Symbol (readonly)
Returns 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.
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
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).
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.
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.
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.
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 |
#inspect ⇒ 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.
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.
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.
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.
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.
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).
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.
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.
168 169 170 |
# File 'lib/philiprehberger/interval/range.rb', line 168 def shift(delta) self.class.new(@start + delta, @finish + delta, type: @type) end |
#size ⇒ Numeric Also known as: length
Return the size (length) of the interval.
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.
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.
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_s ⇒ 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.
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.
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 |