philiprehberger-interval 0.5.0 → 0.7.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: 2cc42dea01af0c3d94ccd1473f57540edadacd0da1763cfb29cfcceaca105900
4
- data.tar.gz: 0455405d87eda7c2a20b913bcd9d26477247dfd69a99adc2f00be29c340aaf5e
3
+ metadata.gz: 9c177128b92f238098513a447db1fd1ea888fad019048acb053e334621b9299a
4
+ data.tar.gz: ace5a8129bd130ec72d08412cf014835ee7ea8877627caf3fbce6539455b3be6
5
5
  SHA512:
6
- metadata.gz: fd54e350ee506ed02827ad6c23f4eec3d492eadc9b68cee0d4a4608773e1611a799ceb4ca2e56409ba0e27145852fe5c6d8adb168875f93c3818f25e82ef4c55
7
- data.tar.gz: 626544c2d371d2c1de5a1a1b4f4ac7f181af2b8397ae85168e617b7d77a8445f0f3f6d7e66a01fc81586bce8f144692410d8c0a677d86cffbf44b323bfc4498a
6
+ metadata.gz: dcfc3793178ca911054202f5c6feeea44d952751c47a33cd3e5b9939fe26fd674d20fb2b4f62b687ae6b510e55ad9db64a0151aa285ffdd65205907fa79bf4f9
7
+ data.tar.gz: 05fd282975ef4dd41fdf9bc16990130395ba55aba456d9bf94dfef2df56fcf1d50e4587b8c0f5dcd761325eab9cf7bf0b6fef3d62179f62b29953fed7f01d1ae
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.7.0] - 2026-05-01
11
+
12
+ ### Added
13
+ - `Interval.span(intervals)` — return the smallest closed interval containing every input interval; complements `merge` (multiple intervals on gaps) and `intersection` (common overlap)
14
+
15
+ ## [0.6.0] - 2026-04-21
16
+
17
+ ### Added
18
+ - `Range#touching?` — strict boundary-touching predicate
19
+ - `Range#length` — alias for `#size`
20
+
21
+ ### Fixed
22
+ - `bug_report.yml` — require Ruby version; add Gem version input per guide
23
+
10
24
  ## [0.5.0] - 2026-04-16
11
25
 
12
26
  ### Added
data/README.md CHANGED
@@ -110,6 +110,33 @@ Philiprehberger::Interval.intersection([
110
110
  # => [5, 8]
111
111
  ```
112
112
 
113
+ ### Spanning Interval
114
+
115
+ Compute the smallest closed interval that contains every input — useful for charting and bounding-box queries. Spans across gaps too:
116
+
117
+ ```ruby
118
+ Philiprehberger::Interval.span([
119
+ Philiprehberger::Interval.new(3, 7),
120
+ Philiprehberger::Interval.new(1, 4),
121
+ Philiprehberger::Interval.new(8, 10)
122
+ ])
123
+ # => [1, 10]
124
+ ```
125
+
126
+ ### Touching Intervals
127
+
128
+ 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:
129
+
130
+ ```ruby
131
+ a = Philiprehberger::Interval.new(1, 5, type: :right_open) # [1, 5)
132
+ b = Philiprehberger::Interval.new(5, 10, type: :closed) # [5, 10]
133
+ a.touching?(b) # => true (5 covered once)
134
+
135
+ c = Philiprehberger::Interval.new(1, 5, type: :closed) # [1, 5]
136
+ d = Philiprehberger::Interval.new(5, 10, type: :closed) # [5, 10]
137
+ c.touching?(d) # => false (5 covered twice)
138
+ ```
139
+
113
140
  ### With Time Values
114
141
 
115
142
  ```ruby
@@ -126,11 +153,13 @@ shift.include?(Time.new(2026, 1, 1, 12)) # => true
126
153
  | `#overlaps?(other)` | Check if two intervals overlap (respects boundary types) |
127
154
  | `#contains?(other)` | Check if interval fully contains another |
128
155
  | `#adjacent?(other)` | Check if intervals are touching but not overlapping |
156
+ | `#touching?(other)` | Check if intervals meet at a single point with exactly one closed side |
129
157
  | `#intersect(other)` | Return the overlap between two intervals |
130
158
  | `#overlap_ratio(other)` | Fraction of self covered by other (0.0–1.0) |
131
159
  | `#union(other)` | Return the combined interval |
132
160
  | `#subtract(other)` | Remove another interval, returning remaining parts |
133
161
  | `#size` | Length of the interval |
162
+ | `#length` | Alias for `#size` |
134
163
  | `#include?(point)` | Check if a point falls within the interval (respects boundary types) |
135
164
  | `#shift(delta)` | Return new interval shifted by delta, preserving type |
136
165
  | `#scale(factor, anchor:)` | Scale width around anchor (`:center`, `:left`, `:right`) |
@@ -140,6 +169,7 @@ shift.include?(Time.new(2026, 1, 1, 12)) # => true
140
169
  | `.merge(intervals)` | Merge overlapping intervals into non-overlapping set |
141
170
  | `.gaps(intervals)` | Find gaps between a set of intervals |
142
171
  | `.intersection(intervals)` | Common overlap of a collection of intervals, or `nil` |
172
+ | `.span(intervals)` | Smallest closed interval enclosing all inputs (or `nil` if empty) |
143
173
 
144
174
  ## Development
145
175
 
@@ -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
  #
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Interval
5
- VERSION = '0.5.0'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  end
@@ -67,5 +67,22 @@ module Philiprehberger
67
67
  end
68
68
  result
69
69
  end
70
+
71
+ # The smallest closed interval that contains every input interval.
72
+ #
73
+ # The result spans from the minimum start to the maximum finish across
74
+ # all inputs and is always returned as a closed interval. Distinct from
75
+ # {.merge} (which yields multiple intervals when there are gaps) and
76
+ # {.intersection} (which yields the common overlap or nil).
77
+ #
78
+ # @param intervals [Array<Range>] the intervals to span
79
+ # @return [Range, nil] the enclosing interval, or `nil` if the input is empty
80
+ def self.span(intervals)
81
+ return nil if intervals.empty?
82
+
83
+ min_start = intervals.map(&:start).min
84
+ max_finish = intervals.map(&:finish).max
85
+ Range.new(min_start, max_finish)
86
+ end
70
87
  end
71
88
  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.5.0
4
+ version: 0.7.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-17 00:00:00.000000000 Z
11
+ date: 2026-05-02 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,