imprimatur 0.1.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.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/Cargo.lock +835 -0
  4. data/Cargo.toml +44 -0
  5. data/LICENSE.txt +29 -0
  6. data/README.md +314 -0
  7. data/ext/imprimatur_native/cli/Cargo.toml +30 -0
  8. data/ext/imprimatur_native/cli/src/app.rs +183 -0
  9. data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
  10. data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
  11. data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
  12. data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
  13. data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
  14. data/ext/imprimatur_native/cli/src/cli.rs +71 -0
  15. data/ext/imprimatur_native/cli/src/main.rs +15 -0
  16. data/ext/imprimatur_native/core/Cargo.toml +33 -0
  17. data/ext/imprimatur_native/core/LICENSE +29 -0
  18. data/ext/imprimatur_native/core/README.md +105 -0
  19. data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
  20. data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
  21. data/ext/imprimatur_native/core/src/doc.rs +210 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +343 -0
  23. data/ext/imprimatur_native/core/src/error.rs +113 -0
  24. data/ext/imprimatur_native/core/src/lib.rs +38 -0
  25. data/ext/imprimatur_native/core/src/opc.rs +51 -0
  26. data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
  27. data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
  28. data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
  29. data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
  30. data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
  31. data/ext/imprimatur_native/core/src/pdf.rs +84 -0
  32. data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
  33. data/ext/imprimatur_native/core/src/read/content.rs +266 -0
  34. data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
  35. data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
  36. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
  37. data/ext/imprimatur_native/core/src/read/image.rs +233 -0
  38. data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
  39. data/ext/imprimatur_native/core/src/read/json.rs +172 -0
  40. data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
  41. data/ext/imprimatur_native/core/src/read/package.rs +191 -0
  42. data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
  43. data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
  44. data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
  45. data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
  46. data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
  47. data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
  48. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
  49. data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
  50. data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
  51. data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
  52. data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
  53. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
  54. data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
  55. data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
  56. data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
  57. data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
  58. data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
  59. data/ext/imprimatur_native/core/src/read/span.rs +22 -0
  60. data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
  61. data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
  62. data/ext/imprimatur_native/core/src/read.rs +145 -0
  63. data/ext/imprimatur_native/core/src/style.rs +82 -0
  64. data/ext/imprimatur_native/core/src/test_support.rs +163 -0
  65. data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
  66. data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
  67. data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
  68. data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
  69. data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
  70. data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
  71. data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
  72. data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
  73. data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
  74. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
  75. data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
  76. data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
  77. data/ext/imprimatur_native/core/src/xml.rs +95 -0
  78. data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
  79. data/ext/imprimatur_native/core/src/zip.rs +161 -0
  80. data/ext/imprimatur_native/extconf.rb +45 -0
  81. data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
  82. data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
  83. data/ext/imprimatur_native/ffi/src/document.rs +138 -0
  84. data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
  85. data/ext/imprimatur_native/ffi/src/read.rs +182 -0
  86. data/ext/imprimatur_native/ffi/src/style.rs +85 -0
  87. data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
  88. data/lib/imprimatur/document.rb +127 -0
  89. data/lib/imprimatur/error.rb +22 -0
  90. data/lib/imprimatur/formula.rb +13 -0
  91. data/lib/imprimatur/native.rb +17 -0
  92. data/lib/imprimatur/reader.rb +59 -0
  93. data/lib/imprimatur/style.rb +31 -0
  94. data/lib/imprimatur/validation.rb +35 -0
  95. data/lib/imprimatur/version.rb +5 -0
  96. data/lib/imprimatur/xlsx.rb +183 -0
  97. data/lib/imprimatur.rb +111 -0
  98. metadata +157 -0
@@ -0,0 +1,258 @@
1
+ //! Data validation: drop-down lists and formula rules on ranges of cells
2
+ //! (ECMA-376 Part 1, 18.3.1.32).
3
+
4
+ #[cfg(test)]
5
+ mod tests;
6
+
7
+ use super::range::CellRange;
8
+ use super::text::escape;
9
+ use crate::xml::XmlWriter;
10
+ use crate::{Error, Result};
11
+
12
+ /// Excel's limit on a list typed into the rule, commas included.
13
+ const MAX_LIST: usize = 255;
14
+ /// Excel's limit on a formula.
15
+ const MAX_FORMULA: usize = 8_192;
16
+ /// Excel's limits on the titles and on the messages of prompts and alerts.
17
+ const MAX_TITLE: usize = 32;
18
+ const MAX_MESSAGE: usize = 255;
19
+
20
+ /// What happens when someone types a value the rule rejects.
21
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
22
+ pub enum Alert {
23
+ /// The value is refused.
24
+ #[default]
25
+ Stop,
26
+ /// The value is kept if the user confirms it.
27
+ Warning,
28
+ /// The value is kept; the message only informs.
29
+ Information,
30
+ /// The value is kept silently: lists still offer their drop-down.
31
+ Off,
32
+ }
33
+
34
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35
+ enum Kind {
36
+ List,
37
+ Custom,
38
+ }
39
+
40
+ /// A rule for the cells of a range, added with
41
+ /// [`Worksheet::add_validation`](super::Worksheet::add_validation):
42
+ ///
43
+ /// ```
44
+ /// use imprimatur::xlsx::{Validation, Workbook};
45
+ ///
46
+ /// let mut book = Workbook::new();
47
+ /// let sheet = book.add_sheet("Orders")?;
48
+ /// let yes_no = Validation::list(["Yes", "No"])?.error("Invalid", "Pick Yes or No.")?;
49
+ /// sheet.add_validation("C2:C100".parse()?, yes_no)?;
50
+ /// sheet.add_validation("D2:D100".parse()?, Validation::list_from("Units!$A$2:$A$20")?)?;
51
+ /// # Ok::<(), imprimatur::Error>(())
52
+ /// ```
53
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
54
+ pub struct Validation {
55
+ kind: Kind,
56
+ formula: String,
57
+ allow_blank: bool,
58
+ dropdown: bool,
59
+ alert: Alert,
60
+ prompt_title: Option<String>,
61
+ prompt: Option<String>,
62
+ error_title: Option<String>,
63
+ error: Option<String>,
64
+ }
65
+
66
+ impl Validation {
67
+ const fn new(kind: Kind, formula: String) -> Self {
68
+ Self {
69
+ kind,
70
+ formula,
71
+ allow_blank: true,
72
+ dropdown: true,
73
+ alert: Alert::Stop,
74
+ prompt_title: None,
75
+ prompt: None,
76
+ error_title: None,
77
+ error: None,
78
+ }
79
+ }
80
+
81
+ /// A drop-down of these values. Excel keeps such a list in the rule
82
+ /// itself, so it is short: 255 characters with the commas between values.
83
+ ///
84
+ /// # Errors
85
+ /// [`Error::InvalidValidation`] for no values, a value with a comma (the
86
+ /// separator) or a control character, or a list over 255 characters.
87
+ pub fn list<S: AsRef<str>>(values: impl IntoIterator<Item = S>) -> Result<Self> {
88
+ let mut joined = String::new();
89
+ for (index, value) in values.into_iter().enumerate() {
90
+ let value = value.as_ref();
91
+ if value.contains(',') || value.chars().any(char::is_control) {
92
+ return Err(invalid(format!(
93
+ "list value {value:?} has a comma or control character"
94
+ )));
95
+ }
96
+ if index > 0 {
97
+ joined.push(',');
98
+ }
99
+ joined.push_str(value);
100
+ }
101
+ if joined.is_empty() {
102
+ return Err(invalid("a list needs at least one value".to_owned()));
103
+ }
104
+ let length = joined.chars().count();
105
+ (length <= MAX_LIST).ok_or_else(|| {
106
+ invalid(format!("the list is {length} characters; Excel takes {MAX_LIST} at most"))
107
+ })?;
108
+ // A formula string literal: quoted, with its quotes doubled.
109
+ Ok(Self::new(Kind::List, format!("\"{}\"", joined.replace('"', "\"\""))))
110
+ }
111
+
112
+ /// A drop-down of the values in a range, such as `Units!$A$2:$A$20`, or
113
+ /// of what a formula returns. A leading `=` is accepted and dropped.
114
+ ///
115
+ /// # Errors
116
+ /// [`Error::InvalidValidation`] when empty or over 8,192 characters.
117
+ pub fn list_from(source: impl Into<String>) -> Result<Self> {
118
+ Ok(Self::new(Kind::List, formula(source.into())?))
119
+ }
120
+
121
+ /// Accepts a value when the formula is true. References in it are
122
+ /// relative to the top-left cell of the range, as in Excel: on `B2:B9`,
123
+ /// `LEN(B2)<=100` checks each cell's own length.
124
+ ///
125
+ /// # Errors
126
+ /// As [`list_from`](Self::list_from).
127
+ pub fn custom(formula_text: impl Into<String>) -> Result<Self> {
128
+ Ok(Self::new(Kind::Custom, formula(formula_text.into())?))
129
+ }
130
+
131
+ /// Whether an empty cell passes, true by default.
132
+ #[must_use]
133
+ pub const fn allow_blank(mut self, allow: bool) -> Self {
134
+ self.allow_blank = allow;
135
+ self
136
+ }
137
+
138
+ /// Whether a list shows its in-cell drop-down arrow, true by default.
139
+ #[must_use]
140
+ pub const fn dropdown(mut self, shown: bool) -> Self {
141
+ self.dropdown = shown;
142
+ self
143
+ }
144
+
145
+ #[must_use]
146
+ pub const fn alert(mut self, alert: Alert) -> Self {
147
+ self.alert = alert;
148
+ self
149
+ }
150
+
151
+ /// A note shown while a cell of the range is selected; either part may
152
+ /// be empty.
153
+ ///
154
+ /// # Errors
155
+ /// [`Error::InvalidValidation`] for a title over 32 characters or a
156
+ /// message over 255.
157
+ pub fn prompt(mut self, title: impl Into<String>, message: impl Into<String>) -> Result<Self> {
158
+ self.prompt_title = text("prompt title", title.into(), MAX_TITLE)?;
159
+ self.prompt = text("prompt", message.into(), MAX_MESSAGE)?;
160
+ Ok(self)
161
+ }
162
+
163
+ /// The title and message of the alert a rejected value raises; Excel
164
+ /// uses its own when they are empty.
165
+ ///
166
+ /// # Errors
167
+ /// As [`prompt`](Self::prompt).
168
+ pub fn error(mut self, title: impl Into<String>, message: impl Into<String>) -> Result<Self> {
169
+ self.error_title = text("error title", title.into(), MAX_TITLE)?;
170
+ self.error = text("error", message.into(), MAX_MESSAGE)?;
171
+ Ok(self)
172
+ }
173
+
174
+ /// `<dataValidation>` for `range`.
175
+ pub(crate) fn write(&self, xml: &mut XmlWriter, range: &CellRange) {
176
+ let sqref = range.to_string();
177
+ let mut attrs = vec![(
178
+ "type",
179
+ match self.kind {
180
+ Kind::List => "list",
181
+ Kind::Custom => "custom",
182
+ },
183
+ )];
184
+ match self.alert {
185
+ Alert::Stop | Alert::Off => {}
186
+ Alert::Warning => attrs.push(("errorStyle", "warning")),
187
+ Alert::Information => attrs.push(("errorStyle", "information")),
188
+ }
189
+ if self.allow_blank {
190
+ attrs.push(("allowBlank", "1"));
191
+ }
192
+ // Inverted in the file format: showDropDown="1" hides the arrow.
193
+ if self.kind == Kind::List && !self.dropdown {
194
+ attrs.push(("showDropDown", "1"));
195
+ }
196
+ if self.prompt_title.is_some() || self.prompt.is_some() {
197
+ attrs.push(("showInputMessage", "1"));
198
+ }
199
+ if self.alert != Alert::Off {
200
+ attrs.push(("showErrorMessage", "1"));
201
+ }
202
+ let texts = [
203
+ ("errorTitle", &self.error_title),
204
+ ("error", &self.error),
205
+ ("promptTitle", &self.prompt_title),
206
+ ("prompt", &self.prompt),
207
+ ]
208
+ .map(|(name, value)| (name, value.as_deref().map(escape)));
209
+ for (name, value) in &texts {
210
+ if let Some(value) = value {
211
+ attrs.push((name, value));
212
+ }
213
+ }
214
+ attrs.push(("sqref", &sqref));
215
+ xml.open("dataValidation", &attrs);
216
+ xml.element("formula1", &[], &escape(&self.formula));
217
+ xml.close("dataValidation");
218
+ }
219
+ }
220
+
221
+ /// `<dataValidations>`, nothing when there are none.
222
+ pub fn write_all(xml: &mut XmlWriter, validations: &[(CellRange, Validation)]) {
223
+ if validations.is_empty() {
224
+ return;
225
+ }
226
+ let count = validations.len().to_string();
227
+ xml.open("dataValidations", &[("count", &count)]);
228
+ for (range, validation) in validations {
229
+ validation.write(xml, range);
230
+ }
231
+ xml.close("dataValidations");
232
+ }
233
+
234
+ const fn invalid(reason: String) -> Error {
235
+ Error::InvalidValidation(reason)
236
+ }
237
+
238
+ fn formula(mut source: String) -> Result<String> {
239
+ if source.starts_with('=') {
240
+ source.remove(0);
241
+ }
242
+ let length = source.chars().count();
243
+ if source.trim().is_empty() || length > MAX_FORMULA {
244
+ return Err(invalid(format!(
245
+ "a formula must be 1 to {MAX_FORMULA} characters, not {length}"
246
+ )));
247
+ }
248
+ Ok(source)
249
+ }
250
+
251
+ /// `None` for empty text, which Excel treats as absent.
252
+ fn text(what: &str, value: String, limit: usize) -> Result<Option<String>> {
253
+ let length = value.chars().count();
254
+ (length <= limit).ok_or_else(|| {
255
+ invalid(format!("the {what} is {length} characters; Excel takes {limit} at most"))
256
+ })?;
257
+ Ok((!value.is_empty()).then_some(value))
258
+ }
@@ -0,0 +1,350 @@
1
+ #[cfg(test)]
2
+ mod tests;
3
+
4
+ use std::collections::{BTreeMap, HashMap};
5
+ use std::sync::Arc;
6
+
7
+ use super::text::check_length;
8
+ use super::{Cell, CellRange, Comment, Format, Validation};
9
+ use crate::{Error, Result};
10
+
11
+ /// Rows in a sheet (Excel 2007 and later).
12
+ pub const MAX_ROWS: u32 = 1_048_576;
13
+ /// Columns in a sheet: `A` to `XFD`.
14
+ pub const MAX_COLUMNS: u16 = 16_384;
15
+ const MAX_FORMULA: usize = 8_192;
16
+ const MAX_COLUMN_WIDTH: f64 = 255.0;
17
+ const DATE_FORMAT: &str = "yyyy-mm-dd";
18
+ const DATE_TIME_FORMAT: &str = "yyyy-mm-dd hh:mm:ss";
19
+ /// Excel's limit on distinct cell formats in a workbook.
20
+ pub const MAX_FORMATS: usize = 64_000;
21
+
22
+ /// A written cell and its format: 0 for the default look, otherwise
23
+ /// `formats[style - 1]`.
24
+ #[derive(Debug)]
25
+ pub struct Entry {
26
+ pub cell: Cell,
27
+ pub style: u32,
28
+ }
29
+
30
+ /// One sheet's cells, kept sparse and in row-major order, with the formats
31
+ /// they use interned once each.
32
+ #[derive(Debug)]
33
+ pub struct Worksheet {
34
+ name: String,
35
+ rows: BTreeMap<u32, BTreeMap<u16, Entry>>,
36
+ next_row: u32,
37
+ /// Shared with `format_ids`, so each format is stored once.
38
+ formats: Vec<Arc<Format>>,
39
+ format_ids: HashMap<Arc<Format>, u32>,
40
+ /// The style of an unformatted date, then of an unformatted date and time.
41
+ date_styles: [Option<u32>; 2],
42
+ widths: BTreeMap<u16, f64>,
43
+ right_to_left: bool,
44
+ validations: Vec<(CellRange, Validation)>,
45
+ comments: BTreeMap<(u32, u16), Comment>,
46
+ }
47
+
48
+ impl Worksheet {
49
+ pub(super) fn new(name: String) -> Self {
50
+ Self {
51
+ name,
52
+ rows: BTreeMap::new(),
53
+ next_row: 0,
54
+ formats: Vec::new(),
55
+ format_ids: HashMap::new(),
56
+ date_styles: [None; 2],
57
+ widths: BTreeMap::new(),
58
+ right_to_left: false,
59
+ validations: Vec::new(),
60
+ comments: BTreeMap::new(),
61
+ }
62
+ }
63
+
64
+ #[must_use]
65
+ pub fn name(&self) -> &str {
66
+ &self.name
67
+ }
68
+
69
+ /// Sets one cell, replacing what was there. Row and column are zero-based.
70
+ ///
71
+ /// # Errors
72
+ /// Out-of-range positions, text or formulas over Excel's limits, and
73
+ /// non-finite numbers.
74
+ pub fn write(&mut self, row: u32, column: u16, value: impl Into<Cell>) -> Result<&mut Self> {
75
+ self.write_cell(row, column, value.into(), None)
76
+ }
77
+
78
+ /// As [`write`](Self::write), with a format.
79
+ ///
80
+ /// # Errors
81
+ /// As [`write`](Self::write).
82
+ pub fn write_with_format(
83
+ &mut self,
84
+ row: u32,
85
+ column: u16,
86
+ value: impl Into<Cell>,
87
+ format: &Format,
88
+ ) -> Result<&mut Self> {
89
+ self.write_cell(row, column, value.into(), Some(format))
90
+ }
91
+
92
+ /// Writes `cells` from column `A` of the row after the last one written.
93
+ /// Nothing is written unless every cell is valid.
94
+ ///
95
+ /// # Errors
96
+ /// As [`write`](Self::write); also when the sheet is full.
97
+ pub fn append_row<C: Into<Cell>>(
98
+ &mut self,
99
+ cells: impl IntoIterator<Item = C>,
100
+ ) -> Result<&mut Self> {
101
+ self.append_sparse_row(cells.into_iter().map(|cell| Some(cell.into())), None)
102
+ }
103
+
104
+ /// As [`append_row`](Self::append_row), every cell with `format`.
105
+ ///
106
+ /// # Errors
107
+ /// As [`append_row`](Self::append_row).
108
+ pub fn append_row_with_format<C: Into<Cell>>(
109
+ &mut self,
110
+ cells: impl IntoIterator<Item = C>,
111
+ format: &Format,
112
+ ) -> Result<&mut Self> {
113
+ self.append_sparse_row(cells.into_iter().map(|cell| Some(cell.into())), Some(format))
114
+ }
115
+
116
+ /// As [`append_row`](Self::append_row), where `None` leaves its column
117
+ /// empty and `format`, if any, applies to every cell written.
118
+ ///
119
+ /// # Errors
120
+ /// As [`append_row`](Self::append_row).
121
+ pub fn append_sparse_row(
122
+ &mut self,
123
+ cells: impl IntoIterator<Item = Option<Cell>>,
124
+ format: Option<&Format>,
125
+ ) -> Result<&mut Self> {
126
+ self.append_styled_row(cells.into_iter().map(|cell| (cell, format)))
127
+ }
128
+
129
+ /// As [`append_sparse_row`](Self::append_sparse_row), each cell with a
130
+ /// format of its own.
131
+ ///
132
+ /// # Errors
133
+ /// As [`append_row`](Self::append_row).
134
+ pub fn append_styled_row<'f>(
135
+ &mut self,
136
+ cells: impl IntoIterator<Item = (Option<Cell>, Option<&'f Format>)>,
137
+ ) -> Result<&mut Self> {
138
+ let row = self.next_row;
139
+ check_row(row)?;
140
+ let mut valid = Vec::new();
141
+ for (index, (cell, format)) in cells.into_iter().enumerate() {
142
+ let column = u16::try_from(index)
143
+ .ok()
144
+ .filter(|&column| column < MAX_COLUMNS)
145
+ .ok_or(Error::ColumnOutOfRange(index as u64))?;
146
+ if let Some(cell) = cell {
147
+ validate(&cell)?;
148
+ valid.push((column, cell, format));
149
+ }
150
+ }
151
+ // Formats are interned only once the whole row is known to be valid.
152
+ let mut values = BTreeMap::new();
153
+ for (column, cell, format) in valid {
154
+ let style = self.style_for(&cell, format)?;
155
+ values.insert(column, Entry { cell, style });
156
+ }
157
+ if !values.is_empty() {
158
+ self.rows.insert(row, values);
159
+ }
160
+ self.next_row = row + 1;
161
+ Ok(self)
162
+ }
163
+
164
+ /// Sets a column's width, in characters of the default font.
165
+ ///
166
+ /// # Errors
167
+ /// [`Error::ColumnOutOfRange`], or [`Error::InvalidColumnWidth`] outside
168
+ /// 0 to 255.
169
+ pub fn set_column_width(&mut self, column: u16, width: f64) -> Result<&mut Self> {
170
+ check_column(column)?;
171
+ (width.is_finite() && (0.0..=MAX_COLUMN_WIDTH).contains(&width))
172
+ .ok_or(Error::InvalidColumnWidth(width))?;
173
+ self.widths.insert(column, width);
174
+ Ok(self)
175
+ }
176
+
177
+ /// Lays the sheet out right to left: column `A` on the right, as Excel
178
+ /// shows sheets for Arabic and Hebrew.
179
+ pub const fn set_right_to_left(&mut self, right_to_left: bool) -> &mut Self {
180
+ self.right_to_left = right_to_left;
181
+ self
182
+ }
183
+
184
+ #[must_use]
185
+ pub const fn is_right_to_left(&self) -> bool {
186
+ self.right_to_left
187
+ }
188
+
189
+ /// Applies a rule to the cells of `range`, whether or not they hold values.
190
+ ///
191
+ /// # Errors
192
+ /// [`Error::OverlappingValidation`] when the range shares a cell with
193
+ /// one already validated: Excel applies a single rule per cell.
194
+ pub fn add_validation(
195
+ &mut self,
196
+ range: CellRange,
197
+ validation: Validation,
198
+ ) -> Result<&mut Self> {
199
+ if self.validations.iter().any(|(taken, _)| taken.overlaps(&range)) {
200
+ return Err(Error::OverlappingValidation(range.to_string()));
201
+ }
202
+ self.validations.push((range, validation));
203
+ Ok(self)
204
+ }
205
+
206
+ /// The rules added, in order, each with its range.
207
+ #[must_use]
208
+ pub fn validations(&self) -> &[(CellRange, Validation)] {
209
+ &self.validations
210
+ }
211
+
212
+ /// Attaches a comment to a cell, replacing any it had. The cell need not
213
+ /// hold a value.
214
+ ///
215
+ /// # Errors
216
+ /// [`Error::RowOutOfRange`] or [`Error::ColumnOutOfRange`].
217
+ pub fn set_comment(&mut self, row: u32, column: u16, comment: Comment) -> Result<&mut Self> {
218
+ check_row(row)?;
219
+ check_column(column)?;
220
+ self.comments.insert((row, column), comment);
221
+ Ok(self)
222
+ }
223
+
224
+ /// The comment on a cell, if it has one.
225
+ #[must_use]
226
+ pub fn comment(&self, row: u32, column: u16) -> Option<&Comment> {
227
+ self.comments.get(&(row, column))
228
+ }
229
+
230
+ /// The cell at a position, if one was written.
231
+ #[must_use]
232
+ pub fn get(&self, row: u32, column: u16) -> Option<&Cell> {
233
+ Some(&self.entry(row, column)?.cell)
234
+ }
235
+
236
+ /// The format of the cell at a position, if it has one.
237
+ #[must_use]
238
+ pub fn format_at(&self, row: u32, column: u16) -> Option<&Format> {
239
+ let style = self.entry(row, column)?.style;
240
+ self.formats.get(usize::try_from(style).ok()?.checked_sub(1)?).map(AsRef::as_ref)
241
+ }
242
+
243
+ /// The zero-based row an [`append_row`](Self::append_row) writes next.
244
+ #[must_use]
245
+ pub const fn next_row(&self) -> u32 {
246
+ self.next_row
247
+ }
248
+
249
+ /// Rows that hold at least one cell, in order, each with its cells in
250
+ /// column order.
251
+ pub(crate) fn rows(&self) -> impl Iterator<Item = (u32, &BTreeMap<u16, Entry>)> {
252
+ self.rows.iter().map(|(&row, cells)| (row, cells))
253
+ }
254
+
255
+ /// The formats cells refer to, `style - 1` indexed.
256
+ pub(crate) fn formats(&self) -> &[Arc<Format>] {
257
+ &self.formats
258
+ }
259
+
260
+ pub(crate) const fn column_widths(&self) -> &BTreeMap<u16, f64> {
261
+ &self.widths
262
+ }
263
+
264
+ /// Comments by `(row, column)`, in row-major order.
265
+ pub(crate) const fn comments(&self) -> &BTreeMap<(u32, u16), Comment> {
266
+ &self.comments
267
+ }
268
+
269
+ fn entry(&self, row: u32, column: u16) -> Option<&Entry> {
270
+ self.rows.get(&row)?.get(&column)
271
+ }
272
+
273
+ fn write_cell(
274
+ &mut self,
275
+ row: u32,
276
+ column: u16,
277
+ cell: Cell,
278
+ format: Option<&Format>,
279
+ ) -> Result<&mut Self> {
280
+ check_row(row)?;
281
+ check_column(column)?;
282
+ validate(&cell)?;
283
+ let style = self.style_for(&cell, format)?;
284
+ self.rows.entry(row).or_default().insert(column, Entry { cell, style });
285
+ self.next_row = self.next_row.max(row + 1);
286
+ Ok(self)
287
+ }
288
+
289
+ /// The style id for a cell: dates without a number format get one, so
290
+ /// they do not show as bare serial numbers.
291
+ fn style_for(&mut self, cell: &Cell, format: Option<&Format>) -> Result<u32> {
292
+ if let Cell::DateTime(date) = cell
293
+ && !format.is_some_and(Format::has_number_format)
294
+ {
295
+ let slot = usize::from(date.has_time());
296
+ // The common case, a date with no format at all, is cached.
297
+ if format.is_none()
298
+ && let Some(id) = self.date_styles[slot]
299
+ {
300
+ return Ok(id);
301
+ }
302
+ let code = if date.has_time() { DATE_TIME_FORMAT } else { DATE_FORMAT };
303
+ let mut dated = format.cloned().unwrap_or_default();
304
+ dated.number_code = Some(code.to_owned());
305
+ let id = self.intern(&dated)?;
306
+ if format.is_none() {
307
+ self.date_styles[slot] = Some(id);
308
+ }
309
+ return Ok(id);
310
+ }
311
+ match format {
312
+ Some(format) if *format != Format::default() => self.intern(format),
313
+ _ => Ok(0),
314
+ }
315
+ }
316
+
317
+ fn intern(&mut self, format: &Format) -> Result<u32> {
318
+ if let Some(&id) = self.format_ids.get(format) {
319
+ return Ok(id);
320
+ }
321
+ let id = u32::try_from(self.formats.len() + 1)
322
+ .ok()
323
+ .filter(|_| self.formats.len() < MAX_FORMATS)
324
+ .ok_or(Error::TooManyFormats)?;
325
+ let shared = Arc::new(format.clone());
326
+ self.formats.push(Arc::clone(&shared));
327
+ self.format_ids.insert(shared, id);
328
+ Ok(id)
329
+ }
330
+ }
331
+
332
+ pub(super) fn check_row(row: u32) -> Result<()> {
333
+ (row < MAX_ROWS).ok_or(Error::RowOutOfRange(row.into()))
334
+ }
335
+
336
+ pub(super) fn check_column(column: u16) -> Result<()> {
337
+ (column < MAX_COLUMNS).ok_or(Error::ColumnOutOfRange(column.into()))
338
+ }
339
+
340
+ fn validate(cell: &Cell) -> Result<()> {
341
+ match cell {
342
+ Cell::Text(text) => check_length(text),
343
+ Cell::Formula(formula) => {
344
+ let length = formula.chars().count();
345
+ (length <= MAX_FORMULA).ok_or(Error::FormulaTooLong(length))
346
+ }
347
+ Cell::Number(number) => number.is_finite().ok_or(Error::NonFiniteNumber(*number)),
348
+ Cell::Bool(_) | Cell::DateTime(_) => Ok(()),
349
+ }
350
+ }