fast_xlsx 0.1.0
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 +34 -0
- data/Cargo.lock +517 -0
- data/Cargo.toml +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +298 -0
- data/Rakefile +22 -0
- data/bench/Gemfile +14 -0
- data/bench/Gemfile.lock +110 -0
- data/bench/compare.rb +113 -0
- data/bench/memory.rb +43 -0
- data/bench/write.rb +53 -0
- data/examples/showcase.rb +146 -0
- data/ext/fast_xlsx/Cargo.toml +18 -0
- data/ext/fast_xlsx/build.rs +5 -0
- data/ext/fast_xlsx/extconf.rb +6 -0
- data/ext/fast_xlsx/src/lib.rs +1339 -0
- data/lib/fast_xlsx/version.rb +5 -0
- data/lib/fast_xlsx.rb +205 -0
- data/sig/fast_xlsx.rbs +100 -0
- metadata +80 -0
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
use std::cell::Cell;
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
3
|
+
|
|
4
|
+
use magnus::{
|
|
5
|
+
function, method, prelude::*, r_hash::ForEach, typed_data::Obj, value::Lazy, Error,
|
|
6
|
+
ExceptionClass, Integer, RArray, RClass, RHash, RModule, RString, Ruby, Symbol, TryConvert,
|
|
7
|
+
Value,
|
|
8
|
+
};
|
|
9
|
+
use rust_xlsxwriter::{
|
|
10
|
+
Color, ConditionalFormat2ColorScale, ConditionalFormat3ColorScale, ConditionalFormatCell,
|
|
11
|
+
ConditionalFormatCellRule, ConditionalFormatDataBar, ConditionalFormatFormula,
|
|
12
|
+
ConditionalFormatText, ConditionalFormatTextRule, ConditionalFormatValue, DataValidation,
|
|
13
|
+
DataValidationRule, FormatAlign, FormatBorder, FormatScript, FormatUnderline,
|
|
14
|
+
IntoDataValidationValue, IntoExcelData, XlsxError,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
use rust_xlsxwriter::{
|
|
18
|
+
Chart, ChartType, DocProperties, Image, Note, Table, TableColumn, TableFunction, TableStyle,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// These constants are defined in lib/fast_xlsx.rb before this extension loads.
|
|
22
|
+
fn fast_xlsx_const<T: TryConvert>(ruby: &Ruby, name: &str) -> T {
|
|
23
|
+
ruby.class_object()
|
|
24
|
+
.const_get::<_, RModule>("FastXlsx")
|
|
25
|
+
.and_then(|m| m.const_get(name))
|
|
26
|
+
.unwrap()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Error"));
|
|
30
|
+
static FORMULA: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Formula"));
|
|
31
|
+
static URL: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "URL"));
|
|
32
|
+
static RICH_STRING: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "RichString"));
|
|
33
|
+
|
|
34
|
+
fn xerr(e: XlsxError) -> Error {
|
|
35
|
+
let ruby = Ruby::get().unwrap();
|
|
36
|
+
Error::new(ruby.get_inner(&ERROR), e.to_string())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Shared = Arc<Mutex<rust_xlsxwriter::Workbook>>;
|
|
40
|
+
|
|
41
|
+
#[magnus::wrap(class = "FastXlsx::Workbook", free_immediately)]
|
|
42
|
+
struct Workbook {
|
|
43
|
+
inner: Shared,
|
|
44
|
+
constant_memory: bool,
|
|
45
|
+
low_memory: bool,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[magnus::wrap(class = "FastXlsx::Worksheet", free_immediately)]
|
|
49
|
+
struct Worksheet {
|
|
50
|
+
wb: Shared,
|
|
51
|
+
index: usize,
|
|
52
|
+
next_row: Cell<u32>,
|
|
53
|
+
// constant_memory and low_memory worksheets write finished rows to disk.
|
|
54
|
+
flushes_rows: bool,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl Workbook {
|
|
58
|
+
fn new(constant_memory: bool, low_memory: bool) -> Self {
|
|
59
|
+
Workbook {
|
|
60
|
+
inner: Arc::new(Mutex::new(rust_xlsxwriter::Workbook::new())),
|
|
61
|
+
constant_memory,
|
|
62
|
+
low_memory,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn add_worksheet(&self, name: Option<String>) -> Result<Worksheet, Error> {
|
|
67
|
+
let mut wb = self.inner.lock().unwrap();
|
|
68
|
+
let ws = if self.low_memory {
|
|
69
|
+
wb.add_worksheet_with_low_memory()
|
|
70
|
+
} else if self.constant_memory {
|
|
71
|
+
wb.add_worksheet_with_constant_memory()
|
|
72
|
+
} else {
|
|
73
|
+
wb.add_worksheet()
|
|
74
|
+
};
|
|
75
|
+
if let Some(name) = name {
|
|
76
|
+
ws.set_name(name).map_err(xerr)?;
|
|
77
|
+
}
|
|
78
|
+
Ok(Worksheet {
|
|
79
|
+
wb: self.inner.clone(),
|
|
80
|
+
index: wb.worksheets().len() - 1,
|
|
81
|
+
next_row: Cell::new(0),
|
|
82
|
+
flushes_rows: self.constant_memory || self.low_memory,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn to_xlsx(ruby: &Ruby, rb_self: &Self) -> Result<RString, Error> {
|
|
87
|
+
let buf = rb_self
|
|
88
|
+
.inner
|
|
89
|
+
.lock()
|
|
90
|
+
.unwrap()
|
|
91
|
+
.save_to_buffer()
|
|
92
|
+
.map_err(xerr)?;
|
|
93
|
+
Ok(ruby.str_from_slice(&buf))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn save(&self, path: String) -> Result<(), Error> {
|
|
97
|
+
self.inner.lock().unwrap().save(path).map_err(xerr)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn set_properties(ruby: &Ruby, rb_self: &Self, fields: RHash) -> Result<(), Error> {
|
|
101
|
+
let mut props = DocProperties::new();
|
|
102
|
+
fields.foreach(|key: Symbol, value: String| {
|
|
103
|
+
props = match &*key.name()? {
|
|
104
|
+
"title" => props.clone().set_title(value),
|
|
105
|
+
"subject" => props.clone().set_subject(value),
|
|
106
|
+
"author" => props.clone().set_author(value),
|
|
107
|
+
"manager" => props.clone().set_manager(value),
|
|
108
|
+
"company" => props.clone().set_company(value),
|
|
109
|
+
"category" => props.clone().set_category(value),
|
|
110
|
+
"keywords" => props.clone().set_keywords(value),
|
|
111
|
+
"comments" => props.clone().set_comment(value),
|
|
112
|
+
"status" => props.clone().set_status(value),
|
|
113
|
+
other => {
|
|
114
|
+
return Err(Error::new(
|
|
115
|
+
ruby.exception_arg_error(),
|
|
116
|
+
format!("unknown property: {other}"),
|
|
117
|
+
))
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
Ok(ForEach::Continue)
|
|
121
|
+
})?;
|
|
122
|
+
rb_self.inner.lock().unwrap().set_properties(&props);
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Excel stores datetimes as days since 1900-01-01 in local time.
|
|
128
|
+
fn excel_time(v: Value) -> Result<f64, Error> {
|
|
129
|
+
let secs: f64 = v.funcall("to_f", ())?;
|
|
130
|
+
let offset: i64 = v.funcall("utc_offset", ())?;
|
|
131
|
+
Ok((secs + offset as f64) / 86400.0 + 25569.0)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Date / DateTime: Julian day and day fraction are both in the object's own
|
|
135
|
+
// offset. JD 2415019 is Excel serial 0 (1899-12-30).
|
|
136
|
+
fn excel_date(v: Value) -> Result<f64, Error> {
|
|
137
|
+
let jd: i64 = v.funcall("jd", ())?;
|
|
138
|
+
let fraction: f64 = f64::try_convert(v.funcall("day_fraction", ())?)?;
|
|
139
|
+
Ok((jd - 2_415_019) as f64 + fraction)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn emit<T: IntoExcelData>(
|
|
143
|
+
ws: &mut rust_xlsxwriter::Worksheet,
|
|
144
|
+
row: u32,
|
|
145
|
+
col: u16,
|
|
146
|
+
data: T,
|
|
147
|
+
format: Option<&Format>,
|
|
148
|
+
) -> Result<(), Error> {
|
|
149
|
+
match format {
|
|
150
|
+
Some(f) => ws.write_with_format(row, col, data, &f.0),
|
|
151
|
+
None => ws.write(row, col, data),
|
|
152
|
+
}
|
|
153
|
+
.map(|_| ())
|
|
154
|
+
.map_err(xerr)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// RichString#segments is [[text, Format or nil], ...]; nil means the default font.
|
|
158
|
+
fn write_rich_string(
|
|
159
|
+
ws: &mut rust_xlsxwriter::Worksheet,
|
|
160
|
+
row: u32,
|
|
161
|
+
col: u16,
|
|
162
|
+
v: Value,
|
|
163
|
+
format: Option<&Format>,
|
|
164
|
+
) -> Result<(), Error> {
|
|
165
|
+
let default = rust_xlsxwriter::Format::default();
|
|
166
|
+
let segments: RArray = v.funcall("segments", ())?;
|
|
167
|
+
let mut parts: Vec<(Option<&Format>, String)> = Vec::with_capacity(segments.len());
|
|
168
|
+
for segment in segments.into_iter() {
|
|
169
|
+
let (text, seg_format): (String, Value) = TryConvert::try_convert(segment)?;
|
|
170
|
+
parts.push((Option::<&Format>::try_convert(seg_format)?, text));
|
|
171
|
+
}
|
|
172
|
+
let rich: Vec<(&rust_xlsxwriter::Format, &str)> = parts
|
|
173
|
+
.iter()
|
|
174
|
+
.map(|(f, text)| (f.map_or(&default, |f| &f.0), text.as_str()))
|
|
175
|
+
.collect();
|
|
176
|
+
match format {
|
|
177
|
+
Some(f) => ws.write_rich_string_with_format(row, col, &rich, &f.0),
|
|
178
|
+
None => ws.write_rich_string(row, col, &rich),
|
|
179
|
+
}
|
|
180
|
+
.map(|_| ())
|
|
181
|
+
.map_err(xerr)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn put(
|
|
185
|
+
ruby: &Ruby,
|
|
186
|
+
ws: &mut rust_xlsxwriter::Worksheet,
|
|
187
|
+
row: u32,
|
|
188
|
+
col: u16,
|
|
189
|
+
v: Value,
|
|
190
|
+
format: Option<&Format>,
|
|
191
|
+
) -> Result<(), Error> {
|
|
192
|
+
if v.is_nil() {
|
|
193
|
+
Ok(())
|
|
194
|
+
} else if let Some(s) = RString::from_value(v) {
|
|
195
|
+
// SAFETY: the borrowed str is copied by the writer before any Ruby code runs.
|
|
196
|
+
emit(ws, row, col, unsafe { s.as_str()? }, format)
|
|
197
|
+
} else if v.is_kind_of(ruby.class_numeric()) {
|
|
198
|
+
emit(ws, row, col, f64::try_convert(v)?, format)
|
|
199
|
+
} else if v.is_kind_of(ruby.class_time()) {
|
|
200
|
+
emit(ws, row, col, excel_time(v)?, format)
|
|
201
|
+
} else if v.is_kind_of(ruby.class_true_class()) || v.is_kind_of(ruby.class_false_class()) {
|
|
202
|
+
emit(ws, row, col, v.to_bool(), format)
|
|
203
|
+
} else if v.is_kind_of(ruby.get_inner(&FORMULA)) {
|
|
204
|
+
let expression: String = v.funcall("expression", ())?;
|
|
205
|
+
emit(
|
|
206
|
+
ws,
|
|
207
|
+
row,
|
|
208
|
+
col,
|
|
209
|
+
rust_xlsxwriter::Formula::new(expression),
|
|
210
|
+
format,
|
|
211
|
+
)
|
|
212
|
+
} else if v.is_kind_of(ruby.get_inner(&URL)) {
|
|
213
|
+
let url: String = v.funcall("url", ())?;
|
|
214
|
+
let mut link = rust_xlsxwriter::Url::new(url);
|
|
215
|
+
if let Some(text) = v.funcall::<_, _, Option<String>>("text", ())? {
|
|
216
|
+
link = link.set_text(text);
|
|
217
|
+
}
|
|
218
|
+
emit(ws, row, col, link, format)
|
|
219
|
+
} else if v.is_kind_of(ruby.get_inner(&RICH_STRING)) {
|
|
220
|
+
write_rich_string(ws, row, col, v, format)
|
|
221
|
+
} else if v.respond_to("jd", false)? {
|
|
222
|
+
emit(ws, row, col, excel_date(v)?, format)
|
|
223
|
+
} else {
|
|
224
|
+
let s: String = v.funcall("to_s", ())?;
|
|
225
|
+
emit(ws, row, col, s, format)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// "#RRGGBB" or 0xRRGGBB.
|
|
230
|
+
fn color(ruby: &Ruby, value: Value) -> Result<Color, Error> {
|
|
231
|
+
let rgb = if let Some(i) = Integer::from_value(value) {
|
|
232
|
+
i.to_u32().ok().filter(|n| *n <= 0xFF_FFFF)
|
|
233
|
+
} else if let Some(s) = RString::from_value(value) {
|
|
234
|
+
let s = s.to_string()?;
|
|
235
|
+
s.strip_prefix('#')
|
|
236
|
+
.filter(|hex| hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit()))
|
|
237
|
+
.and_then(|hex| u32::from_str_radix(hex, 16).ok())
|
|
238
|
+
} else {
|
|
239
|
+
None
|
|
240
|
+
};
|
|
241
|
+
rgb.map(Color::RGB).ok_or_else(|| {
|
|
242
|
+
Error::new(
|
|
243
|
+
ruby.exception_arg_error(),
|
|
244
|
+
format!(
|
|
245
|
+
"invalid color {}: use \"#RRGGBB\" or 0xRRGGBB",
|
|
246
|
+
value.inspect()
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// A symbol option that must be one of `choices`.
|
|
253
|
+
fn choice<T: Clone>(
|
|
254
|
+
ruby: &Ruby,
|
|
255
|
+
option: &str,
|
|
256
|
+
value: Value,
|
|
257
|
+
choices: &[(&str, T)],
|
|
258
|
+
) -> Result<T, Error> {
|
|
259
|
+
let name = Symbol::from_value(value).and_then(|s| s.name().ok());
|
|
260
|
+
name.and_then(|n| choices.iter().find(|(k, _)| *k == n))
|
|
261
|
+
.map(|(_, v)| v.clone())
|
|
262
|
+
.ok_or_else(|| {
|
|
263
|
+
let names: Vec<_> = choices.iter().map(|(k, _)| format!(":{k}")).collect();
|
|
264
|
+
Error::new(
|
|
265
|
+
ruby.exception_arg_error(),
|
|
266
|
+
format!(
|
|
267
|
+
"invalid {option} {}: expected one of {}",
|
|
268
|
+
value.inspect(),
|
|
269
|
+
names.join(", ")
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// A number or string compared against in a conditional format rule.
|
|
276
|
+
fn cf_value(ruby: &Ruby, v: Value) -> Result<ConditionalFormatValue, Error> {
|
|
277
|
+
if let Some(s) = RString::from_value(v) {
|
|
278
|
+
Ok(s.to_string()?.into())
|
|
279
|
+
} else if v.is_kind_of(ruby.class_numeric()) {
|
|
280
|
+
Ok(f64::try_convert(v)?.into())
|
|
281
|
+
} else {
|
|
282
|
+
Err(Error::new(
|
|
283
|
+
ruby.exception_arg_error(),
|
|
284
|
+
format!(
|
|
285
|
+
"invalid conditional format value {}: use a number or string",
|
|
286
|
+
v.inspect()
|
|
287
|
+
),
|
|
288
|
+
))
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A comparison criteria and its operand(s): one value, or [min, max] for the
|
|
293
|
+
// range criteria.
|
|
294
|
+
fn comparison(
|
|
295
|
+
ruby: &Ruby,
|
|
296
|
+
what: &str,
|
|
297
|
+
criteria: Value,
|
|
298
|
+
value: Value,
|
|
299
|
+
) -> Result<(&'static str, Value, Option<Value>), Error> {
|
|
300
|
+
let op = choice(
|
|
301
|
+
ruby,
|
|
302
|
+
what,
|
|
303
|
+
criteria,
|
|
304
|
+
&[
|
|
305
|
+
("==", "=="),
|
|
306
|
+
("!=", "!="),
|
|
307
|
+
(">", ">"),
|
|
308
|
+
(">=", ">="),
|
|
309
|
+
("<", "<"),
|
|
310
|
+
("<=", "<="),
|
|
311
|
+
("between", "between"),
|
|
312
|
+
("not_between", "not_between"),
|
|
313
|
+
],
|
|
314
|
+
)?;
|
|
315
|
+
if op != "between" && op != "not_between" {
|
|
316
|
+
return Ok((op, value, None));
|
|
317
|
+
}
|
|
318
|
+
let bounds = RArray::from_value(value)
|
|
319
|
+
.filter(|a| a.len() == 2)
|
|
320
|
+
.ok_or_else(|| {
|
|
321
|
+
Error::new(
|
|
322
|
+
ruby.exception_arg_error(),
|
|
323
|
+
format!("{op} needs value: [min, max], got {}", value.inspect()),
|
|
324
|
+
)
|
|
325
|
+
})?;
|
|
326
|
+
Ok((op, bounds.entry(0)?, Some(bounds.entry(1)?)))
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Maps a comparison() result onto a rust_xlsxwriter rule enum, converting the
|
|
330
|
+
// operands with $convert.
|
|
331
|
+
macro_rules! comparison_rule {
|
|
332
|
+
($Rule:ident, $cmp:expr, $convert:expr) => {{
|
|
333
|
+
let (op, a, b) = $cmp;
|
|
334
|
+
let a = $convert(a)?;
|
|
335
|
+
match (op, b) {
|
|
336
|
+
("between", Some(b)) => $Rule::Between(a, $convert(b)?),
|
|
337
|
+
("not_between", Some(b)) => $Rule::NotBetween(a, $convert(b)?),
|
|
338
|
+
("==", _) => $Rule::EqualTo(a),
|
|
339
|
+
("!=", _) => $Rule::NotEqualTo(a),
|
|
340
|
+
(">", _) => $Rule::GreaterThan(a),
|
|
341
|
+
(">=", _) => $Rule::GreaterThanOrEqualTo(a),
|
|
342
|
+
("<", _) => $Rule::LessThan(a),
|
|
343
|
+
_ => $Rule::LessThanOrEqualTo(a),
|
|
344
|
+
}
|
|
345
|
+
}};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn cell_rule(
|
|
349
|
+
ruby: &Ruby,
|
|
350
|
+
criteria: Value,
|
|
351
|
+
value: Value,
|
|
352
|
+
) -> Result<ConditionalFormatCellRule<ConditionalFormatValue>, Error> {
|
|
353
|
+
let cmp = comparison(ruby, "cell criteria", criteria, value)?;
|
|
354
|
+
Ok(comparison_rule!(ConditionalFormatCellRule, cmp, |v| {
|
|
355
|
+
cf_value(ruby, v)
|
|
356
|
+
}))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fn validation_rule<T: TryConvert + IntoDataValidationValue>(
|
|
360
|
+
ruby: &Ruby,
|
|
361
|
+
criteria: Value,
|
|
362
|
+
value: Value,
|
|
363
|
+
) -> Result<DataValidationRule<T>, Error> {
|
|
364
|
+
let cmp = comparison(ruby, "validation criteria", criteria, value)?;
|
|
365
|
+
Ok(comparison_rule!(DataValidationRule, cmp, T::try_convert))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Excel's 255-character limit, counted the way rust_xlsxwriter does (with the
|
|
369
|
+
// long &[Page]-style codes shortened), which otherwise drops the text silently.
|
|
370
|
+
fn check_header_footer(ruby: &Ruby, what: &str, text: &str) -> Result<(), Error> {
|
|
371
|
+
let mut expanded = text.to_string();
|
|
372
|
+
for (long, short) in [
|
|
373
|
+
("&[Tab]", "&A"),
|
|
374
|
+
("&[Date]", "&D"),
|
|
375
|
+
("&[File]", "&F"),
|
|
376
|
+
("&[Page]", "&P"),
|
|
377
|
+
("&[Path]", "&Z"),
|
|
378
|
+
("&[Time]", "&T"),
|
|
379
|
+
("&[Pages]", "&N"),
|
|
380
|
+
("&[Picture]", "&G"),
|
|
381
|
+
] {
|
|
382
|
+
expanded = expanded.replace(long, short);
|
|
383
|
+
}
|
|
384
|
+
let len = expanded.chars().count();
|
|
385
|
+
if len > 255 {
|
|
386
|
+
return Err(Error::new(
|
|
387
|
+
ruby.exception_arg_error(),
|
|
388
|
+
format!("{what} is {len} characters; Excel allows 255"),
|
|
389
|
+
));
|
|
390
|
+
}
|
|
391
|
+
Ok(())
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Rejects option keys outside `allowed`, so a typo raises instead of being ignored.
|
|
395
|
+
fn check_keys(ruby: &Ruby, options: RHash, allowed: &[&str], what: &str) -> Result<(), Error> {
|
|
396
|
+
options.foreach(|key: Symbol, _: Value| {
|
|
397
|
+
let name = key.name()?;
|
|
398
|
+
if allowed.contains(&&*name) {
|
|
399
|
+
Ok(ForEach::Continue)
|
|
400
|
+
} else {
|
|
401
|
+
Err(Error::new(
|
|
402
|
+
ruby.exception_arg_error(),
|
|
403
|
+
format!(
|
|
404
|
+
"unknown {what} option: {name} (expected one of {})",
|
|
405
|
+
allowed.join(", ")
|
|
406
|
+
),
|
|
407
|
+
))
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const CHART_TYPES: &[(&str, ChartType)] = &[
|
|
413
|
+
("area", ChartType::Area),
|
|
414
|
+
("area_stacked", ChartType::AreaStacked),
|
|
415
|
+
("bar", ChartType::Bar),
|
|
416
|
+
("bar_stacked", ChartType::BarStacked),
|
|
417
|
+
("column", ChartType::Column),
|
|
418
|
+
("column_stacked", ChartType::ColumnStacked),
|
|
419
|
+
("line", ChartType::Line),
|
|
420
|
+
("line_stacked", ChartType::LineStacked),
|
|
421
|
+
("pie", ChartType::Pie),
|
|
422
|
+
("doughnut", ChartType::Doughnut),
|
|
423
|
+
("radar", ChartType::Radar),
|
|
424
|
+
("scatter", ChartType::Scatter),
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
// :none, :light1..:light21, :medium1..:medium28, :dark1..:dark11.
|
|
428
|
+
const TABLE_STYLES: &[(&str, TableStyle)] = &[
|
|
429
|
+
("none", TableStyle::None),
|
|
430
|
+
("light1", TableStyle::Light1),
|
|
431
|
+
("light2", TableStyle::Light2),
|
|
432
|
+
("light3", TableStyle::Light3),
|
|
433
|
+
("light4", TableStyle::Light4),
|
|
434
|
+
("light5", TableStyle::Light5),
|
|
435
|
+
("light6", TableStyle::Light6),
|
|
436
|
+
("light7", TableStyle::Light7),
|
|
437
|
+
("light8", TableStyle::Light8),
|
|
438
|
+
("light9", TableStyle::Light9),
|
|
439
|
+
("light10", TableStyle::Light10),
|
|
440
|
+
("light11", TableStyle::Light11),
|
|
441
|
+
("light12", TableStyle::Light12),
|
|
442
|
+
("light13", TableStyle::Light13),
|
|
443
|
+
("light14", TableStyle::Light14),
|
|
444
|
+
("light15", TableStyle::Light15),
|
|
445
|
+
("light16", TableStyle::Light16),
|
|
446
|
+
("light17", TableStyle::Light17),
|
|
447
|
+
("light18", TableStyle::Light18),
|
|
448
|
+
("light19", TableStyle::Light19),
|
|
449
|
+
("light20", TableStyle::Light20),
|
|
450
|
+
("light21", TableStyle::Light21),
|
|
451
|
+
("medium1", TableStyle::Medium1),
|
|
452
|
+
("medium2", TableStyle::Medium2),
|
|
453
|
+
("medium3", TableStyle::Medium3),
|
|
454
|
+
("medium4", TableStyle::Medium4),
|
|
455
|
+
("medium5", TableStyle::Medium5),
|
|
456
|
+
("medium6", TableStyle::Medium6),
|
|
457
|
+
("medium7", TableStyle::Medium7),
|
|
458
|
+
("medium8", TableStyle::Medium8),
|
|
459
|
+
("medium9", TableStyle::Medium9),
|
|
460
|
+
("medium10", TableStyle::Medium10),
|
|
461
|
+
("medium11", TableStyle::Medium11),
|
|
462
|
+
("medium12", TableStyle::Medium12),
|
|
463
|
+
("medium13", TableStyle::Medium13),
|
|
464
|
+
("medium14", TableStyle::Medium14),
|
|
465
|
+
("medium15", TableStyle::Medium15),
|
|
466
|
+
("medium16", TableStyle::Medium16),
|
|
467
|
+
("medium17", TableStyle::Medium17),
|
|
468
|
+
("medium18", TableStyle::Medium18),
|
|
469
|
+
("medium19", TableStyle::Medium19),
|
|
470
|
+
("medium20", TableStyle::Medium20),
|
|
471
|
+
("medium21", TableStyle::Medium21),
|
|
472
|
+
("medium22", TableStyle::Medium22),
|
|
473
|
+
("medium23", TableStyle::Medium23),
|
|
474
|
+
("medium24", TableStyle::Medium24),
|
|
475
|
+
("medium25", TableStyle::Medium25),
|
|
476
|
+
("medium26", TableStyle::Medium26),
|
|
477
|
+
("medium27", TableStyle::Medium27),
|
|
478
|
+
("medium28", TableStyle::Medium28),
|
|
479
|
+
("dark1", TableStyle::Dark1),
|
|
480
|
+
("dark2", TableStyle::Dark2),
|
|
481
|
+
("dark3", TableStyle::Dark3),
|
|
482
|
+
("dark4", TableStyle::Dark4),
|
|
483
|
+
("dark5", TableStyle::Dark5),
|
|
484
|
+
("dark6", TableStyle::Dark6),
|
|
485
|
+
("dark7", TableStyle::Dark7),
|
|
486
|
+
("dark8", TableStyle::Dark8),
|
|
487
|
+
("dark9", TableStyle::Dark9),
|
|
488
|
+
("dark10", TableStyle::Dark10),
|
|
489
|
+
("dark11", TableStyle::Dark11),
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
const TABLE_TOTALS: &[(&str, TableFunction)] = &[
|
|
493
|
+
("sum", TableFunction::Sum),
|
|
494
|
+
("average", TableFunction::Average),
|
|
495
|
+
("count", TableFunction::Count),
|
|
496
|
+
("count_numbers", TableFunction::CountNumbers),
|
|
497
|
+
("max", TableFunction::Max),
|
|
498
|
+
("min", TableFunction::Min),
|
|
499
|
+
("std_dev", TableFunction::StdDev),
|
|
500
|
+
("var", TableFunction::Var),
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
// A table column: a header String, or { header:, total:, total_label:, format: }.
|
|
504
|
+
fn table_column(ruby: &Ruby, v: Value) -> Result<TableColumn, Error> {
|
|
505
|
+
let Some(spec) = RHash::from_value(v) else {
|
|
506
|
+
return Ok(TableColumn::new().set_header(String::try_convert(v)?));
|
|
507
|
+
};
|
|
508
|
+
check_keys(
|
|
509
|
+
ruby,
|
|
510
|
+
spec,
|
|
511
|
+
&["header", "total", "total_label", "format"],
|
|
512
|
+
"table column",
|
|
513
|
+
)?;
|
|
514
|
+
let nil = ruby.qnil().as_value();
|
|
515
|
+
let get = |name: &str| spec.get(ruby.to_symbol(name)).unwrap_or(nil);
|
|
516
|
+
let mut column = TableColumn::new().set_header(String::try_convert(get("header"))?);
|
|
517
|
+
if !get("total").is_nil() {
|
|
518
|
+
column =
|
|
519
|
+
column.set_total_function(choice(ruby, "table total", get("total"), TABLE_TOTALS)?);
|
|
520
|
+
}
|
|
521
|
+
if !get("total_label").is_nil() {
|
|
522
|
+
column = column.set_total_label(String::try_convert(get("total_label"))?);
|
|
523
|
+
}
|
|
524
|
+
if let Some(format) = Option::<&Format>::try_convert(get("format"))? {
|
|
525
|
+
column = column.set_format(&format.0);
|
|
526
|
+
}
|
|
527
|
+
Ok(column)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const BORDERS: &[(&str, FormatBorder)] = &[
|
|
531
|
+
("thin", FormatBorder::Thin),
|
|
532
|
+
("medium", FormatBorder::Medium),
|
|
533
|
+
("thick", FormatBorder::Thick),
|
|
534
|
+
("dashed", FormatBorder::Dashed),
|
|
535
|
+
("dotted", FormatBorder::Dotted),
|
|
536
|
+
("double", FormatBorder::Double),
|
|
537
|
+
("hair", FormatBorder::Hair),
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
#[magnus::wrap(class = "FastXlsx::Format", free_immediately)]
|
|
541
|
+
struct Format(rust_xlsxwriter::Format);
|
|
542
|
+
|
|
543
|
+
impl Format {
|
|
544
|
+
fn new(ruby: &Ruby, options: RHash) -> Result<Self, Error> {
|
|
545
|
+
let mut f = rust_xlsxwriter::Format::new();
|
|
546
|
+
options.foreach(|key: Symbol, value: Value| {
|
|
547
|
+
let taken = std::mem::take(&mut f);
|
|
548
|
+
f = match &*key.name()? {
|
|
549
|
+
"bold" if value.to_bool() => taken.set_bold(),
|
|
550
|
+
"italic" if value.to_bool() => taken.set_italic(),
|
|
551
|
+
"underline" if value.is_kind_of(ruby.class_true_class()) => {
|
|
552
|
+
taken.set_underline(FormatUnderline::Single)
|
|
553
|
+
}
|
|
554
|
+
"underline" if value.to_bool() => taken.set_underline(choice(
|
|
555
|
+
ruby,
|
|
556
|
+
"underline",
|
|
557
|
+
value,
|
|
558
|
+
&[
|
|
559
|
+
("single", FormatUnderline::Single),
|
|
560
|
+
("double", FormatUnderline::Double),
|
|
561
|
+
("single_accounting", FormatUnderline::SingleAccounting),
|
|
562
|
+
("double_accounting", FormatUnderline::DoubleAccounting),
|
|
563
|
+
],
|
|
564
|
+
)?),
|
|
565
|
+
"strikeout" if value.to_bool() => taken.set_font_strikethrough(),
|
|
566
|
+
"font_script" => taken.set_font_script(choice(
|
|
567
|
+
ruby,
|
|
568
|
+
"font_script",
|
|
569
|
+
value,
|
|
570
|
+
&[
|
|
571
|
+
("superscript", FormatScript::Superscript),
|
|
572
|
+
("subscript", FormatScript::Subscript),
|
|
573
|
+
],
|
|
574
|
+
)?),
|
|
575
|
+
"rotation" => {
|
|
576
|
+
let degrees = i16::try_convert(value)?;
|
|
577
|
+
if !(-90..=90).contains(°rees) && degrees != 270 {
|
|
578
|
+
return Err(Error::new(
|
|
579
|
+
ruby.exception_arg_error(),
|
|
580
|
+
format!("invalid rotation {degrees}: use -90..90 or 270"),
|
|
581
|
+
));
|
|
582
|
+
}
|
|
583
|
+
taken.set_rotation(degrees)
|
|
584
|
+
}
|
|
585
|
+
"indent" => taken.set_indent(u8::try_convert(value)?),
|
|
586
|
+
"shrink" if value.to_bool() => taken.set_shrink(),
|
|
587
|
+
"border_color" => taken.set_border_color(color(ruby, value)?),
|
|
588
|
+
"num_format" => taken.set_num_format(String::try_convert(value)?),
|
|
589
|
+
"font_size" => taken.set_font_size(f64::try_convert(value)?),
|
|
590
|
+
"font_name" => taken.set_font_name(String::try_convert(value)?),
|
|
591
|
+
"font_color" => taken.set_font_color(color(ruby, value)?),
|
|
592
|
+
"bg_color" => taken.set_background_color(color(ruby, value)?),
|
|
593
|
+
"align" => taken.set_align(choice(
|
|
594
|
+
ruby,
|
|
595
|
+
"align",
|
|
596
|
+
value,
|
|
597
|
+
&[
|
|
598
|
+
("left", FormatAlign::Left),
|
|
599
|
+
("center", FormatAlign::Center),
|
|
600
|
+
("right", FormatAlign::Right),
|
|
601
|
+
],
|
|
602
|
+
)?),
|
|
603
|
+
"valign" => taken.set_align(choice(
|
|
604
|
+
ruby,
|
|
605
|
+
"valign",
|
|
606
|
+
value,
|
|
607
|
+
&[
|
|
608
|
+
("top", FormatAlign::Top),
|
|
609
|
+
("center", FormatAlign::VerticalCenter),
|
|
610
|
+
("bottom", FormatAlign::Bottom),
|
|
611
|
+
],
|
|
612
|
+
)?),
|
|
613
|
+
"text_wrap" if value.to_bool() => taken.set_text_wrap(),
|
|
614
|
+
"border" => taken.set_border(choice(ruby, "border", value, BORDERS)?),
|
|
615
|
+
"border_left" => taken.set_border_left(choice(ruby, "border", value, BORDERS)?),
|
|
616
|
+
"border_right" => taken.set_border_right(choice(ruby, "border", value, BORDERS)?),
|
|
617
|
+
"border_top" => taken.set_border_top(choice(ruby, "border", value, BORDERS)?),
|
|
618
|
+
"border_bottom" => taken.set_border_bottom(choice(ruby, "border", value, BORDERS)?),
|
|
619
|
+
"bold" | "italic" | "underline" | "text_wrap" | "strikeout" | "shrink" => taken,
|
|
620
|
+
other => {
|
|
621
|
+
return Err(Error::new(
|
|
622
|
+
ruby.exception_arg_error(),
|
|
623
|
+
format!("unknown format option: {other}"),
|
|
624
|
+
))
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
Ok(ForEach::Continue)
|
|
628
|
+
})?;
|
|
629
|
+
Ok(Format(f))
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Visits each element by index. RArray::into_iter dups the array first, and
|
|
634
|
+
// dup'ing an Array longer than 3 elements turns the caller's array into a
|
|
635
|
+
// shared root: one extra live Ruby object per row for as long as the data lives.
|
|
636
|
+
// Reading the length each step also stays correct if Ruby code run by `f`
|
|
637
|
+
// (e.g. a to_s) changes the array.
|
|
638
|
+
fn each_entry(
|
|
639
|
+
ary: RArray,
|
|
640
|
+
mut f: impl FnMut(usize, Value) -> Result<(), Error>,
|
|
641
|
+
) -> Result<(), Error> {
|
|
642
|
+
let mut i = 0;
|
|
643
|
+
while i < ary.len() {
|
|
644
|
+
f(i, ary.entry(i as isize)?)?;
|
|
645
|
+
i += 1;
|
|
646
|
+
}
|
|
647
|
+
Ok(())
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// A row's format: one Format (or nil) for every cell, or an Array with one per cell.
|
|
651
|
+
enum RowFormat {
|
|
652
|
+
Same(Option<&'static Format>),
|
|
653
|
+
PerCell(RArray),
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
impl RowFormat {
|
|
657
|
+
fn from_value(v: Value) -> Result<Self, Error> {
|
|
658
|
+
match RArray::from_value(v) {
|
|
659
|
+
Some(formats) => Ok(RowFormat::PerCell(formats)),
|
|
660
|
+
None => Ok(RowFormat::Same(Option::<&Format>::try_convert(v)?)),
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
fn at(&self, col: usize) -> Result<Option<&Format>, Error> {
|
|
665
|
+
match self {
|
|
666
|
+
RowFormat::Same(f) => Ok(*f),
|
|
667
|
+
RowFormat::PerCell(formats) => {
|
|
668
|
+
Option::<&Format>::try_convert(formats.entry::<Value>(col as isize)?)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
impl Worksheet {
|
|
675
|
+
fn advance(&self, row: u32) {
|
|
676
|
+
self.next_row.set(self.next_row.get().max(row + 1));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// rust_xlsxwriter silently drops writes to rows it has already flushed.
|
|
680
|
+
fn check_not_flushed(&self, ruby: &Ruby, row: u32) -> Result<(), Error> {
|
|
681
|
+
if self.flushes_rows && row + 1 < self.next_row.get() {
|
|
682
|
+
return Err(Error::new(
|
|
683
|
+
ruby.get_inner(&ERROR),
|
|
684
|
+
format!(
|
|
685
|
+
"row {row} was already written to disk (constant_memory / low_memory mode)"
|
|
686
|
+
),
|
|
687
|
+
));
|
|
688
|
+
}
|
|
689
|
+
Ok(())
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
fn with_ws<T>(
|
|
693
|
+
&self,
|
|
694
|
+
f: impl FnOnce(&mut rust_xlsxwriter::Worksheet) -> Result<T, Error>,
|
|
695
|
+
) -> Result<T, Error> {
|
|
696
|
+
let mut wb = self.wb.lock().unwrap();
|
|
697
|
+
f(wb.worksheet_from_index(self.index).map_err(xerr)?)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
fn write_row(
|
|
701
|
+
&self,
|
|
702
|
+
ruby: &Ruby,
|
|
703
|
+
row: u32,
|
|
704
|
+
cells: RArray,
|
|
705
|
+
format: &RowFormat,
|
|
706
|
+
) -> Result<(), Error> {
|
|
707
|
+
self.with_ws(|ws| {
|
|
708
|
+
each_entry(cells, |i, v| {
|
|
709
|
+
let col = u16::try_from(i)
|
|
710
|
+
.map_err(|_| Error::new(ruby.exception_arg_error(), "too many columns"))?;
|
|
711
|
+
put(ruby, ws, row, col, v, format.at(i)?)
|
|
712
|
+
})
|
|
713
|
+
})?;
|
|
714
|
+
self.advance(row);
|
|
715
|
+
Ok(())
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
fn write(
|
|
719
|
+
ruby: &Ruby,
|
|
720
|
+
rb_self: &Self,
|
|
721
|
+
row: u32,
|
|
722
|
+
col: u16,
|
|
723
|
+
v: Value,
|
|
724
|
+
format: Option<&Format>,
|
|
725
|
+
) -> Result<(), Error> {
|
|
726
|
+
rb_self.check_not_flushed(ruby, row)?;
|
|
727
|
+
rb_self.with_ws(|ws| put(ruby, ws, row, col, v, format))?;
|
|
728
|
+
rb_self.advance(row);
|
|
729
|
+
Ok(())
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
fn append(
|
|
733
|
+
ruby: &Ruby,
|
|
734
|
+
rb_self: Obj<Self>,
|
|
735
|
+
cells: RArray,
|
|
736
|
+
format: Value,
|
|
737
|
+
) -> Result<Obj<Self>, Error> {
|
|
738
|
+
let format = RowFormat::from_value(format)?;
|
|
739
|
+
rb_self.write_row(ruby, rb_self.next_row.get(), cells, &format)?;
|
|
740
|
+
Ok(rb_self)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
fn push(ruby: &Ruby, rb_self: Obj<Self>, cells: RArray) -> Result<Obj<Self>, Error> {
|
|
744
|
+
rb_self.write_row(ruby, rb_self.next_row.get(), cells, &RowFormat::Same(None))?;
|
|
745
|
+
Ok(rb_self)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
fn concat(ruby: &Ruby, rb_self: Obj<Self>, rows: RArray) -> Result<Obj<Self>, Error> {
|
|
749
|
+
each_entry(rows, |_, r| {
|
|
750
|
+
let cells = RArray::try_convert(r)?;
|
|
751
|
+
rb_self.write_row(ruby, rb_self.next_row.get(), cells, &RowFormat::Same(None))
|
|
752
|
+
})?;
|
|
753
|
+
Ok(rb_self)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
fn set_column_width(&self, first: u16, last: u16, width: f64) -> Result<(), Error> {
|
|
757
|
+
self.with_ws(|ws| {
|
|
758
|
+
ws.set_column_range_width(first, last, width)
|
|
759
|
+
.map(|_| ())
|
|
760
|
+
.map_err(xerr)
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
fn set_column_format(&self, first: u16, last: u16, format: &Format) -> Result<(), Error> {
|
|
765
|
+
self.with_ws(|ws| {
|
|
766
|
+
ws.set_column_range_format(first, last, &format.0)
|
|
767
|
+
.map(|_| ())
|
|
768
|
+
.map_err(xerr)
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
fn autofilter(
|
|
773
|
+
rb_self: Obj<Self>,
|
|
774
|
+
first_row: u32,
|
|
775
|
+
first_col: u16,
|
|
776
|
+
last_row: u32,
|
|
777
|
+
last_col: u16,
|
|
778
|
+
) -> Result<Obj<Self>, Error> {
|
|
779
|
+
rb_self.with_ws(|ws| {
|
|
780
|
+
ws.autofilter(first_row, first_col, last_row, last_col)
|
|
781
|
+
.map(|_| ())
|
|
782
|
+
.map_err(xerr)
|
|
783
|
+
})?;
|
|
784
|
+
Ok(rb_self)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
fn autofit(rb_self: Obj<Self>) -> Result<Obj<Self>, Error> {
|
|
788
|
+
rb_self.with_ws(|ws| {
|
|
789
|
+
ws.autofit();
|
|
790
|
+
Ok(())
|
|
791
|
+
})?;
|
|
792
|
+
Ok(rb_self)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
fn freeze_panes(rb_self: Obj<Self>, row: u32, col: u16) -> Result<Obj<Self>, Error> {
|
|
796
|
+
rb_self.with_ws(|ws| ws.set_freeze_panes(row, col).map(|_| ()).map_err(xerr))?;
|
|
797
|
+
Ok(rb_self)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
fn set_row_height(rb_self: Obj<Self>, row: u32, height: f64) -> Result<Obj<Self>, Error> {
|
|
801
|
+
rb_self.with_ws(|ws| ws.set_row_height(row, height).map(|_| ()).map_err(xerr))?;
|
|
802
|
+
Ok(rb_self)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
fn set_header(ruby: &Ruby, rb_self: &Self, text: String) -> Result<(), Error> {
|
|
806
|
+
check_header_footer(ruby, "header", &text)?;
|
|
807
|
+
rb_self.with_ws(|ws| {
|
|
808
|
+
ws.set_header(text);
|
|
809
|
+
Ok(())
|
|
810
|
+
})
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
fn set_footer(ruby: &Ruby, rb_self: &Self, text: String) -> Result<(), Error> {
|
|
814
|
+
check_header_footer(ruby, "footer", &text)?;
|
|
815
|
+
rb_self.with_ws(|ws| {
|
|
816
|
+
ws.set_footer(text);
|
|
817
|
+
Ok(())
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Inches; a negative value keeps the current margin.
|
|
822
|
+
fn set_margins(
|
|
823
|
+
&self,
|
|
824
|
+
left: f64,
|
|
825
|
+
right: f64,
|
|
826
|
+
top: f64,
|
|
827
|
+
bottom: f64,
|
|
828
|
+
header: f64,
|
|
829
|
+
footer: f64,
|
|
830
|
+
) -> Result<(), Error> {
|
|
831
|
+
self.with_ws(|ws| {
|
|
832
|
+
ws.set_margins(left, right, top, bottom, header, footer);
|
|
833
|
+
Ok(())
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
fn set_page_breaks(rb_self: Obj<Self>, rows: Vec<u32>) -> Result<Obj<Self>, Error> {
|
|
838
|
+
rb_self.with_ws(|ws| ws.set_page_breaks(&rows).map(|_| ()).map_err(xerr))?;
|
|
839
|
+
Ok(rb_self)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
fn set_vertical_page_breaks(rb_self: Obj<Self>, cols: Vec<u32>) -> Result<Obj<Self>, Error> {
|
|
843
|
+
rb_self.with_ws(|ws| ws.set_vertical_page_breaks(&cols).map(|_| ()).map_err(xerr))?;
|
|
844
|
+
Ok(rb_self)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// rust_xlsxwriter only merges with a string, so merge with "" and then write
|
|
848
|
+
// the value into the first cell, which keeps its type.
|
|
849
|
+
#[allow(clippy::too_many_arguments)]
|
|
850
|
+
fn merge_range(
|
|
851
|
+
ruby: &Ruby,
|
|
852
|
+
rb_self: Obj<Self>,
|
|
853
|
+
first_row: u32,
|
|
854
|
+
first_col: u16,
|
|
855
|
+
last_row: u32,
|
|
856
|
+
last_col: u16,
|
|
857
|
+
v: Value,
|
|
858
|
+
format: Option<&Format>,
|
|
859
|
+
) -> Result<Obj<Self>, Error> {
|
|
860
|
+
let default = rust_xlsxwriter::Format::new();
|
|
861
|
+
let merge_format = format.map_or(&default, |f| &f.0);
|
|
862
|
+
rb_self.with_ws(|ws| {
|
|
863
|
+
ws.merge_range(first_row, first_col, last_row, last_col, "", merge_format)
|
|
864
|
+
.map_err(xerr)?;
|
|
865
|
+
put(ruby, ws, first_row, first_col, v, format)
|
|
866
|
+
})?;
|
|
867
|
+
rb_self.advance(last_row);
|
|
868
|
+
Ok(rb_self)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
#[allow(clippy::too_many_arguments)]
|
|
872
|
+
fn conditional_format(
|
|
873
|
+
ruby: &Ruby,
|
|
874
|
+
rb_self: Obj<Self>,
|
|
875
|
+
fr: u32,
|
|
876
|
+
fc: u16,
|
|
877
|
+
lr: u32,
|
|
878
|
+
lc: u16,
|
|
879
|
+
options: RHash,
|
|
880
|
+
) -> Result<Obj<Self>, Error> {
|
|
881
|
+
let nil = ruby.qnil().as_value();
|
|
882
|
+
let opt = |name: &str| options.get(ruby.to_symbol(name)).unwrap_or(nil);
|
|
883
|
+
check_keys(
|
|
884
|
+
ruby,
|
|
885
|
+
options,
|
|
886
|
+
&["type", "criteria", "value", "format", "colors"],
|
|
887
|
+
"conditional_format",
|
|
888
|
+
)?;
|
|
889
|
+
let format = Option::<&Format>::try_convert(opt("format"))?;
|
|
890
|
+
let value = opt("value");
|
|
891
|
+
let kind = choice(
|
|
892
|
+
ruby,
|
|
893
|
+
"conditional format type",
|
|
894
|
+
opt("type"),
|
|
895
|
+
&[
|
|
896
|
+
("cell", "cell"),
|
|
897
|
+
("text", "text"),
|
|
898
|
+
("formula", "formula"),
|
|
899
|
+
("data_bar", "data_bar"),
|
|
900
|
+
("color_scale", "color_scale"),
|
|
901
|
+
],
|
|
902
|
+
)?;
|
|
903
|
+
rb_self.with_ws(|ws| {
|
|
904
|
+
let r = match kind {
|
|
905
|
+
"cell" => {
|
|
906
|
+
let mut cf = ConditionalFormatCell::new().set_rule(cell_rule(
|
|
907
|
+
ruby,
|
|
908
|
+
opt("criteria"),
|
|
909
|
+
value,
|
|
910
|
+
)?);
|
|
911
|
+
if let Some(f) = format {
|
|
912
|
+
cf = cf.set_format(&f.0);
|
|
913
|
+
}
|
|
914
|
+
ws.add_conditional_format(fr, fc, lr, lc, &cf)
|
|
915
|
+
}
|
|
916
|
+
"text" => {
|
|
917
|
+
let text = String::try_convert(value)?;
|
|
918
|
+
let rule = match choice(
|
|
919
|
+
ruby,
|
|
920
|
+
"text criteria",
|
|
921
|
+
opt("criteria"),
|
|
922
|
+
&[
|
|
923
|
+
("contains", 0),
|
|
924
|
+
("not_contains", 1),
|
|
925
|
+
("begins_with", 2),
|
|
926
|
+
("ends_with", 3),
|
|
927
|
+
],
|
|
928
|
+
)? {
|
|
929
|
+
0 => ConditionalFormatTextRule::Contains(text),
|
|
930
|
+
1 => ConditionalFormatTextRule::DoesNotContain(text),
|
|
931
|
+
2 => ConditionalFormatTextRule::BeginsWith(text),
|
|
932
|
+
_ => ConditionalFormatTextRule::EndsWith(text),
|
|
933
|
+
};
|
|
934
|
+
let mut cf = ConditionalFormatText::new().set_rule(rule);
|
|
935
|
+
if let Some(f) = format {
|
|
936
|
+
cf = cf.set_format(&f.0);
|
|
937
|
+
}
|
|
938
|
+
ws.add_conditional_format(fr, fc, lr, lc, &cf)
|
|
939
|
+
}
|
|
940
|
+
"formula" => {
|
|
941
|
+
let mut cf = ConditionalFormatFormula::new()
|
|
942
|
+
.set_rule(String::try_convert(value)?.as_str());
|
|
943
|
+
if let Some(f) = format {
|
|
944
|
+
cf = cf.set_format(&f.0);
|
|
945
|
+
}
|
|
946
|
+
ws.add_conditional_format(fr, fc, lr, lc, &cf)
|
|
947
|
+
}
|
|
948
|
+
"data_bar" => {
|
|
949
|
+
ws.add_conditional_format(fr, fc, lr, lc, &ConditionalFormatDataBar::new())
|
|
950
|
+
}
|
|
951
|
+
_ => {
|
|
952
|
+
let colors = if opt("colors").is_nil() {
|
|
953
|
+
3
|
|
954
|
+
} else {
|
|
955
|
+
u8::try_convert(opt("colors"))?
|
|
956
|
+
};
|
|
957
|
+
match colors {
|
|
958
|
+
2 => ws.add_conditional_format(
|
|
959
|
+
fr,
|
|
960
|
+
fc,
|
|
961
|
+
lr,
|
|
962
|
+
lc,
|
|
963
|
+
&ConditionalFormat2ColorScale::new(),
|
|
964
|
+
),
|
|
965
|
+
3 => ws.add_conditional_format(
|
|
966
|
+
fr,
|
|
967
|
+
fc,
|
|
968
|
+
lr,
|
|
969
|
+
lc,
|
|
970
|
+
&ConditionalFormat3ColorScale::new(),
|
|
971
|
+
),
|
|
972
|
+
n => {
|
|
973
|
+
return Err(Error::new(
|
|
974
|
+
ruby.exception_arg_error(),
|
|
975
|
+
format!("invalid colors {n}: use 2 or 3"),
|
|
976
|
+
))
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
r.map(|_| ()).map_err(xerr)
|
|
982
|
+
})?;
|
|
983
|
+
Ok(rb_self)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
#[allow(clippy::too_many_arguments)]
|
|
987
|
+
fn data_validation(
|
|
988
|
+
ruby: &Ruby,
|
|
989
|
+
rb_self: Obj<Self>,
|
|
990
|
+
fr: u32,
|
|
991
|
+
fc: u16,
|
|
992
|
+
lr: u32,
|
|
993
|
+
lc: u16,
|
|
994
|
+
options: RHash,
|
|
995
|
+
) -> Result<Obj<Self>, Error> {
|
|
996
|
+
let nil = ruby.qnil().as_value();
|
|
997
|
+
let opt = |name: &str| options.get(ruby.to_symbol(name)).unwrap_or(nil);
|
|
998
|
+
check_keys(
|
|
999
|
+
ruby,
|
|
1000
|
+
options,
|
|
1001
|
+
&[
|
|
1002
|
+
"type",
|
|
1003
|
+
"criteria",
|
|
1004
|
+
"value",
|
|
1005
|
+
"input_title",
|
|
1006
|
+
"input_message",
|
|
1007
|
+
"error_title",
|
|
1008
|
+
"error_message",
|
|
1009
|
+
],
|
|
1010
|
+
"data_validation",
|
|
1011
|
+
)?;
|
|
1012
|
+
let (criteria, value) = (opt("criteria"), opt("value"));
|
|
1013
|
+
let dv = DataValidation::new();
|
|
1014
|
+
let mut dv = match choice(
|
|
1015
|
+
ruby,
|
|
1016
|
+
"data validation type",
|
|
1017
|
+
opt("type"),
|
|
1018
|
+
&[
|
|
1019
|
+
("list", "list"),
|
|
1020
|
+
("whole_number", "whole_number"),
|
|
1021
|
+
("decimal", "decimal"),
|
|
1022
|
+
("text_length", "text_length"),
|
|
1023
|
+
],
|
|
1024
|
+
)? {
|
|
1025
|
+
"list" => match RArray::from_value(value) {
|
|
1026
|
+
Some(items) => dv
|
|
1027
|
+
.allow_list_strings(&items.to_vec::<String>()?)
|
|
1028
|
+
.map_err(xerr)?,
|
|
1029
|
+
None => dv
|
|
1030
|
+
.allow_list_formula(rust_xlsxwriter::Formula::new(String::try_convert(value)?)),
|
|
1031
|
+
},
|
|
1032
|
+
"whole_number" => dv.allow_whole_number(validation_rule(ruby, criteria, value)?),
|
|
1033
|
+
"decimal" => dv.allow_decimal_number(validation_rule(ruby, criteria, value)?),
|
|
1034
|
+
_ => dv.allow_text_length(validation_rule(ruby, criteria, value)?),
|
|
1035
|
+
};
|
|
1036
|
+
for (name, set) in [
|
|
1037
|
+
(
|
|
1038
|
+
"input_title",
|
|
1039
|
+
DataValidation::set_input_title as fn(_, String) -> _,
|
|
1040
|
+
),
|
|
1041
|
+
("input_message", DataValidation::set_input_message),
|
|
1042
|
+
("error_title", DataValidation::set_error_title),
|
|
1043
|
+
("error_message", DataValidation::set_error_message),
|
|
1044
|
+
] {
|
|
1045
|
+
let text = opt(name);
|
|
1046
|
+
if !text.is_nil() {
|
|
1047
|
+
dv = set(dv, String::try_convert(text)?).map_err(xerr)?;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
rb_self.with_ws(|ws| {
|
|
1051
|
+
ws.add_data_validation(fr, fc, lr, lc, &dv)
|
|
1052
|
+
.map(|_| ())
|
|
1053
|
+
.map_err(xerr)
|
|
1054
|
+
})?;
|
|
1055
|
+
Ok(rb_self)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
fn write_comment(
|
|
1059
|
+
rb_self: Obj<Self>,
|
|
1060
|
+
row: u32,
|
|
1061
|
+
col: u16,
|
|
1062
|
+
text: String,
|
|
1063
|
+
author: Option<String>,
|
|
1064
|
+
) -> Result<Obj<Self>, Error> {
|
|
1065
|
+
// Keep the text as written; rust_xlsxwriter would prefix "Author:\n".
|
|
1066
|
+
let mut note = Note::new(text).add_author_prefix(false);
|
|
1067
|
+
if let Some(author) = author {
|
|
1068
|
+
note = note.set_author(author);
|
|
1069
|
+
}
|
|
1070
|
+
rb_self.with_ws(|ws| ws.insert_note(row, col, ¬e).map(|_| ()).map_err(xerr))?;
|
|
1071
|
+
Ok(rb_self)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
fn insert_image(
|
|
1075
|
+
ruby: &Ruby,
|
|
1076
|
+
rb_self: Obj<Self>,
|
|
1077
|
+
row: u32,
|
|
1078
|
+
col: u16,
|
|
1079
|
+
bytes: RString,
|
|
1080
|
+
options: RHash,
|
|
1081
|
+
) -> Result<Obj<Self>, Error> {
|
|
1082
|
+
let nil = ruby.qnil().as_value();
|
|
1083
|
+
let opt = |name: &str| options.get(ruby.to_symbol(name)).unwrap_or(nil);
|
|
1084
|
+
check_keys(
|
|
1085
|
+
ruby,
|
|
1086
|
+
options,
|
|
1087
|
+
&[
|
|
1088
|
+
"scale", "width", "height", "x_offset", "y_offset", "alt_text",
|
|
1089
|
+
],
|
|
1090
|
+
"insert_image",
|
|
1091
|
+
)?;
|
|
1092
|
+
let offset = |name: &str| -> Result<u32, Error> {
|
|
1093
|
+
let v = opt(name);
|
|
1094
|
+
if v.is_nil() {
|
|
1095
|
+
Ok(0)
|
|
1096
|
+
} else {
|
|
1097
|
+
u32::try_convert(v)
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
// SAFETY: the bytes are copied into the Image before any Ruby code runs.
|
|
1101
|
+
let mut image = Image::new_from_buffer(unsafe { bytes.as_slice() }).map_err(xerr)?;
|
|
1102
|
+
let pixels = |name: &str| -> Result<Option<f64>, Error> {
|
|
1103
|
+
let v = opt(name);
|
|
1104
|
+
if v.is_nil() {
|
|
1105
|
+
Ok(None)
|
|
1106
|
+
} else {
|
|
1107
|
+
f64::try_convert(v).map(Some)
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
let (width, height) = (pixels("width")?, pixels("height")?);
|
|
1111
|
+
if !opt("scale").is_nil() {
|
|
1112
|
+
if width.is_some() || height.is_some() {
|
|
1113
|
+
return Err(Error::new(
|
|
1114
|
+
ruby.exception_arg_error(),
|
|
1115
|
+
"pass either scale: or width:/height:, not both",
|
|
1116
|
+
));
|
|
1117
|
+
}
|
|
1118
|
+
let scale = f64::try_convert(opt("scale"))?;
|
|
1119
|
+
image = image.set_scale_width(scale).set_scale_height(scale);
|
|
1120
|
+
} else if width.is_some() || height.is_some() {
|
|
1121
|
+
// Displayed size at scale 1, as rust_xlsxwriter computes it from the DPI.
|
|
1122
|
+
let natural_w = image.width() * 96.0 / image.width_dpi();
|
|
1123
|
+
let natural_h = image.height() * 96.0 / image.height_dpi();
|
|
1124
|
+
let scale_w = width.map(|w| w / natural_w);
|
|
1125
|
+
let scale_h = height.map(|h| h / natural_h);
|
|
1126
|
+
// With only one dimension, scale the other by the same factor.
|
|
1127
|
+
let (sw, sh) = (scale_w.or(scale_h).unwrap(), scale_h.or(scale_w).unwrap());
|
|
1128
|
+
image = image.set_scale_width(sw).set_scale_height(sh);
|
|
1129
|
+
}
|
|
1130
|
+
if !opt("alt_text").is_nil() {
|
|
1131
|
+
image = image.set_alt_text(String::try_convert(opt("alt_text"))?);
|
|
1132
|
+
}
|
|
1133
|
+
let (x, y) = (offset("x_offset")?, offset("y_offset")?);
|
|
1134
|
+
rb_self.with_ws(|ws| {
|
|
1135
|
+
ws.insert_image_with_offset(row, col, &image, x, y)
|
|
1136
|
+
.map(|_| ())
|
|
1137
|
+
.map_err(xerr)
|
|
1138
|
+
})?;
|
|
1139
|
+
Ok(rb_self)
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
fn insert_chart(
|
|
1143
|
+
ruby: &Ruby,
|
|
1144
|
+
rb_self: Obj<Self>,
|
|
1145
|
+
row: u32,
|
|
1146
|
+
col: u16,
|
|
1147
|
+
options: RHash,
|
|
1148
|
+
) -> Result<Obj<Self>, Error> {
|
|
1149
|
+
let nil = ruby.qnil().as_value();
|
|
1150
|
+
let arg_error = |msg: String| Error::new(ruby.exception_arg_error(), msg);
|
|
1151
|
+
let text = |v: Value| -> Result<Option<String>, Error> {
|
|
1152
|
+
if v.is_nil() {
|
|
1153
|
+
Ok(None)
|
|
1154
|
+
} else {
|
|
1155
|
+
String::try_convert(v).map(Some)
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
let opt = |name: &str| options.get(ruby.to_symbol(name)).unwrap_or(nil);
|
|
1159
|
+
check_keys(
|
|
1160
|
+
ruby,
|
|
1161
|
+
options,
|
|
1162
|
+
&[
|
|
1163
|
+
"type", "series", "title", "x_axis", "y_axis", "width", "height",
|
|
1164
|
+
],
|
|
1165
|
+
"insert_chart",
|
|
1166
|
+
)?;
|
|
1167
|
+
|
|
1168
|
+
let mut chart = Chart::new(choice(ruby, "chart type", opt("type"), CHART_TYPES)?);
|
|
1169
|
+
let series = RArray::try_convert(opt("series"))?;
|
|
1170
|
+
if series.is_empty() {
|
|
1171
|
+
return Err(arg_error(
|
|
1172
|
+
"series: needs at least one { values:, categories:, name: } hash".into(),
|
|
1173
|
+
));
|
|
1174
|
+
}
|
|
1175
|
+
for s in series.into_iter() {
|
|
1176
|
+
let s = RHash::try_convert(s)?;
|
|
1177
|
+
check_keys(ruby, s, &["values", "categories", "name"], "chart series")?;
|
|
1178
|
+
let get = |name: &str| s.get(ruby.to_symbol(name)).unwrap_or(nil);
|
|
1179
|
+
let values = text(get("values"))?
|
|
1180
|
+
.ok_or_else(|| arg_error(format!("series {} needs values:", s.inspect())))?;
|
|
1181
|
+
let cs = chart.add_series().set_values(values.as_str());
|
|
1182
|
+
if let Some(categories) = text(get("categories"))? {
|
|
1183
|
+
cs.set_categories(categories.as_str());
|
|
1184
|
+
}
|
|
1185
|
+
if let Some(name) = text(get("name"))? {
|
|
1186
|
+
cs.set_name(name.as_str());
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if let Some(title) = text(opt("title"))? {
|
|
1190
|
+
chart.title().set_name(title.as_str());
|
|
1191
|
+
}
|
|
1192
|
+
if let Some(name) = text(opt("x_axis"))? {
|
|
1193
|
+
chart.x_axis().set_name(name.as_str());
|
|
1194
|
+
}
|
|
1195
|
+
if let Some(name) = text(opt("y_axis"))? {
|
|
1196
|
+
chart.y_axis().set_name(name.as_str());
|
|
1197
|
+
}
|
|
1198
|
+
if !opt("width").is_nil() {
|
|
1199
|
+
chart.set_width(u32::try_convert(opt("width"))?);
|
|
1200
|
+
}
|
|
1201
|
+
if !opt("height").is_nil() {
|
|
1202
|
+
chart.set_height(u32::try_convert(opt("height"))?);
|
|
1203
|
+
}
|
|
1204
|
+
rb_self.with_ws(|ws| ws.insert_chart(row, col, &chart).map(|_| ()).map_err(xerr))?;
|
|
1205
|
+
Ok(rb_self)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
#[allow(clippy::too_many_arguments)]
|
|
1209
|
+
fn add_table(
|
|
1210
|
+
ruby: &Ruby,
|
|
1211
|
+
rb_self: Obj<Self>,
|
|
1212
|
+
fr: u32,
|
|
1213
|
+
fc: u16,
|
|
1214
|
+
lr: u32,
|
|
1215
|
+
lc: u16,
|
|
1216
|
+
options: RHash,
|
|
1217
|
+
) -> Result<Obj<Self>, Error> {
|
|
1218
|
+
check_keys(
|
|
1219
|
+
ruby,
|
|
1220
|
+
options,
|
|
1221
|
+
&[
|
|
1222
|
+
"columns",
|
|
1223
|
+
"style",
|
|
1224
|
+
"name",
|
|
1225
|
+
"total_row",
|
|
1226
|
+
"banded_rows",
|
|
1227
|
+
"autofilter",
|
|
1228
|
+
],
|
|
1229
|
+
"add_table",
|
|
1230
|
+
)?;
|
|
1231
|
+
let nil = ruby.qnil().as_value();
|
|
1232
|
+
let opt = |name: &str| options.get(ruby.to_symbol(name)).unwrap_or(nil);
|
|
1233
|
+
let flag = |name: &str, default: bool| {
|
|
1234
|
+
let v = opt(name);
|
|
1235
|
+
if v.is_nil() {
|
|
1236
|
+
default
|
|
1237
|
+
} else {
|
|
1238
|
+
v.to_bool()
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
// The table writes its header row, which must not be flushed yet.
|
|
1242
|
+
rb_self.check_not_flushed(ruby, fr)?;
|
|
1243
|
+
let mut table = Table::new()
|
|
1244
|
+
.set_total_row(flag("total_row", false))
|
|
1245
|
+
.set_banded_rows(flag("banded_rows", true))
|
|
1246
|
+
.set_autofilter(flag("autofilter", true));
|
|
1247
|
+
if !opt("columns").is_nil() {
|
|
1248
|
+
let specs = RArray::try_convert(opt("columns"))?;
|
|
1249
|
+
let width = usize::from(lc.saturating_sub(fc)) + 1;
|
|
1250
|
+
if specs.len() != width {
|
|
1251
|
+
return Err(Error::new(
|
|
1252
|
+
ruby.exception_arg_error(),
|
|
1253
|
+
format!(
|
|
1254
|
+
"columns: has {} entries but the range is {width} columns wide",
|
|
1255
|
+
specs.len()
|
|
1256
|
+
),
|
|
1257
|
+
));
|
|
1258
|
+
}
|
|
1259
|
+
let columns = specs
|
|
1260
|
+
.into_iter()
|
|
1261
|
+
.map(|v| table_column(ruby, v))
|
|
1262
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
1263
|
+
table = table.set_columns(&columns);
|
|
1264
|
+
}
|
|
1265
|
+
if !opt("style").is_nil() {
|
|
1266
|
+
table = table.set_style(choice(ruby, "table style", opt("style"), TABLE_STYLES)?);
|
|
1267
|
+
}
|
|
1268
|
+
if !opt("name").is_nil() {
|
|
1269
|
+
table = table.set_name(String::try_convert(opt("name"))?);
|
|
1270
|
+
}
|
|
1271
|
+
rb_self.with_ws(|ws| {
|
|
1272
|
+
ws.add_table(fr, fc, lr, lc, &table)
|
|
1273
|
+
.map(|_| ())
|
|
1274
|
+
.map_err(xerr)
|
|
1275
|
+
})?;
|
|
1276
|
+
// Continue appending under the header, so `<<` fills the table.
|
|
1277
|
+
rb_self.advance(fr);
|
|
1278
|
+
Ok(rb_self)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
fn name(&self) -> Result<String, Error> {
|
|
1282
|
+
self.with_ws(|ws| Ok(ws.name()))
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
fn next_row(&self) -> u32 {
|
|
1286
|
+
self.next_row.get()
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
#[magnus::init]
|
|
1291
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
1292
|
+
let module = ruby.define_module("FastXlsx")?;
|
|
1293
|
+
|
|
1294
|
+
let wb = module.define_class("Workbook", ruby.class_object())?;
|
|
1295
|
+
wb.define_singleton_method("_new", function!(Workbook::new, 2))?;
|
|
1296
|
+
wb.define_method("_add_worksheet", method!(Workbook::add_worksheet, 1))?;
|
|
1297
|
+
wb.define_method("to_xlsx", method!(Workbook::to_xlsx, 0))?;
|
|
1298
|
+
wb.define_method("save", method!(Workbook::save, 1))?;
|
|
1299
|
+
wb.define_method("_properties", method!(Workbook::set_properties, 1))?;
|
|
1300
|
+
|
|
1301
|
+
let ws = module.define_class("Worksheet", ruby.class_object())?;
|
|
1302
|
+
ws.define_method("_write", method!(Worksheet::write, 4))?;
|
|
1303
|
+
ws.define_method("_append", method!(Worksheet::append, 2))?;
|
|
1304
|
+
ws.define_method("_column_width", method!(Worksheet::set_column_width, 3))?;
|
|
1305
|
+
ws.define_method(
|
|
1306
|
+
"_column_format",
|
|
1307
|
+
method!(Worksheet::set_column_format, 3),
|
|
1308
|
+
)?;
|
|
1309
|
+
ws.define_method("_autofit", method!(Worksheet::autofit, 0))?;
|
|
1310
|
+
ws.define_method("autofilter", method!(Worksheet::autofilter, 4))?;
|
|
1311
|
+
ws.define_method("name", method!(Worksheet::name, 0))?;
|
|
1312
|
+
ws.define_method("_write_comment", method!(Worksheet::write_comment, 4))?;
|
|
1313
|
+
ws.define_method("_insert_image", method!(Worksheet::insert_image, 4))?;
|
|
1314
|
+
ws.define_method("_insert_chart", method!(Worksheet::insert_chart, 3))?;
|
|
1315
|
+
ws.define_method("_add_table", method!(Worksheet::add_table, 5))?;
|
|
1316
|
+
ws.define_method(
|
|
1317
|
+
"_conditional_format",
|
|
1318
|
+
method!(Worksheet::conditional_format, 5),
|
|
1319
|
+
)?;
|
|
1320
|
+
ws.define_method("_data_validation", method!(Worksheet::data_validation, 5))?;
|
|
1321
|
+
ws.define_method("freeze_panes", method!(Worksheet::freeze_panes, 2))?;
|
|
1322
|
+
ws.define_method("row_height", method!(Worksheet::set_row_height, 2))?;
|
|
1323
|
+
ws.define_method("page_breaks", method!(Worksheet::set_page_breaks, 1))?;
|
|
1324
|
+
ws.define_method("_page_header", method!(Worksheet::set_header, 1))?;
|
|
1325
|
+
ws.define_method("_page_footer", method!(Worksheet::set_footer, 1))?;
|
|
1326
|
+
ws.define_method("_margins", method!(Worksheet::set_margins, 6))?;
|
|
1327
|
+
ws.define_method(
|
|
1328
|
+
"vertical_page_breaks",
|
|
1329
|
+
method!(Worksheet::set_vertical_page_breaks, 1),
|
|
1330
|
+
)?;
|
|
1331
|
+
ws.define_method("_merge_range", method!(Worksheet::merge_range, 6))?;
|
|
1332
|
+
|
|
1333
|
+
let format = module.define_class("Format", ruby.class_object())?;
|
|
1334
|
+
format.define_singleton_method("_new", function!(Format::new, 1))?;
|
|
1335
|
+
ws.define_method("<<", method!(Worksheet::push, 1))?;
|
|
1336
|
+
ws.define_method("concat", method!(Worksheet::concat, 1))?;
|
|
1337
|
+
ws.define_method("next_row", method!(Worksheet::next_row, 0))?;
|
|
1338
|
+
Ok(())
|
|
1339
|
+
}
|