Version 6.4.0.3, tag libreoffice-6.4.0.3
[LibreOffice.git] / sc / source / filter / excel / xepivotxml.cxx
blobe5af3d9248631567593442f23db73f3a7e6343c9
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 */
10 #include <xepivotxml.hxx>
11 #include <dpcache.hxx>
12 #include <dpdimsave.hxx>
13 #include <dpitemdata.hxx>
14 #include <dpobject.hxx>
15 #include <dpsave.hxx>
16 #include <dputil.hxx>
17 #include <document.hxx>
18 #include <generalfunction.hxx>
19 #include <unonames.hxx>
20 #include <xestyle.hxx>
21 #include <xeroot.hxx>
23 #include <o3tl/temporary.hxx>
24 #include <oox/export/utils.hxx>
25 #include <oox/token/namespaces.hxx>
26 #include <sax/tools/converter.hxx>
27 #include <sax/fastattribs.hxx>
29 #include <com/sun/star/beans/XPropertySet.hpp>
30 #include <com/sun/star/sheet/DataPilotFieldGroupBy.hpp>
31 #include <com/sun/star/sheet/DataPilotFieldOrientation.hpp>
32 #include <com/sun/star/sheet/DataPilotFieldLayoutMode.hpp>
33 #include <com/sun/star/sheet/DataPilotOutputRangeType.hpp>
34 #include <com/sun/star/sheet/XDimensionsSupplier.hpp>
36 #include <vector>
38 using namespace oox;
39 using namespace com::sun::star;
41 namespace {
43 void savePivotCacheRecordsXml( XclExpXmlStream& rStrm, const ScDPCache& rCache )
45 SCROW nCount = rCache.GetDataSize();
46 size_t nFieldCount = rCache.GetFieldCount();
48 sax_fastparser::FSHelperPtr& pRecStrm = rStrm.GetCurrentStream();
49 pRecStrm->startElement(XML_pivotCacheRecords,
50 XML_xmlns, rStrm.getNamespaceURL(OOX_NS(xls)).toUtf8(),
51 FSNS(XML_xmlns, XML_r), rStrm.getNamespaceURL(OOX_NS(officeRel)).toUtf8(),
52 XML_count, OString::number(static_cast<long>(nCount)));
54 for (SCROW i = 0; i < nCount; ++i)
56 pRecStrm->startElement(XML_r);
57 for (size_t nField = 0; nField < nFieldCount; ++nField)
59 const ScDPCache::IndexArrayType* pArray = rCache.GetFieldIndexArray(nField);
60 assert(pArray);
61 assert(static_cast<size_t>(i) < pArray->size());
63 // We are using XML_x reference (like: <x v="0"/>), instead of values here (eg: <s v="No Discount"/>).
64 // That's why in SavePivotCacheXml method, we need to list all items.
65 pRecStrm->singleElement(XML_x, XML_v, OString::number((*pArray)[i]));
67 pRecStrm->endElement(XML_r);
70 pRecStrm->endElement(XML_pivotCacheRecords);
73 const char* toOOXMLAxisType( sheet::DataPilotFieldOrientation eOrient )
75 switch (eOrient)
77 case sheet::DataPilotFieldOrientation_COLUMN:
78 return "axisCol";
79 case sheet::DataPilotFieldOrientation_ROW:
80 return "axisRow";
81 case sheet::DataPilotFieldOrientation_PAGE:
82 return "axisPage";
83 case sheet::DataPilotFieldOrientation_DATA:
84 return "axisValues";
85 case sheet::DataPilotFieldOrientation_HIDDEN:
86 default:
90 return "";
93 const char* toOOXMLSubtotalType(ScGeneralFunction eFunc)
95 switch (eFunc)
97 case ScGeneralFunction::SUM:
98 return "sum";
99 case ScGeneralFunction::COUNT:
100 return "count";
101 case ScGeneralFunction::AVERAGE:
102 return "average";
103 case ScGeneralFunction::MAX:
104 return "max";
105 case ScGeneralFunction::MIN:
106 return "min";
107 case ScGeneralFunction::PRODUCT:
108 return "product";
109 case ScGeneralFunction::COUNTNUMS:
110 return "countNums";
111 case ScGeneralFunction::STDEV:
112 return "stdDev";
113 case ScGeneralFunction::STDEVP:
114 return "stdDevp";
115 case ScGeneralFunction::VAR:
116 return "var";
117 case ScGeneralFunction::VARP:
118 return "varp";
119 default:
122 return nullptr;
127 XclExpXmlPivotCaches::XclExpXmlPivotCaches( const XclExpRoot& rRoot ) :
128 XclExpRoot(rRoot) {}
130 void XclExpXmlPivotCaches::SaveXml( XclExpXmlStream& rStrm )
132 sax_fastparser::FSHelperPtr& pWorkbookStrm = rStrm.GetCurrentStream();
133 pWorkbookStrm->startElement(XML_pivotCaches);
135 for (size_t i = 0, n = maCaches.size(); i < n; ++i)
137 const Entry& rEntry = maCaches[i];
139 sal_Int32 nCacheId = i + 1;
140 OUString aRelId;
141 sax_fastparser::FSHelperPtr pPCStrm = rStrm.CreateOutputStream(
142 XclXmlUtils::GetStreamName("xl/pivotCache/", "pivotCacheDefinition", nCacheId),
143 XclXmlUtils::GetStreamName(nullptr, "pivotCache/pivotCacheDefinition", nCacheId),
144 rStrm.GetCurrentStream()->getOutputStream(),
145 CREATE_XL_CONTENT_TYPE("pivotCacheDefinition"),
146 CREATE_OFFICEDOC_RELATION_TYPE("pivotCacheDefinition"),
147 &aRelId);
149 pWorkbookStrm->singleElement(XML_pivotCache,
150 XML_cacheId, OString::number(nCacheId),
151 FSNS(XML_r, XML_id), aRelId.toUtf8());
153 rStrm.PushStream(pPCStrm);
154 SavePivotCacheXml(rStrm, rEntry, nCacheId);
155 rStrm.PopStream();
158 pWorkbookStrm->endElement(XML_pivotCaches);
161 void XclExpXmlPivotCaches::SetCaches( const std::vector<Entry>& rCaches )
163 maCaches = rCaches;
166 bool XclExpXmlPivotCaches::HasCaches() const
168 return !maCaches.empty();
171 const XclExpXmlPivotCaches::Entry* XclExpXmlPivotCaches::GetCache( sal_Int32 nCacheId ) const
173 if (nCacheId <= 0)
174 // cache ID is 1-based.
175 return nullptr;
177 size_t nPos = nCacheId - 1;
178 if (nPos >= maCaches.size())
179 return nullptr;
181 return &maCaches[nPos];
184 namespace {
186 * Create combined date and time string according the requirements of Excel.
187 * A single point in time can be represented by concatenating a complete date expression,
188 * the letter T as a delimiter, and a valid time expression. For example, "2007-04-05T14:30".
190 * fSerialDateTime - a number representing the number of days since 1900-Jan-0 (integer portion of the number),
191 * plus a fractional portion of a 24 hour day (fractional portion of the number).
193 OUString GetExcelFormattedDate( double fSerialDateTime, const SvNumberFormatter& rFormatter )
195 // tdf#125055: properly round the value to seconds when truncating nanoseconds below
196 constexpr double fHalfSecond = 1 / 86400.0 * 0.5;
197 css::util::DateTime aUDateTime
198 = (DateTime(rFormatter.GetNullDate()) + fSerialDateTime + fHalfSecond).GetUNODateTime();
199 // We need to reset nanoseconds, to avoid string like: "1982-02-18T16:04:47.999999849"
200 aUDateTime.NanoSeconds = 0;
201 OUStringBuffer sBuf;
202 ::sax::Converter::convertDateTime(sBuf, aUDateTime, nullptr, true);
203 return sBuf.makeStringAndClear();
206 // Excel seems to expect different order of group item values; we need to rearrange elements
207 // to output "<date1" first, then elements, then ">date2" last.
208 // Since ScDPItemData::DateFirst is -1, ScDPItemData::DateLast is 10000, and other date group
209 // items would fit between those in order (like 0 = Jan, 1 = Feb, etc.), we can simply sort
210 // the items by value.
211 std::vector<OUString> SortGroupItems(const ScDPCache& rCache, long nDim)
213 struct ItemData
215 sal_Int32 nVal;
216 const ScDPItemData* pData;
218 std::vector<ItemData> aDataToSort;
219 ScfInt32Vec aGIIds;
220 rCache.GetGroupDimMemberIds(nDim, aGIIds);
221 for (sal_Int32 id : aGIIds)
223 const ScDPItemData* pGIData = rCache.GetItemDataById(nDim, id);
224 if (pGIData->GetType() == ScDPItemData::GroupValue)
226 auto aGroupVal = pGIData->GetGroupValue();
227 aDataToSort.push_back({ aGroupVal.mnValue, pGIData });
230 std::sort(aDataToSort.begin(), aDataToSort.end(),
231 [](const ItemData& a, const ItemData& b) { return a.nVal < b.nVal; });
233 std::vector<OUString> aSortedResult;
234 for (const auto& el : aDataToSort)
236 aSortedResult.push_back(rCache.GetFormattedString(nDim, *el.pData, false));
238 return aSortedResult;
240 } // namespace
242 void XclExpXmlPivotCaches::SavePivotCacheXml( XclExpXmlStream& rStrm, const Entry& rEntry, sal_Int32 nCounter )
244 assert(rEntry.mpCache);
245 const ScDPCache& rCache = *rEntry.mpCache;
247 sax_fastparser::FSHelperPtr& pDefStrm = rStrm.GetCurrentStream();
249 OUString aRelId;
250 sax_fastparser::FSHelperPtr pRecStrm = rStrm.CreateOutputStream(
251 XclXmlUtils::GetStreamName("xl/pivotCache/", "pivotCacheRecords", nCounter),
252 XclXmlUtils::GetStreamName(nullptr, "pivotCacheRecords", nCounter),
253 pDefStrm->getOutputStream(),
254 CREATE_XL_CONTENT_TYPE("pivotCacheRecords"),
255 CREATE_OFFICEDOC_RELATION_TYPE("pivotCacheRecords"),
256 &aRelId);
258 rStrm.PushStream(pRecStrm);
259 savePivotCacheRecordsXml(rStrm, rCache);
260 rStrm.PopStream();
262 pDefStrm->startElement(XML_pivotCacheDefinition,
263 XML_xmlns, rStrm.getNamespaceURL(OOX_NS(xls)).toUtf8(),
264 FSNS(XML_xmlns, XML_r), rStrm.getNamespaceURL(OOX_NS(officeRel)).toUtf8(),
265 FSNS(XML_r, XML_id), aRelId.toUtf8(),
266 XML_recordCount, OString::number(rEntry.mpCache->GetDataSize()),
267 XML_createdVersion, "3"); // MS Excel 2007, tdf#112936: setting version number makes MSO to handle the pivot table differently
269 pDefStrm->startElement(XML_cacheSource, XML_type, "worksheet");
271 OUString aSheetName;
272 GetDoc().GetName(rEntry.maSrcRange.aStart.Tab(), aSheetName);
273 pDefStrm->singleElement(XML_worksheetSource,
274 XML_ref, XclXmlUtils::ToOString(&rStrm.GetRoot().GetDoc(), rEntry.maSrcRange),
275 XML_sheet, aSheetName.toUtf8());
277 pDefStrm->endElement(XML_cacheSource);
279 size_t nCount = rCache.GetFieldCount();
280 const size_t nGroupFieldCount = rCache.GetGroupFieldCount();
281 pDefStrm->startElement(XML_cacheFields,
282 XML_count, OString::number(static_cast<long>(nCount + nGroupFieldCount)));
284 auto WriteFieldGroup = [this, &rCache, pDefStrm](size_t i, size_t base) {
285 const sal_Int32 nDatePart = rCache.GetGroupType(i);
286 if (!nDatePart)
287 return;
288 OString sGroupBy;
289 switch (nDatePart)
291 case sheet::DataPilotFieldGroupBy::SECONDS:
292 sGroupBy = "seconds";
293 break;
294 case sheet::DataPilotFieldGroupBy::MINUTES:
295 sGroupBy = "minutes";
296 break;
297 case sheet::DataPilotFieldGroupBy::HOURS:
298 sGroupBy = "hours";
299 break;
300 case sheet::DataPilotFieldGroupBy::DAYS:
301 sGroupBy = "days";
302 break;
303 case sheet::DataPilotFieldGroupBy::MONTHS:
304 sGroupBy = "months";
305 break;
306 case sheet::DataPilotFieldGroupBy::QUARTERS:
307 sGroupBy = "quarters";
308 break;
309 case sheet::DataPilotFieldGroupBy::YEARS:
310 sGroupBy = "years";
311 break;
314 // fieldGroup element
315 pDefStrm->startElement(XML_fieldGroup, XML_base, OString::number(base));
317 SvNumberFormatter& rFormatter = GetFormatter();
319 // rangePr element
320 const ScDPNumGroupInfo* pGI = rCache.GetNumGroupInfo(i);
321 auto pGroupAttList = sax_fastparser::FastSerializerHelper::createAttrList();
322 pGroupAttList->add(XML_groupBy, sGroupBy);
323 // Possible TODO: find out when to write autoStart attribute for years grouping
324 pGroupAttList->add(XML_startDate, GetExcelFormattedDate(pGI->mfStart, rFormatter).toUtf8());
325 pGroupAttList->add(XML_endDate, GetExcelFormattedDate(pGI->mfEnd, rFormatter).toUtf8());
326 if (pGI->mfStep)
327 pGroupAttList->add(XML_groupInterval, OString::number(pGI->mfStep));
328 pDefStrm->singleElement(XML_rangePr, pGroupAttList);
330 // groupItems element
331 auto aElemVec = SortGroupItems(rCache, i);
332 pDefStrm->startElement(XML_groupItems, XML_count, OString::number(aElemVec.size()));
333 for (const auto& sElem : aElemVec)
335 pDefStrm->singleElement(XML_s, XML_v, sElem.toUtf8());
337 pDefStrm->endElement(XML_groupItems);
338 pDefStrm->endElement(XML_fieldGroup);
341 for (size_t i = 0; i < nCount; ++i)
343 OUString aName = rCache.GetDimensionName(i);
345 pDefStrm->startElement(XML_cacheField,
346 XML_name, aName.toUtf8(),
347 XML_numFmtId, OString::number(0));
349 const ScDPCache::ScDPItemDataVec& rFieldItems = rCache.GetDimMemberValues(i);
351 std::set<ScDPItemData::Type> aDPTypes;
352 double fMin = std::numeric_limits<double>::infinity(), fMax = -std::numeric_limits<double>::infinity();
353 bool isValueInteger = true;
354 bool isContainsDate = rCache.IsDateDimension(i);
355 bool isLongText = false;
356 for (const auto& rFieldItem : rFieldItems)
358 ScDPItemData::Type eType = rFieldItem.GetType();
359 // tdf#123939 : error and string are same for cache; if both are present, keep only one
360 if (eType == ScDPItemData::Error)
361 eType = ScDPItemData::String;
362 aDPTypes.insert(eType);
363 if (eType == ScDPItemData::Value)
365 double fVal = rFieldItem.GetValue();
366 fMin = std::min(fMin, fVal);
367 fMax = std::max(fMax, fVal);
369 // Check if all values are integers
370 if (isValueInteger && (modf(fVal, &o3tl::temporary(double())) != 0.0))
372 isValueInteger = false;
375 else if (eType == ScDPItemData::String && !isLongText)
377 isLongText = rFieldItem.GetString().getLength() > 255;
381 auto pAttList = sax_fastparser::FastSerializerHelper::createAttrList();
382 // TODO In same cases, disable listing of items, as it is done in MS Excel.
383 // Exporting savePivotCacheRecordsXml method needs to be updated accordingly
384 //bool bListItems = true;
386 std::set<ScDPItemData::Type> aDPTypesWithoutBlank = aDPTypes;
387 aDPTypesWithoutBlank.erase(ScDPItemData::Empty);
389 const bool isContainsString = aDPTypesWithoutBlank.count(ScDPItemData::String) > 0;
390 const bool isContainsBlank = aDPTypes.count(ScDPItemData::Empty) > 0;
391 const bool isContainsNumber
392 = !isContainsDate && aDPTypesWithoutBlank.count(ScDPItemData::Value) > 0;
393 bool isContainsNonDate = !(isContainsDate && aDPTypesWithoutBlank.size() <= 1);
395 // XML_containsSemiMixedTypes possible values:
396 // 1 - (Default) at least one text value, or can also contain a mix of other data types and blank values,
397 // or blank values only
398 // 0 - the field does not have a mix of text and other values
399 if (!(isContainsString || (aDPTypes.size() > 1) || (isContainsBlank && aDPTypesWithoutBlank.empty())))
400 pAttList->add(XML_containsSemiMixedTypes, ToPsz10(false));
402 if (!isContainsNonDate)
403 pAttList->add(XML_containsNonDate, ToPsz10(false));
405 if (isContainsDate)
406 pAttList->add(XML_containsDate, ToPsz10(true));
408 // default for containsString field is true, so we are writing only when is false
409 if (!isContainsString)
410 pAttList->add(XML_containsString, ToPsz10(false));
412 if (isContainsBlank)
413 pAttList->add(XML_containsBlank, ToPsz10(true));
415 // XML_containsMixedType possible values:
416 // 1 - field contains more than one data type
417 // 0 - (Default) only one data type. The field can still contain blank values (that's why we are using aDPTypesWithoutBlank)
418 if (aDPTypesWithoutBlank.size() > 1)
419 pAttList->add(XML_containsMixedTypes, ToPsz10(true));
421 // If field contain mixed types (Date and Numbers), MS Excel is saving only "minDate" and "maxDate" and not "minValue" and "maxValue"
422 // Example how Excel is saving mixed Date and Numbers:
423 // <sharedItems containsSemiMixedTypes="0" containsDate="1" containsString="0" containsMixedTypes="1" minDate="1900-01-03T22:26:04" maxDate="1900-01-07T14:02:04" />
424 // Example how Excel is saving Dates only:
425 // <sharedItems containsSemiMixedTypes="0" containsNonDate="0" containsDate="1" containsString="0" minDate="1903-08-24T07:40:48" maxDate="2024-05-23T07:12:00"/>
426 if (isContainsNumber)
427 pAttList->add(XML_containsNumber, ToPsz10(true));
429 if (isValueInteger && isContainsNumber)
430 pAttList->add(XML_containsInteger, ToPsz10(true));
433 // Number type fields could be mixed with blank types, and it shouldn't be treated as listed items.
434 // Example:
435 // <cacheField name="employeeID" numFmtId="0">
436 // <sharedItems containsString="0" containsBlank="1" containsNumber="1" containsInteger="1" minValue="35" maxValue="89"/>
437 // </cacheField>
438 if (isContainsNumber)
440 pAttList->add(XML_minValue, OString::number(fMin));
441 pAttList->add(XML_maxValue, OString::number(fMax));
444 if (isContainsDate)
446 pAttList->add(XML_minDate, GetExcelFormattedDate(fMin, GetFormatter()).toUtf8());
447 pAttList->add(XML_maxDate, GetExcelFormattedDate(fMax, GetFormatter()).toUtf8());
450 //if (bListItems) // see TODO above
452 pAttList->add(XML_count, OString::number(static_cast<long>(rFieldItems.size())));
455 if (isLongText)
457 pAttList->add(XML_longText, ToPsz10(true));
460 sax_fastparser::XFastAttributeListRef xAttributeList(pAttList);
462 pDefStrm->startElement(XML_sharedItems, xAttributeList);
464 //if (bListItems) // see TODO above
466 for (const ScDPItemData& rItem : rFieldItems)
468 switch (rItem.GetType())
470 case ScDPItemData::String:
471 pDefStrm->singleElement(XML_s, XML_v, rItem.GetString().toUtf8());
472 break;
473 case ScDPItemData::Value:
474 if (isContainsDate)
476 pDefStrm->singleElement(XML_d,
477 XML_v, GetExcelFormattedDate(rItem.GetValue(), GetFormatter()).toUtf8());
479 else
480 pDefStrm->singleElement(XML_n,
481 XML_v, OString::number(rItem.GetValue()));
482 break;
483 case ScDPItemData::Empty:
484 pDefStrm->singleElement(XML_m);
485 break;
486 case ScDPItemData::Error:
487 pDefStrm->singleElement(XML_e,
488 XML_v, rItem.GetString().toUtf8());
489 break;
490 case ScDPItemData::GroupValue: // Should not happen here!
491 case ScDPItemData::RangeStart:
492 // TODO : What do we do with these types?
493 pDefStrm->singleElement(XML_m);
494 break;
495 default:
501 pDefStrm->endElement(XML_sharedItems);
503 WriteFieldGroup(i, i);
505 pDefStrm->endElement(XML_cacheField);
508 ScDPObject* pDPObject
509 = rCache.GetAllReferences().empty() ? nullptr : *rCache.GetAllReferences().begin();
511 for (size_t i = nCount; pDPObject && i < nCount + nGroupFieldCount; ++i)
513 const OUString aName = pDPObject->GetDimName(i, o3tl::temporary(bool()));
514 // tdf#126748: DPObject might not reference all group fields, when there are several
515 // DPObjects referencing this cache. Trying to get a dimension data for a field not used
516 // in a given DPObject will give nullptr, and dereferencing it then will crash. To avoid
517 // the crash, until there's a correct method to find the names of group fields in cache,
518 // just skip the fields, creating bad cache data, which is of course a temporary hack.
519 // TODO: reimplement the whole block to get the names from another source, not from first
520 // cache reference.
521 if (aName.isEmpty())
522 break;
524 ScDPSaveData* pSaveData = pDPObject->GetSaveData();
525 assert(pSaveData);
527 const ScDPSaveGroupDimension* pDim = pSaveData->GetDimensionData()->GetNamedGroupDim(aName);
528 assert(pDim);
530 const SCCOL nBase = rCache.GetDimensionIndex(pDim->GetSourceDimName());
531 assert(nBase >= 0);
533 pDefStrm->startElement(XML_cacheField, XML_name, aName.toUtf8(), XML_numFmtId,
534 OString::number(0), XML_databaseField, ToPsz10(false));
535 WriteFieldGroup(i, nBase);
536 pDefStrm->endElement(XML_cacheField);
539 pDefStrm->endElement(XML_cacheFields);
541 pDefStrm->endElement(XML_pivotCacheDefinition);
544 XclExpXmlPivotTableManager::XclExpXmlPivotTableManager( const XclExpRoot& rRoot ) :
545 XclExpRoot(rRoot), maCaches(rRoot) {}
547 void XclExpXmlPivotTableManager::Initialize()
549 ScDocument& rDoc = GetDoc();
550 if (!rDoc.HasPivotTable())
551 // No pivot table to export.
552 return;
554 ScDPCollection* pDPColl = rDoc.GetDPCollection();
555 if (!pDPColl)
556 return;
558 // Update caches from DPObject
559 for (size_t i = 0; i < pDPColl->GetCount(); ++i)
561 ScDPObject& rDPObj = (*pDPColl)[i];
562 rDPObj.SyncAllDimensionMembers();
563 (void)rDPObj.GetOutputRangeByType(sheet::DataPilotOutputRangeType::TABLE);
566 // Go through the caches first.
568 std::vector<XclExpXmlPivotCaches::Entry> aCaches;
569 const ScDPCollection::SheetCaches& rSheetCaches = pDPColl->GetSheetCaches();
570 const std::vector<ScRange>& rRanges = rSheetCaches.getAllRanges();
571 for (const auto & rRange : rRanges)
573 const ScDPCache* pCache = rSheetCaches.getExistingCache(rRange);
574 if (!pCache)
575 continue;
577 // Get all pivot objects that reference this cache, and set up an
578 // object to cache ID mapping.
579 const ScDPCache::ScDPObjectSet& rRefs = pCache->GetAllReferences();
580 for (const auto& rRef : rRefs)
581 maCacheIdMap.emplace(rRef, aCaches.size()+1);
583 XclExpXmlPivotCaches::Entry aEntry;
584 aEntry.mpCache = pCache;
585 aEntry.maSrcRange = rRange;
586 aCaches.push_back(aEntry); // Cache ID equals position + 1.
589 // TODO : Handle name and database caches as well.
591 for (size_t i = 0, n = pDPColl->GetCount(); i < n; ++i)
593 const ScDPObject& rDPObj = (*pDPColl)[i];
595 // Get the cache ID for this pivot table.
596 CacheIdMapType::iterator itCache = maCacheIdMap.find(&rDPObj);
597 if (itCache == maCacheIdMap.end())
598 // No cache ID found. Something is wrong here...
599 continue;
601 sal_Int32 nCacheId = itCache->second;
602 SCTAB nTab = rDPObj.GetOutRange().aStart.Tab();
604 TablesType::iterator it = m_Tables.find(nTab);
605 if (it == m_Tables.end())
607 // Insert a new instance for this sheet index.
608 std::pair<TablesType::iterator, bool> r =
609 m_Tables.insert(std::make_pair(nTab, std::make_unique<XclExpXmlPivotTables>(GetRoot(), maCaches)));
610 it = r.first;
613 XclExpXmlPivotTables *const p = it->second.get();
614 p->AppendTable(&rDPObj, nCacheId, i+1);
617 maCaches.SetCaches(aCaches);
620 XclExpXmlPivotCaches& XclExpXmlPivotTableManager::GetCaches()
622 return maCaches;
625 XclExpXmlPivotTables* XclExpXmlPivotTableManager::GetTablesBySheet( SCTAB nTab )
627 TablesType::iterator const it = m_Tables.find(nTab);
628 return it == m_Tables.end() ? nullptr : it->second.get();
631 XclExpXmlPivotTables::Entry::Entry( const ScDPObject* pTable, sal_Int32 nCacheId, sal_Int32 nPivotId ) :
632 mpTable(pTable), mnCacheId(nCacheId), mnPivotId(nPivotId) {}
634 XclExpXmlPivotTables::XclExpXmlPivotTables( const XclExpRoot& rRoot, const XclExpXmlPivotCaches& rCaches ) :
635 XclExpRoot(rRoot), mrCaches(rCaches) {}
637 void XclExpXmlPivotTables::SaveXml( XclExpXmlStream& rStrm )
639 sax_fastparser::FSHelperPtr& pWSStrm = rStrm.GetCurrentStream(); // worksheet stream
641 for (const auto& rTable : maTables)
643 const ScDPObject& rObj = *rTable.mpTable;
644 sal_Int32 nCacheId = rTable.mnCacheId;
645 sal_Int32 nPivotId = rTable.mnPivotId;
647 sax_fastparser::FSHelperPtr pPivotStrm = rStrm.CreateOutputStream(
648 XclXmlUtils::GetStreamName("xl/pivotTables/", "pivotTable", nPivotId),
649 XclXmlUtils::GetStreamName(nullptr, "../pivotTables/pivotTable", nPivotId),
650 pWSStrm->getOutputStream(),
651 CREATE_XL_CONTENT_TYPE("pivotTable"),
652 CREATE_OFFICEDOC_RELATION_TYPE("pivotTable"));
654 rStrm.PushStream(pPivotStrm);
655 SavePivotTableXml(rStrm, rObj, nCacheId);
656 rStrm.PopStream();
660 namespace {
662 struct DataField
664 long const mnPos; // field index in pivot cache.
665 const ScDPSaveDimension* mpDim;
667 DataField( long nPos, const ScDPSaveDimension* pDim ) : mnPos(nPos), mpDim(pDim) {}
670 /** Returns an OOXML subtotal function name string. See ECMA-376-1:2016 18.18.43 */
671 OString GetSubtotalFuncName(ScGeneralFunction eFunc)
673 switch (eFunc)
675 case ScGeneralFunction::SUM: return "sum";
676 case ScGeneralFunction::COUNT: return "count";
677 case ScGeneralFunction::AVERAGE: return "avg";
678 case ScGeneralFunction::MAX: return "max";
679 case ScGeneralFunction::MIN: return "min";
680 case ScGeneralFunction::PRODUCT: return "product";
681 case ScGeneralFunction::COUNTNUMS: return "countA";
682 case ScGeneralFunction::STDEV: return "stdDev";
683 case ScGeneralFunction::STDEVP: return "stdDevP";
684 case ScGeneralFunction::VAR: return "var";
685 case ScGeneralFunction::VARP: return "varP";
686 default:;
688 return "default";
691 sal_Int32 GetSubtotalAttrToken(ScGeneralFunction eFunc)
693 switch (eFunc)
695 case ScGeneralFunction::SUM: return XML_sumSubtotal;
696 case ScGeneralFunction::COUNT: return XML_countSubtotal;
697 case ScGeneralFunction::AVERAGE: return XML_avgSubtotal;
698 case ScGeneralFunction::MAX: return XML_maxSubtotal;
699 case ScGeneralFunction::MIN: return XML_minSubtotal;
700 case ScGeneralFunction::PRODUCT: return XML_productSubtotal;
701 case ScGeneralFunction::COUNTNUMS: return XML_countASubtotal;
702 case ScGeneralFunction::STDEV: return XML_stdDevSubtotal;
703 case ScGeneralFunction::STDEVP: return XML_stdDevPSubtotal;
704 case ScGeneralFunction::VAR: return XML_varSubtotal;
705 case ScGeneralFunction::VARP: return XML_varPSubtotal;
706 default:;
708 return XML_defaultSubtotal;
711 // An item is expected to contain sequences of css::xml::FastAttribute and css::xml::Attribute
712 void WriteGrabBagItemToStream(XclExpXmlStream& rStrm, sal_Int32 tokenId, const css::uno::Any& rItem)
714 if (css::uno::Sequence<css::uno::Any> aSeqs; rItem >>= aSeqs)
716 auto& pStrm = rStrm.GetCurrentStream();
717 pStrm->write("<")->writeId(tokenId);
719 css::uno::Sequence<css::xml::FastAttribute> aFastSeq;
720 css::uno::Sequence<css::xml::Attribute> aUnkSeq;
721 for (const auto& a : std::as_const(aSeqs))
723 if (a >>= aFastSeq)
725 for (const auto& rAttr : std::as_const(aFastSeq))
726 rStrm.WriteAttributes(rAttr.Token, rAttr.Value);
728 else if (a >>= aUnkSeq)
730 for (const auto& rAttr : std::as_const(aUnkSeq))
731 pStrm->write(" ")
732 ->write(rAttr.Name)
733 ->write("=\"")
734 ->writeEscaped(rAttr.Value)
735 ->write("\"");
739 pStrm->write("/>");
744 void XclExpXmlPivotTables::SavePivotTableXml( XclExpXmlStream& rStrm, const ScDPObject& rDPObj, sal_Int32 nCacheId )
746 typedef std::unordered_map<OUString, long> NameToIdMapType;
748 const XclExpXmlPivotCaches::Entry* pCacheEntry = mrCaches.GetCache(nCacheId);
749 if (!pCacheEntry)
750 // Something is horribly wrong. Check your logic.
751 return;
753 const ScDPCache& rCache = *pCacheEntry->mpCache;
755 const ScDPSaveData& rSaveData = *rDPObj.GetSaveData();
757 size_t nFieldCount = rCache.GetFieldCount() + rCache.GetGroupFieldCount();
758 std::vector<const ScDPSaveDimension*> aCachedDims;
759 NameToIdMapType aNameToIdMap;
761 aCachedDims.reserve(nFieldCount);
762 for (size_t i = 0; i < nFieldCount; ++i)
764 OUString aName = const_cast<ScDPObject&>(rDPObj).GetDimName(i, o3tl::temporary(bool()));
765 aNameToIdMap.emplace(aName, aCachedDims.size());
766 const ScDPSaveDimension* pDim = rSaveData.GetExistingDimensionByName(aName);
767 aCachedDims.push_back(pDim);
770 std::vector<long> aRowFields;
771 std::vector<long> aColFields;
772 std::vector<long> aPageFields;
773 std::vector<DataField> aDataFields;
775 long nDataDimCount = rSaveData.GetDataDimensionCount();
776 // Use dimensions in the save data to get their correct ordering.
777 // Dimension order here is significant as they specify the order of
778 // appearance in each axis.
779 const ScDPSaveData::DimsType& rDims = rSaveData.GetDimensions();
780 bool bTabularMode = false;
781 for (const auto & i : rDims)
783 const ScDPSaveDimension& rDim = *i;
785 long nPos = -1; // position in cache
786 if (rDim.IsDataLayout())
787 nPos = -2; // Excel uses an index of -2 to indicate a data layout field.
788 else
790 OUString aSrcName = ScDPUtil::getSourceDimensionName(rDim.GetName());
791 NameToIdMapType::iterator it = aNameToIdMap.find(aSrcName);
792 if (it != aNameToIdMap.end())
793 nPos = it->second;
795 if (nPos == -1)
796 continue;
798 if (!aCachedDims[nPos])
799 continue;
802 sheet::DataPilotFieldOrientation eOrient = rDim.GetOrientation();
804 switch (eOrient)
806 case sheet::DataPilotFieldOrientation_COLUMN:
807 if (nPos == -2 && nDataDimCount <= 1)
808 break;
809 aColFields.push_back(nPos);
810 break;
811 case sheet::DataPilotFieldOrientation_ROW:
812 aRowFields.push_back(nPos);
813 break;
814 case sheet::DataPilotFieldOrientation_PAGE:
815 aPageFields.push_back(nPos);
816 break;
817 case sheet::DataPilotFieldOrientation_DATA:
818 aDataFields.emplace_back(nPos, &rDim);
819 break;
820 case sheet::DataPilotFieldOrientation_HIDDEN:
821 default:
824 if(rDim.GetLayoutInfo())
825 bTabularMode |= (rDim.GetLayoutInfo()->LayoutMode == sheet::DataPilotFieldLayoutMode::TABULAR_LAYOUT);
828 sax_fastparser::FSHelperPtr& pPivotStrm = rStrm.GetCurrentStream();
829 pPivotStrm->startElement(XML_pivotTableDefinition,
830 XML_xmlns, rStrm.getNamespaceURL(OOX_NS(xls)).toUtf8(),
831 XML_name, rDPObj.GetName().toUtf8(),
832 XML_cacheId, OString::number(nCacheId),
833 XML_applyNumberFormats, ToPsz10(false),
834 XML_applyBorderFormats, ToPsz10(false),
835 XML_applyFontFormats, ToPsz10(false),
836 XML_applyPatternFormats, ToPsz10(false),
837 XML_applyAlignmentFormats, ToPsz10(false),
838 XML_applyWidthHeightFormats, ToPsz10(false),
839 XML_dataCaption, "Values",
840 XML_useAutoFormatting, ToPsz10(false),
841 XML_itemPrintTitles, ToPsz10(true),
842 XML_indent, ToPsz10(false),
843 XML_outline, ToPsz10(!bTabularMode),
844 XML_outlineData, ToPsz10(!bTabularMode),
845 XML_compact, ToPsz10(false),
846 XML_compactData, ToPsz10(false));
848 // NB: Excel's range does not include page field area (if any).
849 ScRange aOutRange = rDPObj.GetOutputRangeByType(sheet::DataPilotOutputRangeType::TABLE);
851 sal_Int32 nFirstHeaderRow = rDPObj.GetHeaderLayout() ? 2 : 1;
852 sal_Int32 nFirstDataRow = 2;
853 sal_Int32 nFirstDataCol = 1;
854 ScRange aResRange = rDPObj.GetOutputRangeByType(sheet::DataPilotOutputRangeType::RESULT);
856 if (!aOutRange.IsValid())
857 aOutRange = rDPObj.GetOutRange();
859 if (aOutRange.IsValid() && aResRange.IsValid())
861 nFirstDataRow = aResRange.aStart.Row() - aOutRange.aStart.Row();
862 nFirstDataCol = aResRange.aStart.Col() - aOutRange.aStart.Col();
865 pPivotStrm->write("<")->writeId(XML_location);
866 rStrm.WriteAttributes(XML_ref,
867 XclXmlUtils::ToOString(&rStrm.GetRoot().GetDoc(), aOutRange),
868 XML_firstHeaderRow, OUString::number(nFirstHeaderRow),
869 XML_firstDataRow, OUString::number(nFirstDataRow),
870 XML_firstDataCol, OUString::number(nFirstDataCol));
872 if (!aPageFields.empty())
874 rStrm.WriteAttributes(XML_rowPageCount, OUString::number(static_cast<long>(aPageFields.size())));
875 rStrm.WriteAttributes(XML_colPageCount, OUString::number(1));
878 pPivotStrm->write("/>");
880 // <pivotFields> - It must contain all fields in the pivot cache even if
881 // only some of them are used in the pivot table. The order must be as
882 // they appear in the cache.
884 pPivotStrm->startElement(XML_pivotFields,
885 XML_count, OString::number(static_cast<long>(aCachedDims.size())));
887 for (size_t i = 0; i < nFieldCount; ++i)
889 const ScDPSaveDimension* pDim = aCachedDims[i];
890 if (!pDim)
892 pPivotStrm->singleElement(XML_pivotField,
893 XML_compact, ToPsz10(false),
894 XML_showAll, ToPsz10(false));
895 continue;
898 bool bDimInTabularMode = false;
899 if(pDim->GetLayoutInfo())
900 bDimInTabularMode = (pDim->GetLayoutInfo()->LayoutMode == sheet::DataPilotFieldLayoutMode::TABULAR_LAYOUT);
902 sheet::DataPilotFieldOrientation eOrient = pDim->GetOrientation();
904 if (eOrient == sheet::DataPilotFieldOrientation_HIDDEN)
906 if(bDimInTabularMode)
908 pPivotStrm->singleElement(XML_pivotField,
909 XML_compact, ToPsz10(false),
910 XML_showAll, ToPsz10(false),
911 XML_outline, ToPsz10(false));
913 else
915 pPivotStrm->singleElement(XML_pivotField,
916 XML_compact, ToPsz10(false),
917 XML_showAll, ToPsz10(false));
919 continue;
922 if (eOrient == sheet::DataPilotFieldOrientation_DATA)
924 if(bDimInTabularMode)
926 pPivotStrm->singleElement(XML_pivotField,
927 XML_dataField, ToPsz10(true),
928 XML_compact, ToPsz10(false),
929 XML_showAll, ToPsz10(false),
930 XML_outline, ToPsz10(false));
932 else
934 pPivotStrm->singleElement(XML_pivotField,
935 XML_dataField, ToPsz10(true),
936 XML_compact, ToPsz10(false),
937 XML_showAll, ToPsz10(false));
939 continue;
942 // Dump field items.
943 std::vector<ScDPLabelData::Member> aMembers;
945 // We need to get the members in actual order, getting which requires non-const reference here
946 auto& dpo = const_cast<ScDPObject&>(rDPObj);
947 dpo.GetMembers(i, dpo.GetUsedHierarchy(i), aMembers);
950 std::vector<OUString> aCacheFieldItems;
951 if (i < rCache.GetFieldCount() && !rCache.GetGroupType(i))
953 for (const auto& it : rCache.GetDimMemberValues(i))
955 OUString sFormattedName;
956 if (it.HasStringData() || it.IsEmpty())
957 sFormattedName = it.GetString();
958 else
959 sFormattedName = const_cast<ScDPObject&>(rDPObj).GetFormattedString(
960 pDim->GetName(), it.GetValue());
961 aCacheFieldItems.push_back(sFormattedName);
964 else
966 aCacheFieldItems = SortGroupItems(rCache, i);
968 // The pair contains the member index in cache and if it is hidden
969 std::vector< std::pair<size_t, bool> > aMemberSequence;
970 std::set<size_t> aUsedCachePositions;
971 for (const auto & rMember : aMembers)
973 auto it = std::find(aCacheFieldItems.begin(), aCacheFieldItems.end(), rMember.maName);
974 if (it != aCacheFieldItems.end())
976 size_t nCachePos = static_cast<size_t>(std::distance(aCacheFieldItems.begin(), it));
977 auto aInserted = aUsedCachePositions.insert(nCachePos);
978 if (aInserted.second)
979 aMemberSequence.emplace_back(std::make_pair(nCachePos, !rMember.mbVisible));
982 // Now add all remaining cache items as hidden
983 for (size_t nItem = 0; nItem < aCacheFieldItems.size(); ++nItem)
985 if (aUsedCachePositions.find(nItem) == aUsedCachePositions.end())
986 aMemberSequence.emplace_back(nItem, true);
989 // tdf#125086: check if this field *also* appears in Data region
990 bool bAppearsInData = false;
992 OUString aSrcName = ScDPUtil::getSourceDimensionName(pDim->GetName());
993 const auto it = std::find_if(
994 aDataFields.begin(), aDataFields.end(), [&aSrcName](const DataField& rDataField) {
995 OUString aThisName
996 = ScDPUtil::getSourceDimensionName(rDataField.mpDim->GetName());
997 return aThisName == aSrcName;
999 if (it != aDataFields.end())
1000 bAppearsInData = true;
1003 auto pAttList = sax_fastparser::FastSerializerHelper::createAttrList();
1004 pAttList->add(XML_axis, toOOXMLAxisType(eOrient));
1005 if (bAppearsInData)
1006 pAttList->add(XML_dataField, ToPsz10(true));
1007 pAttList->add(XML_compact, ToPsz10(false));
1008 pAttList->add(XML_showAll, ToPsz10(false));
1010 long nSubTotalCount = pDim->GetSubTotalsCount();
1011 std::vector<OString> aSubtotalSequence;
1012 bool bHasDefaultSubtotal = false;
1013 for (long nSubTotal = 0; nSubTotal < nSubTotalCount; ++nSubTotal)
1015 ScGeneralFunction eFunc = pDim->GetSubTotalFunc(nSubTotal);
1016 aSubtotalSequence.push_back(GetSubtotalFuncName(eFunc));
1017 sal_Int32 nAttToken = GetSubtotalAttrToken(eFunc);
1018 if (nAttToken == XML_defaultSubtotal)
1019 bHasDefaultSubtotal = true;
1020 else if (!pAttList->hasAttribute(nAttToken))
1021 pAttList->add(nAttToken, ToPsz10(true));
1023 // XML_defaultSubtotal is true by default; only write it if it's false
1024 if (!bHasDefaultSubtotal)
1025 pAttList->add(XML_defaultSubtotal, ToPsz10(false));
1027 if(bDimInTabularMode)
1028 pAttList->add( XML_outline, ToPsz10(false));
1029 sax_fastparser::XFastAttributeListRef xAttributeList(pAttList);
1030 pPivotStrm->startElement(XML_pivotField, xAttributeList);
1032 pPivotStrm->startElement(XML_items,
1033 XML_count, OString::number(static_cast<long>(aMemberSequence.size() + aSubtotalSequence.size())));
1035 for (const auto & nMember : aMemberSequence)
1037 auto pItemAttList = sax_fastparser::FastSerializerHelper::createAttrList();
1038 if (nMember.second)
1039 pItemAttList->add(XML_h, ToPsz10(true));
1040 pItemAttList->add(XML_x, OString::number(static_cast<long>(nMember.first)));
1041 sax_fastparser::XFastAttributeListRef xItemAttributeList(pItemAttList);
1042 pPivotStrm->singleElement(XML_item, xItemAttributeList);
1045 for (const OString& sSubtotal : aSubtotalSequence)
1047 pPivotStrm->singleElement(XML_item, XML_t, sSubtotal);
1050 pPivotStrm->endElement(XML_items);
1051 pPivotStrm->endElement(XML_pivotField);
1054 pPivotStrm->endElement(XML_pivotFields);
1056 // <rowFields>
1058 if (!aRowFields.empty())
1060 pPivotStrm->startElement(XML_rowFields,
1061 XML_count, OString::number(static_cast<long>(aRowFields.size())));
1063 for (const auto& rRowField : aRowFields)
1065 pPivotStrm->singleElement(XML_field, XML_x, OString::number(rRowField));
1068 pPivotStrm->endElement(XML_rowFields);
1071 // <rowItems>
1073 // <colFields>
1075 if (!aColFields.empty())
1077 pPivotStrm->startElement(XML_colFields,
1078 XML_count, OString::number(static_cast<long>(aColFields.size())));
1080 for (const auto& rColField : aColFields)
1082 pPivotStrm->singleElement(XML_field, XML_x, OString::number(rColField));
1085 pPivotStrm->endElement(XML_colFields);
1088 // <colItems>
1090 // <pageFields>
1092 if (!aPageFields.empty())
1094 pPivotStrm->startElement(XML_pageFields,
1095 XML_count, OString::number(static_cast<long>(aPageFields.size())));
1097 for (const auto& rPageField : aPageFields)
1099 pPivotStrm->singleElement(XML_pageField,
1100 XML_fld, OString::number(rPageField),
1101 XML_hier, OString::number(-1)); // TODO : handle this correctly.
1104 pPivotStrm->endElement(XML_pageFields);
1107 // <dataFields>
1109 if (!aDataFields.empty())
1111 css::uno::Reference<css::container::XNameAccess> xDimsByName;
1112 if (auto xDimSupplier = const_cast<ScDPObject&>(rDPObj).GetSource())
1113 xDimsByName = xDimSupplier->getDimensions();
1115 pPivotStrm->startElement(XML_dataFields,
1116 XML_count, OString::number(static_cast<long>(aDataFields.size())));
1118 for (const auto& rDataField : aDataFields)
1120 long nDimIdx = rDataField.mnPos;
1121 assert(aCachedDims[nDimIdx]); // the loop above should have screened for NULL's.
1122 const ScDPSaveDimension& rDim = *rDataField.mpDim;
1123 boost::optional<OUString> pName = rDim.GetLayoutName();
1124 // tdf#124651: despite being optional in CT_DataField according to ECMA-376 Part 1,
1125 // Excel (at least 2016) seems to insist on the presence of "name" attribute in
1126 // dataField element.
1127 // tdf#124881: try to create a meaningful name; don't use empty string.
1128 if (!pName)
1129 pName = ScDPUtil::getDisplayedMeasureName(
1130 rDim.GetName(), ScDPUtil::toSubTotalFunc(rDim.GetFunction()));
1131 auto pItemAttList = sax_fastparser::FastSerializerHelper::createAttrList();
1132 pItemAttList->add(XML_name, pName->toUtf8());
1133 pItemAttList->add(XML_fld, OString::number(nDimIdx));
1134 const char* pSubtotal = toOOXMLSubtotalType(rDim.GetFunction());
1135 if (pSubtotal)
1136 pItemAttList->add(XML_subtotal, pSubtotal);
1137 if (xDimsByName)
1141 css::uno::Reference<css::beans::XPropertySet> xDimProps(
1142 xDimsByName->getByName(rDim.GetName()), uno::UNO_QUERY_THROW);
1143 css::uno::Any aVal = xDimProps->getPropertyValue(SC_UNONAME_NUMFMT);
1144 sal_uInt32 nScNumFmt = aVal.get<sal_uInt32>();
1145 sal_uInt16 nXclNumFmt = GetRoot().GetNumFmtBuffer().Insert(nScNumFmt);
1146 pItemAttList->add(XML_numFmtId, OString::number(nXclNumFmt));
1148 catch (uno::Exception&)
1150 SAL_WARN("sc.filter",
1151 "Couldn't get number format for data field " << rDim.GetName());
1152 // Just skip exporting number format
1155 sax_fastparser::XFastAttributeListRef xItemAttributeList(pItemAttList);
1156 pPivotStrm->singleElement(XML_dataField, xItemAttributeList);
1159 pPivotStrm->endElement(XML_dataFields);
1162 // Now add style info (use grab bag, or just a set which is default on Excel 2007 through 2016)
1163 if (const auto [bHas, aVal] = rDPObj.GetInteropGrabBagValue("pivotTableStyleInfo"); bHas)
1164 WriteGrabBagItemToStream(rStrm, XML_pivotTableStyleInfo, aVal);
1165 else
1166 pPivotStrm->singleElement(XML_pivotTableStyleInfo, XML_name, "PivotStyleLight16",
1167 XML_showRowHeaders, "1", XML_showColHeaders, "1",
1168 XML_showRowStripes, "0", XML_showColStripes, "0",
1169 XML_showLastColumn, "1");
1171 OUString aBuf = "../pivotCache/pivotCacheDefinition" +
1172 OUString::number(nCacheId) +
1173 ".xml";
1175 rStrm.addRelation(
1176 pPivotStrm->getOutputStream(),
1177 CREATE_OFFICEDOC_RELATION_TYPE("pivotCacheDefinition"),
1178 aBuf);
1180 pPivotStrm->endElement(XML_pivotTableDefinition);
1183 void XclExpXmlPivotTables::AppendTable( const ScDPObject* pTable, sal_Int32 nCacheId, sal_Int32 nPivotId )
1185 maTables.emplace_back(pTable, nCacheId, nPivotId);
1188 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */