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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8326db5fb0c181c2c47af1f6b0d5799aa0bb69ff2d3b63d5286dbd1cd32fa2b8
4
- data.tar.gz: 560335ade1b435b9084cb3d48e4e4b20d906140c4d2a6e243751d93d1eb01964
3
+ metadata.gz: eddcb1a0c063b94222c0a83e15b7a855a83b26674390ee87f59f1938d7007c9f
4
+ data.tar.gz: 5aa9b9a7b6dbfb70bb53364b16fa31bb2c745534aefc9f0631367ed6cf658043
5
5
  SHA512:
6
- metadata.gz: 360e7a3686eb4705527139fc7c50834046aa4d45d7e3e79e6f913475cb039de2e64119600a8c9a0e1147136be98a6776f8bbe8db267b67533e99c390fdaced4e
7
- data.tar.gz: c23307efb6c2c0ceb1faef4336be02512f98cda7bff60b01324e614d82374247187ee1225a04c04a1cf9208d8b4abce3852a943f166f57f53801c743c4df1af3
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.5.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.5.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
@@ -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
- CellValue::Empty => Ok(()),
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 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.
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
- match columns
1105
+ let row_style = rows
1059
1106
  .iter()
1060
1107
  .rev()
1061
- .find(|(first, last, _)| (*first..=*last).contains(&col))
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((_, _, style)) => style.for_date(time),
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
- Self::format_for(&tables, &columns, row, *col, format.as_deref(), value);
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
- let format = match args.get(3) {
1114
- Some(f) => Option::<&Format>::try_convert(*f)?,
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(&tables, &columns, row, col, format.map(|f| &*f.0), &value),
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))?;
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FastXlsx
4
4
  # The gem version.
5
- VERSION = "0.5.0"
5
+ VERSION = "0.7.0"
6
6
  end
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 { |part| part.is_a?(Array) ? [part[0].to_s, part[1]] : [part.to_s, nil] }.freeze
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).tap { |ws| worksheets << ws }
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
- _conditional_format(*CellRange.split(range).first, { type: type, ** })
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
- _add_table(*CellRange.split(range).first, { ** })
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
- options = { border: options[:border], **options.except(:border) } if options.key?(:border)
565
- _new(options)
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, Format?]) parts) -> void
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 write: (Integer row, Integer col, cell value, ?Format? format) -> self
60
- | (String cell, cell value, ?Format? format) -> self
61
- def append: (Array[cell] values, ?format: Format? | Array[Format?]) -> self
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 column_format: (Integer | Range[Integer] columns, Format format) -> self
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, ?Format? format) -> self
99
- | (String range, cell value, ?Format? format) -> self
100
- | (indexes rows, indexes cols, cell value, ?Format? format) -> self
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: Format, ?colors: 2 | 3) -> self
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: 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,
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.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac