roo-oxide 0.0.2

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,894 @@
1
+ //! Keeps a worksheet's raw records and builds the roo cell for one only when
2
+ //! it is read, for `Roo::Excelx#cell` and the other single-cell lookups.
3
+
4
+ use std::ops::Range;
5
+
6
+ mod formatters;
7
+
8
+ use magnus::gc::Marker;
9
+ use magnus::rb_sys::AsRawValue;
10
+ use magnus::typed_data::Obj;
11
+ use magnus::value::{LazyId, Opaque, ReprValue};
12
+ use magnus::{
13
+ prelude::*, DataTypeFunctions, Error, Integer, RArray, RHash, RString, Ruby, TypedData, Value,
14
+ };
15
+
16
+ use crate::archive::Source;
17
+ use crate::convert::{Converter, CsvOutput};
18
+ use crate::reader::{Body, Kind, RawCell, RawRow, Reference, Style};
19
+ use crate::Reader;
20
+
21
+ type Span = (u32, u32);
22
+ static VALUE: LazyId = LazyId::new("value");
23
+ static TYPE: LazyId = LazyId::new("type");
24
+
25
+ fn range((start, end): Span) -> Range<usize> {
26
+ start as usize..end as usize
27
+ }
28
+
29
+ #[derive(Clone, Copy, PartialEq, Eq)]
30
+ enum Content {
31
+ Empty,
32
+ Inline,
33
+ Value,
34
+ }
35
+
36
+ /// A record roo's `cells` keeps a cell for, as [`RawCell`] holds it but
37
+ /// without its heap parts, in 20 bytes: the coordinate is always known, the
38
+ /// style is an index, `text` is the span of an `Inline` or `Value` content in
39
+ /// [`Store::text`], and a record with a formula has its span in
40
+ /// [`Store::formulas`].
41
+ #[derive(Clone, Copy)]
42
+ struct Record {
43
+ row: u32,
44
+ text: Span,
45
+ column: u16,
46
+ style: u16,
47
+ kind: Kind,
48
+ content: Content,
49
+ formula: bool,
50
+ }
51
+
52
+ const _: () = assert!(size_of::<Record>() == 20);
53
+
54
+ impl Record {
55
+ /// The record and the span of its formula, or `None` for a record that
56
+ /// needs a heap part, or Ruby, to read, or does not fit.
57
+ fn new(raw: RawCell, row: i64, column: i64, offset: usize) -> Option<(Self, Option<Span>)> {
58
+ let span = |range: Range<usize>| -> Option<Span> {
59
+ Some((
60
+ u32::try_from(range.start + offset).ok()?,
61
+ u32::try_from(range.end + offset).ok()?,
62
+ ))
63
+ };
64
+ let (content, text) = match raw.body {
65
+ Body::Empty => (Content::Empty, (0, 0)),
66
+ Body::Inline(text) => (Content::Inline, span(text)?),
67
+ Body::Value(text) => (Content::Value, span(text)?),
68
+ };
69
+ let formula = match raw.formula {
70
+ Some(formula) => Some(span(formula)?),
71
+ None => None,
72
+ };
73
+ let record = Self {
74
+ row: u32::try_from(row).ok()?,
75
+ text,
76
+ column: u16::try_from(column).ok()?,
77
+ style: match raw.style {
78
+ Style::Index(index) => u16::try_from(index).ok()?,
79
+ Style::Other(_) => return None,
80
+ },
81
+ kind: raw.kind,
82
+ content,
83
+ formula: formula.is_some(),
84
+ };
85
+ Some((record, formula))
86
+ }
87
+
88
+ /// The cell as [`RawCell`] holds it, with `formula` the span of its
89
+ /// formula when it has one.
90
+ fn raw(&self, formula: Option<Span>) -> RawCell {
91
+ RawCell {
92
+ reference: Reference::At {
93
+ row: self.row.into(),
94
+ column: self.column.into(),
95
+ },
96
+ style: Style::Index(self.style.into()),
97
+ kind: self.kind,
98
+ formula: formula.map(range),
99
+ body: match self.content {
100
+ Content::Empty => Body::Empty,
101
+ Content::Inline => Body::Inline(range(self.text)),
102
+ Content::Value => Body::Value(range(self.text)),
103
+ },
104
+ }
105
+ }
106
+
107
+ fn key(&self) -> (u32, u32) {
108
+ (self.row, self.column.into())
109
+ }
110
+
111
+ fn built_key(&self) -> u64 {
112
+ u64::from(self.row) << 32 | u64::from(self.column)
113
+ }
114
+
115
+ fn shared_string(&self) -> bool {
116
+ self.kind == Kind::Shared && self.content == Content::Value
117
+ }
118
+ }
119
+
120
+ /// The span of the formula of each record that has one, by record, sorted.
121
+ #[derive(Default)]
122
+ struct Formulas(Vec<(u32, Span)>);
123
+
124
+ impl Formulas {
125
+ /// [`Record::raw`] for the record at `index` of `records`.
126
+ fn raw(&self, records: &[Record], index: u32) -> RawCell {
127
+ let record = &records[index as usize];
128
+ record.raw(record.formula.then(|| {
129
+ let i = self
130
+ .0
131
+ .binary_search_by_key(&index, |&(i, _)| i)
132
+ .expect("a record with a formula has its span");
133
+ self.0[i].1
134
+ }))
135
+ }
136
+ }
137
+
138
+ /// The records of a worksheet read in document mode, the way
139
+ /// `Roo::Excelx::SheetDoc#extract_cells` reads it, when every cell can be
140
+ /// built on its own: none raises, so roo would not raise for the sheet.
141
+ #[derive(TypedData)]
142
+ #[magnus(class = "Roo::Oxide::Native::Store", mark, free_immediately, size)]
143
+ pub struct Store {
144
+ converter: Opaque<Obj<Converter>>,
145
+ text: String,
146
+ /// In document order.
147
+ records: Vec<Record>,
148
+ formulas: Formulas,
149
+ /// For each key of roo's `cells` hash, the record whose cell it holds
150
+ /// (the last one with that key), sorted by key.
151
+ index: Vec<u32>,
152
+ /// The cells built so far, by [`key`].
153
+ built: Opaque<RHash>,
154
+ /// By record, the values [`Store::value`] handed out before their cell
155
+ /// was built, which that cell then holds: `nil`, or an object that is
156
+ /// not a shared string (the converter picks the same one again).
157
+ handed_out: Opaque<RArray>,
158
+ /// Sanitized values, separate from the original values roo's cells retain.
159
+ cleaned: Opaque<RArray>,
160
+ /// The keys more than one record has, sorted.
161
+ repeated: Vec<u64>,
162
+ }
163
+
164
+ impl DataTypeFunctions for Store {
165
+ fn mark(&self, marker: &Marker) {
166
+ marker.mark(self.converter);
167
+ marker.mark(self.built);
168
+ marker.mark(self.handed_out);
169
+ marker.mark(self.cleaned);
170
+ }
171
+
172
+ fn size(&self) -> usize {
173
+ size_of::<Self>()
174
+ + self.text.capacity()
175
+ + self.records.capacity() * size_of::<Record>()
176
+ + self.formulas.0.capacity() * size_of::<(u32, Span)>()
177
+ + self.index.capacity() * size_of::<u32>()
178
+ + self.repeated.capacity() * size_of::<u64>()
179
+ }
180
+ }
181
+
182
+ struct Records {
183
+ text: String,
184
+ records: Vec<Record>,
185
+ formulas: Formulas,
186
+ }
187
+
188
+ impl Records {
189
+ /// `None` once a record does not fit in a [`Record`].
190
+ fn add(&mut self, row: RawRow, row_number: usize, empty_cell: bool) -> Option<()> {
191
+ let offset = self.text.len();
192
+ self.text.push_str(&row.text);
193
+ for (index, raw) in row.cells.into_iter().enumerate() {
194
+ let (row, column) = raw.position(row_number, index + 1)?;
195
+ if raw.body != Body::Empty || empty_cell {
196
+ let (record, formula) = Record::new(raw, row, column, offset)?;
197
+ if let Some(formula) = formula {
198
+ let index = u32::try_from(self.records.len()).ok()?;
199
+ self.formulas.0.push((index, formula));
200
+ }
201
+ self.records.push(record);
202
+ }
203
+ }
204
+ Some(())
205
+ }
206
+ }
207
+
208
+ /// Cells parsed ahead while Ruby-dependent checks hold the GVL.
209
+ const BATCH_CELLS: usize = 16_384;
210
+
211
+ /// Reads every row with the GVL released, in batches. `None` as soon as a
212
+ /// record does not fit in a [`Record`], or `converter` might not build it
213
+ /// safely.
214
+ fn read(ruby: &Ruby, source: Source, converter: &Converter) -> Result<Option<Records>, Error> {
215
+ let reader = Reader::new(ruby, source, false, BATCH_CELLS)?;
216
+ let mut records = Records {
217
+ text: String::new(),
218
+ records: Vec::new(),
219
+ formulas: Formulas::default(),
220
+ };
221
+ let empty_cell = converter.empty_cell();
222
+ let mut rows = 0;
223
+ let mut checked: u32 = 0;
224
+ loop {
225
+ let batch = reader.read_batch(ruby)?;
226
+ if batch.is_empty() {
227
+ break;
228
+ }
229
+ for row in batch {
230
+ rows += 1;
231
+ if records.add(row, rows, empty_cell).is_none() {
232
+ return Ok(None);
233
+ }
234
+ }
235
+ let Ok(end) = u32::try_from(records.records.len()) else {
236
+ return Ok(None);
237
+ };
238
+ if !(checked..end).all(|i| {
239
+ let raw = records.formulas.raw(&records.records, i);
240
+ converter.builds_safely(ruby, &records.text, &raw)
241
+ }) {
242
+ return Ok(None);
243
+ }
244
+ checked = end;
245
+ }
246
+ records.text.shrink_to_fit();
247
+ records.records.shrink_to_fit();
248
+ records.formulas.0.shrink_to_fit();
249
+ Ok(Some(records))
250
+ }
251
+
252
+ impl Store {
253
+ /// The store for the worksheet `source`, or `nil` when its cells have to
254
+ /// be built all at once, in order. `context` is a `Roo::Oxide::Context`.
255
+ fn read(ruby: &Ruby, source: Source, context: Value) -> Result<Option<Self>, Error> {
256
+ let converter = ruby.obj_wrap(Converter::new(ruby, context, false)?);
257
+ let Some(Records {
258
+ text,
259
+ records,
260
+ formulas,
261
+ }) = read(ruby, source, &converter)?
262
+ else {
263
+ return Ok(None);
264
+ };
265
+ let Ok(count) = u32::try_from(records.len()) else {
266
+ return Ok(None);
267
+ };
268
+ let mut index: Vec<u32> = (0..count).collect();
269
+ let mut repeated = Vec::new();
270
+ // Stable, so records with the same key stay in document order, and the
271
+ // last one is kept, as a Hash keeps the last value stored for a key.
272
+ index.sort_by_key(|&i| records[i as usize].key());
273
+ index.dedup_by(|later, kept| {
274
+ let same = records[*later as usize].key() == records[*kept as usize].key();
275
+ if same {
276
+ let key = records[*kept as usize].built_key();
277
+ if repeated.last() != Some(&key) {
278
+ repeated.push(key);
279
+ }
280
+ *kept = *later;
281
+ }
282
+ same
283
+ });
284
+ index.shrink_to_fit();
285
+ let store = Self {
286
+ converter: converter.into(),
287
+ text,
288
+ records,
289
+ formulas,
290
+ index,
291
+ built: ruby.hash_new().into(),
292
+ handed_out: ruby.ary_new().into(),
293
+ cleaned: ruby.ary_new().into(),
294
+ repeated,
295
+ };
296
+ // `Roo::Link.new` copies a linked cell's shared string, which roo does
297
+ // before a caller can change that string in place.
298
+ for &i in &store.index {
299
+ let record = &store.records[i as usize];
300
+ if record.shared_string()
301
+ && converter.has_link(ruby, record.row.into(), record.column.into())
302
+ {
303
+ store.build(ruby, i)?;
304
+ }
305
+ }
306
+ Ok(Some(store))
307
+ }
308
+
309
+ fn build(&self, ruby: &Ruby, record: u32) -> Result<Value, Error> {
310
+ let built = ruby.get_inner(self.built);
311
+ let key = self.records[record as usize].built_key();
312
+ if let Some(cell) = built.get(key) {
313
+ return Ok(cell);
314
+ }
315
+ let handed_out: Value = ruby.get_inner(self.handed_out).entry(record as isize)?;
316
+ let known = (!handed_out.is_nil()).then_some(handed_out);
317
+ let raw = self.formulas.raw(&self.records, record);
318
+ let (_, cell) = ruby
319
+ .get_inner(self.converter)
320
+ .cell(ruby, &self.text, &raw, 0, 0, known)?
321
+ .expect("the store holds only records roo keeps");
322
+ self.apply_cleaned_value(ruby, record, cell)?;
323
+ built.aset(key, cell)?;
324
+ Ok(cell)
325
+ }
326
+
327
+ /// `cells[key]&.value` for the key of `record`, from its cell when it is
328
+ /// built, else as that cell will hold it.
329
+ fn value(&self, ruby: &Ruby, record: u32) -> Result<Value, Error> {
330
+ let raw = &self.records[record as usize];
331
+ let built = ruby.get_inner(self.built);
332
+ if !built.is_empty() {
333
+ if let Some(cell) = built.get(raw.built_key()) {
334
+ return cell.funcall(*VALUE, ());
335
+ }
336
+ }
337
+ let cleaned: Value = ruby.get_inner(self.cleaned).entry(record as isize)?;
338
+ if !cleaned.is_nil() {
339
+ return Ok(cleaned);
340
+ }
341
+ let handed_out = ruby.get_inner(self.handed_out);
342
+ let kept: Value = handed_out.entry(record as isize)?;
343
+ if !kept.is_nil() {
344
+ return Ok(kept);
345
+ }
346
+ let converter = ruby.get_inner(self.converter);
347
+ let cell = self.formulas.raw(&self.records, record);
348
+ let Some(value) = converter.known_value(ruby, &self.text, &cell)? else {
349
+ return self.build(ruby, record)?.funcall(*VALUE, ());
350
+ };
351
+ if !rb_sys::SPECIAL_CONST_P(value.as_raw()) && !raw.shared_string() {
352
+ handed_out.store(record as isize, value)?;
353
+ }
354
+ Ok(value)
355
+ }
356
+
357
+ fn apply_cleaned_value(&self, ruby: &Ruby, record: u32, cell: Value) -> Result<(), Error> {
358
+ let value: Value = ruby.get_inner(self.cleaned).entry(record as isize)?;
359
+ if !value.is_nil() {
360
+ cell.funcall::<_, _, Value>("value=", (value,))?;
361
+ }
362
+ Ok(())
363
+ }
364
+
365
+ fn clean(ruby: &Ruby, rb_self: &Self, book: Value) -> Result<(), Error> {
366
+ for (record, raw) in (0..).zip(&rb_self.records) {
367
+ if rb_self.repeated.binary_search(&raw.built_key()).is_ok()
368
+ && rb_self.position(raw.key()).map(|i| rb_self.index[i]) != Ok(record)
369
+ {
370
+ continue;
371
+ }
372
+ let value = rb_self.value(ruby, record)?;
373
+ if RString::from_value(value).is_none() {
374
+ continue;
375
+ }
376
+ let value: Value = book.funcall("sanitize_value", (value,))?;
377
+ if let Some(cell) = ruby.get_inner(rb_self.built).get(raw.built_key()) {
378
+ cell.funcall::<_, _, Value>("value=", (value,))?;
379
+ } else {
380
+ ruby.get_inner(rb_self.cleaned)
381
+ .store(record as isize, value)?;
382
+ }
383
+ }
384
+ Ok(())
385
+ }
386
+
387
+ /// The position in [`Store::index`] of the record for `key`.
388
+ fn position(&self, key: (u32, u32)) -> Result<usize, usize> {
389
+ self.index
390
+ .binary_search_by_key(&key, |&i| self.records[i as usize].key())
391
+ }
392
+
393
+ /// The first position from `start` on in [`Store::index`] whose key is
394
+ /// `key` or after it, searched in windows that double from `start`.
395
+ fn position_from(&self, start: usize, key: (u32, u32)) -> usize {
396
+ let before = |&i: &u32| self.records[i as usize].key() < key;
397
+ let rest = &self.index[start..];
398
+ let mut bound = 1;
399
+ while bound < rest.len() && before(&rest[bound]) {
400
+ bound *= 2;
401
+ }
402
+ let from = bound / 2;
403
+ start + from + rest[from..rest.len().min(bound)].partition_point(before)
404
+ }
405
+
406
+ /// `cells` as `SheetDoc#extract_cells` builds it, holding the cells
407
+ /// built so far and the values handed out. Of the records with a
408
+ /// repeated key, all but the one [`Store::index`] keeps get a cell of
409
+ /// their own, as roo builds one for each.
410
+ fn cells(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
411
+ let cells = ruby.hash_new_capa(rb_self.index.len());
412
+ let built = ruby.get_inner(rb_self.built);
413
+ let handed_out = ruby.get_inner(rb_self.handed_out);
414
+ let converter = ruby.get_inner(rb_self.converter);
415
+ for (record, raw) in (0..).zip(&rb_self.records) {
416
+ let key = raw.built_key();
417
+ let kept = rb_self.repeated.binary_search(&key).is_err()
418
+ || rb_self.position(raw.key()).map(|i| rb_self.index[i]) == Ok(record);
419
+ if let Some(cell) = built.get(key).filter(|_| kept) {
420
+ cells.aset(cell.funcall::<_, _, Value>("coordinate", ())?, cell)?;
421
+ continue;
422
+ }
423
+ let value: Value = handed_out.entry(record as isize)?;
424
+ let known = (kept && !value.is_nil()).then_some(value);
425
+ let (coordinate, cell) = converter
426
+ .cell(
427
+ ruby,
428
+ &rb_self.text,
429
+ &rb_self.formulas.raw(&rb_self.records, record),
430
+ 0,
431
+ 0,
432
+ known,
433
+ )?
434
+ .expect("the store holds only records roo keeps");
435
+ if kept {
436
+ rb_self.apply_cleaned_value(ruby, record, cell)?;
437
+ }
438
+ cells.aset(coordinate, cell)?;
439
+ }
440
+ Ok(cells)
441
+ }
442
+
443
+ /// `cells[key]`.
444
+ fn get(ruby: &Ruby, rb_self: &Self, key: Value) -> Result<Option<Value>, Error> {
445
+ let Some(key) = RArray::from_value(key).filter(|key| key.len() == 2) else {
446
+ return Ok(None);
447
+ };
448
+ Self::cell_at(ruby, rb_self, key.entry(0)?, key.entry(1)?)
449
+ }
450
+
451
+ /// `cells[[row, column]]` without allocating the key.
452
+ fn cell_at(
453
+ ruby: &Ruby,
454
+ rb_self: &Self,
455
+ row: Value,
456
+ column: Value,
457
+ ) -> Result<Option<Value>, Error> {
458
+ match rb_self.record_at(row, column) {
459
+ Some(record) => rb_self.build(ruby, record).map(Some),
460
+ None => Ok(None),
461
+ }
462
+ }
463
+
464
+ fn record_at(&self, row: Value, column: Value) -> Option<u32> {
465
+ let part = |value| Integer::from_value(value).and_then(|n| n.to_u32().ok());
466
+ let (Some(row), Some(column)) = (part(row), part(column)) else {
467
+ return None;
468
+ };
469
+ self.position((row, column)).ok().map(|i| self.index[i])
470
+ }
471
+
472
+ fn value_at(
473
+ ruby: &Ruby,
474
+ rb_self: &Self,
475
+ row: Value,
476
+ column: Value,
477
+ ) -> Result<Option<Value>, Error> {
478
+ let Some(record) = rb_self.record_at(row, column) else {
479
+ return Ok(None);
480
+ };
481
+ let raw = &rb_self.records[record as usize];
482
+ let built = ruby.get_inner(rb_self.built);
483
+ if !built.is_empty() {
484
+ if let Some(cell) = built.get(raw.built_key()) {
485
+ return cell.funcall(*VALUE, ()).map(Some);
486
+ }
487
+ }
488
+ let cleaned: Value = ruby.get_inner(rb_self.cleaned).entry(record as isize)?;
489
+ if !cleaned.is_nil() {
490
+ return Ok(Some(cleaned));
491
+ }
492
+ let cell = rb_self.formulas.raw(&rb_self.records, record);
493
+ if let Some(value) =
494
+ ruby.get_inner(rb_self.converter)
495
+ .known_value(ruby, &rb_self.text, &cell)?
496
+ {
497
+ if rb_sys::SPECIAL_CONST_P(value.as_raw()) || raw.shared_string() {
498
+ return Ok(Some(value));
499
+ }
500
+ }
501
+ rb_self.build(ruby, record)?.funcall(*VALUE, ()).map(Some)
502
+ }
503
+
504
+ fn type_at(
505
+ ruby: &Ruby,
506
+ rb_self: &Self,
507
+ row: Value,
508
+ column: Value,
509
+ ) -> Result<Option<Value>, Error> {
510
+ let Some(record) = rb_self.record_at(row, column) else {
511
+ return Ok(None);
512
+ };
513
+ let built = ruby.get_inner(rb_self.built);
514
+ if !built.is_empty() {
515
+ if let Some(cell) = built.get(rb_self.records[record as usize].built_key()) {
516
+ return cell.funcall(*TYPE, ()).map(Some);
517
+ }
518
+ }
519
+ let raw = rb_self.formulas.raw(&rb_self.records, record);
520
+ match ruby
521
+ .get_inner(rb_self.converter)
522
+ .formatter_type(ruby, &raw)?
523
+ {
524
+ Some(kind) => Ok(Some(ruby.sym_new(**kind).as_value())),
525
+ None => rb_self.build(ruby, record)?.funcall(*TYPE, ()).map(Some),
526
+ }
527
+ }
528
+
529
+ /// `first_column.upto(last_column).map { |column| cells[[row, column]]&.value }`,
530
+ /// building only the cells whose value needs one.
531
+ fn row(
532
+ ruby: &Ruby,
533
+ rb_self: &Self,
534
+ row: Value,
535
+ first_column: i64,
536
+ last_column: i64,
537
+ ) -> Result<RArray, Error> {
538
+ let row = Integer::from_value(row).and_then(|n| n.to_u32().ok());
539
+ let from = u32::try_from(first_column.max(0)).unwrap_or(u32::MAX);
540
+ let mut start = row.map_or(0, |row| {
541
+ rb_self.position((row, from)).unwrap_or_else(|start| start)
542
+ });
543
+ rb_self.row_values(ruby, row, first_column, last_column, &mut start)
544
+ }
545
+
546
+ fn row_values(
547
+ &self,
548
+ ruby: &Ruby,
549
+ row: Option<u32>,
550
+ first_column: i64,
551
+ last_column: i64,
552
+ start: &mut usize,
553
+ ) -> Result<RArray, Error> {
554
+ let width = usize::try_from(last_column - first_column + 1).unwrap_or(0);
555
+ let values = ruby.ary_new_capa(width);
556
+ let mut column = first_column;
557
+ if let Some(row) = row {
558
+ while let Some(&record) = self.index.get(*start) {
559
+ let (record_row, record_column) = self.records[record as usize].key();
560
+ if record_row != row || i64::from(record_column) > last_column {
561
+ break;
562
+ }
563
+ for _ in column..i64::from(record_column) {
564
+ values.push(ruby.qnil())?;
565
+ }
566
+ values.push(self.value(ruby, record)?)?;
567
+ column = i64::from(record_column) + 1;
568
+ *start += 1;
569
+ }
570
+ }
571
+ for _ in column..=last_column {
572
+ values.push(ruby.qnil())?;
573
+ }
574
+ Ok(values)
575
+ }
576
+
577
+ /// Yields rows until the block returns false, then returns the next row number.
578
+ fn each_row(
579
+ ruby: &Ruby,
580
+ rb_self: &Self,
581
+ last_row: u32,
582
+ first_column: i64,
583
+ last_column: i64,
584
+ ) -> Result<u64, Error> {
585
+ let mut start = 0;
586
+ let from = u32::try_from(first_column.max(0)).unwrap_or(u32::MAX);
587
+ for row in 1..=last_row {
588
+ start = rb_self.position_from(start, (row, from));
589
+ let values =
590
+ rb_self.row_values(ruby, Some(row), first_column, last_column, &mut start)?;
591
+ if !ruby.yield_value::<_, bool>(values)? {
592
+ return Ok(u64::from(row) + 1);
593
+ }
594
+ }
595
+ Ok(u64::from(last_row) + 1)
596
+ }
597
+
598
+ /// `first_row.upto(last_row).map { |row| cells[[row, column]]&.value }`,
599
+ /// building only the cells whose value needs one.
600
+ fn column(
601
+ ruby: &Ruby,
602
+ rb_self: &Self,
603
+ column: Value,
604
+ first_row: i64,
605
+ last_row: i64,
606
+ ) -> Result<RArray, Error> {
607
+ let height = usize::try_from(last_row - first_row + 1).unwrap_or(0);
608
+ let values = ruby.ary_new_capa(height);
609
+ let column = Integer::from_value(column).and_then(|n| n.to_u32().ok());
610
+ let mut start = 0;
611
+ for row in first_row..=last_row {
612
+ let record = match (u32::try_from(row), column) {
613
+ (Ok(row), Some(column)) => {
614
+ start = rb_self.position_from(start, (row, column));
615
+ let record = rb_self.index.get(start).copied();
616
+ record.filter(|&i| rb_self.records[i as usize].key() == (row, column))
617
+ }
618
+ _ => None,
619
+ };
620
+ match record {
621
+ Some(record) => values.push(rb_self.value(ruby, record)?)?,
622
+ None => values.push(ruby.qnil())?,
623
+ }
624
+ }
625
+ Ok(values)
626
+ }
627
+
628
+ /// `fields.each_with_object({}) { |(key, slot), hash| hash[key] = values[slot] }`
629
+ /// for the `values` of [`Store::row`].
630
+ fn hash_row(
631
+ ruby: &Ruby,
632
+ rb_self: &Self,
633
+ row: Value,
634
+ first_column: i64,
635
+ last_column: i64,
636
+ fields: RArray,
637
+ ) -> Result<RHash, Error> {
638
+ let row = Integer::from_value(row).and_then(|n| n.to_u32().ok());
639
+ let start = row.map(|row| {
640
+ rb_self
641
+ .position((row, u32::try_from(first_column.max(0)).unwrap_or(u32::MAX)))
642
+ .unwrap_or_else(|start| start)
643
+ });
644
+ let width = (last_column - first_column + 1).max(0);
645
+ let mut position = start.unwrap_or(0);
646
+ let mut previous_column = first_column;
647
+ let hash = ruby.hash_new_capa(fields.len());
648
+ for field in fields {
649
+ let field = RArray::try_convert(field)?;
650
+ let key: Value = field.entry(0)?;
651
+ let slot: i64 = field.entry(1)?;
652
+ let slot = if slot < 0 { width + slot } else { slot };
653
+ let record = match (row, start) {
654
+ (Some(row), Some(start)) if (0..width).contains(&slot) => {
655
+ let column = first_column + slot;
656
+ u32::try_from(column).ok().and_then(|column| {
657
+ if i64::from(column) < previous_column {
658
+ position = start;
659
+ }
660
+ previous_column = i64::from(column);
661
+ position = rb_self.position_from(position, (row, column));
662
+ rb_self
663
+ .index
664
+ .get(position)
665
+ .copied()
666
+ .filter(|&i| rb_self.records[i as usize].key() == (row, column))
667
+ })
668
+ }
669
+ _ => None,
670
+ };
671
+ let value = match record {
672
+ Some(record) => rb_self.value(ruby, record)?,
673
+ None => ruby.qnil().as_value(),
674
+ };
675
+ hash.aset(key, value)?;
676
+ }
677
+ Ok(hash)
678
+ }
679
+
680
+ /// What `Roo::Excelx::Sheet#first_last_row_col` computes from `cells`.
681
+ fn bounds(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
682
+ let presence = |cell: Value| -> Result<bool, Error> {
683
+ Ok(cell.funcall::<_, _, Value>("presence", ())?.to_bool())
684
+ };
685
+ let built = ruby.get_inner(rb_self.built);
686
+ let has_built = !built.is_empty();
687
+ let handed_out = ruby.get_inner(rb_self.handed_out);
688
+ let has_handed_out = !handed_out.is_empty();
689
+ let converter = ruby.get_inner(rb_self.converter);
690
+ let mut bounds: Option<(u32, u32, u32, u32)> = None;
691
+ for &i in &rb_self.index {
692
+ let record = &rb_self.records[i as usize];
693
+ // A cell already handed out may have been changed in place.
694
+ let cell = if has_built {
695
+ built.get(record.built_key())
696
+ } else {
697
+ None
698
+ };
699
+ let cleaned: Value = ruby.get_inner(rb_self.cleaned).entry(i as isize)?;
700
+ let present = match cell {
701
+ Some(cell) => presence(cell)?,
702
+ None if !cleaned.is_nil() => !RString::try_convert(cleaned)?.is_empty(),
703
+ None => match (record.content, record.kind) {
704
+ (Content::Empty, _) => false,
705
+ (Content::Value, Kind::Shared) => match converter.presence(
706
+ ruby,
707
+ &rb_self.text,
708
+ &rb_self.formulas.raw(&rb_self.records, i),
709
+ )? {
710
+ Some(present) => present,
711
+ None => presence(rb_self.build(ruby, i)?)?,
712
+ },
713
+ (Content::Inline, _) | (Content::Value, Kind::Str) => {
714
+ let value: Value = if has_handed_out {
715
+ handed_out.entry(i as isize)?
716
+ } else {
717
+ ruby.qnil().as_value()
718
+ };
719
+ match RString::from_value(value) {
720
+ Some(value) => !value.is_empty(),
721
+ None => {
722
+ record.content == Content::Inline || record.text.0 != record.text.1
723
+ }
724
+ }
725
+ }
726
+ (Content::Value, _) => true,
727
+ },
728
+ };
729
+ if present {
730
+ let (row, column) = record.key();
731
+ bounds = Some(match bounds {
732
+ None => (row, row, column, column),
733
+ Some((first_row, last_row, first_column, last_column)) => (
734
+ first_row.min(row),
735
+ last_row.max(row),
736
+ first_column.min(column),
737
+ last_column.max(column),
738
+ ),
739
+ });
740
+ }
741
+ }
742
+ let (first_row, last_row, first_column, last_column) = match bounds {
743
+ Some((a, b, c, d)) => (Some(a), Some(b), Some(c), Some(d)),
744
+ None => (None, None, None, None),
745
+ };
746
+ let hash = ruby.hash_new();
747
+ hash.aset(ruby.sym_new("first_row"), first_row)?;
748
+ hash.aset(ruby.sym_new("last_row"), last_row)?;
749
+ hash.aset(ruby.sym_new("first_column"), first_column)?;
750
+ hash.aset(ruby.sym_new("last_column"), last_column)?;
751
+ Ok(hash)
752
+ }
753
+ }
754
+
755
+ /// What [`Store::write_csv`] writes: `rows` lines of `width` fields, with
756
+ /// the fields outside the sheet's first and last row and column left empty.
757
+ struct CsvArea {
758
+ rows: i64,
759
+ width: i64,
760
+ first_row: i64,
761
+ last_row: i64,
762
+ first_column: i64,
763
+ last_column: i64,
764
+ }
765
+
766
+ impl Store {
767
+ /// `Roo::Oxide::CSV#write_csv_lines` for the cells of this store, with
768
+ /// `book` the `Roo::Excelx` whose CSV helpers format the fields Ruby
769
+ /// formats, and `area` `[rows, width, first_row, last_row, first_column,
770
+ /// last_column]`. When a field raises, `io` holds the text up to it, as
771
+ /// in roo.
772
+ fn write_csv(
773
+ ruby: &Ruby,
774
+ rb_self: &Self,
775
+ io: Value,
776
+ book: Value,
777
+ separator: RString,
778
+ area: RArray,
779
+ ) -> Result<(), Error> {
780
+ let area = CsvArea {
781
+ rows: area.entry(0)?,
782
+ width: area.entry(1)?,
783
+ first_row: area.entry(2)?,
784
+ last_row: area.entry(3)?,
785
+ first_column: area.entry(4)?,
786
+ last_column: area.entry(5)?,
787
+ };
788
+ let mut output = CsvOutput::new(io, unsafe { separator.as_slice() }.to_vec());
789
+ let written = rb_self.csv_lines(ruby, book, &mut output, &area);
790
+ output.print(ruby).and(written)
791
+ }
792
+
793
+ fn csv_lines(
794
+ &self,
795
+ ruby: &Ruby,
796
+ book: Value,
797
+ output: &mut CsvOutput,
798
+ area: &CsvArea,
799
+ ) -> Result<(), Error> {
800
+ let CsvArea { rows, width, .. } = *area;
801
+ let mut line_row = 0;
802
+ let mut written = 0;
803
+ for &record in &self.index {
804
+ let (row, column) = self.records[record as usize].key();
805
+ let (row, column) = (i64::from(row), i64::from(column));
806
+ if row > rows {
807
+ break;
808
+ }
809
+ if row < 1 || column < 1 || column > width {
810
+ continue;
811
+ }
812
+ if row != line_row {
813
+ if line_row > 0 {
814
+ output.end_line(ruby, width - 1 - written)?;
815
+ }
816
+ for _ in line_row + 1..row {
817
+ output.end_line(ruby, width - 1)?;
818
+ }
819
+ line_row = row;
820
+ written = 0;
821
+ }
822
+ output.separators(column - 1 - written);
823
+ written = column - 1;
824
+ let in_bounds = (area.first_row..=area.last_row).contains(&row)
825
+ && (area.first_column..=area.last_column).contains(&column);
826
+ self.csv_field(ruby, book, record, in_bounds, output)?;
827
+ }
828
+ if line_row > 0 {
829
+ output.end_line(ruby, width - 1 - written)?;
830
+ }
831
+ for _ in line_row + 1..=rows {
832
+ output.end_line(ruby, width - 1)?;
833
+ }
834
+ Ok(())
835
+ }
836
+
837
+ /// Appends the field of `record`, empty when its cell is empty or
838
+ /// outside the sheet's bounds. A cell already built may have been changed
839
+ /// since, so it is asked, as is one whose field needs the cell.
840
+ fn csv_field(
841
+ &self,
842
+ ruby: &Ruby,
843
+ book: Value,
844
+ record: u32,
845
+ in_bounds: bool,
846
+ output: &mut CsvOutput,
847
+ ) -> Result<(), Error> {
848
+ let key = self.records[record as usize].built_key();
849
+ let cell = match ruby.get_inner(self.built).get(key) {
850
+ Some(cell) => cell,
851
+ None if !in_bounds => return Ok(()),
852
+ None => {
853
+ let cleaned: Value = ruby.get_inner(self.cleaned).entry(record as isize)?;
854
+ let kept: Value = if cleaned.is_nil() {
855
+ ruby.get_inner(self.handed_out).entry(record as isize)?
856
+ } else {
857
+ cleaned
858
+ };
859
+ let kept = (!kept.is_nil()).then_some(kept);
860
+ let raw = self.formulas.raw(&self.records, record);
861
+ let converter = ruby.get_inner(self.converter);
862
+ if converter.csv_field(ruby, book, &self.text, &raw, kept, output)? {
863
+ return Ok(());
864
+ }
865
+ self.build(ruby, record)?
866
+ }
867
+ };
868
+ if cell.funcall::<_, _, Value>("empty?", ())?.to_bool() || !in_bounds {
869
+ return Ok(());
870
+ }
871
+ output.field(ruby, book.funcall("csv_field", (cell,))?)
872
+ }
873
+ }
874
+
875
+ pub fn define(ruby: &Ruby, native: magnus::RModule) -> Result<(), Error> {
876
+ let store = native.define_class("Store", ruby.class_object())?;
877
+ store.define_singleton_method("read", magnus::function!(Store::read, 2))?;
878
+ store.define_method("[]", magnus::method!(Store::get, 1))?;
879
+ store.define_method("cell_at", magnus::method!(Store::cell_at, 2))?;
880
+ store.define_method("value_at", magnus::method!(Store::value_at, 2))?;
881
+ store.define_method("type_at", magnus::method!(Store::type_at, 2))?;
882
+ store.define_method("bounds", magnus::method!(Store::bounds, 0))?;
883
+ store.define_method("row", magnus::method!(Store::row, 3))?;
884
+ store.define_method("each_row", magnus::method!(Store::each_row, 3))?;
885
+ store.define_method("column", magnus::method!(Store::column, 3))?;
886
+ store.define_method("hash_row", magnus::method!(Store::hash_row, 4))?;
887
+ store.define_method("cells", magnus::method!(Store::cells, 0))?;
888
+ store.define_method("clean", magnus::method!(Store::clean, 1))?;
889
+ store.define_method("write_csv", magnus::method!(Store::write_csv, 4))?;
890
+ store.define_method("to_yaml", magnus::method!(Store::to_yaml, 3))?;
891
+ store.define_method("xml_cells", magnus::method!(Store::xml_cells, 2))?;
892
+ store.define_method("matrix_rows", magnus::method!(Store::matrix_rows, 1))?;
893
+ Ok(())
894
+ }