rust_xlsxwriter/utility.rs
1// Some utility functions for the `rust_xlsxwriter` module.
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4//
5// Copyright 2022-2026, John McNamara, jmcnamara@cpan.org
6
7//! Utility functions for `rust_xlsxwriter`.
8//!
9//! The `rust_xlsxwriter` library provides a number of utility functions for
10//! dealing with cell ranges, Chrono and Jiff Serde serialization, and other
11//! helper method.
12//!
13//!
14//! # Examples:
15//!
16//! ```
17//! use rust_xlsxwriter::{cell_range, column_number_to_name};
18//!
19//! assert_eq!(column_number_to_name(1), "B");
20//! assert_eq!(column_number_to_name(702), "AAA");
21//!
22//! assert_eq!(cell_range(0, 0, 9, 0), "A1:A10");
23//! assert_eq!(cell_range(1, 2, 8, 2), "C2:C9");
24//! assert_eq!(cell_range(0, 0, 3, 4), "A1:E4");
25//! ```
26
27#![warn(missing_docs)]
28mod tests;
29
30use crate::COL_MAX;
31use crate::MAX_AUTOFIT_WIDTH_PIXELS;
32use crate::ROW_MAX;
33
34#[cfg(feature = "serde")]
35use crate::IntoExcelDateTime;
36
37#[cfg(feature = "serde")]
38use serde::Serializer;
39
40use crate::worksheet::ColNum;
41use crate::worksheet::RowNum;
42use crate::XlsxError;
43
44/// Convert a zero indexed column cell reference to a string like `"A"`.
45///
46/// This is a utility function to convert a zero based column reference to a
47/// string representation. This can be useful when constructing ranges for
48/// formulas.
49///
50/// # Parameters
51///
52/// - `col_num`: The zero indexed column number.
53///
54///
55/// # Examples:
56///
57/// ```
58/// use rust_xlsxwriter::column_number_to_name;
59///
60/// assert_eq!(column_number_to_name(0), "A");
61/// assert_eq!(column_number_to_name(1), "B");
62/// assert_eq!(column_number_to_name(702), "AAA");
63/// ```
64///
65pub fn column_number_to_name(col_num: ColNum) -> String {
66 let mut col_name = String::new();
67
68 let mut col_num = col_num + 1;
69
70 while col_num > 0 {
71 // Set remainder from 1 .. 26
72 let mut remainder = col_num % 26;
73
74 if remainder == 0 {
75 remainder = 26;
76 }
77
78 // Convert the remainder to a character.
79 let col_letter = char::from_u32(64u32 + u32::from(remainder)).unwrap();
80
81 // Accumulate the column letters, right to left.
82 col_name = format!("{col_letter}{col_name}");
83
84 // Get the next order of magnitude.
85 col_num = (col_num - 1) / 26;
86 }
87
88 col_name
89}
90
91/// Convert a column string such as `"A"` to a zero indexed column reference.
92///
93/// This is a utility function to convert a column string representation to a
94/// zero based column reference.
95///
96/// # Parameters
97///
98/// - `column`: A string representing a column reference.
99///
100/// # Examples:
101///
102/// ```
103/// use rust_xlsxwriter::column_name_to_number;
104///
105/// assert_eq!(column_name_to_number("A"), 0);
106/// assert_eq!(column_name_to_number("B"), 1);
107/// assert_eq!(column_name_to_number("AAA"), 702);
108/// ```
109///
110pub fn column_name_to_number(column: &str) -> ColNum {
111 if column.is_empty() {
112 return 0;
113 }
114
115 let mut col_num = 0;
116 for char in column.chars() {
117 col_num = (col_num * 26) + (char as u16 - 'A' as u16 + 1);
118 }
119
120 col_num - 1
121}
122
123/// Convert zero indexed row and column cell numbers to a `A1` style string.
124///
125/// This is a utility function to convert zero indexed row and column cell
126/// values to an `A1` cell reference. This can be useful when constructing
127/// ranges for formulas.
128///
129/// # Parameters
130///
131/// - `row`: The zero indexed row number.
132/// - `col`: The zero indexed column number.
133///
134/// # Examples:
135///
136/// ```
137/// use rust_xlsxwriter::row_col_to_cell;
138///
139/// assert_eq!(row_col_to_cell(0, 0), "A1");
140/// assert_eq!(row_col_to_cell(0, 1), "B1");
141/// assert_eq!(row_col_to_cell(1, 1), "B2");
142/// ```
143///
144pub fn row_col_to_cell(row: RowNum, col: ColNum) -> String {
145 format!("{}{}", column_number_to_name(col), row + 1)
146}
147
148/// Convert zero indexed row and column cell numbers to an absolute `$A$1` style
149/// range string.
150///
151/// This is a utility function to convert zero indexed row and column cell
152/// values to an absolute `$A$1` cell reference. This can be useful when
153/// constructing ranges for formulas.
154///
155/// # Parameters
156///
157/// - `row`: The zero indexed row number.
158/// - `col`: The zero indexed column number.
159///
160/// # Examples:
161///
162/// ```
163/// use rust_xlsxwriter::row_col_to_cell_absolute;
164///
165/// assert_eq!(row_col_to_cell_absolute(0, 0), "$A$1");
166/// assert_eq!(row_col_to_cell_absolute(0, 1), "$B$1");
167/// assert_eq!(row_col_to_cell_absolute(1, 1), "$B$2");
168/// ```
169///
170pub fn row_col_to_cell_absolute(row: RowNum, col: ColNum) -> String {
171 format!("${}${}", column_number_to_name(col), row + 1)
172}
173
174/// Convert zero indexed row and col cell numbers to a `A1:B1` style range
175/// string.
176///
177/// This is a utility function to convert zero based row and column cell values
178/// to an `A1:B1` style range reference.
179///
180/// Note, this function should not be used to create a chart range. Use the
181/// 5-tuple version of [`IntoChartRange`](crate::IntoChartRange) instead.
182///
183/// # Parameters
184///
185/// - `first_row`: The first row of the range. (All zero indexed.)
186/// - `first_col`: The first row of the range.
187/// - `last_row`: The last row of the range.
188/// - `last_col`: The last row of the range.
189///
190/// # Examples:
191///
192/// ```
193/// use rust_xlsxwriter::cell_range;
194///
195/// assert_eq!(cell_range(0, 0, 9, 0), "A1:A10");
196/// assert_eq!(cell_range(1, 2, 8, 2), "C2:C9");
197/// assert_eq!(cell_range(0, 0, 3, 4), "A1:E4");
198/// ```
199///
200/// If the start and end cell are the same then a single cell range is created:
201///
202/// ```
203/// use rust_xlsxwriter::cell_range;
204///
205/// assert_eq!(cell_range(0, 0, 0, 0), "A1");
206/// ```
207///
208pub fn cell_range(
209 first_row: RowNum,
210 first_col: ColNum,
211 last_row: RowNum,
212 last_col: ColNum,
213) -> String {
214 let range1 = row_col_to_cell(first_row, first_col);
215 let range2 = row_col_to_cell(last_row, last_col);
216
217 if range1 == range2 {
218 range1
219 } else {
220 format!("{range1}:{range2}")
221 }
222}
223
224/// Convert zero indexed row and col cell numbers to an absolute `$A$1:$B$1`
225/// style range string.
226///
227/// This is a utility function to convert zero based row and column cell values
228/// to an absolute `$A$1:$B$1` style range reference.
229///
230/// Note, this function should not be used to create a chart range. Use the
231/// 5-tuple version of [`IntoChartRange`](crate::IntoChartRange) instead.
232///
233/// # Parameters
234///
235/// - `first_row`: The first row of the range. (All zero indexed.)
236/// - `first_col`: The first row of the range.
237/// - `last_row`: The last row of the range.
238/// - `last_col`: The last row of the range.
239///
240/// # Examples:
241///
242/// ```
243/// use rust_xlsxwriter::cell_range_absolute;
244///
245/// assert_eq!(cell_range_absolute(0, 0, 9, 0), "$A$1:$A$10");
246/// assert_eq!(cell_range_absolute(1, 2, 8, 2), "$C$2:$C$9");
247/// assert_eq!(cell_range_absolute(0, 0, 3, 4), "$A$1:$E$4");
248/// ```
249///
250/// If the start and end cell are the same then a single cell range is created:
251///
252/// ```
253/// use rust_xlsxwriter::cell_range_absolute;
254///
255/// assert_eq!(cell_range_absolute(0, 0, 0, 0), "$A$1");
256/// ```
257///
258pub fn cell_range_absolute(
259 first_row: RowNum,
260 first_col: ColNum,
261 last_row: RowNum,
262 last_col: ColNum,
263) -> String {
264 let range1 = row_col_to_cell_absolute(first_row, first_col);
265 let range2 = row_col_to_cell_absolute(last_row, last_col);
266
267 if range1 == range2 {
268 range1
269 } else {
270 format!("{range1}:{range2}")
271 }
272}
273
274/// Convert a worksheet name and cell reference to an Excel "Sheet1!A1:B1" style
275/// range string.
276///
277/// This is a utility function to convert a worksheet name zero based column
278/// reference to a string representation. This can be useful when constructing
279/// ranges for formulas.
280///
281/// Note, this function should not be used to create a chart range. Use the
282/// 5-tuple version of [`IntoChartRange`](crate::IntoChartRange) instead.
283///
284/// # Parameters
285///
286/// - `sheet_name`: The worksheet name that the range refers to.
287/// - `first_row`: The first row of the range. (All zero indexed.)
288/// - `first_col`: The first row of the range.
289/// - `last_row`: The last row of the range.
290/// - `last_col`: The last row of the range.
291///
292/// # Examples:
293///
294/// ```
295/// use rust_xlsxwriter::worksheet_range;
296///
297/// // Single cell range.
298/// let range = worksheet_range("Sheet1", 0, 0, 0, 0);
299/// assert_eq!(range, "Sheet1!A1");
300///
301/// // Cell range.
302/// let range = worksheet_range("Sheet1", 0, 0, 9, 0);
303/// assert_eq!(range, "Sheet1!A1:A10");
304///
305/// // Sheetname that requires quoting.
306/// let range = worksheet_range("Sheet 1", 0, 0, 9, 0);
307/// assert_eq!(range, "'Sheet 1'!A1:A10");
308/// ```
309///
310pub fn worksheet_range(
311 sheet_name: &str,
312 first_row: RowNum,
313 first_col: ColNum,
314 last_row: RowNum,
315 last_col: ColNum,
316) -> String {
317 chart_range(sheet_name, first_row, first_col, last_row, last_col)
318}
319
320/// Convert a worksheet name and cell reference to an Excel "Sheet1!$A$1:$B$1"
321/// style absolute range string.
322///
323/// This is a utility function to convert a worksheet name zero based column
324/// reference to a string representation. This can be useful when constructing
325/// ranges for formulas.
326///
327/// Note, this function should not be used to create a chart range. Use the
328/// 5-tuple version of [`IntoChartRange`](crate::IntoChartRange) instead.
329///
330/// # Parameters
331///
332/// - `sheet_name`: The worksheet name that the range refers to.
333/// - `first_row`: The first row of the range. (All zero indexed.)
334/// - `first_col`: The first row of the range.
335/// - `last_row`: The last row of the range.
336/// - `last_col`: The last row of the range.
337///
338/// # Examples:
339///
340/// ```
341/// use rust_xlsxwriter::worksheet_range_absolute;
342///
343/// // Single cell range.
344/// let range = worksheet_range_absolute("Sheet1", 0, 0, 0, 0);
345/// assert_eq!(range, "Sheet1!$A$1");
346///
347/// // Cell range.
348/// let range = worksheet_range_absolute("Sheet1", 0, 0, 9, 0);
349/// assert_eq!(range, "Sheet1!$A$1:$A$10");
350///
351/// // Sheetname that requires quoting.
352/// let range = worksheet_range_absolute("Sheet 1", 0, 0, 9, 0);
353/// assert_eq!(range, "'Sheet 1'!$A$1:$A$10");
354/// ```
355///
356pub fn worksheet_range_absolute(
357 sheet_name: &str,
358 first_row: RowNum,
359 first_col: ColNum,
360 last_row: RowNum,
361 last_col: ColNum,
362) -> String {
363 chart_range_abs(sheet_name, first_row, first_col, last_row, last_col)
364}
365
366/// Serialize a naive/civil date/time to an Excel value.
367///
368/// This is a helper function for serializing [`Chrono`] or [`Jiff`] naive/civil
369/// date/time fields using [Serde](https://serde.rs). "Naive" and "Civil" means
370/// that the dates/times don't have timezone information, like Excel.
371///
372/// The function works for the following types:
373///
374/// - [`chrono::NaiveDateTime`]
375/// - [`chrono::NaiveDate`]
376/// - [`chrono::NaiveTime`]
377/// - [`jiff::civil::Datetime`]
378/// - [`jiff::civil::Date`]
379/// - [`jiff::civil::Time`]
380///
381/// [`Chrono`]: https://docs.rs/chrono/latest/chrono
382/// [`chrono::NaiveDate`]:
383/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
384/// [`chrono::NaiveTime`]:
385/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html
386/// [`chrono::NaiveDateTime`]:
387/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
388///
389/// [`Jiff`]: https://docs.rs/jiff/latest/jiff
390/// [`jiff::civil::Datetime`]:
391/// https://docs.rs/jiff/latest/jiff/civil/struct.DateTime.html
392/// [`jiff::civil::Date`]:
393/// https://docs.rs/jiff/latest/jiff/civil/struct.Date.html
394/// [`jiff::civil::Time`]:
395/// https://docs.rs/jiff/latest/jiff/civil/struct.Time.html
396///
397/// Support for these types is enabled via the `chrono` and `jiff` cargo
398/// features.
399///
400/// `Option<T>` datetime types can be handled with
401/// [`serialize_option_datetime_to_excel()`].
402///
403/// See [Working with Serde](crate::serializer#working-with-serde) for more
404/// information about serialization with `rust_xlsxwriter`.
405///
406/// # Parameters
407///
408/// - `datetime`: A date/time instance that implements [`IntoExcelDateTime`].
409/// - `serializer`: A type/instance that implements the [`serde`] `Serializer`
410/// trait.
411///
412/// # Errors
413///
414/// - [`XlsxError::SerdeError`] - A wrapped serialization error.
415///
416/// # Examples
417///
418/// Example of a serializable struct with a Chrono Naive value with a helper
419/// function.
420///
421/// ```
422/// # // This code is available in examples/doc_worksheet_serialize_datetime3.rs
423/// #
424/// use chrono::NaiveDate;
425/// use serde::Serialize;
426///
427/// use rust_xlsxwriter::utility::serialize_datetime_to_excel;
428///
429/// fn main() {
430/// #[derive(Serialize)]
431/// struct Student {
432/// full_name: String,
433///
434/// #[serde(serialize_with = "serialize_datetime_to_excel")]
435/// birth_date: NaiveDate,
436///
437/// id_number: u32,
438/// }
439/// }
440/// ```
441///
442#[cfg(feature = "serde")]
443#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
444pub fn serialize_datetime_to_excel<S>(
445 datetime: impl IntoExcelDateTime,
446 serializer: S,
447) -> Result<S::Ok, S::Error>
448where
449 S: Serializer,
450{
451 serializer.serialize_f64(datetime.to_excel_serial_date())
452}
453
454/// Serialize an `Option` naive/civil date/time to an Excel value.
455///
456/// This is a helper function for serializing [`Chrono`] or [`Jiff`] naive/civil
457/// date/time fields using [Serde](https://serde.rs). "Naive" and "Civil" means
458/// that the dates/times don't have timezone information, like Excel.
459///
460/// A helper function is provided for [`Option`] Chrono and Jiff values since it
461/// is common to have `Option<T>` values as a result of deserialization. It also
462/// takes care of the use case where you want a `None` value to be written as a
463/// blank cell with the same cell format as other values of the field type.
464///
465/// The function works for the following `T` types in an `Option<T>`:
466///
467/// - [`chrono::NaiveDateTime`]
468/// - [`chrono::NaiveDate`]
469/// - [`chrono::NaiveTime`]
470/// - [`jiff::civil::Datetime`]
471/// - [`jiff::civil::Date`]
472/// - [`jiff::civil::Time`]
473///
474/// [`Chrono`]: https://docs.rs/chrono/latest/chrono
475/// [`chrono::NaiveDate`]:
476/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
477/// [`chrono::NaiveTime`]:
478/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html
479/// [`chrono::NaiveDateTime`]:
480/// https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
481///
482/// [`Jiff`]: https://docs.rs/jiff/latest/jiff
483/// [`jiff::civil::Datetime`]:
484/// https://docs.rs/jiff/latest/jiff/civil/struct.DateTime.html
485/// [`jiff::civil::Date`]:
486/// https://docs.rs/jiff/latest/jiff/civil/struct.Date.html
487/// [`jiff::civil::Time`]:
488/// https://docs.rs/jiff/latest/jiff/civil/struct.Time.html
489///
490/// Support for these types is enabled via the `chrono` and `jiff` cargo
491/// features.
492///
493/// Non `Option<T>` types can be handled with [`serialize_datetime_to_excel()`].
494///
495/// See [Working with Serde](crate::serializer#working-with-serde) for more
496/// information about serialization with `rust_xlsxwriter`.
497///
498/// # Parameters
499///
500/// - `datetime`: A date/time instance that implements [`IntoExcelDateTime`]
501/// wrapped in an [`Option`].
502/// - `serializer`: A type/instance that implements the [`serde`] `Serializer`
503/// trait.
504///
505/// # Errors
506///
507/// - [`XlsxError::SerdeError`] - A wrapped serialization error.
508///
509/// # Examples
510///
511/// Example of a serializable struct with an Option Chrono Naive value with a
512/// helper function.
513///
514///
515/// ```
516/// # // This code is available in examples/doc_worksheet_serialize_datetime5.rs
517/// #
518/// use chrono::NaiveDate;
519/// use serde::Serialize;
520///
521/// use rust_xlsxwriter::utility::serialize_option_datetime_to_excel;
522///
523/// fn main() {
524/// #[derive(Serialize)]
525/// struct Student {
526/// full_name: String,
527///
528/// #[serde(serialize_with = "serialize_option_datetime_to_excel")]
529/// birth_date: Option<NaiveDate>,
530///
531/// id_number: u32,
532/// }
533/// }
534/// ```
535///
536#[cfg(feature = "serde")]
537#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
538pub fn serialize_option_datetime_to_excel<S>(
539 datetime: &Option<impl IntoExcelDateTime>,
540 serializer: S,
541) -> Result<S::Ok, S::Error>
542where
543 S: Serializer,
544{
545 match datetime {
546 Some(datetime) => serializer.serialize_f64(datetime.to_excel_serial_date()),
547 None => serializer.serialize_none(),
548 }
549}
550
551/// Serialize a chrono naive date/time to an Excel value.
552///
553/// This is deprecated. Use [`serialize_datetime_to_excel()`] instead.
554///
555/// # Parameters
556///
557/// - `datetime`: A date/time instance that implements [`IntoExcelDateTime`].
558/// - `serializer`: A type/instance that implements the [`serde`] `Serializer`
559/// trait.
560///
561/// # Errors
562///
563/// - [`XlsxError::SerdeError`] - A wrapped serialization error.
564///
565#[cfg(feature = "serde")]
566#[doc(hidden)]
567#[deprecated(since = "0.88.0", note = "use `serialize_datetime_to_excel()` instead")]
568pub fn serialize_chrono_naive_to_excel<S>(
569 datetime: impl IntoExcelDateTime,
570 serializer: S,
571) -> Result<S::Ok, S::Error>
572where
573 S: Serializer,
574{
575 serializer.serialize_f64(datetime.to_excel_serial_date())
576}
577
578/// Serialize an `Option` chrono naive date/time to an Excel value.
579///
580/// This is deprecated. Use [`serialize_option_datetime_to_excel()`] instead.
581///
582/// # Parameters
583///
584/// - `datetime`: A date/time instance that implements [`IntoExcelDateTime`]
585/// wrapped in an [`Option`].
586/// - `serializer`: A type/instance that implements the [`serde`] `Serializer`
587/// trait.
588///
589/// # Errors
590///
591/// - [`XlsxError::SerdeError`] - A wrapped serialization error.
592///
593#[cfg(feature = "serde")]
594#[doc(hidden)]
595#[deprecated(
596 since = "0.88.0",
597 note = "use `serialize_option_datetime_to_excel()` instead"
598)]
599pub fn serialize_chrono_option_naive_to_excel<S>(
600 datetime: &Option<impl IntoExcelDateTime>,
601 serializer: S,
602) -> Result<S::Ok, S::Error>
603where
604 S: Serializer,
605{
606 match datetime {
607 Some(datetime) => serializer.serialize_f64(datetime.to_excel_serial_date()),
608 None => serializer.serialize_none(),
609 }
610}
611
612// Convert zero indexed row and col cell references to a non-absolute chart
613// "Sheet1!A1:B1" style range string.
614pub(crate) fn chart_range(
615 sheet_name: &str,
616 first_row: RowNum,
617 first_col: ColNum,
618 last_row: RowNum,
619 last_col: ColNum,
620) -> String {
621 let sheet_name = quote_sheet_name(sheet_name);
622 let range1 = row_col_to_cell(first_row, first_col);
623 let range2 = row_col_to_cell(last_row, last_col);
624
625 if range1 == range2 {
626 format!("{sheet_name}!{range1}")
627 } else {
628 format!("{sheet_name}!{range1}:{range2}")
629 }
630}
631
632// Convert zero indexed row and col cell references to an absolute chart
633// "Sheet1!$A$1:$B$1" style range string.
634pub(crate) fn chart_range_abs(
635 sheet_name: &str,
636 first_row: RowNum,
637 first_col: ColNum,
638 last_row: RowNum,
639 last_col: ColNum,
640) -> String {
641 let sheet_name = quote_sheet_name(sheet_name);
642 let range1 = row_col_to_cell_absolute(first_row, first_col);
643 let range2 = row_col_to_cell_absolute(last_row, last_col);
644
645 if range1 == range2 {
646 format!("{sheet_name}!{range1}")
647 } else {
648 format!("{sheet_name}!{range1}:{range2}")
649 }
650}
651
652// Convert zero indexed row and col cell references to a range and tuple string
653// suitable for an error message.
654pub(crate) fn chart_error_range(
655 sheet_name: &str,
656 first_row: RowNum,
657 first_col: ColNum,
658 last_row: RowNum,
659 last_col: ColNum,
660) -> String {
661 let sheet_name = quote_sheet_name(sheet_name);
662 let range1 = row_col_to_cell(first_row, first_col);
663 let range2 = row_col_to_cell(last_row, last_col);
664
665 if range1 == range2 {
666 format!("{sheet_name}!{range1}/({first_row}, {first_col})")
667 } else {
668 format!("{sheet_name}!{range1}:{range2}/({first_row}, {first_col}, {last_row}, {last_col})")
669 }
670}
671
672/// Enclose a worksheet name in single quotes as required by Excel.
673///
674/// Worksheet names that are used in Excel range references must be single
675/// quoted if they contain non-word characters or if they look like cell
676/// references. The most common instance of this is when the worksheet name
677/// contains spaces. For example, `Sheet1` would be represented without
678/// change in a formula as `=Sheet1!A1`, whereas `Sheet 1` would be represented
679/// as `='Sheet 1'!A1`.
680///
681/// # Parameters
682///
683/// - `sheetname`: The worksheet name to quote.
684///
685/// # Examples
686///
687/// The following example demonstrates quoting worksheet names.
688///
689/// ```
690/// use rust_xlsxwriter::utility::quote_sheet_name;
691///
692/// // Doesn't need to be quoted.
693/// let result = quote_sheet_name("Sheet1");
694/// assert_eq!(result, "Sheet1");
695///
696/// // Spaces need to be quoted.
697/// let result = quote_sheet_name("Sheet 1");
698/// assert_eq!(result, "'Sheet 1'");
699///
700/// // Special characters need to be quoted.
701/// let result = quote_sheet_name("Sheet-1");
702/// assert_eq!(result, "'Sheet-1'");
703///
704/// // Single quotes need to be escaped with a quote.
705/// let result = quote_sheet_name("Sheet'1");
706/// assert_eq!(result, "'Sheet''1'");
707///
708/// // A1 style cell references don't need to be quoted.
709/// let result = quote_sheet_name("A1");
710/// assert_eq!(result, "'A1'");
711///
712/// // R1C1 style cell references need to be quoted.
713/// let result = quote_sheet_name("RC1");
714/// assert_eq!(result, "'RC1'");
715/// ```
716///
717#[allow(clippy::if_same_then_else)]
718pub fn quote_sheet_name(sheetname: &str) -> String {
719 // Sheetnames used in references should be quoted if they contain any
720 // spaces, special characters or if they look like a A1 or RC cell
721 // reference. The rules are shown inline below.
722 let mut sheetname = sheetname.to_string();
723 let uppercase_sheetname = sheetname.to_uppercase();
724 let mut requires_quoting = false;
725 let col_max = u64::from(COL_MAX);
726 let row_max = u64::from(ROW_MAX);
727
728 // Split sheetnames that look like A1 and R1C1 style cell references into a
729 // leading string and a trailing number.
730 let (string_part, number_part) = split_cell_reference(&sheetname);
731
732 // The number part of the sheet name can have trailing non-digit characters
733 // and still be a valid R1C1 match. However, to test the R1C1 row/col part
734 // we need to extract just the number part.
735 let mut number_parts = number_part.split(|c: char| !c.is_ascii_digit());
736 let rc_number_part = number_parts.next().unwrap_or_default();
737
738 // Ignore strings that are already quoted.
739 if !sheetname.starts_with('\'') {
740 // --------------------------------------------------------------------
741 // Rule 1. Sheet names that contain anything other than \w and "."
742 // characters must be quoted.
743 // --------------------------------------------------------------------
744
745 if !sheetname
746 .chars()
747 .all(|c| c.is_alphanumeric() || c == '_' || c == '.' || is_emoji(c))
748 {
749 requires_quoting = true;
750 }
751 // --------------------------------------------------------------------
752 // Rule 2. Sheet names that start with a digit or "." must be quoted.
753 // --------------------------------------------------------------------
754 else if sheetname.starts_with(|c: char| c.is_ascii_digit() || c == '.' || is_emoji(c)) {
755 requires_quoting = true;
756 }
757 // --------------------------------------------------------------------
758 // Rule 3. Sheet names must not be a valid A1 style cell reference.
759 // Valid means that the row and column range values must also be within
760 // Excel row and column limits.
761 // --------------------------------------------------------------------
762 else if (1..=3).contains(&string_part.len())
763 && number_part.chars().all(|c| c.is_ascii_digit())
764 {
765 let col = column_name_to_number(&string_part);
766 let col = u64::from(col + 1);
767
768 let row = number_part.parse::<u64>().unwrap_or_default();
769
770 if row > 0 && row <= row_max && col <= col_max {
771 requires_quoting = true;
772 }
773 }
774 // --------------------------------------------------------------------
775 // Rule 4. Sheet names must not *start* with a valid RC style cell
776 // reference. Other characters after the valid RC reference are ignored
777 // by Excel. Valid means that the row and column range values must also
778 // be within Excel row and column limits.
779 //
780 // Note: references without trailing characters like R12345 or C12345
781 // are caught by Rule 3. Negative references like R-12345 are caught by
782 // Rule 1 due to dash.
783 // --------------------------------------------------------------------
784
785 // Rule 4a. Check for sheet names that start with R1 style references.
786 else if string_part == "R" {
787 let row = rc_number_part.parse::<u64>().unwrap_or_default();
788
789 if row > 0 && row <= row_max {
790 requires_quoting = true;
791 }
792 }
793 // Rule 4b. Check for sheet names that start with C1 or RC1 style
794 // references.
795 else if string_part == "RC" || string_part == "C" {
796 let col = rc_number_part.parse::<u64>().unwrap_or_default();
797
798 if col > 0 && col <= col_max {
799 requires_quoting = true;
800 }
801 }
802 // Rule 4c. Check for some single R/C references.
803 else if uppercase_sheetname == "R"
804 || uppercase_sheetname == "C"
805 || uppercase_sheetname == "RC"
806 {
807 requires_quoting = true;
808 }
809 }
810
811 if requires_quoting {
812 // Double up any single quotes.
813 sheetname = sheetname.replace('\'', "''");
814
815 // Single quote the sheet name.
816 sheetname = format!("'{sheetname}'");
817 }
818
819 sheetname
820}
821
822// Unquote an Excel single quoted string.
823pub(crate) fn unquote_sheetname(sheetname: &str) -> String {
824 if sheetname.starts_with('\'') && sheetname.ends_with('\'') {
825 let sheetname = sheetname[1..sheetname.len() - 1].to_string();
826 sheetname.replace("''", "'")
827 } else {
828 sheetname.to_string()
829 }
830}
831
832// Match emoji characters when quoting sheetnames. The following were generated from:
833// https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AEmoji%3DYes%3A%5D&abb=on&esc=on&g=&i=
834//
835pub(crate) fn is_emoji(c: char) -> bool {
836 if c < '\u{203C}' {
837 // Shortcut for most chars in the lower range. We ignore '#', '*',
838 // '0-9', '©️' and '®️' which are in this range and which are, strictly
839 // speaking, emoji symbols but they are not treated so by Excel in the
840 // context of this check.
841 return false;
842 }
843
844 if c < '\u{01F004}' {
845 return matches!(c,
846 '\u{203C}' | '\u{2049}' | '\u{2122}' | '\u{2139}' | '\u{2194}'..='\u{2199}' |
847 '\u{21A9}' | '\u{21AA}' | '\u{231A}' | '\u{231B}' | '\u{2328}' | '\u{23CF}' |
848 '\u{23E9}'..='\u{23F3}' | '\u{23F8}'..='\u{23FA}' | '\u{24C2}' | '\u{25AA}' |
849 '\u{25AB}' | '\u{25B6}' | '\u{25C0}' | '\u{25FB}'..='\u{25FE}' |
850 '\u{2600}'..='\u{2604}' | '\u{260E}' | '\u{2611}' | '\u{2614}' | '\u{2615}' |
851 '\u{2618}' | '\u{261D}' | '\u{2620}' | '\u{2622}' | '\u{2623}' | '\u{2626}' |
852 '\u{262A}' | '\u{262E}' | '\u{262F}' | '\u{2638}'..='\u{263A}' | '\u{2640}' |
853 '\u{2642}' | '\u{2648}'..='\u{2653}' | '\u{265F}' | '\u{2660}' | '\u{2663}' |
854 '\u{2665}' | '\u{2666}' | '\u{2668}' | '\u{267B}' | '\u{267E}' | '\u{267F}' |
855 '\u{2692}'..='\u{2697}' | '\u{2699}' | '\u{269B}' | '\u{269C}' | '\u{26A0}' |
856 '\u{26A1}' | '\u{26A7}' | '\u{26AA}' | '\u{26AB}' | '\u{26B0}' | '\u{26B1}' |
857 '\u{26BD}' | '\u{26BE}' | '\u{26C4}' | '\u{26C5}' | '\u{26C8}' | '\u{26CE}' |
858 '\u{26CF}' | '\u{26D1}' | '\u{26D3}' | '\u{26D4}' | '\u{26E9}' | '\u{26EA}' |
859 '\u{26F0}'..='\u{26F5}' | '\u{26F7}'..='\u{26FA}' | '\u{26FD}' | '\u{2702}' |
860 '\u{2705}' | '\u{2708}'..='\u{270D}' | '\u{270F}' | '\u{2712}' | '\u{2714}' |
861 '\u{2716}' | '\u{271D}' | '\u{2721}' | '\u{2728}' | '\u{2733}' | '\u{2734}' |
862 '\u{2744}' | '\u{2747}' | '\u{274C}' | '\u{274E}' | '\u{2753}'..='\u{2755}' |
863 '\u{2757}' | '\u{2763}' | '\u{2764}' | '\u{2795}'..='\u{2797}' | '\u{27A1}' |
864 '\u{27B0}' | '\u{27BF}' | '\u{2934}' | '\u{2935}' | '\u{2B05}'..='\u{2B07}' |
865 '\u{2B1B}' | '\u{2B1C}' | '\u{2B50}' | '\u{2B55}' | '\u{3030}' | '\u{303D}' |
866 '\u{3297}' | '\u{3299}'
867 );
868 }
869
870 matches!(c,
871 '\u{01F004}' | '\u{01F0CF}' | '\u{01F170}' | '\u{01F171}' | '\u{01F17E}' | '\u{01F17F}' |
872 '\u{01F18E}' | '\u{01F191}'..='\u{01F19A}' | '\u{01F1E6}'..='\u{01F1FF}' | '\u{01F201}' |
873 '\u{01F202}' | '\u{01F21A}' | '\u{01F22F}' | '\u{01F232}'..='\u{01F23A}' | '\u{01F250}' |
874 '\u{01F251}' | '\u{01F300}'..='\u{01F321}' | '\u{01F324}'..='\u{01F393}' | '\u{01F396}' |
875 '\u{01F397}' | '\u{01F399}'..='\u{01F39B}' | '\u{01F39E}'..='\u{01F3F0}' |
876 '\u{01F3F3}'..='\u{01F3F5}' | '\u{01F3F7}'..='\u{01F4FD}' | '\u{01F4FF}'..='\u{01F53D}' |
877 '\u{01F549}'..='\u{01F54E}' | '\u{01F550}'..='\u{01F567}' | '\u{01F56F}' | '\u{01F570}' |
878 '\u{01F573}'..='\u{01F57A}' | '\u{01F587}' | '\u{01F58A}'..='\u{01F58D}' | '\u{01F590}' |
879 '\u{01F595}' | '\u{01F596}' | '\u{01F5A4}' | '\u{01F5A5}' | '\u{01F5A8}' | '\u{01F5B1}' |
880 '\u{01F5B2}' | '\u{01F5BC}' | '\u{01F5C2}'..='\u{01F5C4}' | '\u{01F5D1}'..='\u{01F5D3}' |
881 '\u{01F5DC}'..='\u{01F5DE}' | '\u{01F5E1}' | '\u{01F5E3}' | '\u{01F5E8}' | '\u{01F5EF}' |
882 '\u{01F5F3}' | '\u{01F5FA}'..='\u{01F64F}' | '\u{01F680}'..='\u{01F6C5}' |
883 '\u{01F6CB}'..='\u{01F6D2}' | '\u{01F6D5}'..='\u{01F6D7}' | '\u{01F6DC}'..='\u{01F6E5}' |
884 '\u{01F6E9}' | '\u{01F6EB}' | '\u{01F6EC}' | '\u{01F6F0}' | '\u{01F6F3}'..='\u{01F6FC}' |
885 '\u{01F7E0}'..='\u{01F7EB}' | '\u{01F7F0}' | '\u{01F90C}'..='\u{01F93A}' |
886 '\u{01F93C}'..='\u{01F945}' | '\u{01F947}'..='\u{01F9FF}' | '\u{01FA70}'..='\u{01FA7C}' |
887 '\u{01FA80}'..='\u{01FA88}' | '\u{01FA90}'..='\u{01FABD}' | '\u{01FABF}'..='\u{01FAC5}' |
888 '\u{01FACE}'..='\u{01FADB}' | '\u{01FAE0}'..='\u{01FAE8}' | '\u{01FAF0}'..='\u{01FAF8}'
889 )
890}
891
892// Split sheetnames that look like A1 and R1C1 style cell references into a
893// leading string and a trailing number.
894pub(crate) fn split_cell_reference(sheetname: &str) -> (String, String) {
895 match sheetname.find(|c: char| c.is_ascii_digit()) {
896 Some(position) => (
897 (sheetname[..position]).to_uppercase(),
898 (sheetname[position..]).to_uppercase(),
899 ),
900 None => (String::new(), String::new()),
901 }
902}
903
904// Check that a range string like "A1" or "A1:B3" are valid. This function
905// assumes that the '$' absolute anchor has already been stripped.
906pub(crate) fn is_valid_range(range: &str) -> bool {
907 if range.is_empty() {
908 return false;
909 }
910
911 // The range should start with a letter and end in a number.
912 if !range.starts_with(|c: char| c.is_ascii_uppercase())
913 || !range.ends_with(|c: char| c.is_ascii_digit())
914 {
915 return false;
916 }
917
918 // The range should only include the characters 'A-Z', '0-9' and ':'
919 if !range
920 .chars()
921 .all(|c: char| c.is_ascii_uppercase() || c.is_ascii_digit() || c == ':')
922 {
923 return false;
924 }
925
926 true
927}
928
929/// Check that a worksheet name is valid in Excel.
930///
931/// This function checks if an worksheet name is valid according to the Excel
932/// rules:
933///
934/// - The name is less than 32 characters.
935/// - The name isn't blank.
936/// - The name doesn't contain any of the characters: `[ ] : * ? / \`.
937/// - The name doesn't start or end with an apostrophe.
938///
939/// The worksheet name "History" isn't allowed in English versions of Excel
940/// since it is a reserved name. However it is allowed in some other language
941/// versions so this function doesn't raise it as an error. Overall it is best
942/// to avoid using it.
943///
944/// The rules for worksheet names in Excel are explained in the [Microsoft
945/// Office documentation].
946///
947/// [Microsoft Office documentation]:
948/// https://support.office.com/en-ie/article/rename-a-worksheet-3f1f7148-ee83-404d-8ef0-9ff99fbad1f9
949///
950/// # Parameters
951///
952/// - `name`: The worksheet name. It must follow the Excel rules, shown above.
953///
954/// # Errors
955///
956/// - [`XlsxError::SheetnameCannotBeBlank`] - Worksheet name cannot be blank.
957/// - [`XlsxError::SheetnameLengthExceeded`] - Worksheet name exceeds Excel's
958/// limit of 31 characters.
959/// - [`XlsxError::SheetnameContainsInvalidCharacter`] - Worksheet name cannot
960/// contain invalid characters: `[ ] : * ? / \`
961/// - [`XlsxError::SheetnameStartsOrEndsWithApostrophe`] - Worksheet name cannot
962/// start or end with an apostrophe.
963///
964/// # Examples
965///
966/// The following example demonstrates testing for a valid worksheet name.
967///
968/// ```
969/// # // This code is available in examples/doc_utility_check_sheet_name.rs
970/// #
971/// # use rust_xlsxwriter::{utility, XlsxError};
972/// #
973/// # fn main() -> Result<(), XlsxError> {
974/// // This worksheet name is valid and doesn't raise an error.
975/// utility::check_sheet_name("2030-01-01")?;
976///
977/// // This worksheet name isn't valid due to the forward slashes.
978/// let result = utility::check_sheet_name("2030/01/01");
979///
980/// assert!(matches!(
981/// result,
982/// Err(XlsxError::SheetnameContainsInvalidCharacter(_))
983/// ));
984/// #
985/// # Ok(())
986/// # }
987///
988pub fn check_sheet_name(name: &str) -> Result<(), XlsxError> {
989 let error_message = format!("Invalid Excel worksheet name '{name}'");
990 validate_sheetname(name, &error_message)
991}
992
993// Internal function to validate worksheet name.
994pub(crate) fn validate_sheetname(name: &str, message: &str) -> Result<(), XlsxError> {
995 // Check that the sheet name isn't blank.
996 if name.is_empty() {
997 return Err(XlsxError::SheetnameCannotBeBlank(message.to_string()));
998 }
999
1000 // Check that sheet sheetname is <= 31, an Excel limit.
1001 if name.chars().count() > 31 {
1002 return Err(XlsxError::SheetnameLengthExceeded(message.to_string()));
1003 }
1004
1005 // Check that the sheet name doesn't contain any invalid characters.
1006 if name.contains(['*', '?', ':', '[', ']', '\\', '/']) {
1007 return Err(XlsxError::SheetnameContainsInvalidCharacter(
1008 message.to_string(),
1009 ));
1010 }
1011
1012 // Check that the sheet name doesn't start or end with an apostrophe.
1013 if name.starts_with('\'') || name.ends_with('\'') {
1014 return Err(XlsxError::SheetnameStartsOrEndsWithApostrophe(
1015 message.to_string(),
1016 ));
1017 }
1018
1019 Ok(())
1020}
1021
1022// Internal function to validate VBA code names.
1023pub(crate) fn validate_vba_name(name: &str) -> Result<(), XlsxError> {
1024 // Check that the name isn't blank.
1025 if name.is_empty() {
1026 return Err(XlsxError::VbaNameError(
1027 "VBA name cannot be blank".to_string(),
1028 ));
1029 }
1030
1031 // Check that name is <= 31, an Excel limit.
1032 if name.chars().count() > 31 {
1033 return Err(XlsxError::VbaNameError(
1034 "VBA name exceeds Excel limit of 31 characters: {name}".to_string(),
1035 ));
1036 }
1037
1038 // Check for anything other than letters, numbers, and underscores.
1039 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
1040 return Err(XlsxError::VbaNameError(
1041 "VBA name contains non-word character: {name}".to_string(),
1042 ));
1043 }
1044
1045 // Check that the name starts with a letter.
1046 if !name.chars().next().unwrap().is_alphabetic() {
1047 return Err(XlsxError::VbaNameError(
1048 "VBA name must start with letter character: {name}".to_string(),
1049 ));
1050 }
1051
1052 Ok(())
1053}
1054
1055/// Calculate the width required to auto-fit a string in a cell.
1056///
1057/// The [`Worksheet::autofit()`](crate::Worksheet::autofit) method can be used
1058/// to auto-fit cell data to the optimal column width. However, in some cases
1059/// you may wish to handle auto-fitting yourself and apply additional logic to
1060/// limit the maximum and minimum ranges.
1061///
1062/// The `cell_autofit_width()` function can be used to perform the required
1063/// calculation. It works by estimating the pixel width of a string based on the
1064/// width of each character. It also adds a 7 pixel padding for the cell
1065/// boundary in the same way that Excel does.
1066///
1067/// You can use the calculated width in conjunction with the
1068/// [`Worksheet::set_column_autofit_width()`](crate::Worksheet::set_column_autofit_width)
1069/// method, see the example below.
1070///
1071/// Notes:
1072///
1073/// - The width calculation is based on the default Excel font type of Calibri
1074/// and character size of 11. It will not give correct results for other fonts
1075/// or font sizes.
1076///
1077/// - If you are autofitting a header with an autofilter dropdown you should add
1078/// an additional 6 pixels to account for the dropdown symbol.
1079///
1080/// - When dealing with large data sets you can use `cell_autofit_width()` with
1081/// just 50 or 100 rows of data as a performance optimization . This will
1082/// produce a reasonably accurate autofit for the first visible page of data
1083/// without incurring the performance penalty of calculating widths for
1084/// thousands of non-visible strings.
1085///
1086/// # Parameters
1087///
1088/// - `string`: The string reference to calculate the cell width.
1089///
1090/// # Examples
1091///
1092/// The following example demonstrates "auto"-fitting the the width of a column
1093/// in Excel based on the maximum string width. See also the
1094/// [`Worksheet::autofit()`](crate::Worksheet::autofit) command.
1095///
1096/// ```
1097/// # // This code is available in examples/doc_worksheet_set_column_autofit_width.rs
1098/// #
1099/// # use rust_xlsxwriter::{Workbook, XlsxError, cell_autofit_width};
1100/// #
1101/// # fn main() -> Result<(), XlsxError> {
1102/// # let mut workbook = Workbook::new();
1103/// #
1104/// # // Add a worksheet to the workbook.
1105/// # let worksheet = workbook.add_worksheet();
1106/// #
1107/// // Some string data to write.
1108/// let cities = ["Addis Ababa", "Buenos Aires", "Cairo", "Dhaka"];
1109///
1110/// // Write the strings:
1111/// worksheet.write_column(0, 0, cities)?;
1112///
1113/// // Find the maximum column width in pixels.
1114/// let max_width = cities.iter().map(|s| cell_autofit_width(s)).max().unwrap();
1115///
1116/// // Set the column width as if it was auto-fitted.
1117/// worksheet.set_column_autofit_width(0, max_width)?;
1118/// #
1119/// # workbook.save("worksheet.xlsx")?;
1120/// #
1121/// # Ok(())
1122/// # }
1123/// ```
1124///
1125/// Output file:
1126///
1127/// <img
1128/// src="https://rustxlsxwriter.github.io/images/worksheet_set_column_autofit_width.png">
1129///
1130pub fn cell_autofit_width(string: &str) -> u32 {
1131 let cell_padding = 7;
1132
1133 pixel_width(string) + cell_padding
1134}
1135
1136// Get the pixel width of a string based on character widths taken from Excel.
1137// Non-ascii characters are given a default width of 8 pixels.
1138#[allow(clippy::match_same_arms)]
1139pub(crate) fn pixel_width(string: &str) -> u32 {
1140 let mut length = 0;
1141
1142 // Limit the autofit width to Excel's limit of 1790 pixels.
1143 if string.chars().count() > 233 {
1144 return MAX_AUTOFIT_WIDTH_PIXELS;
1145 }
1146
1147 for char in string.chars() {
1148 match char {
1149 ' ' | '\'' => length += 3,
1150
1151 ',' | '.' | ':' | ';' | 'I' | '`' | 'i' | 'j' | 'l' => length += 4,
1152
1153 '!' | '(' | ')' | '-' | 'J' | '[' | ']' | 'f' | 'r' | 't' | '{' | '}' => length += 5,
1154
1155 '"' | '/' | 'L' | '\\' | 'c' | 's' | 'z' => length += 6,
1156
1157 '#' | '$' | '*' | '+' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
1158 | '<' | '=' | '>' | '?' | 'E' | 'F' | 'S' | 'T' | 'Y' | 'Z' | '^' | '_' | 'a' | 'g'
1159 | 'k' | 'v' | 'x' | 'y' | '|' | '~' => length += 7,
1160
1161 'B' | 'C' | 'K' | 'P' | 'R' | 'X' | 'b' | 'd' | 'e' | 'h' | 'n' | 'o' | 'p' | 'q'
1162 | 'u' => length += 8,
1163
1164 'A' | 'D' | 'G' | 'H' | 'U' | 'V' => length += 9,
1165
1166 '&' | 'N' | 'O' | 'Q' => length += 10,
1167
1168 '%' | 'w' => length += 11,
1169
1170 'M' | 'm' => length += 12,
1171
1172 '@' | 'W' => length += 13,
1173
1174 _ => length += 8,
1175 }
1176 }
1177
1178 std::cmp::min(length, MAX_AUTOFIT_WIDTH_PIXELS)
1179}
1180
1181// Hash a worksheet password. Based on the algorithm in ECMA-376-4:2016, Office
1182// Open XML File Formats — Transitional Migration Features, Additional
1183// attributes for workbookProtection element (Part 1, §18.2.29).
1184pub(crate) fn hash_password(password: &str) -> u16 {
1185 let mut hash: u16 = 0;
1186 let length = password.len() as u16;
1187
1188 if password.is_empty() {
1189 return 0;
1190 }
1191
1192 for byte in password.as_bytes().iter().rev() {
1193 hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7FFF);
1194 hash ^= u16::from(*byte);
1195 }
1196
1197 hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7FFF);
1198 hash ^= length;
1199 hash ^= 0xCE4B;
1200
1201 hash
1202}
1203
1204// Clone and strip the leading '=' from formulas, if present.
1205pub(crate) fn formula_to_string(formula: &str) -> String {
1206 let mut formula = formula.to_string();
1207
1208 if formula.starts_with('=') {
1209 formula.remove(0);
1210 }
1211
1212 formula
1213}
1214
1215// Get default font metrics for a default column width.
1216//
1217// This function returns the font metrics (max_digit_width, padding,
1218// max_col_width) based on the column pixel width for a default font.
1219//
1220// To add support for additional fonts and sizes please open a GitHub request
1221// with an empty sample workbook with one worksheet.
1222//
1223pub(crate) fn default_column_metrics(width: u32) -> Option<(u32, u32, u32)> {
1224 match width {
1225 56 => Some((6, 5, 1533)),
1226 64 => Some((7, 5, 1790)),
1227 72 => Some((8, 5, 2043)),
1228 80 => Some((9, 7, 2300)),
1229 96 => Some((11, 7, 2810)),
1230 104 => Some((12, 7, 3065)),
1231 120 => Some((13, 9, 3323)),
1232 _ => None,
1233 }
1234}
1235
1236// Trait to convert bool to XML "0" or "1".
1237pub(crate) trait ToXmlBoolean {
1238 fn to_xml_bool(self) -> String;
1239}
1240
1241impl ToXmlBoolean for bool {
1242 fn to_xml_bool(self) -> String {
1243 u8::from(self).to_string()
1244 }
1245}