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,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
+ }