fast_xlsx 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +7 -0
- data/CHANGELOG.md +22 -0
- data/README.md +25 -4
- data/bench/Gemfile.lock +2 -2
- data/ext/fast_xlsx/src/lib.rs +426 -91
- data/ext/fast_xlsx/tests/ruby_suite.rs +3 -3
- data/lib/fast_xlsx/version.rb +2 -1
- data/lib/fast_xlsx.rb +334 -55
- data/sig/fast_xlsx.rbs +13 -1
- metadata +2 -1
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::{
|
|
@@ -34,9 +34,52 @@ 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
36
|
|
|
37
|
+
// A row or column outside the sheet is a RangeError, like a negative one
|
|
38
|
+
// (which fails converting to an unsigned number before reaching the writer).
|
|
37
39
|
fn xerr(e: XlsxError) -> Error {
|
|
38
40
|
let ruby = Ruby::get().unwrap();
|
|
39
|
-
|
|
41
|
+
let class = match e {
|
|
42
|
+
XlsxError::RowColumnLimitError => ruby.exception_range_error(),
|
|
43
|
+
// Values that aren't allowed, like a reversed range.
|
|
44
|
+
XlsxError::RowColumnOrderError | XlsxError::MergeRangeSingleCell => {
|
|
45
|
+
ruby.exception_arg_error()
|
|
46
|
+
}
|
|
47
|
+
_ => ruby.get_inner(&ERROR),
|
|
48
|
+
};
|
|
49
|
+
Error::new(class, e.to_string())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Excel's limits that rust_xlsxwriter doesn't check: a negative width or
|
|
53
|
+
// height hides the column or row, a larger one is capped or invalid.
|
|
54
|
+
// A size that must be a finite number above 0 (NaN or a negative one would
|
|
55
|
+
// be written into the file as is).
|
|
56
|
+
fn check_positive(ruby: &Ruby, what: &str, size: f64) -> Result<f64, Error> {
|
|
57
|
+
if size.is_finite() && size > 0.0 {
|
|
58
|
+
return Ok(size);
|
|
59
|
+
}
|
|
60
|
+
Err(Error::new(
|
|
61
|
+
ruby.exception_arg_error(),
|
|
62
|
+
format!("invalid {what} {size}: use a number above 0"),
|
|
63
|
+
))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A range of rows is checked before any row changes, so it is all or nothing.
|
|
67
|
+
fn check_last_row(last: u32) -> Result<(), Error> {
|
|
68
|
+
if last < MAX_ROWS {
|
|
69
|
+
Ok(())
|
|
70
|
+
} else {
|
|
71
|
+
Err(xerr(XlsxError::RowColumnLimitError))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn check_size(ruby: &Ruby, what: &str, size: f64, max: f64) -> Result<(), Error> {
|
|
76
|
+
if (0.0..=max).contains(&size) {
|
|
77
|
+
return Ok(());
|
|
78
|
+
}
|
|
79
|
+
Err(Error::new(
|
|
80
|
+
ruby.exception_arg_error(),
|
|
81
|
+
format!("invalid {what} {size}: use 0..{max}"),
|
|
82
|
+
))
|
|
40
83
|
}
|
|
41
84
|
|
|
42
85
|
type Shared = Arc<Mutex<rust_xlsxwriter::Workbook>>;
|
|
@@ -113,14 +156,20 @@ struct Worksheet {
|
|
|
113
156
|
table_formats: RefCell<Vec<TableColumnFormat>>,
|
|
114
157
|
// Merged ranges as (first_row, first_col, last_row, last_col).
|
|
115
158
|
merges: RefCell<Vec<(u32, u16, u32, u16)>>,
|
|
116
|
-
//
|
|
117
|
-
formatted_columns: RefCell<Vec<(u16, u16)>>,
|
|
159
|
+
// Formats given with column_format: (first, last, style).
|
|
160
|
+
formatted_columns: RefCell<Vec<(u16, u16, Arc<Style>)>>,
|
|
161
|
+
// Formats given with row_format: (first, last, style).
|
|
162
|
+
formatted_rows: RefCell<Vec<(u32, u32, Arc<Style>)>>,
|
|
163
|
+
// Whether a row got options (format, hidden, outline level):
|
|
164
|
+
// rust_xlsxwriter copies the default row height into a row then, so
|
|
165
|
+
// default_row_height must come first.
|
|
166
|
+
row_options_set: Cell<bool>,
|
|
118
167
|
}
|
|
119
168
|
|
|
120
169
|
struct TableColumnFormat {
|
|
121
170
|
rows: std::ops::RangeInclusive<u32>,
|
|
122
171
|
col: u16,
|
|
123
|
-
format: Arc<
|
|
172
|
+
format: Arc<Style>,
|
|
124
173
|
}
|
|
125
174
|
|
|
126
175
|
impl Workbook {
|
|
@@ -183,6 +232,8 @@ impl Workbook {
|
|
|
183
232
|
table_formats: RefCell::new(Vec::new()),
|
|
184
233
|
merges: RefCell::new(Vec::new()),
|
|
185
234
|
formatted_columns: RefCell::new(Vec::new()),
|
|
235
|
+
formatted_rows: RefCell::new(Vec::new()),
|
|
236
|
+
row_options_set: Cell::new(false),
|
|
186
237
|
})
|
|
187
238
|
}
|
|
188
239
|
|
|
@@ -307,7 +358,7 @@ enum CellValue {
|
|
|
307
358
|
Formula(String),
|
|
308
359
|
Url(String, Option<String>),
|
|
309
360
|
// RichString segments; None means the default font.
|
|
310
|
-
Rich(Vec<(Option<Arc<
|
|
361
|
+
Rich(Vec<(Option<Arc<Style>>, String)>),
|
|
311
362
|
}
|
|
312
363
|
|
|
313
364
|
impl CellValue {
|
|
@@ -348,16 +399,10 @@ impl CellValue {
|
|
|
348
399
|
Ok(value)
|
|
349
400
|
}
|
|
350
401
|
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
fn default_format(&self) -> Option<&'static rust_xlsxwriter::Format> {
|
|
354
|
-
static DATE: LazyLock<rust_xlsxwriter::Format> =
|
|
355
|
-
LazyLock::new(|| rust_xlsxwriter::Format::new().set_num_format("yyyy-mm-dd"));
|
|
356
|
-
static DATE_TIME: LazyLock<rust_xlsxwriter::Format> =
|
|
357
|
-
LazyLock::new(|| rust_xlsxwriter::Format::new().set_num_format("yyyy-mm-dd hh:mm:ss"));
|
|
402
|
+
// Some(has a time of day) for date cells.
|
|
403
|
+
fn date_kind(&self) -> Option<bool> {
|
|
358
404
|
match self {
|
|
359
|
-
CellValue::Date(_,
|
|
360
|
-
CellValue::Date(_, true) => Some(&DATE_TIME),
|
|
405
|
+
CellValue::Date(_, time) => Some(*time),
|
|
361
406
|
_ => None,
|
|
362
407
|
}
|
|
363
408
|
}
|
|
@@ -390,7 +435,11 @@ impl CellValue {
|
|
|
390
435
|
format: Option<&rust_xlsxwriter::Format>,
|
|
391
436
|
) -> Result<(), Error> {
|
|
392
437
|
match self {
|
|
393
|
-
|
|
438
|
+
// nil writes nothing, unless it has a format (e.g. a border).
|
|
439
|
+
CellValue::Empty => match format {
|
|
440
|
+
Some(f) => ws.write_blank(row, col, f).map(|_| ()).map_err(xerr),
|
|
441
|
+
None => Ok(()),
|
|
442
|
+
},
|
|
394
443
|
CellValue::Text(s) => emit(ws, row, col, s.as_str(), format),
|
|
395
444
|
CellValue::Number(n) | CellValue::Date(n, _) => emit(ws, row, col, *n, format),
|
|
396
445
|
CellValue::Bool(b) => emit(ws, row, col, *b, format),
|
|
@@ -412,7 +461,7 @@ impl CellValue {
|
|
|
412
461
|
let default = rust_xlsxwriter::Format::default();
|
|
413
462
|
let rich: Vec<(&rust_xlsxwriter::Format, &str)> = parts
|
|
414
463
|
.iter()
|
|
415
|
-
.map(|(f, text)| (f.as_deref().
|
|
464
|
+
.map(|(f, text)| (f.as_deref().map_or(&default, |s| &s.format), text.as_str()))
|
|
416
465
|
.collect();
|
|
417
466
|
match format {
|
|
418
467
|
Some(f) => ws.write_rich_string_with_format(row, col, &rich, f),
|
|
@@ -583,7 +632,7 @@ fn check_header_footer(ruby: &Ruby, what: &str, text: &str) -> Result<(), Error>
|
|
|
583
632
|
let len = expanded.chars().count();
|
|
584
633
|
if len > 255 {
|
|
585
634
|
return Err(Error::new(
|
|
586
|
-
ruby.
|
|
635
|
+
ruby.get_inner(&ERROR),
|
|
587
636
|
format!("{what} is {len} characters; Excel allows 255"),
|
|
588
637
|
));
|
|
589
638
|
}
|
|
@@ -714,10 +763,7 @@ const TABLE_TOTALS: &[(&str, TableFunction)] = &[
|
|
|
714
763
|
|
|
715
764
|
// A table column: a header String, or { header:, total:, total_label:, format: }.
|
|
716
765
|
// Also returns a copy of the column's format, if any, for later writes.
|
|
717
|
-
fn table_column(
|
|
718
|
-
ruby: &Ruby,
|
|
719
|
-
v: Value,
|
|
720
|
-
) -> Result<(TableColumn, Option<Arc<rust_xlsxwriter::Format>>), Error> {
|
|
766
|
+
fn table_column(ruby: &Ruby, v: Value) -> Result<(TableColumn, Option<Arc<Style>>), Error> {
|
|
721
767
|
let Some(spec) = RHash::from_value(v) else {
|
|
722
768
|
return Ok((TableColumn::new().set_header(String::try_convert(v)?), None));
|
|
723
769
|
};
|
|
@@ -737,7 +783,7 @@ fn table_column(
|
|
|
737
783
|
}
|
|
738
784
|
let format = opt::<&Format>(ruby, spec, "format")?.map(|f| f.0.clone());
|
|
739
785
|
if let Some(f) = &format {
|
|
740
|
-
column = column.set_format(
|
|
786
|
+
column = column.set_format(&f.format);
|
|
741
787
|
}
|
|
742
788
|
Ok((column, format))
|
|
743
789
|
}
|
|
@@ -761,6 +807,29 @@ const PROTECTION_ALLOW: &[(&str, ProtectionFlag)] = &[
|
|
|
761
807
|
("edit_objects", |o| &mut o.edit_objects),
|
|
762
808
|
];
|
|
763
809
|
|
|
810
|
+
// Not IgnoreError::TwoDigitTextYear: rust_xlsxwriter 0.99 writes it as
|
|
811
|
+
// "TwoDigitTextYear", not the schema's "twoDigitTextYear", which Excel would
|
|
812
|
+
// flag as damaged content. Add it once that is fixed upstream.
|
|
813
|
+
const IGNORE_ERRORS: &[(&str, IgnoreError)] = &[
|
|
814
|
+
("number_stored_as_text", IgnoreError::NumberStoredAsText),
|
|
815
|
+
("formula_error", IgnoreError::FormulaError),
|
|
816
|
+
("formula_differs", IgnoreError::FormulaDiffers),
|
|
817
|
+
(
|
|
818
|
+
"formula_refers_to_empty_cells",
|
|
819
|
+
IgnoreError::FormulaRefersToEmptyCells,
|
|
820
|
+
),
|
|
821
|
+
("formula_omits_cells", IgnoreError::FormulaOmitsCells),
|
|
822
|
+
("data_validation_error", IgnoreError::DataValidationError),
|
|
823
|
+
(
|
|
824
|
+
"unlocked_cells_with_formula",
|
|
825
|
+
IgnoreError::UnlockedCellsWithFormula,
|
|
826
|
+
),
|
|
827
|
+
(
|
|
828
|
+
"inconsistent_column_formula",
|
|
829
|
+
IgnoreError::InconsistentColumnFormula,
|
|
830
|
+
),
|
|
831
|
+
];
|
|
832
|
+
|
|
764
833
|
// Excel's paper size codes; page_setup also takes the number itself.
|
|
765
834
|
const PAPER_SIZES: &[(&str, u8)] = &[
|
|
766
835
|
("letter", 1),
|
|
@@ -782,11 +851,52 @@ const BORDERS: &[(&str, FormatBorder)] = &[
|
|
|
782
851
|
];
|
|
783
852
|
|
|
784
853
|
#[magnus::wrap(class = "FastXlsx::Format", free_immediately)]
|
|
785
|
-
struct Format(Arc<
|
|
854
|
+
struct Format(Arc<Style>);
|
|
855
|
+
|
|
856
|
+
// A Format as written: the format itself, and for one without a num_format,
|
|
857
|
+
// copies with the default date formats added, used for date cells (Excel
|
|
858
|
+
// shows a date with no number format as its serial number).
|
|
859
|
+
struct Style {
|
|
860
|
+
format: rust_xlsxwriter::Format,
|
|
861
|
+
date: Option<rust_xlsxwriter::Format>,
|
|
862
|
+
date_time: Option<rust_xlsxwriter::Format>,
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const DATE_FORMAT: &str = "yyyy-mm-dd";
|
|
866
|
+
const DATE_TIME_FORMAT: &str = "yyyy-mm-dd hh:mm:ss";
|
|
867
|
+
|
|
868
|
+
impl Style {
|
|
869
|
+
fn new(format: rust_xlsxwriter::Format, has_num_format: bool) -> Self {
|
|
870
|
+
let with =
|
|
871
|
+
|num_format: &str| (!has_num_format).then(|| format.clone().set_num_format(num_format));
|
|
872
|
+
Style {
|
|
873
|
+
date: with(DATE_FORMAT),
|
|
874
|
+
date_time: with(DATE_TIME_FORMAT),
|
|
875
|
+
format,
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// The date variant for a date cell (`time`: it has a time of day), if
|
|
880
|
+
// this format has no num_format of its own.
|
|
881
|
+
fn for_date(&self, time: bool) -> Option<&rust_xlsxwriter::Format> {
|
|
882
|
+
if time {
|
|
883
|
+
self.date_time.as_ref()
|
|
884
|
+
} else {
|
|
885
|
+
self.date.as_ref()
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// What a value is written with: `date` is Some(has time) for date cells.
|
|
890
|
+
fn pick(&self, date: Option<bool>) -> &rust_xlsxwriter::Format {
|
|
891
|
+
date.and_then(|time| self.for_date(time))
|
|
892
|
+
.unwrap_or(&self.format)
|
|
893
|
+
}
|
|
894
|
+
}
|
|
786
895
|
|
|
787
896
|
impl Format {
|
|
788
897
|
fn new(ruby: &Ruby, options: RHash) -> Result<Self, Error> {
|
|
789
898
|
let mut f = rust_xlsxwriter::Format::new();
|
|
899
|
+
let mut has_num_format = false;
|
|
790
900
|
options.foreach(|key: Symbol, value: Value| {
|
|
791
901
|
let taken = std::mem::take(&mut f);
|
|
792
902
|
f = match &*key.name()? {
|
|
@@ -834,8 +944,15 @@ impl Format {
|
|
|
834
944
|
"locked" => taken.set_unlocked(),
|
|
835
945
|
"hidden" if value.to_bool() => taken.set_hidden(),
|
|
836
946
|
"border_color" => taken.set_border_color(color(ruby, value)?),
|
|
837
|
-
"num_format" =>
|
|
838
|
-
|
|
947
|
+
"num_format" => {
|
|
948
|
+
has_num_format = true;
|
|
949
|
+
taken.set_num_format(String::try_convert(value)?)
|
|
950
|
+
}
|
|
951
|
+
"font_size" => taken.set_font_size(check_positive(
|
|
952
|
+
ruby,
|
|
953
|
+
"font_size",
|
|
954
|
+
f64::try_convert(value)?,
|
|
955
|
+
)?),
|
|
839
956
|
"font_name" => taken.set_font_name(String::try_convert(value)?),
|
|
840
957
|
"font_color" => taken.set_font_color(color(ruby, value)?),
|
|
841
958
|
"bg_color" => taken.set_background_color(color(ruby, value)?),
|
|
@@ -876,7 +993,7 @@ impl Format {
|
|
|
876
993
|
};
|
|
877
994
|
Ok(ForEach::Continue)
|
|
878
995
|
})?;
|
|
879
|
-
Ok(Format(Arc::new(f)))
|
|
996
|
+
Ok(Format(Arc::new(Style::new(f, has_num_format))))
|
|
880
997
|
}
|
|
881
998
|
}
|
|
882
999
|
|
|
@@ -899,7 +1016,7 @@ fn each_entry(
|
|
|
899
1016
|
|
|
900
1017
|
// A row's format: one Format (or nil) for every cell, or an Array with one per cell.
|
|
901
1018
|
enum RowFormat {
|
|
902
|
-
Same(Option<Arc<
|
|
1019
|
+
Same(Option<Arc<Style>>),
|
|
903
1020
|
PerCell(RArray),
|
|
904
1021
|
}
|
|
905
1022
|
|
|
@@ -913,7 +1030,7 @@ impl RowFormat {
|
|
|
913
1030
|
}
|
|
914
1031
|
}
|
|
915
1032
|
|
|
916
|
-
fn at(&self, col: usize) -> Result<Option<Arc<
|
|
1033
|
+
fn at(&self, col: usize) -> Result<Option<Arc<Style>>, Error> {
|
|
917
1034
|
match self {
|
|
918
1035
|
RowFormat::Same(f) => Ok(f.clone()),
|
|
919
1036
|
RowFormat::PerCell(formats) => Ok(Option::<&Format>::try_convert(
|
|
@@ -955,42 +1072,52 @@ impl Worksheet {
|
|
|
955
1072
|
f(wb.worksheet_from_index(self.index).map_err(xerr)?)
|
|
956
1073
|
}
|
|
957
1074
|
|
|
958
|
-
// The format
|
|
959
|
-
|
|
1075
|
+
// The format a value is written with: its own, else its table column's.
|
|
1076
|
+
// A date cell takes that format's date variant (when it has no
|
|
1077
|
+
// num_format); with neither, its row_format's, else its column_format's
|
|
1078
|
+
// date variant (rust_xlsxwriter's order), else the default date format.
|
|
1079
|
+
// Other cells without one get None, so rust_xlsxwriter applies any row or
|
|
1080
|
+
// column format itself.
|
|
1081
|
+
fn format_for<'a>(
|
|
960
1082
|
tables: &'a [TableColumnFormat],
|
|
1083
|
+
rows: &'a [(u32, u32, Arc<Style>)],
|
|
1084
|
+
columns: &'a [(u16, u16, Arc<Style>)],
|
|
961
1085
|
row: u32,
|
|
962
1086
|
col: u16,
|
|
963
|
-
own: Option<&'a
|
|
1087
|
+
own: Option<&'a Style>,
|
|
1088
|
+
value: &CellValue,
|
|
964
1089
|
) -> Option<&'a rust_xlsxwriter::Format> {
|
|
965
|
-
|
|
1090
|
+
static DEFAULTS: LazyLock<Style> =
|
|
1091
|
+
LazyLock::new(|| Style::new(rust_xlsxwriter::Format::new(), false));
|
|
1092
|
+
let date = value.date_kind();
|
|
1093
|
+
let style = own.or_else(|| {
|
|
966
1094
|
tables
|
|
967
1095
|
.iter()
|
|
968
1096
|
.rev()
|
|
969
1097
|
.find(|t| t.col == col && t.rows.contains(&row))
|
|
970
1098
|
.map(|t| &*t.format)
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
})
|
|
1099
|
+
});
|
|
1100
|
+
if let Some(style) = style {
|
|
1101
|
+
return Some(style.pick(date));
|
|
1102
|
+
}
|
|
1103
|
+
let time = date?;
|
|
1104
|
+
let row_style = rows
|
|
1105
|
+
.iter()
|
|
1106
|
+
.rev()
|
|
1107
|
+
.find(|(first, last, _)| (*first..=*last).contains(&row))
|
|
1108
|
+
.map(|(_, _, style)| style);
|
|
1109
|
+
let column_style = || {
|
|
1110
|
+
columns
|
|
1111
|
+
.iter()
|
|
1112
|
+
.rev()
|
|
1113
|
+
.find(|(first, last, _)| (*first..=*last).contains(&col))
|
|
1114
|
+
.map(|(_, _, style)| style)
|
|
1115
|
+
};
|
|
1116
|
+
match row_style.or_else(column_style) {
|
|
1117
|
+
// None when it has a num_format: rust_xlsxwriter applies it.
|
|
1118
|
+
Some(style) => style.for_date(time),
|
|
1119
|
+
None => DEFAULTS.for_date(time),
|
|
1120
|
+
}
|
|
994
1121
|
}
|
|
995
1122
|
|
|
996
1123
|
fn write_row(
|
|
@@ -1006,7 +1133,7 @@ impl Worksheet {
|
|
|
1006
1133
|
each_entry(cells, |i, v| {
|
|
1007
1134
|
if i >= MAX_COLS {
|
|
1008
1135
|
return Err(Error::new(
|
|
1009
|
-
ruby.
|
|
1136
|
+
ruby.exception_range_error(),
|
|
1010
1137
|
format!("too many columns: Excel allows {MAX_COLS}"),
|
|
1011
1138
|
));
|
|
1012
1139
|
}
|
|
@@ -1015,11 +1142,21 @@ impl Worksheet {
|
|
|
1015
1142
|
Ok(())
|
|
1016
1143
|
})?;
|
|
1017
1144
|
let tables = self.table_formats.borrow();
|
|
1145
|
+
let rows = self.formatted_rows.borrow();
|
|
1146
|
+
let columns = self.formatted_columns.borrow();
|
|
1018
1147
|
self.with_ws(|ws| {
|
|
1019
1148
|
// Values were checked when converted, so only a bad row number
|
|
1020
1149
|
// fails here, and it fails on the first cell.
|
|
1021
1150
|
for (col, value, format) in &values {
|
|
1022
|
-
let format =
|
|
1151
|
+
let format = Self::format_for(
|
|
1152
|
+
&tables,
|
|
1153
|
+
&rows,
|
|
1154
|
+
&columns,
|
|
1155
|
+
row,
|
|
1156
|
+
*col,
|
|
1157
|
+
format.as_deref(),
|
|
1158
|
+
value,
|
|
1159
|
+
);
|
|
1023
1160
|
value.write(ws, row, *col, format)?;
|
|
1024
1161
|
}
|
|
1025
1162
|
Ok(())
|
|
@@ -1063,12 +1200,22 @@ impl Worksheet {
|
|
|
1063
1200
|
rb_self.check_not_flushed(ruby, row)?;
|
|
1064
1201
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1065
1202
|
let tables = rb_self.table_formats.borrow();
|
|
1203
|
+
let rows = rb_self.formatted_rows.borrow();
|
|
1204
|
+
let columns = rb_self.formatted_columns.borrow();
|
|
1066
1205
|
rb_self.with_ws(|ws| {
|
|
1067
1206
|
value.write(
|
|
1068
1207
|
ws,
|
|
1069
1208
|
row,
|
|
1070
1209
|
col,
|
|
1071
|
-
|
|
1210
|
+
Self::format_for(
|
|
1211
|
+
&tables,
|
|
1212
|
+
&rows,
|
|
1213
|
+
&columns,
|
|
1214
|
+
row,
|
|
1215
|
+
col,
|
|
1216
|
+
format.map(|f| &*f.0),
|
|
1217
|
+
&value,
|
|
1218
|
+
),
|
|
1072
1219
|
)
|
|
1073
1220
|
})?;
|
|
1074
1221
|
rb_self.advance(row);
|
|
@@ -1100,21 +1247,167 @@ impl Worksheet {
|
|
|
1100
1247
|
Ok(rb_self)
|
|
1101
1248
|
}
|
|
1102
1249
|
|
|
1103
|
-
fn set_column_width(
|
|
1104
|
-
|
|
1250
|
+
fn set_column_width(
|
|
1251
|
+
ruby: &Ruby,
|
|
1252
|
+
rb_self: &Self,
|
|
1253
|
+
first: u16,
|
|
1254
|
+
last: u16,
|
|
1255
|
+
width: f64,
|
|
1256
|
+
) -> Result<(), Error> {
|
|
1257
|
+
check_size(ruby, "column width", width, 255.0)?;
|
|
1258
|
+
rb_self.with_ws(|ws| {
|
|
1105
1259
|
ws.set_column_range_width(first, last, width)
|
|
1106
1260
|
.map(|_| ())
|
|
1107
1261
|
.map_err(xerr)
|
|
1108
1262
|
})
|
|
1109
1263
|
}
|
|
1110
1264
|
|
|
1265
|
+
// Row options are written with the row, so rows on disk ignore them.
|
|
1266
|
+
fn hide_rows(
|
|
1267
|
+
ruby: &Ruby,
|
|
1268
|
+
rb_self: Obj<Self>,
|
|
1269
|
+
first: u32,
|
|
1270
|
+
last: u32,
|
|
1271
|
+
) -> Result<Obj<Self>, Error> {
|
|
1272
|
+
rb_self.check_not_flushed(ruby, first)?;
|
|
1273
|
+
check_last_row(last)?;
|
|
1274
|
+
rb_self.with_ws(|ws| {
|
|
1275
|
+
for row in first..=last {
|
|
1276
|
+
ws.set_row_hidden(row).map_err(xerr)?;
|
|
1277
|
+
}
|
|
1278
|
+
Ok(())
|
|
1279
|
+
})?;
|
|
1280
|
+
rb_self.row_options_set.set(true);
|
|
1281
|
+
Ok(rb_self)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
fn selection(
|
|
1285
|
+
rb_self: Obj<Self>,
|
|
1286
|
+
fr: u32,
|
|
1287
|
+
fc: u16,
|
|
1288
|
+
lr: u32,
|
|
1289
|
+
lc: u16,
|
|
1290
|
+
) -> Result<Obj<Self>, Error> {
|
|
1291
|
+
rb_self.with_ws(|ws| ws.set_selection(fr, fc, lr, lc).map(|_| ()).map_err(xerr))?;
|
|
1292
|
+
Ok(rb_self)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
fn top_left_cell(rb_self: Obj<Self>, row: u32, col: u16) -> Result<Obj<Self>, Error> {
|
|
1296
|
+
rb_self.with_ws(|ws| ws.set_top_left_cell(row, col).map(|_| ()).map_err(xerr))?;
|
|
1297
|
+
Ok(rb_self)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
#[allow(clippy::too_many_arguments)]
|
|
1301
|
+
fn ignore_error(
|
|
1302
|
+
ruby: &Ruby,
|
|
1303
|
+
rb_self: Obj<Self>,
|
|
1304
|
+
fr: u32,
|
|
1305
|
+
fc: u16,
|
|
1306
|
+
lr: u32,
|
|
1307
|
+
lc: u16,
|
|
1308
|
+
kind: Value,
|
|
1309
|
+
) -> Result<Obj<Self>, Error> {
|
|
1310
|
+
let error = choice(ruby, "error to ignore", kind, IGNORE_ERRORS)?;
|
|
1311
|
+
rb_self.with_ws(|ws| {
|
|
1312
|
+
ws.ignore_error_range(fr, fc, lr, lc, error)
|
|
1313
|
+
.map(|_| ())
|
|
1314
|
+
.map_err(xerr)
|
|
1315
|
+
})?;
|
|
1316
|
+
Ok(rb_self)
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
#[allow(clippy::too_many_arguments)]
|
|
1320
|
+
fn unprotect_range(
|
|
1321
|
+
rb_self: Obj<Self>,
|
|
1322
|
+
fr: u32,
|
|
1323
|
+
fc: u16,
|
|
1324
|
+
lr: u32,
|
|
1325
|
+
lc: u16,
|
|
1326
|
+
name: Option<String>,
|
|
1327
|
+
password: Option<String>,
|
|
1328
|
+
) -> Result<Obj<Self>, Error> {
|
|
1329
|
+
rb_self.with_ws(|ws| {
|
|
1330
|
+
ws.unprotect_range_with_options(
|
|
1331
|
+
fr,
|
|
1332
|
+
fc,
|
|
1333
|
+
lr,
|
|
1334
|
+
lc,
|
|
1335
|
+
name.as_deref().unwrap_or(""),
|
|
1336
|
+
password.as_deref().unwrap_or(""),
|
|
1337
|
+
)
|
|
1338
|
+
.map(|_| ())
|
|
1339
|
+
.map_err(xerr)
|
|
1340
|
+
})?;
|
|
1341
|
+
Ok(rb_self)
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
fn hide_columns(rb_self: Obj<Self>, first: u16, last: u16) -> Result<Obj<Self>, Error> {
|
|
1345
|
+
rb_self.with_ws(|ws| {
|
|
1346
|
+
ws.set_column_range_hidden(first, last)
|
|
1347
|
+
.map(|_| ())
|
|
1348
|
+
.map_err(xerr)
|
|
1349
|
+
})?;
|
|
1350
|
+
Ok(rb_self)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
fn set_row_format(
|
|
1354
|
+
ruby: &Ruby,
|
|
1355
|
+
rb_self: &Self,
|
|
1356
|
+
first: u32,
|
|
1357
|
+
last: u32,
|
|
1358
|
+
format: &Format,
|
|
1359
|
+
) -> Result<(), Error> {
|
|
1360
|
+
rb_self.check_not_flushed(ruby, first)?;
|
|
1361
|
+
check_last_row(last)?;
|
|
1362
|
+
rb_self.with_ws(|ws| {
|
|
1363
|
+
for row in first..=last {
|
|
1364
|
+
ws.set_row_format(row, &format.0.format).map_err(xerr)?;
|
|
1365
|
+
}
|
|
1366
|
+
Ok(())
|
|
1367
|
+
})?;
|
|
1368
|
+
rb_self.row_options_set.set(true);
|
|
1369
|
+
rb_self
|
|
1370
|
+
.formatted_rows
|
|
1371
|
+
.borrow_mut()
|
|
1372
|
+
.push((first, last, format.0.clone()));
|
|
1373
|
+
Ok(())
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
fn default_row_height(
|
|
1377
|
+
ruby: &Ruby,
|
|
1378
|
+
rb_self: Obj<Self>,
|
|
1379
|
+
height: f64,
|
|
1380
|
+
) -> Result<Obj<Self>, Error> {
|
|
1381
|
+
// rust_xlsxwriter ignores 0.
|
|
1382
|
+
if height == 0.0 {
|
|
1383
|
+
return Err(Error::new(
|
|
1384
|
+
ruby.exception_arg_error(),
|
|
1385
|
+
"invalid default row height 0: use a number above 0",
|
|
1386
|
+
));
|
|
1387
|
+
}
|
|
1388
|
+
check_size(ruby, "row height", height, 409.0)?;
|
|
1389
|
+
if rb_self.row_options_set.get() {
|
|
1390
|
+
return Err(Error::new(
|
|
1391
|
+
ruby.get_inner(&ERROR),
|
|
1392
|
+
"call default_row_height before row_format, hide_rows, group_rows or row_height(row, 0): rows given those keep the earlier default",
|
|
1393
|
+
));
|
|
1394
|
+
}
|
|
1395
|
+
rb_self.with_ws(|ws| {
|
|
1396
|
+
ws.set_default_row_height(height);
|
|
1397
|
+
Ok(())
|
|
1398
|
+
})?;
|
|
1399
|
+
Ok(rb_self)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1111
1402
|
fn set_column_format(&self, first: u16, last: u16, format: &Format) -> Result<(), Error> {
|
|
1112
1403
|
self.with_ws(|ws| {
|
|
1113
|
-
ws.set_column_range_format(first, last, &format.0)
|
|
1404
|
+
ws.set_column_range_format(first, last, &format.0.format)
|
|
1114
1405
|
.map(|_| ())
|
|
1115
1406
|
.map_err(xerr)
|
|
1116
1407
|
})?;
|
|
1117
|
-
self.formatted_columns
|
|
1408
|
+
self.formatted_columns
|
|
1409
|
+
.borrow_mut()
|
|
1410
|
+
.push((first, last, format.0.clone()));
|
|
1118
1411
|
Ok(())
|
|
1119
1412
|
}
|
|
1120
1413
|
|
|
@@ -1146,8 +1439,18 @@ impl Worksheet {
|
|
|
1146
1439
|
Ok(rb_self)
|
|
1147
1440
|
}
|
|
1148
1441
|
|
|
1149
|
-
fn set_row_height(
|
|
1442
|
+
fn set_row_height(
|
|
1443
|
+
ruby: &Ruby,
|
|
1444
|
+
rb_self: Obj<Self>,
|
|
1445
|
+
row: u32,
|
|
1446
|
+
height: f64,
|
|
1447
|
+
) -> Result<Obj<Self>, Error> {
|
|
1448
|
+
check_size(ruby, "row height", height, 409.0)?;
|
|
1150
1449
|
rb_self.with_ws(|ws| ws.set_row_height(row, height).map(|_| ()).map_err(xerr))?;
|
|
1450
|
+
// A height is kept as given; only 0 (hiding the row) copies the default.
|
|
1451
|
+
if height == 0.0 {
|
|
1452
|
+
rb_self.row_options_set.set(true);
|
|
1453
|
+
}
|
|
1151
1454
|
Ok(rb_self)
|
|
1152
1455
|
}
|
|
1153
1456
|
|
|
@@ -1167,17 +1470,32 @@ impl Worksheet {
|
|
|
1167
1470
|
})
|
|
1168
1471
|
}
|
|
1169
1472
|
|
|
1170
|
-
// Inches;
|
|
1473
|
+
// Inches; None keeps the current margin.
|
|
1474
|
+
#[allow(clippy::too_many_arguments)]
|
|
1171
1475
|
fn set_margins(
|
|
1172
|
-
&
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1476
|
+
ruby: &Ruby,
|
|
1477
|
+
rb_self: &Self,
|
|
1478
|
+
left: Option<f64>,
|
|
1479
|
+
right: Option<f64>,
|
|
1480
|
+
top: Option<f64>,
|
|
1481
|
+
bottom: Option<f64>,
|
|
1482
|
+
header: Option<f64>,
|
|
1483
|
+
footer: Option<f64>,
|
|
1179
1484
|
) -> Result<(), Error> {
|
|
1180
|
-
|
|
1485
|
+
// rust_xlsxwriter keeps a margin given as a negative number.
|
|
1486
|
+
let inches = |margin: Option<f64>| -> Result<f64, Error> {
|
|
1487
|
+
match margin {
|
|
1488
|
+
None => Ok(-1.0),
|
|
1489
|
+
Some(m) if m.is_finite() && m >= 0.0 => Ok(m),
|
|
1490
|
+
Some(m) => Err(Error::new(
|
|
1491
|
+
ruby.exception_arg_error(),
|
|
1492
|
+
format!("invalid margin {m}: use 0 or more inches"),
|
|
1493
|
+
)),
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
let (left, right, top) = (inches(left)?, inches(right)?, inches(top)?);
|
|
1497
|
+
let (bottom, header, footer) = (inches(bottom)?, inches(header)?, inches(footer)?);
|
|
1498
|
+
rb_self.with_ws(|ws| {
|
|
1181
1499
|
ws.set_margins(left, right, top, bottom, header, footer);
|
|
1182
1500
|
Ok(())
|
|
1183
1501
|
})
|
|
@@ -1238,8 +1556,17 @@ impl Worksheet {
|
|
|
1238
1556
|
}
|
|
1239
1557
|
let value = CellValue::from_ruby(ruby, v)?;
|
|
1240
1558
|
let tables = rb_self.table_formats.borrow();
|
|
1241
|
-
let
|
|
1242
|
-
|
|
1559
|
+
let rows = rb_self.formatted_rows.borrow();
|
|
1560
|
+
let columns = rb_self.formatted_columns.borrow();
|
|
1561
|
+
let format = Self::format_for(
|
|
1562
|
+
&tables,
|
|
1563
|
+
&rows,
|
|
1564
|
+
&columns,
|
|
1565
|
+
first_row,
|
|
1566
|
+
first_col,
|
|
1567
|
+
format.map(|f| &*f.0),
|
|
1568
|
+
&value,
|
|
1569
|
+
);
|
|
1243
1570
|
let default = rust_xlsxwriter::Format::new();
|
|
1244
1571
|
rb_self.with_ws(|ws| {
|
|
1245
1572
|
// The value was checked when converted, so once the range is
|
|
@@ -1306,7 +1633,7 @@ impl Worksheet {
|
|
|
1306
1633
|
let mut cf =
|
|
1307
1634
|
ConditionalFormatCell::new().set_rule(cell_rule(ruby, criteria, value)?);
|
|
1308
1635
|
if let Some(f) = format {
|
|
1309
|
-
cf = cf.set_format(
|
|
1636
|
+
cf = cf.set_format(&f.0.format);
|
|
1310
1637
|
}
|
|
1311
1638
|
rule(range, cf)
|
|
1312
1639
|
}
|
|
@@ -1330,7 +1657,7 @@ impl Worksheet {
|
|
|
1330
1657
|
};
|
|
1331
1658
|
let mut cf = ConditionalFormatText::new().set_rule(text_rule);
|
|
1332
1659
|
if let Some(f) = format {
|
|
1333
|
-
cf = cf.set_format(
|
|
1660
|
+
cf = cf.set_format(&f.0.format);
|
|
1334
1661
|
}
|
|
1335
1662
|
rule(range, cf)
|
|
1336
1663
|
}
|
|
@@ -1338,7 +1665,7 @@ impl Worksheet {
|
|
|
1338
1665
|
let mut cf =
|
|
1339
1666
|
ConditionalFormatFormula::new().set_rule(String::try_convert(value)?.as_str());
|
|
1340
1667
|
if let Some(f) = format {
|
|
1341
|
-
cf = cf.set_format(
|
|
1668
|
+
cf = cf.set_format(&f.0.format);
|
|
1342
1669
|
}
|
|
1343
1670
|
rule(range, cf)
|
|
1344
1671
|
}
|
|
@@ -1469,9 +1796,14 @@ impl Worksheet {
|
|
|
1469
1796
|
)?;
|
|
1470
1797
|
// SAFETY: the bytes are copied into the Image before any Ruby code runs.
|
|
1471
1798
|
let mut image = Image::new_from_buffer(unsafe { bytes.as_slice() }).map_err(xerr)?;
|
|
1472
|
-
let
|
|
1473
|
-
|
|
1474
|
-
|
|
1799
|
+
let positive = |name: &str| -> Result<Option<f64>, Error> {
|
|
1800
|
+
opt::<f64>(ruby, options, name)?
|
|
1801
|
+
.map(|v| check_positive(ruby, name, v))
|
|
1802
|
+
.transpose()
|
|
1803
|
+
};
|
|
1804
|
+
let width = positive("width")?;
|
|
1805
|
+
let height = positive("height")?;
|
|
1806
|
+
if let Some(scale) = positive("scale")? {
|
|
1475
1807
|
if width.is_some() || height.is_some() {
|
|
1476
1808
|
return Err(Error::new(
|
|
1477
1809
|
ruby.exception_arg_error(),
|
|
@@ -1680,16 +2012,7 @@ impl Worksheet {
|
|
|
1680
2012
|
Ok(rb_self)
|
|
1681
2013
|
}
|
|
1682
2014
|
|
|
1683
|
-
fn zoom(ruby: &Ruby, rb_self: Obj<Self>, percent:
|
|
1684
|
-
// Integers only: converting would quietly truncate 150.9.
|
|
1685
|
-
let percent = Integer::from_value(percent)
|
|
1686
|
-
.ok_or_else(|| {
|
|
1687
|
-
Error::new(
|
|
1688
|
-
ruby.exception_type_error(),
|
|
1689
|
-
format!("zoom must be an Integer, got {}", percent.inspect()),
|
|
1690
|
-
)
|
|
1691
|
-
})?
|
|
1692
|
-
.to_i64()?;
|
|
2015
|
+
fn zoom(ruby: &Ruby, rb_self: Obj<Self>, percent: i64) -> Result<Obj<Self>, Error> {
|
|
1693
2016
|
// rust_xlsxwriter only prints a warning for these.
|
|
1694
2017
|
if !(10..=400).contains(&percent) {
|
|
1695
2018
|
return Err(Error::new(
|
|
@@ -1813,6 +2136,7 @@ impl Worksheet {
|
|
|
1813
2136
|
.map(|_| ())
|
|
1814
2137
|
.map_err(xerr)
|
|
1815
2138
|
})?;
|
|
2139
|
+
rb_self.row_options_set.set(true);
|
|
1816
2140
|
Ok(rb_self)
|
|
1817
2141
|
}
|
|
1818
2142
|
|
|
@@ -1903,6 +2227,17 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1903
2227
|
"vertical_page_breaks",
|
|
1904
2228
|
method!(Worksheet::set_vertical_page_breaks, 1),
|
|
1905
2229
|
)?;
|
|
2230
|
+
ws.define_method("_hide_rows", method!(Worksheet::hide_rows, 2))?;
|
|
2231
|
+
ws.define_method("_hide_columns", method!(Worksheet::hide_columns, 2))?;
|
|
2232
|
+
ws.define_method("_row_format", method!(Worksheet::set_row_format, 3))?;
|
|
2233
|
+
ws.define_method(
|
|
2234
|
+
"default_row_height",
|
|
2235
|
+
method!(Worksheet::default_row_height, 1),
|
|
2236
|
+
)?;
|
|
2237
|
+
ws.define_method("_selection", method!(Worksheet::selection, 4))?;
|
|
2238
|
+
ws.define_method("_top_left_cell", method!(Worksheet::top_left_cell, 2))?;
|
|
2239
|
+
ws.define_method("_ignore_error", method!(Worksheet::ignore_error, 5))?;
|
|
2240
|
+
ws.define_method("_unprotect_range", method!(Worksheet::unprotect_range, 6))?;
|
|
1906
2241
|
ws.define_method("activate", method!(Worksheet::activate, 0))?;
|
|
1907
2242
|
ws.define_method("hide", method!(Worksheet::hide, 0))?;
|
|
1908
2243
|
ws.define_method("zoom", method!(Worksheet::zoom, 1))?;
|