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,516 @@
1
+ //! The roo cells themselves. Under a roo version whose constructors it knows,
2
+ //! [`Converter::cell`] allocates each cell and sets the instance variables
3
+ //! its constructor sets, in the same order and with the same values;
4
+ //! otherwise, and for any value it does not compute exactly the way roo
5
+ //! does, it calls roo's constructor with the same arguments.
6
+
7
+ use magnus::rb_sys::{AsRawId, AsRawValue, FromRawValue};
8
+ use magnus::value::{IntoId, Lazy, LazyId, ReprValue};
9
+ use magnus::{prelude::*, Error, IntoValue, RArray, RClass, RObject, RString, Ruby, Value};
10
+
11
+ use super::{
12
+ Converter, NumberFormat, ValueType, CELL_BOOLEAN, CELL_DATE, CELL_DATETIME, CELL_EMPTY,
13
+ CELL_NUMBER, CELL_STRING, CELL_TIME, COORDINATE, NUMERIC_OR_FORMULA,
14
+ };
15
+ use crate::numeric::{create_numeric, leading_integer};
16
+ use crate::reader::{Body, Kind, Mode, RawCell, Reference};
17
+
18
+ static TIME: Lazy<RClass> = Lazy::new(|r| r.class_time());
19
+ static LINK: Lazy<RClass> = Lazy::new(|r| r.eval("Roo::Link").expect("roo defines Roo::Link"));
20
+
21
+ static CELL_VALUE: LazyId = LazyId::new("@cell_value");
22
+ static CELL_TYPE: LazyId = LazyId::new("@cell_type");
23
+ static FORMULA: LazyId = LazyId::new("@formula");
24
+ static STYLE: LazyId = LazyId::new("@style");
25
+ static COORDINATE_IVAR: LazyId = LazyId::new("@coordinate");
26
+ static VALUE: LazyId = LazyId::new("@value");
27
+ static FORMAT: LazyId = LazyId::new("@format");
28
+ static DATETIME: LazyId = LazyId::new("@datetime");
29
+
30
+ const SECONDS_IN_DAY: f64 = 86_400.0;
31
+
32
+ /// The roo cell a value cell becomes, with what its value is computed from.
33
+ #[derive(Clone, Copy)]
34
+ enum NumericCell {
35
+ Number(NumberFormat),
36
+ /// `Cell::Date` from the cell's text.
37
+ DateFromText,
38
+ /// `Cell::Date`, `Cell::DateTime`, and `Cell::Time` from the text's
39
+ /// `to_f`.
40
+ DateFromNumber(f64),
41
+ DateTime(f64),
42
+ Time(f64),
43
+ }
44
+
45
+ impl NumericCell {
46
+ fn temporal(number: f64) -> Self {
47
+ if number < 1.0 {
48
+ Self::Time(number)
49
+ } else if (number - number.floor()).abs() > 0.000001 {
50
+ Self::DateTime(number)
51
+ } else {
52
+ Self::DateFromNumber(number)
53
+ }
54
+ }
55
+ }
56
+
57
+ struct Common {
58
+ formula: Option<RString>,
59
+ style: Value,
60
+ link: Value,
61
+ coordinate: Value,
62
+ }
63
+
64
+ impl Converter {
65
+ /// `Coordinate.new(row, column)`.
66
+ pub(super) fn new_coordinate<T: IntoValue>(
67
+ &self,
68
+ ruby: &Ruby,
69
+ row: T,
70
+ column: T,
71
+ ) -> Result<Value, Error> {
72
+ let class = ruby.get_inner(&COORDINATE);
73
+ if !self.build_cells {
74
+ return class.new_instance((row, column));
75
+ }
76
+ let row = row.into_value_with(ruby);
77
+ let column = column.into_value_with(ruby);
78
+ let coordinate = magnus::rb_sys::protect(|| unsafe {
79
+ let coordinate = rb_sys::rb_obj_alloc(class.as_raw());
80
+ rb_sys::rb_ary_push(coordinate, row.as_raw());
81
+ rb_sys::rb_ary_push(coordinate, column.as_raw());
82
+ rb_sys::rb_obj_freeze(coordinate)
83
+ })?;
84
+ Ok(unsafe { Value::from_raw(coordinate) })
85
+ }
86
+
87
+ /// Builds the cell `SheetDoc#cell_from_xml` builds for `raw`. `None` means
88
+ /// roo stores no cell (an empty cell without the `empty_cell` option).
89
+ /// `known` is the value [`Self::known_value`] returned for `raw`, which
90
+ /// the cell then holds.
91
+ pub fn cell(
92
+ &self,
93
+ ruby: &Ruby,
94
+ buffer: &str,
95
+ raw: &RawCell,
96
+ row_number: usize,
97
+ column: usize,
98
+ known: Option<Value>,
99
+ ) -> Result<Option<(Value, Value)>, Error> {
100
+ let coordinate = self.coordinate(ruby, raw, row_number, column)?;
101
+ let style = self.style(ruby, raw)?;
102
+ let common = Common {
103
+ formula: raw
104
+ .formula
105
+ .as_ref()
106
+ .map(|f| ruby.str_new(&buffer[f.clone()])),
107
+ style,
108
+ link: self.link(ruby, coordinate),
109
+ coordinate,
110
+ };
111
+ let cell = match &raw.body {
112
+ Body::Empty => {
113
+ if !self.empty_cell {
114
+ return Ok(None);
115
+ }
116
+ if self.build_cells {
117
+ let cell = Self::allocate(ruby, &CELL_EMPTY)?;
118
+ cell.ivar_set(*COORDINATE_IVAR, coordinate)?;
119
+ cell.as_value()
120
+ } else {
121
+ ruby.get_inner(&CELL_EMPTY).new_instance((coordinate,))?
122
+ }
123
+ }
124
+ Body::Inline(text) => {
125
+ let value = known
126
+ .filter(|_| !common.link.to_bool())
127
+ .unwrap_or_else(|| ruby.str_new(&buffer[text.clone()]).as_value());
128
+ self.string(ruby, value, &common, known)?
129
+ }
130
+ // roo looks up the style format for every `v` cell, which raises when
131
+ // the workbook has no styles part, so this does too.
132
+ Body::Value(text) => {
133
+ let text = &buffer[text.clone()];
134
+ match (raw.kind, self.style_info(ruby, style)?) {
135
+ (Kind::Shared, _) => {
136
+ let value = self.shared_string(ruby, text)?;
137
+ self.string(ruby, value, &common, known)?
138
+ }
139
+ (Kind::Boolean, _) => self.boolean(ruby, text, &common)?,
140
+ (Kind::Str, _) => {
141
+ let value = known
142
+ .filter(|_| !common.link.to_bool())
143
+ .unwrap_or_else(|| ruby.str_new(text).as_value());
144
+ self.string(ruby, value, &common, known)?
145
+ }
146
+ (kind, (format, format_type, number_format)) => {
147
+ let value_type = if kind == Kind::InlineStr {
148
+ ValueType::Number
149
+ } else {
150
+ format_type
151
+ };
152
+ let read = (value_type, number_format);
153
+ self.numeric(ruby, text, format, read, known, &common)?
154
+ }
155
+ }
156
+ }
157
+ };
158
+ Ok(Some((coordinate, cell)))
159
+ }
160
+
161
+ /// The `value` of the cell [`Self::cell`] builds for `raw` without its
162
+ /// constructor, without building it. `None` when that cell would come
163
+ /// from its constructor.
164
+ pub fn known_value(
165
+ &self,
166
+ ruby: &Ruby,
167
+ buffer: &str,
168
+ raw: &RawCell,
169
+ ) -> Result<Option<Value>, Error> {
170
+ let Reference::At { row, column } = raw.reference else {
171
+ return Ok(None);
172
+ };
173
+ if !self.build_cells {
174
+ return Ok(None);
175
+ }
176
+ let link = self.link_at(ruby, row, column);
177
+ if link.to_bool() {
178
+ let value = match (&raw.body, raw.kind) {
179
+ (Body::Inline(text), _) | (Body::Value(text), Kind::Str) => {
180
+ ruby.str_new(&buffer[text.clone()]).as_value()
181
+ }
182
+ _ => return Ok(None),
183
+ };
184
+ return ruby.get_inner(&LINK).new_instance((link, value)).map(Some);
185
+ }
186
+ let text = match &raw.body {
187
+ Body::Empty => return Ok(Some(ruby.qnil().as_value())),
188
+ Body::Inline(text) => return Ok(Some(ruby.str_new(&buffer[text.clone()]).as_value())),
189
+ Body::Value(text) => &buffer[text.clone()],
190
+ };
191
+ let (_, format_type, number_format) = self.style_info(ruby, self.style(ruby, raw)?)?;
192
+ let numeric = match (raw.kind, format_type) {
193
+ (Kind::Shared, _) => return Ok(Some(self.shared_string(ruby, text)?)),
194
+ (Kind::Boolean, _) => {
195
+ let value = match text {
196
+ "0" => Some(false),
197
+ "1" => Some(true),
198
+ _ => None,
199
+ };
200
+ return Ok(value.map(|value| value.into_value_with(ruby)));
201
+ }
202
+ (Kind::Str, _) => return Ok(Some(ruby.str_new(text).as_value())),
203
+ (Kind::InlineStr, _) | (Kind::ByFormat, ValueType::Number) => {
204
+ NumericCell::Number(number_format)
205
+ }
206
+ (Kind::ByFormat, ValueType::Date) => NumericCell::DateFromText,
207
+ (Kind::ByFormat, ValueType::DateTime | ValueType::Time) => {
208
+ NumericCell::temporal(ruby.str_new(text).funcall("to_f", ())?)
209
+ }
210
+ };
211
+ if let NumericCell::Time(number) = numeric {
212
+ return Ok(to_i64((number * SECONDS_IN_DAY).round())
213
+ .map(|seconds| ruby.integer_from_i64(seconds).as_value()));
214
+ }
215
+ Ok(self.computed(ruby, numeric, text)?.map(|(value, _)| value))
216
+ }
217
+
218
+ /// Non-text cells with a truthy link keep roo's link constructors.
219
+ fn builds(&self, common: &Common) -> bool {
220
+ self.build_cells && !common.link.to_bool()
221
+ }
222
+
223
+ fn allocate(ruby: &Ruby, class: &Lazy<RClass>) -> Result<RObject, Error> {
224
+ let cell = ruby.get_inner(class).obj_alloc()?;
225
+ Ok(RObject::from_value(cell).expect("roo's cells are plain objects"))
226
+ }
227
+
228
+ /// Allocates a cell and sets what `Cell::Base#initialize` sets, in its
229
+ /// order, with `value` as the final `@value`, then `rest`.
230
+ fn build_cell(
231
+ ruby: &Ruby,
232
+ class: &Lazy<RClass>,
233
+ cell_value: Value,
234
+ cell_type: Option<RArray>,
235
+ common: &Common,
236
+ value: Value,
237
+ rest: &[(&LazyId, Value)],
238
+ ) -> Result<Value, Error> {
239
+ let class = ruby.get_inner(class);
240
+ let fields = [
241
+ Some((*CELL_VALUE, cell_value)),
242
+ cell_type.map(|value| (*CELL_TYPE, value.as_value())),
243
+ common.formula.map(|value| (*FORMULA, value.as_value())),
244
+ (!matches!(i64::try_convert(common.style), Ok(1))).then(|| (*STYLE, common.style)),
245
+ Some((*COORDINATE_IVAR, common.coordinate)),
246
+ Some((*VALUE, value)),
247
+ ];
248
+ let cell = magnus::rb_sys::protect(|| unsafe {
249
+ let cell = rb_sys::rb_obj_alloc(class.as_raw());
250
+ for (name, value) in fields.into_iter().flatten() {
251
+ rb_sys::rb_ivar_set(cell, name.into_id_with(ruby).as_raw(), value.as_raw());
252
+ }
253
+ for (name, value) in rest {
254
+ rb_sys::rb_ivar_set(cell, (***name).into_id_with(ruby).as_raw(), value.as_raw());
255
+ }
256
+ cell
257
+ })?;
258
+ Ok(unsafe { Value::from_raw(cell) })
259
+ }
260
+
261
+ /// `Cell::String.new(value, formula, style, link, coordinate)`.
262
+ fn string(
263
+ &self,
264
+ ruby: &Ruby,
265
+ value: Value,
266
+ common: &Common,
267
+ known: Option<Value>,
268
+ ) -> Result<Value, Error> {
269
+ if self.build_cells {
270
+ let result = if !common.link.to_bool() {
271
+ value
272
+ } else {
273
+ match known {
274
+ Some(link) => link,
275
+ None => ruby.get_inner(&LINK).new_instance((common.link, value))?,
276
+ }
277
+ };
278
+ return Self::build_cell(ruby, &CELL_STRING, value, None, common, result, &[]);
279
+ }
280
+ let Common {
281
+ formula,
282
+ style,
283
+ link,
284
+ coordinate,
285
+ } = *common;
286
+ ruby.get_inner(&CELL_STRING)
287
+ .new_instance((value, formula, style, link, coordinate))
288
+ }
289
+
290
+ /// `Cell::Boolean.new(text, formula, style, link, coordinate)`.
291
+ fn boolean(&self, ruby: &Ruby, text: &str, common: &Common) -> Result<Value, Error> {
292
+ let content = ruby.str_new(text).as_value();
293
+ // `create_boolean` is `text.to_i == 1`.
294
+ let value = match text {
295
+ "0" => Some(false),
296
+ "1" => Some(true),
297
+ _ => None,
298
+ };
299
+ if let Some(value) = value.filter(|_| self.builds(common)) {
300
+ let value = value.into_value_with(ruby);
301
+ return Self::build_cell(ruby, &CELL_BOOLEAN, content, None, common, value, &[]);
302
+ }
303
+ let Common {
304
+ formula,
305
+ style,
306
+ link,
307
+ coordinate,
308
+ } = *common;
309
+ ruby.get_inner(&CELL_BOOLEAN)
310
+ .new_instance((content, formula, style, link, coordinate))
311
+ }
312
+
313
+ /// The `Cell::Number`, `Cell::Date`, `Cell::DateTime`, or `Cell::Time`
314
+ /// for a value cell.
315
+ fn numeric(
316
+ &self,
317
+ ruby: &Ruby,
318
+ text: &str,
319
+ format: Value,
320
+ (value_type, number_format): (ValueType, NumberFormat),
321
+ known: Option<Value>,
322
+ common: &Common,
323
+ ) -> Result<Value, Error> {
324
+ let content = ruby.str_new(text);
325
+ let numeric = match value_type {
326
+ ValueType::Number => NumericCell::Number(number_format),
327
+ ValueType::Date => NumericCell::DateFromText,
328
+ ValueType::DateTime | ValueType::Time => {
329
+ NumericCell::temporal(content.funcall("to_f", ())?)
330
+ }
331
+ };
332
+ let (class, cell_value, base) = match numeric {
333
+ NumericCell::Number(_) => (&CELL_NUMBER, content.as_value(), None),
334
+ NumericCell::DateFromText => (&CELL_DATE, content.as_value(), Some(self.base_date)),
335
+ NumericCell::DateFromNumber(number) => (
336
+ &CELL_DATE,
337
+ ruby.float_from_f64(number).as_value(),
338
+ Some(self.base_date),
339
+ ),
340
+ NumericCell::DateTime(number) => (
341
+ &CELL_DATETIME,
342
+ ruby.float_from_f64(number).as_value(),
343
+ Some(self.base_timestamp),
344
+ ),
345
+ NumericCell::Time(number) => (
346
+ &CELL_TIME,
347
+ ruby.float_from_f64(number).as_value(),
348
+ Some(self.base_timestamp),
349
+ ),
350
+ };
351
+ let numeric_or_formula = ruby.get_inner(&NUMERIC_OR_FORMULA).as_value();
352
+ let excelx_type = ruby.ary_new_from_values(&[numeric_or_formula, format]);
353
+ if self.builds(common) {
354
+ let cell_type = Some(excelx_type);
355
+ let computed = match (known, numeric) {
356
+ (Some(value), NumericCell::Time(_)) => self
357
+ .computed(ruby, numeric, text)?
358
+ .map(|(_, datetime)| (value, datetime)),
359
+ (Some(value), _) => Some((value, None)),
360
+ (None, _) => self.computed(ruby, numeric, text)?,
361
+ };
362
+ match computed {
363
+ Some((value, None)) => {
364
+ let rest = [(&FORMAT, format)];
365
+ return Self::build_cell(
366
+ ruby, class, cell_value, cell_type, common, value, &rest,
367
+ );
368
+ }
369
+ Some((value, Some(datetime))) => {
370
+ let rest = [(&FORMAT, format), (&DATETIME, datetime)];
371
+ return Self::build_cell(
372
+ ruby, class, cell_value, cell_type, common, value, &rest,
373
+ );
374
+ }
375
+ None => {}
376
+ }
377
+ }
378
+ let Common {
379
+ formula,
380
+ style,
381
+ link,
382
+ coordinate,
383
+ } = *common;
384
+ let class = ruby.get_inner(class);
385
+ match base {
386
+ None => {
387
+ let cell = class.new_instance((
388
+ cell_value,
389
+ formula,
390
+ excelx_type,
391
+ style,
392
+ link,
393
+ coordinate,
394
+ ))?;
395
+ if link.is_nil() {
396
+ self.keep_number_as_read(ruby, style, format, content, cell)?;
397
+ }
398
+ Ok(cell)
399
+ }
400
+ Some(base) => class.new_instance((
401
+ cell_value,
402
+ formula,
403
+ excelx_type,
404
+ style,
405
+ link,
406
+ ruby.get_inner(base),
407
+ coordinate,
408
+ )),
409
+ }
410
+ }
411
+
412
+ /// Document mode: roo builds every cell before a caller can change a
413
+ /// format string in place, and `Cell::Number#create_numeric` reads it,
414
+ /// so a cell built later by its constructor gets the value the format
415
+ /// first read gives.
416
+ fn keep_number_as_read(
417
+ &self,
418
+ ruby: &Ruby,
419
+ style: Value,
420
+ format: Value,
421
+ content: RString,
422
+ cell: Value,
423
+ ) -> Result<(), Error> {
424
+ if self.mode != Mode::Document {
425
+ return Ok(());
426
+ }
427
+ let Some(info) = self.style_as_read(style) else {
428
+ return Ok(());
429
+ };
430
+ if info.read_from(format) {
431
+ return Ok(());
432
+ }
433
+ let as_read = ruby.enc_str_new(&info.format_text, info.format_encoding);
434
+ as_read.freeze();
435
+ let excelx_type = ruby.ary_new_from_values(&[
436
+ ruby.get_inner(&NUMERIC_OR_FORMULA).as_value(),
437
+ as_read.as_value(),
438
+ ]);
439
+ let value: Value = ruby
440
+ .get_inner(&CELL_NUMBER)
441
+ .new_instance((
442
+ content,
443
+ ruby.qnil(),
444
+ excelx_type,
445
+ style,
446
+ ruby.qnil(),
447
+ ruby.qnil(),
448
+ ))?
449
+ .funcall("value", ())?;
450
+ cell.funcall::<_, _, Value>("value=", (value,))?;
451
+ Ok(())
452
+ }
453
+
454
+ /// The `@value` roo's constructor computes, plus the `@datetime` of a
455
+ /// `Cell::Time`, or `None` when they cannot be computed the way roo does.
456
+ fn computed(
457
+ &self,
458
+ ruby: &Ruby,
459
+ numeric: NumericCell,
460
+ text: &str,
461
+ ) -> Result<Option<(Value, Option<Value>)>, Error> {
462
+ let base_date = ruby.get_inner(self.base_date);
463
+ Ok(match numeric {
464
+ NumericCell::Number(number_format) => {
465
+ let number = match number_format {
466
+ NumberFormat::Float => create_numeric(text, true),
467
+ NumberFormat::ByText => create_numeric(text, false),
468
+ NumberFormat::Unmatchable => None,
469
+ };
470
+ number.map(|number| (number.into_value_with(ruby), None))
471
+ }
472
+ // `base_date + value.to_i`
473
+ NumericCell::DateFromText => match leading_integer(text) {
474
+ Some(days) => Some((base_date.funcall("+", (days,))?, None)),
475
+ None => None,
476
+ },
477
+ NumericCell::DateFromNumber(number) => match to_i64(number.trunc()) {
478
+ Some(days) => Some((base_date.funcall("+", (days,))?, None)),
479
+ None => None,
480
+ },
481
+ NumericCell::DateTime(number) => {
482
+ self.datetime(ruby, number)?.map(|value| (value, None))
483
+ }
484
+ // `(value.to_f * 86_400).round.to_i`
485
+ NumericCell::Time(number) => match (
486
+ to_i64((number * SECONDS_IN_DAY).round()),
487
+ self.datetime(ruby, number)?,
488
+ ) {
489
+ (Some(seconds), Some(datetime)) => {
490
+ Some((ruby.integer_from_i64(seconds).as_value(), Some(datetime)))
491
+ }
492
+ _ => None,
493
+ },
494
+ })
495
+ }
496
+
497
+ /// `Cell::DateTime#create_datetime(base_timestamp, number)`:
498
+ /// `Time.at((base_timestamp + number.round(6) * 86_400).round(0)).utc.to_datetime`.
499
+ fn datetime(&self, ruby: &Ruby, number: f64) -> Result<Option<Value>, Error> {
500
+ let Some(base_seconds) = self.base_seconds else {
501
+ return Ok(None);
502
+ };
503
+ let rounded: f64 = ruby.float_from_f64(number).funcall("round", (6,))?;
504
+ let Some(timestamp) = to_i64((base_seconds as f64 + rounded * SECONDS_IN_DAY).round())
505
+ else {
506
+ return Ok(None);
507
+ };
508
+ let time: Value = ruby.get_inner(&TIME).funcall("at", (timestamp,))?;
509
+ let utc: Value = time.funcall("utc", ())?;
510
+ Ok(Some(utc.funcall("to_datetime", ())?))
511
+ }
512
+ }
513
+
514
+ fn to_i64(integral: f64) -> Option<i64> {
515
+ (integral.abs() < 9.0e18).then_some(integral as i64)
516
+ }