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
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
-
#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.
224 225 226 227 |
# File 'lib/philiprehberger/interval/range.rb', line 224 def <=>(other) result = @start <=> other.start result.zero? ? @finish <=> other.finish : result end |
#==(other) ⇒ Boolean
230 231 232 |
# File 'lib/philiprehberger/interval/range.rb', line 230 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.
213 214 215 216 217 218 |
# File 'lib/philiprehberger/interval/range.rb', line 213 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.
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 |
#inspect ⇒ String
242 243 244 |
# File 'lib/philiprehberger/interval/range.rb', line 242 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.
247 248 249 |
# File 'lib/philiprehberger/interval/range.rb', line 247 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.
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.
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.
252 253 254 |
# File 'lib/philiprehberger/interval/range.rb', line 252 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).
199 200 201 202 203 204 205 206 207 |
# File 'lib/philiprehberger/interval/range.rb', line 199 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.
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.
147 148 149 |
# File 'lib/philiprehberger/interval/range.rb', line 147 def shift(delta) self.class.new(@start + delta, @finish + delta, type: @type) end |
#size ⇒ Numeric
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.
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.
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
235 236 237 238 239 |
# File 'lib/philiprehberger/interval/range.rb', line 235 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.
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 |