diff --git a/include/xlnt/worksheet/worksheet.hpp b/include/xlnt/worksheet/worksheet.hpp index 856c1a41..a4cf9e07 100644 --- a/include/xlnt/worksheet/worksheet.hpp +++ b/include/xlnt/worksheet/worksheet.hpp @@ -652,37 +652,52 @@ public: /// /// Sets rows to repeat at top during printing. /// - void print_title_rows(row_t first_row, row_t last_row); + void print_title_rows(row_t start, row_t end); /// - /// Sets rows to repeat at top during printing. + /// Get rows to repeat at top during printing. /// - void print_title_rows(row_t last_row); + optional> print_title_rows() const; /// /// Sets columns to repeat at left during printing. /// - void print_title_cols(column_t first_column, column_t last_column); + void print_title_cols(column_t start, column_t end); + + /// + /// Get columns to repeat at left during printing. + /// + optional> print_title_cols() const; /// - /// Sets columns to repeat at left during printing. + /// Returns true if the sheet has print titles defined. /// - void print_title_cols(column_t last_column); + bool has_print_titles() const; /// - /// Returns a string representation of the defined print titles. + /// Remove all print titles. /// - std::string print_titles() const; + void clear_print_titles(); /// /// Sets the print area of this sheet to print_area. /// void print_area(const std::string &print_area); + /// + /// Clear the print area of this sheet. + /// + void clear_print_area(); + /// /// Returns the print area defined for this sheet. /// range_reference print_area() const; + + /// + /// Returns true if the print area is defined for this sheet. + /// + bool has_print_area() const; /// /// Returns true if this sheet has any number of views defined. diff --git a/source/detail/implementations/worksheet_impl.hpp b/source/detail/implementations/worksheet_impl.hpp index 8c776a32..7afcfb1d 100644 --- a/source/detail/implementations/worksheet_impl.hpp +++ b/source/detail/implementations/worksheet_impl.hpp @@ -143,9 +143,8 @@ struct worksheet_impl optional phonetic_properties_; optional header_footer_; - std::string print_title_cols_; - std::string print_title_rows_; - + optional> print_title_cols_; + optional> print_title_rows_; optional print_area_; std::vector views_; diff --git a/source/detail/serialization/defined_name.hpp b/source/detail/serialization/defined_name.hpp new file mode 100644 index 00000000..3aeb9132 --- /dev/null +++ b/source/detail/serialization/defined_name.hpp @@ -0,0 +1,40 @@ +// Copyright (c) 2014-2021 Thomas Fussell +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE +// +// @license: http://www.opensource.org/licenses/mit-license.php +// @author: see AUTHORS file + +#pragma once + +#include + +namespace xlnt { +namespace detail { + +struct defined_name +{ + std::string name; + std::size_t sheet_id; + bool hidden; + std::string value; +}; + +} // namespace detail +} // namespace xlnt diff --git a/source/detail/serialization/xlsx_consumer.cpp b/source/detail/serialization/xlsx_consumer.cpp index 7959372a..88272872 100644 --- a/source/detail/serialization/xlsx_consumer.cpp +++ b/source/detail/serialization/xlsx_consumer.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -448,6 +449,70 @@ void xlsx_consumer::read_worksheet(const std::string &rel_id) } } +void read_defined_names(worksheet ws, std::vector defined_names) +{ + for (auto &name : defined_names) + { + if (name.sheet_id != ws.id() - 1) + { + continue; + } + + if (name.name == "_xlnm.Print_Titles") + { + // Basic print titles parser + // A print title definition looks like "'Sheet3'!$B:$E,'Sheet3'!$2:$4" + // There are three cases: columns only, rows only, and both (separated by a comma). + // For this reason, we loop up to two times parsing each component. + // Titles may be quoted (with single quotes) or unquoted. We ignore them for now anyways. + // References are always absolute. + // Move this into a separate function if it needs to be used in other places. + auto i = std::size_t(0); + for (auto count = 0; count < 2; count++) + { + // Split into components based on "!", ":", and "," characters + auto j = i; + i = name.value.find("!", j); + auto title = name.value.substr(j, i - j); + j = i + 2; // skip "!$" + i = name.value.find(":", j); + auto from = name.value.substr(j, i - j); + j = i + 2; // skip ":$" + i = name.value.find(",", j); + auto to = name.value.substr(j, i - j); + + // Apply to the worksheet + if (isalpha(from.front())) // alpha=>columns + { + ws.print_title_cols(from, to); + } + else // numeric=>rows + { + ws.print_title_rows(std::stoul(from), std::stoul(to)); + } + + // Check for end condition + if (i == std::string::npos) + { + break; + } + + i++; // skip "," for next iteration + } + } + else if (name.name == "_xlnm._FilterDatabase") + { + auto i = name.value.find("!"); + ws.auto_filter(name.value.substr(i + 1)); + } + else if (name.name == "_xlnm.Print_Area") + { + auto i = name.value.find("!"); + ws.print_area(name.value.substr(i + 1)); + } + } +} + std::string xlsx_consumer::read_worksheet_begin(const std::string &rel_id) { if (streaming_ && streaming_cell_ == nullptr) @@ -468,6 +533,8 @@ std::string xlsx_consumer::read_worksheet_begin(const std::string &rel_id) expect_start_element(qn("spreadsheetml", "worksheet"), xml::content::complex); // CT_Worksheet skip_attributes({qn("mc", "Ignorable")}); + + read_defined_names(ws, defined_names_); while (in_element(qn("spreadsheetml", "worksheet"))) { @@ -2015,7 +2082,24 @@ void xlsx_consumer::read_office_document(const std::string &content_type) // CT_ } else if (current_workbook_element == qn("workbook", "definedNames")) // CT_DefinedNames 0-1 { - skip_remaining_content(current_workbook_element); + while (in_element(qn("workbook", "definedNames"))) + { + expect_start_element(qn("spreadsheetml", "definedName"), xml::content::mixed); + + defined_name name; + name.name = parser().attribute("name"); + name.sheet_id = parser().attribute("localSheetId"); + name.hidden = false; + if (parser().attribute_present("hidden")) + { + name.hidden = is_true(parser().attribute("hidden")); + } + parser().attribute_map(); // skip remaining attributes + name.value = read_text(); + defined_names_.push_back(name); + + expect_end_element(qn("spreadsheetml", "definedName")); + } } else if (current_workbook_element == qn("workbook", "calcPr")) // CT_CalcPr 0-1 { @@ -2123,14 +2207,14 @@ void xlsx_consumer::read_office_document(const std::string &content_type) // CT_ target_.d_->sheet_title_rel_id_map_.end(), [&](const std::pair &p) { return p.second == worksheet_rel.id(); - }) - ->first; + })->first; auto id = sheet_title_id_map_[title]; auto index = sheet_title_index_map_[title]; auto insertion_iter = target_.d_->worksheets_.begin(); - while (insertion_iter != target_.d_->worksheets_.end() && sheet_title_index_map_[insertion_iter->title_] < index) + while (insertion_iter != target_.d_->worksheets_.end() + && sheet_title_index_map_[insertion_iter->title_] < index) { ++insertion_iter; } diff --git a/source/detail/serialization/xlsx_consumer.hpp b/source/detail/serialization/xlsx_consumer.hpp index 914b76e9..182c9b36 100644 --- a/source/detail/serialization/xlsx_consumer.hpp +++ b/source/detail/serialization/xlsx_consumer.hpp @@ -56,6 +56,7 @@ namespace detail { class izstream; struct cell_impl; +struct defined_name; struct worksheet_impl; /// @@ -424,6 +425,8 @@ private: detail::worksheet_impl *current_worksheet_; number_serialiser converter_; + + std::vector defined_names_; }; } // namespace detail diff --git a/source/detail/serialization/xlsx_producer.cpp b/source/detail/serialization/xlsx_producer.cpp index 82df530a..6f6e26cc 100644 --- a/source/detail/serialization/xlsx_producer.cpp +++ b/source/detail/serialization/xlsx_producer.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -432,7 +433,7 @@ void xlsx_producer::write_custom_properties(const relationship & /*rel*/) void xlsx_producer::write_workbook(const relationship &rel) { std::size_t num_visible = 0; - bool any_defined_names = false; + std::vector defined_names; for (auto ws : source_) { @@ -440,10 +441,54 @@ void xlsx_producer::write_workbook(const relationship &rel) { num_visible++; } - + + auto title_ref = "'" + ws.title() + "'!"; + if (ws.has_auto_filter()) { - any_defined_names = true; + defined_name name; + name.sheet_id = ws.id(); + name.name = "_xlnm._FilterDatabase"; + name.hidden = true; + name.value = title_ref + range_reference::make_absolute(ws.auto_filter()).to_string(); + defined_names.push_back(name); + } + + if (ws.has_print_area()) + { + defined_name name; + name.sheet_id = ws.id(); + name.name = "_xlnm.Print_Area"; + name.hidden = false; + name.value = title_ref + range_reference::make_absolute(ws.print_area()).to_string(); + defined_names.push_back(name); + } + + if (ws.has_print_titles()) + { + defined_name name; + name.sheet_id = ws.id(); + name.name = "_xlnm.Print_Titles"; + name.hidden = false; + + auto cols = ws.print_title_cols(); + if (cols.is_set()) + { + name.value = title_ref + "$" + cols.get().first.column_string() + + ":" + "$" + cols.get().second.column_string(); + } + auto rows = ws.print_title_rows(); + if (rows.is_set()) + { + if (!name.value.empty()) + { + name.value.push_back(','); + } + name.value += title_ref + "$" + std::to_string(rows.get().first) + + ":" + "$" + std::to_string(rows.get().second); + } + + defined_names.push_back(name); } } @@ -612,16 +657,21 @@ void xlsx_producer::write_workbook(const relationship &rel) write_end_element(xmlns, "sheets"); - if (any_defined_names) + if (!defined_names.empty()) { write_start_element(xmlns, "definedNames"); - /* - write_attribute("name", "_xlnm._FilterDatabase"); - write_attribute("hidden", write_bool(true)); - write_attribute("localSheetId", "0"); - write_characters("'" + ws.title() + "'!" + - range_reference::make_absolute(ws.auto_filter()).to_string()); - */ + for (auto name : defined_names) + { + write_start_element(xmlns, "definedName"); + write_attribute("name", name.name); + if (name.hidden) + { + write_attribute("hidden", write_bool(true)); + } + write_attribute("localSheetId", std::to_string(name.sheet_id - 1)); // 0-indexed for some reason + write_characters(name.value); + write_end_element(xmlns, "definedName"); + } write_end_element(xmlns, "definedNames"); } diff --git a/source/worksheet/worksheet.cpp b/source/worksheet/worksheet.cpp index d7e7d9ef..bd64ac37 100644 --- a/source/worksheet/worksheet.cpp +++ b/source/worksheet/worksheet.cpp @@ -1146,40 +1146,35 @@ worksheet::const_iterator worksheet::end() const return cend(); } -void worksheet::print_title_rows(row_t last_row) +void worksheet::print_title_rows(row_t start, row_t end) { - print_title_rows(1, last_row); + d_->print_title_rows_ = std::make_pair(start, end); } -void worksheet::print_title_rows(row_t first_row, row_t last_row) +optional> worksheet::print_title_rows() const { - d_->print_title_rows_ = std::to_string(first_row) + ":" + std::to_string(last_row); + return d_->print_title_rows_; } -void worksheet::print_title_cols(column_t last_column) +void worksheet::print_title_cols(column_t start, column_t end) { - print_title_cols(1, last_column); + d_->print_title_cols_ = std::make_pair(start, end); } -void worksheet::print_title_cols(column_t first_column, column_t last_column) +optional> worksheet::print_title_cols() const { - d_->print_title_cols_ = first_column.column_string() + ":" + last_column.column_string(); + return d_->print_title_cols_; } -std::string worksheet::print_titles() const +bool worksheet::has_print_titles() const { - if (!d_->print_title_rows_.empty() && !d_->print_title_cols_.empty()) - { - return d_->title_ + "!" + d_->print_title_rows_ + "," + d_->title_ + "!" + d_->print_title_cols_; - } - else if (!d_->print_title_cols_.empty()) - { - return d_->title_ + "!" + d_->print_title_cols_; - } - else - { - return d_->title_ + "!" + d_->print_title_rows_; - } + return d_->print_title_cols_.is_set() || d_->print_title_rows_.is_set(); +} + +void worksheet::clear_print_titles() +{ + d_->print_title_rows_.clear(); + d_->print_title_cols_.clear(); } void worksheet::print_area(const std::string &print_area) @@ -1192,6 +1187,16 @@ range_reference worksheet::print_area() const return d_->print_area_.get(); } +bool worksheet::has_print_area() const +{ + return d_->print_area_.is_set(); +} + +void worksheet::clear_print_area() +{ + return d_->print_area_.clear(); +} + bool worksheet::has_view() const { return !d_->views_.empty(); diff --git a/tests/data/19_defined_names.xlsx b/tests/data/19_defined_names.xlsx new file mode 100644 index 00000000..4c430ba3 Binary files /dev/null and b/tests/data/19_defined_names.xlsx differ diff --git a/tests/utils/datetime_test_suite.cpp b/tests/utils/datetime_test_suite.cpp index 1007b918..36c6649e 100644 --- a/tests/utils/datetime_test_suite.cpp +++ b/tests/utils/datetime_test_suite.cpp @@ -117,9 +117,11 @@ public: void test_weekday() { - xlnt_assert_equals(xlnt::date(2000, 1, 1).weekday(), 6); - xlnt_assert_equals(xlnt::date(2016, 7, 15).weekday(), 5); - xlnt_assert_equals(xlnt::date(2018, 10, 29).weekday(), 1); + xlnt_assert_equals(xlnt::date(2000, 1, 1).weekday(), 6); // January 1st 2000 was a Saturday + xlnt_assert_equals(xlnt::date(2016, 7, 15).weekday(), 5); // July 15th 2016 was a Friday + xlnt_assert_equals(xlnt::date(2018, 10, 29).weekday(), 1); // October 29th 2018 was Monday + xlnt_assert_equals(xlnt::date(1970, 1, 1).weekday(), 4); // January 1st 1970 was a Thursday } }; + static datetime_test_suite x; diff --git a/tests/worksheet/worksheet_test_suite.cpp b/tests/worksheet/worksheet_test_suite.cpp index 1ca50caa..9c1097a0 100644 --- a/tests/worksheet/worksheet_test_suite.cpp +++ b/tests/worksheet/worksheet_test_suite.cpp @@ -52,7 +52,6 @@ public: register_test(test_no_cols); register_test(test_one_cell); register_test(test_cols); - register_test(test_auto_filter); register_test(test_getitem); register_test(test_setitem); register_test(test_getslice); @@ -62,9 +61,7 @@ public: register_test(test_merge_range_string); register_test(test_unmerge_bad); register_test(test_unmerge_range_string); - register_test(test_print_titles_old); - register_test(test_print_titles_new); - register_test(test_print_area); + register_test(test_defined_names); register_test(test_freeze_panes_horiz); register_test(test_freeze_panes_vert); register_test(test_freeze_panes_both); @@ -303,21 +300,6 @@ public: xlnt_assert_equals(cols[2][8].value(), "last"); } - void test_auto_filter() - { - xlnt::workbook wb; - auto ws = wb.active_sheet(); - - ws.auto_filter(ws.range("a1:f1")); - xlnt_assert_equals(ws.auto_filter(), "A1:F1"); - - ws.clear_auto_filter(); - xlnt_assert(!ws.has_auto_filter()); - - ws.auto_filter("c1:g9"); - xlnt_assert_equals(ws.auto_filter(), "C1:G9"); - } - void test_getitem() { xlnt::workbook wb; @@ -416,43 +398,53 @@ public: xlnt_assert_equals(ws.merged_ranges().size(), 0); } - void test_print_titles_old() + void test_defined_names() { xlnt::workbook wb; + wb.load(path_helper::test_file("19_defined_names.xlsx")); + + auto ws1 = wb.sheet_by_index(0); - auto ws = wb.active_sheet(); - ws.print_title_rows(3); - xlnt_assert_equals(ws.print_titles(), "Sheet1!1:3"); + xlnt_assert(!ws1.has_print_area()); - auto ws2 = wb.create_sheet(); - ws2.print_title_cols(4); - xlnt_assert_equals(ws2.print_titles(), "Sheet2!A:D"); - } + xlnt_assert(ws1.has_auto_filter()); + xlnt_assert_equals(ws1.auto_filter().to_string(), "A1:A6"); - void test_print_titles_new() - { - xlnt::workbook wb; + xlnt_assert(ws1.has_print_titles()); + xlnt_assert(!ws1.print_title_cols().is_set()); + xlnt_assert(ws1.print_title_rows().is_set()); + xlnt_assert_equals(ws1.print_title_rows().get().first, 1); + xlnt_assert_equals(ws1.print_title_rows().get().second, 3); + + auto ws2 = wb.sheet_by_index(1); - auto ws = wb.active_sheet(); - ws.print_title_rows(4); - xlnt_assert_equals(ws.print_titles(), "Sheet1!1:4"); + xlnt_assert(ws2.has_print_area()); + xlnt_assert_equals(ws2.print_area().to_string(), "$B$4:$B$4"); - auto ws2 = wb.create_sheet(); - ws2.print_title_cols("F"); - xlnt_assert_equals(ws2.print_titles(), "Sheet2!A:F"); + xlnt_assert(!ws2.has_auto_filter()); - auto ws3 = wb.create_sheet(); - ws3.print_title_rows(2, 3); - ws3.print_title_cols("C", "D"); - xlnt_assert_equals(ws3.print_titles(), "Sheet3!2:3,Sheet3!C:D"); - } + xlnt_assert(ws2.has_print_titles()); + xlnt_assert(!ws2.print_title_rows().is_set()); + xlnt_assert(ws2.print_title_cols().is_set()); + xlnt_assert_equals(ws2.print_title_cols().get().first, "A"); + xlnt_assert_equals(ws2.print_title_cols().get().second, "D"); + + auto ws3 = wb.sheet_by_index(2); - void test_print_area() - { - xlnt::workbook wb; - auto ws = wb.active_sheet(); - ws.print_area("A1:F5"); - xlnt_assert_equals(ws.print_area(), "$A$1:$F$5"); + xlnt_assert(ws3.has_print_area()); + xlnt_assert_equals(ws3.print_area().to_string(), "$B$2:$E$4"); + + xlnt_assert(!ws3.has_auto_filter()); + + xlnt_assert(ws3.has_print_titles()); + xlnt_assert(ws3.print_title_rows().is_set()); + xlnt_assert(ws3.print_title_cols().is_set()); + xlnt_assert_equals(ws3.print_title_cols().get().first, "B"); + xlnt_assert_equals(ws3.print_title_cols().get().second, "E"); + xlnt_assert_equals(ws3.print_title_rows().get().first, 2); + xlnt_assert_equals(ws3.print_title_rows().get().second, 4); + + wb.save("titles1.xlsx"); } void test_freeze_panes_horiz()