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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +63 -0
- data/Cargo.lock +341 -0
- data/Cargo.toml +7 -0
- data/LICENSE +21 -0
- data/README.md +220 -0
- data/ext/roo_oxide/Cargo.toml +19 -0
- data/ext/roo_oxide/extconf.rb +14 -0
- data/ext/roo_oxide/src/archive.rs +546 -0
- data/ext/roo_oxide/src/convert/cells.rs +516 -0
- data/ext/roo_oxide/src/convert/csv.rs +209 -0
- data/ext/roo_oxide/src/convert.rs +687 -0
- data/ext/roo_oxide/src/lib.rs +306 -0
- data/ext/roo_oxide/src/numeric.rs +174 -0
- data/ext/roo_oxide/src/reader/attributes.rs +199 -0
- data/ext/roo_oxide/src/reader/tests.rs +475 -0
- data/ext/roo_oxide/src/reader.rs +944 -0
- data/ext/roo_oxide/src/shared_strings.rs +684 -0
- data/ext/roo_oxide/src/sheet_parts.rs +312 -0
- data/ext/roo_oxide/src/store/formatters.rs +177 -0
- data/ext/roo_oxide/src/store.rs +894 -0
- data/lib/roo/calamine/version.rb +5 -0
- data/lib/roo/calamine.rb +3 -0
- data/lib/roo/oxide/context.rb +74 -0
- data/lib/roo/oxide/csv.rb +157 -0
- data/lib/roo/oxide/deferred_sheets.rb +284 -0
- data/lib/roo/oxide/excelx.rb +156 -0
- data/lib/roo/oxide/formatters.rb +114 -0
- data/lib/roo/oxide/row_index.rb +93 -0
- data/lib/roo/oxide/row_iteration.rb +33 -0
- data/lib/roo/oxide/shared_strings.rb +46 -0
- data/lib/roo/oxide/sheet.rb +99 -0
- data/lib/roo/oxide/sheet_doc.rb +138 -0
- data/lib/roo/oxide/sheet_parts.rb +78 -0
- data/lib/roo/oxide/version.rb +7 -0
- data/lib/roo/oxide.rb +100 -0
- metadata +110 -0
|
@@ -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
|
+
}
|