philiprehberger-interval 0.1.4 → 0.2.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: 32f7667847de4edfef6e6bed9ba746bb591406c692db772569d09d46d9bbc412
4
- data.tar.gz: 8e4a1395612a8fe831f347425fc620d98199c203271477775174f9f1e12a97a6
3
+ metadata.gz: 6039cd1187d208aeb17ffae5d095d4dd9416fd8175f95084984600dc36c15b74
4
+ data.tar.gz: 4cb497c7a97490954a47568a6b024e517f71a680f040644073b60b316f6fe174
5
5
  SHA512:
6
- metadata.gz: 15efa08d08720b493aa265bd88db732d235cbb8a88f6364ffd3f9aa5865c7a42ed2a5f33851e7a7e43f54f8a7b36ea5d4a0f5d6fb20f5ef885b7bb38a8d7ac6a
7
- data.tar.gz: 0dd947ea52c1536545d019c2bef21288fefcb6d7650464ac406ec7ccdcd87d1f521229f601424066866b4ef710e401437a5740dbda5f0e2cc27e6ed763fae386
6
+ metadata.gz: 60355c282c23da5623dc104947363632290343db018701c800f0b10e65bb64d87e5c5e0a69b08ca309ee7241318d92381cca64a1f9fc0f5be036a139d2258b9a
7
+ data.tar.gz: 4c686abfd2318284ca810c87d25787804029ec3355039f56f3a859d3abdae8925704e0320a3ac6be192174dbe426fd28622ce73be36d514e0e18ae00b26117f1
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.2.0] - 2026-04-03
11
+
12
+ ### Added
13
+ - Open, half-open interval types via `type:` parameter (`:closed`, `:open`, `:left_open`, `:right_open`)
14
+ - `shift` method for translating intervals
15
+ - `scale` method for resizing around an anchor point
16
+ - `split` method for dividing into equal sub-intervals
17
+ - `clamp` method for constraining values to interval bounds
18
+
19
+ ## [0.1.6] - 2026-03-31
20
+
21
+ ### Added
22
+ - Add GitHub issue templates, dependabot config, and PR template
23
+
24
+ ## [0.1.5] - 2026-03-31
25
+
26
+ ### Changed
27
+ - Standardize README badges, support section, and license format
28
+
10
29
  ## [0.1.4] - 2026-03-26
11
30
 
12
31
  ### Changed
data/README.md CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  [![Tests](https://github.com/philiprehberger/rb-interval/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-interval/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-interval.svg)](https://rubygems.org/gems/philiprehberger-interval)
5
- [![License](https://img.shields.io/github/license/philiprehberger/rb-interval)](LICENSE)
6
- [![Sponsor](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ec6cb9)](https://github.com/sponsors/philiprehberger)
5
+ [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-interval)](https://github.com/philiprehberger/rb-interval/commits/main)
7
6
 
8
- Interval data type with overlap detection, merging, and gap finding
7
+ Interval data type with open/closed boundaries, arithmetic, merging, and gap finding
9
8
 
10
9
  ## Requirements
11
10
 
@@ -39,6 +38,32 @@ a.union(b) # => [1, 8]
39
38
  a.include?(4) # => true
40
39
  ```
41
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
+
42
67
  ### Merging Intervals
43
68
 
44
69
  ```ruby
@@ -69,15 +94,20 @@ shift.include?(Time.new(2026, 1, 1, 12)) # => true
69
94
 
70
95
  | Method | Description |
71
96
  |--------|-------------|
72
- | `.new(start, finish)` | Create a closed interval |
73
- | `#overlaps?(other)` | Check if two intervals overlap |
97
+ | `.new(start, finish, type:)` | Create an interval (`:closed`, `:open`, `:left_open`, `:right_open`) |
98
+ | `#type` | Return the interval boundary type |
99
+ | `#overlaps?(other)` | Check if two intervals overlap (respects boundary types) |
74
100
  | `#contains?(other)` | Check if interval fully contains another |
75
101
  | `#adjacent?(other)` | Check if intervals are touching but not overlapping |
76
102
  | `#intersect(other)` | Return the overlap between two intervals |
77
103
  | `#union(other)` | Return the combined interval |
78
104
  | `#subtract(other)` | Remove another interval, returning remaining parts |
79
105
  | `#size` | Length of the interval |
80
- | `#include?(point)` | Check if a point falls within the interval |
106
+ | `#include?(point)` | Check if a point falls within the interval (respects boundary types) |
107
+ | `#shift(delta)` | Return new interval shifted by delta, preserving type |
108
+ | `#scale(factor, anchor:)` | Scale width around anchor (`:center`, `:left`, `:right`) |
109
+ | `#split(n)` | Split into n equal sub-intervals |
110
+ | `#clamp(value)` | Clamp value to interval bounds |
81
111
  | `.merge(intervals)` | Merge overlapping intervals into non-overlapping set |
82
112
  | `.gaps(intervals)` | Find gaps between a set of intervals |
83
113
 
@@ -89,6 +119,24 @@ bundle exec rspec
89
119
  bundle exec rubocop
90
120
  ```
91
121
 
122
+ ## Support
123
+
124
+ If you find this project useful:
125
+
126
+ ⭐ [Star the repo](https://github.com/philiprehberger/rb-interval)
127
+
128
+ 🐛 [Report issues](https://github.com/philiprehberger/rb-interval/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
129
+
130
+ 💡 [Suggest features](https://github.com/philiprehberger/rb-interval/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
131
+
132
+ ❤️ [Sponsor development](https://github.com/sponsors/philiprehberger)
133
+
134
+ 🌐 [All Open Source Projects](https://philiprehberger.com/open-source-packages)
135
+
136
+ 💻 [GitHub Profile](https://github.com/philiprehberger)
137
+
138
+ 🔗 [LinkedIn Profile](https://www.linkedin.com/in/philiprehberger)
139
+
92
140
  ## License
93
141
 
94
142
  [MIT](LICENSE)
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Interval
5
- # Represents a closed interval [start, finish] over any Comparable type.
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
- def initialize(start, finish)
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
- @start <= other.finish && other.start <= @finish
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 >= @start && point <= @finish
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
- "[#{@start}, #{@finish}]"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Interval
5
- VERSION = "0.1.4"
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  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.
@@ -44,7 +44,7 @@ module Philiprehberger
44
44
  # @param intervals [Array<Range>] the intervals to analyze
45
45
  # @return [Array<Range>] gaps between the merged intervals
46
46
  def self.gaps(intervals)
47
- merged = self.merge(intervals)
47
+ merged = merge(intervals)
48
48
  return [] if merged.length < 2
49
49
 
50
50
  result = []
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.1.4
4
+ version: 0.2.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-03-27 00:00:00.000000000 Z
11
+ date: 2026-04-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Closed interval data type supporting overlap detection, containment,
14
- intersection, union, subtraction, merging collections, and finding gaps. Works with
15
- any Comparable type including Numeric and Time.
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://github.com/philiprehberger/rb-interval
29
+ homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-interval
29
30
  licenses:
30
31
  - MIT
31
32
  metadata:
32
- homepage_uri: https://github.com/philiprehberger/rb-interval
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 overlap detection, merging, and gap finding
56
+ summary: Interval data type with open/closed boundaries, arithmetic, merging, and
57
+ gap finding
56
58
  test_files: []