Files
2026-09-16 14:07:40 +08:00

629 lines
23 KiB
C++
Vendored

/*
____ ____ ___ ____ ____ ____ ___
6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M'
8P Y8 `MM. d' MM 6M' ` `MM. d'
6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d'
MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d'
MM MM MM' `Mb 6M' `Mb MMM9 `Mb `MMd MM YMMMMb `MMd
MM MM MM MM MM MM MM' MM dMM. MM `Mb dMM.
MM MM MM MM MMMMMMMM MM MM d'`MM. MM MM d'`MM.
YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM.
8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM.
YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_
MM
MM
_MM_
Copyright (c) 2018, Kenneth Troldal Balslev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the author nor the
names of any contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// ===== External Includes ===== //
#include <algorithm>
#include <iterator>
#include <pugixml.hpp>
#include <vector>
// ===== OpenXLSX Includes ===== //
#include "XLDocument.hpp"
#include "XLSheet.hpp"
#include "XLWorkbook.hpp"
using namespace OpenXLSX;
namespace
{
/**
* @brief
* @param doc
* @return
*/
XMLNode sheetsNode(const XMLDocument& doc)
{
return doc.document_element().child("sheets");
}
} // namespace
/**
* @details The constructor initializes the member variables and calls the loadXMLData from the
* XLAbstractXMLFile base class.
*/
XLWorkbook::XLWorkbook(XLXmlData* xmlData) : XLXmlFile(xmlData) {}
/**
* @details
*/
XLWorkbook::~XLWorkbook() = default;
/**
* @details
*/
XLSheet XLWorkbook::sheet(const std::string& sheetName)
{
// ===== First determine if the sheet exists.
if (xmlDocument().document_element().child("sheets").find_child_by_attribute("name", sheetName.c_str()) == nullptr)
throw XLInputError("Sheet \"" + sheetName + "\" does not exist");
// ===== Find the sheet data corresponding to the sheet with the requested name
std::string xmlID =
xmlDocument().document_element().child("sheets").find_child_by_attribute("name", sheetName.c_str()).attribute("r:id").value();
XLQuery pathQuery(XLQueryType::QuerySheetRelsTarget);
pathQuery.setParam("sheetID", xmlID);
auto xmlPath = parentDoc().execQuery(pathQuery).result<std::string>();
// Some spreadsheets use absolute rather than relative paths in relationship items.
if (xmlPath.substr(0,4) == "/xl/") xmlPath = xmlPath.substr(4);
XLQuery xmlQuery(XLQueryType::QueryXmlData);
xmlQuery.setParam("xmlPath", "xl/" + xmlPath);
return XLSheet(parentDoc().execQuery(xmlQuery).result<XLXmlData*>());
}
/**
* @details Create a vector with sheet nodes, retrieve the node at the requested index, get sheet name and return the
* corresponding sheet object.
*/
XLSheet XLWorkbook::sheet(uint16_t index)
{
if (index < 1 || index > sheetCount()) throw XLInputError("Sheet index is out of bounds");
return sheet(
std::vector<XMLNode>(sheetsNode(xmlDocument()).begin(), sheetsNode(xmlDocument()).end())[index - 1].attribute("name").as_string());
}
/**
* @details
*/
XLWorksheet XLWorkbook::worksheet(const std::string& sheetName)
{
return sheet(sheetName).get<XLWorksheet>();
}
/**
* @details
*/
XLChartsheet XLWorkbook::chartsheet(const std::string& sheetName)
{
return sheet(sheetName).get<XLChartsheet>();
}
/**
* @details
*/
bool XLWorkbook::hasSharedStrings() const
{
return true;//parentDoc().executeQuery(XLQuerySharedStrings()).sharedStrings() != nullptr;
}
/**
* @details
*/
XLSharedStrings XLWorkbook::sharedStrings()
{
XLQuery query(XLQueryType::QuerySharedStrings);
return parentDoc().execQuery(query).result<XLSharedStrings>();
}
/**
* @details
*/
void XLWorkbook::deleteNamedRanges()
{
for (auto& child : xmlDocument().document_element().child("definedNames").children()) child.parent().remove_child(child);
}
/**
* @details
*/
void XLWorkbook::deleteSheet(const std::string& sheetName)
{
// ===== Determine ID and type of sheet, as well as current worksheet count.
auto sheetID = sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()).attribute("r:id").value(); // NOLINT
XLQuery sheetTypeQuery(XLQueryType::QuerySheetType);
sheetTypeQuery.setParam("sheetID", relationshipID());
auto sheetType = parentDoc().execQuery(sheetTypeQuery).result<XLContentType>();
auto worksheetCount =
std::count_if(sheetsNode(xmlDocument()).children().begin(), sheetsNode(xmlDocument()).children().end(), [&](const XMLNode& item) {
XLQuery query(XLQueryType::QuerySheetType);
query.setParam("sheetID", std::string(item.attribute("r:id").value()));
return parentDoc().execQuery(query).result<XLContentType>() == XLContentType::Worksheet;
});
// ===== If this is the last worksheet in the workbook, throw an exception.
if (worksheetCount == 1 && sheetType == XLContentType::Worksheet)
throw XLInputError("Invalid operation. There must be at least one worksheet in the workbook.");
// ===== Delete the sheet data as well as the sheet node from Workbook.xml
parentDoc().execCommand(XLCommand(XLCommandType::DeleteSheet)
.setParam("sheetID", std::string(sheetID))
.setParam("sheetName", sheetName));
sheetsNode(xmlDocument()).remove_child(sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()));
if (sheetIsActive(sheetID))
xmlDocument().document_element().child("bookViews").first_child().remove_attribute("activeTab");
}
/**
* @details
*/
void XLWorkbook::addWorksheet(const std::string& sheetName)
{
// ===== If a sheet with the given name already exists, throw an exception.
if (xmlDocument().document_element().child("sheets").find_child_by_attribute("name", sheetName.c_str()))
throw XLInputError("Sheet named \"" + sheetName + "\" already exists.");
// ===== Create new internal (workbook) ID for the sheet
auto internalID = createInternalSheetID();
// ===== Create xml file for new worksheet and add metadata to the workbook file.
parentDoc().execCommand(XLCommand(XLCommandType::AddWorksheet)
.setParam("sheetName", sheetName)
.setParam("sheetPath", "/xl/worksheets/sheet" + std::to_string(internalID) + ".xml"));
prepareSheetMetadata(sheetName, internalID);
}
/**
* @details
* @todo If the original sheet's tabSelected attribute is set, ensure it is un-set in the clone.
*/
void XLWorkbook::cloneSheet(const std::string& existingName, const std::string& newName)
{
parentDoc().execCommand(XLCommand(XLCommandType::CloneSheet)
.setParam("sheetID", sheetID(existingName))
.setParam("cloneName", newName));
}
/**
* @details
*/
uint16_t XLWorkbook::createInternalSheetID()
{
return static_cast<uint16_t>(std::max_element(xmlDocument().document_element().child("sheets").children().begin(),
xmlDocument().document_element().child("sheets").children().end(),
[](const XMLNode& a, const XMLNode& b) {
return a.attribute("sheetId").as_uint() < b.attribute("sheetId").as_uint();
})
->attribute("sheetId")
.as_uint() +
1);
}
/**
* @details
*/
std::string XLWorkbook::sheetID(const std::string& sheetName)
{
return xmlDocument().document_element().child("sheets").find_child_by_attribute("name", sheetName.c_str()).attribute("r:id").value();
}
/**
* @details
*/
std::string XLWorkbook::sheetName(const std::string& sheetID) const
{
return xmlDocument().document_element().child("sheets").find_child_by_attribute("r:id", sheetID.c_str()).attribute("name").value();
}
/**
* @details
*/
std::string XLWorkbook::sheetVisibility(const std::string& sheetID) const
{
return xmlDocument().document_element().child("sheets").find_child_by_attribute("r:id", sheetID.c_str()).attribute("state").value();
}
/**
* @details
*/
void XLWorkbook::prepareSheetMetadata(const std::string& sheetName, uint16_t internalID)
{
// ===== Add new child node to the "sheets" node.
auto node = sheetsNode(xmlDocument()).append_child("sheet");
// ===== append the required attributes to the newly created sheet node.
std::string sheetPath = "/xl/worksheets/sheet" + std::to_string(internalID) + ".xml";
node.append_attribute("name") = sheetName.c_str();
node.append_attribute("sheetId") = std::to_string(internalID).c_str();
XLQuery query(XLQueryType::QuerySheetRelsID);
query.setParam("sheetPath", sheetPath);
node.append_attribute("r:id") = parentDoc().execQuery(query).result<std::string>().c_str();
}
/**
* @details
*/
void XLWorkbook::setSheetName(const std::string& sheetRID, const std::string& newName)
{
auto sheetName = xmlDocument().document_element().child("sheets").find_child_by_attribute("r:id", sheetRID.c_str()).attribute("name");
updateSheetReferences(sheetName.value(), newName);
sheetName.set_value(newName.c_str());
}
/**
* @details
*/
void XLWorkbook::setSheetVisibility(const std::string& sheetRID, const std::string& state)
{
// ===== First, determine if there are other sheets that are visible
int visibleSheets = 0;
for (const auto& item : xmlDocument().document_element().child("sheets").children()) {
if (std::string(item.attribute("r:id").value()) != sheetRID) {
if (!item.attribute("state") || !(std::string(item.attribute("state").value()) == "hidden" || std::string(item.attribute("state").value()) == "veryHidden"))
++visibleSheets;
}
}
// ===== If there are no other visible sheets, and the current sheet is to be made hidden, throw an exception.
if ((state == "hidden" || state == "veryHidden") && visibleSheets == 0)
throw XLSheetError("At least one sheet must be visible.");
// ===== Then, retrieve or create the visibility ("state") attribute for the sheet, and set it to the "state" value
auto stateAttribute =
xmlDocument().document_element().child("sheets").find_child_by_attribute("r:id", sheetRID.c_str()).attribute("state");
if (!stateAttribute) {
stateAttribute =
xmlDocument().document_element().child("sheets").find_child_by_attribute("r:id", sheetRID.c_str()).prepend_attribute("state");
}
stateAttribute.set_value(state.c_str());
// Next, find the index of the sheet...
std::string name = xmlDocument().document_element().child("sheets").find_child_by_attribute("r:id", sheetRID.c_str()).attribute("name").value();
auto index = indexOfSheet(name) - 1;
// ...and determine the index of the active sheet
auto activeTabAttribute = xmlDocument().document_element().child("bookViews").first_child().attribute("activeTab");
if (!activeTabAttribute) {
activeTabAttribute = xmlDocument().document_element().child("bookViews").first_child().append_attribute("activeTab");
activeTabAttribute.set_value(0);
}
auto activeTabIndex = activeTabAttribute.as_uint();
// Finally, if the current sheet is the active one, set the "activeTab" attribute to the first visible sheet in the workbook
if (activeTabIndex == index) {
for (auto& item : xmlDocument().document_element().child("sheets").children()) {
if (!item.attribute("state") || std::string(item.attribute("state").value()) != "hidden" || std::string(item.attribute("state").value()) != "veryHidden")
activeTabAttribute.set_value(indexOfSheet(item.attribute("name").value()) - 1);
}
}
}
/**
* @details
* @todo In some cases (eg. if a sheet is moved to the position before the selected sheet), multiple sheets are selected when opened in Excel.
*/
void XLWorkbook::setSheetIndex(const std::string& sheetName, unsigned int index)
{
// ===== Check that the input is valid
// if (index < 1 || index > std::distance(xmlDocument().document_element().child("sheets").children().begin(),
// xmlDocument().document_element().child("sheets").children().end()))
// throw XLException("Invalid sheet index");
// ===== If the new index is equal to the current, don't do anything
if (index-1 == std::distance(xmlDocument().document_element().child("sheets").children().begin(),
std::find_if(xmlDocument().document_element().child("sheets").children().begin(),
xmlDocument().document_element().child("sheets").children().end(),
[&](const XMLNode& item) { return sheetName == item.attribute("name").value(); })))
return;
// ===== Modify the node in the XML file
if (index <= 1)
sheetsNode(xmlDocument()).prepend_move(sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()));
else if (index >= sheetCount())
sheetsNode(xmlDocument()).append_move(sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()));
else {
auto vec = std::vector<XMLNode>(sheetsNode(xmlDocument()).children().begin(), sheetsNode(xmlDocument()).children().end());
auto existingSheet = vec[index - 1];
if (indexOfSheet(sheetName) < index) {
sheetsNode(xmlDocument())
.insert_move_after(sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()), existingSheet);
}
else if (indexOfSheet(sheetName) > index) {
sheetsNode(xmlDocument())
.insert_move_before(sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()), existingSheet);
}
}
// ===== Updated defined names with worksheet scopes.
for (auto& definedName : xmlDocument().document_element().child("definedNames").children()) {
definedName.attribute("localSheetId").set_value(indexOfSheet(sheetName) - 1);
}
// ===== Update the activeTab attribute.
// unsigned int index = 0;
// for (auto& item : getSheetsNode().children()) {
// if (m_activeSheet == item) {
// XmlDocument().first_child().child("bookViews").first_child().attribute("activeTab").set_value(index);
// break;
// }
// index++;
// }
}
/**
* @details
*/
unsigned int XLWorkbook::indexOfSheet(const std::string& sheetName) const
{
// ===== Iterate through sheet nodes. When a match is found, return the index;
unsigned int index = 1;
for (auto& sheet : sheetsNode(xmlDocument()).children()) {
if (sheetName == sheet.attribute("name").value()) return index;
index++;
}
// ===== If a match is not found, throw an exception.
throw XLInputError("Sheet does not exist");
}
/**
* @details
*/
XLSheetType XLWorkbook::typeOfSheet(const std::string& sheetName) const
{
if (!sheetExists(sheetName)) throw XLInputError("Sheet with name \"" + sheetName + "\" doesn't exist.");
if (worksheetExists(sheetName))
return XLSheetType::Worksheet;
return XLSheetType::Chartsheet;
}
/**
* @details
*/
XLSheetType XLWorkbook::typeOfSheet(unsigned int index) const
{
std::string name =
std::vector<XMLNode>(sheetsNode(xmlDocument()).begin(), sheetsNode(xmlDocument()).end())[index - 1].attribute("name").as_string();
return typeOfSheet(name);
}
/**
* @details
*/
unsigned int XLWorkbook::sheetCount() const
{
return static_cast<unsigned int>(
std::distance(sheetsNode(xmlDocument()).children().begin(), sheetsNode(xmlDocument()).children().end()));
}
/**
* @details
*/
unsigned int XLWorkbook::worksheetCount() const
{
return static_cast<unsigned int>(worksheetNames().size());
}
/**
* @details
*/
unsigned int XLWorkbook::chartsheetCount() const
{
return static_cast<unsigned int>(chartsheetNames().size());
}
/**
* @details
*/
std::vector<std::string> XLWorkbook::sheetNames() const
{
std::vector<std::string> results;
for (const auto& item : sheetsNode(xmlDocument()).children()) results.emplace_back(item.attribute("name").value());
return results;
}
/**
* @details
*/
std::vector<std::string> XLWorkbook::worksheetNames() const
{
std::vector<std::string> results;
for (const auto& item : sheetsNode(xmlDocument()).children()) {
XLQuery query(XLQueryType::QuerySheetType);
query.setParam("sheetID", std::string(item.attribute("r:id").value()));
if (parentDoc().execQuery(query).result<XLContentType>() == XLContentType::Worksheet)
results.emplace_back(item.attribute("name").value());
}
return results;
}
/**
* @details
*/
std::vector<std::string> XLWorkbook::chartsheetNames() const
{
std::vector<std::string> results;
for (const auto& item : sheetsNode(xmlDocument()).children()) {
XLQuery query(XLQueryType::QuerySheetType);
query.setParam("sheetID", std::string(item.attribute("r:id").value()));
if (parentDoc().execQuery(query).result<XLContentType>() == XLContentType::Chartsheet)
results.emplace_back(item.attribute("name").value());
}
return results;
}
/**
* @details
*/
bool XLWorkbook::sheetExists(const std::string& sheetName) const
{
return chartsheetExists(sheetName) || worksheetExists(sheetName);
}
/**
* @details
*/
bool XLWorkbook::worksheetExists(const std::string& sheetName) const
{
auto wksNames = worksheetNames();
return std::find(wksNames.begin(), wksNames.end(), sheetName) != wksNames.end();
}
/**
* @details
*/
bool XLWorkbook::chartsheetExists(const std::string& sheetName) const
{
auto chsNames = chartsheetNames();
return std::find(chsNames.begin(), chsNames.end(), sheetName) != chsNames.end();
}
/**
* @details The UpdateSheetName member function searches throug the usages of the old name and replaces with the
* new sheet name.
* @todo Currently, this function only searches through defined names. Consider using this function to update the
* actual sheet name as well.
*/
void XLWorkbook::updateSheetReferences(const std::string& oldName, const std::string& newName)
{
// for (auto& sheet : m_sheets) {
// if (sheet.sheetType == XLSheetType::WorkSheet)
// Worksheet(sheet.sheetNode.attribute("name").getValue())->UpdateSheetName(oldName, newName);
// }
// ===== Set up temporary variables
std::string oldNameTemp = oldName;
std::string newNameTemp = newName;
std::string formula;
// ===== If the sheet name contains spaces, it should be enclosed in single quotes (')
if (oldName.find(' ') != std::string::npos) oldNameTemp = "\'" + oldName + "\'";
if (newName.find(' ') != std::string::npos) newNameTemp = "\'" + newName + "\'";
// ===== Ensure only sheet names are replaced (references to sheets always ends with a '!')
oldNameTemp += '!';
newNameTemp += '!';
// ===== Iterate through all defined names
for (auto& definedName : xmlDocument().document_element().child("definedNames").children()) {
formula = definedName.text().get();
// ===== Skip if formula contains a '[' and ']' (means that the defined refers to external workbook)
if (formula.find('[') == std::string::npos && formula.find(']') == std::string::npos) {
// ===== For all instances of the old sheet name in the formula, replace with the new name.
while (formula.find(oldNameTemp) != std::string::npos) { // NOLINT
formula.replace(formula.find(oldNameTemp), oldNameTemp.length(), newNameTemp);
}
definedName.text().set(formula.c_str());
}
}
}
/**
* @details
*/
void XLWorkbook::setFullCalculationOnLoad()
{
auto calcPr = xmlDocument().document_element().child("calcPr");
auto getOrCreateAttribute = [&calcPr](const char * attributeName)
{
auto attr = calcPr.attribute(attributeName);
if (!attr)
attr = calcPr.append_attribute(attributeName);
return attr;
};
getOrCreateAttribute("forceFullCalc").set_value(true);
getOrCreateAttribute("fullCalcOnLoad").set_value(true);
}
/**
* @details
*/
bool XLWorkbook::sheetIsActive(const std::string& sheetRID) const
{
auto activeTabAttribute = xmlDocument().document_element().child("bookViews").first_child().attribute("activeTab");
auto activeTabIndex = (activeTabAttribute ? activeTabAttribute.as_uint() : 0);
unsigned int index = 0;
for (const auto& item : sheetsNode(xmlDocument()).children()){
if (std::string(item.attribute("r:id").value()) == sheetRID) break;
++index;
}
return index == activeTabIndex;
}
/**
* @details
*/
void XLWorkbook::setSheetActive(const std::string& sheetRID) {
unsigned int index = 0;
for (const auto& item : sheetsNode(xmlDocument()).children()){
if (std::string(item.attribute("r:id").value()) == sheetRID && std::string(item.attribute("state").value()) != "hidden") break;
if (item == sheetsNode(xmlDocument()).last_child()) {
index = 0;
break;
}
++index;
}
if (index == 0) {
xmlDocument().document_element().child("bookViews").first_child().remove_attribute("activeTab");
}
else {
if (xmlDocument().document_element().child("bookViews").first_child().attribute("activeTab") == XMLAttribute())
xmlDocument().document_element().child("bookViews").first_child().append_attribute("activeTab");
xmlDocument().document_element().child("bookViews").first_child().attribute("activeTab").set_value(index);
}
}