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.
@@ -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