Merge pull request #647 from tfussell/shared-array-formulas

Implement shared and array formulas
This commit is contained in:
Thomas Fussell 2022-08-13 13:38:01 -05:00 committed by GitHub
commit 1945691bb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 69 deletions

View File

@ -115,6 +115,11 @@ public:
/// </summary>
std::string to_string() const;
/// <summary>
/// Returns true if the given cell reference is within the bounds of this range reference.
/// </summary>
bool contains(const cell_reference &ref) const;
/// <summary>
/// Returns true if this range is equivalent to the other range.
/// </summary>

View File

@ -86,6 +86,7 @@
#include <xlnt/worksheet/range_reference.hpp>
#include <xlnt/worksheet/row_properties.hpp>
#include <xlnt/worksheet/selection.hpp>
#include <xlnt/worksheet/sheet_format_properties.hpp>
#include <xlnt/worksheet/sheet_protection.hpp>
#include <xlnt/worksheet/sheet_view.hpp>
#include <xlnt/worksheet/worksheet.hpp>

View File

@ -165,7 +165,7 @@ xlnt::cell_type type_from_string(const std::string &str)
return xlnt::cell::type::shared_string;
}
xlnt::detail::Cell parse_cell(xlnt::row_t row_arg, xml::parser *parser)
xlnt::detail::Cell parse_cell(xlnt::row_t row_arg, xml::parser *parser, std::unordered_map<std::string, std::string> &array_formulae, std::unordered_map<int, std::string> &shared_formulae)
{
xlnt::detail::Cell c;
for (auto &attr : parser->attribute_map())
@ -202,6 +202,16 @@ xlnt::detail::Cell parse_cell(xlnt::row_t row_arg, xml::parser *parser)
switch (e)
{
case xml::parser::start_element: {
if (string_equal(parser->name(), "f") && parser->attribute_present("t"))
{
// Skip shared formulas with a ref attribute because it indicates that this
// is the master cell which will be handled in the xml::parser::characters case.
if (parser->attribute("t") == "shared" && !parser->attribute_present("ref"))
{
auto shared_index = parser->attribute<int>("si");
c.formula_string = shared_formulae[shared_index];
}
}
++level;
break;
}
@ -223,6 +233,21 @@ xlnt::detail::Cell parse_cell(xlnt::row_t row_arg, xml::parser *parser)
else if (string_equal(parser->name(), "f"))
{
c.formula_string += std::move(parser->value());
if (parser->attribute_present("t"))
{
auto formula_ref = parser->attribute("ref");
auto formula_type = parser->attribute("t");
if (formula_type == "shared")
{
auto shared_index = parser->attribute<int>("si");
shared_formulae[shared_index] = c.formula_string;
}
else if (formula_type == "array")
{
array_formulae[formula_ref] = c.formula_string;
}
}
}
}
else if (level == 3)
@ -251,7 +276,7 @@ xlnt::detail::Cell parse_cell(xlnt::row_t row_arg, xml::parser *parser)
}
// <row> inside <sheetData> element
std::pair<xlnt::row_properties, int> parse_row(xml::parser *parser, xlnt::detail::number_serialiser &converter, std::vector<xlnt::detail::Cell> &parsed_cells)
std::pair<xlnt::row_properties, int> parse_row(xml::parser *parser, xlnt::detail::number_serialiser &converter, std::vector<xlnt::detail::Cell> &parsed_cells, std::unordered_map<std::string, std::string> &array_formulae, std::unordered_map<int, std::string> &shared_formulae)
{
std::pair<xlnt::row_properties, int> props;
for (auto &attr : parser->attribute_map())
@ -301,7 +326,7 @@ std::pair<xlnt::row_properties, int> parse_row(xml::parser *parser, xlnt::detail
switch (e)
{
case xml::parser::start_element: {
parsed_cells.push_back(parse_cell(static_cast<xlnt::row_t>(props.second), parser));
parsed_cells.push_back(parse_cell(static_cast<xlnt::row_t>(props.second), parser, array_formulae, shared_formulae));
break;
}
case xml::parser::end_element: {
@ -326,7 +351,7 @@ std::pair<xlnt::row_properties, int> parse_row(xml::parser *parser, xlnt::detail
}
// <sheetData> inside <worksheet> element
Sheet_Data parse_sheet_data(xml::parser *parser, xlnt::detail::number_serialiser &converter)
Sheet_Data parse_sheet_data(xml::parser *parser, xlnt::detail::number_serialiser &converter, std::unordered_map<std::string, std::string> &array_formulae, std::unordered_map<int, std::string> &shared_formulae)
{
Sheet_Data sheet_data;
int level = 1; // nesting level
@ -339,7 +364,7 @@ Sheet_Data parse_sheet_data(xml::parser *parser, xlnt::detail::number_serialiser
switch (e)
{
case xml::parser::start_element: {
sheet_data.parsed_rows.push_back(parse_row(parser, converter, sheet_data.parsed_cells));
sheet_data.parsed_rows.push_back(parse_row(parser, converter, sheet_data.parsed_cells, array_formulae, shared_formulae));
break;
}
case xml::parser::end_element: {
@ -429,6 +454,9 @@ std::string xlsx_consumer::read_worksheet_begin(const std::string &rel_id)
{
streaming_cell_.reset(new detail::cell_impl());
}
array_formulae_.clear();
shared_formulae_.clear();
auto title = std::find_if(target_.d_->sheet_title_rel_id_map_.begin(),
target_.d_->sheet_title_rel_id_map_.end(),
@ -742,7 +770,8 @@ void xlsx_consumer::read_worksheet_sheetdata()
{
return;
}
Sheet_Data ws_data = parse_sheet_data(parser_, converter_);
auto ws_data = parse_sheet_data(parser_, converter_, array_formulae_, shared_formulae_);
// NOTE: parse->construct are seperated here and could easily be threaded
// with a SPSC queue for what is likely to be an easy performance win
for (auto &row : ws_data.parsed_rows)
@ -803,6 +832,8 @@ void xlsx_consumer::read_worksheet_sheetdata()
}
}
stack_.pop_back();
}
worksheet xlsx_consumer::read_worksheet_end(const std::string &rel_id)
@ -1258,6 +1289,17 @@ worksheet xlsx_consumer::read_worksheet_end(const std::string &rel_id)
manifest.relationship(sheet_path,
relationship_type::printer_settings)});
}
for (auto array_formula : array_formulae_)
{
for (auto row : ws.range(array_formula.first))
{
for (auto cell : row)
{
cell.formula(array_formula.second);
}
}
}
return ws;
}
@ -1356,10 +1398,7 @@ bool xlsx_consumer::has_cell()
auto has_value = false;
auto value_string = std::string();
auto has_formula = false;
auto has_shared_formula = false;
auto formula_value_string = std::string();
auto formula_string = std::string();
while (in_element(qn("spreadsheetml", "c")))
{
@ -1372,17 +1411,56 @@ bool xlsx_consumer::has_cell()
}
else if (current_element == qn("spreadsheetml", "f")) // CT_CellFormula
{
has_formula = true;
auto has_shared_formula = false;
auto has_array_formula = false;
auto is_master_cell = false;
auto shared_formula_index = 0;
auto formula_range = range_reference();
if (parser().attribute_present("t"))
{
has_shared_formula = parser().attribute("t") == "shared";
auto formula_type = parser().attribute("t");
if (formula_type == "shared")
{
has_shared_formula = true;
shared_formula_index = parser().attribute<int>("si");
if (parser().attribute_present("ref"))
{
is_master_cell = true;
}
}
else if (formula_type == "array")
{
has_array_formula = true;
formula_range = range_reference(parser().attribute("ref"));
is_master_cell = true;
}
}
skip_attributes({"aca", "ref", "dt2D", "dtr", "del1",
"del2", "r1", "r2", "ca", "si", "bx"});
skip_attributes({"aca", "dt2D", "dtr", "del1", "del2", "r1",
"r2", "ca", "bx"});
formula_value_string = read_text();
formula_string = read_text();
if (is_master_cell)
{
if (has_shared_formula)
{
shared_formulae_[shared_formula_index] = formula_string;
}
else if (has_array_formula)
{
array_formulae_[formula_range.to_string()] = formula_string;
}
}
else if (has_shared_formula)
{
auto shared_formula = shared_formulae_.find(shared_formula_index);
if (shared_formula != shared_formulae_.end())
{
formula_string = shared_formula->second;
}
}
}
else if (current_element == qn("spreadsheetml", "is")) // CT_Rst
{
@ -1401,9 +1479,9 @@ bool xlsx_consumer::has_cell()
expect_end_element(qn("spreadsheetml", "c"));
if (has_formula && !has_shared_formula)
if (!formula_string.empty())
{
cell.formula(formula_value_string);
cell.formula(formula_string);
}
if (has_value)

View File

@ -45,6 +45,7 @@ class manifest;
template<typename T>
class optional;
class path;
class range_reference;
class relationship;
class streaming_workbook_reader;
class variant;
@ -417,6 +418,9 @@ private:
bool streaming_ = false;
std::unique_ptr<detail::cell_impl> streaming_cell_;
std::unordered_map<int, std::string> shared_formulae_;
std::unordered_map<std::string, std::string> array_formulae_;
detail::worksheet_impl *current_worksheet_;
number_serialiser converter_;

View File

@ -132,12 +132,9 @@ const cell_vector range::vector(std::size_t vector_index) const
return cell_vector(ws_, cursor, ref_, order_, skip_null_, false);
}
bool range::contains(const cell_reference &ref)
bool range::contains(const cell_reference &cell_ref)
{
return ref_.top_left().column_index() <= ref.column_index()
&& ref_.bottom_right().column_index() >= ref.column_index()
&& ref_.top_left().row() <= ref.row()
&& ref_.bottom_right().row() >= ref.row();
return ref_.contains(cell_ref);
}
range range::alignment(const xlnt::alignment &new_alignment)

View File

@ -145,6 +145,14 @@ cell_reference range_reference::bottom_right() const
return bottom_right_;
}
bool range_reference::contains(const cell_reference &ref) const
{
return top_left_.column_index() <= ref.column_index()
&& bottom_right_.column_index() >= ref.column_index()
&& top_left_.row() <= ref.row()
&& bottom_right_.row() >= ref.row();
}
bool range_reference::operator==(const std::string &reference_string) const
{
return *this == range_reference(reference_string);

BIN
tests/data/18_formulae.xlsx Normal file

Binary file not shown.

View File

@ -23,31 +23,7 @@
#include <iostream>
#include <xlnt/cell/cell.hpp>
#include <xlnt/cell/comment.hpp>
#include <xlnt/cell/hyperlink.hpp>
#include <xlnt/styles/border.hpp>
#include <xlnt/styles/fill.hpp>
#include <xlnt/styles/font.hpp>
#include <xlnt/styles/format.hpp>
#include <xlnt/styles/number_format.hpp>
#include <xlnt/styles/style.hpp>
#include <xlnt/utils/date.hpp>
#include <xlnt/utils/datetime.hpp>
#include <xlnt/utils/time.hpp>
#include <xlnt/utils/timedelta.hpp>
#include <xlnt/utils/variant.hpp>
#include <xlnt/workbook/metadata_property.hpp>
#include <xlnt/workbook/streaming_workbook_reader.hpp>
#include <xlnt/workbook/streaming_workbook_writer.hpp>
#include <xlnt/workbook/workbook.hpp>
#include <xlnt/worksheet/column_properties.hpp>
#include <xlnt/worksheet/header_footer.hpp>
#include <xlnt/worksheet/row_properties.hpp>
#include <xlnt/worksheet/sheet_format_properties.hpp>
#include <xlnt/worksheet/worksheet.hpp>
#include <detail/cryptography/xlsx_crypto_consumer.hpp>
#include <detail/serialization/vector_streambuf.hpp>
#include <xlnt/xlnt.hpp>
#include <helpers/path_helper.hpp>
#include <helpers/temporary_file.hpp>
#include <helpers/test_suite.hpp>
@ -62,7 +38,7 @@ public:
register_test(test_produce_simple_excel);
register_test(test_save_after_sheet_deletion);
register_test(test_write_comments_hyperlinks_formulae);
register_test(test_save_after_clear_all_formulae);
register_test(test_save_after_clear_formula);
register_test(test_load_non_xlsx);
register_test(test_decrypt_agile);
register_test(test_decrypt_libre_office);
@ -304,21 +280,20 @@ public:
xlnt_assert(workbook_matches_file(wb, path));
}
void test_save_after_clear_all_formulae()
void test_save_after_clear_formula()
{
xlnt::workbook wb;
const auto path = path_helper::test_file("10_comments_hyperlinks_formulae.xlsx");
const auto path = path_helper::test_file("18_formulae.xlsx");
wb.load(path);
auto ws1 = wb.sheet_by_index(0);
xlnt_assert(ws1.cell("C1").has_formula());
xlnt_assert_equals(ws1.cell("C1").formula(), "CONCATENATE(C2,C3)");
ws1.cell("C1").clear_formula();
auto ws2 = wb.sheet_by_index(1);
xlnt_assert(ws2.cell("C1").has_formula());
xlnt_assert_equals(ws2.cell("C1").formula(), "C2*C3");
ws2.cell("C1").clear_formula();
for (auto row : ws1)
{
for (auto cell : row)
{
cell.clear_formula();
}
}
wb.save("clear_formulae.xlsx");
}
@ -425,20 +400,41 @@ public:
void test_read_formulae()
{
xlnt::workbook wb;
const auto path = path_helper::test_file("10_comments_hyperlinks_formulae.xlsx");
const auto path = path_helper::test_file("18_formulae.xlsx");
wb.load(path);
auto ws1 = wb.sheet_by_index(0);
xlnt_assert(ws1.cell("C1").has_formula());
xlnt_assert_equals(ws1.cell("C1").formula(), "CONCATENATE(C2,C3)");
xlnt_assert_equals(ws1.cell("C2").value<std::string>(), "a");
xlnt_assert_equals(ws1.cell("C3").value<std::string>(), "b");
// test has_formula
// A1:B3 are plain text cells
// C1:G3,I2,F4 have formulae
for (auto row = 1; row < 4; row++)
{
for (auto column = 1; column < 8; column++)
{
if (column < 3)
{
xlnt_assert(!ws1.cell(column, row).has_formula());
}
else
{
xlnt_assert(ws1.cell(column, row).has_formula());
}
}
}
auto ws2 = wb.sheet_by_index(1);
xlnt_assert(ws2.cell("C1").has_formula());
xlnt_assert_equals(ws2.cell("C1").formula(), "C2*C3");
xlnt_assert_equals(ws2.cell("C2").value<int>(), 2);
xlnt_assert_equals(ws2.cell("C3").value<int>(), 3);
xlnt_assert(ws1.cell("I2").has_formula());
xlnt_assert(ws1.cell("F4").has_formula());
xlnt_assert(!ws1.cell("C9").has_formula()); // empty cell
xlnt_assert(!ws1.cell("F5").has_formula()); // text cell
xlnt_assert_equals(ws1.cell("C1").formula(), "B1^2"); // basic math with reference
xlnt_assert_equals(ws1.cell("D1").formula(), "CONCATENATE(A1,B1)"); // concat with ref
xlnt_assert_equals(ws1.cell("E1").formula(), "CONCATENATE($C$1,$D$1)"); // concat with absolute ref
xlnt_assert_equals(ws1.cell("F1").formula(), "1+1"); // basic math
xlnt_assert_equals(ws1.cell("G1").formula(), "PI()"); // constant
xlnt_assert_equals(ws1.cell("I2").formula(), "COS(C2)+IMAGINARY(SIN(B2))"); // fancy math
}
void test_read_headers_and_footers()
@ -657,7 +653,7 @@ public:
{
xlnt_assert(round_trip_matches_rw(path_helper::test_file("13_custom_heights_widths.xlsx")));
}
void test_round_trip_rw_encrypted_agile()
{
xlnt_assert(round_trip_matches_rw(path_helper::test_file("5_encrypted_agile.xlsx"), "secret"));