fast_xlsx 0.3.0 → 0.5.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 +4 -4
- data/.yardopts +7 -0
- data/CHANGELOG.md +17 -0
- data/README.md +31 -15
- data/bench/Gemfile.lock +2 -2
- data/ext/fast_xlsx/src/lib.rs +213 -58
- data/ext/fast_xlsx/tests/ruby_suite.rs +3 -3
- data/lib/fast_xlsx/version.rb +2 -1
- data/lib/fast_xlsx.rb +261 -55
- data/sig/fast_xlsx.rbs +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8326db5fb0c181c2c47af1f6b0d5799aa0bb69ff2d3b63d5286dbd1cd32fa2b8
|
|
4
|
+
data.tar.gz: 560335ade1b435b9084cb3d48e4e4b20d906140c4d2a6e243751d93d1eb01964
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 360e7a3686eb4705527139fc7c50834046aa4d45d7e3e79e6f913475cb039de2e64119600a8c9a0e1147136be98a6776f8bbe8db267b67533e99c390fdaced4e
|
|
7
|
+
data.tar.gz: c23307efb6c2c0ceb1faef4336be02512f98cda7bff60b01324e614d82374247187ee1225a04c04a1cf9208d8b4abce3852a943f166f57f53801c743c4df1af3
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- API documentation: every public method has YARD docs (shown on rubydoc.info).
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Errors follow one rule (see the README): a row or column outside the sheet raises `RangeError` (row 1,048,576, column 16,384 or a row of more than 16,384 cells raised `FastXlsx::Error` or `ArgumentError`); a reversed range or single-cell merge raises `ArgumentError` (was `FastXlsx::Error`); a header or footer over 255 characters raises `FastXlsx::Error` like other text over Excel's limits (was `ArgumentError`); a negative or NaN image size, font size or margin raises `ArgumentError` (was written into the file or ignored); `zoom` converts numbers with `to_int` like other arguments (`zoom(150.9)` raised `TypeError`); `column_width` outside 0..255 and `row_height` outside 0..409 raise `ArgumentError` (a negative one hid the column or row).
|
|
12
|
+
- Date and time cells written with a format that has no `num_format` (e.g. `append(row, format: bold)`) get the default date format added, instead of showing as serial numbers. This applies to a cell's own format, a table column's and `column_format`'s; a format with a `num_format` is used as is.
|
|
13
|
+
|
|
14
|
+
## [0.4.0] - 2026-10-01
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Dates and times written without a format now get `yyyy-mm-dd` (`Date`) or `yyyy-mm-dd hh:mm:ss` (`Time`, `DateTime`) instead of showing as serial numbers. A cell's own format, its table column's and `column_format` still win. Applying the format makes writing date-heavy data about 15% slower, the same as passing a date format yourself.
|
|
19
|
+
|
|
3
20
|
## [0.3.0] - 2026-10-01
|
|
4
21
|
|
|
5
22
|
### Changed
|
data/README.md
CHANGED
|
@@ -82,6 +82,8 @@ ws.write(1, 2, Date.today, date) # format one cell
|
|
|
82
82
|
|
|
83
83
|
Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
|
|
84
84
|
|
|
85
|
+
Dates and times are shown as `yyyy-mm-dd` or `yyyy-mm-dd hh:mm:ss`. A format without a `num_format` (yours, a table column's or `column_format`'s) keeps its look and gets that date format added, so `append(row, format: bold)` gives bold dates; a format with a `num_format` is used as is.
|
|
86
|
+
|
|
85
87
|
### Columns and filters
|
|
86
88
|
|
|
87
89
|
```ruby
|
|
@@ -250,8 +252,8 @@ Values are mapped by type:
|
|
|
250
252
|
|---|---|
|
|
251
253
|
| `Integer`, `Float`, any `Numeric` | number |
|
|
252
254
|
| `String` | string |
|
|
253
|
-
| `Time` |
|
|
254
|
-
| `Date
|
|
255
|
+
| `Time`, `DateTime` | date and time (`yyyy-mm-dd hh:mm:ss` unless formatted), in its own offset |
|
|
256
|
+
| `Date` | date (`yyyy-mm-dd` unless formatted) |
|
|
255
257
|
| `FastXlsx::Formula.new("SUM(A1:A9)")` | formula |
|
|
256
258
|
| `FastXlsx::URL.new("https://…")`, `URL.new(url, text: "Title")` | hyperlink (optionally showing other text) |
|
|
257
259
|
| `FastXlsx::RichString.new(["Total: ", bold], "1,234")` | text with a format per segment |
|
|
@@ -261,7 +263,16 @@ Values are mapped by type:
|
|
|
261
263
|
|
|
262
264
|
`<<` and `concat` append after the last row written to that worksheet. In `:constant` / `:low` memory mode rows are written to disk as you go, so fill each worksheet top to bottom.
|
|
263
265
|
|
|
264
|
-
Errors
|
|
266
|
+
### Errors
|
|
267
|
+
|
|
268
|
+
| Error | Raised for |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `TypeError` | an argument of the wrong type, e.g. a String where a row number goes |
|
|
271
|
+
| `RangeError` | a row or column outside the sheet (1,048,576 × 16,384), or a negative count (an infinite Float raises `FloatDomainError`, a `RangeError`) |
|
|
272
|
+
| `ArgumentError` | the right type but a value that isn't allowed: an unknown option or symbol, an invalid color or cell reference, a reversed range or single-cell merge, a size or margin that is negative, NaN or beyond Excel's limits |
|
|
273
|
+
| `FastXlsx::Error` | what the workbook can't do: duplicate or invalid names, writing to rows already on disk, overlapping merges, hiding the sheet Excel opens on, text over Excel's limits (cells, URLs, headers and footers), unsupported images |
|
|
274
|
+
|
|
275
|
+
Numbers are converted like Ruby's own methods do (`to_int`), so `1.9` as a row is row `1`.
|
|
265
276
|
|
|
266
277
|
## Performance
|
|
267
278
|
|
|
@@ -273,17 +284,17 @@ Apple Silicon, Ruby 4.0.5. Each library uses its own idiomatic row-append API; x
|
|
|
273
284
|
|
|
274
285
|
| Library | Time | vs fastest | Ruby objects allocated |
|
|
275
286
|
|---|---:|---:|---:|
|
|
276
|
-
| **fast_xlsx** (`memory: :constant`) | **
|
|
277
|
-
| **fast_xlsx** (`memory: :low`) | **
|
|
278
|
-
| **fast_xlsx** | **
|
|
279
|
-
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 |
|
|
280
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) |
|
|
281
|
-
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 |
|
|
282
|
-
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 |
|
|
283
|
-
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 |
|
|
284
|
-
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 |
|
|
285
|
-
|
|
286
|
-
All outputs are
|
|
287
|
+
| **fast_xlsx** (`memory: :constant`) | **77 ms** | 1.0x | 7 |
|
|
288
|
+
| **fast_xlsx** (`memory: :low`) | **86 ms** | 1.1x | 7 |
|
|
289
|
+
| **fast_xlsx** | **89 ms** | 1.1x | 10 |
|
|
290
|
+
| [xlsxtream](https://github.com/felixbuenemann/xlsxtream) 3.1 | 183 ms | 2.4x | 561,728 |
|
|
291
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 (constant_memory) | 200 ms | 2.6x | 20,079 |
|
|
292
|
+
| [fast_excel](https://github.com/Paxa/fast_excel) 0.5 | 238 ms | 3.1x | 320,076 |
|
|
293
|
+
| [write_xlsx](https://github.com/cxn03651/write_xlsx) 1.15 | 608 ms | 7.8x | 1,483,899 |
|
|
294
|
+
| [caxlsx](https://github.com/caxlsx/caxlsx) 4.5 | 689 ms | 8.9x | 745,122 |
|
|
295
|
+
| [rubyXL](https://github.com/weshatheleopard/rubyXL) 3.4 | 2755 ms | 35.6x | 8,700,448 |
|
|
296
|
+
|
|
297
|
+
All outputs are 689–750 KB.
|
|
287
298
|
|
|
288
299
|
### Memory
|
|
289
300
|
|
|
@@ -321,7 +332,12 @@ BUNDLE_GEMFILE=bench/Gemfile /usr/bin/time -l bundle exec ruby bench/memory.rb f
|
|
|
321
332
|
bundle add fast_xlsx
|
|
322
333
|
```
|
|
323
334
|
|
|
324
|
-
|
|
335
|
+
### Support and versioning
|
|
336
|
+
|
|
337
|
+
- **Ruby:** CRuby 3.3 and later. A Ruby version is dropped only after it reaches its end of life, and only in a minor release. JRuby and TruffleRuby are not supported (this is a native extension).
|
|
338
|
+
- **Precompiled gems:** Linux (x86_64 and aarch64, glibc and musl; ARM musl), macOS (arm64 and x86_64) and Windows (x64). Before each release the Linux x86_64, macOS arm64 and Windows gems are installed and loaded on Ruby 3.3, 3.4 and 4.0. Other platforms build from source and need a Rust toolchain.
|
|
339
|
+
- **Versions:** from 1.0, [Semantic Versioning](https://semver.org): no breaking API change before 2.0. Until then a minor release (0.x.0) can change behaviour; the [changelog](CHANGELOG.md) lists every change under "Changed".
|
|
340
|
+
- **Output:** `.xlsx` files that open in Excel 2007 and later, LibreOffice and Google Sheets. Updating rust_xlsxwriter can change the bytes of the file but not what it contains.
|
|
325
341
|
|
|
326
342
|
## Development
|
|
327
343
|
|
data/bench/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
fast_xlsx (0.
|
|
4
|
+
fast_xlsx (0.5.0)
|
|
5
5
|
rb_sys (~> 0.9.130)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -77,7 +77,7 @@ DEPENDENCIES
|
|
|
77
77
|
CHECKSUMS
|
|
78
78
|
caxlsx (4.5.0) sha256=e3d98d859f148df05d5462086b5079b523f29c1766b569535f1d68629ce743ff
|
|
79
79
|
fast_excel (0.5.0) sha256=59c418bdcf586a6030798d4e3a9f575742badaceaf7bb90f38eee5feb5bdc478
|
|
80
|
-
fast_xlsx (0.
|
|
80
|
+
fast_xlsx (0.5.0)
|
|
81
81
|
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
|
82
82
|
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
|
83
83
|
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
|
data/ext/fast_xlsx/src/lib.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::cell::{Cell, RefCell};
|
|
2
2
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
3
|
+
use std::sync::LazyLock;
|
|
3
4
|
use std::sync::{Arc, Mutex};
|
|
4
5
|
|
|
5
6
|
use magnus::{
|
|
@@ -33,9 +34,43 @@ static FORMULA: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Formula")
|
|
|
33
34
|
static URL: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "URL"));
|
|
34
35
|
static RICH_STRING: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "RichString"));
|
|
35
36
|
|
|
37
|
+
// A row or column outside the sheet is a RangeError, like a negative one
|
|
38
|
+
// (which fails converting to an unsigned number before reaching the writer).
|
|
36
39
|
fn xerr(e: XlsxError) -> Error {
|
|
37
40
|
let ruby = Ruby::get().unwrap();
|
|
38
|
-
|
|
41
|
+
let class = match e {
|
|
42
|
+
XlsxError::RowColumnLimitError => ruby.exception_range_error(),
|
|
43
|
+
// Values that aren't allowed, like a reversed range.
|
|
44
|
+
XlsxError::RowColumnOrderError | XlsxError::MergeRangeSingleCell => {
|
|
45
|
+
ruby.exception_arg_error()
|
|
46
|
+
}
|
|
47
|
+
_ => ruby.get_inner(&ERROR),
|
|
48
|
+
};
|
|
49
|
+
Error::new(class, e.to_string())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Excel's limits that rust_xlsxwriter doesn't check: a negative width or
|
|
53
|
+
// height hides the column or row, a larger one is capped or invalid.
|
|
54
|
+
// A size that must be a finite number above 0 (NaN or a negative one would
|
|
55
|
+
// be written into the file as is).
|
|
56
|
+
fn check_positive(ruby: &Ruby, what: &str, size: f64) -> Result<f64, Error> {
|
|
57
|
+
if size.is_finite() && size > 0.0 {
|
|
58
|
+
return Ok(size);
|
|
59
|
+
}
|
|
60
|
+
Err(Error::new(
|
|
61
|
+
ruby.exception_arg_error(),
|
|
62
|
+
format!("invalid {what} {size}: use a number above 0"),
|
|
63
|
+
))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn check_size(ruby: &Ruby, what: &str, size: f64, max: f64) -> Result<(), Error> {
|
|
67
|
+
if (0.0..=max).contains(&size) {
|
|
68
|
+
return Ok(());
|
|
69
|
+
}
|
|
70
|
+
Err(Error::new(
|
|
71
|
+
ruby.exception_arg_error(),
|
|
72
|
+
format!("invalid {what} {size}: use 0..{max}"),
|
|
73
|
+
))
|
|
39
74
|
}
|
|
40
75
|
|
|
41
76
|
type Shared = Arc<Mutex<rust_xlsxwriter::Workbook>>;
|
|
@@ -112,12 +147,14 @@ struct Worksheet {
|
|
|
112
147
|
table_formats: RefCell<Vec<TableColumnFormat>>,
|
|
113
148
|
// Merged ranges as (first_row, first_col, last_row, last_col).
|
|
114
149
|
merges: RefCell<Vec<(u32, u16, u32, u16)>>,
|
|
150
|
+
// Formats given with column_format: (first, last, style).
|
|
151
|
+
formatted_columns: RefCell<Vec<(u16, u16, Arc<Style>)>>,
|
|
115
152
|
}
|
|
116
153
|
|
|
117
154
|
struct TableColumnFormat {
|
|
118
155
|
rows: std::ops::RangeInclusive<u32>,
|
|
119
156
|
col: u16,
|
|
120
|
-
format: Arc<
|
|
157
|
+
format: Arc<Style>,
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
impl Workbook {
|
|
@@ -179,6 +216,7 @@ impl Workbook {
|
|
|
179
216
|
flushes_rows: self.constant_memory || self.low_memory,
|
|
180
217
|
table_formats: RefCell::new(Vec::new()),
|
|
181
218
|
merges: RefCell::new(Vec::new()),
|
|
219
|
+
formatted_columns: RefCell::new(Vec::new()),
|
|
182
220
|
})
|
|
183
221
|
}
|
|
184
222
|
|
|
@@ -297,11 +335,13 @@ enum CellValue {
|
|
|
297
335
|
Empty,
|
|
298
336
|
Text(String),
|
|
299
337
|
Number(f64),
|
|
338
|
+
// An Excel serial date; true when it has a time of day (Time, DateTime).
|
|
339
|
+
Date(f64, bool),
|
|
300
340
|
Bool(bool),
|
|
301
341
|
Formula(String),
|
|
302
342
|
Url(String, Option<String>),
|
|
303
343
|
// RichString segments; None means the default font.
|
|
304
|
-
Rich(Vec<(Option<Arc<
|
|
344
|
+
Rich(Vec<(Option<Arc<Style>>, String)>),
|
|
305
345
|
}
|
|
306
346
|
|
|
307
347
|
impl CellValue {
|
|
@@ -315,7 +355,7 @@ impl CellValue {
|
|
|
315
355
|
} else if v.is_kind_of(ruby.class_numeric()) {
|
|
316
356
|
CellValue::Number(f64::try_convert(v)?)
|
|
317
357
|
} else if v.is_kind_of(ruby.class_time()) {
|
|
318
|
-
CellValue::
|
|
358
|
+
CellValue::Date(excel_time(v)?, true)
|
|
319
359
|
} else if v.is_kind_of(ruby.class_true_class()) || v.is_kind_of(ruby.class_false_class()) {
|
|
320
360
|
CellValue::Bool(v.to_bool())
|
|
321
361
|
} else if v.is_kind_of(ruby.get_inner(&FORMULA)) {
|
|
@@ -333,7 +373,8 @@ impl CellValue {
|
|
|
333
373
|
})?;
|
|
334
374
|
CellValue::Rich(parts)
|
|
335
375
|
} else if v.respond_to("jd", false)? {
|
|
336
|
-
|
|
376
|
+
// Date has no #hour; DateTime does.
|
|
377
|
+
CellValue::Date(excel_date(v)?, v.respond_to("hour", false)?)
|
|
337
378
|
} else {
|
|
338
379
|
CellValue::Text(v.funcall("to_s", ())?)
|
|
339
380
|
};
|
|
@@ -341,6 +382,14 @@ impl CellValue {
|
|
|
341
382
|
Ok(value)
|
|
342
383
|
}
|
|
343
384
|
|
|
385
|
+
// Some(has a time of day) for date cells.
|
|
386
|
+
fn date_kind(&self) -> Option<bool> {
|
|
387
|
+
match self {
|
|
388
|
+
CellValue::Date(_, time) => Some(*time),
|
|
389
|
+
_ => None,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
344
393
|
// Rejects what the writer would, so a row or merge fails before anything
|
|
345
394
|
// is written. Numbers, booleans and formulas only fail on a bad row/column.
|
|
346
395
|
fn check(&self) -> Result<(), Error> {
|
|
@@ -371,7 +420,7 @@ impl CellValue {
|
|
|
371
420
|
match self {
|
|
372
421
|
CellValue::Empty => Ok(()),
|
|
373
422
|
CellValue::Text(s) => emit(ws, row, col, s.as_str(), format),
|
|
374
|
-
CellValue::Number(n) => emit(ws, row, col, *n, format),
|
|
423
|
+
CellValue::Number(n) | CellValue::Date(n, _) => emit(ws, row, col, *n, format),
|
|
375
424
|
CellValue::Bool(b) => emit(ws, row, col, *b, format),
|
|
376
425
|
CellValue::Formula(f) => emit(
|
|
377
426
|
ws,
|
|
@@ -391,7 +440,7 @@ impl CellValue {
|
|
|
391
440
|
let default = rust_xlsxwriter::Format::default();
|
|
392
441
|
let rich: Vec<(&rust_xlsxwriter::Format, &str)> = parts
|
|
393
442
|
.iter()
|
|
394
|
-
.map(|(f, text)| (f.as_deref().
|
|
443
|
+
.map(|(f, text)| (f.as_deref().map_or(&default, |s| &s.format), text.as_str()))
|
|
395
444
|
.collect();
|
|
396
445
|
match format {
|
|
397
446
|
Some(f) => ws.write_rich_string_with_format(row, col, &rich, f),
|
|
@@ -562,7 +611,7 @@ fn check_header_footer(ruby: &Ruby, what: &str, text: &str) -> Result<(), Error>
|
|
|
562
611
|
let len = expanded.chars().count();
|
|
563
612
|
if len > 255 {
|
|
564
613
|
return Err(Error::new(
|
|
565
|
-
ruby.
|
|
614
|
+
ruby.get_inner(&ERROR),
|
|
566
615
|
format!("{what} is {len} characters; Excel allows 255"),
|
|
567
616
|
));
|
|
568
617
|
}
|
|
@@ -693,10 +742,7 @@ const TABLE_TOTALS: &[(&str, TableFunction)] = &[
|
|
|
693
742
|
|
|
694
743
|
// A table column: a header String, or { header:, total:, total_label:, format: }.
|
|
695
744
|
// Also returns a copy of the column's format, if any, for later writes.
|
|
696
|
-
fn table_column(
|
|
697
|
-
ruby: &Ruby,
|
|
698
|
-
v: Value,
|
|
699
|
-
) -> Result<(TableColumn, Option<Arc<rust_xlsxwriter::Format>>), Error> {
|
|
745
|
+
fn table_column(ruby: &Ruby, v: Value) -> Result<(TableColumn, Option<Arc<Style>>), Error> {
|
|
700
746
|
let Some(spec) = RHash::from_value(v) else {
|
|
701
747
|
return Ok((TableColumn::new().set_header(String::try_convert(v)?), None));
|
|
702
748
|
};
|
|
@@ -716,7 +762,7 @@ fn table_column(
|
|
|
716
762
|
}
|
|
717
763
|
let format = opt::<&Format>(ruby, spec, "format")?.map(|f| f.0.clone());
|
|
718
764
|
if let Some(f) = &format {
|
|
719
|
-
column = column.set_format(
|
|
765
|
+
column = column.set_format(&f.format);
|
|
720
766
|
}
|
|
721
767
|
Ok((column, format))
|
|
722
768
|
}
|
|
@@ -761,11 +807,52 @@ const BORDERS: &[(&str, FormatBorder)] = &[
|
|
|
761
807
|
];
|
|
762
808
|
|
|
763
809
|
#[magnus::wrap(class = "FastXlsx::Format", free_immediately)]
|
|
764
|
-
struct Format(Arc<
|
|
810
|
+
struct Format(Arc<Style>);
|
|
811
|
+
|
|
812
|
+
// A Format as written: the format itself, and for one without a num_format,
|
|
813
|
+
// copies with the default date formats added, used for date cells (Excel
|
|
814
|
+
// shows a date with no number format as its serial number).
|
|
815
|
+
struct Style {
|
|
816
|
+
format: rust_xlsxwriter::Format,
|
|
817
|
+
date: Option<rust_xlsxwriter::Format>,
|
|
818
|
+
date_time: Option<rust_xlsxwriter::Format>,
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const DATE_FORMAT: &str = "yyyy-mm-dd";
|
|
822
|
+
const DATE_TIME_FORMAT: &str = "yyyy-mm-dd hh:mm:ss";
|
|
823
|
+
|
|
824
|
+
impl Style {
|
|
825
|
+
fn new(format: rust_xlsxwriter::Format, has_num_format: bool) -> Self {
|
|
826
|
+
let with =
|
|
827
|
+
|num_format: &str| (!has_num_format).then(|| format.clone().set_num_format(num_format));
|
|
828
|
+
Style {
|
|
829
|
+
date: with(DATE_FORMAT),
|
|
830
|
+
date_time: with(DATE_TIME_FORMAT),
|
|
831
|
+
format,
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// The date variant for a date cell (`time`: it has a time of day), if
|
|
836
|
+
// this format has no num_format of its own.
|
|
837
|
+
fn for_date(&self, time: bool) -> Option<&rust_xlsxwriter::Format> {
|
|
838
|
+
if time {
|
|
839
|
+
self.date_time.as_ref()
|
|
840
|
+
} else {
|
|
841
|
+
self.date.as_ref()
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// What a value is written with: `date` is Some(has time) for date cells.
|
|
846
|
+
fn pick(&self, date: Option<bool>) -> &rust_xlsxwriter::Format {
|
|
847
|
+
date.and_then(|time| self.for_date(time))
|
|
848
|
+
.unwrap_or(&self.format)
|
|
849
|
+
}
|
|
850
|
+
}
|
|
765
851
|
|
|
766
852
|
impl Format {
|
|
767
853
|
fn new(ruby: &Ruby, options: RHash) -> Result<Self, Error> {
|
|
768
854
|
let mut f = rust_xlsxwriter::Format::new();
|
|
855
|
+
let mut has_num_format = false;
|
|
769
856
|
options.foreach(|key: Symbol, value: Value| {
|
|
770
857
|
let taken = std::mem::take(&mut f);
|
|
771
858
|
f = match &*key.name()? {
|
|
@@ -813,8 +900,15 @@ impl Format {
|
|
|
813
900
|
"locked" => taken.set_unlocked(),
|
|
814
901
|
"hidden" if value.to_bool() => taken.set_hidden(),
|
|
815
902
|
"border_color" => taken.set_border_color(color(ruby, value)?),
|
|
816
|
-
"num_format" =>
|
|
817
|
-
|
|
903
|
+
"num_format" => {
|
|
904
|
+
has_num_format = true;
|
|
905
|
+
taken.set_num_format(String::try_convert(value)?)
|
|
906
|
+
}
|
|
907
|
+
"font_size" => taken.set_font_size(check_positive(
|
|
908
|
+
ruby,
|
|
909
|
+
"font_size",
|
|
910
|
+
f64::try_convert(value)?,
|
|
911
|
+
)?),
|
|
818
912
|
"font_name" => taken.set_font_name(String::try_convert(value)?),
|
|
819
913
|
"font_color" => taken.set_font_color(color(ruby, value)?),
|
|
820
914
|
"bg_color" => taken.set_background_color(color(ruby, value)?),
|
|
@@ -855,7 +949,7 @@ impl Format {
|
|
|
855
949
|
};
|
|
856
950
|
Ok(ForEach::Continue)
|
|
857
951
|
})?;
|
|
858
|
-
Ok(Format(Arc::new(f)))
|
|
952
|
+
Ok(Format(Arc::new(Style::new(f, has_num_format))))
|
|
859
953
|
}
|
|
860
954
|
}
|
|
861
955
|
|
|
@@ -878,7 +972,7 @@ fn each_entry(
|
|
|
878
972
|
|
|
879
973
|
// A row's format: one Format (or nil) for every cell, or an Array with one per cell.
|
|
880
974
|
enum RowFormat {
|
|
881
|
-
Same(Option<Arc<
|
|
975
|
+
Same(Option<Arc<Style>>),
|
|
882
976
|
PerCell(RArray),
|
|
883
977
|
}
|
|
884
978
|
|
|
@@ -892,7 +986,7 @@ impl RowFormat {
|
|
|
892
986
|
}
|
|
893
987
|
}
|
|
894
988
|
|
|
895
|
-
fn at(&self, col: usize) -> Result<Option<Arc<
|
|
989
|
+
fn at(&self, col: usize) -> Result<Option<Arc<Style>>, Error> {
|
|
896
990
|
match self {
|
|
897
991
|
RowFormat::Same(f) => Ok(f.clone()),
|
|
898
992
|
RowFormat::PerCell(formats) => Ok(Option::<&Format>::try_convert(
|
|
@@ -934,20 +1028,42 @@ impl Worksheet {
|
|
|
934
1028
|
f(wb.worksheet_from_index(self.index).map_err(xerr)?)
|
|
935
1029
|
}
|
|
936
1030
|
|
|
937
|
-
// The format
|
|
938
|
-
|
|
1031
|
+
// The format a value is written with: its own, else its table column's.
|
|
1032
|
+
// A date cell takes that format's date variant (when it has no
|
|
1033
|
+
// num_format); with neither, its column_format's date variant, or the
|
|
1034
|
+
// default date format when the column has no format. Other cells without
|
|
1035
|
+
// one get None, so rust_xlsxwriter applies any column format itself.
|
|
1036
|
+
fn format_for<'a>(
|
|
939
1037
|
tables: &'a [TableColumnFormat],
|
|
1038
|
+
columns: &'a [(u16, u16, Arc<Style>)],
|
|
940
1039
|
row: u32,
|
|
941
1040
|
col: u16,
|
|
942
|
-
own: Option<&'a
|
|
1041
|
+
own: Option<&'a Style>,
|
|
1042
|
+
value: &CellValue,
|
|
943
1043
|
) -> Option<&'a rust_xlsxwriter::Format> {
|
|
944
|
-
|
|
1044
|
+
static DEFAULTS: LazyLock<Style> =
|
|
1045
|
+
LazyLock::new(|| Style::new(rust_xlsxwriter::Format::new(), false));
|
|
1046
|
+
let date = value.date_kind();
|
|
1047
|
+
let style = own.or_else(|| {
|
|
945
1048
|
tables
|
|
946
1049
|
.iter()
|
|
947
1050
|
.rev()
|
|
948
1051
|
.find(|t| t.col == col && t.rows.contains(&row))
|
|
949
1052
|
.map(|t| &*t.format)
|
|
950
|
-
})
|
|
1053
|
+
});
|
|
1054
|
+
if let Some(style) = style {
|
|
1055
|
+
return Some(style.pick(date));
|
|
1056
|
+
}
|
|
1057
|
+
let time = date?;
|
|
1058
|
+
match columns
|
|
1059
|
+
.iter()
|
|
1060
|
+
.rev()
|
|
1061
|
+
.find(|(first, last, _)| (*first..=*last).contains(&col))
|
|
1062
|
+
{
|
|
1063
|
+
// None when it has a num_format: rust_xlsxwriter applies it.
|
|
1064
|
+
Some((_, _, style)) => style.for_date(time),
|
|
1065
|
+
None => DEFAULTS.for_date(time),
|
|
1066
|
+
}
|
|
951
1067
|
}
|
|
952
1068
|
|
|
953
1069
|
fn write_row(
|
|
@@ -963,7 +1079,7 @@ impl Worksheet {
|
|
|
963
1079
|
each_entry(cells, |i, v| {
|
|
964
1080
|
if i >= MAX_COLS {
|
|
965
1081
|
return Err(Error::new(
|
|
966
|
-
ruby.
|
|
1082
|
+
ruby.exception_range_error(),
|
|
967
1083
|
format!("too many columns: Excel allows {MAX_COLS}"),
|
|
968
1084
|
));
|
|
969
1085
|
}
|
|
@@ -972,11 +1088,13 @@ impl Worksheet {
|
|
|
972
1088
|
Ok(())
|
|
973
1089
|
})?;
|
|
974
1090
|
let tables = self.table_formats.borrow();
|
|
1091
|
+
let columns = self.formatted_columns.borrow();
|
|
975
1092
|
self.with_ws(|ws| {
|
|
976
1093
|
// Values were checked when converted, so only a bad row number
|
|
977
1094
|
// fails here, and it fails on the first cell.
|
|
978
1095
|
for (col, value, format) in &values {
|
|
979
|
-
let format =
|
|
1096
|
+
let format =
|
|
1097
|
+
Self::format_for(&tables, &columns, row, *col, format.as_deref(), value);
|
|
980
1098
|
value.write(ws, row, *col, format)?;
|
|
981
1099
|
}
|
|
982
1100
|
Ok(())
|
|
@@ -1020,12 +1138,13 @@ impl Worksheet {
|
|
|
1020
1138
|
rb_self.check_not_flushed(ruby, row)?;
|
|
1021
1139
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1022
1140
|
let tables = rb_self.table_formats.borrow();
|
|
1141
|
+
let columns = rb_self.formatted_columns.borrow();
|
|
1023
1142
|
rb_self.with_ws(|ws| {
|
|
1024
1143
|
value.write(
|
|
1025
1144
|
ws,
|
|
1026
1145
|
row,
|
|
1027
1146
|
col,
|
|
1028
|
-
Self::
|
|
1147
|
+
Self::format_for(&tables, &columns, row, col, format.map(|f| &*f.0), &value),
|
|
1029
1148
|
)
|
|
1030
1149
|
})?;
|
|
1031
1150
|
rb_self.advance(row);
|
|
@@ -1057,8 +1176,15 @@ impl Worksheet {
|
|
|
1057
1176
|
Ok(rb_self)
|
|
1058
1177
|
}
|
|
1059
1178
|
|
|
1060
|
-
fn set_column_width(
|
|
1061
|
-
|
|
1179
|
+
fn set_column_width(
|
|
1180
|
+
ruby: &Ruby,
|
|
1181
|
+
rb_self: &Self,
|
|
1182
|
+
first: u16,
|
|
1183
|
+
last: u16,
|
|
1184
|
+
width: f64,
|
|
1185
|
+
) -> Result<(), Error> {
|
|
1186
|
+
check_size(ruby, "column width", width, 255.0)?;
|
|
1187
|
+
rb_self.with_ws(|ws| {
|
|
1062
1188
|
ws.set_column_range_width(first, last, width)
|
|
1063
1189
|
.map(|_| ())
|
|
1064
1190
|
.map_err(xerr)
|
|
@@ -1067,10 +1193,14 @@ impl Worksheet {
|
|
|
1067
1193
|
|
|
1068
1194
|
fn set_column_format(&self, first: u16, last: u16, format: &Format) -> Result<(), Error> {
|
|
1069
1195
|
self.with_ws(|ws| {
|
|
1070
|
-
ws.set_column_range_format(first, last, &format.0)
|
|
1196
|
+
ws.set_column_range_format(first, last, &format.0.format)
|
|
1071
1197
|
.map(|_| ())
|
|
1072
1198
|
.map_err(xerr)
|
|
1073
|
-
})
|
|
1199
|
+
})?;
|
|
1200
|
+
self.formatted_columns
|
|
1201
|
+
.borrow_mut()
|
|
1202
|
+
.push((first, last, format.0.clone()));
|
|
1203
|
+
Ok(())
|
|
1074
1204
|
}
|
|
1075
1205
|
|
|
1076
1206
|
fn autofilter(
|
|
@@ -1101,7 +1231,13 @@ impl Worksheet {
|
|
|
1101
1231
|
Ok(rb_self)
|
|
1102
1232
|
}
|
|
1103
1233
|
|
|
1104
|
-
fn set_row_height(
|
|
1234
|
+
fn set_row_height(
|
|
1235
|
+
ruby: &Ruby,
|
|
1236
|
+
rb_self: Obj<Self>,
|
|
1237
|
+
row: u32,
|
|
1238
|
+
height: f64,
|
|
1239
|
+
) -> Result<Obj<Self>, Error> {
|
|
1240
|
+
check_size(ruby, "row height", height, 409.0)?;
|
|
1105
1241
|
rb_self.with_ws(|ws| ws.set_row_height(row, height).map(|_| ()).map_err(xerr))?;
|
|
1106
1242
|
Ok(rb_self)
|
|
1107
1243
|
}
|
|
@@ -1122,17 +1258,32 @@ impl Worksheet {
|
|
|
1122
1258
|
})
|
|
1123
1259
|
}
|
|
1124
1260
|
|
|
1125
|
-
// Inches;
|
|
1261
|
+
// Inches; None keeps the current margin.
|
|
1262
|
+
#[allow(clippy::too_many_arguments)]
|
|
1126
1263
|
fn set_margins(
|
|
1127
|
-
&
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1264
|
+
ruby: &Ruby,
|
|
1265
|
+
rb_self: &Self,
|
|
1266
|
+
left: Option<f64>,
|
|
1267
|
+
right: Option<f64>,
|
|
1268
|
+
top: Option<f64>,
|
|
1269
|
+
bottom: Option<f64>,
|
|
1270
|
+
header: Option<f64>,
|
|
1271
|
+
footer: Option<f64>,
|
|
1134
1272
|
) -> Result<(), Error> {
|
|
1135
|
-
|
|
1273
|
+
// rust_xlsxwriter keeps a margin given as a negative number.
|
|
1274
|
+
let inches = |margin: Option<f64>| -> Result<f64, Error> {
|
|
1275
|
+
match margin {
|
|
1276
|
+
None => Ok(-1.0),
|
|
1277
|
+
Some(m) if m.is_finite() && m >= 0.0 => Ok(m),
|
|
1278
|
+
Some(m) => Err(Error::new(
|
|
1279
|
+
ruby.exception_arg_error(),
|
|
1280
|
+
format!("invalid margin {m}: use 0 or more inches"),
|
|
1281
|
+
)),
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
let (left, right, top) = (inches(left)?, inches(right)?, inches(top)?);
|
|
1285
|
+
let (bottom, header, footer) = (inches(bottom)?, inches(header)?, inches(footer)?);
|
|
1286
|
+
rb_self.with_ws(|ws| {
|
|
1136
1287
|
ws.set_margins(left, right, top, bottom, header, footer);
|
|
1137
1288
|
Ok(())
|
|
1138
1289
|
})
|
|
@@ -1193,7 +1344,15 @@ impl Worksheet {
|
|
|
1193
1344
|
}
|
|
1194
1345
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1195
1346
|
let tables = rb_self.table_formats.borrow();
|
|
1196
|
-
let
|
|
1347
|
+
let columns = rb_self.formatted_columns.borrow();
|
|
1348
|
+
let format = Self::format_for(
|
|
1349
|
+
&tables,
|
|
1350
|
+
&columns,
|
|
1351
|
+
first_row,
|
|
1352
|
+
first_col,
|
|
1353
|
+
format.map(|f| &*f.0),
|
|
1354
|
+
&value,
|
|
1355
|
+
);
|
|
1197
1356
|
let default = rust_xlsxwriter::Format::new();
|
|
1198
1357
|
rb_self.with_ws(|ws| {
|
|
1199
1358
|
// The value was checked when converted, so once the range is
|
|
@@ -1260,7 +1419,7 @@ impl Worksheet {
|
|
|
1260
1419
|
let mut cf =
|
|
1261
1420
|
ConditionalFormatCell::new().set_rule(cell_rule(ruby, criteria, value)?);
|
|
1262
1421
|
if let Some(f) = format {
|
|
1263
|
-
cf = cf.set_format(
|
|
1422
|
+
cf = cf.set_format(&f.0.format);
|
|
1264
1423
|
}
|
|
1265
1424
|
rule(range, cf)
|
|
1266
1425
|
}
|
|
@@ -1284,7 +1443,7 @@ impl Worksheet {
|
|
|
1284
1443
|
};
|
|
1285
1444
|
let mut cf = ConditionalFormatText::new().set_rule(text_rule);
|
|
1286
1445
|
if let Some(f) = format {
|
|
1287
|
-
cf = cf.set_format(
|
|
1446
|
+
cf = cf.set_format(&f.0.format);
|
|
1288
1447
|
}
|
|
1289
1448
|
rule(range, cf)
|
|
1290
1449
|
}
|
|
@@ -1292,7 +1451,7 @@ impl Worksheet {
|
|
|
1292
1451
|
let mut cf =
|
|
1293
1452
|
ConditionalFormatFormula::new().set_rule(String::try_convert(value)?.as_str());
|
|
1294
1453
|
if let Some(f) = format {
|
|
1295
|
-
cf = cf.set_format(
|
|
1454
|
+
cf = cf.set_format(&f.0.format);
|
|
1296
1455
|
}
|
|
1297
1456
|
rule(range, cf)
|
|
1298
1457
|
}
|
|
@@ -1423,9 +1582,14 @@ impl Worksheet {
|
|
|
1423
1582
|
)?;
|
|
1424
1583
|
// SAFETY: the bytes are copied into the Image before any Ruby code runs.
|
|
1425
1584
|
let mut image = Image::new_from_buffer(unsafe { bytes.as_slice() }).map_err(xerr)?;
|
|
1426
|
-
let
|
|
1427
|
-
|
|
1428
|
-
|
|
1585
|
+
let positive = |name: &str| -> Result<Option<f64>, Error> {
|
|
1586
|
+
opt::<f64>(ruby, options, name)?
|
|
1587
|
+
.map(|v| check_positive(ruby, name, v))
|
|
1588
|
+
.transpose()
|
|
1589
|
+
};
|
|
1590
|
+
let width = positive("width")?;
|
|
1591
|
+
let height = positive("height")?;
|
|
1592
|
+
if let Some(scale) = positive("scale")? {
|
|
1429
1593
|
if width.is_some() || height.is_some() {
|
|
1430
1594
|
return Err(Error::new(
|
|
1431
1595
|
ruby.exception_arg_error(),
|
|
@@ -1634,16 +1798,7 @@ impl Worksheet {
|
|
|
1634
1798
|
Ok(rb_self)
|
|
1635
1799
|
}
|
|
1636
1800
|
|
|
1637
|
-
fn zoom(ruby: &Ruby, rb_self: Obj<Self>, percent:
|
|
1638
|
-
// Integers only: converting would quietly truncate 150.9.
|
|
1639
|
-
let percent = Integer::from_value(percent)
|
|
1640
|
-
.ok_or_else(|| {
|
|
1641
|
-
Error::new(
|
|
1642
|
-
ruby.exception_type_error(),
|
|
1643
|
-
format!("zoom must be an Integer, got {}", percent.inspect()),
|
|
1644
|
-
)
|
|
1645
|
-
})?
|
|
1646
|
-
.to_i64()?;
|
|
1801
|
+
fn zoom(ruby: &Ruby, rb_self: Obj<Self>, percent: i64) -> Result<Obj<Self>, Error> {
|
|
1647
1802
|
// rust_xlsxwriter only prints a warning for these.
|
|
1648
1803
|
if !(10..=400).contains(&percent) {
|
|
1649
1804
|
return Err(Error::new(
|
|
@@ -14,9 +14,9 @@ fn ruby_test_suite_passes() {
|
|
|
14
14
|
.args(["exec", "rake", "compile", "test"])
|
|
15
15
|
.current_dir(&root)
|
|
16
16
|
.env("RB_SYS_CARGO_PROFILE", "dev")
|
|
17
|
-
// The suite
|
|
18
|
-
//
|
|
19
|
-
.env("
|
|
17
|
+
// The suite takes seconds; a mutant that hangs (a loop or a deadlock)
|
|
18
|
+
// is killed instead of running on after cargo-mutants gives up on it.
|
|
19
|
+
.env("FAST_XLSX_TEST_TIMEOUT_SECONDS", "120")
|
|
20
20
|
.status()
|
|
21
21
|
.expect("could not run `bundle exec rake compile test`");
|
|
22
22
|
assert!(status.success(), "the Ruby test suite failed");
|
data/lib/fast_xlsx/version.rb
CHANGED
data/lib/fast_xlsx.rb
CHANGED
|
@@ -3,21 +3,48 @@
|
|
|
3
3
|
require_relative "fast_xlsx/version"
|
|
4
4
|
|
|
5
5
|
# Fast .xlsx writer backed by rust_xlsxwriter.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# wb = FastXlsx::Workbook.new
|
|
9
|
+
# ws = wb.add_worksheet("Report")
|
|
10
|
+
# ws << ["id", "name", "created_at"]
|
|
11
|
+
# ws.concat(records.map { |r| [r.id, r.name, r.created_at] })
|
|
12
|
+
# wb.save("report.xlsx")
|
|
13
|
+
#
|
|
14
|
+
# Rows and columns are 0-based. Errors: TypeError for an argument of the wrong
|
|
15
|
+
# type, RangeError for a row or column outside the sheet, ArgumentError for a
|
|
16
|
+
# value that isn't allowed, {FastXlsx::Error} for what the workbook can't do.
|
|
6
17
|
module FastXlsx
|
|
18
|
+
# Raised for what the workbook can't do: duplicate or invalid names, writing
|
|
19
|
+
# to rows already on disk, overlapping merges, text over Excel's limits, ...
|
|
7
20
|
class Error < StandardError; end
|
|
8
21
|
|
|
9
|
-
#
|
|
22
|
+
# A cell value written as an Excel formula.
|
|
23
|
+
#
|
|
24
|
+
# @!attribute [r] expression
|
|
25
|
+
# @return [String] the formula, without the leading "="
|
|
26
|
+
# @example
|
|
27
|
+
# ws << [1, 2, FastXlsx::Formula.new("SUM(A1:B1)")]
|
|
10
28
|
Formula = Data.define(:expression) do
|
|
11
29
|
def initialize(expression:)
|
|
12
30
|
super(expression: expression.to_s)
|
|
13
31
|
end
|
|
14
32
|
end
|
|
15
33
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
34
|
+
# A cell value written as a hyperlink.
|
|
35
|
+
#
|
|
36
|
+
# @!attribute [r] url
|
|
37
|
+
# @return [String] http(s)://, mailto:, file:// or internal:Sheet2!A1
|
|
38
|
+
# @!attribute [r] text
|
|
39
|
+
# @return [String, nil] shown in the cell instead of the URL
|
|
40
|
+
# @example
|
|
41
|
+
# FastXlsx::URL.new("https://example.com")
|
|
42
|
+
# FastXlsx::URL.new("https://example.com/report/42", text: "Q3 report")
|
|
18
43
|
# Subclassed (not a Data.define block) so .new can call super, which lets it
|
|
19
44
|
# accept URL.new(url, text: ...) as well as the usual Data forms.
|
|
20
45
|
class URL < Data.define(:url, :text) # rubocop:disable Style/DataInheritance
|
|
46
|
+
# @overload new(url, text = nil)
|
|
47
|
+
# @overload new(url:, text: nil)
|
|
21
48
|
def self.new(*args, **kwargs)
|
|
22
49
|
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..2)" if args.size > 2
|
|
23
50
|
|
|
@@ -32,11 +59,18 @@ module FastXlsx
|
|
|
32
59
|
end
|
|
33
60
|
end
|
|
34
61
|
|
|
35
|
-
# Text with a format per segment
|
|
36
|
-
#
|
|
62
|
+
# Text with a format per segment.
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# bold = FastXlsx::Format.new(bold: true)
|
|
66
|
+
# ws << [FastXlsx::RichString.new(["Total: ", bold], "1,234")]
|
|
37
67
|
class RichString
|
|
68
|
+
# @return [Array<Array(String, Format)>] [text, format or nil] per segment
|
|
38
69
|
attr_reader :segments
|
|
39
70
|
|
|
71
|
+
# @param parts [Array<String, Array(String, Format)>] each a String (default
|
|
72
|
+
# font) or [String, Format]
|
|
73
|
+
# @raise [ArgumentError] when no segment is given
|
|
40
74
|
def initialize(*parts)
|
|
41
75
|
raise ArgumentError, "RichString needs at least one segment" if parts.empty?
|
|
42
76
|
|
|
@@ -53,15 +87,31 @@ module FastXlsx
|
|
|
53
87
|
require "fast_xlsx/fast_xlsx"
|
|
54
88
|
end
|
|
55
89
|
|
|
56
|
-
# Owns the worksheets; serialize with #to_xlsx or #save.
|
|
90
|
+
# Owns the worksheets; serialize with {#to_xlsx} or {#save}.
|
|
91
|
+
#
|
|
92
|
+
# @!method to_xlsx
|
|
93
|
+
# The workbook as an .xlsx file. Runs without Ruby's global lock, so
|
|
94
|
+
# other threads keep running meanwhile.
|
|
95
|
+
# @return [String] binary
|
|
96
|
+
# @raise [FastXlsx::Error] e.g. duplicate defined names
|
|
97
|
+
#
|
|
98
|
+
# @!method define_name(name, formula)
|
|
99
|
+
# A defined name: "Rate" for the whole workbook, "Sheet1!Sales" for one
|
|
100
|
+
# sheet. Duplicates and unknown sheets are reported when saving.
|
|
101
|
+
# @param name [String]
|
|
102
|
+
# @param formula [String] e.g. "=0.96" or "=Sheet1!$A$1:$A$9"
|
|
103
|
+
# @return [self]
|
|
104
|
+
# @raise [FastXlsx::Error] for a name Excel doesn't allow
|
|
57
105
|
class Workbook
|
|
106
|
+
# Accepted memory: modes.
|
|
58
107
|
MEMORY_MODES = %i[standard constant low].freeze
|
|
59
108
|
|
|
60
|
-
# memory
|
|
61
|
-
# :low write each finished row to disk, so each worksheet
|
|
62
|
-
# top to bottom
|
|
63
|
-
# keeps Excel's shared string table (memory grows with
|
|
64
|
-
# unique strings
|
|
109
|
+
# @param memory [Symbol] :standard keeps every cell in memory until saving.
|
|
110
|
+
# :constant and :low write each finished row to disk, so each worksheet
|
|
111
|
+
# must be filled top to bottom: :constant stores strings inline (memory
|
|
112
|
+
# stays flat), :low keeps Excel's shared string table (memory grows with
|
|
113
|
+
# the number of unique strings).
|
|
114
|
+
# @raise [ArgumentError] for an unknown mode
|
|
65
115
|
def self.new(memory: :standard)
|
|
66
116
|
unless MEMORY_MODES.include?(memory)
|
|
67
117
|
raise ArgumentError, "unknown memory mode #{memory.inspect} (expected one of #{MEMORY_MODES.join(", ")})"
|
|
@@ -70,28 +120,40 @@ module FastXlsx
|
|
|
70
120
|
_new(memory == :constant, memory == :low)
|
|
71
121
|
end
|
|
72
122
|
|
|
123
|
+
# @param name [String, nil] nil takes the first free "SheetN"
|
|
124
|
+
# @return [Worksheet]
|
|
125
|
+
# @raise [FastXlsx::Error] for an invalid name or one already used
|
|
126
|
+
# (names ignore case)
|
|
73
127
|
def add_worksheet(name = nil)
|
|
74
128
|
_add_worksheet(name).tap { |ws| worksheets << ws }
|
|
75
129
|
end
|
|
76
130
|
|
|
77
131
|
# The same Worksheet objects add_worksheet returned, so their append
|
|
78
132
|
# position is shared.
|
|
133
|
+
# @return [Array<Worksheet>]
|
|
79
134
|
def worksheets
|
|
80
135
|
@worksheets ||= []
|
|
81
136
|
end
|
|
82
137
|
|
|
138
|
+
# @param name [String]
|
|
139
|
+
# @return [Worksheet, nil]
|
|
83
140
|
def worksheet(name)
|
|
84
141
|
worksheets.find { |ws| ws.name == name }
|
|
85
142
|
end
|
|
86
143
|
|
|
87
|
-
#
|
|
144
|
+
# Writes the .xlsx file. Runs without Ruby's global lock, like {#to_xlsx}.
|
|
145
|
+
# @param path [String, Pathname, #to_path]
|
|
146
|
+
# @return [nil]
|
|
88
147
|
def save(path)
|
|
89
148
|
_save(File.path(path))
|
|
90
149
|
end
|
|
91
150
|
|
|
92
|
-
# Document properties shown in Excel's File > Info
|
|
93
|
-
#
|
|
94
|
-
#
|
|
151
|
+
# Document properties shown in Excel's File > Info. Later calls add to
|
|
152
|
+
# earlier ones.
|
|
153
|
+
# @param fields [Hash{Symbol => String}] title:, subject:, author:,
|
|
154
|
+
# manager:, company:, category:, keywords:, comments:, status:
|
|
155
|
+
# @return [self]
|
|
156
|
+
# @raise [ArgumentError] for an unknown field
|
|
95
157
|
def properties(**fields)
|
|
96
158
|
merged = (@properties || {}).merge(fields)
|
|
97
159
|
_properties(merged) # validates before anything is remembered
|
|
@@ -100,7 +162,73 @@ module FastXlsx
|
|
|
100
162
|
end
|
|
101
163
|
end
|
|
102
164
|
|
|
103
|
-
# Cell writer for one sheet; create with Workbook#add_worksheet.
|
|
165
|
+
# Cell writer for one sheet; create with {Workbook#add_worksheet}.
|
|
166
|
+
#
|
|
167
|
+
# A cell is (row, col) or a reference like "B2". A range is four 0-based
|
|
168
|
+
# numbers (first_row, first_col, last_row, last_col), a reference like
|
|
169
|
+
# "A1:D10", or rows and columns as Integers or Ranges, e.g. (0..9, 0..3).
|
|
170
|
+
#
|
|
171
|
+
# @!method write(*cell, value, format = nil)
|
|
172
|
+
# Writes one cell.
|
|
173
|
+
# @overload write(row, col, value, format = nil)
|
|
174
|
+
# @overload write(ref, value, format = nil)
|
|
175
|
+
# @param value [Numeric, String, Time, Date, DateTime, true, false, nil,
|
|
176
|
+
# Formula, URL, RichString, #to_s] dates get yyyy-mm-dd (hh:mm:ss) unless
|
|
177
|
+
# the format has a num_format
|
|
178
|
+
# @param format [Format, nil]
|
|
179
|
+
# @return [self]
|
|
180
|
+
#
|
|
181
|
+
# @!method <<(values)
|
|
182
|
+
# Appends a row after the last row written.
|
|
183
|
+
# @param values [Array] cell values, as for {#write}
|
|
184
|
+
# @return [self]
|
|
185
|
+
#
|
|
186
|
+
# @!method concat(rows)
|
|
187
|
+
# Appends several rows.
|
|
188
|
+
# @param rows [Array<Array>]
|
|
189
|
+
# @return [self]
|
|
190
|
+
#
|
|
191
|
+
# @!method next_row
|
|
192
|
+
# @return [Integer] the row {#<<} writes next
|
|
193
|
+
#
|
|
194
|
+
# @!method name
|
|
195
|
+
# @return [String]
|
|
196
|
+
#
|
|
197
|
+
# @!method row_height(row, height)
|
|
198
|
+
# @param height [Numeric] points, 0..409
|
|
199
|
+
# @return [self]
|
|
200
|
+
# @raise [ArgumentError] for a height outside 0..409
|
|
201
|
+
#
|
|
202
|
+
# @!method page_breaks(rows)
|
|
203
|
+
# Starts a printed page before each of these rows.
|
|
204
|
+
# @param rows [Array<Integer>]
|
|
205
|
+
# @return [self]
|
|
206
|
+
#
|
|
207
|
+
# @!method vertical_page_breaks(cols)
|
|
208
|
+
# Starts a printed page before each of these columns.
|
|
209
|
+
# @param cols [Array<Integer>]
|
|
210
|
+
# @return [self]
|
|
211
|
+
#
|
|
212
|
+
# @!method activate
|
|
213
|
+
# Makes Excel open on this sheet (un-hiding it if hidden).
|
|
214
|
+
# @return [self]
|
|
215
|
+
#
|
|
216
|
+
# @!method hide
|
|
217
|
+
# @return [self]
|
|
218
|
+
# @raise [FastXlsx::Error] for the sheet Excel opens on (the first one
|
|
219
|
+
# unless another is activated)
|
|
220
|
+
#
|
|
221
|
+
# @!method zoom(percent)
|
|
222
|
+
# @param percent [Integer, #to_int] 10..400
|
|
223
|
+
# @return [self]
|
|
224
|
+
#
|
|
225
|
+
# @!method tab_color(color)
|
|
226
|
+
# @param color [String, Integer] "#RRGGBB" or 0xRRGGBB
|
|
227
|
+
# @return [self]
|
|
228
|
+
#
|
|
229
|
+
# @!method hide_gridlines
|
|
230
|
+
# Hides the gridlines on screen (see {#page_setup} for printing).
|
|
231
|
+
# @return [self]
|
|
104
232
|
class Worksheet
|
|
105
233
|
# write(row, col, value, format = nil) or write("B2", value, format = nil)
|
|
106
234
|
# is native: the (row, col) form runs once per cell, so it skips a Ruby
|
|
@@ -112,11 +240,18 @@ module FastXlsx
|
|
|
112
240
|
end
|
|
113
241
|
private :_write_ref
|
|
114
242
|
|
|
243
|
+
# Appends a row after the last row written.
|
|
244
|
+
# @param values [Array] cell values, as for {#write}
|
|
245
|
+
# @param format [Format, Array<Format, nil>, nil] one for every cell, or
|
|
246
|
+
# one per cell
|
|
247
|
+
# @return [self]
|
|
115
248
|
def append(values, format: nil)
|
|
116
249
|
_append(values, format)
|
|
117
250
|
end
|
|
118
251
|
|
|
119
|
-
# columns
|
|
252
|
+
# @param columns [Integer, Range<Integer>]
|
|
253
|
+
# @param width [Numeric] characters, 0..255
|
|
254
|
+
# @return [self]
|
|
120
255
|
def column_width(columns, width)
|
|
121
256
|
range = CellRange.bounds(columns)
|
|
122
257
|
_column_width(*range, width)
|
|
@@ -126,89 +261,131 @@ module FastXlsx
|
|
|
126
261
|
self
|
|
127
262
|
end
|
|
128
263
|
|
|
129
|
-
# Sizes columns to the data written so far
|
|
130
|
-
# column_width are kept.
|
|
264
|
+
# Sizes columns to the data written so far (in :constant / :low mode, the
|
|
265
|
+
# rows still in memory). Widths set with {#column_width} are kept.
|
|
266
|
+
# @return [self]
|
|
131
267
|
def autofit
|
|
132
268
|
_autofit
|
|
133
269
|
@fixed_widths&.each { |bounds, width| _column_width(*bounds, width) }
|
|
134
270
|
self
|
|
135
271
|
end
|
|
136
272
|
|
|
137
|
-
# The methods below take a cell range in any of these styles:
|
|
138
|
-
# (first_row, first_col, last_row, last_col) four 0-based numbers
|
|
139
|
-
# ("A1:D10") or ("B2") an Excel reference
|
|
140
|
-
# (rows, cols) Integers or Ranges, e.g. (0..9, 0..3)
|
|
141
|
-
|
|
142
273
|
# Filter buttons on the range's first row.
|
|
274
|
+
# @param range a cell range (see {Worksheet})
|
|
275
|
+
# @return [self]
|
|
143
276
|
def autofilter(*range)
|
|
144
277
|
_autofilter(*CellRange.split(range).first)
|
|
145
278
|
end
|
|
146
279
|
|
|
147
280
|
# Merges the range and writes value (any cell type) into its first cell.
|
|
281
|
+
# @overload merge_range(*range, value, format = nil)
|
|
282
|
+
# @return [self]
|
|
283
|
+
# @raise [FastXlsx::Error] when it overlaps an earlier merge
|
|
148
284
|
def merge_range(*args)
|
|
149
285
|
range, (value, format) = CellRange.split(args, 1..2)
|
|
150
286
|
_merge_range(*range, value, format)
|
|
151
287
|
end
|
|
152
288
|
|
|
153
|
-
# Highlights cells in the range by rule
|
|
154
|
-
#
|
|
289
|
+
# Highlights cells in the range by rule; see the README for each type's
|
|
290
|
+
# options.
|
|
291
|
+
# @param range a cell range (see {Worksheet})
|
|
292
|
+
# @param type [Symbol] :cell, :text, :formula, :data_bar or :color_scale
|
|
293
|
+
# @option options [Symbol] :criteria e.g. :>, :between, :contains
|
|
294
|
+
# @option options [Numeric, String, Array] :value
|
|
295
|
+
# @option options [Format] :format
|
|
296
|
+
# @option options [Integer] :colors 2 or 3 (:color_scale)
|
|
297
|
+
# @return [self]
|
|
155
298
|
def conditional_format(*range, type:, **)
|
|
156
299
|
_conditional_format(*CellRange.split(range).first, { type: type, ** })
|
|
157
300
|
end
|
|
158
301
|
|
|
159
|
-
# Restricts what can be entered in the range
|
|
160
|
-
#
|
|
302
|
+
# Restricts what can be entered in the range; see the README.
|
|
303
|
+
# @param range a cell range (see {Worksheet})
|
|
304
|
+
# @param type [Symbol] :list, :whole_number, :decimal or :text_length
|
|
305
|
+
# @option options [Symbol] :criteria e.g. :>, :between
|
|
306
|
+
# @option options [Numeric, String, Array] :value
|
|
307
|
+
# @option options [String] :input_title, :input_message, :error_title, :error_message
|
|
308
|
+
# @return [self]
|
|
161
309
|
def data_validation(*range, type:, **)
|
|
162
310
|
_data_validation(*CellRange.split(range).first, { type: type, ** })
|
|
163
311
|
end
|
|
164
312
|
|
|
165
|
-
# Adds a comment (Excel "note") to a cell.
|
|
313
|
+
# Adds a comment (an Excel "note") to a cell.
|
|
314
|
+
# @overload write_comment(*cell, text, author: nil)
|
|
315
|
+
# @return [self]
|
|
166
316
|
def write_comment(*args, author: nil)
|
|
167
317
|
row, col, (text, *) = CellRange.cell(args, 1..1)
|
|
168
318
|
_write_comment(row, col, text, author)
|
|
169
319
|
end
|
|
170
320
|
|
|
171
|
-
# Inserts a PNG, JPEG, GIF or BMP image with its top-left corner in the
|
|
172
|
-
# cell
|
|
173
|
-
#
|
|
321
|
+
# Inserts a PNG, JPEG, GIF or BMP image with its top-left corner in the cell.
|
|
322
|
+
# @overload insert_image(*cell, source, **options)
|
|
323
|
+
# @param source [String, #read] a file path or an IO
|
|
324
|
+
# @option options [Numeric] :scale
|
|
325
|
+
# @option options [Numeric] :width, :height pixels (the other keeps the ratio)
|
|
326
|
+
# @option options [Integer] :x_offset, :y_offset pixels
|
|
327
|
+
# @option options [String] :alt_text
|
|
328
|
+
# @return [self]
|
|
329
|
+
# @raise [FastXlsx::Error] for data that isn't a supported image
|
|
174
330
|
def insert_image(*args, **)
|
|
175
331
|
row, col, (source, *) = CellRange.cell(args, 1..1)
|
|
176
332
|
bytes = source.respond_to?(:read) ? source.read : File.binread(source)
|
|
177
333
|
_insert_image(row, col, bytes, { ** })
|
|
178
334
|
end
|
|
179
335
|
|
|
180
|
-
# Inserts a chart with its top-left corner in the cell.
|
|
181
|
-
#
|
|
182
|
-
#
|
|
336
|
+
# Inserts a chart with its top-left corner in the cell.
|
|
337
|
+
# @param cell (row, col) or "B2"
|
|
338
|
+
# @param type [Symbol] :area, :bar, :column, :line, :pie, :doughnut,
|
|
339
|
+
# :radar, :scatter, and the stacked variants
|
|
340
|
+
# @param series [Array<Hash>] { values:, categories:, name: } with ranges
|
|
341
|
+
# such as "Sheet1!$B$2:$B$13"
|
|
342
|
+
# @option options [String] :title, :x_axis, :y_axis
|
|
343
|
+
# @option options [Integer] :width, :height pixels
|
|
344
|
+
# @return [self]
|
|
183
345
|
def insert_chart(*cell, type:, series:, **)
|
|
184
346
|
row, col, = CellRange.cell(cell)
|
|
185
347
|
_insert_chart(row, col, { type: type, series: series, ** })
|
|
186
348
|
end
|
|
187
349
|
|
|
188
350
|
# Turns the range (header row included, total row too when total_row: true)
|
|
189
|
-
# into an Excel table.
|
|
190
|
-
#
|
|
191
|
-
#
|
|
351
|
+
# into an Excel table. Rows appended later under the header fill it.
|
|
352
|
+
# @param range a cell range (see {Worksheet})
|
|
353
|
+
# @option options [Array<String, Hash>] :columns header Strings or
|
|
354
|
+
# { header:, total:, total_label:, format: }
|
|
355
|
+
# @option options [Symbol] :style e.g. :medium2
|
|
356
|
+
# @option options [String] :name
|
|
357
|
+
# @option options [Boolean] :total_row, :banded_rows, :autofilter
|
|
358
|
+
# @return [self]
|
|
192
359
|
def add_table(*range, **)
|
|
193
360
|
_add_table(*CellRange.split(range).first, { ** })
|
|
194
361
|
end
|
|
195
362
|
|
|
196
|
-
# Printed page header
|
|
197
|
-
# margin
|
|
363
|
+
# Printed page header, using Excel codes such as "&CPage &P of &N".
|
|
364
|
+
# @param margin [Numeric, nil] inches
|
|
365
|
+
# @return [self]
|
|
198
366
|
def page_header(text, margin: nil)
|
|
199
367
|
_page_header(text)
|
|
200
368
|
margin ? margins(header: margin) : self
|
|
201
369
|
end
|
|
202
370
|
|
|
371
|
+
# Printed page footer; see {#page_header}.
|
|
372
|
+
# @return [self]
|
|
203
373
|
def page_footer(text, margin: nil)
|
|
204
374
|
_page_footer(text)
|
|
205
375
|
margin ? margins(footer: margin) : self
|
|
206
376
|
end
|
|
207
377
|
|
|
208
|
-
# Printing
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
378
|
+
# Printing options.
|
|
379
|
+
# @option options [Boolean] :landscape
|
|
380
|
+
# @option options [Symbol, Integer] :paper :letter, :legal, :tabloid, :a3,
|
|
381
|
+
# :a4, :a5 or Excel's paper number (0: the printer's default)
|
|
382
|
+
# @option options [Integer] :fit_width, :fit_height pages (0 or left out:
|
|
383
|
+
# as many as needed)
|
|
384
|
+
# @param repeat_rows [Integer, Range, nil] printed on every page
|
|
385
|
+
# @param repeat_columns [Integer, Range, nil] printed on every page
|
|
386
|
+
# @param print_area a cell range: "A1:D100" or [first_row, first_col, last_row, last_col]
|
|
387
|
+
# @option options [Boolean] :gridlines print the gridlines
|
|
388
|
+
# @return [self]
|
|
212
389
|
def page_setup(repeat_rows: nil, repeat_columns: nil, print_area: nil, **options)
|
|
213
390
|
options[:repeat_rows] = CellRange.bounds(repeat_rows) if repeat_rows
|
|
214
391
|
options[:repeat_columns] = CellRange.bounds(repeat_columns) if repeat_columns
|
|
@@ -218,39 +395,56 @@ module FastXlsx
|
|
|
218
395
|
end
|
|
219
396
|
|
|
220
397
|
# Print margins in inches; margins not given keep their current value.
|
|
398
|
+
# @return [self]
|
|
399
|
+
# @raise [ArgumentError] for a negative or NaN margin
|
|
221
400
|
def margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
|
|
222
|
-
_margins(
|
|
401
|
+
_margins(left, right, top, bottom, header, footer)
|
|
223
402
|
self
|
|
224
403
|
end
|
|
225
404
|
|
|
226
|
-
# Outline group with an expand/collapse button. rows
|
|
227
|
-
#
|
|
228
|
-
#
|
|
405
|
+
# Outline group with an expand/collapse button. Grouping rows already
|
|
406
|
+
# grouped nests them (up to 7 levels).
|
|
407
|
+
# @param rows [Integer, Range<Integer>]
|
|
408
|
+
# @param collapsed [Boolean] hidden until expanded
|
|
409
|
+
# @return [self]
|
|
410
|
+
# @raise [FastXlsx::Error] in :constant / :low memory mode, which can't
|
|
411
|
+
# write outline levels for rows
|
|
229
412
|
def group_rows(rows, collapsed: false)
|
|
230
413
|
_group_rows(*CellRange.bounds(rows), collapsed)
|
|
231
414
|
end
|
|
232
415
|
|
|
416
|
+
# Outline group of columns; see {#group_rows}. Works in every memory mode.
|
|
417
|
+
# @param columns [Integer, Range<Integer>]
|
|
418
|
+
# @return [self]
|
|
233
419
|
def group_columns(columns, collapsed: false)
|
|
234
420
|
_group_columns(*CellRange.bounds(columns), collapsed)
|
|
235
421
|
end
|
|
236
422
|
|
|
237
423
|
# Keeps the rows above and the columns left of the cell visible while
|
|
238
424
|
# scrolling: (1, 0) or "A2" freezes the first row.
|
|
425
|
+
# @return [self]
|
|
239
426
|
def freeze_panes(*cell)
|
|
240
427
|
_freeze_panes(*CellRange.cell(cell).first(2))
|
|
241
428
|
self
|
|
242
429
|
end
|
|
243
430
|
|
|
244
431
|
# Locks the sheet against editing. Cells whose format has locked: false
|
|
245
|
-
# stay editable.
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
432
|
+
# stay editable. Calling it again replaces the password and actions.
|
|
433
|
+
# @param password [String, nil] stops editing in Excel; it doesn't
|
|
434
|
+
# encrypt the file
|
|
435
|
+
# @param allow [Symbol, Array<Symbol>] actions users may still take:
|
|
436
|
+
# :format_cells, :format_columns, :format_rows, :insert_columns,
|
|
437
|
+
# :insert_rows, :insert_links, :delete_columns, :delete_rows, :sort,
|
|
438
|
+
# :use_autofilter, :use_pivot_tables, :edit_scenarios, :edit_objects
|
|
439
|
+
# @return [self]
|
|
249
440
|
def protect(password: nil, allow: [])
|
|
250
441
|
_protect(password, Array(allow))
|
|
251
442
|
end
|
|
252
443
|
|
|
253
444
|
# Default format for cells in these columns that are written without one.
|
|
445
|
+
# @param columns [Integer, Range<Integer>]
|
|
446
|
+
# @param format [Format]
|
|
447
|
+
# @return [self]
|
|
254
448
|
def column_format(columns, format)
|
|
255
449
|
_column_format(*CellRange.bounds(columns), format)
|
|
256
450
|
self
|
|
@@ -260,6 +454,7 @@ module FastXlsx
|
|
|
260
454
|
# Cell ranges in the styles Worksheet methods accept: four 0-based numbers,
|
|
261
455
|
# an Excel reference ("A1:D10", "B2"), or rows and columns as Integers or
|
|
262
456
|
# Ranges.
|
|
457
|
+
# @api private
|
|
263
458
|
module CellRange
|
|
264
459
|
REF = /\A\$?([A-Za-z]{1,3})\$?([1-9]\d*)\z/ # ASCII only: /i also matches the Kelvin sign
|
|
265
460
|
FORMS = 'a range is (first_row, first_col, last_row, last_col), "A1:D10" or (rows, cols)'
|
|
@@ -279,8 +474,8 @@ module FastXlsx
|
|
|
279
474
|
case args
|
|
280
475
|
in [Numeric, Numeric, Numeric, Numeric, *rest] then [args.first(4), rest]
|
|
281
476
|
in [String => ref, *rest] then [excel(ref), rest]
|
|
282
|
-
in [Integer | Range
|
|
283
|
-
[rows_and_cols(
|
|
477
|
+
in [Integer | Range, Integer | Range, *rest] # (no "=> name" here: YARD can't parse it)
|
|
478
|
+
[rows_and_cols(args[0], args[1]), rest]
|
|
284
479
|
else
|
|
285
480
|
raise ArgumentError, "expected a cell range, got #{args.inspect}; #{FORMS}"
|
|
286
481
|
end
|
|
@@ -351,8 +546,19 @@ module FastXlsx
|
|
|
351
546
|
end
|
|
352
547
|
private_constant :CellRange
|
|
353
548
|
|
|
354
|
-
#
|
|
549
|
+
# A cell style; pass it to {Worksheet#write}, {Worksheet#append} and the
|
|
550
|
+
# other methods that take a format. See the README for every option.
|
|
551
|
+
#
|
|
552
|
+
# @example
|
|
553
|
+
# header = FastXlsx::Format.new(bold: true, bg_color: "#DDEBF7", border_bottom: :thin)
|
|
554
|
+
# money = FastXlsx::Format.new(num_format: "#,##0.00")
|
|
355
555
|
class Format
|
|
556
|
+
# @param options [Hash] bold:, italic:, underline:, strikeout:,
|
|
557
|
+
# font_script:, font_size:, font_name:, font_color:, bg_color:,
|
|
558
|
+
# num_format:, align:, valign:, text_wrap:, rotation:, indent:, shrink:,
|
|
559
|
+
# border:, border_left:, border_right:, border_top:, border_bottom:,
|
|
560
|
+
# border_color:, locked:, hidden:
|
|
561
|
+
# @raise [ArgumentError] for an unknown option or an invalid value
|
|
356
562
|
def self.new(**options)
|
|
357
563
|
# Apply border: first so border_left: etc. override it whatever the order.
|
|
358
564
|
options = { border: options[:border], **options.except(:border) } if options.key?(:border)
|
data/sig/fast_xlsx.rbs
CHANGED
|
@@ -81,7 +81,7 @@ module FastXlsx
|
|
|
81
81
|
def page_footer: (String text, ?margin: Numeric?) -> self
|
|
82
82
|
def activate: () -> self
|
|
83
83
|
def hide: () -> self
|
|
84
|
-
def zoom: (
|
|
84
|
+
def zoom: (int percent) -> self
|
|
85
85
|
def tab_color: (String | Integer color) -> self
|
|
86
86
|
def hide_gridlines: () -> self
|
|
87
87
|
def page_setup: (?landscape: bool, ?paper: :letter | :legal | :tabloid | :a3 | :a4 | :a5 | Integer,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast_xlsx
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
@@ -32,6 +32,7 @@ extensions:
|
|
|
32
32
|
- ext/fast_xlsx/extconf.rb
|
|
33
33
|
extra_rdoc_files: []
|
|
34
34
|
files:
|
|
35
|
+
- ".yardopts"
|
|
35
36
|
- CHANGELOG.md
|
|
36
37
|
- Cargo.lock
|
|
37
38
|
- Cargo.toml
|