fast_xlsx 0.1.2 → 0.2.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: d4a6f42c2c4c1f1a9e4a5f59441d183e2257abbbbfd9593fa22d7be4a9cf7519
4
- data.tar.gz: 1c89a99feafa34f127e14d49175503da754809ca2436197bbd1e4e59290a145d
3
+ metadata.gz: 85532243a1cd812798374658f4b5673ea9cda88586e876b76281f99cbd8dd065
4
+ data.tar.gz: 67747bd17f6f4ecf7ed9041767498de5d0d3d416922250578bf5b87c0d41ad1c
5
5
  SHA512:
6
- metadata.gz: 75823da46a7dcf4a0bc921211871fe591f9be3ccf788ce42456bd109f77778a9886dce51123b1d86e18255169770e3b28d9ebb5e03f4de497b170be6092bf150
7
- data.tar.gz: 5f5439f853da37aa5730636dc20e6daf784b9b0374ad03aba78c10720e3976d080ec287938209ea62fb3e996931b904b0ecd05e59a83b5dda8468fde0424fb8b
6
+ metadata.gz: 75d89ae4b66fce6129f09d8ba18d1766571fe740e3069eebc9be5334dbd254f5a75dbccf56c3d250a6a0deacfa109d86971b0974289d0822467b8971d6b3d40c
7
+ data.tar.gz: e9ff13d9d4014d5b2034ffdff56897d87ff00df6688145c6ae4bc637690323389ccd53c4c8e651e5da5177fb06ba2f141f96a3fe9dc60e0949ce69672984edb4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-10-01
4
+
5
+ ### Added
6
+
7
+ - Range methods (`autofilter`, `merge_range`, `conditional_format`, `data_validation`, `add_table`) also take an Excel reference (`"A1:D10"`) or rows and columns as Integers or Ranges (`0..9, 0..3`).
8
+ - `Workbook#define_name` for workbook-wide and sheet-scoped defined names.
9
+ - `Worksheet#group_rows` and `#group_columns` (outline groups, optionally collapsed); `group_rows` raises in `:constant` / `:low` mode.
10
+ - `Worksheet#protect(password:, allow:)` locks a sheet; `Format` options `locked: false` and `hidden: true` keep cells editable or hide formulas.
11
+
12
+ ### Changed
13
+
14
+ - `column_width`, `column_format` and the other methods that take a Range raise `ArgumentError` for an empty, reversed or endless Range, or one that isn't Integers, instead of a `TypeError` or `RangeError`.
15
+
3
16
  ## [0.1.2] - 2026-09-30
4
17
 
5
18
  ### Fixed
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # FastXlsx
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/fast_xlsx.svg)](https://badge.fury.io/rb/fast_xlsx)
3
4
  [![codecov](https://codecov.io/gh/7a6163/fast_xlsx/graph/badge.svg)](https://codecov.io/gh/7a6163/fast_xlsx)
4
5
 
5
6
  Fast `.xlsx` writer for Ruby, built on [rust_xlsxwriter](https://github.com/jmcnamara/rust_xlsxwriter) via [magnus](https://github.com/matsadler/magnus).
@@ -75,6 +76,8 @@ ws.write(1, 2, Date.today, date) # format one cell
75
76
  | `valign` | `:top`, `:center`, `:bottom` |
76
77
  | `border`, `border_left`, `border_right`, `border_top`, `border_bottom` | `:thin`, `:medium`, `:thick`, `:dashed`, `:dotted`, `:double`, `:hair` |
77
78
  | `border_color` | `"#RRGGBB"` or `0xRRGGBB` |
79
+ | `locked` | `false` keeps the cell editable on a protected sheet (default `true`) |
80
+ | `hidden` | `true` hides the cell's formula on a protected sheet |
78
81
 
79
82
  Per-side borders override `border`. Unknown options and invalid values raise `ArgumentError`.
80
83
 
@@ -85,7 +88,18 @@ ws.column_width(0, 20) # column A, width in characters
85
88
  ws.column_width(1..3, 12) # columns B–D
86
89
  ws.column_format(4, FastXlsx::Format.new(num_format: "#,##0.00")) # default for cells in E written without a format
87
90
  ws.autofit # size other columns to the data written so far; set widths are kept
88
- ws.autofilter(0, 0, 100, 3) # filter buttons on A1:D101 (first_row, first_col, last_row, last_col)
91
+ ws.autofilter("A1:D101") # filter buttons on A1:D101
92
+ ```
93
+
94
+ ### Cell ranges
95
+
96
+ `autofilter`, `merge_range`, `conditional_format`, `data_validation` and `add_table` take a range in any of these styles:
97
+
98
+ ```ruby
99
+ ws.autofilter(0, 0, 100, 3) # four 0-based numbers: first_row, first_col, last_row, last_col
100
+ ws.autofilter("A1:D101") # an Excel reference; "$A$1:$D$101" and a single "B2" work too
101
+ ws.autofilter(0..100, 0..3) # rows and columns, each an Integer or a Range
102
+ ws.merge_range(0, 0..3, "Q3 report", title) # row 1, columns A–D
89
103
  ```
90
104
 
91
105
  `autofit` only sees rows still in memory, so in `:constant` / `:low` memory mode it ignores rows already written to disk; set widths with `column_width` instead.
@@ -103,6 +117,36 @@ ws.page_footer("&L&A", margin: 0.2) # sheet name on the left; margin
103
117
  ws.margins(left: 0.5, top: 1) # other margins keep Excel's defaults
104
118
  ```
105
119
 
120
+ ### Outline groups
121
+
122
+ ```ruby
123
+ ws.group_rows(1..10) # rows 2–11 get an expand/collapse button
124
+ ws.group_rows(1..4) # grouping again nests them (up to 7 levels)
125
+ ws.group_columns(2..3, collapsed: true) # columns C–D, collapsed until expanded
126
+ ```
127
+
128
+ `group_rows` needs `memory: :standard`: in `:constant` / `:low` mode rust_xlsxwriter writes rows without their outline level, so it raises. `group_columns` works in every mode.
129
+
130
+ ### Defined names
131
+
132
+ ```ruby
133
+ wb.define_name("Rate", "=0.96") # workbook-wide; use as =A1*Rate
134
+ wb.define_name("Report!Sales", "=Report!$B$2:$B$13") # only on the Report sheet
135
+ ```
136
+
137
+ Invalid names raise `FastXlsx::Error` right away; duplicate names, and names for a sheet that doesn't exist, raise when saving.
138
+
139
+ ### Protection
140
+
141
+ ```ruby
142
+ input = FastXlsx::Format.new(locked: false)
143
+ ws.write(1, 1, 0, input) # B2 stays editable
144
+ ws.protect # lock everything else
145
+ ws.protect(password: "secret", allow: %i[sort use_autofilter]) # or with a password and allowed actions
146
+ ```
147
+
148
+ `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.
149
+
106
150
  ### Conditional formats
107
151
 
108
152
  ```ruby
data/bench/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- fast_xlsx (0.1.2)
4
+ fast_xlsx (0.2.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.1.2)
80
+ fast_xlsx (0.2.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
@@ -15,7 +15,8 @@ use rust_xlsxwriter::{
15
15
  };
16
16
 
17
17
  use rust_xlsxwriter::{
18
- Chart, ChartType, DocProperties, Image, Note, Table, TableColumn, TableFunction, TableStyle,
18
+ Chart, ChartType, DocProperties, Image, Note, ProtectionOptions, Table, TableColumn,
19
+ TableFunction, TableStyle,
19
20
  };
20
21
 
21
22
  // These constants are defined in lib/fast_xlsx.rb before this extension loads.
@@ -141,6 +142,19 @@ impl Workbook {
141
142
  self.inner.lock().unwrap().save(path).map_err(xerr)
142
143
  }
143
144
 
145
+ // "Name" for the whole workbook, "Sheet1!Name" for one sheet. Duplicate
146
+ // names and unknown sheets are reported when saving, since sheets can be
147
+ // added after the name.
148
+ fn define_name(rb_self: Obj<Self>, name: String, formula: String) -> Result<Obj<Self>, Error> {
149
+ rb_self
150
+ .inner
151
+ .lock()
152
+ .unwrap()
153
+ .define_name(name, &formula)
154
+ .map_err(xerr)?;
155
+ Ok(rb_self)
156
+ }
157
+
144
158
  fn set_properties(ruby: &Ruby, rb_self: &Self, fields: RHash) -> Result<(), Error> {
145
159
  let mut props = DocProperties::new();
146
160
  fields.foreach(|key: Symbol, value: String| {
@@ -653,6 +667,25 @@ fn table_column(
653
667
  Ok((column, format))
654
668
  }
655
669
 
670
+ // Actions users may still take on a protected sheet (selecting cells is
671
+ // always allowed).
672
+ type ProtectionFlag = fn(&mut ProtectionOptions) -> &mut bool;
673
+ const PROTECTION_ALLOW: &[(&str, ProtectionFlag)] = &[
674
+ ("format_cells", |o| &mut o.format_cells),
675
+ ("format_columns", |o| &mut o.format_columns),
676
+ ("format_rows", |o| &mut o.format_rows),
677
+ ("insert_columns", |o| &mut o.insert_columns),
678
+ ("insert_rows", |o| &mut o.insert_rows),
679
+ ("insert_links", |o| &mut o.insert_links),
680
+ ("delete_columns", |o| &mut o.delete_columns),
681
+ ("delete_rows", |o| &mut o.delete_rows),
682
+ ("sort", |o| &mut o.sort),
683
+ ("use_autofilter", |o| &mut o.use_autofilter),
684
+ ("use_pivot_tables", |o| &mut o.use_pivot_tables),
685
+ ("edit_scenarios", |o| &mut o.edit_scenarios),
686
+ ("edit_objects", |o| &mut o.edit_objects),
687
+ ];
688
+
656
689
  const BORDERS: &[(&str, FormatBorder)] = &[
657
690
  ("thin", FormatBorder::Thin),
658
691
  ("medium", FormatBorder::Medium),
@@ -710,6 +743,11 @@ impl Format {
710
743
  }
711
744
  "indent" => taken.set_indent(u8::try_convert(value)?),
712
745
  "shrink" if value.to_bool() => taken.set_shrink(),
746
+ // For protected sheets: locked: false leaves a cell editable,
747
+ // hidden: true hides its formula.
748
+ "locked" if value.to_bool() => taken.set_locked(),
749
+ "locked" => taken.set_unlocked(),
750
+ "hidden" if value.to_bool() => taken.set_hidden(),
713
751
  "border_color" => taken.set_border_color(color(ruby, value)?),
714
752
  "num_format" => taken.set_num_format(String::try_convert(value)?),
715
753
  "font_size" => taken.set_font_size(f64::try_convert(value)?),
@@ -742,7 +780,8 @@ impl Format {
742
780
  "border_right" => taken.set_border_right(choice(ruby, "border", value, BORDERS)?),
743
781
  "border_top" => taken.set_border_top(choice(ruby, "border", value, BORDERS)?),
744
782
  "border_bottom" => taken.set_border_bottom(choice(ruby, "border", value, BORDERS)?),
745
- "bold" | "italic" | "underline" | "text_wrap" | "strikeout" | "shrink" => taken,
783
+ "bold" | "italic" | "underline" | "text_wrap" | "strikeout" | "shrink"
784
+ | "hidden" => taken,
746
785
  other => {
747
786
  return Err(Error::new(
748
787
  ruby.exception_arg_error(),
@@ -1451,6 +1490,71 @@ impl Worksheet {
1451
1490
  Ok(rb_self)
1452
1491
  }
1453
1492
 
1493
+ fn group_rows(
1494
+ ruby: &Ruby,
1495
+ rb_self: Obj<Self>,
1496
+ first: u32,
1497
+ last: u32,
1498
+ collapsed: bool,
1499
+ ) -> Result<Obj<Self>, Error> {
1500
+ // rust_xlsxwriter writes no outline levels when it writes rows to
1501
+ // disk as it goes.
1502
+ if rb_self.flushes_rows {
1503
+ return Err(Error::new(
1504
+ ruby.get_inner(&ERROR),
1505
+ "group_rows needs memory: :standard (rows written to disk as they go lose their outline level)",
1506
+ ));
1507
+ }
1508
+ rb_self.with_ws(|ws| {
1509
+ if collapsed {
1510
+ ws.group_rows_collapsed(first, last)
1511
+ } else {
1512
+ ws.group_rows(first, last)
1513
+ }
1514
+ .map(|_| ())
1515
+ .map_err(xerr)
1516
+ })?;
1517
+ Ok(rb_self)
1518
+ }
1519
+
1520
+ fn group_columns(
1521
+ rb_self: Obj<Self>,
1522
+ first: u16,
1523
+ last: u16,
1524
+ collapsed: bool,
1525
+ ) -> Result<Obj<Self>, Error> {
1526
+ rb_self.with_ws(|ws| {
1527
+ if collapsed {
1528
+ ws.group_columns_collapsed(first, last)
1529
+ } else {
1530
+ ws.group_columns(first, last)
1531
+ }
1532
+ .map(|_| ())
1533
+ .map_err(xerr)
1534
+ })?;
1535
+ Ok(rb_self)
1536
+ }
1537
+
1538
+ fn protect(
1539
+ ruby: &Ruby,
1540
+ rb_self: Obj<Self>,
1541
+ password: Option<String>,
1542
+ allow: RArray,
1543
+ ) -> Result<Obj<Self>, Error> {
1544
+ let mut options = ProtectionOptions::new();
1545
+ each_entry(allow, |_, action| {
1546
+ *choice(ruby, "protect action", action, PROTECTION_ALLOW)?(&mut options) = true;
1547
+ Ok(())
1548
+ })?;
1549
+ rb_self.with_ws(|ws| {
1550
+ // Always set the password, so protecting again replaces it ("" is none).
1551
+ ws.protect_with_password(password.as_deref().unwrap_or(""));
1552
+ ws.protect_with_options(&options); // keeps the password
1553
+ Ok(())
1554
+ })?;
1555
+ Ok(rb_self)
1556
+ }
1557
+
1454
1558
  fn name(&self) -> Result<String, Error> {
1455
1559
  self.with_ws(|ws| Ok(ws.name()))
1456
1560
  }
@@ -1469,6 +1573,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1469
1573
  wb.define_method("_add_worksheet", method!(Workbook::add_worksheet, 1))?;
1470
1574
  wb.define_method("to_xlsx", method!(Workbook::to_xlsx, 0))?;
1471
1575
  wb.define_method("_save", method!(Workbook::save, 1))?;
1576
+ wb.define_method("define_name", method!(Workbook::define_name, 2))?;
1472
1577
  wb.define_method("_properties", method!(Workbook::set_properties, 1))?;
1473
1578
 
1474
1579
  let ws = module.define_class("Worksheet", ruby.class_object())?;
@@ -1477,7 +1582,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1477
1582
  ws.define_method("_column_width", method!(Worksheet::set_column_width, 3))?;
1478
1583
  ws.define_method("_column_format", method!(Worksheet::set_column_format, 3))?;
1479
1584
  ws.define_method("_autofit", method!(Worksheet::autofit, 0))?;
1480
- ws.define_method("autofilter", method!(Worksheet::autofilter, 4))?;
1585
+ ws.define_method("_autofilter", method!(Worksheet::autofilter, 4))?;
1481
1586
  ws.define_method("name", method!(Worksheet::name, 0))?;
1482
1587
  ws.define_method("_write_comment", method!(Worksheet::write_comment, 4))?;
1483
1588
  ws.define_method("_insert_image", method!(Worksheet::insert_image, 4))?;
@@ -1498,6 +1603,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1498
1603
  "vertical_page_breaks",
1499
1604
  method!(Worksheet::set_vertical_page_breaks, 1),
1500
1605
  )?;
1606
+ ws.define_method("_group_rows", method!(Worksheet::group_rows, 3))?;
1607
+ ws.define_method("_group_columns", method!(Worksheet::group_columns, 3))?;
1608
+ ws.define_method("_protect", method!(Worksheet::protect, 2))?;
1501
1609
  ws.define_method("_merge_range", method!(Worksheet::merge_range, 6))?;
1502
1610
 
1503
1611
  let format = module.define_class("Format", ruby.class_object())?;
@@ -14,6 +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 needs a few CPU seconds; a mutant that loops forever is
18
+ // stopped instead of running on after cargo-mutants gives up on it.
19
+ .env("FAST_XLSX_TEST_CPU_SECONDS", "120")
17
20
  .status()
18
21
  .expect("could not run `bundle exec rake compile test`");
19
22
  assert!(status.success(), "the Ruby test suite failed");
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastXlsx
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/fast_xlsx.rb CHANGED
@@ -113,11 +113,11 @@ module FastXlsx
113
113
 
114
114
  # columns: a 0-based column index or a Range of them. width is in characters.
115
115
  def column_width(columns, width)
116
- bounds = column_bounds(columns)
117
- _column_width(*bounds, width)
116
+ range = CellRange.bounds(columns)
117
+ _column_width(*range, width)
118
118
  @fixed_widths ||= {}
119
- @fixed_widths.delete(bounds) # re-insert so autofit replays calls in order
120
- @fixed_widths[bounds] = width
119
+ @fixed_widths.delete(range) # re-insert so autofit replays calls in order
120
+ @fixed_widths[range] = width
121
121
  self
122
122
  end
123
123
 
@@ -129,21 +129,32 @@ module FastXlsx
129
129
  self
130
130
  end
131
131
 
132
+ # The methods below take a cell range in any of these styles:
133
+ # (first_row, first_col, last_row, last_col) four 0-based numbers
134
+ # ("A1:D10") or ("B2") an Excel reference
135
+ # (rows, cols) Integers or Ranges, e.g. (0..9, 0..3)
136
+
137
+ # Filter buttons on the range's first row.
138
+ def autofilter(*range)
139
+ _autofilter(*CellRange.split(range).first)
140
+ end
141
+
132
142
  # Merges the range and writes value (any cell type) into its first cell.
133
- def merge_range(first_row, first_col, last_row, last_col, value, format = nil)
134
- _merge_range(first_row, first_col, last_row, last_col, value, format)
143
+ def merge_range(*args)
144
+ range, (value, format) = CellRange.split(args, 1..2)
145
+ _merge_range(*range, value, format)
135
146
  end
136
147
 
137
148
  # Highlights cells in the range by rule. type: :cell, :text, :formula,
138
149
  # :data_bar or :color_scale; see the README for each type's options.
139
- def conditional_format(first_row, first_col, last_row, last_col, type:, **)
140
- _conditional_format(first_row, first_col, last_row, last_col, { type: type, ** })
150
+ def conditional_format(*range, type:, **)
151
+ _conditional_format(*CellRange.split(range).first, { type: type, ** })
141
152
  end
142
153
 
143
154
  # Restricts what can be entered in the range. type: :list, :whole_number,
144
155
  # :decimal or :text_length; see the README for the options.
145
- def data_validation(first_row, first_col, last_row, last_col, type:, **)
146
- _data_validation(first_row, first_col, last_row, last_col, { type: type, ** })
156
+ def data_validation(*range, type:, **)
157
+ _data_validation(*CellRange.split(range).first, { type: type, ** })
147
158
  end
148
159
 
149
160
  # Adds a comment (Excel "note") to a cell.
@@ -170,8 +181,8 @@ module FastXlsx
170
181
  # into an Excel table. columns: header Strings or { header:, total:,
171
182
  # total_label:, format: }; other options: style:, name:, total_row:,
172
183
  # banded_rows:, autofilter:.
173
- def add_table(first_row, first_col, last_row, last_col, **)
174
- _add_table(first_row, first_col, last_row, last_col, { ** })
184
+ def add_table(*range, **)
185
+ _add_table(*CellRange.split(range).first, { ** })
175
186
  end
176
187
 
177
188
  # Printed page header/footer using Excel codes such as "&CPage &P of &N".
@@ -192,18 +203,106 @@ module FastXlsx
192
203
  self
193
204
  end
194
205
 
206
+ # Outline group with an expand/collapse button. rows: a 0-based row index
207
+ # or a Range; grouping rows already grouped nests them (up to 7 levels).
208
+ # collapsed: true hides them until expanded.
209
+ def group_rows(rows, collapsed: false)
210
+ _group_rows(*CellRange.bounds(rows), collapsed)
211
+ end
212
+
213
+ def group_columns(columns, collapsed: false)
214
+ _group_columns(*CellRange.bounds(columns), collapsed)
215
+ end
216
+
217
+ # Locks the sheet against editing. Cells whose format has locked: false
218
+ # stay editable. allow: actions users may still take, any of :format_cells,
219
+ # :format_columns, :format_rows, :insert_columns, :insert_rows,
220
+ # :insert_links, :delete_columns, :delete_rows, :sort, :use_autofilter,
221
+ # :use_pivot_tables, :edit_scenarios, :edit_objects.
222
+ def protect(password: nil, allow: [])
223
+ _protect(password, Array(allow))
224
+ end
225
+
195
226
  # Default format for cells in these columns that are written without one.
196
227
  def column_format(columns, format)
197
- _column_format(*column_bounds(columns), format)
228
+ _column_format(*CellRange.bounds(columns), format)
198
229
  self
199
230
  end
231
+ end
232
+
233
+ # Cell ranges in the styles Worksheet methods accept: four 0-based numbers,
234
+ # an Excel reference ("A1:D10", "B2"), or rows and columns as Integers or
235
+ # Ranges.
236
+ module CellRange
237
+ REF = /\A\$?([A-Za-z]{1,3})\$?([1-9]\d*)\z/ # ASCII only: /i also matches the Kelvin sign
238
+ FORMS = 'a range is (first_row, first_col, last_row, last_col), "A1:D10" or (rows, cols)'
239
+
240
+ module_function
241
+
242
+ # Splits a range off the front of args and checks how many args follow.
243
+ # Returns [[first_row, first_col, last_row, last_col], the args after it].
244
+ def split(args, following = 0..0)
245
+ range, rest = parse(args)
246
+ unless following.cover?(rest.size)
247
+ expected = following.minmax.uniq.join("..")
248
+ raise ArgumentError, "wrong number of arguments after the cell range (given #{rest.size}, " \
249
+ "expected #{expected}); #{FORMS}"
250
+ end
251
+
252
+ [range, rest]
253
+ end
254
+
255
+ def parse(args)
256
+ case args
257
+ in [Numeric, Numeric, Numeric, Numeric, *rest] then [args.first(4), rest]
258
+ in [String => ref, *rest] then [excel(ref), rest]
259
+ in [Integer | Range => rows, Integer | Range => cols, *rest]
260
+ [rows_and_cols(rows, cols), rest]
261
+ else
262
+ raise ArgumentError, "expected a cell range, got #{args.inspect}; #{FORMS}"
263
+ end
264
+ end
265
+
266
+ def rows_and_cols(rows, cols)
267
+ first_row, last_row = bounds(rows)
268
+ first_col, last_col = bounds(cols)
269
+ [first_row, first_col, last_row, last_col]
270
+ end
271
+
272
+ # "A1:D10", "$A$1:$D$10" or a single "B2".
273
+ def excel(ref)
274
+ cells = cell_matches(ref)
275
+ rows = cells.map { |m| m[2].to_i - 1 }
276
+ cols = cells.map { |m| column(m[1]) }
277
+ [rows.min, cols.min, rows.max, cols.max]
278
+ end
279
+
280
+ def cell_matches(ref)
281
+ cells = ref.split(":", -1).map { |cell| REF.match(cell) }
282
+ return cells if (1..2).cover?(cells.size) && cells.all?
283
+
284
+ raise ArgumentError, "invalid cell range #{ref.inspect}: use e.g. \"A1:D10\" or \"B2\""
285
+ end
286
+
287
+ # "A" => 0, "AA" => 26.
288
+ def column(letters)
289
+ letters.upcase.each_char.reduce(0) { |n, c| (n * 26) + c.ord - 64 } - 1
290
+ end
291
+
292
+ # [first, last] of an index or a Range.
293
+ def bounds(indexes)
294
+ return [indexes, indexes] if indexes.is_a?(Integer)
295
+ unless indexes.is_a?(Range) && indexes.begin.is_a?(Integer) && indexes.end.is_a?(Integer)
296
+ raise ArgumentError, "expected an Integer or a Range of Integers, got #{indexes.inspect}"
297
+ end
200
298
 
201
- private
299
+ first, last = indexes.minmax
300
+ raise ArgumentError, "empty range #{indexes.inspect}" unless first
202
301
 
203
- def column_bounds(columns)
204
- columns.is_a?(Integer) ? [columns, columns] : columns.minmax
302
+ [first, last]
205
303
  end
206
304
  end
305
+ private_constant :CellRange
207
306
 
208
307
  # Cell style, e.g. Format.new(bold: true). Pass to Worksheet#write.
209
308
  class Format
data/sig/fast_xlsx.rbs CHANGED
@@ -36,6 +36,7 @@ module FastXlsx
36
36
  ?company: String, ?category: String, ?keywords: String,
37
37
  ?comments: String, ?status: String) -> self
38
38
  def to_xlsx: () -> String
39
+ def define_name: (String name, String formula) -> self
39
40
  def save: (String | _ToPath path) -> void
40
41
  end
41
42
 
@@ -46,7 +47,7 @@ module FastXlsx
46
47
  type underline = bool | :single | :double | :single_accounting | :double_accounting
47
48
 
48
49
  def self.new: (?bold: bool, ?italic: bool, ?underline: underline, ?strikeout: bool, ?text_wrap: bool,
49
- ?shrink: bool, ?num_format: String, ?font_size: Numeric, ?font_name: String,
50
+ ?shrink: bool, ?locked: bool, ?hidden: bool, ?num_format: String, ?font_size: Numeric, ?font_name: String,
50
51
  ?font_color: color, ?bg_color: color, ?font_script: :superscript | :subscript,
51
52
  ?align: :left | :center | :right, ?valign: :top | :center | :bottom,
52
53
  ?rotation: Integer, ?indent: Integer,
@@ -62,7 +63,14 @@ module FastXlsx
62
63
  def column_width: (Integer | Range[Integer] columns, Numeric width) -> self
63
64
  def column_format: (Integer | Range[Integer] columns, Format format) -> self
64
65
  def autofit: () -> self
66
+ # A cell range: four 0-based numbers, an Excel reference ("A1:D10"), or
67
+ # rows and columns as Integers or Ranges. Methods with options take it as
68
+ # *cell_range so the keywords can follow.
69
+ type indexes = Integer | Range[Integer]
70
+ type cell_range = Integer | String | Range[Integer]
65
71
  def autofilter: (Integer first_row, Integer first_col, Integer last_row, Integer last_col) -> self
72
+ | (String range) -> self
73
+ | (indexes rows, indexes cols) -> self
66
74
  def freeze_panes: (Integer row, Integer col) -> self
67
75
  def row_height: (Integer row, Numeric height) -> self
68
76
  def page_breaks: (Array[Integer] rows) -> self
@@ -71,12 +79,20 @@ module FastXlsx
71
79
  def page_footer: (String text, ?margin: Numeric?) -> self
72
80
  def margins: (?left: Numeric?, ?right: Numeric?, ?top: Numeric?, ?bottom: Numeric?,
73
81
  ?header: Numeric?, ?footer: Numeric?) -> self
82
+ type protect_action = :format_cells | :format_columns | :format_rows | :insert_columns | :insert_rows
83
+ | :insert_links | :delete_columns | :delete_rows | :sort | :use_autofilter
84
+ | :use_pivot_tables | :edit_scenarios | :edit_objects
85
+ def group_rows: (Integer | Range[Integer] rows, ?collapsed: bool) -> self
86
+ def group_columns: (Integer | Range[Integer] columns, ?collapsed: bool) -> self
87
+ def protect: (?password: String?, ?allow: protect_action | Array[protect_action] | nil) -> self
74
88
  def merge_range: (Integer first_row, Integer first_col, Integer last_row, Integer last_col, cell value, ?Format? format) -> self
75
- def conditional_format: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
89
+ | (String range, cell value, ?Format? format) -> self
90
+ | (indexes rows, indexes cols, cell value, ?Format? format) -> self
91
+ def conditional_format: (*cell_range range,
76
92
  type: :cell | :text | :formula | :data_bar | :color_scale,
77
93
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
78
94
  ?format: Format, ?colors: 2 | 3) -> self
79
- def data_validation: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
95
+ def data_validation: (*cell_range range,
80
96
  type: :list | :whole_number | :decimal | :text_length,
81
97
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
82
98
  ?input_title: String, ?input_message: String,
@@ -91,7 +107,7 @@ module FastXlsx
91
107
  ?width: Integer, ?height: Integer) -> self
92
108
  type table_column = String | { header: String, ?total: Symbol, ?total_label: String, ?format: Format }
93
109
 
94
- def add_table: (Integer first_row, Integer first_col, Integer last_row, Integer last_col,
110
+ def add_table: (*cell_range range,
95
111
  ?columns: Array[table_column], ?style: Symbol, ?name: String,
96
112
  ?total_row: bool, ?banded_rows: bool, ?autofilter: bool) -> self
97
113
  def 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.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac