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> /// </summary>
std::string to_string() const; 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> /// <summary>
/// Returns true if this range is equivalent to the other range. /// Returns true if this range is equivalent to the other range.
/// </summary> /// </summary>

View File

@ -86,6 +86,7 @@
#include <xlnt/worksheet/range_reference.hpp> #include <xlnt/worksheet/range_reference.hpp>
#include <xlnt/worksheet/row_properties.hpp> #include <xlnt/worksheet/row_properties.hpp>
#include <xlnt/worksheet/selection.hpp> #include <xlnt/worksheet/selection.hpp>
#include <xlnt/worksheet/sheet_format_properties.hpp>
#include <xlnt/worksheet/sheet_protection.hpp> #include <xlnt/worksheet/sheet_protection.hpp>
#include <xlnt/worksheet/sheet_view.hpp> #include <xlnt/worksheet/sheet_view.hpp>
#include <xlnt/worksheet/worksheet.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; 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; xlnt::detail::Cell c;
for (auto &attr : parser->attribute_map()) 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) switch (e)
{ {
case xml::parser::start_element: { 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; ++level;
break; 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")) else if (string_equal(parser->name(), "f"))
{ {
c.formula_string += std::move(parser->value()); 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) 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 // <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; std::pair<xlnt::row_properties, int> props;
for (auto &attr : parser->attribute_map()) 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) switch (e)
{ {
case xml::parser::start_element: { 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; break;
} }
case xml::parser::end_element: { 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 // <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; Sheet_Data sheet_data;
int level = 1; // nesting level int level = 1; // nesting level
@ -339,7 +364,7 @@ Sheet_Data parse_sheet_data(xml::parser *parser, xlnt::detail::number_serialiser
switch (e) switch (e)
{ {
case xml::parser::start_element: { 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; break;
} }
case xml::parser::end_element: { case xml::parser::end_element: {
@ -430,6 +455,9 @@ std::string xlsx_consumer::read_worksheet_begin(const std::string &rel_id)
streaming_cell_.reset(new detail::cell_impl()); 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(), auto title = std::find_if(target_.d_->sheet_title_rel_id_map_.begin(),
target_.d_->sheet_title_rel_id_map_.end(), target_.d_->sheet_title_rel_id_map_.end(),
[&](const std::pair<std::string, std::string> &p) { [&](const std::pair<std::string, std::string> &p) {
@ -742,7 +770,8 @@ void xlsx_consumer::read_worksheet_sheetdata()
{ {
return; 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 // 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 // with a SPSC queue for what is likely to be an easy performance win
for (auto &row : ws_data.parsed_rows) for (auto &row : ws_data.parsed_rows)
@ -803,6 +832,8 @@ void xlsx_consumer::read_worksheet_sheetdata()
} }
} }
stack_.pop_back(); stack_.pop_back();
} }
worksheet xlsx_consumer::read_worksheet_end(const std::string &rel_id) worksheet xlsx_consumer::read_worksheet_end(const std::string &rel_id)
@ -1259,6 +1290,17 @@ worksheet xlsx_consumer::read_worksheet_end(const std::string &rel_id)
relationship_type::printer_settings)}); 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; return ws;
} }
@ -1356,10 +1398,7 @@ bool xlsx_consumer::has_cell()
auto has_value = false; auto has_value = false;
auto value_string = std::string(); auto value_string = std::string();
auto formula_string = std::string();
auto has_formula = false;
auto has_shared_formula = false;
auto formula_value_string = std::string();
while (in_element(qn("spreadsheetml", "c"))) while (in_element(qn("spreadsheetml", "c")))
{ {
@ -1372,17 +1411,56 @@ bool xlsx_consumer::has_cell()
} }
else if (current_element == qn("spreadsheetml", "f")) // CT_CellFormula 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")) 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", skip_attributes({"aca", "dt2D", "dtr", "del1", "del2", "r1",
"del2", "r1", "r2", "ca", "si", "bx"}); "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 else if (current_element == qn("spreadsheetml", "is")) // CT_Rst
{ {
@ -1401,9 +1479,9 @@ bool xlsx_consumer::has_cell()
expect_end_element(qn("spreadsheetml", "c")); 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) if (has_value)

View File

@ -45,6 +45,7 @@ class manifest;
template<typename T> template<typename T>
class optional; class optional;
class path; class path;
class range_reference;
class relationship; class relationship;
class streaming_workbook_reader; class streaming_workbook_reader;
class variant; class variant;
@ -418,6 +419,9 @@ private:
std::unique_ptr<detail::cell_impl> streaming_cell_; 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_; detail::worksheet_impl *current_worksheet_;
number_serialiser converter_; 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); 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() return ref_.contains(cell_ref);
&& ref_.bottom_right().column_index() >= ref.column_index()
&& ref_.top_left().row() <= ref.row()
&& ref_.bottom_right().row() >= ref.row();
} }
range range::alignment(const xlnt::alignment &new_alignment) range range::alignment(const xlnt::alignment &new_alignment)

View File

@ -145,6 +145,14 @@ cell_reference range_reference::bottom_right() const
return bottom_right_; 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 bool range_reference::operator==(const std::string &reference_string) const
{ {
return *this == range_reference(reference_string); 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 <iostream>
#include <xlnt/cell/cell.hpp> #include <xlnt/xlnt.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 <helpers/path_helper.hpp> #include <helpers/path_helper.hpp>
#include <helpers/temporary_file.hpp> #include <helpers/temporary_file.hpp>
#include <helpers/test_suite.hpp> #include <helpers/test_suite.hpp>
@ -62,7 +38,7 @@ public:
register_test(test_produce_simple_excel); register_test(test_produce_simple_excel);
register_test(test_save_after_sheet_deletion); register_test(test_save_after_sheet_deletion);
register_test(test_write_comments_hyperlinks_formulae); 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_load_non_xlsx);
register_test(test_decrypt_agile); register_test(test_decrypt_agile);
register_test(test_decrypt_libre_office); register_test(test_decrypt_libre_office);
@ -304,21 +280,20 @@ public:
xlnt_assert(workbook_matches_file(wb, path)); xlnt_assert(workbook_matches_file(wb, path));
} }
void test_save_after_clear_all_formulae() void test_save_after_clear_formula()
{ {
xlnt::workbook wb; 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); wb.load(path);
auto ws1 = wb.sheet_by_index(0); auto ws1 = wb.sheet_by_index(0);
xlnt_assert(ws1.cell("C1").has_formula()); for (auto row : ws1)
xlnt_assert_equals(ws1.cell("C1").formula(), "CONCATENATE(C2,C3)"); {
ws1.cell("C1").clear_formula(); for (auto cell : row)
{
auto ws2 = wb.sheet_by_index(1); cell.clear_formula();
xlnt_assert(ws2.cell("C1").has_formula()); }
xlnt_assert_equals(ws2.cell("C1").formula(), "C2*C3"); }
ws2.cell("C1").clear_formula();
wb.save("clear_formulae.xlsx"); wb.save("clear_formulae.xlsx");
} }
@ -425,20 +400,41 @@ public:
void test_read_formulae() void test_read_formulae()
{ {
xlnt::workbook wb; 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); wb.load(path);
auto ws1 = wb.sheet_by_index(0); 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");
auto ws2 = wb.sheet_by_index(1); // test has_formula
xlnt_assert(ws2.cell("C1").has_formula()); // A1:B3 are plain text cells
xlnt_assert_equals(ws2.cell("C1").formula(), "C2*C3"); // C1:G3,I2,F4 have formulae
xlnt_assert_equals(ws2.cell("C2").value<int>(), 2); for (auto row = 1; row < 4; row++)
xlnt_assert_equals(ws2.cell("C3").value<int>(), 3); {
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());
}
}
}
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() void test_read_headers_and_footers()