philiprehberger-interval 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecb8d7c68d3603d44c8df9a87a224c52300377809ad98ab1d19fe1a89ec13449
4
- data.tar.gz: 75b3438103596f8a4de7f358a9f1cc25cd39fe5e80a47dfb6f25d38852e26964
3
+ metadata.gz: 8aaad786d674299bcc97c96acb85df9aad7bcd91b3794404a6aad674946c8461
4
+ data.tar.gz: 1ec309715dd29b1f132481150d32a1c00d95413d3c0ad3aca1d6ea79420daa0e
5
5
  SHA512:
6
- metadata.gz: 78198caa2a0cf621b3545997e8fdfb58ad7424d7304e7d4367873dd69eaa02db2c91f1ed5a95cd365074be886063fb91941dea72180b2ce4e099c6e1982fddcf
7
- data.tar.gz: 2dbb700fce8cd9cf5d973ab0baabece8e8b94e8d79f71939a805418f7fdfdbf626ca183ae9ab0fc18c2c036f21a2c0de9ddd28d90e1978da02139ac8eb3d1178
6
+ metadata.gz: 34ff8595dd0ede63f6cc187e06ba0de8a51fb80d15d777845c72512f8721402e225e54dbefc56089eb44610549e6de4307e03c2503a133cd9844e8446c5dbe80
7
+ data.tar.gz: 27c3dc2e5c9215554f995de13fbed3090311f0f09e337ea1b8d100d68e526731de1d089b141dbcb2c938421b1a8a5dd9ada07d95d3d9b123aaa72a0ab1b70f6e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-04-21
11
+
12
+ ### Added
13
+ - `Range#touching?` — strict boundary-touching predicate
14
+ - `Range#length` — alias for `#size`
15
+
16
+ ### Fixed
17
+ - `bug_report.yml` — require Ruby version; add Gem version input per guide
18
+
19
+ ## [0.5.0] - 2026-04-16
20
+
21
+ ### Added
22
+ - `Range#sample(n = nil)` — return a random Float within the interval, or an array of n random Floats; respects open/closed boundaries via rejection sampling; raises on empty intervals; returns start for point intervals
23
+
10
24
  ## [0.4.0] - 2026-04-15
11
25
 
12
26
  ### Added
data/README.md CHANGED
@@ -84,6 +84,19 @@ Philiprehberger::Interval.gaps(intervals)
84
84
  # => [[7, 10]]
85
85
  ```
86
86
 
87
+ ### Sampling Random Values
88
+
89
+ Return one or more random Floats from within an interval. Open boundaries are respected via rejection sampling:
90
+
91
+ ```ruby
92
+ interval = Philiprehberger::Interval.new(1.0, 5.0)
93
+ interval.sample # => 3.247... (single Float)
94
+ interval.sample(3) # => [1.823..., 4.501..., 2.964...]
95
+
96
+ open = Philiprehberger::Interval.new(1.0, 5.0, type: :open)
97
+ open.sample # => always strictly between 1.0 and 5.0
98
+ ```
99
+
87
100
  ### Finding a Common Intersection
88
101
 
89
102
  Compute the overlap shared by every interval, or `nil` if any pair is disjoint:
@@ -97,6 +110,20 @@ Philiprehberger::Interval.intersection([
97
110
  # => [5, 8]
98
111
  ```
99
112
 
113
+ ### Touching Intervals
114
+
115
+ Check whether two intervals meet at exactly one point with no overlap and no gap — useful when partitioning a range so every value is covered exactly once:
116
+
117
+ ```ruby
118
+ a = Philiprehberger::Interval.new(1, 5, type: :right_open) # [1, 5)
119
+ b = Philiprehberger::Interval.new(5, 10, type: :closed) # [5, 10]
120
+ a.touching?(b) # => true (5 covered once)
121
+
122
+ c = Philiprehberger::Interval.new(1, 5, type: :closed) # [1, 5]
123
+ d = Philiprehberger::Interval.new(5, 10, type: :closed) # [5, 10]
124
+ c.touching?(d) # => false (5 covered twice)
125
+ ```
126
+
100
127
  ### With Time Values
101
128
 
102
129
  ```ruby
@@ -113,16 +140,19 @@ shift.include?(Time.new(2026, 1, 1, 12)) # => true
113
140
  | `#overlaps?(other)` | Check if two intervals overlap (respects boundary types) |
114
141
  | `#contains?(other)` | Check if interval fully contains another |
115
142
  | `#adjacent?(other)` | Check if intervals are touching but not overlapping |
143
+ | `#touching?(other)` | Check if intervals meet at a single point with exactly one closed side |
116
144
  | `#intersect(other)` | Return the overlap between two intervals |
117
145
  | `#overlap_ratio(other)` | Fraction of self covered by other (0.0–1.0) |
118
146
  | `#union(other)` | Return the combined interval |
119
147
  | `#subtract(other)` | Remove another interval, returning remaining parts |
120
148
  | `#size` | Length of the interval |
149
+ | `#length` | Alias for `#size` |
121
150
  | `#include?(point)` | Check if a point falls within the interval (respects boundary types) |
122
151
  | `#shift(delta)` | Return new interval shifted by delta, preserving type |
123
152
  | `#scale(factor, anchor:)` | Scale width around anchor (`:center`, `:left`, `:right`) |
124
153
  | `#split(n)` | Split into n equal sub-intervals |
125
154
  | `#clamp(value)` | Clamp value to interval bounds |
155
+ | `#sample(n = nil)` | Return a random Float, or array of n Floats, within the interval (respects boundaries) |
126
156
  | `.merge(intervals)` | Merge overlapping intervals into non-overlapping set |
127
157
  | `.gaps(intervals)` | Find gaps between a set of intervals |
128
158
  | `.intersection(intervals)` | Common overlap of a collection of intervals, or `nil` |
@@ -112,6 +112,27 @@ module Philiprehberger
112
112
  def size
113
113
  @finish - @start
114
114
  end
115
+ alias length size
116
+
117
+ # Check if this interval strictly touches another at a single endpoint.
118
+ #
119
+ # Returns +true+ iff the two intervals share exactly one endpoint value
120
+ # and exactly one of the two boundaries at that point is closed — so the
121
+ # meeting point is covered exactly once, with no gap and no overlap.
122
+ #
123
+ # @param other [Range] the other interval
124
+ # @return [Boolean]
125
+ def touching?(other)
126
+ return false if overlaps?(other)
127
+
128
+ if @finish == other.start
129
+ right_closed? ^ other.left_closed?
130
+ elsif other.finish == @start
131
+ other.right_closed? ^ left_closed?
132
+ else
133
+ false
134
+ end
135
+ end
115
136
 
116
137
  # Return the fraction of self that is covered by another interval.
117
138
  #
@@ -186,6 +207,26 @@ module Philiprehberger
186
207
  end
187
208
  end
188
209
 
210
+ # Return one or more random Floats within the interval.
211
+ #
212
+ # Without an argument, returns a single random Float. With an integer
213
+ # argument, returns an array of that many random Floats. Respects boundary
214
+ # types: open boundaries are excluded via rejection sampling. Returns
215
+ # +start+ immediately for point intervals (start == finish, closed).
216
+ #
217
+ # @param n [Integer, nil] number of samples, or nil for a single value
218
+ # @return [Float, Array<Float>]
219
+ # @raise [Error] if the interval is empty
220
+ def sample(n = nil)
221
+ raise Error, 'cannot sample an empty interval' if empty?
222
+
223
+ if n.nil?
224
+ sample_one
225
+ else
226
+ Array.new(n) { sample_one }
227
+ end
228
+ end
229
+
189
230
  # Clamp a value to the interval bounds.
190
231
  #
191
232
  # @param value [Comparable] the value to clamp
@@ -239,6 +280,22 @@ module Philiprehberger
239
280
  def empty?
240
281
  @start == @finish && @type != :closed
241
282
  end
283
+
284
+ private
285
+
286
+ def sample_one
287
+ return @start.to_f if @start == @finish
288
+
289
+ lo = @start.to_f
290
+ hi = @finish.to_f
291
+
292
+ loop do
293
+ value = lo + (rand * (hi - lo))
294
+ left_ok = left_closed? ? value >= lo : value > lo
295
+ right_ok = right_closed? ? value <= hi : value < hi
296
+ return value if left_ok && right_ok
297
+ end
298
+ end
242
299
  end
243
300
  end
244
301
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Interval
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-interval
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-15 00:00:00.000000000 Z
11
+ date: 2026-04-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Interval data type supporting closed, open, and half-open boundaries
14
14
  with overlap detection, containment, intersection, union, subtraction, shift, scale,