fast_xlsx 0.5.0 → 0.7.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/CHANGELOG.md +29 -0
- data/README.md +42 -1
- data/bench/Gemfile.lock +2 -2
- data/ext/fast_xlsx/src/lib.rs +249 -14
- data/lib/fast_xlsx/version.rb +1 -1
- data/lib/fast_xlsx.rb +199 -17
- data/sig/fast_xlsx.rbs +34 -12
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eddcb1a0c063b94222c0a83e15b7a855a83b26674390ee87f59f1938d7007c9f
|
|
4
|
+
data.tar.gz: 5aa9b9a7b6dbfb70bb53364b16fa31bb2c745534aefc9f0631367ed6cf658043
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d854850a2dd9d9b82d8bbbe4568f41336b3e4a89b977ddfcffb8e08a083305222111dded66d33e8f999dfbfae4d1da319e16f40835e83962bb7ac6081686b333
|
|
7
|
+
data.tar.gz: 973794fd6d14e139f8173e0726f0938f61022f5b503d5b761b4abaf089b9e0dd3debec9083248859f9380a64fd6ead89ec2efc8af64296338960b6caed1269df
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-10-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- A Hash of options works wherever a format goes (`write(0, 0, "x", { bold: true })`, `append(row, format: { bold: true })`, table columns, conditional formats, rich strings); equal Hashes share one format.
|
|
8
|
+
- `ws["B2"] = value` and `ws[row, col] = value` write a cell.
|
|
9
|
+
- `Format#to_h` (its options) and `Format#merge` (a new format with more options, from keywords, a Hash or another Format).
|
|
10
|
+
- Formats with the same options are `==`; `eql?` and `hash` follow `Hash#eql?` (so `10` and `10.0` differ there), letting formats serve as Hash keys. Options may have String keys (e.g. parsed from JSON or YAML); the same option as a String and a Symbol raises `ArgumentError`.
|
|
11
|
+
- `Workbook.new` and `Workbook#add_worksheet` yield the new object to a block.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- A `Format` is frozen, and keeps frozen copies of its option values: one is shared wherever the same options are used.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `Workbook.new { |wb| ... }` ignored its block.
|
|
20
|
+
|
|
21
|
+
## [0.6.0] - 2026-10-02
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `Worksheet#selection`, `#top_left_cell` (what is selected and scrolled to when the file opens), `#ignore_error` (turns off Excel's green-triangle warnings in a range) and `#unprotect_range` (a range users can edit on a protected sheet).
|
|
26
|
+
- `Worksheet#hide_rows`, `#hide_columns`, `#row_format` (default format for a row's cells) and `#default_row_height` (call it before `row_format`, `hide_rows` and `group_rows`, which copy the default into their rows; it raises otherwise).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `nil` written with a format (e.g. a border, or a row format in `append`) writes a blank cell with that format; the format was dropped.
|
|
31
|
+
|
|
3
32
|
## [0.5.0] - 2026-10-01
|
|
4
33
|
|
|
5
34
|
### Added
|
data/README.md
CHANGED
|
@@ -19,11 +19,36 @@ ws << ["id", "name", "created_at"] # append a row
|
|
|
19
19
|
ws.concat(records.map { |r| [r.id, r.name, r.created_at] }) # append many rows in one call
|
|
20
20
|
ws.write(0, 5, 42) # write a single cell (row, col, value)
|
|
21
21
|
ws.write("F1", 42) # or by its Excel reference
|
|
22
|
+
ws["F1"] = 42 # the same (ws[0, 5] = 42 too)
|
|
22
23
|
|
|
23
24
|
wb.properties(title: "Q3 report", author: "Zac", keywords: "Confidential") # File > Info in Excel
|
|
24
25
|
wb.save("report.xlsx") # or wb.to_xlsx => binary String
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
`Workbook.new` and `add_worksheet` also take a block, which gets the new object:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
wb = FastXlsx::Workbook.new do |wb|
|
|
32
|
+
wb.add_worksheet("Users") do |ws|
|
|
33
|
+
ws.append(%w[id name email], format: { bold: true })
|
|
34
|
+
User.find_each { |u| ws << u.attributes.values_at("id", "name", "email") } # a Hash's values in column order
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Rails
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
def export
|
|
43
|
+
wb = FastXlsx::Workbook.new(memory: :constant) # large exports: memory stays flat
|
|
44
|
+
ws = wb.add_worksheet("Orders")
|
|
45
|
+
ws.append(["Order", "Customer", "Total", "Placed at"], format: { bold: true })
|
|
46
|
+
Order.includes(:customer).find_each { |o| ws << [o.number, o.customer.name, o.total, o.created_at] }
|
|
47
|
+
send_data wb.to_xlsx, filename: "orders.xlsx",
|
|
48
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
27
52
|
### Memory modes
|
|
28
53
|
|
|
29
54
|
By default every cell stays in memory until the file is saved. For large exports, two modes write each finished row to a temp file instead:
|
|
@@ -80,6 +105,15 @@ ws.write(1, 2, Date.today, date) # format one cell
|
|
|
80
105
|
| `locked` | `false` keeps the cell editable on a protected sheet (default `true`) |
|
|
81
106
|
| `hidden` | `true` hides the cell's formula on a protected sheet |
|
|
82
107
|
|
|
108
|
+
A Hash of options works wherever a format goes (equal Hashes share one format), and a format can be extended:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
ws.append(["Total", 1_234], format: { bold: true })
|
|
112
|
+
title = header.merge(font_size: 16) # header's options plus font_size; header.to_h lists them
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
In a loop over many cells, create the `Format` once: building one from a Hash on every call is slower.
|
|
116
|
+
|
|
83
117
|
Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
|
|
84
118
|
|
|
85
119
|
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.
|
|
@@ -101,6 +135,9 @@ ws.zoom(150) # 10..400 percent
|
|
|
101
135
|
ws.tab_color("#C00000")
|
|
102
136
|
ws.hide_gridlines
|
|
103
137
|
ws.activate # Excel opens on this sheet (un-hides it if hidden)
|
|
138
|
+
ws.selection("B2:C3") # the cells selected when it opens
|
|
139
|
+
ws.top_left_cell("A50") # and the cell scrolled to the top left
|
|
140
|
+
ws.ignore_error("A2:A1000", :number_stored_as_text) # no green triangles (one kind per range; don't overlap ranges)
|
|
104
141
|
other.hide # the sheet Excel opens on can't be hidden: activate another one first
|
|
105
142
|
|
|
106
143
|
ws.page_setup(landscape: true, paper: :a4, # or :letter, :legal, :tabloid, :a3, :a5, Excel's paper number, 0 = printer default
|
|
@@ -130,6 +167,9 @@ Methods that take one cell (`write`, `write_comment`, `insert_image`, `insert_ch
|
|
|
130
167
|
```ruby
|
|
131
168
|
ws.freeze_panes(1, 0) # keep the first row visible while scrolling
|
|
132
169
|
ws.row_height(0, 30) # row 1, height in points
|
|
170
|
+
ws.default_row_height(18) # every other row; call it before row_format, hide_rows, group_rows
|
|
171
|
+
ws.row_format(0, header) # default for cells in row 1 written without a format
|
|
172
|
+
ws.hide_rows(10..20) # also hide_columns(3), an index or a Range
|
|
133
173
|
ws.merge_range(0, 0, 0, 3, "Q3 report", title) # merge A1:D1; the value can be any cell type
|
|
134
174
|
ws.page_breaks([50, 100]) # print a new page before rows 51 and 101
|
|
135
175
|
ws.vertical_page_breaks([8]) # and before column I
|
|
@@ -164,6 +204,7 @@ input = FastXlsx::Format.new(locked: false)
|
|
|
164
204
|
ws.write(1, 1, 0, input) # B2 stays editable
|
|
165
205
|
ws.protect # lock everything else
|
|
166
206
|
ws.protect(password: "secret", allow: %i[sort use_autofilter]) # or with a password and allowed actions
|
|
207
|
+
ws.unprotect_range("B2:D10", name: "Inputs") # a range users can still edit (optional password:)
|
|
167
208
|
```
|
|
168
209
|
|
|
169
210
|
`allow:` takes `:format_cells`, `:format_columns`, `:format_rows`, `:insert_columns`, `:insert_rows`, `:insert_links`, `:delete_columns`, `:delete_rows`, `:sort`, `:use_autofilter`, `:use_pivot_tables`, `:edit_scenarios`, `:edit_objects`. The password only stops editing in Excel; it does not encrypt the file.
|
|
@@ -258,7 +299,7 @@ Values are mapped by type:
|
|
|
258
299
|
| `FastXlsx::URL.new("https://…")`, `URL.new(url, text: "Title")` | hyperlink (optionally showing other text) |
|
|
259
300
|
| `FastXlsx::RichString.new(["Total: ", bold], "1,234")` | text with a format per segment |
|
|
260
301
|
| `true` / `false` | boolean |
|
|
261
|
-
| `nil` | empty cell |
|
|
302
|
+
| `nil` | empty cell (a blank cell with the format, when one is given) |
|
|
262
303
|
| anything else | `to_s` as string |
|
|
263
304
|
|
|
264
305
|
`<<` 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.
|
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.7.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.7.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
|
@@ -13,7 +13,7 @@ use rust_xlsxwriter::{
|
|
|
13
13
|
ConditionalFormatCell, ConditionalFormatCellRule, ConditionalFormatDataBar,
|
|
14
14
|
ConditionalFormatFormula, ConditionalFormatText, ConditionalFormatTextRule,
|
|
15
15
|
ConditionalFormatValue, DataValidation, DataValidationRule, FormatAlign, FormatBorder,
|
|
16
|
-
FormatScript, FormatUnderline, IntoDataValidationValue, IntoExcelData, XlsxError,
|
|
16
|
+
FormatScript, FormatUnderline, IgnoreError, IntoDataValidationValue, IntoExcelData, XlsxError,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
use rust_xlsxwriter::{
|
|
@@ -33,6 +33,7 @@ static ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Err
|
|
|
33
33
|
static FORMULA: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Formula"));
|
|
34
34
|
static URL: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "URL"));
|
|
35
35
|
static RICH_STRING: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "RichString"));
|
|
36
|
+
static FORMAT: Lazy<RClass> = Lazy::new(|ruby| fast_xlsx_const(ruby, "Format"));
|
|
36
37
|
|
|
37
38
|
// A row or column outside the sheet is a RangeError, like a negative one
|
|
38
39
|
// (which fails converting to an unsigned number before reaching the writer).
|
|
@@ -63,6 +64,15 @@ fn check_positive(ruby: &Ruby, what: &str, size: f64) -> Result<f64, Error> {
|
|
|
63
64
|
))
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// A range of rows is checked before any row changes, so it is all or nothing.
|
|
68
|
+
fn check_last_row(last: u32) -> Result<(), Error> {
|
|
69
|
+
if last < MAX_ROWS {
|
|
70
|
+
Ok(())
|
|
71
|
+
} else {
|
|
72
|
+
Err(xerr(XlsxError::RowColumnLimitError))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
fn check_size(ruby: &Ruby, what: &str, size: f64, max: f64) -> Result<(), Error> {
|
|
67
77
|
if (0.0..=max).contains(&size) {
|
|
68
78
|
return Ok(());
|
|
@@ -149,6 +159,12 @@ struct Worksheet {
|
|
|
149
159
|
merges: RefCell<Vec<(u32, u16, u32, u16)>>,
|
|
150
160
|
// Formats given with column_format: (first, last, style).
|
|
151
161
|
formatted_columns: RefCell<Vec<(u16, u16, Arc<Style>)>>,
|
|
162
|
+
// Formats given with row_format: (first, last, style).
|
|
163
|
+
formatted_rows: RefCell<Vec<(u32, u32, Arc<Style>)>>,
|
|
164
|
+
// Whether a row got options (format, hidden, outline level):
|
|
165
|
+
// rust_xlsxwriter copies the default row height into a row then, so
|
|
166
|
+
// default_row_height must come first.
|
|
167
|
+
row_options_set: Cell<bool>,
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
struct TableColumnFormat {
|
|
@@ -217,6 +233,8 @@ impl Workbook {
|
|
|
217
233
|
table_formats: RefCell::new(Vec::new()),
|
|
218
234
|
merges: RefCell::new(Vec::new()),
|
|
219
235
|
formatted_columns: RefCell::new(Vec::new()),
|
|
236
|
+
formatted_rows: RefCell::new(Vec::new()),
|
|
237
|
+
row_options_set: Cell::new(false),
|
|
220
238
|
})
|
|
221
239
|
}
|
|
222
240
|
|
|
@@ -418,7 +436,11 @@ impl CellValue {
|
|
|
418
436
|
format: Option<&rust_xlsxwriter::Format>,
|
|
419
437
|
) -> Result<(), Error> {
|
|
420
438
|
match self {
|
|
421
|
-
|
|
439
|
+
// nil writes nothing, unless it has a format (e.g. a border).
|
|
440
|
+
CellValue::Empty => match format {
|
|
441
|
+
Some(f) => ws.write_blank(row, col, f).map(|_| ()).map_err(xerr),
|
|
442
|
+
None => Ok(()),
|
|
443
|
+
},
|
|
422
444
|
CellValue::Text(s) => emit(ws, row, col, s.as_str(), format),
|
|
423
445
|
CellValue::Number(n) | CellValue::Date(n, _) => emit(ws, row, col, *n, format),
|
|
424
446
|
CellValue::Bool(b) => emit(ws, row, col, *b, format),
|
|
@@ -786,6 +808,29 @@ const PROTECTION_ALLOW: &[(&str, ProtectionFlag)] = &[
|
|
|
786
808
|
("edit_objects", |o| &mut o.edit_objects),
|
|
787
809
|
];
|
|
788
810
|
|
|
811
|
+
// Not IgnoreError::TwoDigitTextYear: rust_xlsxwriter 0.99 writes it as
|
|
812
|
+
// "TwoDigitTextYear", not the schema's "twoDigitTextYear", which Excel would
|
|
813
|
+
// flag as damaged content. Add it once that is fixed upstream.
|
|
814
|
+
const IGNORE_ERRORS: &[(&str, IgnoreError)] = &[
|
|
815
|
+
("number_stored_as_text", IgnoreError::NumberStoredAsText),
|
|
816
|
+
("formula_error", IgnoreError::FormulaError),
|
|
817
|
+
("formula_differs", IgnoreError::FormulaDiffers),
|
|
818
|
+
(
|
|
819
|
+
"formula_refers_to_empty_cells",
|
|
820
|
+
IgnoreError::FormulaRefersToEmptyCells,
|
|
821
|
+
),
|
|
822
|
+
("formula_omits_cells", IgnoreError::FormulaOmitsCells),
|
|
823
|
+
("data_validation_error", IgnoreError::DataValidationError),
|
|
824
|
+
(
|
|
825
|
+
"unlocked_cells_with_formula",
|
|
826
|
+
IgnoreError::UnlockedCellsWithFormula,
|
|
827
|
+
),
|
|
828
|
+
(
|
|
829
|
+
"inconsistent_column_formula",
|
|
830
|
+
IgnoreError::InconsistentColumnFormula,
|
|
831
|
+
),
|
|
832
|
+
];
|
|
833
|
+
|
|
789
834
|
// Excel's paper size codes; page_setup also takes the number itself.
|
|
790
835
|
const PAPER_SIZES: &[(&str, u8)] = &[
|
|
791
836
|
("letter", 1),
|
|
@@ -1030,11 +1075,13 @@ impl Worksheet {
|
|
|
1030
1075
|
|
|
1031
1076
|
// The format a value is written with: its own, else its table column's.
|
|
1032
1077
|
// A date cell takes that format's date variant (when it has no
|
|
1033
|
-
// num_format); with neither, its
|
|
1034
|
-
//
|
|
1035
|
-
// one get None, so rust_xlsxwriter applies any
|
|
1078
|
+
// num_format); with neither, its row_format's, else its column_format's
|
|
1079
|
+
// date variant (rust_xlsxwriter's order), else the default date format.
|
|
1080
|
+
// Other cells without one get None, so rust_xlsxwriter applies any row or
|
|
1081
|
+
// column format itself.
|
|
1036
1082
|
fn format_for<'a>(
|
|
1037
1083
|
tables: &'a [TableColumnFormat],
|
|
1084
|
+
rows: &'a [(u32, u32, Arc<Style>)],
|
|
1038
1085
|
columns: &'a [(u16, u16, Arc<Style>)],
|
|
1039
1086
|
row: u32,
|
|
1040
1087
|
col: u16,
|
|
@@ -1055,13 +1102,21 @@ impl Worksheet {
|
|
|
1055
1102
|
return Some(style.pick(date));
|
|
1056
1103
|
}
|
|
1057
1104
|
let time = date?;
|
|
1058
|
-
|
|
1105
|
+
let row_style = rows
|
|
1059
1106
|
.iter()
|
|
1060
1107
|
.rev()
|
|
1061
|
-
.find(|(first, last, _)| (*first..=*last).contains(&
|
|
1062
|
-
|
|
1108
|
+
.find(|(first, last, _)| (*first..=*last).contains(&row))
|
|
1109
|
+
.map(|(_, _, style)| style);
|
|
1110
|
+
let column_style = || {
|
|
1111
|
+
columns
|
|
1112
|
+
.iter()
|
|
1113
|
+
.rev()
|
|
1114
|
+
.find(|(first, last, _)| (*first..=*last).contains(&col))
|
|
1115
|
+
.map(|(_, _, style)| style)
|
|
1116
|
+
};
|
|
1117
|
+
match row_style.or_else(column_style) {
|
|
1063
1118
|
// None when it has a num_format: rust_xlsxwriter applies it.
|
|
1064
|
-
Some(
|
|
1119
|
+
Some(style) => style.for_date(time),
|
|
1065
1120
|
None => DEFAULTS.for_date(time),
|
|
1066
1121
|
}
|
|
1067
1122
|
}
|
|
@@ -1088,13 +1143,21 @@ impl Worksheet {
|
|
|
1088
1143
|
Ok(())
|
|
1089
1144
|
})?;
|
|
1090
1145
|
let tables = self.table_formats.borrow();
|
|
1146
|
+
let rows = self.formatted_rows.borrow();
|
|
1091
1147
|
let columns = self.formatted_columns.borrow();
|
|
1092
1148
|
self.with_ws(|ws| {
|
|
1093
1149
|
// Values were checked when converted, so only a bad row number
|
|
1094
1150
|
// fails here, and it fails on the first cell.
|
|
1095
1151
|
for (col, value, format) in &values {
|
|
1096
|
-
let format =
|
|
1097
|
-
|
|
1152
|
+
let format = Self::format_for(
|
|
1153
|
+
&tables,
|
|
1154
|
+
&rows,
|
|
1155
|
+
&columns,
|
|
1156
|
+
row,
|
|
1157
|
+
*col,
|
|
1158
|
+
format.as_deref(),
|
|
1159
|
+
value,
|
|
1160
|
+
);
|
|
1098
1161
|
value.write(ws, row, *col, format)?;
|
|
1099
1162
|
}
|
|
1100
1163
|
Ok(())
|
|
@@ -1110,8 +1173,16 @@ impl Worksheet {
|
|
|
1110
1173
|
fn write_any(ruby: &Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Obj<Self>, Error> {
|
|
1111
1174
|
let index = |i: usize| args.get(i).and_then(|v| Integer::from_value(*v));
|
|
1112
1175
|
if let (3 | 4, Some(row), Some(col)) = (args.len(), index(0), index(1)) {
|
|
1113
|
-
|
|
1114
|
-
|
|
1176
|
+
// A Hash of options becomes a (cached) Format, as in Ruby.
|
|
1177
|
+
let format_arg = match args.get(3) {
|
|
1178
|
+
Some(f) if RHash::from_value(*f).is_some() => Some(
|
|
1179
|
+
ruby.get_inner(&FORMAT)
|
|
1180
|
+
.funcall::<_, _, Value>("_coerce", (*f,))?,
|
|
1181
|
+
),
|
|
1182
|
+
f => f.copied(),
|
|
1183
|
+
};
|
|
1184
|
+
let format = match format_arg {
|
|
1185
|
+
Some(f) => Option::<&Format>::try_convert(f)?,
|
|
1115
1186
|
None => None,
|
|
1116
1187
|
};
|
|
1117
1188
|
Self::write(
|
|
@@ -1138,13 +1209,22 @@ impl Worksheet {
|
|
|
1138
1209
|
rb_self.check_not_flushed(ruby, row)?;
|
|
1139
1210
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1140
1211
|
let tables = rb_self.table_formats.borrow();
|
|
1212
|
+
let rows = rb_self.formatted_rows.borrow();
|
|
1141
1213
|
let columns = rb_self.formatted_columns.borrow();
|
|
1142
1214
|
rb_self.with_ws(|ws| {
|
|
1143
1215
|
value.write(
|
|
1144
1216
|
ws,
|
|
1145
1217
|
row,
|
|
1146
1218
|
col,
|
|
1147
|
-
Self::format_for(
|
|
1219
|
+
Self::format_for(
|
|
1220
|
+
&tables,
|
|
1221
|
+
&rows,
|
|
1222
|
+
&columns,
|
|
1223
|
+
row,
|
|
1224
|
+
col,
|
|
1225
|
+
format.map(|f| &*f.0),
|
|
1226
|
+
&value,
|
|
1227
|
+
),
|
|
1148
1228
|
)
|
|
1149
1229
|
})?;
|
|
1150
1230
|
rb_self.advance(row);
|
|
@@ -1191,6 +1271,143 @@ impl Worksheet {
|
|
|
1191
1271
|
})
|
|
1192
1272
|
}
|
|
1193
1273
|
|
|
1274
|
+
// Row options are written with the row, so rows on disk ignore them.
|
|
1275
|
+
fn hide_rows(
|
|
1276
|
+
ruby: &Ruby,
|
|
1277
|
+
rb_self: Obj<Self>,
|
|
1278
|
+
first: u32,
|
|
1279
|
+
last: u32,
|
|
1280
|
+
) -> Result<Obj<Self>, Error> {
|
|
1281
|
+
rb_self.check_not_flushed(ruby, first)?;
|
|
1282
|
+
check_last_row(last)?;
|
|
1283
|
+
rb_self.with_ws(|ws| {
|
|
1284
|
+
for row in first..=last {
|
|
1285
|
+
ws.set_row_hidden(row).map_err(xerr)?;
|
|
1286
|
+
}
|
|
1287
|
+
Ok(())
|
|
1288
|
+
})?;
|
|
1289
|
+
rb_self.row_options_set.set(true);
|
|
1290
|
+
Ok(rb_self)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
fn selection(
|
|
1294
|
+
rb_self: Obj<Self>,
|
|
1295
|
+
fr: u32,
|
|
1296
|
+
fc: u16,
|
|
1297
|
+
lr: u32,
|
|
1298
|
+
lc: u16,
|
|
1299
|
+
) -> Result<Obj<Self>, Error> {
|
|
1300
|
+
rb_self.with_ws(|ws| ws.set_selection(fr, fc, lr, lc).map(|_| ()).map_err(xerr))?;
|
|
1301
|
+
Ok(rb_self)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
fn top_left_cell(rb_self: Obj<Self>, row: u32, col: u16) -> Result<Obj<Self>, Error> {
|
|
1305
|
+
rb_self.with_ws(|ws| ws.set_top_left_cell(row, col).map(|_| ()).map_err(xerr))?;
|
|
1306
|
+
Ok(rb_self)
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
#[allow(clippy::too_many_arguments)]
|
|
1310
|
+
fn ignore_error(
|
|
1311
|
+
ruby: &Ruby,
|
|
1312
|
+
rb_self: Obj<Self>,
|
|
1313
|
+
fr: u32,
|
|
1314
|
+
fc: u16,
|
|
1315
|
+
lr: u32,
|
|
1316
|
+
lc: u16,
|
|
1317
|
+
kind: Value,
|
|
1318
|
+
) -> Result<Obj<Self>, Error> {
|
|
1319
|
+
let error = choice(ruby, "error to ignore", kind, IGNORE_ERRORS)?;
|
|
1320
|
+
rb_self.with_ws(|ws| {
|
|
1321
|
+
ws.ignore_error_range(fr, fc, lr, lc, error)
|
|
1322
|
+
.map(|_| ())
|
|
1323
|
+
.map_err(xerr)
|
|
1324
|
+
})?;
|
|
1325
|
+
Ok(rb_self)
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
#[allow(clippy::too_many_arguments)]
|
|
1329
|
+
fn unprotect_range(
|
|
1330
|
+
rb_self: Obj<Self>,
|
|
1331
|
+
fr: u32,
|
|
1332
|
+
fc: u16,
|
|
1333
|
+
lr: u32,
|
|
1334
|
+
lc: u16,
|
|
1335
|
+
name: Option<String>,
|
|
1336
|
+
password: Option<String>,
|
|
1337
|
+
) -> Result<Obj<Self>, Error> {
|
|
1338
|
+
rb_self.with_ws(|ws| {
|
|
1339
|
+
ws.unprotect_range_with_options(
|
|
1340
|
+
fr,
|
|
1341
|
+
fc,
|
|
1342
|
+
lr,
|
|
1343
|
+
lc,
|
|
1344
|
+
name.as_deref().unwrap_or(""),
|
|
1345
|
+
password.as_deref().unwrap_or(""),
|
|
1346
|
+
)
|
|
1347
|
+
.map(|_| ())
|
|
1348
|
+
.map_err(xerr)
|
|
1349
|
+
})?;
|
|
1350
|
+
Ok(rb_self)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
fn hide_columns(rb_self: Obj<Self>, first: u16, last: u16) -> Result<Obj<Self>, Error> {
|
|
1354
|
+
rb_self.with_ws(|ws| {
|
|
1355
|
+
ws.set_column_range_hidden(first, last)
|
|
1356
|
+
.map(|_| ())
|
|
1357
|
+
.map_err(xerr)
|
|
1358
|
+
})?;
|
|
1359
|
+
Ok(rb_self)
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
fn set_row_format(
|
|
1363
|
+
ruby: &Ruby,
|
|
1364
|
+
rb_self: &Self,
|
|
1365
|
+
first: u32,
|
|
1366
|
+
last: u32,
|
|
1367
|
+
format: &Format,
|
|
1368
|
+
) -> Result<(), Error> {
|
|
1369
|
+
rb_self.check_not_flushed(ruby, first)?;
|
|
1370
|
+
check_last_row(last)?;
|
|
1371
|
+
rb_self.with_ws(|ws| {
|
|
1372
|
+
for row in first..=last {
|
|
1373
|
+
ws.set_row_format(row, &format.0.format).map_err(xerr)?;
|
|
1374
|
+
}
|
|
1375
|
+
Ok(())
|
|
1376
|
+
})?;
|
|
1377
|
+
rb_self.row_options_set.set(true);
|
|
1378
|
+
rb_self
|
|
1379
|
+
.formatted_rows
|
|
1380
|
+
.borrow_mut()
|
|
1381
|
+
.push((first, last, format.0.clone()));
|
|
1382
|
+
Ok(())
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
fn default_row_height(
|
|
1386
|
+
ruby: &Ruby,
|
|
1387
|
+
rb_self: Obj<Self>,
|
|
1388
|
+
height: f64,
|
|
1389
|
+
) -> Result<Obj<Self>, Error> {
|
|
1390
|
+
// rust_xlsxwriter ignores 0.
|
|
1391
|
+
if height == 0.0 {
|
|
1392
|
+
return Err(Error::new(
|
|
1393
|
+
ruby.exception_arg_error(),
|
|
1394
|
+
"invalid default row height 0: use a number above 0",
|
|
1395
|
+
));
|
|
1396
|
+
}
|
|
1397
|
+
check_size(ruby, "row height", height, 409.0)?;
|
|
1398
|
+
if rb_self.row_options_set.get() {
|
|
1399
|
+
return Err(Error::new(
|
|
1400
|
+
ruby.get_inner(&ERROR),
|
|
1401
|
+
"call default_row_height before row_format, hide_rows, group_rows or row_height(row, 0): rows given those keep the earlier default",
|
|
1402
|
+
));
|
|
1403
|
+
}
|
|
1404
|
+
rb_self.with_ws(|ws| {
|
|
1405
|
+
ws.set_default_row_height(height);
|
|
1406
|
+
Ok(())
|
|
1407
|
+
})?;
|
|
1408
|
+
Ok(rb_self)
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1194
1411
|
fn set_column_format(&self, first: u16, last: u16, format: &Format) -> Result<(), Error> {
|
|
1195
1412
|
self.with_ws(|ws| {
|
|
1196
1413
|
ws.set_column_range_format(first, last, &format.0.format)
|
|
@@ -1239,6 +1456,10 @@ impl Worksheet {
|
|
|
1239
1456
|
) -> Result<Obj<Self>, Error> {
|
|
1240
1457
|
check_size(ruby, "row height", height, 409.0)?;
|
|
1241
1458
|
rb_self.with_ws(|ws| ws.set_row_height(row, height).map(|_| ()).map_err(xerr))?;
|
|
1459
|
+
// A height is kept as given; only 0 (hiding the row) copies the default.
|
|
1460
|
+
if height == 0.0 {
|
|
1461
|
+
rb_self.row_options_set.set(true);
|
|
1462
|
+
}
|
|
1242
1463
|
Ok(rb_self)
|
|
1243
1464
|
}
|
|
1244
1465
|
|
|
@@ -1344,9 +1565,11 @@ impl Worksheet {
|
|
|
1344
1565
|
}
|
|
1345
1566
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1346
1567
|
let tables = rb_self.table_formats.borrow();
|
|
1568
|
+
let rows = rb_self.formatted_rows.borrow();
|
|
1347
1569
|
let columns = rb_self.formatted_columns.borrow();
|
|
1348
1570
|
let format = Self::format_for(
|
|
1349
1571
|
&tables,
|
|
1572
|
+
&rows,
|
|
1350
1573
|
&columns,
|
|
1351
1574
|
first_row,
|
|
1352
1575
|
first_col,
|
|
@@ -1922,6 +2145,7 @@ impl Worksheet {
|
|
|
1922
2145
|
.map(|_| ())
|
|
1923
2146
|
.map_err(xerr)
|
|
1924
2147
|
})?;
|
|
2148
|
+
rb_self.row_options_set.set(true);
|
|
1925
2149
|
Ok(rb_self)
|
|
1926
2150
|
}
|
|
1927
2151
|
|
|
@@ -2012,6 +2236,17 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
2012
2236
|
"vertical_page_breaks",
|
|
2013
2237
|
method!(Worksheet::set_vertical_page_breaks, 1),
|
|
2014
2238
|
)?;
|
|
2239
|
+
ws.define_method("_hide_rows", method!(Worksheet::hide_rows, 2))?;
|
|
2240
|
+
ws.define_method("_hide_columns", method!(Worksheet::hide_columns, 2))?;
|
|
2241
|
+
ws.define_method("_row_format", method!(Worksheet::set_row_format, 3))?;
|
|
2242
|
+
ws.define_method(
|
|
2243
|
+
"default_row_height",
|
|
2244
|
+
method!(Worksheet::default_row_height, 1),
|
|
2245
|
+
)?;
|
|
2246
|
+
ws.define_method("_selection", method!(Worksheet::selection, 4))?;
|
|
2247
|
+
ws.define_method("_top_left_cell", method!(Worksheet::top_left_cell, 2))?;
|
|
2248
|
+
ws.define_method("_ignore_error", method!(Worksheet::ignore_error, 5))?;
|
|
2249
|
+
ws.define_method("_unprotect_range", method!(Worksheet::unprotect_range, 6))?;
|
|
2015
2250
|
ws.define_method("activate", method!(Worksheet::activate, 0))?;
|
|
2016
2251
|
ws.define_method("hide", method!(Worksheet::hide, 0))?;
|
|
2017
2252
|
ws.define_method("zoom", method!(Worksheet::zoom, 1))?;
|
data/lib/fast_xlsx/version.rb
CHANGED
data/lib/fast_xlsx.rb
CHANGED
|
@@ -74,7 +74,9 @@ module FastXlsx
|
|
|
74
74
|
def initialize(*parts)
|
|
75
75
|
raise ArgumentError, "RichString needs at least one segment" if parts.empty?
|
|
76
76
|
|
|
77
|
-
@segments = parts.map
|
|
77
|
+
@segments = parts.map do |part|
|
|
78
|
+
part.is_a?(Array) ? [part[0].to_s, Format._coerce(part[1])] : [part.to_s, nil]
|
|
79
|
+
end.freeze
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
@@ -111,21 +113,33 @@ module FastXlsx
|
|
|
111
113
|
# must be filled top to bottom: :constant stores strings inline (memory
|
|
112
114
|
# stays flat), :low keeps Excel's shared string table (memory grows with
|
|
113
115
|
# the number of unique strings).
|
|
116
|
+
# @yieldparam workbook [Workbook] when a block is given
|
|
117
|
+
# @return [Workbook]
|
|
114
118
|
# @raise [ArgumentError] for an unknown mode
|
|
119
|
+
# @example
|
|
120
|
+
# wb = FastXlsx::Workbook.new do |wb|
|
|
121
|
+
# wb.add_worksheet("Report") { |ws| ws << %w[id name] }
|
|
122
|
+
# end
|
|
115
123
|
def self.new(memory: :standard)
|
|
116
124
|
unless MEMORY_MODES.include?(memory)
|
|
117
125
|
raise ArgumentError, "unknown memory mode #{memory.inspect} (expected one of #{MEMORY_MODES.join(", ")})"
|
|
118
126
|
end
|
|
119
127
|
|
|
120
|
-
_new(memory == :constant, memory == :low)
|
|
128
|
+
workbook = _new(memory == :constant, memory == :low)
|
|
129
|
+
yield workbook if block_given?
|
|
130
|
+
workbook
|
|
121
131
|
end
|
|
122
132
|
|
|
123
133
|
# @param name [String, nil] nil takes the first free "SheetN"
|
|
134
|
+
# @yieldparam worksheet [Worksheet] when a block is given
|
|
124
135
|
# @return [Worksheet]
|
|
125
136
|
# @raise [FastXlsx::Error] for an invalid name or one already used
|
|
126
137
|
# (names ignore case)
|
|
127
138
|
def add_worksheet(name = nil)
|
|
128
|
-
_add_worksheet(name)
|
|
139
|
+
worksheet = _add_worksheet(name)
|
|
140
|
+
worksheets << worksheet
|
|
141
|
+
yield worksheet if block_given?
|
|
142
|
+
worksheet
|
|
129
143
|
end
|
|
130
144
|
|
|
131
145
|
# The same Worksheet objects add_worksheet returned, so their append
|
|
@@ -175,7 +189,7 @@ module FastXlsx
|
|
|
175
189
|
# @param value [Numeric, String, Time, Date, DateTime, true, false, nil,
|
|
176
190
|
# Formula, URL, RichString, #to_s] dates get yyyy-mm-dd (hh:mm:ss) unless
|
|
177
191
|
# the format has a num_format
|
|
178
|
-
# @param format [Format, nil]
|
|
192
|
+
# @param format [Format, Hash, nil] a Hash of options works too
|
|
179
193
|
# @return [self]
|
|
180
194
|
#
|
|
181
195
|
# @!method <<(values)
|
|
@@ -226,6 +240,14 @@ module FastXlsx
|
|
|
226
240
|
# @param color [String, Integer] "#RRGGBB" or 0xRRGGBB
|
|
227
241
|
# @return [self]
|
|
228
242
|
#
|
|
243
|
+
# @!method default_row_height(height)
|
|
244
|
+
# Height of rows not given one with {#row_height}. Call it before
|
|
245
|
+
# {#row_format}, {#hide_rows} and {#group_rows}: rows given those keep
|
|
246
|
+
# the earlier default.
|
|
247
|
+
# @param height [Numeric] points, above 0 and up to 409
|
|
248
|
+
# @return [self]
|
|
249
|
+
# @raise [FastXlsx::Error] when called after those
|
|
250
|
+
#
|
|
229
251
|
# @!method hide_gridlines
|
|
230
252
|
# Hides the gridlines on screen (see {#page_setup} for printing).
|
|
231
253
|
# @return [self]
|
|
@@ -235,18 +257,28 @@ module FastXlsx
|
|
|
235
257
|
# wrapper. Other forms come here.
|
|
236
258
|
def _write_ref(*args)
|
|
237
259
|
row, col, (value, format) = CellRange.cell(args, 1..2)
|
|
238
|
-
_write(row, col, value, format)
|
|
260
|
+
_write(row, col, value, Format._coerce(format))
|
|
239
261
|
self
|
|
240
262
|
end
|
|
241
263
|
private :_write_ref
|
|
242
264
|
|
|
265
|
+
# Writes one cell: ws["B2"] = 42 or ws[1, 1] = 42. Use {#write} to give
|
|
266
|
+
# it a format.
|
|
267
|
+
# @overload []=(ref, value)
|
|
268
|
+
# @overload []=(row, col, value)
|
|
269
|
+
def []=(*cell, value)
|
|
270
|
+
row, col = CellRange.cell(cell)
|
|
271
|
+
write(row, col, value)
|
|
272
|
+
value
|
|
273
|
+
end
|
|
274
|
+
|
|
243
275
|
# Appends a row after the last row written.
|
|
244
276
|
# @param values [Array] cell values, as for {#write}
|
|
245
|
-
# @param format [Format, Array<Format, nil>, nil] one for every cell, or
|
|
277
|
+
# @param format [Format, Hash, Array<Format, Hash, nil>, nil] one for every cell, or
|
|
246
278
|
# one per cell
|
|
247
279
|
# @return [self]
|
|
248
280
|
def append(values, format: nil)
|
|
249
|
-
_append(values, format)
|
|
281
|
+
_append(values, format.is_a?(Array) ? format.map { |f| Format._coerce(f) } : Format._coerce(format))
|
|
250
282
|
end
|
|
251
283
|
|
|
252
284
|
# @param columns [Integer, Range<Integer>]
|
|
@@ -283,7 +315,7 @@ module FastXlsx
|
|
|
283
315
|
# @raise [FastXlsx::Error] when it overlaps an earlier merge
|
|
284
316
|
def merge_range(*args)
|
|
285
317
|
range, (value, format) = CellRange.split(args, 1..2)
|
|
286
|
-
_merge_range(*range, value, format)
|
|
318
|
+
_merge_range(*range, value, Format._coerce(format))
|
|
287
319
|
end
|
|
288
320
|
|
|
289
321
|
# Highlights cells in the range by rule; see the README for each type's
|
|
@@ -292,11 +324,12 @@ module FastXlsx
|
|
|
292
324
|
# @param type [Symbol] :cell, :text, :formula, :data_bar or :color_scale
|
|
293
325
|
# @option options [Symbol] :criteria e.g. :>, :between, :contains
|
|
294
326
|
# @option options [Numeric, String, Array] :value
|
|
295
|
-
# @option options [Format] :format
|
|
327
|
+
# @option options [Format, Hash] :format
|
|
296
328
|
# @option options [Integer] :colors 2 or 3 (:color_scale)
|
|
297
329
|
# @return [self]
|
|
298
|
-
def conditional_format(*range, type:, **)
|
|
299
|
-
|
|
330
|
+
def conditional_format(*range, type:, **options)
|
|
331
|
+
options[:format] = Format._coerce(options[:format]) if options.key?(:format)
|
|
332
|
+
_conditional_format(*CellRange.split(range).first, { type: type, **options })
|
|
300
333
|
end
|
|
301
334
|
|
|
302
335
|
# Restricts what can be entered in the range; see the README.
|
|
@@ -356,8 +389,13 @@ module FastXlsx
|
|
|
356
389
|
# @option options [String] :name
|
|
357
390
|
# @option options [Boolean] :total_row, :banded_rows, :autofilter
|
|
358
391
|
# @return [self]
|
|
359
|
-
def add_table(*range, **)
|
|
360
|
-
|
|
392
|
+
def add_table(*range, **options)
|
|
393
|
+
if options[:columns].is_a?(Array)
|
|
394
|
+
options[:columns] = options[:columns].map do |column|
|
|
395
|
+
column.is_a?(Hash) && column.key?(:format) ? column.merge(format: Format._coerce(column[:format])) : column
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
_add_table(*CellRange.split(range).first, options)
|
|
361
399
|
end
|
|
362
400
|
|
|
363
401
|
# Printed page header, using Excel codes such as "&CPage &P of &N".
|
|
@@ -441,12 +479,77 @@ module FastXlsx
|
|
|
441
479
|
_protect(password, Array(allow))
|
|
442
480
|
end
|
|
443
481
|
|
|
482
|
+
# A range users can still edit on a protected sheet ({#protect}),
|
|
483
|
+
# optionally with its own password. Give each range its own name.
|
|
484
|
+
# @overload unprotect_range(*range, name: nil, password: nil)
|
|
485
|
+
# @param name [String, nil] shown in Excel's "Allow Edit Ranges"
|
|
486
|
+
# @return [self]
|
|
487
|
+
def unprotect_range(*range, name: nil, password: nil)
|
|
488
|
+
_unprotect_range(*CellRange.split(range).first, name, password)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# The cells selected when the file opens.
|
|
492
|
+
# @param range a cell range (see {Worksheet})
|
|
493
|
+
# @return [self]
|
|
494
|
+
def selection(*range)
|
|
495
|
+
_selection(*CellRange.split(range).first)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# The cell scrolled to the top left when the file opens.
|
|
499
|
+
# @param cell (row, col) or "B2"
|
|
500
|
+
# @return [self]
|
|
501
|
+
def top_left_cell(*cell)
|
|
502
|
+
_top_left_cell(*CellRange.cell(cell).first(2))
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Turns off one of Excel's warnings (green triangles) in the range, e.g.
|
|
506
|
+
# for codes stored as text. One kind per range (a rust_xlsxwriter limit);
|
|
507
|
+
# overlapping ranges are accepted, but Excel then uses only one of them.
|
|
508
|
+
# @overload ignore_error(*range, error)
|
|
509
|
+
# @param error [Symbol] :number_stored_as_text, :formula_error,
|
|
510
|
+
# :formula_differs, :formula_refers_to_empty_cells,
|
|
511
|
+
# :formula_omits_cells, :data_validation_error,
|
|
512
|
+
# :unlocked_cells_with_formula, :inconsistent_column_formula
|
|
513
|
+
# @return [self]
|
|
514
|
+
# @raise [FastXlsx::Error] when the range already has one
|
|
515
|
+
def ignore_error(*args)
|
|
516
|
+
range, (error, *) = CellRange.split(args, 1..1)
|
|
517
|
+
_ignore_error(*range, error)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Each row in the range is kept (about 1 KB) until saving.
|
|
521
|
+
# @param rows [Integer, Range<Integer>]
|
|
522
|
+
# @return [self]
|
|
523
|
+
# @raise [FastXlsx::Error] in :constant / :low mode, for rows already on disk
|
|
524
|
+
def hide_rows(rows)
|
|
525
|
+
_hide_rows(*CellRange.bounds(rows))
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# @param columns [Integer, Range<Integer>]
|
|
529
|
+
# @return [self]
|
|
530
|
+
def hide_columns(columns)
|
|
531
|
+
_hide_columns(*CellRange.bounds(columns))
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Default format for cells in these rows that are written without one.
|
|
535
|
+
# Use it or {#column_format} for a cell, not both: where both apply, Excel
|
|
536
|
+
# may combine them or show the row's. Each row is kept (about 1 KB) until
|
|
537
|
+
# saving, so style a whole sheet with {#column_format}.
|
|
538
|
+
# @param rows [Integer, Range<Integer>]
|
|
539
|
+
# @param format [Format, Hash]
|
|
540
|
+
# @return [self]
|
|
541
|
+
# @raise [FastXlsx::Error] in :constant / :low mode, for rows already on disk
|
|
542
|
+
def row_format(rows, format)
|
|
543
|
+
_row_format(*CellRange.bounds(rows), Format._coerce(format))
|
|
544
|
+
self
|
|
545
|
+
end
|
|
546
|
+
|
|
444
547
|
# Default format for cells in these columns that are written without one.
|
|
445
548
|
# @param columns [Integer, Range<Integer>]
|
|
446
|
-
# @param format [Format]
|
|
549
|
+
# @param format [Format, Hash]
|
|
447
550
|
# @return [self]
|
|
448
551
|
def column_format(columns, format)
|
|
449
|
-
_column_format(*CellRange.bounds(columns), format)
|
|
552
|
+
_column_format(*CellRange.bounds(columns), Format._coerce(format))
|
|
450
553
|
self
|
|
451
554
|
end
|
|
452
555
|
end
|
|
@@ -560,9 +663,88 @@ module FastXlsx
|
|
|
560
663
|
# border_color:, locked:, hidden:
|
|
561
664
|
# @raise [ArgumentError] for an unknown option or an invalid value
|
|
562
665
|
def self.new(**options)
|
|
666
|
+
options = _options(options)
|
|
563
667
|
# Apply border: first so border_left: etc. override it whatever the order.
|
|
564
|
-
|
|
565
|
-
_new(
|
|
668
|
+
ordered = options.key?(:border) ? { border: options[:border], **options.except(:border) } : options
|
|
669
|
+
format = _new(ordered)
|
|
670
|
+
format.instance_variable_set(:@options, options)
|
|
671
|
+
format.freeze
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Options with Symbol keys (String ones come from JSON or YAML) and frozen
|
|
675
|
+
# copies of the values, so the format can't change behind the file's back.
|
|
676
|
+
# @api private
|
|
677
|
+
def self._options(given)
|
|
678
|
+
options = given.to_h { |key, value| [key.to_sym, _frozen(value)] }.freeze
|
|
679
|
+
return options if options.size == given.size
|
|
680
|
+
|
|
681
|
+
twice = given.keys.map(&:to_sym).tally.select { |_, count| count > 1 }.keys
|
|
682
|
+
raise ArgumentError, "format option given twice (as a String and a Symbol): #{twice.join(", ")}"
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# @api private
|
|
686
|
+
def self._frozen(value)
|
|
687
|
+
value.frozen? ? value : value.dup.freeze
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# The options this format was created with.
|
|
691
|
+
# @return [Hash{Symbol => Object}] frozen
|
|
692
|
+
def to_h
|
|
693
|
+
@options
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Formats with the same options are equal; 10 and 10.0 count as the same.
|
|
697
|
+
# @return [Boolean]
|
|
698
|
+
def ==(other)
|
|
699
|
+
other.is_a?(Format) && to_h == other.to_h
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Equal as Hash keys: like Hash#eql?, 10 and 10.0 differ, matching {#hash}.
|
|
703
|
+
# @return [Boolean]
|
|
704
|
+
def eql?(other)
|
|
705
|
+
other.is_a?(Format) && to_h.eql?(other.to_h)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# @return [Integer]
|
|
709
|
+
def hash
|
|
710
|
+
[Format, to_h].hash
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# A new format with these options added to (or replacing) this one's.
|
|
714
|
+
# @param other [Format, Hash, nil] options to add, like Hash#merge
|
|
715
|
+
# @example
|
|
716
|
+
# title = header.merge(font_size: 16)
|
|
717
|
+
# bold_money = bold.merge(money)
|
|
718
|
+
# @return [Format]
|
|
719
|
+
# @raise [TypeError] when other has no #to_h
|
|
720
|
+
def merge(other = nil, **)
|
|
721
|
+
unless other.respond_to?(:to_h)
|
|
722
|
+
raise TypeError,
|
|
723
|
+
"can't merge #{other.class} into a Format: use a Hash or a Format"
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
Format.new(**to_h, **other.to_h, **)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Distinct Hashes kept by {._coerce}; past this, the oldest is dropped.
|
|
730
|
+
CACHE_SIZE = 1_024
|
|
731
|
+
|
|
732
|
+
# A Format for a Hash of options; equal Hashes share one, so a Hash written
|
|
733
|
+
# with every row costs about as much as a Format made once. Other values
|
|
734
|
+
# are returned as they are. An invalid Hash raises and isn't kept.
|
|
735
|
+
# @api private
|
|
736
|
+
def self._coerce(format)
|
|
737
|
+
return format unless format.is_a?(Hash)
|
|
738
|
+
|
|
739
|
+
# ponytail: evicts the oldest entry, not the least used one, since
|
|
740
|
+
# moving each hit costs every write; a Hash still in use is rebuilt at
|
|
741
|
+
# worst once per CACHE_SIZE new Hashes.
|
|
742
|
+
cache = (@cache ||= {})
|
|
743
|
+
cache.fetch(format) do
|
|
744
|
+
coerced = new(**format)
|
|
745
|
+
cache.shift if cache.size >= CACHE_SIZE
|
|
746
|
+
cache[format.dup.freeze] = coerced
|
|
747
|
+
end
|
|
566
748
|
end
|
|
567
749
|
end
|
|
568
750
|
end
|
data/sig/fast_xlsx.rbs
CHANGED
|
@@ -8,11 +8,14 @@ module FastXlsx
|
|
|
8
8
|
def read: () -> String
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# A Format, or a Hash of its options.
|
|
12
|
+
type format = Format | Hash[Symbol | String, untyped]
|
|
13
|
+
|
|
11
14
|
type cell = Numeric | String | Time | Date | Formula | URL | RichString | bool | nil | _ToS
|
|
12
15
|
|
|
13
16
|
class RichString
|
|
14
17
|
attr_reader segments: Array[[String, Format?]]
|
|
15
|
-
def initialize: (*(_ToS | [_ToS,
|
|
18
|
+
def initialize: (*(_ToS | [_ToS, format?]) parts) -> void
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
class Formula
|
|
@@ -28,8 +31,8 @@ module FastXlsx
|
|
|
28
31
|
|
|
29
32
|
class Workbook
|
|
30
33
|
MEMORY_MODES: Array[Symbol]
|
|
31
|
-
def self.new: (?memory: :standard | :constant | :low) -> Workbook
|
|
32
|
-
def add_worksheet: (?String? name) -> Worksheet
|
|
34
|
+
def self.new: (?memory: :standard | :constant | :low) ?{ (Workbook) -> void } -> Workbook
|
|
35
|
+
def add_worksheet: (?String? name) ?{ (Worksheet) -> void } -> Worksheet
|
|
33
36
|
def worksheets: () -> Array[Worksheet]
|
|
34
37
|
def worksheet: (String name) -> Worksheet?
|
|
35
38
|
def properties: (?title: String, ?subject: String, ?author: String, ?manager: String,
|
|
@@ -53,16 +56,35 @@ module FastXlsx
|
|
|
53
56
|
?rotation: Integer, ?indent: Integer,
|
|
54
57
|
?border: border, ?border_left: border, ?border_right: border,
|
|
55
58
|
?border_top: border, ?border_bottom: border, ?border_color: color) -> Format
|
|
59
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
60
|
+
def merge: (?format? other, **untyped options) -> Format
|
|
61
|
+
def ==: (untyped other) -> bool
|
|
62
|
+
def eql?: (untyped other) -> bool
|
|
63
|
+
def hash: () -> Integer
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
class Worksheet
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
def
|
|
67
|
+
def []=: (String cell, cell value) -> cell
|
|
68
|
+
| (Integer row, Integer col, cell value) -> cell
|
|
69
|
+
def write: (Integer row, Integer col, cell value, ?format? format) -> self
|
|
70
|
+
| (String cell, cell value, ?format? format) -> self
|
|
71
|
+
def append: (Array[cell] values, ?format: format? | Array[format?]) -> self
|
|
62
72
|
def <<: (Array[cell] row) -> self
|
|
63
73
|
def concat: (Array[Array[cell]] rows) -> self
|
|
64
74
|
def column_width: (Integer | Range[Integer] columns, Numeric width) -> self
|
|
65
|
-
def
|
|
75
|
+
def hide_rows: (Integer | Range[Integer] rows) -> self
|
|
76
|
+
def hide_columns: (Integer | Range[Integer] columns) -> self
|
|
77
|
+
def row_format: (Integer | Range[Integer] rows, format format) -> self
|
|
78
|
+
def default_row_height: (Numeric height) -> self
|
|
79
|
+
def selection: (*cell_range range) -> self
|
|
80
|
+
def top_left_cell: (Integer row, Integer col) -> self
|
|
81
|
+
| (String cell) -> self
|
|
82
|
+
type ignorable_error = :number_stored_as_text | :formula_error | :formula_differs | :formula_refers_to_empty_cells
|
|
83
|
+
| :formula_omits_cells | :data_validation_error | :unlocked_cells_with_formula
|
|
84
|
+
| :inconsistent_column_formula
|
|
85
|
+
def ignore_error: (*cell_range | ignorable_error args) -> self
|
|
86
|
+
def unprotect_range: (*cell_range range, ?name: String?, ?password: String?) -> self
|
|
87
|
+
def column_format: (Integer | Range[Integer] columns, format format) -> self
|
|
66
88
|
def autofit: () -> self
|
|
67
89
|
# A cell range: four 0-based numbers, an Excel reference ("A1:D10"), or
|
|
68
90
|
# rows and columns as Integers or Ranges. Methods with options take it as
|
|
@@ -95,13 +117,13 @@ module FastXlsx
|
|
|
95
117
|
def group_rows: (Integer | Range[Integer] rows, ?collapsed: bool) -> self
|
|
96
118
|
def group_columns: (Integer | Range[Integer] columns, ?collapsed: bool) -> self
|
|
97
119
|
def protect: (?password: String?, ?allow: protect_action | Array[protect_action] | nil) -> self
|
|
98
|
-
def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?
|
|
99
|
-
| (String range, cell value, ?
|
|
100
|
-
| (indexes rows, indexes cols, cell value, ?
|
|
120
|
+
def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?format? format) -> self
|
|
121
|
+
| (String range, cell value, ?format? format) -> self
|
|
122
|
+
| (indexes rows, indexes cols, cell value, ?format? format) -> self
|
|
101
123
|
def conditional_format: (*cell_range range,
|
|
102
124
|
type: :cell | :text | :formula | :data_bar | :color_scale,
|
|
103
125
|
?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
|
|
104
|
-
?format:
|
|
126
|
+
?format: format, ?colors: 2 | 3) -> self
|
|
105
127
|
def data_validation: (*cell_range range,
|
|
106
128
|
type: :list | :whole_number | :decimal | :text_length,
|
|
107
129
|
?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
|
|
@@ -121,7 +143,7 @@ module FastXlsx
|
|
|
121
143
|
| (String cell, type: Symbol, series: Array[chart_series],
|
|
122
144
|
?title: String, ?x_axis: String, ?y_axis: String,
|
|
123
145
|
?width: Integer, ?height: Integer) -> self
|
|
124
|
-
type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format:
|
|
146
|
+
type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: format }
|
|
125
147
|
|
|
126
148
|
def add_table: (*cell_range range,
|
|
127
149
|
?columns: Array[table_column], ?style: Symbol, ?name: String,
|