philiprehberger-interval 0.1.5 → 0.3.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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +49 -4
- data/lib/philiprehberger/interval/range.rb +108 -6
- data/lib/philiprehberger/interval/version.rb +1 -1
- data/lib/philiprehberger/interval.rb +16 -2
- metadata +10 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2614076496fada8f6202df687ec993c5438e3a47945824e05e6bb4d00cc1c046
|
|
4
|
+
data.tar.gz: 2557c885218f82fe7de28ea6e91e660b9b901ab2995ca8d68e3a1ef9cf9b2d21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed05a860c2d28ce9cfdf00787585e117c41ee03bd1a4f917f4c06d4c461313a7fcdfe0fa7ce9f6073804c4c67c1169e84a7988a2070d8f54f42a725a0caef36b
|
|
7
|
+
data.tar.gz: 8451b55300a3d8d489cc895792256f4e7a1077d9be2a0df25aee1e7f9ef42d9defa2de2fe7eabe3b3d3c58c6c959037f65f89a06eaa8e96d61025ff91a09e285
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-04-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Interval.intersection(intervals)` — compute the common overlap of a collection of intervals
|
|
14
|
+
|
|
15
|
+
## [0.2.0] - 2026-04-03
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Open, half-open interval types via `type:` parameter (`:closed`, `:open`, `:left_open`, `:right_open`)
|
|
19
|
+
- `shift` method for translating intervals
|
|
20
|
+
- `scale` method for resizing around an anchor point
|
|
21
|
+
- `split` method for dividing into equal sub-intervals
|
|
22
|
+
- `clamp` method for constraining values to interval bounds
|
|
23
|
+
|
|
24
|
+
## [0.1.6] - 2026-03-31
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Add GitHub issue templates, dependabot config, and PR template
|
|
28
|
+
|
|
10
29
|
## [0.1.5] - 2026-03-31
|
|
11
30
|
|
|
12
31
|
### Changed
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-interval)
|
|
5
5
|
[](https://github.com/philiprehberger/rb-interval/commits/main)
|
|
6
6
|
|
|
7
|
-
Interval data type with
|
|
7
|
+
Interval data type with open/closed boundaries, arithmetic, merging, and gap finding
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
@@ -38,6 +38,32 @@ a.union(b) # => [1, 8]
|
|
|
38
38
|
a.include?(4) # => true
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
### Interval Types
|
|
42
|
+
|
|
43
|
+
Supports closed, open, and half-open boundaries:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
closed = Philiprehberger::Interval.new(1, 5, type: :closed) # [1, 5] (default)
|
|
47
|
+
open = Philiprehberger::Interval.new(1, 5, type: :open) # (1, 5)
|
|
48
|
+
left_open = Philiprehberger::Interval.new(1, 5, type: :left_open) # (1, 5]
|
|
49
|
+
right_open = Philiprehberger::Interval.new(1, 5, type: :right_open) # [1, 5)
|
|
50
|
+
|
|
51
|
+
closed.include?(5) # => true
|
|
52
|
+
right_open.include?(5) # => false
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Interval Arithmetic
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
interval = Philiprehberger::Interval.new(2, 8)
|
|
59
|
+
|
|
60
|
+
interval.shift(3) # => [5, 11]
|
|
61
|
+
interval.scale(2, anchor: :left) # => [2, 14]
|
|
62
|
+
interval.scale(0.5, anchor: :center) # => [3.5, 6.5]
|
|
63
|
+
interval.split(3) # => [[2, 4], [4, 6], [6, 8]]
|
|
64
|
+
interval.clamp(10) # => 8
|
|
65
|
+
```
|
|
66
|
+
|
|
41
67
|
### Merging Intervals
|
|
42
68
|
|
|
43
69
|
```ruby
|
|
@@ -57,6 +83,19 @@ Philiprehberger::Interval.gaps(intervals)
|
|
|
57
83
|
# => [[7, 10]]
|
|
58
84
|
```
|
|
59
85
|
|
|
86
|
+
### Finding a Common Intersection
|
|
87
|
+
|
|
88
|
+
Compute the overlap shared by every interval, or `nil` if any pair is disjoint:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Philiprehberger::Interval.intersection([
|
|
92
|
+
Philiprehberger::Interval.new(1, 10),
|
|
93
|
+
Philiprehberger::Interval.new(3, 8),
|
|
94
|
+
Philiprehberger::Interval.new(5, 12)
|
|
95
|
+
])
|
|
96
|
+
# => [5, 8]
|
|
97
|
+
```
|
|
98
|
+
|
|
60
99
|
### With Time Values
|
|
61
100
|
|
|
62
101
|
```ruby
|
|
@@ -68,17 +107,23 @@ shift.include?(Time.new(2026, 1, 1, 12)) # => true
|
|
|
68
107
|
|
|
69
108
|
| Method | Description |
|
|
70
109
|
|--------|-------------|
|
|
71
|
-
| `.new(start, finish)` | Create
|
|
72
|
-
| `#
|
|
110
|
+
| `.new(start, finish, type:)` | Create an interval (`:closed`, `:open`, `:left_open`, `:right_open`) |
|
|
111
|
+
| `#type` | Return the interval boundary type |
|
|
112
|
+
| `#overlaps?(other)` | Check if two intervals overlap (respects boundary types) |
|
|
73
113
|
| `#contains?(other)` | Check if interval fully contains another |
|
|
74
114
|
| `#adjacent?(other)` | Check if intervals are touching but not overlapping |
|
|
75
115
|
| `#intersect(other)` | Return the overlap between two intervals |
|
|
76
116
|
| `#union(other)` | Return the combined interval |
|
|
77
117
|
| `#subtract(other)` | Remove another interval, returning remaining parts |
|
|
78
118
|
| `#size` | Length of the interval |
|
|
79
|
-
| `#include?(point)` | Check if a point falls within the interval |
|
|
119
|
+
| `#include?(point)` | Check if a point falls within the interval (respects boundary types) |
|
|
120
|
+
| `#shift(delta)` | Return new interval shifted by delta, preserving type |
|
|
121
|
+
| `#scale(factor, anchor:)` | Scale width around anchor (`:center`, `:left`, `:right`) |
|
|
122
|
+
| `#split(n)` | Split into n equal sub-intervals |
|
|
123
|
+
| `#clamp(value)` | Clamp value to interval bounds |
|
|
80
124
|
| `.merge(intervals)` | Merge overlapping intervals into non-overlapping set |
|
|
81
125
|
| `.gaps(intervals)` | Find gaps between a set of intervals |
|
|
126
|
+
| `.intersection(intervals)` | Common overlap of a collection of intervals, or `nil` |
|
|
82
127
|
|
|
83
128
|
## Development
|
|
84
129
|
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Philiprehberger
|
|
4
4
|
module Interval
|
|
5
|
-
|
|
5
|
+
VALID_TYPES = %i[closed open left_open right_open].freeze
|
|
6
|
+
|
|
7
|
+
# Represents an interval over any Comparable type.
|
|
8
|
+
# Supports closed [a, b], open (a, b), left-open (a, b], and right-open [a, b) boundaries.
|
|
6
9
|
class Range
|
|
7
10
|
include Comparable
|
|
8
11
|
|
|
@@ -12,16 +15,23 @@ module Philiprehberger
|
|
|
12
15
|
# @return [Comparable] the end of the interval
|
|
13
16
|
attr_reader :finish
|
|
14
17
|
|
|
18
|
+
# @return [Symbol] the interval type (:closed, :open, :left_open, :right_open)
|
|
19
|
+
attr_reader :type
|
|
20
|
+
|
|
15
21
|
# Create a new interval.
|
|
16
22
|
#
|
|
17
23
|
# @param start [Comparable] the start value
|
|
18
24
|
# @param finish [Comparable] the end value
|
|
25
|
+
# @param type [Symbol] boundary type (:closed, :open, :left_open, :right_open)
|
|
19
26
|
# @raise [Error] if start > finish
|
|
20
|
-
|
|
27
|
+
# @raise [Error] if type is invalid
|
|
28
|
+
def initialize(start, finish, type: :closed)
|
|
21
29
|
raise Error, 'start must be <= finish' if start > finish
|
|
30
|
+
raise Error, "invalid interval type: #{type}" unless VALID_TYPES.include?(type)
|
|
22
31
|
|
|
23
32
|
@start = start
|
|
24
33
|
@finish = finish
|
|
34
|
+
@type = type
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
# Check if this interval overlaps with another.
|
|
@@ -29,7 +39,21 @@ module Philiprehberger
|
|
|
29
39
|
# @param other [Range] the other interval
|
|
30
40
|
# @return [Boolean]
|
|
31
41
|
def overlaps?(other)
|
|
32
|
-
|
|
42
|
+
return false if empty? || other.empty?
|
|
43
|
+
|
|
44
|
+
left_ok = if right_closed? && other.left_closed?
|
|
45
|
+
@finish >= other.start
|
|
46
|
+
else
|
|
47
|
+
@finish > other.start
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
right_ok = if other.right_closed? && left_closed?
|
|
51
|
+
other.finish >= @start
|
|
52
|
+
else
|
|
53
|
+
other.finish > @start
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
left_ok && right_ok
|
|
33
57
|
end
|
|
34
58
|
|
|
35
59
|
# Check if this interval fully contains another.
|
|
@@ -94,7 +118,66 @@ module Philiprehberger
|
|
|
94
118
|
# @param point [Comparable] the point to check
|
|
95
119
|
# @return [Boolean]
|
|
96
120
|
def include?(point)
|
|
97
|
-
point
|
|
121
|
+
left = left_closed? ? point >= @start : point > @start
|
|
122
|
+
right = right_closed? ? point <= @finish : point < @finish
|
|
123
|
+
left && right
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Return a new interval shifted by delta, preserving type.
|
|
127
|
+
#
|
|
128
|
+
# @param delta [Numeric] the amount to shift
|
|
129
|
+
# @return [Range] a new shifted interval
|
|
130
|
+
def shift(delta)
|
|
131
|
+
self.class.new(@start + delta, @finish + delta, type: @type)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Scale interval width around an anchor point, preserving type.
|
|
135
|
+
#
|
|
136
|
+
# @param factor [Numeric] the scale factor
|
|
137
|
+
# @param anchor [Symbol] the anchor point (:center, :left, :right)
|
|
138
|
+
# @return [Range] a new scaled interval
|
|
139
|
+
def scale(factor, anchor: :center)
|
|
140
|
+
current_size = size
|
|
141
|
+
new_size = current_size * factor
|
|
142
|
+
|
|
143
|
+
case anchor
|
|
144
|
+
when :left
|
|
145
|
+
self.class.new(@start, @start + new_size, type: @type)
|
|
146
|
+
when :right
|
|
147
|
+
self.class.new(@finish - new_size, @finish, type: @type)
|
|
148
|
+
when :center
|
|
149
|
+
center = @start + (current_size / 2.0)
|
|
150
|
+
half = new_size / 2.0
|
|
151
|
+
self.class.new(center - half, center + half, type: @type)
|
|
152
|
+
else
|
|
153
|
+
raise Error, "invalid anchor: #{anchor}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Split interval into n equal sub-intervals, preserving type.
|
|
158
|
+
#
|
|
159
|
+
# @param n [Integer] number of sub-intervals
|
|
160
|
+
# @return [Array<Range>] array of n equal sub-intervals
|
|
161
|
+
def split(n)
|
|
162
|
+
raise Error, 'n must be positive' if n < 1
|
|
163
|
+
|
|
164
|
+
step = size.to_f / n
|
|
165
|
+
Array.new(n) do |i|
|
|
166
|
+
sub_start = @start + (step * i)
|
|
167
|
+
sub_finish = @start + (step * (i + 1))
|
|
168
|
+
self.class.new(sub_start, sub_finish, type: @type)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Clamp a value to the interval bounds.
|
|
173
|
+
#
|
|
174
|
+
# @param value [Comparable] the value to clamp
|
|
175
|
+
# @return [Comparable] the clamped value
|
|
176
|
+
def clamp(value)
|
|
177
|
+
return @start if value < @start
|
|
178
|
+
return @finish if value > @finish
|
|
179
|
+
|
|
180
|
+
value
|
|
98
181
|
end
|
|
99
182
|
|
|
100
183
|
# Compare intervals by start then finish.
|
|
@@ -108,18 +191,37 @@ module Philiprehberger
|
|
|
108
191
|
|
|
109
192
|
# @return [Boolean]
|
|
110
193
|
def ==(other)
|
|
111
|
-
other.is_a?(self.class) && @start == other.start && @finish == other.finish
|
|
194
|
+
other.is_a?(self.class) && @start == other.start && @finish == other.finish && @type == other.type
|
|
112
195
|
end
|
|
113
196
|
|
|
114
197
|
# @return [String]
|
|
115
198
|
def to_s
|
|
116
|
-
|
|
199
|
+
left_bracket = left_closed? ? '[' : '('
|
|
200
|
+
right_bracket = right_closed? ? ']' : ')'
|
|
201
|
+
"#{left_bracket}#{@start}, #{@finish}#{right_bracket}"
|
|
117
202
|
end
|
|
118
203
|
|
|
119
204
|
# @return [String]
|
|
120
205
|
def inspect
|
|
121
206
|
"#<#{self.class} #{self}>"
|
|
122
207
|
end
|
|
208
|
+
|
|
209
|
+
# @return [Boolean] true if the left endpoint is included
|
|
210
|
+
def left_closed?
|
|
211
|
+
@type == :closed || @type == :right_open
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @return [Boolean] true if the right endpoint is included
|
|
215
|
+
def right_closed?
|
|
216
|
+
@type == :closed || @type == :left_open
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
protected
|
|
220
|
+
|
|
221
|
+
# @return [Boolean] true if the interval contains no points
|
|
222
|
+
def empty?
|
|
223
|
+
@start == @finish && @type != :closed
|
|
224
|
+
end
|
|
123
225
|
end
|
|
124
226
|
end
|
|
125
227
|
end
|
|
@@ -12,8 +12,8 @@ module Philiprehberger
|
|
|
12
12
|
# @param start [Comparable] the start value
|
|
13
13
|
# @param finish [Comparable] the end value
|
|
14
14
|
# @return [Range] a new interval
|
|
15
|
-
def self.new(start, finish)
|
|
16
|
-
Range.new(start, finish)
|
|
15
|
+
def self.new(start, finish, type: :closed)
|
|
16
|
+
Range.new(start, finish, type: type)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Merge a collection of overlapping or adjacent intervals into non-overlapping intervals.
|
|
@@ -39,6 +39,20 @@ module Philiprehberger
|
|
|
39
39
|
merged
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Compute the common overlap across a collection of intervals.
|
|
43
|
+
#
|
|
44
|
+
# @param intervals [Array<Range>] the intervals to intersect
|
|
45
|
+
# @return [Range, nil] the common intersection, or nil if any pair is disjoint
|
|
46
|
+
def self.intersection(intervals)
|
|
47
|
+
return nil if intervals.empty?
|
|
48
|
+
|
|
49
|
+
intervals[1..].reduce(intervals.first) do |acc, current|
|
|
50
|
+
return nil if acc.nil?
|
|
51
|
+
|
|
52
|
+
acc.intersect(current)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
42
56
|
# Find gaps between a collection of intervals.
|
|
43
57
|
#
|
|
44
58
|
# @param intervals [Array<Range>] the intervals to analyze
|
metadata
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-interval
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.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-
|
|
11
|
+
date: 2026-04-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description:
|
|
14
|
-
intersection, union, subtraction,
|
|
15
|
-
|
|
13
|
+
description: Interval data type supporting closed, open, and half-open boundaries
|
|
14
|
+
with overlap detection, containment, intersection, union, subtraction, shift, scale,
|
|
15
|
+
split, clamp, merging collections, and finding gaps. Works with any Comparable type
|
|
16
|
+
including Numeric and Time.
|
|
16
17
|
email:
|
|
17
18
|
- me@philiprehberger.com
|
|
18
19
|
executables: []
|
|
@@ -25,11 +26,11 @@ files:
|
|
|
25
26
|
- lib/philiprehberger/interval.rb
|
|
26
27
|
- lib/philiprehberger/interval/range.rb
|
|
27
28
|
- lib/philiprehberger/interval/version.rb
|
|
28
|
-
homepage: https://
|
|
29
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-interval
|
|
29
30
|
licenses:
|
|
30
31
|
- MIT
|
|
31
32
|
metadata:
|
|
32
|
-
homepage_uri: https://
|
|
33
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-interval
|
|
33
34
|
source_code_uri: https://github.com/philiprehberger/rb-interval
|
|
34
35
|
changelog_uri: https://github.com/philiprehberger/rb-interval/blob/main/CHANGELOG.md
|
|
35
36
|
bug_tracker_uri: https://github.com/philiprehberger/rb-interval/issues
|
|
@@ -52,5 +53,6 @@ requirements: []
|
|
|
52
53
|
rubygems_version: 3.5.22
|
|
53
54
|
signing_key:
|
|
54
55
|
specification_version: 4
|
|
55
|
-
summary: Interval data type with
|
|
56
|
+
summary: Interval data type with open/closed boundaries, arithmetic, merging, and
|
|
57
|
+
gap finding
|
|
56
58
|
test_files: []
|