denebola 0.2.2 → 0.4.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 +8 -1
- data/README.md +57 -1
- data/lib/denebola/code_editor_buffer.rb +52 -0
- data/lib/denebola/sheet.rb +796 -0
- data/lib/denebola/sheet_range_index.rb +327 -0
- data/lib/denebola/version.rb +1 -1
- data/lib/denebola.rb +2 -0
- data/sig/denebola.rbs +45 -0
- metadata +5 -2
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Denebola
|
|
4
|
+
class Sheet
|
|
5
|
+
# Maps current axis coordinates to stable RangeIndex coordinates. Structural
|
|
6
|
+
# edits split or remove these runs without moving unaffected index entries.
|
|
7
|
+
# ponytail: flat runs make edits O(S) and rectangle queries O(Sr×Sc); use a persistent interval tree if fragmented-axis workloads dominate.
|
|
8
|
+
class IndexAxis
|
|
9
|
+
Segment = Struct.new(:length, :index_start, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
attr_reader :extent, :next_index, :segments
|
|
12
|
+
|
|
13
|
+
def initialize(segments: [], extent: 0, next_index: 0)
|
|
14
|
+
@segments = segments.freeze
|
|
15
|
+
@extent, @next_index = extent, next_index
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.identity(extent)
|
|
20
|
+
new(segments: extent.positive? ? [segment(extent, 0)] : [], extent: extent, next_index: extent)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ensure_length(length)
|
|
24
|
+
return self if length == extent
|
|
25
|
+
raise ArgumentError, "axis length cannot shrink" unless length > extent
|
|
26
|
+
|
|
27
|
+
append(length - extent)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def index_at(position)
|
|
31
|
+
raise RangeError, "axis position out of bounds" unless position.is_a?(Integer) && position.between?(0, extent - 1)
|
|
32
|
+
|
|
33
|
+
offset = 0
|
|
34
|
+
segments.each do |segment|
|
|
35
|
+
return segment.index_start + position - offset if position < offset + segment.length
|
|
36
|
+
offset += segment.length
|
|
37
|
+
end
|
|
38
|
+
raise "axis mapping is incomplete"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ranges(first, last)
|
|
42
|
+
return [] if first > last || first >= extent
|
|
43
|
+
|
|
44
|
+
last = [last, extent - 1].min
|
|
45
|
+
offset = 0
|
|
46
|
+
segments.filter_map do |segment|
|
|
47
|
+
finish = offset + segment.length - 1
|
|
48
|
+
from = [first, offset].max
|
|
49
|
+
to = [last, finish].min
|
|
50
|
+
mapped = [segment.index_start + from - offset, segment.index_start + to - offset] if from <= to
|
|
51
|
+
offset = finish + 1
|
|
52
|
+
mapped
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def insert(at, count)
|
|
57
|
+
return self if count.zero?
|
|
58
|
+
|
|
59
|
+
left, right = split(at)
|
|
60
|
+
self.class.new(segments: coalesce(left + [self.class.segment(count, next_index)] + right),
|
|
61
|
+
extent: extent + count, next_index: next_index + count)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def delete(at, count)
|
|
65
|
+
count = [count, extent - at].min
|
|
66
|
+
return self if count.zero?
|
|
67
|
+
|
|
68
|
+
left, tail = split(at)
|
|
69
|
+
_removed, right = split_segments(tail, count)
|
|
70
|
+
self.class.new(segments: coalesce(left + right), extent: extent - count, next_index: next_index)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_invariants!
|
|
74
|
+
raise "invalid axis extent" unless segments.sum(&:length) == extent
|
|
75
|
+
raise "invalid axis segment" unless segments.all? { |segment| segment.length.positive? && segment.index_start >= 0 }
|
|
76
|
+
raise "coalescible axis segments" if segments.each_cons(2).any? { |left, right| left.index_start + left.length == right.index_start }
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def append(length)
|
|
83
|
+
self.class.new(segments: coalesce(segments + [self.class.segment(length, next_index)]),
|
|
84
|
+
extent: extent + length, next_index: next_index + length)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def split(position)
|
|
88
|
+
raise RangeError, "axis position out of bounds" unless position.between?(0, extent)
|
|
89
|
+
left, right = split_segments(segments, position)
|
|
90
|
+
[left, right]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def split_segments(items, length)
|
|
94
|
+
left = []
|
|
95
|
+
right = []
|
|
96
|
+
offset = 0
|
|
97
|
+
items.each do |segment|
|
|
98
|
+
before = [[length - offset, 0].max, segment.length].min
|
|
99
|
+
left << self.class.segment(before, segment.index_start) if before.positive?
|
|
100
|
+
after = segment.length - before
|
|
101
|
+
right << self.class.segment(after, segment.index_start + before) if after.positive?
|
|
102
|
+
offset += segment.length
|
|
103
|
+
end
|
|
104
|
+
[left, right]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def coalesce(items)
|
|
108
|
+
items.each_with_object([]) do |segment, result|
|
|
109
|
+
previous = result.last
|
|
110
|
+
if previous && previous.index_start + previous.length == segment.index_start
|
|
111
|
+
result[-1] = self.class.segment(previous.length + segment.length, previous.index_start)
|
|
112
|
+
else
|
|
113
|
+
result << segment
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.segment(length, index_start) = Segment.new(length:, index_start:).freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Summary
|
|
122
|
+
attr_reader :count, :sum, :min, :max, :types
|
|
123
|
+
|
|
124
|
+
def initialize(count: 0, sum: 0, min: nil, max: nil, types: {})
|
|
125
|
+
@count, @sum, @min, @max = count, sum, min, max
|
|
126
|
+
@types = types.freeze
|
|
127
|
+
freeze
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
ZERO = new
|
|
131
|
+
def self.zero = ZERO
|
|
132
|
+
def self.value(value)
|
|
133
|
+
numeric = value.is_a?(Numeric) && (value <=> value)
|
|
134
|
+
new(count: 1, sum: value.is_a?(Numeric) ? value : 0,
|
|
135
|
+
min: numeric ? value : nil, max: numeric ? value : nil,
|
|
136
|
+
types: { value.class => 1 })
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def +(other)
|
|
140
|
+
types = @types.dup
|
|
141
|
+
other.types.each { |type, count| types[type] = types.fetch(type, 0) + count }
|
|
142
|
+
types.freeze
|
|
143
|
+
self.class.new(count: count + other.count, sum: sum + other.sum,
|
|
144
|
+
min: extreme(min, other.min, :min), max: extreme(max, other.max, :max),
|
|
145
|
+
types: types)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ==(other) = other.is_a?(Summary) && [count, sum, min, max, types] == [other.count, other.sum, other.min, other.max, other.types]
|
|
149
|
+
alias eql? ==
|
|
150
|
+
def hash = [count, sum, min, max, types].hash
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def extreme(left, right, direction)
|
|
155
|
+
return right if left.nil?
|
|
156
|
+
return left if right.nil?
|
|
157
|
+
comparison = left <=> right
|
|
158
|
+
return nil unless comparison
|
|
159
|
+
direction == :min ? (comparison <= 0 ? left : right) : (comparison >= 0 ? left : right)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class ColumnSummary
|
|
164
|
+
attr_reader :values, :column_count
|
|
165
|
+
def initialize(values: Summary.zero, column_count: 0)
|
|
166
|
+
@values, @column_count = values, column_count
|
|
167
|
+
freeze
|
|
168
|
+
end
|
|
169
|
+
ZERO = new
|
|
170
|
+
def self.zero = ZERO
|
|
171
|
+
def +(other) = self.class.new(values: values + other.values, column_count: column_count + other.column_count)
|
|
172
|
+
def ==(other) = other.is_a?(ColumnSummary) && values == other.values && column_count == other.column_count
|
|
173
|
+
alias eql? ==
|
|
174
|
+
def hash = [values, column_count].hash
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class RowSummary
|
|
178
|
+
attr_reader :values, :row_count
|
|
179
|
+
def initialize(values: Summary.zero, row_count: 0)
|
|
180
|
+
@values, @row_count = values, row_count
|
|
181
|
+
freeze
|
|
182
|
+
end
|
|
183
|
+
ZERO = new
|
|
184
|
+
def self.zero = ZERO
|
|
185
|
+
def +(other) = self.class.new(values: values + other.values, row_count: row_count + other.row_count)
|
|
186
|
+
def ==(other) = other.is_a?(RowSummary) && values == other.values && row_count == other.row_count
|
|
187
|
+
alias eql? ==
|
|
188
|
+
def hash = [values, row_count].hash
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
class Gap
|
|
192
|
+
attr_reader :length
|
|
193
|
+
def initialize(length, axis:)
|
|
194
|
+
raise ArgumentError, "gap length must be positive" unless length.is_a?(Integer) && length.positive?
|
|
195
|
+
@length, @axis = length, axis
|
|
196
|
+
@summary = axis == :row ? RowSummary.new(row_count: length) : ColumnSummary.new(column_count: length)
|
|
197
|
+
freeze
|
|
198
|
+
end
|
|
199
|
+
attr_reader :summary
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class Cell
|
|
203
|
+
attr_reader :value
|
|
204
|
+
def initialize(value)
|
|
205
|
+
@value = value.is_a?(String) ? value.dup.freeze : value
|
|
206
|
+
@summary = ColumnSummary.new(values: Summary.value(@value), column_count: 1)
|
|
207
|
+
freeze
|
|
208
|
+
end
|
|
209
|
+
attr_reader :summary
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class Row
|
|
213
|
+
attr_reader :columns
|
|
214
|
+
def initialize(columns)
|
|
215
|
+
@columns = columns
|
|
216
|
+
@summary = RowSummary.new(values: columns.summary.values, row_count: 1)
|
|
217
|
+
freeze
|
|
218
|
+
end
|
|
219
|
+
attr_reader :summary
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
DEFAULT_BRANCHING = Tree::DEFAULT_BRANCHING
|
|
223
|
+
private_constant :IndexAxis, :ColumnSummary, :RowSummary, :Gap, :Cell, :Row
|
|
224
|
+
|
|
225
|
+
attr_reader :row_count, :column_count, :cell_count
|
|
226
|
+
|
|
227
|
+
def self.from_rows(rows, column_count, branching, range_index = nil, row_axis = nil, column_axis = nil)
|
|
228
|
+
sheet = allocate
|
|
229
|
+
sheet.instance_variable_set(:@branching, branching)
|
|
230
|
+
sheet.__send__(:initialize_state, rows, column_count, range_index, row_axis, column_axis)
|
|
231
|
+
end
|
|
232
|
+
private_class_method :from_rows
|
|
233
|
+
|
|
234
|
+
def initialize(branching: DEFAULT_BRANCHING)
|
|
235
|
+
raise ArgumentError, "branching must be an even integer >= 4" unless branching.is_a?(Integer) && branching >= 4 && branching.even?
|
|
236
|
+
@branching = branching
|
|
237
|
+
initialize_state(Tree.new(summary: RowSummary, branching: branching), 0, nil)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def initialize_state(rows, column_count, range_index, row_axis = nil, column_axis = nil)
|
|
241
|
+
raise ArgumentError, "column_count must be a non-negative integer" unless column_count.is_a?(Integer) && column_count >= 0
|
|
242
|
+
@rows = rows
|
|
243
|
+
@row_count = @rows.summary.row_count
|
|
244
|
+
@column_count = column_count
|
|
245
|
+
@cell_count = @rows.summary.values.count
|
|
246
|
+
@range_index = range_index
|
|
247
|
+
@row_axis = row_axis || IndexAxis.identity(@row_count)
|
|
248
|
+
@column_axis = column_axis || IndexAxis.identity(@column_count)
|
|
249
|
+
raise ArgumentError, "row-axis extent differs from sheet" unless @row_axis.extent == @row_count
|
|
250
|
+
raise ArgumentError, "column-axis extent differs from sheet" unless @column_axis.extent == @column_count
|
|
251
|
+
freeze
|
|
252
|
+
end
|
|
253
|
+
private :initialize_state
|
|
254
|
+
|
|
255
|
+
def [](row, column)
|
|
256
|
+
validate_coordinate(row, column)
|
|
257
|
+
_index, item, prefix = @rows.locate(row, :row_count)
|
|
258
|
+
return nil unless item.is_a?(Row) && row == prefix.row_count
|
|
259
|
+
column_at(item.columns, column)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def set(row, column, value)
|
|
263
|
+
validate_coordinate(row, column)
|
|
264
|
+
return delete(row, column) if value.nil?
|
|
265
|
+
rows = row >= row_count ? insert_axis(@rows, row_count, row - row_count + 1, :row) : @rows
|
|
266
|
+
before, tail = split_axis(rows, row, :row)
|
|
267
|
+
current, after = split_axis(tail, 1, :row)
|
|
268
|
+
item = current[0]
|
|
269
|
+
item = Row.new(empty_columns) unless item.is_a?(Row)
|
|
270
|
+
columns = set_column(item.columns, column, value)
|
|
271
|
+
updated = Row.new(columns)
|
|
272
|
+
rows = concat(concat(before, tree([updated], RowSummary)), after)
|
|
273
|
+
new_column_count = [@column_count, column + 1].max
|
|
274
|
+
row_axis = @row_axis.ensure_length([row_count, row + 1].max)
|
|
275
|
+
column_axis = @column_axis.ensure_length(new_column_count)
|
|
276
|
+
index = if new_column_count <= 1
|
|
277
|
+
nil
|
|
278
|
+
elsif @range_index
|
|
279
|
+
@range_index.update(row_axis.index_at(row), column_axis.index_at(column), self[row, column], value)
|
|
280
|
+
else
|
|
281
|
+
RangeIndex.from_rows(rows, row_axis, column_axis)
|
|
282
|
+
end
|
|
283
|
+
with_rows(rows, new_column_count, index, row_axis, column_axis)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Apply [row, column, value] edits against one snapshot; the last duplicate wins.
|
|
287
|
+
def set_many(changes)
|
|
288
|
+
grouped = {}
|
|
289
|
+
changes.each do |change|
|
|
290
|
+
raise ArgumentError, "each cell change must contain row, column, and value" unless change.respond_to?(:length) && change.length == 3
|
|
291
|
+
|
|
292
|
+
row, column, value = change
|
|
293
|
+
validate_coordinate(row, column)
|
|
294
|
+
(grouped[row] ||= {})[column] = value
|
|
295
|
+
end
|
|
296
|
+
return self if grouped.empty?
|
|
297
|
+
|
|
298
|
+
updates = []
|
|
299
|
+
new_row_count = row_count
|
|
300
|
+
new_column_count = column_count
|
|
301
|
+
grouped.sort.each do |row, values|
|
|
302
|
+
edits = values.sort
|
|
303
|
+
has_value = false
|
|
304
|
+
edits.each do |column, value|
|
|
305
|
+
next if value.nil?
|
|
306
|
+
|
|
307
|
+
has_value = true
|
|
308
|
+
new_row_count = row + 1 if row + 1 > new_row_count
|
|
309
|
+
new_column_count = column + 1 if column + 1 > new_column_count
|
|
310
|
+
end
|
|
311
|
+
updates << [row, edits] if row < row_count || has_value
|
|
312
|
+
end
|
|
313
|
+
return self if updates.empty?
|
|
314
|
+
|
|
315
|
+
result = []
|
|
316
|
+
update_index = 0
|
|
317
|
+
position = 0
|
|
318
|
+
changed = false
|
|
319
|
+
|
|
320
|
+
@rows.each do |item|
|
|
321
|
+
if item.is_a?(Gap)
|
|
322
|
+
finish = position + item.length
|
|
323
|
+
while update_index < updates.length && updates[update_index][0] < finish
|
|
324
|
+
row, edits = updates[update_index]
|
|
325
|
+
append_gap(result, row - position) if row > position
|
|
326
|
+
updated = updated_row(nil, edits)
|
|
327
|
+
append_row(result, updated)
|
|
328
|
+
changed ||= !updated.nil?
|
|
329
|
+
position = row + 1
|
|
330
|
+
update_index += 1
|
|
331
|
+
end
|
|
332
|
+
append_gap(result, finish - position) if finish > position
|
|
333
|
+
position = finish
|
|
334
|
+
else
|
|
335
|
+
if update_index < updates.length && updates[update_index][0] == position
|
|
336
|
+
updated = updated_row(item, updates[update_index][1])
|
|
337
|
+
append_row(result, updated)
|
|
338
|
+
changed ||= !updated.equal?(item)
|
|
339
|
+
update_index += 1
|
|
340
|
+
else
|
|
341
|
+
result << item
|
|
342
|
+
end
|
|
343
|
+
position += 1
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
while update_index < updates.length
|
|
348
|
+
row, edits = updates[update_index]
|
|
349
|
+
append_gap(result, row - position) if row > position
|
|
350
|
+
updated = updated_row(nil, edits)
|
|
351
|
+
append_row(result, updated)
|
|
352
|
+
changed ||= !updated.nil?
|
|
353
|
+
position = row + 1
|
|
354
|
+
update_index += 1
|
|
355
|
+
end
|
|
356
|
+
append_gap(result, new_row_count - position) if new_row_count > position
|
|
357
|
+
return self unless changed || new_row_count != row_count || new_column_count != column_count
|
|
358
|
+
|
|
359
|
+
rows = tree(result, RowSummary)
|
|
360
|
+
row_axis = @row_axis.ensure_length(new_row_count)
|
|
361
|
+
column_axis = @column_axis.ensure_length(new_column_count)
|
|
362
|
+
index = if new_column_count <= 1
|
|
363
|
+
nil
|
|
364
|
+
elsif @range_index
|
|
365
|
+
range_index = @range_index
|
|
366
|
+
updates.each do |row, edits|
|
|
367
|
+
edits.each do |column, value|
|
|
368
|
+
old_value = self[row, column]
|
|
369
|
+
if old_value != value
|
|
370
|
+
range_index = range_index.update(row_axis.index_at(row), column_axis.index_at(column), old_value, value)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
range_index
|
|
375
|
+
else
|
|
376
|
+
RangeIndex.from_rows(rows, row_axis, column_axis)
|
|
377
|
+
end
|
|
378
|
+
with_rows(rows, new_column_count, index, row_axis, column_axis)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def delete(row, column)
|
|
382
|
+
validate_coordinate(row, column)
|
|
383
|
+
return self if row >= row_count || column >= column_count
|
|
384
|
+
before, tail = split_axis(@rows, row, :row)
|
|
385
|
+
current, after = split_axis(tail, 1, :row)
|
|
386
|
+
item = current[0]
|
|
387
|
+
return self unless item.is_a?(Row)
|
|
388
|
+
columns = delete_column(item.columns, column)
|
|
389
|
+
return self if columns.equal?(item.columns)
|
|
390
|
+
replacement = columns.summary.values.count.zero? ? Gap.new(1, axis: :row) : Row.new(columns)
|
|
391
|
+
rows = concat(concat(before, tree([replacement], RowSummary)), after)
|
|
392
|
+
index = if column_count <= 1
|
|
393
|
+
nil
|
|
394
|
+
elsif @range_index
|
|
395
|
+
@range_index.update(@row_axis.index_at(row), @column_axis.index_at(column), self[row, column], nil)
|
|
396
|
+
else
|
|
397
|
+
RangeIndex.from_rows(rows, @row_axis, @column_axis)
|
|
398
|
+
end
|
|
399
|
+
with_rows(rows, column_count, index)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def each_in(top, left, bottom, right)
|
|
403
|
+
return enum_for(__method__, top, left, bottom, right) unless block_given?
|
|
404
|
+
validate_range(top, left, bottom, right)
|
|
405
|
+
return self if top >= row_count || left >= column_count
|
|
406
|
+
_, tail = split_axis(@rows, top, :row)
|
|
407
|
+
rows, = split_axis(tail, [bottom + 1 - top, row_count - top].min, :row)
|
|
408
|
+
row = top
|
|
409
|
+
rows.each do |item|
|
|
410
|
+
if item.is_a?(Gap)
|
|
411
|
+
row += item.length
|
|
412
|
+
next
|
|
413
|
+
end
|
|
414
|
+
each_column(item.columns, left, right) { |column, value| yield Point.new(row, column), value }
|
|
415
|
+
row += 1
|
|
416
|
+
end
|
|
417
|
+
self
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def summary(top, left, bottom, right)
|
|
421
|
+
validate_range(top, left, bottom, right)
|
|
422
|
+
return Summary.zero if top >= row_count || left >= column_count
|
|
423
|
+
_, tail = split_axis(@rows, top, :row)
|
|
424
|
+
rows, = split_axis(tail, [bottom + 1 - top, row_count - top].min, :row)
|
|
425
|
+
return rows.summary.values if left.zero? && right >= column_count - 1
|
|
426
|
+
raise "missing 2D range index for partial summary" unless @range_index
|
|
427
|
+
row_ranges = @row_axis.ranges(top, bottom)
|
|
428
|
+
column_ranges = @column_axis.ranges(left, right)
|
|
429
|
+
row_ranges.sum(Summary.zero) do |row_first, row_last|
|
|
430
|
+
column_ranges.sum(Summary.zero) do |column_first, column_last|
|
|
431
|
+
@range_index.summary(row_first, column_first, row_last, column_last)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def insert_rows(at, count)
|
|
437
|
+
validate_insert(at, count, row_count)
|
|
438
|
+
return self if count.zero?
|
|
439
|
+
before, after = split_axis(@rows, at, :row)
|
|
440
|
+
inserted = tree([Gap.new(count, axis: :row)], RowSummary)
|
|
441
|
+
rows = concat(concat(before, inserted), after)
|
|
442
|
+
row_axis = @row_axis.insert(at, count)
|
|
443
|
+
index = @range_index || RangeIndex.new if column_count > 1
|
|
444
|
+
with_rows(rows, column_count, index, row_axis)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def delete_rows(at, count)
|
|
448
|
+
validate_delete(at, count, row_count)
|
|
449
|
+
return self if count.zero? || at == row_count
|
|
450
|
+
before, tail = split_axis(@rows, at, :row)
|
|
451
|
+
removed_count = [count, row_count - at].min
|
|
452
|
+
removed, after = split_axis(tail, removed_count, :row)
|
|
453
|
+
rows = concat(before, after)
|
|
454
|
+
index = @range_index
|
|
455
|
+
if index && column_count > 1
|
|
456
|
+
row = at
|
|
457
|
+
removed.each do |item|
|
|
458
|
+
if item.is_a?(Gap)
|
|
459
|
+
row += item.length
|
|
460
|
+
next
|
|
461
|
+
end
|
|
462
|
+
row_index = @row_axis.index_at(row)
|
|
463
|
+
column = 0
|
|
464
|
+
item.columns.each do |cell|
|
|
465
|
+
if cell.is_a?(Cell)
|
|
466
|
+
index = index.update(row_index, @column_axis.index_at(column), cell.value, nil)
|
|
467
|
+
column += 1
|
|
468
|
+
else
|
|
469
|
+
column += cell.length
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
row += 1
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
row_axis = @row_axis.delete(at, count)
|
|
476
|
+
with_rows(rows, column_count, index, row_axis)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def insert_columns(at, count)
|
|
480
|
+
validate_insert(at, count, column_count)
|
|
481
|
+
return self if count.zero?
|
|
482
|
+
rows = []
|
|
483
|
+
@rows.each do |item|
|
|
484
|
+
if item.is_a?(Row) && at < item.columns.summary.column_count
|
|
485
|
+
rows << Row.new(insert_axis(item.columns, at, count, :column))
|
|
486
|
+
else
|
|
487
|
+
rows << item
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
rows = tree(coalesce_gaps(rows, :row), RowSummary)
|
|
491
|
+
new_column_count = column_count + count
|
|
492
|
+
column_axis = @column_axis.insert(at, count)
|
|
493
|
+
index = @range_index || RangeIndex.new if new_column_count > 1
|
|
494
|
+
with_rows(rows, new_column_count, index, @row_axis, column_axis)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def delete_columns(at, count)
|
|
498
|
+
validate_delete(at, count, column_count)
|
|
499
|
+
return self if count.zero? || at == column_count
|
|
500
|
+
rows = []
|
|
501
|
+
index = @range_index
|
|
502
|
+
delete_count = [count, column_count - at].min
|
|
503
|
+
row = 0
|
|
504
|
+
@rows.each do |item|
|
|
505
|
+
if item.is_a?(Gap)
|
|
506
|
+
rows << item
|
|
507
|
+
row += item.length
|
|
508
|
+
next
|
|
509
|
+
end
|
|
510
|
+
if index && column_count - delete_count > 1
|
|
511
|
+
each_column(item.columns, at, at + delete_count - 1) do |column, value|
|
|
512
|
+
index = index.update(@row_axis.index_at(row), @column_axis.index_at(column), value, nil)
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
if item.is_a?(Row) && at < item.columns.summary.column_count
|
|
516
|
+
columns = delete_axis(item.columns, at, [count, item.columns.summary.column_count - at].min, :column)
|
|
517
|
+
rows << (columns.summary.values.count.zero? ? Gap.new(1, axis: :row) : Row.new(columns))
|
|
518
|
+
else
|
|
519
|
+
rows << item
|
|
520
|
+
end
|
|
521
|
+
row += 1
|
|
522
|
+
end
|
|
523
|
+
rows = tree(coalesce_gaps(rows, :row), RowSummary)
|
|
524
|
+
new_column_count = column_count - delete_count
|
|
525
|
+
column_axis = @column_axis.delete(at, count)
|
|
526
|
+
index = nil if new_column_count <= 1
|
|
527
|
+
index ||= RangeIndex.new if new_column_count > 1
|
|
528
|
+
with_rows(rows, new_column_count, index, @row_axis, column_axis)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def snapshot = self
|
|
532
|
+
|
|
533
|
+
def check_invariants!
|
|
534
|
+
@rows.check_invariants!
|
|
535
|
+
@row_axis.check_invariants!
|
|
536
|
+
@column_axis.check_invariants!
|
|
537
|
+
raise "incorrect row-axis extent" unless @row_axis.extent == row_count
|
|
538
|
+
raise "incorrect column-axis extent" unless @column_axis.extent == column_count
|
|
539
|
+
if column_count > 1
|
|
540
|
+
raise "missing 2D range index" unless @range_index
|
|
541
|
+
@range_index.check_invariants!
|
|
542
|
+
range_summary = indexed_summary(0, 0, row_count - 1, column_count - 1)
|
|
543
|
+
raise "incorrect 2D range-index summary" unless range_summary == @rows.summary.values
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
actual_rows = 0
|
|
547
|
+
actual_cells = 0
|
|
548
|
+
previous_gap = false
|
|
549
|
+
@rows.each do |item|
|
|
550
|
+
raise "adjacent row gaps" if previous_gap && item.is_a?(Gap)
|
|
551
|
+
if item.is_a?(Gap)
|
|
552
|
+
actual_rows += item.length
|
|
553
|
+
previous_gap = true
|
|
554
|
+
else
|
|
555
|
+
raise "invalid row item" unless item.is_a?(Row)
|
|
556
|
+
item.columns.check_invariants!
|
|
557
|
+
actual_rows += 1
|
|
558
|
+
actual_cells += item.columns.summary.values.count
|
|
559
|
+
previous_gap = false
|
|
560
|
+
previous_column_gap = false
|
|
561
|
+
item.columns.each do |cell|
|
|
562
|
+
raise "adjacent column gaps" if previous_column_gap && cell.is_a?(Gap)
|
|
563
|
+
raise "invalid column item" unless cell.is_a?(Gap) || cell.is_a?(Cell)
|
|
564
|
+
previous_column_gap = cell.is_a?(Gap)
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
raise "incorrect row count" unless actual_rows == row_count
|
|
569
|
+
raise "incorrect cell count" unless actual_cells == cell_count
|
|
570
|
+
raise "invalid column count" unless column_count >= 0
|
|
571
|
+
true
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
private
|
|
575
|
+
|
|
576
|
+
def validate_coordinate(row, column)
|
|
577
|
+
raise ArgumentError, "row and column must be non-negative integers" unless row.is_a?(Integer) && row >= 0 && column.is_a?(Integer) && column >= 0
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def validate_range(top, left, bottom, right)
|
|
581
|
+
validate_coordinate(top, left)
|
|
582
|
+
validate_coordinate(bottom, right)
|
|
583
|
+
raise ArgumentError, "range end precedes range start" if bottom < top || right < left
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def validate_insert(at, count, extent)
|
|
587
|
+
raise ArgumentError, "insert position must be within the sheet" unless at.is_a?(Integer) && at.between?(0, extent)
|
|
588
|
+
raise ArgumentError, "count must be a non-negative integer" unless count.is_a?(Integer) && count >= 0
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def validate_delete(at, count, extent)
|
|
592
|
+
raise ArgumentError, "delete position must be within the sheet" unless at.is_a?(Integer) && at.between?(0, extent)
|
|
593
|
+
raise ArgumentError, "count must be a non-negative integer" unless count.is_a?(Integer) && count >= 0
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def empty_columns = Tree.new(summary: ColumnSummary, branching: @branching)
|
|
597
|
+
def empty_tree(summary) = Tree.new(summary: summary, branching: @branching)
|
|
598
|
+
def tree(items, summary) = Tree.new(items, summary: summary, branching: @branching)
|
|
599
|
+
def with_rows(rows, columns, range_index = nil, row_axis = @row_axis, column_axis = @column_axis)
|
|
600
|
+
self.class.__send__(:from_rows, rows, columns, @branching, range_index, row_axis, column_axis)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def indexed_summary(top, left, bottom, right)
|
|
604
|
+
return Summary.zero if top > bottom || left > right
|
|
605
|
+
|
|
606
|
+
row_ranges = @row_axis.ranges(top, bottom)
|
|
607
|
+
column_ranges = @column_axis.ranges(left, right)
|
|
608
|
+
row_ranges.sum(Summary.zero) do |row_first, row_last|
|
|
609
|
+
column_ranges.sum(Summary.zero) do |column_first, column_last|
|
|
610
|
+
@range_index.summary(row_first, column_first, row_last, column_last)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def split_axis(source, position, axis)
|
|
616
|
+
total = axis == :row ? source.summary.row_count : source.summary.column_count
|
|
617
|
+
raise RangeError, "axis position out of bounds" unless position.is_a?(Integer) && position.between?(0, total)
|
|
618
|
+
summary_type = axis == :row ? RowSummary : ColumnSummary
|
|
619
|
+
return [source, empty_tree(summary_type)] if position == total
|
|
620
|
+
index, item, prefix = source.locate(position, axis == :row ? :row_count : :column_count)
|
|
621
|
+
local = position - (axis == :row ? prefix.row_count : prefix.column_count)
|
|
622
|
+
return source.split_at(index) if local.zero?
|
|
623
|
+
before, after = source.split_at(index)
|
|
624
|
+
left_length = item.length
|
|
625
|
+
left = before.push(Gap.new(local, axis: axis))
|
|
626
|
+
right = tree([Gap.new(left_length - local, axis: axis)], summary_type).append(after.slice(1, after.size - 1))
|
|
627
|
+
[left, right]
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def concat(left, right)
|
|
631
|
+
return right if left.empty?
|
|
632
|
+
return left if right.empty?
|
|
633
|
+
last = left[left.size - 1]
|
|
634
|
+
first = right[0]
|
|
635
|
+
return left.append(right) unless last.is_a?(Gap) && first.is_a?(Gap)
|
|
636
|
+
left = left.replace_at(left.size - 1, [Gap.new(last.length + first.length, axis: last_axis(last))])
|
|
637
|
+
left.append(right.slice(1, right.size - 1))
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def last_axis(gap)
|
|
641
|
+
gap.summary.is_a?(RowSummary) ? :row : :column
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def insert_axis(source, at, count, axis)
|
|
645
|
+
before, after = split_axis(source, at, axis)
|
|
646
|
+
summary_type = axis == :row ? RowSummary : ColumnSummary
|
|
647
|
+
concat(concat(before, tree([Gap.new(count, axis: axis)], summary_type)), after)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def delete_axis(source, at, count, axis)
|
|
651
|
+
before, tail = split_axis(source, at, axis)
|
|
652
|
+
_, after = split_axis(tail, [count, (axis == :row ? tail.summary.row_count : tail.summary.column_count)].min, axis)
|
|
653
|
+
concat(before, after)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def set_column(columns, column, value)
|
|
657
|
+
columns = insert_axis(columns, columns.summary.column_count, column + 1 - columns.summary.column_count, :column) if column >= columns.summary.column_count
|
|
658
|
+
before, tail = split_axis(columns, column, :column)
|
|
659
|
+
_current, after = split_axis(tail, 1, :column)
|
|
660
|
+
concat(concat(before, tree([Cell.new(value)], ColumnSummary)), after)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def delete_column(columns, column)
|
|
664
|
+
return columns if column >= columns.summary.column_count
|
|
665
|
+
before, tail = split_axis(columns, column, :column)
|
|
666
|
+
current, after = split_axis(tail, 1, :column)
|
|
667
|
+
return columns unless current[0].is_a?(Cell)
|
|
668
|
+
concat(concat(before, tree([Gap.new(1, axis: :column)], ColumnSummary)), after)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def column_at(columns, column)
|
|
672
|
+
return nil if column >= columns.summary.column_count
|
|
673
|
+
_, item, prefix = columns.locate(column, :column_count)
|
|
674
|
+
item.is_a?(Cell) && column == prefix.column_count ? item.value : nil
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def updated_row(row, edits)
|
|
678
|
+
columns = row ? row.columns : empty_columns
|
|
679
|
+
updated = update_columns(columns, edits)
|
|
680
|
+
return row if row && updated.equal?(columns)
|
|
681
|
+
return nil if updated.summary.values.count.zero?
|
|
682
|
+
|
|
683
|
+
Row.new(updated)
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def update_columns(columns, edits)
|
|
687
|
+
return columns if edits.empty?
|
|
688
|
+
|
|
689
|
+
extent = [columns.summary.column_count, *edits.filter_map { |column, value| column + 1 unless value.nil? }].max
|
|
690
|
+
result = []
|
|
691
|
+
changed = false
|
|
692
|
+
position = 0
|
|
693
|
+
edit_index = 0
|
|
694
|
+
|
|
695
|
+
columns.each do |item|
|
|
696
|
+
if item.is_a?(Gap)
|
|
697
|
+
finish = position + item.length
|
|
698
|
+
while edit_index < edits.length && edits[edit_index][0] < finish
|
|
699
|
+
column, value = edits[edit_index]
|
|
700
|
+
if value.nil?
|
|
701
|
+
append_gap(result, column - position + 1, :column) if column >= position
|
|
702
|
+
position = column + 1
|
|
703
|
+
else
|
|
704
|
+
append_gap(result, column - position, :column) if column > position
|
|
705
|
+
result << Cell.new(value)
|
|
706
|
+
position = column + 1
|
|
707
|
+
changed = true
|
|
708
|
+
end
|
|
709
|
+
edit_index += 1
|
|
710
|
+
end
|
|
711
|
+
append_gap(result, finish - position, :column) if finish > position
|
|
712
|
+
position = finish
|
|
713
|
+
else
|
|
714
|
+
if edit_index < edits.length && edits[edit_index][0] == position
|
|
715
|
+
value = edits[edit_index][1]
|
|
716
|
+
if value.nil?
|
|
717
|
+
append_gap(result, 1, :column)
|
|
718
|
+
changed = true
|
|
719
|
+
elsif value == item.value
|
|
720
|
+
result << item
|
|
721
|
+
else
|
|
722
|
+
result << Cell.new(value)
|
|
723
|
+
changed = true
|
|
724
|
+
end
|
|
725
|
+
edit_index += 1
|
|
726
|
+
else
|
|
727
|
+
result << item
|
|
728
|
+
end
|
|
729
|
+
position += 1
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
while edit_index < edits.length
|
|
734
|
+
column, value = edits[edit_index]
|
|
735
|
+
if !value.nil?
|
|
736
|
+
append_gap(result, column - position, :column) if column > position
|
|
737
|
+
result << Cell.new(value)
|
|
738
|
+
position = column + 1
|
|
739
|
+
changed = true
|
|
740
|
+
end
|
|
741
|
+
edit_index += 1
|
|
742
|
+
end
|
|
743
|
+
append_gap(result, extent - position, :column) if extent > position
|
|
744
|
+
return columns unless changed
|
|
745
|
+
|
|
746
|
+
tree(result, ColumnSummary)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def append_row(items, row)
|
|
750
|
+
append_gap(items, 1) unless row
|
|
751
|
+
items << row if row
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def append_gap(items, length, axis = :row)
|
|
755
|
+
return if length.zero?
|
|
756
|
+
|
|
757
|
+
if items.last.is_a?(Gap)
|
|
758
|
+
items[-1] = Gap.new(items.last.length + length, axis: axis)
|
|
759
|
+
else
|
|
760
|
+
items << Gap.new(length, axis: axis)
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def each_column(columns, left, right)
|
|
765
|
+
return if left >= columns.summary.column_count
|
|
766
|
+
_, tail = split_axis(columns, left, :column)
|
|
767
|
+
selected, = split_axis(tail, [right + 1 - left, columns.summary.column_count - left].min, :column)
|
|
768
|
+
column = left
|
|
769
|
+
selected.each do |item|
|
|
770
|
+
if item.is_a?(Gap)
|
|
771
|
+
column += item.length
|
|
772
|
+
else
|
|
773
|
+
yield column, item.value
|
|
774
|
+
column += 1
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def column_summary(columns, left, right)
|
|
780
|
+
return Summary.zero if left >= columns.summary.column_count
|
|
781
|
+
_, tail = split_axis(columns, left, :column)
|
|
782
|
+
selected, = split_axis(tail, [right + 1 - left, columns.summary.column_count - left].min, :column)
|
|
783
|
+
selected.summary.values
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def coalesce_gaps(items, axis)
|
|
787
|
+
items.each_with_object([]) do |item, result|
|
|
788
|
+
if item.is_a?(Gap) && result.last.is_a?(Gap)
|
|
789
|
+
result[-1] = Gap.new(result.last.length + item.length, axis: axis)
|
|
790
|
+
else
|
|
791
|
+
result << item
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
end
|