denebola 0.2.1 → 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.
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Denebola
4
+ class Sheet
5
+ # A persistent row segment tree whose nodes cache a persistent column segment
6
+ # tree. Point edits touch log(row extent) * log(column extent) index nodes.
7
+ class RangeIndex
8
+ class ValueNode
9
+ attr_reader :key, :count, :left, :right, :height, :minimum, :maximum
10
+
11
+ def initialize(key, count, left, right)
12
+ @key, @count, @left, @right = key, count, left, right
13
+ @height = 1 + [left&.height || 0, right&.height || 0].max
14
+ @minimum = left ? left.minimum : key
15
+ @maximum = right ? right.maximum : key
16
+ freeze
17
+ end
18
+ end
19
+
20
+ module ValueTree
21
+ module_function
22
+
23
+ def update(root, key, delta)
24
+ if root.nil?
25
+ raise "missing value in range index" unless delta.positive?
26
+ return ValueNode.new(key, delta, nil, nil)
27
+ end
28
+
29
+ comparison = key <=> root.key
30
+ raise ArgumentError, "numeric cell values are not mutually comparable" unless comparison
31
+
32
+ if comparison.zero?
33
+ count = root.count + delta
34
+ return remove_root(root) if count.zero?
35
+ raise "negative value count in range index" if count.negative?
36
+ return ValueNode.new(root.key, count, root.left, root.right)
37
+ end
38
+
39
+ if comparison.negative?
40
+ balance(ValueNode.new(root.key, root.count, update(root.left, key, delta), root.right))
41
+ else
42
+ balance(ValueNode.new(root.key, root.count, root.left, update(root.right, key, delta)))
43
+ end
44
+ end
45
+
46
+ def remove_root(root)
47
+ return root.right unless root.left
48
+ return root.left unless root.right
49
+
50
+ successor = root.right
51
+ successor = successor.left while successor.left
52
+ right = update(root.right, successor.key, -successor.count)
53
+ balance(ValueNode.new(successor.key, successor.count, root.left, right))
54
+ end
55
+
56
+ def balance(root)
57
+ factor = height(root.left) - height(root.right)
58
+ if factor > 1
59
+ left = root.left
60
+ left = rotate_left(left) if height(left.left) < height(left.right)
61
+ return rotate_right(ValueNode.new(root.key, root.count, left, root.right))
62
+ elsif factor < -1
63
+ right = root.right
64
+ right = rotate_right(right) if height(right.right) < height(right.left)
65
+ return rotate_left(ValueNode.new(root.key, root.count, root.left, right))
66
+ end
67
+ root
68
+ end
69
+
70
+ def rotate_left(root)
71
+ pivot = root.right
72
+ left = ValueNode.new(root.key, root.count, root.left, pivot.left)
73
+ ValueNode.new(pivot.key, pivot.count, left, pivot.right)
74
+ end
75
+
76
+ def rotate_right(root)
77
+ pivot = root.left
78
+ right = ValueNode.new(root.key, root.count, pivot.right, root.right)
79
+ ValueNode.new(pivot.key, pivot.count, pivot.left, right)
80
+ end
81
+
82
+ def height(node) = node&.height || 0
83
+ end
84
+
85
+ class ValueBag
86
+ attr_reader :summary, :count
87
+
88
+ def initialize(count: 0, sum: 0, types: {}, numbers: nil)
89
+ @count, @sum, @types, @numbers = count, sum, types.freeze, numbers
90
+ @summary = Sheet::Summary.new(count: count, sum: sum,
91
+ min: numbers&.minimum, max: numbers&.maximum, types: @types)
92
+ freeze
93
+ end
94
+
95
+ ZERO = new
96
+
97
+ def self.zero = ZERO
98
+
99
+ def replace(old_value, new_value)
100
+ bag = old_value.nil? ? self : update(old_value, -1)
101
+ new_value.nil? ? bag : bag.update(new_value, 1)
102
+ end
103
+
104
+ def update(value, delta)
105
+ return self if delta.zero?
106
+
107
+ types = @types.dup
108
+ type_count = types.fetch(value.class, 0) + delta
109
+ raise "negative type count in range index" if type_count.negative?
110
+ type_count.zero? ? types.delete(value.class) : types[value.class] = type_count
111
+ sum = @sum + (value.is_a?(Numeric) ? value * delta : 0)
112
+ numbers = if comparable_number?(value)
113
+ ValueTree.update(@numbers, value, delta)
114
+ else
115
+ @numbers
116
+ end
117
+ self.class.new(count: @count + delta, sum: sum, types: types, numbers: numbers)
118
+ end
119
+
120
+ def empty? = count.zero?
121
+
122
+ private
123
+
124
+ def comparable_number?(value)
125
+ value.is_a?(Numeric) && !(value <=> value).nil?
126
+ end
127
+ end
128
+
129
+ class ColumnNode
130
+ attr_reader :left, :right, :bag, :summary
131
+
132
+ def initialize(left: nil, right: nil, bag: nil)
133
+ @left, @right, @bag = left, right, bag
134
+ @summary = bag ? bag.summary : summary_of(left) + summary_of(right)
135
+ freeze
136
+ end
137
+
138
+ private
139
+
140
+ def summary_of(node) = node&.summary || Sheet::Summary.zero
141
+ end
142
+
143
+ class ColumnIndex
144
+ attr_reader :root, :capacity
145
+
146
+ def initialize(root: nil, capacity: 1)
147
+ @root, @capacity = root, capacity
148
+ freeze
149
+ end
150
+
151
+ def update(column, old_value, new_value)
152
+ return self if old_value == new_value
153
+
154
+ root, capacity = @root, @capacity
155
+ while column >= capacity
156
+ root = ColumnNode.new(left: root, right: nil) if root
157
+ capacity *= 2
158
+ end
159
+ root = update_node(root, 0, capacity, column, old_value, new_value)
160
+ self.class.new(root: root, capacity: capacity)
161
+ end
162
+
163
+ def summary(left, right)
164
+ return Sheet::Summary.zero unless @root && left < @capacity
165
+
166
+ query(@root, 0, @capacity, left, [right + 1, @capacity].min)
167
+ end
168
+
169
+ def empty? = @root.nil?
170
+
171
+ def check_invariants!
172
+ check_node(@root, 0, @capacity) if @root
173
+ true
174
+ end
175
+
176
+ private
177
+
178
+ def update_node(node, first, finish, column, old_value, new_value)
179
+ if finish - first == 1
180
+ bag = (node&.bag || ValueBag.zero).replace(old_value, new_value)
181
+ return nil if bag.empty?
182
+
183
+ return ColumnNode.new(bag: bag)
184
+ end
185
+
186
+ middle = (first + finish) / 2
187
+ left, right = node&.left, node&.right
188
+ if column < middle
189
+ left = update_node(left, first, middle, column, old_value, new_value)
190
+ else
191
+ right = update_node(right, middle, finish, column, old_value, new_value)
192
+ end
193
+ return nil unless left || right
194
+
195
+ ColumnNode.new(left: left, right: right)
196
+ end
197
+
198
+ def query(node, first, finish, left, right)
199
+ return Sheet::Summary.zero unless node && left < finish && first < right
200
+ return node.summary if left <= first && finish <= right
201
+
202
+ middle = (first + finish) / 2
203
+ query(node.left, first, middle, left, right) + query(node.right, middle, finish, left, right)
204
+ end
205
+
206
+ def check_node(node, first, finish)
207
+ if finish - first == 1
208
+ raise "invalid column-index leaf" unless node.bag && !node.left && !node.right && node.summary == node.bag.summary
209
+ return node.summary
210
+ end
211
+ raise "invalid column-index branch" if node.bag
212
+ middle = (first + finish) / 2
213
+ summary = (node.left ? check_node(node.left, first, middle) : Sheet::Summary.zero) +
214
+ (node.right ? check_node(node.right, middle, finish) : Sheet::Summary.zero)
215
+ raise "incorrect column-index summary" unless node.summary == summary
216
+ summary
217
+ end
218
+ end
219
+
220
+ class RowNode
221
+ attr_reader :left, :right, :columns
222
+
223
+ def initialize(left, right, columns)
224
+ @left, @right, @columns = left, right, columns
225
+ freeze
226
+ end
227
+ end
228
+
229
+ attr_reader :root, :capacity
230
+
231
+ def initialize(root: nil, capacity: 1)
232
+ @root, @capacity = root, capacity
233
+ freeze
234
+ end
235
+
236
+ def update(row, column, old_value, new_value)
237
+ return self if unchanged_value?(old_value, new_value)
238
+
239
+ root, capacity = @root, @capacity
240
+ while row >= capacity
241
+ root = RowNode.new(root, nil, root.columns) if root
242
+ capacity *= 2
243
+ end
244
+ root = update_row(root, 0, capacity, row, column, old_value, new_value)
245
+ self.class.new(root: root, capacity: capacity)
246
+ end
247
+
248
+ def summary(top, left, bottom, right)
249
+ return Sheet::Summary.zero unless @root && top < @capacity
250
+
251
+ query(@root, 0, @capacity, top, [bottom + 1, @capacity].min, left, right)
252
+ end
253
+
254
+ def self.from_rows(rows, row_axis, column_axis)
255
+ index = new
256
+ row = 0
257
+ rows.each do |item|
258
+ if item.is_a?(Gap)
259
+ row += item.length
260
+ next
261
+ end
262
+ column = 0
263
+ item.columns.each do |cell|
264
+ if cell.is_a?(Gap)
265
+ column += cell.length
266
+ else
267
+ index = index.update(row_axis.index_at(row), column_axis.index_at(column), nil, cell.value)
268
+ column += 1
269
+ end
270
+ end
271
+ row += 1
272
+ end
273
+ index
274
+ end
275
+
276
+ def check_invariants!
277
+ check_row(@root, 0, @capacity) if @root
278
+ true
279
+ end
280
+
281
+ private
282
+
283
+ def unchanged_value?(old_value, new_value)
284
+ old_value.nil? ? new_value.nil? : !new_value.nil? && old_value.class == new_value.class && old_value == new_value
285
+ end
286
+
287
+ def update_row(node, first, finish, row, column, old_value, new_value)
288
+ columns = (node&.columns || ColumnIndex.new).update(column, old_value, new_value)
289
+ if finish - first == 1
290
+ return nil if columns.empty?
291
+ return RowNode.new(nil, nil, columns)
292
+ end
293
+
294
+ middle = (first + finish) / 2
295
+ left, right = node&.left, node&.right
296
+ if row < middle
297
+ left = update_row(left, first, middle, row, column, old_value, new_value)
298
+ else
299
+ right = update_row(right, middle, finish, row, column, old_value, new_value)
300
+ end
301
+ return nil if columns.empty?
302
+
303
+ RowNode.new(left, right, columns)
304
+ end
305
+
306
+ def query(node, first, finish, top, bottom, left, right)
307
+ return Sheet::Summary.zero unless node && top < finish && first < bottom
308
+ return node.columns.summary(left, right) if top <= first && finish <= bottom
309
+
310
+ middle = (first + finish) / 2
311
+ query(node.left, first, middle, top, bottom, left, right) +
312
+ query(node.right, middle, finish, top, bottom, left, right)
313
+ end
314
+
315
+ def check_row(node, first, finish)
316
+ node.columns.check_invariants!
317
+ return if finish - first == 1
318
+
319
+ middle = (first + finish) / 2
320
+ check_row(node.left, first, middle) if node.left
321
+ check_row(node.right, middle, finish) if node.right
322
+ end
323
+ end
324
+
325
+ private_constant :RangeIndex
326
+ end
327
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Denebola
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/denebola.rb CHANGED
@@ -11,3 +11,5 @@ require_relative "denebola/edit"
11
11
  require_relative "denebola/rope"
12
12
  require_relative "denebola/anchor"
13
13
  require_relative "denebola/lazy_rope"
14
+ require_relative "denebola/sheet"
15
+ require_relative "denebola/sheet_range_index"
data/sig/denebola.rbs CHANGED
@@ -162,7 +162,9 @@ module Denebola
162
162
  def utf16_length: () -> Integer
163
163
  def line_count: (?exact: bool) -> Integer
164
164
  def line_start: (Integer) -> Integer
165
+ def line_end: (Integer) -> Integer
165
166
  def line: (Integer) -> String
167
+ def line_window: (Integer, ?from: Integer, ?max_bytes: Integer) -> [String, Integer]
166
168
  def byteslice: (Integer | Range[Integer?], ?Integer?) -> Rope
167
169
  def each_chunk: () { (String) -> void } -> self
168
170
  | () -> Enumerator[String, self]
@@ -172,6 +174,7 @@ module Denebola
172
174
  def insert: (Integer, String) -> self
173
175
  def delete: (Range[Integer?]) -> self
174
176
  def replace: (Range[Integer?], String) -> self
177
+ def apply_edits: (Array[[Range[Integer?], String]]) -> LazyRope
175
178
  def point_at: (Integer) -> Point
176
179
  def offset_at: (Point) -> Integer
177
180
  def utf16_offset_at: (Integer) -> Integer
@@ -189,4 +192,34 @@ module Denebola
189
192
  def initialize: (Integer, ?bias: :left | :right) -> void
190
193
  def transform: (Array[[Range[Integer], String]]) -> Anchor
191
194
  end
195
+
196
+ class Sheet
197
+ DEFAULT_BRANCHING: Integer
198
+ attr_reader row_count: Integer
199
+ attr_reader column_count: Integer
200
+ attr_reader cell_count: Integer
201
+ def initialize: (?branching: Integer) -> void
202
+ def []: (Integer, Integer) -> untyped
203
+ def set: (Integer, Integer, untyped) -> Sheet
204
+ def set_many: (Enumerable[[Integer, Integer, untyped]]) -> Sheet
205
+ def delete: (Integer, Integer) -> Sheet
206
+ def each_in: (Integer, Integer, Integer, Integer) { (Point, untyped) -> void } -> self
207
+ | (Integer, Integer, Integer, Integer) -> Enumerator[[Point, untyped], self]
208
+ def summary: (Integer, Integer, Integer, Integer) -> Summary
209
+ def insert_rows: (Integer, Integer) -> Sheet
210
+ def delete_rows: (Integer, Integer) -> Sheet
211
+ def insert_columns: (Integer, Integer) -> Sheet
212
+ def delete_columns: (Integer, Integer) -> Sheet
213
+ def snapshot: () -> Sheet
214
+ def check_invariants!: () -> true
215
+
216
+ class Summary
217
+ attr_reader count: Integer
218
+ attr_reader sum: Numeric
219
+ attr_reader min: Numeric?
220
+ attr_reader max: Numeric?
221
+ attr_reader types: Hash[Class, Integer]
222
+ def self.zero: () -> Summary
223
+ end
224
+ end
192
225
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: denebola
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-16 00:00:00.000000000 Z
11
+ date: 2026-09-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Immutable text ropes plus bounded-memory, file-backed editing with line
14
14
  and UTF-16 indexing.
@@ -30,6 +30,8 @@ files:
30
30
  - lib/denebola/lazy_rope.rb
31
31
  - lib/denebola/point.rb
32
32
  - lib/denebola/rope.rb
33
+ - lib/denebola/sheet.rb
34
+ - lib/denebola/sheet_range_index.rb
33
35
  - lib/denebola/text_summary.rb
34
36
  - lib/denebola/tree.rb
35
37
  - lib/denebola/version.rb