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,209 @@
|
|
|
1
|
+
//! The text `Roo::Formatters::CSV#cell_to_csv` writes for a cell, from the
|
|
2
|
+
//! value the cell holds or would hold, without building the cell when its
|
|
3
|
+
//! value is enough. Formatting that depends on Ruby (numbers, dates, errors)
|
|
4
|
+
//! stays the `Roo::Oxide::CSV` helpers of the workbook.
|
|
5
|
+
|
|
6
|
+
use std::io::Write;
|
|
7
|
+
|
|
8
|
+
use magnus::encoding::EncodingCapable;
|
|
9
|
+
use magnus::value::ReprValue;
|
|
10
|
+
use magnus::{Error, Integer, RString, Ruby, TryConvert, Value};
|
|
11
|
+
|
|
12
|
+
use super::{Converter, ValueType};
|
|
13
|
+
use crate::reader::{Body, Kind, RawCell, Reference};
|
|
14
|
+
|
|
15
|
+
/// The output prints its text at the end of the line that takes it past this
|
|
16
|
+
/// many bytes.
|
|
17
|
+
const CHUNK: usize = 64 * 1024;
|
|
18
|
+
|
|
19
|
+
/// CSV text on its way to an IO, printed in chunks of UTF-8 text.
|
|
20
|
+
pub struct CsvOutput {
|
|
21
|
+
io: Value,
|
|
22
|
+
bytes: Vec<u8>,
|
|
23
|
+
separator: Vec<u8>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl CsvOutput {
|
|
27
|
+
pub fn new(io: Value, separator: Vec<u8>) -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
io,
|
|
30
|
+
bytes: Vec::with_capacity(CHUNK * 2),
|
|
31
|
+
separator,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub fn separators(&mut self, count: i64) {
|
|
36
|
+
for _ in 0..count {
|
|
37
|
+
self.bytes.extend_from_slice(&self.separator);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Ends the line with `separators` separators and a newline.
|
|
42
|
+
pub fn end_line(&mut self, ruby: &Ruby, separators: i64) -> Result<(), Error> {
|
|
43
|
+
self.separators(separators);
|
|
44
|
+
self.bytes.push(b'\n');
|
|
45
|
+
if self.bytes.len() >= CHUNK {
|
|
46
|
+
self.print(ruby)?;
|
|
47
|
+
}
|
|
48
|
+
Ok(())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Prints the text not printed yet.
|
|
52
|
+
pub fn print(&mut self, ruby: &Ruby) -> Result<(), Error> {
|
|
53
|
+
if self.bytes.is_empty() {
|
|
54
|
+
return Ok(());
|
|
55
|
+
}
|
|
56
|
+
let text = ruby.enc_str_new(&self.bytes, ruby.utf8_encoding());
|
|
57
|
+
self.bytes.clear();
|
|
58
|
+
self.io.funcall::<_, _, Value>("print", (text,))?;
|
|
59
|
+
Ok(())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Writes a field a Ruby call returned. roo prints each field on its
|
|
63
|
+
/// own, and the IO may convert one in another encoding, so such a field
|
|
64
|
+
/// is printed on its own too.
|
|
65
|
+
pub fn field(&mut self, ruby: &Ruby, text: RString) -> Result<(), Error> {
|
|
66
|
+
if utf8(ruby, text) {
|
|
67
|
+
self.bytes.extend_from_slice(unsafe { text.as_slice() });
|
|
68
|
+
return Ok(());
|
|
69
|
+
}
|
|
70
|
+
self.print(ruby)?;
|
|
71
|
+
self.io.funcall::<_, _, Value>("print", (text,))?;
|
|
72
|
+
Ok(())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// `%("#{text.gsub('"', '""')}")`.
|
|
76
|
+
fn quote(&mut self, text: &[u8]) {
|
|
77
|
+
self.bytes.push(b'"');
|
|
78
|
+
let mut from = 0;
|
|
79
|
+
for (i, &byte) in text.iter().enumerate() {
|
|
80
|
+
if byte == b'"' {
|
|
81
|
+
self.bytes.extend_from_slice(&text[from..=i]);
|
|
82
|
+
self.bytes.push(b'"');
|
|
83
|
+
from = i + 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
self.bytes.extend_from_slice(&text[from..]);
|
|
87
|
+
self.bytes.push(b'"');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn utf8(ruby: &Ruby, text: RString) -> bool {
|
|
92
|
+
let index = text.enc_get();
|
|
93
|
+
index == ruby.utf8_encindex() || index == ruby.usascii_encindex()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// The roo cell class a record becomes, as far as its CSV text tells them
|
|
97
|
+
/// apart.
|
|
98
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
99
|
+
enum Class {
|
|
100
|
+
String,
|
|
101
|
+
Boolean,
|
|
102
|
+
Number,
|
|
103
|
+
Temporal,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
impl Converter {
|
|
107
|
+
/// Writes what `csv_field` writes for the cell [`Self::cell`] builds for
|
|
108
|
+
/// `raw` when `cell.empty?` is false, and nothing when it is true. `kept`
|
|
109
|
+
/// is the value that cell will hold, when one was handed out before it
|
|
110
|
+
/// was built. `false`, with nothing written, when the cell has to be
|
|
111
|
+
/// built and asked.
|
|
112
|
+
pub fn csv_field(
|
|
113
|
+
&self,
|
|
114
|
+
ruby: &Ruby,
|
|
115
|
+
book: Value,
|
|
116
|
+
buffer: &str,
|
|
117
|
+
raw: &RawCell,
|
|
118
|
+
kept: Option<Value>,
|
|
119
|
+
output: &mut CsvOutput,
|
|
120
|
+
) -> Result<bool, Error> {
|
|
121
|
+
let Reference::At { row, column } = raw.reference else {
|
|
122
|
+
return Ok(false);
|
|
123
|
+
};
|
|
124
|
+
if !self.build_cells || self.has_link(ruby, row, column) {
|
|
125
|
+
return Ok(false);
|
|
126
|
+
}
|
|
127
|
+
let class = match (&raw.body, raw.kind) {
|
|
128
|
+
(Body::Empty, _) => return Ok(true),
|
|
129
|
+
(Body::Inline(text), _) | (Body::Value(text), Kind::Str) if kept.is_none() => {
|
|
130
|
+
if !text.is_empty() {
|
|
131
|
+
output.quote(buffer[text.clone()].as_bytes());
|
|
132
|
+
}
|
|
133
|
+
return Ok(true);
|
|
134
|
+
}
|
|
135
|
+
(Body::Inline(_), _) | (Body::Value(_), Kind::Shared | Kind::Str) => Class::String,
|
|
136
|
+
(Body::Value(_), Kind::Boolean) => Class::Boolean,
|
|
137
|
+
(Body::Value(_), Kind::InlineStr) => Class::Number,
|
|
138
|
+
(Body::Value(_), Kind::ByFormat) => {
|
|
139
|
+
match self.style_info(ruby, self.style(ruby, raw)?)?.1 {
|
|
140
|
+
ValueType::Number => Class::Number,
|
|
141
|
+
ValueType::Date | ValueType::DateTime | ValueType::Time => Class::Temporal,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
let value = match kept {
|
|
146
|
+
Some(value) => value,
|
|
147
|
+
None => match self.known_value(ruby, buffer, raw)? {
|
|
148
|
+
Some(value) => value,
|
|
149
|
+
None => return Ok(false),
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
let formula = raw.formula.is_some();
|
|
153
|
+
match (class, RString::from_value(value)) {
|
|
154
|
+
// `:string`, and a `:formula` String: quoted unless empty.
|
|
155
|
+
(Class::String, Some(text)) => quote_string(ruby, book, text, output)?,
|
|
156
|
+
(Class::String, None) => return Ok(false),
|
|
157
|
+
(_, Some(text)) if formula => quote_string(ruby, book, text, output)?,
|
|
158
|
+
(Class::Boolean, _) => {
|
|
159
|
+
let text = match (value.to_bool(), formula) {
|
|
160
|
+
(true, false) => "\"true\"",
|
|
161
|
+
(false, false) => "\"false\"",
|
|
162
|
+
(true, true) => "true",
|
|
163
|
+
(false, true) => "false",
|
|
164
|
+
};
|
|
165
|
+
output.bytes.extend_from_slice(text.as_bytes());
|
|
166
|
+
}
|
|
167
|
+
(_, _) if formula => {
|
|
168
|
+
output.field(ruby, book.funcall("csv_formula_value", (value,))?)?
|
|
169
|
+
}
|
|
170
|
+
(Class::Number, _) => output.field(ruby, book.funcall("csv_number", (value,))?)?,
|
|
171
|
+
(Class::Temporal, _) if Integer::from_value(value).is_some() => {
|
|
172
|
+
if let Ok(seconds) = i32::try_convert(value) {
|
|
173
|
+
write!(
|
|
174
|
+
output.bytes,
|
|
175
|
+
"{:02}:{:02}:{:02}",
|
|
176
|
+
seconds.div_euclid(3600),
|
|
177
|
+
seconds.rem_euclid(3600) / 60,
|
|
178
|
+
seconds.rem_euclid(60)
|
|
179
|
+
)
|
|
180
|
+
.expect("writing to a Vec cannot fail");
|
|
181
|
+
} else {
|
|
182
|
+
output.field(ruby, book.funcall("integer_to_timestring", (value,))?)?;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
(Class::Temporal, _) => output.field(ruby, value.funcall("to_s", ())?)?,
|
|
186
|
+
}
|
|
187
|
+
Ok(true)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// `csv_quote(text) unless text.empty?`, quoting UTF-8 text here, where
|
|
192
|
+
/// `gsub` doubles each `"` byte even in broken text, and leaving any other
|
|
193
|
+
/// encoding to Ruby.
|
|
194
|
+
fn quote_string(
|
|
195
|
+
ruby: &Ruby,
|
|
196
|
+
book: Value,
|
|
197
|
+
text: RString,
|
|
198
|
+
output: &mut CsvOutput,
|
|
199
|
+
) -> Result<(), Error> {
|
|
200
|
+
if text.is_empty() {
|
|
201
|
+
return Ok(());
|
|
202
|
+
}
|
|
203
|
+
if utf8(ruby, text) {
|
|
204
|
+
output.quote(unsafe { text.as_slice() });
|
|
205
|
+
Ok(())
|
|
206
|
+
} else {
|
|
207
|
+
output.field(ruby, book.funcall("csv_quote", (text,))?)
|
|
208
|
+
}
|
|
209
|
+
}
|