rangeable 1.0.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 +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE +21 -0
- data/README.md +115 -0
- data/lib/rangeable/boundary_index.rb +192 -0
- data/lib/rangeable/disjoint_set.rb +92 -0
- data/lib/rangeable/interval.rb +48 -0
- data/lib/rangeable/slot.rb +36 -0
- data/lib/rangeable/version.rb +9 -0
- data/lib/rangeable.rb +246 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3b0a56ae009cc8892842f7b40ca828a0aa0f716c389b58b2e172afe08bca8e70
|
|
4
|
+
data.tar.gz: 67ecd4f99e0e9857a952301eacc7564507d1d0c422a9ee61b2e34eb141ad852d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 119170a145bf1229fc3777caa8d6c7747349cd9f8a05cf5411b609b01ff22d2b28a1914d4eaa3494dfccbf6a9f40716e2456662b4cf507c8479f0c5071ddaba1
|
|
7
|
+
data.tar.gz: 2e76959c07b80799a06b5f01aedaa71d807f3a842be70e4565bf38aed64b3a860d1b7380aa1f9c125e18aa4d1bab5ca59f548e27cb48b0b97cfdad8034fd6c15
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] — 2026-05-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial public release of `Rangeable<Element>`, the Ruby reference implementation of [RangeableRFC](https://github.com/ZhgChgLi/RangeableRFC).
|
|
13
|
+
- Per-element sorted disjoint-interval list with idempotent containment fast-path.
|
|
14
|
+
- Lazy boundary-event index, rebuilt on the next read after a mutating insert; version-counter based invalidation.
|
|
15
|
+
- Public API: `insert(element, start:, end:)`, `[i].objs`, `get_range(element)`, `transitions(over:)`, `count`, `empty?`, `each`, `copy`.
|
|
16
|
+
- Refinement-style sugar `using Rangeable::Refinements` to enable `element.get_range(from: r)` without polluting `Object`.
|
|
17
|
+
- Full RFC § 10 normative test contract (23 tests) plus a 1000-iteration property test against a brute-force oracle and a markdown-shaped micro-benchmark.
|
|
18
|
+
- Cross-language fixture (160 ops, 86 probes) shared with the Swift reference implementation; outputs are byte-identical.
|
|
19
|
+
|
|
20
|
+
### Performance
|
|
21
|
+
|
|
22
|
+
- Micro-benchmark: ~5.5× speedup over brute-force at m=50, L=2000.
|
|
23
|
+
- Real-world consumer ([ZMediumToMarkdown](https://github.com/ZhgChgLi/ZMediumToMarkdown)): 2.23× end-to-end speedup, 55% render-time reduction; all 306 existing tests pass byte-identical.
|
|
24
|
+
|
|
25
|
+
### Specification
|
|
26
|
+
|
|
27
|
+
- Conforms to [RangeableRFC v1.0](https://github.com/ZhgChgLi/RangeableRFC), reviewed and APPROVED by an independent academic reviewer (round 2; round-1 verdict REJECTED with 6 MUST-FIX items addressed).
|
|
28
|
+
|
|
29
|
+
[1.0.0]: https://github.com/ZhgChgLi/RubyRangeable/releases/tag/v1.0.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZhgChgLi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# RubyRangeable
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/rangeable) []() [](LICENSE)
|
|
4
|
+
|
|
5
|
+
Reference Ruby implementation of [`Rangeable<Element>`](https://github.com/ZhgChgLi/RangeableRFC) — a generic, integer-coordinate, closed-interval set container with first-insert ordered active queries.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your `Gemfile`:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'rangeable', '~> 1.0'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install directly:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
$ gem install rangeable
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require 'rangeable'
|
|
25
|
+
|
|
26
|
+
# Any Ruby object with sensible == / hash works as an element.
|
|
27
|
+
Strong = Struct.new(:tag)
|
|
28
|
+
Italic = Struct.new(:tag)
|
|
29
|
+
|
|
30
|
+
r = Rangeable.new
|
|
31
|
+
r.insert(Strong.new(:bold), start: 2, end: 5)
|
|
32
|
+
r.insert(Strong.new(:bold), start: 3, end: 7) # merges with [2, 5] → [2, 7]
|
|
33
|
+
r.insert(Strong.new(:bold), start: 9, end: 11) # disjoint
|
|
34
|
+
r.insert(Italic.new(:em), start: 3, end: 8)
|
|
35
|
+
|
|
36
|
+
r.get_range(Strong.new(:bold)) # => [[2, 7], [9, 11]]
|
|
37
|
+
r.get_range(Italic.new(:em)) # => [[3, 8]]
|
|
38
|
+
|
|
39
|
+
r[4].objs # => [Strong<:bold>, Italic<:em>] first-insert order
|
|
40
|
+
r[8].objs # => [Italic<:em>]
|
|
41
|
+
r[10].objs # => [Strong<:bold>]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Refinement sugar
|
|
45
|
+
|
|
46
|
+
Avoid polluting the global namespace:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
using Rangeable::Refinements
|
|
50
|
+
|
|
51
|
+
Strong.new(:bold).get_range(from: r) # => [[2, 7], [9, 11]]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Sweep iteration via transitions
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
r.transitions(over: 0..15).each do |event|
|
|
58
|
+
case event.kind
|
|
59
|
+
when :open then puts "#{event.coordinate}: open #{event.element}"
|
|
60
|
+
when :close then puts "#{event.coordinate}: close #{event.element}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
| Method | Returns | Notes |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `Rangeable.new` | empty `Rangeable` | |
|
|
70
|
+
| `r.insert(element, start:, end:)` | `:mutated` / `:idempotent` | raises `Rangeable::InvalidIntervalError` if `start > end` |
|
|
71
|
+
| `r[i]` | `Rangeable::Slot` | wrapper exposing `.objs` |
|
|
72
|
+
| `r[i].objs` | frozen `Array<Element>` | first-insert order |
|
|
73
|
+
| `r.get_range(element)` | `Array<[lo, hi]>` | merged disjoint ranges |
|
|
74
|
+
| `r.transitions(over: a..b)` | `Array<Event>` | each `Event` has `coordinate`, `kind`, `element` |
|
|
75
|
+
| `r.count` | `Integer` | distinct elements |
|
|
76
|
+
| `r.empty?` | `Boolean` | |
|
|
77
|
+
| `r.each { \|element, ranges\| ... }` | `self` | iteration |
|
|
78
|
+
| `r.copy` | `Rangeable` | deep copy |
|
|
79
|
+
|
|
80
|
+
## Semantics
|
|
81
|
+
|
|
82
|
+
- **End is inclusive**: `[a, b]` covers `a..b`, both ends.
|
|
83
|
+
- **Same-element merging**: equal elements (by `==` / `hash`) merge on overlap or integer adjacency. `[2, 4] ∪ [5, 7] = [2, 7]`.
|
|
84
|
+
- **Idempotent insert**: re-inserting a contained interval costs no version bump.
|
|
85
|
+
- **Out-of-order rejected**: `insert(_, start: 5, end: 2)` raises.
|
|
86
|
+
- **Active-set ordering**: deterministic across runs — first-insert order of the element, not hash bucket order.
|
|
87
|
+
- **Element immutability**: elements are frozen on insert (`element.dup.freeze`); mutating an element after insert is undefined behaviour.
|
|
88
|
+
|
|
89
|
+
See [RangeableRFC](https://github.com/ZhgChgLi/RangeableRFC) § 4 for normative semantics, § 6 for algorithms, § 7 for the Φ-potential amortised-complexity proof, and § 10 for the 23-case normative test contract this gem must pass.
|
|
90
|
+
|
|
91
|
+
## Performance
|
|
92
|
+
|
|
93
|
+
| Workload | Result |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Brute-force baseline (m=50, L=2000) vs `Rangeable` | **~5.5× speedup** in micro-benchmark |
|
|
96
|
+
| Real consumer ([ZMediumToMarkdown](https://github.com/ZhgChgLi/ZMediumToMarkdown)) | **2.23× end-to-end speedup**, 55% render-time reduction; all 306 existing tests byte-identical |
|
|
97
|
+
|
|
98
|
+
## See also
|
|
99
|
+
|
|
100
|
+
- **[RangeableRFC](https://github.com/ZhgChgLi/RangeableRFC)** — the normative specification this gem implements.
|
|
101
|
+
- **[SwiftRangeable](https://github.com/ZhgChgLi/SwiftRangeable)** — sibling reference implementation in Swift; produces byte-identical outputs against a shared cross-language fixture.
|
|
102
|
+
- **[ZMediumToMarkdown](https://github.com/ZhgChgLi/ZMediumToMarkdown)** — real-world consumer that exercises this gem on Medium GraphQL renderers.
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
$ bundle install
|
|
108
|
+
$ bundle exec rake test
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The test suite covers the full RFC § 10 contract (23 normative tests), a 1000-iteration property test against a brute-force oracle, and a markdown-shaped micro-benchmark.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT © [ZhgChgLi](https://github.com/ZhgChgLi)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Rangeable
|
|
4
|
+
# Lazy boundary-event index per RFC §5.2 / §6.3.
|
|
5
|
+
#
|
|
6
|
+
# Built from a snapshot of the per-element interval map plus the
|
|
7
|
+
# insertion-order map `ord`. Carries:
|
|
8
|
+
#
|
|
9
|
+
# * `events` — sorted Array of TransitionEvent under §4.5 ordering.
|
|
10
|
+
# * `segments` — sorted, disjoint Array of (seg_lo, seg_hi, frozen active)
|
|
11
|
+
# triples covering every coordinate at which the active
|
|
12
|
+
# set is non-empty. Active sets are frozen and sorted by
|
|
13
|
+
# `ord(e)` ascending.
|
|
14
|
+
# * `version` — snapshot of Rangeable.version at build time. The owner
|
|
15
|
+
# Rangeable invalidates the index by setting it to nil
|
|
16
|
+
# on any mutation; reads compare versions to decide
|
|
17
|
+
# whether to rebuild (T3 mutex pattern, §11).
|
|
18
|
+
#
|
|
19
|
+
# `nil` close coordinates encode +∞ for hi == Integer::MAX boundaries
|
|
20
|
+
# (§4.7 C4). Comparison against `nil` always treats it as greater than
|
|
21
|
+
# any finite Integer; we centralise that logic in `coord_lt?` /
|
|
22
|
+
# `coord_le?`.
|
|
23
|
+
class BoundaryIndex
|
|
24
|
+
# Public TransitionEvent type returned by `Rangeable#transitions`.
|
|
25
|
+
# `coordinate` is normally an Integer; it is `nil` for close events
|
|
26
|
+
# whose underlying interval ends at the implementation's +∞ sentinel
|
|
27
|
+
# (i.e. `hi` was the maximum representable boundary in the cross-language
|
|
28
|
+
# contract). `kind` is `:open` or `:close`.
|
|
29
|
+
TransitionEvent = Struct.new(:coordinate, :kind, :element, :ord) do
|
|
30
|
+
def open?
|
|
31
|
+
kind == :open
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close?
|
|
35
|
+
kind == :close
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{ coordinate: coordinate, kind: kind, element: element }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Sentinel: callers may pass `Integer.MAX` semantics by passing the
|
|
44
|
+
# special value below as `hi`. We default to no upper bound so close
|
|
45
|
+
# coords are always `hi + 1` finite Integer except when the caller
|
|
46
|
+
# explicitly opts into the +∞ sentinel via `Rangeable::INT_MAX`.
|
|
47
|
+
#
|
|
48
|
+
# Ruby has unbounded Integer, so this sentinel only matters for
|
|
49
|
+
# cross-language byte-identical fixtures that need to round-trip
|
|
50
|
+
# Swift's `Int.max` boundary (Test #23.A).
|
|
51
|
+
|
|
52
|
+
Segment = Struct.new(:lo, :hi, :active)
|
|
53
|
+
|
|
54
|
+
attr_reader :events, :segments, :version
|
|
55
|
+
|
|
56
|
+
def initialize(events, segments, version)
|
|
57
|
+
@events = events.freeze
|
|
58
|
+
@segments = segments.freeze
|
|
59
|
+
@version = version
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Find the segment containing `coord`, or nil if none. O(log |segments|).
|
|
64
|
+
# `coord` must be a finite Integer.
|
|
65
|
+
def segment_at(coord)
|
|
66
|
+
idx = @segments.bsearch_index { |seg| seg.hi >= coord }
|
|
67
|
+
return nil unless idx
|
|
68
|
+
|
|
69
|
+
seg = @segments[idx]
|
|
70
|
+
return nil unless seg.lo <= coord
|
|
71
|
+
seg
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns the events whose coordinate falls in `[lo, succ(hi)]` per
|
|
75
|
+
# RFC §6.4. `lo` is a finite Integer; `upper_coord` may be `nil` to
|
|
76
|
+
# mean "include all events through +∞".
|
|
77
|
+
def events_in_range(lo, upper_coord)
|
|
78
|
+
i_start = @events.bsearch_index { |ev| coord_ge?(ev.coordinate, lo) } || @events.size
|
|
79
|
+
result = []
|
|
80
|
+
i = i_start
|
|
81
|
+
while i < @events.size && coord_le?(@events[i].coordinate, upper_coord)
|
|
82
|
+
result << @events[i]
|
|
83
|
+
i += 1
|
|
84
|
+
end
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Build a fresh index from the per-element interval map and the
|
|
89
|
+
# insertion-order map `ord`. `int_max_sentinel` (default nil) lets the
|
|
90
|
+
# caller opt into "treat hi == sentinel as +∞" semantics for cross-
|
|
91
|
+
# language fixture parity; if it is nil we never coerce close coords
|
|
92
|
+
# to nil (Ruby's unbounded Integer makes that unnecessary).
|
|
93
|
+
def self.build(intervals, ord, snapshot_version, int_max_sentinel: nil)
|
|
94
|
+
events = []
|
|
95
|
+
intervals.each do |element, set|
|
|
96
|
+
element_ord = ord[element]
|
|
97
|
+
set.each do |interval|
|
|
98
|
+
events << TransitionEvent.new(interval.lo, :open, element, element_ord)
|
|
99
|
+
close_coord =
|
|
100
|
+
if !int_max_sentinel.nil? && interval.hi == int_max_sentinel
|
|
101
|
+
nil
|
|
102
|
+
else
|
|
103
|
+
interval.hi + 1
|
|
104
|
+
end
|
|
105
|
+
events << TransitionEvent.new(close_coord, :close, element, element_ord)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
events.sort! do |a, b|
|
|
110
|
+
cmp = compare_coord(a.coordinate, b.coordinate)
|
|
111
|
+
next cmp unless cmp.zero?
|
|
112
|
+
|
|
113
|
+
# Same coord: opens before closes.
|
|
114
|
+
cmp = (a.kind == :open ? 0 : 1) <=> (b.kind == :open ? 0 : 1)
|
|
115
|
+
next cmp unless cmp.zero?
|
|
116
|
+
|
|
117
|
+
# Same coord + same kind: open ascending by ord, close descending.
|
|
118
|
+
if a.kind == :open
|
|
119
|
+
a.ord <=> b.ord
|
|
120
|
+
else
|
|
121
|
+
b.ord <=> a.ord
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
segments = materialise_segments(events)
|
|
126
|
+
new(events, segments, snapshot_version)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Sweep events linearly, materialising a Segment for every maximal run
|
|
130
|
+
# of integers over which the active set is constant. Per RFC §6.3 we
|
|
131
|
+
# do not emit a segment whose active set is empty (no phantom
|
|
132
|
+
# `(-inf, first_open - 1)` segment).
|
|
133
|
+
def self.materialise_segments(events)
|
|
134
|
+
segments = []
|
|
135
|
+
active_by_ord = {} # ord => element (treated as a sorted set keyed by ord)
|
|
136
|
+
prev_coord = nil
|
|
137
|
+
i = 0
|
|
138
|
+
while i < events.size
|
|
139
|
+
# Group events at the same coord; we apply all of them before
|
|
140
|
+
# snapshotting the active set so that segment boundaries land on
|
|
141
|
+
# transitions, not in the middle of a same-coord burst.
|
|
142
|
+
ev = events[i]
|
|
143
|
+
coord = ev.coordinate
|
|
144
|
+
|
|
145
|
+
if !prev_coord.nil? && !active_by_ord.empty?
|
|
146
|
+
segments << Segment.new(prev_coord, coord - 1, snapshot_active(active_by_ord))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Apply every event at this coord.
|
|
150
|
+
while i < events.size && events[i].coordinate == coord
|
|
151
|
+
ev_i = events[i]
|
|
152
|
+
if ev_i.open?
|
|
153
|
+
active_by_ord[ev_i.ord] = ev_i.element
|
|
154
|
+
else
|
|
155
|
+
active_by_ord.delete(ev_i.ord)
|
|
156
|
+
end
|
|
157
|
+
i += 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
prev_coord = coord
|
|
161
|
+
end
|
|
162
|
+
segments
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Snapshot the active set as a frozen Array sorted by ord ascending.
|
|
166
|
+
# The Hash insertion order is not guaranteed to match ord ascending
|
|
167
|
+
# (we add/remove arbitrarily), so we sort explicitly.
|
|
168
|
+
def self.snapshot_active(active_by_ord)
|
|
169
|
+
active_by_ord.keys.sort.map { |o| active_by_ord[o] }.freeze
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Total order over coordinates: nil (== +∞) is greater than any finite.
|
|
173
|
+
# Returns -1 / 0 / +1.
|
|
174
|
+
def self.compare_coord(a, b)
|
|
175
|
+
return 0 if a.nil? && b.nil?
|
|
176
|
+
return 1 if a.nil?
|
|
177
|
+
return -1 if b.nil?
|
|
178
|
+
|
|
179
|
+
a <=> b
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def coord_ge?(coord, threshold)
|
|
185
|
+
self.class.compare_coord(coord, threshold) >= 0
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def coord_le?(coord, upper)
|
|
189
|
+
self.class.compare_coord(coord, upper) <= 0
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'interval'
|
|
4
|
+
|
|
5
|
+
class Rangeable
|
|
6
|
+
# Sorted, disjoint, non-adjacent list of `Interval` for a single element.
|
|
7
|
+
#
|
|
8
|
+
# Maintains RFC §5.1 (I1) invariant:
|
|
9
|
+
# * sorted by lo strictly ascending
|
|
10
|
+
# * any two adjacent entries (lo1, hi1), (lo2, hi2) satisfy hi1 + 1 < lo2
|
|
11
|
+
# (no overlap, no integer adjacency)
|
|
12
|
+
# * lo <= hi for every entry
|
|
13
|
+
#
|
|
14
|
+
# `insert` follows RFC §6.1 cleaner variant (containment idempotent
|
|
15
|
+
# fast-path) and signals to the caller whether the mutation actually
|
|
16
|
+
# changed the canonical state.
|
|
17
|
+
class DisjointSet
|
|
18
|
+
# Result codes returned from `insert`. The caller (Rangeable) uses these
|
|
19
|
+
# to decide whether to bump the container-level version counter.
|
|
20
|
+
MUTATED = :mutated
|
|
21
|
+
IDEMPOTENT = :idempotent
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@entries = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns a frozen snapshot of the merged intervals as `[[lo, hi], ...]`.
|
|
28
|
+
# The list satisfies RFC §5.1 (I1).
|
|
29
|
+
def to_pairs
|
|
30
|
+
@entries.map(&:to_a)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def each(&block)
|
|
34
|
+
@entries.each(&block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def empty?
|
|
38
|
+
@entries.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def size
|
|
42
|
+
@entries.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Insert `[lo, hi]` into the set, performing union-with-merge per RFC §6.1.
|
|
46
|
+
# Returns `MUTATED` if the canonical state changed (caller should bump
|
|
47
|
+
# version), `IDEMPOTENT` if the insert was absorbed by an existing entry
|
|
48
|
+
# (caller MUST NOT bump version, per Test #21 and Lemma 6.5.B).
|
|
49
|
+
def insert(lo, hi)
|
|
50
|
+
raise ArgumentError, "lo (#{lo}) > hi (#{hi})" if lo > hi
|
|
51
|
+
|
|
52
|
+
# Step 4 of §6.1: bsearch for the leftmost touch candidate.
|
|
53
|
+
# Predicate: `iv.hi + 1 >= lo`. We use `iv.hi + 1` (not `lo - 1`) to
|
|
54
|
+
# avoid Integer underflow at lo == Int.min boundaries (§4.7 C5).
|
|
55
|
+
i0 = @entries.bsearch_index { |iv| iv.hi + 1 >= lo } || @entries.size
|
|
56
|
+
|
|
57
|
+
# Step 5: collect contiguous touch entries while `entries[i].lo <= hi + 1`.
|
|
58
|
+
to_merge_end = i0
|
|
59
|
+
while to_merge_end < @entries.size && @entries[to_merge_end].lo <= hi + 1
|
|
60
|
+
to_merge_end += 1
|
|
61
|
+
end
|
|
62
|
+
merge_count = to_merge_end - i0
|
|
63
|
+
|
|
64
|
+
# Step 6: containment idempotent fast-path. If we touch exactly one
|
|
65
|
+
# existing entry that fully covers [lo, hi], this insert is a no-op.
|
|
66
|
+
# MUST NOT mutate, MUST NOT bump version.
|
|
67
|
+
if merge_count == 1
|
|
68
|
+
existing = @entries[i0]
|
|
69
|
+
if existing.lo <= lo && hi <= existing.hi
|
|
70
|
+
return IDEMPOTENT
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Step 7: real mutation path. Compute merged bounds, splice in.
|
|
75
|
+
new_lo = lo
|
|
76
|
+
new_hi = hi
|
|
77
|
+
if merge_count.positive?
|
|
78
|
+
first = @entries[i0]
|
|
79
|
+
last = @entries[to_merge_end - 1]
|
|
80
|
+
new_lo = first.lo if first.lo < new_lo
|
|
81
|
+
new_hi = last.hi if last.hi > new_hi
|
|
82
|
+
end
|
|
83
|
+
merged = Interval.new(new_lo, new_hi)
|
|
84
|
+
# `slice!` removes the touched range; then we splice the merged
|
|
85
|
+
# interval at i0. Equivalent to the §6.1 reference pseudocode but
|
|
86
|
+
# avoids repeated O(m_e) shifts that delete_at would cause in a loop.
|
|
87
|
+
@entries.slice!(i0, merge_count) if merge_count.positive?
|
|
88
|
+
@entries.insert(i0, merged)
|
|
89
|
+
MUTATED
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Rangeable
|
|
4
|
+
# Immutable closed integer interval [lo, hi].
|
|
5
|
+
#
|
|
6
|
+
# Used internally as the storage form within Rangeable::DisjointSet. Instances
|
|
7
|
+
# are produced by `insert` and never mutated; equality is structural so two
|
|
8
|
+
# intervals with the same `lo` / `hi` compare equal under `==`, `eql?` and
|
|
9
|
+
# `hash`.
|
|
10
|
+
class Interval
|
|
11
|
+
attr_reader :lo, :hi
|
|
12
|
+
|
|
13
|
+
def initialize(lo, hi)
|
|
14
|
+
raise ArgumentError, "lo (#{lo}) > hi (#{hi})" if lo > hi
|
|
15
|
+
|
|
16
|
+
@lo = lo
|
|
17
|
+
@hi = hi
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Inclusive containment check: true when `coord` falls inside [lo, hi].
|
|
22
|
+
def include?(coord)
|
|
23
|
+
lo <= coord && coord <= hi
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the interval as a 2-element array `[lo, hi]`.
|
|
27
|
+
def to_a
|
|
28
|
+
[lo, hi]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
other.is_a?(Interval) && other.lo == lo && other.hi == hi
|
|
33
|
+
end
|
|
34
|
+
alias eql? ==
|
|
35
|
+
|
|
36
|
+
def hash
|
|
37
|
+
[Interval, lo, hi].hash
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
"[#{lo}, #{hi}]"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inspect
|
|
45
|
+
"#<Rangeable::Interval lo=#{lo} hi=#{hi}>"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Rangeable
|
|
4
|
+
# Thin wrapper returned by `Rangeable#[]`. Wraps the active-element list at
|
|
5
|
+
# a single coordinate so we can grow the surface (e.g. add `count`,
|
|
6
|
+
# `transitions`, etc.) without changing the public type.
|
|
7
|
+
#
|
|
8
|
+
# `objs` is always a frozen `Array` of elements ordered by first-insert
|
|
9
|
+
# `ord(e)` ascending (RFC §4.5). We use a `Struct` (rather than a plain
|
|
10
|
+
# class) because the hot-path call sites are `r[i].objs` and the Struct
|
|
11
|
+
# accessor is materially faster than going through `attr_reader` + an
|
|
12
|
+
# ivar in MRI.
|
|
13
|
+
Slot = Struct.new(:objs) do
|
|
14
|
+
def empty?
|
|
15
|
+
objs.empty?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def size
|
|
19
|
+
objs.size
|
|
20
|
+
end
|
|
21
|
+
alias_method :count, :size
|
|
22
|
+
alias_method :length, :size
|
|
23
|
+
|
|
24
|
+
def each(&block)
|
|
25
|
+
objs.each(&block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_a
|
|
29
|
+
objs.dup
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def inspect
|
|
33
|
+
"#<Rangeable::Slot objs=#{objs.inspect}>"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Defined as a class because the main `Rangeable` symbol in `rangeable.rb` is
|
|
4
|
+
# a class. We declare it the same way here so that requiring this file
|
|
5
|
+
# alone (e.g. from the gemspec) does not lock the symbol into being a
|
|
6
|
+
# module.
|
|
7
|
+
class Rangeable
|
|
8
|
+
VERSION = '1.0.0'
|
|
9
|
+
end
|
data/lib/rangeable.rb
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rangeable/version'
|
|
4
|
+
require_relative 'rangeable/interval'
|
|
5
|
+
require_relative 'rangeable/slot'
|
|
6
|
+
require_relative 'rangeable/disjoint_set'
|
|
7
|
+
require_relative 'rangeable/boundary_index'
|
|
8
|
+
|
|
9
|
+
# `Rangeable` is the language-neutral, generic, integer-coordinate closed-
|
|
10
|
+
# interval set container described in `RFC.md`. It pairs hashable elements
|
|
11
|
+
# with their merged disjoint integer ranges and supports three query
|
|
12
|
+
# families: by-element (`get_range`), by-position (`r[i]`) and by-range
|
|
13
|
+
# (`transitions`). See RFC §3 for the API surface.
|
|
14
|
+
class Rangeable
|
|
15
|
+
# Raised when an interval is malformed (start > end). Subclassing
|
|
16
|
+
# `ArgumentError` keeps it `rescue`able alongside other invalid-arg
|
|
17
|
+
# mistakes per RFC §3.7.
|
|
18
|
+
class InvalidIntervalError < ArgumentError; end
|
|
19
|
+
|
|
20
|
+
# `Rangeable::TransitionEvent` is the public alias for the Struct defined
|
|
21
|
+
# inside the index. Re-exposed here so callers don't need to reach through
|
|
22
|
+
# `BoundaryIndex` to reference its type.
|
|
23
|
+
TransitionEvent = ::Rangeable::BoundaryIndex::TransitionEvent unless const_defined?(:TransitionEvent, false)
|
|
24
|
+
|
|
25
|
+
# Frozen empty Array we hand back from `[]` when the coordinate is
|
|
26
|
+
# outside every segment. Avoids re-allocating an empty Array per query.
|
|
27
|
+
EMPTY_ACTIVE = [].freeze unless const_defined?(:EMPTY_ACTIVE, false)
|
|
28
|
+
|
|
29
|
+
attr_reader :version
|
|
30
|
+
|
|
31
|
+
# Build a fresh empty container. RFC §3.1.
|
|
32
|
+
def initialize
|
|
33
|
+
@intervals = {} # element => DisjointSet
|
|
34
|
+
@insertion_order = [] # Array<element> in first-insert order
|
|
35
|
+
@ord = {} # element => Integer (1-based)
|
|
36
|
+
@version = 0
|
|
37
|
+
@event_index = nil # BoundaryIndex or nil (lazy)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Sugar: `Rangeable.empty` matches the RFC §3.1 alias.
|
|
41
|
+
def self.empty
|
|
42
|
+
new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Insert `element` covering `[start, end_]` (closed interval). Idempotent
|
|
46
|
+
# by RFC §3.2: re-inserting a sub-range that is already fully contained
|
|
47
|
+
# leaves the container unchanged and does NOT bump version.
|
|
48
|
+
#
|
|
49
|
+
# The RFC reference uses the keyword `end:`, but Ruby treats `end` inside
|
|
50
|
+
# method headers as a soft-keyword that is fine in keyword-arg position.
|
|
51
|
+
# We accept it that way to match the RFC API exactly.
|
|
52
|
+
def insert(element, start:, end:)
|
|
53
|
+
end_ = binding.local_variable_get(:end)
|
|
54
|
+
raise InvalidIntervalError, "start (#{start}) > end (#{end_})" if start > end_
|
|
55
|
+
|
|
56
|
+
e = freeze_for_insert(element)
|
|
57
|
+
|
|
58
|
+
set = @intervals[e]
|
|
59
|
+
if set.nil?
|
|
60
|
+
set = DisjointSet.new
|
|
61
|
+
@intervals[e] = set
|
|
62
|
+
@insertion_order << e
|
|
63
|
+
@ord[e] = @insertion_order.length
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
result = set.insert(start, end_)
|
|
67
|
+
if result == DisjointSet::MUTATED
|
|
68
|
+
@version += 1
|
|
69
|
+
@event_index = nil
|
|
70
|
+
end
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Active-element list at `i`. RFC §3.3.
|
|
75
|
+
# O(log |segments| + r) once the index is built.
|
|
76
|
+
#
|
|
77
|
+
# Hot path: we inline the segment bsearch here (instead of going through
|
|
78
|
+
# a private helper or method dispatch into BoundaryIndex#segment_at) so
|
|
79
|
+
# `r[i]` stays allocation-free apart from the unavoidable `Slot`
|
|
80
|
+
# envelope. The cached active array on the segment is already frozen,
|
|
81
|
+
# so `Slot.new` does not duplicate it (see `Slot#initialize`).
|
|
82
|
+
def [](i)
|
|
83
|
+
ensure_event_index_fresh
|
|
84
|
+
segs = @event_index.segments
|
|
85
|
+
idx = segs.bsearch_index { |seg| seg.hi >= i }
|
|
86
|
+
if idx
|
|
87
|
+
seg = segs[idx]
|
|
88
|
+
if seg.lo <= i
|
|
89
|
+
Slot.new(seg.active)
|
|
90
|
+
else
|
|
91
|
+
Slot.new(EMPTY_ACTIVE)
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
Slot.new(EMPTY_ACTIVE)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
alias active_at_index []
|
|
98
|
+
|
|
99
|
+
# Same as `[]` but spelled out to match RFC §3.3.
|
|
100
|
+
def active_at(index:)
|
|
101
|
+
self[index]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Merged ranges for `element` as `[[lo, hi], ...]`. RFC §3.4. Returns an
|
|
105
|
+
# empty Array when the element has never been inserted.
|
|
106
|
+
def get_range(element)
|
|
107
|
+
set = @intervals[element]
|
|
108
|
+
return [] unless set
|
|
109
|
+
|
|
110
|
+
set.to_pairs
|
|
111
|
+
end
|
|
112
|
+
alias range_of get_range
|
|
113
|
+
alias get_range_of get_range
|
|
114
|
+
|
|
115
|
+
# Transitions (open / close events) within an inclusive coordinate range.
|
|
116
|
+
# Accepts a Ruby `Range` (inclusive or exclusive). RFC §3.5.
|
|
117
|
+
def transitions(over:)
|
|
118
|
+
raise InvalidIntervalError, "transitions range must be a Range" unless over.is_a?(Range)
|
|
119
|
+
|
|
120
|
+
lo = over.begin
|
|
121
|
+
hi = over.end
|
|
122
|
+
raise InvalidIntervalError, "open-ended begin not supported" if lo.nil?
|
|
123
|
+
|
|
124
|
+
# Normalise exclusive end (`a...b` ⇒ inclusive end `b - 1`).
|
|
125
|
+
hi = hi - 1 if !hi.nil? && over.exclude_end?
|
|
126
|
+
|
|
127
|
+
# Open-ended top (`a..nil`) means +∞ ⇒ include all events.
|
|
128
|
+
if hi.nil?
|
|
129
|
+
raise InvalidIntervalError, "lo (#{lo}) > hi (nil)" if lo.nil?
|
|
130
|
+
else
|
|
131
|
+
raise InvalidIntervalError, "lo (#{lo}) > hi (#{hi})" if lo > hi
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
ensure_event_index_fresh
|
|
135
|
+
upper = hi.nil? ? nil : hi + 1
|
|
136
|
+
@event_index.events_in_range(lo, upper).map do |ev|
|
|
137
|
+
TransitionEvent.new(ev.coordinate, ev.kind, ev.element, ev.ord)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Number of distinct equivalence-class elements ever inserted. RFC §3.5.1.
|
|
142
|
+
def count
|
|
143
|
+
@insertion_order.length
|
|
144
|
+
end
|
|
145
|
+
alias size count
|
|
146
|
+
alias length count
|
|
147
|
+
|
|
148
|
+
# `count == 0`. RFC §3.5.1.
|
|
149
|
+
def empty?
|
|
150
|
+
@insertion_order.empty?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Iterate `(element, ranges)` pairs in insertion-order ascending. RFC §3.5.1.
|
|
154
|
+
def each(&block)
|
|
155
|
+
return enum_for(:each) unless block_given?
|
|
156
|
+
|
|
157
|
+
@insertion_order.each do |element|
|
|
158
|
+
yield element, @intervals[element].to_pairs
|
|
159
|
+
end
|
|
160
|
+
self
|
|
161
|
+
end
|
|
162
|
+
include Enumerable
|
|
163
|
+
|
|
164
|
+
# Deep copy of the entire container per RFC §3.5.1. Mutation on the copy
|
|
165
|
+
# MUST NOT affect this instance and vice versa.
|
|
166
|
+
def copy
|
|
167
|
+
dup_instance = self.class.new
|
|
168
|
+
@insertion_order.each do |element|
|
|
169
|
+
dup_instance.send(:replant, element, @intervals[element], @ord[element])
|
|
170
|
+
end
|
|
171
|
+
dup_instance.send(:set_version_after_copy, @version)
|
|
172
|
+
dup_instance
|
|
173
|
+
end
|
|
174
|
+
alias dup copy
|
|
175
|
+
alias clone copy
|
|
176
|
+
|
|
177
|
+
# Sugar form: `Rangeable.range_of(element, from: r)`. RFC §3.4.
|
|
178
|
+
def self.range_of(element, from:)
|
|
179
|
+
from.get_range(element)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Refinement-only sugar so `element.get_range(from: r)` works without
|
|
183
|
+
# polluting the global Object namespace. Caller does `using Rangeable::Refinements`.
|
|
184
|
+
module Refinements
|
|
185
|
+
refine Object do
|
|
186
|
+
def get_range(from:)
|
|
187
|
+
from.get_range(self)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
# RFC §4.6 (M2): freeze on insert as cheap defence against caller-side
|
|
195
|
+
# mutation of hash-affecting state. We try to dup-then-freeze; if `dup`
|
|
196
|
+
# is not supported (some Symbol-like cases) we fall back to the original.
|
|
197
|
+
def freeze_for_insert(element)
|
|
198
|
+
return element if element.frozen?
|
|
199
|
+
|
|
200
|
+
begin
|
|
201
|
+
element.dup.freeze
|
|
202
|
+
rescue TypeError
|
|
203
|
+
element
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def ensure_event_index_fresh
|
|
208
|
+
return if @event_index && @event_index.version == @version
|
|
209
|
+
|
|
210
|
+
# Snapshot the version at the start of the build. After build, recheck
|
|
211
|
+
# against `@version` (T3 mutex pattern, §11). In Ruby's GVL world this
|
|
212
|
+
# is mostly a no-op, but the comment exists to flag the contract for
|
|
213
|
+
# any future Ractor port.
|
|
214
|
+
v_start = @version
|
|
215
|
+
rebuilt = BoundaryIndex.build(@intervals, @ord, v_start)
|
|
216
|
+
@event_index = rebuilt if @version == v_start
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Public-ish helper kept available for direct callers. The hot path in
|
|
220
|
+
# `[]` inlines this for one less method dispatch per query.
|
|
221
|
+
def active_at_coord(coord)
|
|
222
|
+
ensure_event_index_fresh
|
|
223
|
+
seg = @event_index.segment_at(coord)
|
|
224
|
+
seg ? seg.active : EMPTY_ACTIVE
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Used by `copy` to clone an element's interval set with its ord intact.
|
|
228
|
+
# Lives here as a private setter so the public surface stays clean.
|
|
229
|
+
def replant(element, source_set, source_ord)
|
|
230
|
+
new_set = DisjointSet.new
|
|
231
|
+
source_set.each do |iv|
|
|
232
|
+
# MUTATED is the only outcome for sequential insertions of disjoint,
|
|
233
|
+
# ordered intervals into an empty set; we don't need to inspect the
|
|
234
|
+
# return value but we still call insert to keep all invariants in
|
|
235
|
+
# one place.
|
|
236
|
+
new_set.insert(iv.lo, iv.hi)
|
|
237
|
+
end
|
|
238
|
+
@intervals[element] = new_set
|
|
239
|
+
@insertion_order << element
|
|
240
|
+
@ord[element] = source_ord
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def set_version_after_copy(v)
|
|
244
|
+
@version = v
|
|
245
|
+
end
|
|
246
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rangeable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ZhgChgLi
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
description: |
|
|
42
|
+
Rangeable is a language-neutral, generic, integer-coordinate closed-interval
|
|
43
|
+
set container. It pairs hashable elements with their merged disjoint integer
|
|
44
|
+
ranges and answers three queries: by-element ranges, by-position active set,
|
|
45
|
+
and by-range transition events. The Ruby reference implementation follows
|
|
46
|
+
the Rangeable RFC normatively, including idempotent containment fast-path,
|
|
47
|
+
lazy boundary-event indexing, and first-insert deterministic ordering.
|
|
48
|
+
email:
|
|
49
|
+
- zhgchgli@gmail.com
|
|
50
|
+
executables: []
|
|
51
|
+
extensions: []
|
|
52
|
+
extra_rdoc_files: []
|
|
53
|
+
files:
|
|
54
|
+
- CHANGELOG.md
|
|
55
|
+
- LICENSE
|
|
56
|
+
- README.md
|
|
57
|
+
- lib/rangeable.rb
|
|
58
|
+
- lib/rangeable/boundary_index.rb
|
|
59
|
+
- lib/rangeable/disjoint_set.rb
|
|
60
|
+
- lib/rangeable/interval.rb
|
|
61
|
+
- lib/rangeable/slot.rb
|
|
62
|
+
- lib/rangeable/version.rb
|
|
63
|
+
homepage: https://github.com/ZhgChgLi/RubyRangeable
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
homepage_uri: https://github.com/ZhgChgLi/RubyRangeable
|
|
68
|
+
source_code_uri: https://github.com/ZhgChgLi/RubyRangeable
|
|
69
|
+
bug_tracker_uri: https://github.com/ZhgChgLi/RubyRangeable/issues
|
|
70
|
+
changelog_uri: https://github.com/ZhgChgLi/RubyRangeable/blob/main/CHANGELOG.md
|
|
71
|
+
documentation_uri: https://github.com/ZhgChgLi/RangeableRFC/blob/main/RFC.md
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
73
|
+
post_install_message:
|
|
74
|
+
rdoc_options: []
|
|
75
|
+
require_paths:
|
|
76
|
+
- lib
|
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.2'
|
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0'
|
|
87
|
+
requirements: []
|
|
88
|
+
rubygems_version: 3.4.1
|
|
89
|
+
signing_key:
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Hashable-element interval set with first-insert ordered active queries.
|
|
92
|
+
test_files: []
|