add failing tests and begin restructuring worksheet reading [ci skip]

This commit is contained in:
Thomas Fussell 2017-06-17 10:53:37 -04:00
parent 2305eae8b6
commit d7e7526beb
5 changed files with 407 additions and 254 deletions

View File

@ -58,12 +58,16 @@ public:
/// </summary>
void close();
bool has_cell();
/// <summary>
/// Reads the next cell in the current worksheet and optionally returns it if
/// the last cell in the sheet has not yet been read.
/// </summary>
cell read_cell();
bool has_worksheet();
/// <summary>
/// Beings reading of the next worksheet in the workbook and optionally
/// returns its title if the last worksheet has not yet been read.

View File

@ -129,7 +129,8 @@ namespace detail {
xlsx_consumer::xlsx_consumer(workbook &target)
: target_(target),
parser_(nullptr)
parser_(nullptr),
stream_cell_(nullptr)
{
}
@ -150,9 +151,252 @@ cell xlsx_consumer::read_cell()
return cell(nullptr);
}
std::string xlsx_consumer::begin_worksheet()
std::string xlsx_consumer::read_worksheet_begin()
{
if (worksheet_queue_.empty())
{
return "";
}
const auto back = worksheet_queue_.back();
const auto rel_id = back.id();
worksheet_queue_.pop_back();
auto title = std::find_if(target_.d_->sheet_title_rel_id_map_.begin(),
target_.d_->sheet_title_rel_id_map_.end(),
[&](const std::pair<std::string, std::string> &p) {
return p.second == rel_id;
})->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)
{
++insertion_iter;
}
target_.d_->worksheets_.emplace(insertion_iter, &target_, id, title);
auto ws = target_.sheet_by_id(id);
expect_start_element(qn("spreadsheetml", "worksheet"), xml::content::complex); // CT_Worksheet
skip_attributes({ qn("mc", "Ignorable") });
read_namespaces();
xlnt::range_reference full_range;
auto &manifest = target_.manifest();
const auto workbook_rel = manifest.relationship(path("/"), relationship_type::office_document);
const auto sheet_rel = manifest.relationship(workbook_rel.target().path(), rel_id);
path sheet_path(sheet_rel.source().path().parent().append(sheet_rel.target().path()));
auto hyperlinks = manifest.relationships(sheet_path, xlnt::relationship_type::hyperlink);
while (in_element(qn("spreadsheetml", "worksheet")))
{
auto current_worksheet_element = expect_start_element(xml::content::complex);
if (current_worksheet_element == qn("spreadsheetml", "sheetPr")) // CT_SheetPr 0-1
{
while (in_element(current_worksheet_element))
{
auto sheet_pr_child_element = expect_start_element(xml::content::simple);
if (sheet_pr_child_element == qn("spreadsheetml", "tabColor")) // CT_Color 0-1
{
read_color();
}
else if (sheet_pr_child_element == qn("spreadsheetml", "outlinePr")) // CT_OutlinePr 0-1
{
skip_attribute("applyStyles"); // optional, boolean, false
skip_attribute("summaryBelow"); // optional, boolean, true
skip_attribute("summaryRight"); // optional, boolean, true
skip_attribute("showOutlineSymbols"); // optional, boolean, true
}
else if (sheet_pr_child_element == qn("spreadsheetml", "pageSetUpPr")) // CT_PageSetUpPr 0-1
{
skip_attribute("autoPageBreaks"); // optional, boolean, true
skip_attribute("fitToPage"); // optional, boolean, false
}
else
{
unexpected_element(sheet_pr_child_element);
}
expect_end_element(sheet_pr_child_element);
}
skip_attribute("syncHorizontal"); // optional, boolean, false
skip_attribute("syncVertical"); // optional, boolean, false
skip_attribute("syncRef"); // optional, ST_Ref, false
skip_attribute("transitionEvaluation"); // optional, boolean, false
skip_attribute("transitionEntry"); // optional, boolean, false
skip_attribute("published"); // optional, boolean, true
skip_attribute("codeName"); // optional, string
skip_attribute("filterMode"); // optional, boolean, false
skip_attribute("enableFormatConditionsCalculation"); // optional, boolean, true
}
else if (current_worksheet_element == qn("spreadsheetml", "dimension")) // CT_SheetDimension 0-1
{
full_range = xlnt::range_reference(parser().attribute("ref"));
}
else if (current_worksheet_element == qn("spreadsheetml", "sheetViews")) // CT_SheetViews 0-1
{
while (in_element(current_worksheet_element))
{
expect_start_element(qn("spreadsheetml", "sheetView"), xml::content::complex); // CT_SheetView 1+
sheet_view new_view;
new_view.id(parser().attribute<std::size_t>("workbookViewId"));
if (parser().attribute_present("showGridLines")) // default="true"
{
new_view.show_grid_lines(is_true(parser().attribute("showGridLines")));
}
if (parser().attribute_present("defaultGridColor")) // default="true"
{
new_view.default_grid_color(is_true(parser().attribute("defaultGridColor")));
}
if (parser().attribute_present("view") && parser().attribute("view") != "normal")
{
new_view.type(parser().attribute("view") == "pageBreakPreview" ? sheet_view_type::page_break_preview
: sheet_view_type::page_layout);
}
skip_attributes({ "windowProtection", "showFormulas", "showRowColHeaders", "showZeros", "rightToLeft",
"tabSelected", "showRuler", "showOutlineSymbols", "showWhiteSpace", "view", "topLeftCell",
"colorId", "zoomScale", "zoomScaleNormal", "zoomScaleSheetLayoutView", "zoomScalePageLayoutView" });
while (in_element(qn("spreadsheetml", "sheetView")))
{
auto sheet_view_child_element = expect_start_element(xml::content::simple);
if (sheet_view_child_element == qn("spreadsheetml", "pane")) // CT_Pane 0-1
{
pane new_pane;
if (parser().attribute_present("topLeftCell"))
{
new_pane.top_left_cell = cell_reference(parser().attribute("topLeftCell"));
}
if (parser().attribute_present("xSplit"))
{
new_pane.x_split = parser().attribute<column_t::index_t>("xSplit");
}
if (parser().attribute_present("ySplit"))
{
new_pane.y_split = parser().attribute<row_t>("ySplit");
}
if (parser().attribute_present("activePane"))
{
new_pane.active_pane = parser().attribute<pane_corner>("activePane");
}
if (parser().attribute_present("state"))
{
new_pane.state = parser().attribute<pane_state>("state");
}
new_view.pane(new_pane);
}
else if (sheet_view_child_element == qn("spreadsheetml", "selection")) // CT_Selection 0-4
{
skip_remaining_content(sheet_view_child_element);
}
else if (sheet_view_child_element == qn("spreadsheetml", "pivotSelection")) // CT_PivotSelection 0-4
{
skip_remaining_content(sheet_view_child_element);
}
else if (sheet_view_child_element == qn("spreadsheetml", "extLst")) // CT_ExtensionList 0-1
{
skip_remaining_content(sheet_view_child_element);
}
else
{
unexpected_element(sheet_view_child_element);
}
expect_end_element(sheet_view_child_element);
}
expect_end_element(qn("spreadsheetml", "sheetView"));
ws.d_->views_.push_back(new_view);
}
}
else if (current_worksheet_element == qn("spreadsheetml", "sheetFormatPr")) // CT_SheetFormatPr 0-1
{
skip_remaining_content(current_worksheet_element);
}
else if (current_worksheet_element == qn("spreadsheetml", "cols")) // CT_Cols 0+
{
while (in_element(qn("spreadsheetml", "cols")))
{
expect_start_element(qn("spreadsheetml", "col"), xml::content::simple);
skip_attributes({ "bestFit", "collapsed", "outlineLevel" });
auto min = static_cast<column_t::index_t>(std::stoull(parser().attribute("min")));
auto max = static_cast<column_t::index_t>(std::stoull(parser().attribute("max")));
optional<double> width;
if (parser().attribute_present("width"))
{
width = parser().attribute<double>("width");
}
optional<std::size_t> column_style;
if (parser().attribute_present("style"))
{
column_style = parser().attribute<std::size_t>("style");
}
auto custom =
parser().attribute_present("customWidth") ? is_true(parser().attribute("customWidth")) : false;
auto hidden = parser().attribute_present("hidden") ? is_true(parser().attribute("hidden")) : false;
expect_end_element(qn("spreadsheetml", "col"));
for (auto column = min; column <= max; column++)
{
column_properties props;
if (width.is_set())
{
props.width = width.get();
}
if (column_style.is_set())
{
props.style = column_style.get();
}
props.hidden = hidden;
props.custom_width = custom;
ws.add_column_properties(column, props);
}
}
}
else if (current_worksheet_element == qn("spreadsheetml", "sheetData")) // CT_SheetData 1
{
in_sheet_data_ = true;
}
}
if (!in_sheet_data_)
{
read_worksheet_end();
}
return title;
}
worksheet xlsx_consumer::end_worksheet()
@ -165,6 +409,16 @@ xml::parser &xlsx_consumer::parser()
return *parser_;
}
bool xlsx_consumer::has_cell()
{
return stream_cell_ != nullptr;
}
bool xlsx_consumer::has_worksheet()
{
return !worksheet_queue_.empty();
}
std::vector<relationship> xlsx_consumer::read_relationships(const path &part)
{
const auto part_rels_path = part.parent().append("_rels")
@ -1434,238 +1688,17 @@ void xlsx_consumer::read_volatile_dependencies()
}
// CT_Worksheet
void xlsx_consumer::read_worksheet(const std::string &rel_id)
void xlsx_consumer::read_worksheet_end()
{
/*
static const auto &xmlns = constants::namespace_("spreadsheetml");
static const auto &xmlns_mc = constants::namespace_("mc");
static const auto &xmlns_x14ac = constants::namespace_("x14ac");
static const auto &xmlns_r = constants::namespace_("r");
*/
auto title = std::find_if(target_.d_->sheet_title_rel_id_map_.begin(),
target_.d_->sheet_title_rel_id_map_.end(),
[&](const std::pair<std::string, std::string> &p) {
return p.second == rel_id;
})->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)
{
++insertion_iter;
}
target_.d_->worksheets_.emplace(insertion_iter, &target_, id, title);
auto ws = target_.sheet_by_id(id);
expect_start_element(qn("spreadsheetml", "worksheet"), xml::content::complex); // CT_Worksheet
skip_attributes({qn("mc", "Ignorable")});
read_namespaces();
xlnt::range_reference full_range;
auto &manifest = target_.manifest();
const auto workbook_rel = manifest.relationship(path("/"), relationship_type::office_document);
const auto sheet_rel = manifest.relationship(workbook_rel.target().path(), rel_id);
path sheet_path(sheet_rel.source().path().parent().append(sheet_rel.target().path()));
auto hyperlinks = manifest.relationships(sheet_path, xlnt::relationship_type::hyperlink);
in_sheet_data_ = false;
auto ws = worksheet(stream_worksheet_);
std::vector<relationship> hyperlinks;
while (in_element(qn("spreadsheetml", "worksheet")))
{
auto current_worksheet_element = expect_start_element(xml::content::complex);
if (current_worksheet_element == qn("spreadsheetml", "sheetPr")) // CT_SheetPr 0-1
{
while (in_element(current_worksheet_element))
{
auto sheet_pr_child_element = expect_start_element(xml::content::simple);
if (sheet_pr_child_element == qn("spreadsheetml", "tabColor")) // CT_Color 0-1
{
read_color();
}
else if (sheet_pr_child_element == qn("spreadsheetml", "outlinePr")) // CT_OutlinePr 0-1
{
skip_attribute("applyStyles"); // optional, boolean, false
skip_attribute("summaryBelow"); // optional, boolean, true
skip_attribute("summaryRight"); // optional, boolean, true
skip_attribute("showOutlineSymbols"); // optional, boolean, true
}
else if (sheet_pr_child_element == qn("spreadsheetml", "pageSetUpPr")) // CT_PageSetUpPr 0-1
{
skip_attribute("autoPageBreaks"); // optional, boolean, true
skip_attribute("fitToPage"); // optional, boolean, false
}
else
{
unexpected_element(sheet_pr_child_element);
}
expect_end_element(sheet_pr_child_element);
}
skip_attribute("syncHorizontal"); // optional, boolean, false
skip_attribute("syncVertical"); // optional, boolean, false
skip_attribute("syncRef"); // optional, ST_Ref, false
skip_attribute("transitionEvaluation"); // optional, boolean, false
skip_attribute("transitionEntry"); // optional, boolean, false
skip_attribute("published"); // optional, boolean, true
skip_attribute("codeName"); // optional, string
skip_attribute("filterMode"); // optional, boolean, false
skip_attribute("enableFormatConditionsCalculation"); // optional, boolean, true
}
else if (current_worksheet_element == qn("spreadsheetml", "dimension")) // CT_SheetDimension 0-1
{
full_range = xlnt::range_reference(parser().attribute("ref"));
}
else if (current_worksheet_element == qn("spreadsheetml", "sheetViews")) // CT_SheetViews 0-1
{
while (in_element(current_worksheet_element))
{
expect_start_element(qn("spreadsheetml", "sheetView"), xml::content::complex); // CT_SheetView 1+
sheet_view new_view;
new_view.id(parser().attribute<std::size_t>("workbookViewId"));
if (parser().attribute_present("showGridLines")) // default="true"
{
new_view.show_grid_lines(is_true(parser().attribute("showGridLines")));
}
if (parser().attribute_present("defaultGridColor")) // default="true"
{
new_view.default_grid_color(is_true(parser().attribute("defaultGridColor")));
}
if (parser().attribute_present("view") && parser().attribute("view") != "normal")
{
new_view.type(parser().attribute("view") == "pageBreakPreview" ? sheet_view_type::page_break_preview
: sheet_view_type::page_layout);
}
skip_attributes({"windowProtection", "showFormulas", "showRowColHeaders", "showZeros", "rightToLeft",
"tabSelected", "showRuler", "showOutlineSymbols", "showWhiteSpace", "view", "topLeftCell",
"colorId", "zoomScale", "zoomScaleNormal", "zoomScaleSheetLayoutView", "zoomScalePageLayoutView"});
while (in_element(qn("spreadsheetml", "sheetView")))
{
auto sheet_view_child_element = expect_start_element(xml::content::simple);
if (sheet_view_child_element == qn("spreadsheetml", "pane")) // CT_Pane 0-1
{
pane new_pane;
if (parser().attribute_present("topLeftCell"))
{
new_pane.top_left_cell = cell_reference(parser().attribute("topLeftCell"));
}
if (parser().attribute_present("xSplit"))
{
new_pane.x_split = parser().attribute<column_t::index_t>("xSplit");
}
if (parser().attribute_present("ySplit"))
{
new_pane.y_split = parser().attribute<row_t>("ySplit");
}
if (parser().attribute_present("activePane"))
{
new_pane.active_pane = parser().attribute<pane_corner>("activePane");
}
if (parser().attribute_present("state"))
{
new_pane.state = parser().attribute<pane_state>("state");
}
new_view.pane(new_pane);
}
else if (sheet_view_child_element == qn("spreadsheetml", "selection")) // CT_Selection 0-4
{
skip_remaining_content(sheet_view_child_element);
}
else if (sheet_view_child_element == qn("spreadsheetml", "pivotSelection")) // CT_PivotSelection 0-4
{
skip_remaining_content(sheet_view_child_element);
}
else if (sheet_view_child_element == qn("spreadsheetml", "extLst")) // CT_ExtensionList 0-1
{
skip_remaining_content(sheet_view_child_element);
}
else
{
unexpected_element(sheet_view_child_element);
}
expect_end_element(sheet_view_child_element);
}
expect_end_element(qn("spreadsheetml", "sheetView"));
ws.d_->views_.push_back(new_view);
}
}
else if (current_worksheet_element == qn("spreadsheetml", "sheetFormatPr")) // CT_SheetFormatPr 0-1
{
skip_remaining_content(current_worksheet_element);
}
else if (current_worksheet_element == qn("spreadsheetml", "cols")) // CT_Cols 0+
{
while (in_element(qn("spreadsheetml", "cols")))
{
expect_start_element(qn("spreadsheetml", "col"), xml::content::simple);
skip_attributes({"bestFit", "collapsed", "outlineLevel"});
auto min = static_cast<column_t::index_t>(std::stoull(parser().attribute("min")));
auto max = static_cast<column_t::index_t>(std::stoull(parser().attribute("max")));
optional<double> width;
if (parser().attribute_present("width"))
{
width = parser().attribute<double>("width");
}
optional<std::size_t> column_style;
if (parser().attribute_present("style"))
{
column_style = parser().attribute<std::size_t>("style");
}
auto custom =
parser().attribute_present("customWidth") ? is_true(parser().attribute("customWidth")) : false;
auto hidden = parser().attribute_present("hidden") ? is_true(parser().attribute("hidden")) : false;
expect_end_element(qn("spreadsheetml", "col"));
for (auto column = min; column <= max; column++)
{
column_properties props;
if (width.is_set())
{
props.width = width.get();
}
if (column_style.is_set())
{
props.style = column_style.get();
}
props.hidden = hidden;
props.custom_width = custom;
ws.add_column_properties(column, props);
}
}
}
else if (current_worksheet_element == qn("spreadsheetml", "sheetData")) // CT_SheetData 1
if (current_worksheet_element == qn("spreadsheetml", "sheetData")) // CT_SheetData 1
{
while (in_element(qn("spreadsheetml", "sheetData")))
{
@ -2089,10 +2122,15 @@ void xlsx_consumer::read_worksheet(const std::string &rel_id)
expect_end_element(qn("spreadsheetml", "worksheet"));
auto &manifest = target_.manifest();
xlnt::path sheet_path;
relationship workbook_rel, sheet_rel;
if (manifest.has_relationship(sheet_path, xlnt::relationship_type::comments))
{
auto comments_part = manifest.canonicalize(
{workbook_rel, sheet_rel, manifest.relationship(sheet_path, xlnt::relationship_type::comments)});
auto comments_part = manifest.canonicalize({workbook_rel, sheet_rel,
manifest.relationship(sheet_path, xlnt::relationship_type::comments)});
auto receive = xml::parser::receive_default;
auto comments_part_streambuf = archive_->open(comments_part);
@ -2104,8 +2142,8 @@ void xlsx_consumer::read_worksheet(const std::string &rel_id)
if (manifest.has_relationship(sheet_path, xlnt::relationship_type::vml_drawing))
{
auto vml_drawings_part = manifest.canonicalize(
{workbook_rel, sheet_rel, manifest.relationship(sheet_path, xlnt::relationship_type::vml_drawing)});
auto vml_drawings_part = manifest.canonicalize({workbook_rel, sheet_rel,
manifest.relationship(sheet_path, xlnt::relationship_type::vml_drawing)});
auto vml_drawings_part_streambuf = archive_->open(comments_part);
std::istream vml_drawings_part_stream(comments_part_streambuf.get());

View File

@ -45,6 +45,7 @@ template<typename T>
class optional;
class path;
class relationship;
class streaming_workbook_reader;
class variant;
class workbook;
class worksheet;
@ -52,6 +53,8 @@ class worksheet;
namespace detail {
class izstream;
struct cell_impl;
struct worksheet_impl;
/// <summary>
/// Handles writing a workbook into an XLSX file.
@ -61,8 +64,18 @@ class xlsx_consumer
public:
xlsx_consumer(workbook &destination);
void read(std::istream &source);
void read(std::istream &source, const std::string &password);
private:
friend class streaming_workbook_reader;
void open(std::istream &source);
bool has_cell();
/// <summary>
/// Reads the next cell in the current worksheet and optionally returns it if
/// the last cell in the sheet has not yet been read. An exception will be thrown
@ -70,13 +83,6 @@ public:
/// </summary>
cell read_cell();
/// <summary>
/// Beings reading of the next worksheet in the workbook and optionally
/// returns its title if the last worksheet has not yet been read. An
/// exception will be thrown if this is not open as a streaming consumer.
/// </summary>
std::string begin_worksheet();
/// <summary>
/// Ends reading of the current worksheet in the workbook and optionally
/// returns a worksheet object corresponding to the worksheet with the title
@ -85,11 +91,8 @@ public:
/// </summary>
worksheet end_worksheet();
void read(std::istream &source);
bool has_worksheet();
void read(std::istream &source, const std::string &password);
private:
/// <summary>
/// Read all the files needed from the XLSX archive and initialize all of
/// the data in the workbook to match.
@ -208,6 +211,16 @@ private:
/// </summary>
void read_worksheet(const std::string &title);
/// <summary>
/// xl/sheets/*.xml
/// </summary>
std::string read_worksheet_begin();
/// <summary>
/// xl/sheets/*.xml
/// </summary>
void read_worksheet_end();
// Sheet Relationship Target Parts
/// <summary>
@ -398,6 +411,14 @@ private:
std::vector<xml::qname> stack_;
bool preserve_space_ = false;
std::vector<relationship> worksheet_queue_;
detail::cell_impl *stream_cell_;
detail::worksheet_impl *stream_worksheet_;
bool in_sheet_data_;
};
} // namespace detail

View File

@ -32,6 +32,45 @@
#include <xlnt/workbook/workbook.hpp>
#include <xlnt/worksheet/worksheet.hpp>
namespace {
//TODO: (important) this is duplicated from workbook.cpp, find a common place to keep it
#ifdef _MSC_VER
void open_stream(std::ifstream &stream, const std::wstring &path)
{
stream.open(path, std::ios::binary);
}
void open_stream(std::ofstream &stream, const std::wstring &path)
{
stream.open(path, std::ios::binary);
}
void open_stream(std::ifstream &stream, const std::string &path)
{
open_stream(stream, xlnt::path(path).wstring());
}
void open_stream(std::ofstream &stream, const std::string &path)
{
open_stream(stream, xlnt::path(path).wstring());
}
#else
void open_stream(std::ifstream &stream, const std::string &path)
{
stream.open(path, std::ios::binary);
}
void open_stream(std::ofstream &stream, const std::string &path)
{
stream.open(path, std::ios::binary);
}
#endif
} // namespace
namespace xlnt {
streaming_workbook_reader::~streaming_workbook_reader()
@ -47,14 +86,24 @@ void streaming_workbook_reader::close()
}
}
bool streaming_workbook_reader::has_cell()
{
return consumer_->has_cell();
}
cell streaming_workbook_reader::read_cell()
{
return consumer_->read_cell();
}
bool streaming_workbook_reader::has_worksheet()
{
return consumer_->has_worksheet();
}
std::string streaming_workbook_reader::begin_worksheet()
{
return consumer_->begin_worksheet();
return consumer_->read_worksheet_begin();
}
worksheet streaming_workbook_reader::end_worksheet()
@ -71,21 +120,22 @@ void streaming_workbook_reader::open(const std::vector<std::uint8_t> &data)
void streaming_workbook_reader::open(const std::string &filename)
{
std::ifstream file_stream(filename, std::ios::binary);
open(file_stream);
std::ifstream file_stream;
open_stream(file_stream, filename);
}
#ifdef _MSC_VER
void streaming_workbook_reader::open(const std::wstring &filename)
{
std::ifstream file_stream(filename, std::ios::binary);
open(file_stream);
std::ifstream file_stream;
open_stream(file_stream, filename);
}
#endif
void streaming_workbook_reader::open(const xlnt::path &filename)
{
open(filename.string());
std::ifstream file_stream;
open_stream(file_stream, filename.string());
}
void streaming_workbook_reader::open(std::istream &stream)

View File

@ -31,6 +31,8 @@
#include <helpers/test_suite.hpp>
#include <helpers/path_helper.hpp>
#include <helpers/xml_helper.hpp>
#include <xlnt/workbook/streaming_workbook_reader.hpp>
#include <xlnt/workbook/streaming_workbook_writer.hpp>
#include <xlnt/workbook/workbook.hpp>
class serialization_test_suite : public test_suite
@ -461,4 +463,42 @@ public:
xlnt_assert(round_trip_matches_rw(path, password));
}
}
void test_streaming_read()
{
const auto path = path_helper::test_file("4_every_style.xlsx");
xlnt::streaming_workbook_reader reader;
reader.open(path);
while (reader.has_worksheet())
{
reader.begin_worksheet();
while (reader.has_cell())
{
const auto cell = reader.read_cell();
std::cout << cell.reference().to_string() << std::endl;
}
const auto ws = reader.end_worksheet();
}
}
void test_streaming_write()
{
const auto path = std::string("stream-out.xlsx");
xlnt::streaming_workbook_writer writer;
writer.open(path);
writer.add_sheet("stream");
auto b2 = writer.add_cell("B2");
b2.value("B2!");
auto c3 = writer.add_cell("C3");
b2.value("should not change");
c3.value("C3!");
}
};