Files
odfstudio/sc/source/core/data/PivotTableFormatOutput.cxx
Tomaž Vajngerl 6afc54463c sc: Support subtotals, label and offset in pivot format matching
Subtotal matching: split field checking into two passes. First check
referenced fields (tracking subtotal matches), then check unreferenced
fields. When bHasSubtotal is set and bSelected is true, only match
actual subtotal lines. When selected=0, also match data rows in
scope (controlled by two pass matching). Regular name matches now skip
subtotal lines.

Label positioning: use the column header start row (nDataStartRow minus
column field count) as offset base for column direction, fixing labels
going to the field name row instead of the actual header row. Labels
without offset now use the matched field's index to compute the target
position, fixing the issue where oPosition only reflected the last
field's row/column.

Offset handling: parse offsets as ScRange to handle range references
like "IV2:IV3". Range offsets filter which lines within the matched
group get formatted. Smart fallback when offset exceeds the label area.

Single item group expansion: for data formats targeting subtotal
intersections (all set fields on both axes have subtotals),
single data row groups also get the format applied to the data row
preceding the subtotal.

Enable the subtotal tests and convert them to use the reference
matching like most other tests use.

Change-Id: I65ecbbd15e10f3b0e8825f5e79b72d1d9de9af97
Signed-off-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.co.uk>
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/2733
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins CPCI <releng@collaboraoffice.com>
(cherry picked from commit 074c71ef1a3d5ece4c5ac6e7d708a067135cd420)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/205787
Tested-by: Jenkins
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
2026-05-28 09:34:46 +02:00

766 lines
28 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This file is part of the LibreOffice project.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <pivot/PivotTableFormatOutput.hxx>
#include <set>
#include <pivot/DPOutLevelData.hxx>
#include <dpoutput.hxx>
#include <dpobject.hxx>
#include <pivot.hxx>
#include <dptabdat.hxx>
#include <dpcache.hxx>
#include <document.hxx>
#include <com/sun/star/sheet/MemberResultFlags.hpp>
namespace sc
{
namespace
{
class NameResolver
{
private:
ScDPObject& mrObject;
ScDPCache const& mrCache;
std::unordered_map<sal_Int32, std::vector<OUString>> maNameCache;
void fillNamesForItems(std::vector<OUString>& rNames, ScDPCache::ScDPItemDataVec const& rItems,
sal_Int32 nDimension)
{
for (const auto& rItemData : rItems)
{
OUString sFormattedName;
if (rItemData.HasStringData() || rItemData.IsEmpty())
sFormattedName = rItemData.GetString();
else
sFormattedName = mrCache.GetFormattedString(nDimension, rItemData, false);
rNames.push_back(sFormattedName);
}
}
void fillNamesForDimension(std::vector<OUString>& rNames, sal_Int32 nDimension)
{
// Try to get member names in pivot field order first.
// This matches the order used by the pivot table output.
std::vector<ScDPLabelData::Member> aMembers;
if (mrObject.GetMembers(nDimension, 0, aMembers) && !aMembers.empty())
{
for (const auto& rMember : aMembers)
rNames.push_back(rMember.maName);
return;
}
// Fallback to cache order
if (mrCache.IsValidDimensionIndex(nDimension))
{
fillNamesForItems(rNames, mrCache.GetDimMemberValues(nDimension), nDimension);
}
else
{
auto* pGroup = mrCache.GetGroupItems(nDimension);
if (pGroup)
{
fillNamesForItems(rNames, pGroup->maItems, nDimension);
}
}
}
public:
NameResolver(ScDPObject& rObject, ScDPCache const& rCache)
: mrObject(rObject)
, mrCache(rCache)
{
}
OUString getNameForIndex(sal_uInt32 nIndex, sal_Int32 nDimension)
{
auto iterator = maNameCache.find(nDimension);
if (iterator == maNameCache.end())
{
std::vector<OUString> aNames;
fillNamesForDimension(aNames, nDimension);
iterator = maNameCache.emplace(nDimension, aNames).first;
}
const std::vector<OUString>& rNames = iterator->second;
if (nIndex >= rNames.size())
return OUString();
return rNames[nIndex];
}
};
void initLines(std::vector<LineData>& rLines, std::vector<ScDPOutLevelData> const& rFields)
{
for (const ScDPOutLevelData& rField : rFields)
{
size_t nFieldLength(rField.maResult.getLength());
if (rLines.size() < nFieldLength)
rLines.resize(nFieldLength);
for (LineData& rLineData : rLines)
{
rLineData.maFields.resize(rFields.size());
}
}
}
Selection const* findSelection(PivotTableFormat const& rFormat, tools::Long nDimension)
{
for (Selection const& rSelection : rFormat.getSelections())
{
if (rSelection.nField == nDimension)
return &rSelection;
}
return nullptr;
}
void fillOutputFieldFromSelection(FormatOutputField& rOutputField, Selection const& rSelection,
size_t nSelectionIndex, NameResolver& rNameResolver)
{
if (rSelection.nIndices.empty())
{
rOutputField.bMatchesAll = true;
}
else
{
if (rSelection.nIndices.size() > 1 && rSelection.nIndices.size() > nSelectionIndex)
rOutputField.nIndex = rSelection.nIndices[nSelectionIndex];
else
rOutputField.nIndex = rSelection.nIndices[0];
if (rOutputField.nDimension == constDataDimension)
rOutputField.aName = "DATA";
else
rOutputField.aName
= rNameResolver.getNameForIndex(rOutputField.nIndex, rOutputField.nDimension);
}
rOutputField.bSelected = rSelection.bSelected;
rOutputField.bHasSubtotal = rSelection.bHasSubtotal;
rOutputField.bSet = true;
}
void initFormatOutputField(size_t nSelectionIndex, std::vector<FormatOutputField>& rOutputFields,
std::vector<ScDPOutLevelData> const& rFields,
PivotTableFormat const& rFormat, NameResolver& rNameResolver)
{
rOutputFields.resize(rFields.size());
for (size_t i = 0; i < rOutputFields.size(); i++)
{
FormatOutputField& rOutputField = rOutputFields[i];
if (!rFields[i].mbDataLayout)
rOutputField.nDimension = rFields[i].mnDim;
Selection const* pSelection = findSelection(rFormat, rOutputField.nDimension);
if (pSelection == nullptr)
continue;
fillOutputFieldFromSelection(rOutputField, *pSelection, nSelectionIndex, rNameResolver);
}
}
} // end anonymous namespace
void FormatOutput::prepare(SCTAB nTab, std::vector<ScDPOutLevelData> const& rColumnFields,
std::vector<ScDPOutLevelData> const& rRowFields,
bool bColumnFieldIsDataOnly)
{
if (!mpFormats)
return;
// initialize row and column lines, so the number of fields matches the pivot table output
initLines(maRowLines, rRowFields);
if (rColumnFields.size() == 0 && bColumnFieldIsDataOnly)
{
maColumnLines.resize(1);
maColumnLines[0].maFields.resize(1);
}
else
{
initLines(maColumnLines, rColumnFields);
}
// check the table data exists
auto* pTableData = mrObject.GetTableData();
if (!pTableData)
return;
ScDPFilteredCache const& rFilteredCache = pTableData->GetCacheTable();
ScDPCache const& rCache = rFilteredCache.getCache();
NameResolver aNameResolver(mrObject, rCache);
// Initialize format output entries (FormatOutputEntry) and set the data already available from output fields
// (rColumnFields and rRowFields) and the pivot table format list (PivotTableFormat).
for (PivotTableFormat const& rFormat : mpFormats->getVector())
{
size_t nMaxNumberOfIndices = 1;
for (auto const& rSelection : rFormat.aSelections)
{
if (rSelection.nIndices.size() > 1)
nMaxNumberOfIndices = rSelection.nIndices.size();
}
for (size_t nSelectionIndex = 0; nSelectionIndex < nMaxNumberOfIndices; nSelectionIndex++)
{
sc::FormatOutputEntry aEntry;
aEntry.pPattern = rFormat.pPattern;
aEntry.onTab = nTab;
aEntry.eType = rFormat.eType;
aEntry.bGrandRow = rFormat.bGrandRow;
aEntry.bGrandColumn = rFormat.bGrandColumn;
aEntry.oOffset = rFormat.oOffset;
initFormatOutputField(nSelectionIndex, aEntry.aRowOutputFields, rRowFields, rFormat,
aNameResolver);
// If column fields list is empty, but there is a data field in columns that is not part of column fields
if (rColumnFields.size() == 0 && bColumnFieldIsDataOnly)
{
// Initialize column output fields to have 1 data output field
aEntry.aColumnOutputFields.resize(1);
FormatOutputField& rOutputField = aEntry.aColumnOutputFields[0];
rOutputField.nDimension = constDataDimension;
Selection const* pSelection = findSelection(rFormat, constDataDimension);
if (pSelection)
fillOutputFieldFromSelection(rOutputField, *pSelection, nSelectionIndex,
aNameResolver);
}
else
{
initFormatOutputField(nSelectionIndex, aEntry.aColumnOutputFields, rColumnFields,
rFormat, aNameResolver);
}
maFormatOutputEntries.push_back(std::move(aEntry));
}
}
}
void FormatOutput::insertEmptyDataColumn(SCCOL nColPos, SCROW nRowPos)
{
if (!mpFormats)
return;
LineData& rLine = maColumnLines[0];
rLine.oLine = nColPos;
rLine.oPosition = nRowPos;
FieldData& rFieldData = rLine.maFields[0];
rFieldData.nIndex = 0;
rFieldData.bIsSet = true;
}
namespace
{
void fillLineAndFieldData(std::vector<LineData>& rLineDataVector, size_t nFieldIndex,
ScDPOutLevelData const& rField, tools::Long nMemberIndex,
sheet::MemberResult const& rMember, SCCOLROW nLine, SCCOLROW nPosition)
{
LineData& rLine = rLineDataVector[nMemberIndex];
rLine.oLine = nLine;
rLine.oPosition = nPosition;
FieldData& rFieldData = rLine.maFields[nFieldIndex];
if (!rField.mbDataLayout)
rFieldData.mnDimension = rField.mnDim;
rFieldData.aName = rMember.Name;
rFieldData.nIndex = nMemberIndex;
rFieldData.bIsSet = true;
rFieldData.bIsMember = rMember.Flags & sheet::MemberResultFlags::HASMEMBER;
rFieldData.bSubtotal = rMember.Flags & sheet::MemberResultFlags::SUBTOTAL;
rFieldData.bContinue = rMember.Flags & sheet::MemberResultFlags::CONTINUE;
// Search previous entries for the name / value
if (rFieldData.bContinue)
{
tools::Long nCurrent = nMemberIndex - 1;
while (nCurrent >= 0 && rLineDataVector[nCurrent].maFields[nFieldIndex].bContinue)
nCurrent--;
if (nCurrent >= 0)
{
FieldData& rCurrentFieldData = rLineDataVector[nCurrent].maFields[nFieldIndex];
rFieldData.aName = rCurrentFieldData.aName;
rFieldData.nIndex = rCurrentFieldData.nIndex;
}
}
}
} // end anonymous namespace
void FormatOutput::insertFieldMember(size_t nFieldIndex, ScDPOutLevelData const& rField,
tools::Long nMemberIndex, sheet::MemberResult const& rMember,
SCCOL nColPos, SCROW nRowPos,
sc::FormatResultDirection eResultDirection)
{
if (!mpFormats)
return;
if (eResultDirection == sc::FormatResultDirection::ROW)
{
fillLineAndFieldData(maRowLines, nFieldIndex, rField, nMemberIndex, rMember, nRowPos,
nColPos);
}
else if (eResultDirection == sc::FormatResultDirection::COLUMN)
{
fillLineAndFieldData(maColumnLines, nFieldIndex, rField, nMemberIndex, rMember, nColPos,
nRowPos);
}
}
namespace
{
/** Find lines matching the format's field criteria.
*
* For labels: unreferenced fields must have a member/continuation.
* For data: exact matches take priority over broad matches. Falls back to broad if no exact.
*/
void findMatchingLines(std::vector<LineData> const& rLines,
std::vector<FormatOutputField> const& rFormatOutputField, FormatType eType,
std::vector<std::reference_wrapper<const LineData>>& rMatches)
{
std::vector<std::reference_wrapper<const LineData>> aBroadMatches;
for (LineData const& rLineData : rLines)
{
size_t nNoOfFields = rLineData.maFields.size();
bool bAllMatch = true;
bool bHasWildcardMember = false;
bool bHasSubtotalMatch = false;
// Check dimension alignment and referenced fields.
// Track whether any referenced field matched as a subtotal.
for (size_t nIndex = 0; nIndex < nNoOfFields && bAllMatch; nIndex++)
{
FieldData const& rFieldData = rLineData.maFields[nIndex];
FormatOutputField const& rFormatEntry = rFormatOutputField[nIndex];
if (rFieldData.mnDimension != rFormatEntry.nDimension)
{
bAllMatch = false;
break;
}
if (!rFormatEntry.bSet)
continue;
bool bFieldMatch = false;
if (rFormatEntry.bHasSubtotal)
{
if (rFieldData.bSubtotal && rFieldData.aName == rFormatEntry.aName)
{
// Exact subtotal match
bFieldMatch = true;
bHasSubtotalMatch = true;
}
else if (!rFormatEntry.bSelected && !rFieldData.bSubtotal
&& rFieldData.aName == rFormatEntry.aName)
{
// selected=0 + subtotal: also match data rows in scope.
// Two pass matching will handle the broad/exact distinction.
bFieldMatch = true;
}
}
else if (rFormatEntry.bMatchesAll && !rFieldData.bSubtotal)
bFieldMatch = true;
else if (rFormatEntry.nDimension == constDataDimension
&& rFieldData.nIndex == rFormatEntry.nIndex)
bFieldMatch = true;
else if (rFormatEntry.nDimension != constDataDimension && !rFieldData.bSubtotal
&& rFieldData.aName == rFormatEntry.aName)
bFieldMatch = true;
if (!bFieldMatch)
bAllMatch = false;
}
// Check unreferenced fields.
// On subtotal lines, unreferenced fields are empty. Allow them
// when a referenced field matched as a subtotal.
for (size_t nIndex = 0; nIndex < nNoOfFields && bAllMatch; nIndex++)
{
FormatOutputField const& rFormatEntry = rFormatOutputField[nIndex];
if (rFormatEntry.bSet)
continue;
FieldData const& rFieldData = rLineData.maFields[nIndex];
if (eType == FormatType::Label)
{
if (!bHasSubtotalMatch && !rFieldData.bIsMember && !rFieldData.bContinue)
bAllMatch = false;
}
else if (eType == FormatType::Data)
{
if (!bHasSubtotalMatch && (rFieldData.bIsMember || rFieldData.bContinue))
bHasWildcardMember = true;
}
}
if (bAllMatch)
{
if (bHasWildcardMember)
aBroadMatches.push_back(std::cref(rLineData));
else
rMatches.push_back(std::cref(rLineData));
}
}
// For data, use exact matching if available, otherwise fall back to broad
if (eType == FormatType::Data && rMatches.empty() && !aBroadMatches.empty())
rMatches = std::move(aBroadMatches);
}
/** Find the index of the first referenced (set) field in the format output fields */
size_t findMatchedFieldIndex(std::vector<FormatOutputField> const& rOutputFields)
{
for (size_t i = 0; i < rOutputFields.size(); i++)
{
if (rOutputFields[i].bSet)
return i;
}
return 0;
}
/** Apply matched lines — labels go directly to cells, data collects rows/columns */
void applyMatchedLines(ScDocument& rDocument,
std::vector<std::reference_wrapper<const LineData>> const& rMatches,
std::vector<SCCOLROW>& aRows, std::vector<SCCOLROW>& aColumns,
FormatOutputEntry const& rEntry,
std::vector<FormatOutputField> const& rOutputFields,
FormatResultDirection eResultDirection, SCCOL nTabStartColumn,
SCROW nColumnHeaderStartRow, SCCOL nDataStartColumn, SCROW nDataStartRow)
{
assert(rEntry.onTab);
assert(rEntry.pPattern);
// For range offsets (e.g. "IV2:IV3"), track position within the matched
// group to filter which lines get formatted.
bool bHasRowRange = false;
SCROW nOffsetRowStart = 0;
SCROW nOffsetRowEnd = 0;
if (rEntry.oOffset && eResultDirection == FormatResultDirection::ROW)
{
nOffsetRowStart = rEntry.oOffset->aStart.Row();
nOffsetRowEnd = rEntry.oOffset->aEnd.Row();
if (nOffsetRowStart != nOffsetRowEnd)
bHasRowRange = true;
}
size_t nMatchIndex = 0;
for (LineData const& rLineData : rMatches)
{
if (!rLineData.oLine || !rLineData.oPosition)
continue;
if (rEntry.eType == FormatType::Label)
{
// Range offset: only format lines within the row/col range
if (bHasRowRange)
{
SCROW nIdx = static_cast<SCROW>(nMatchIndex);
if (nIdx < nOffsetRowStart || nIdx > nOffsetRowEnd)
{
nMatchIndex++;
continue;
}
}
SCCOLROW nColumn = *rLineData.oLine;
SCCOLROW nRow = *rLineData.oPosition;
if (eResultDirection == FormatResultDirection::ROW)
std::swap(nRow, nColumn);
// Determine the target column/row for the label.
size_t nFieldIndex = findMatchedFieldIndex(rOutputFields);
if (eResultDirection == FormatResultDirection::ROW)
{
SCCOL nLabelColumns = nDataStartColumn - nTabStartColumn;
if (rEntry.oOffset && !bHasRowRange)
{
SCCOL nOffsetColumn = rEntry.oOffset->aStart.Col();
if (nOffsetColumn < nLabelColumns)
nColumn = nTabStartColumn + nOffsetColumn;
else
nColumn = nTabStartColumn + nFieldIndex;
}
else
{
nColumn = nTabStartColumn + nFieldIndex;
}
}
else if (eResultDirection == FormatResultDirection::COLUMN)
{
SCROW nHeaderRows = nDataStartRow - nColumnHeaderStartRow;
if (rEntry.oOffset)
{
SCROW nOffsetRow = rEntry.oOffset->aStart.Row();
if (nOffsetRow < nHeaderRows)
nRow = nColumnHeaderStartRow + nOffsetRow;
else
nRow = nColumnHeaderStartRow + nFieldIndex;
}
else
{
nRow = nColumnHeaderStartRow + nFieldIndex;
}
}
rDocument.ApplyPattern(nColumn, nRow, *rEntry.onTab, *rEntry.pPattern);
}
else if (rEntry.eType == FormatType::Data)
{
if (eResultDirection == FormatResultDirection::ROW)
aRows.push_back(*rLineData.oLine);
else if (eResultDirection == FormatResultDirection::COLUMN)
aColumns.push_back(*rLineData.oLine);
}
nMatchIndex++;
}
}
/** Check if any output field in the vector has been set (has a reference). */
bool hasSetFields(std::vector<FormatOutputField> const& rFields)
{
if (rFields.empty())
return false;
return std::any_of(rFields.begin(), rFields.end(),
[](auto const& rField) { return rField.bSet; });
}
/** Iterate matched lines and call the applicator for each line position, but
* skip the grand total position. */
template <typename LinesT>
void applyToMatchedLines(LinesT const& rMatchedLines, SCCOLROW nGrandTotalPositionToSkip,
std::function<void(SCCOLROW nLinePosition)> const& rApplicator)
{
for (LineData const& rLine : rMatchedLines)
{
if (!rLine.oLine)
continue;
if (nGrandTotalPositionToSkip >= 0 && *rLine.oLine == nGrandTotalPositionToSkip)
continue;
rApplicator(*rLine.oLine);
}
}
/** Apply a grand total data format along one axis.
* If there are references, then find specific lines, otherwise apply to all
* lines. Skips the grand total position to avoid setting the intersection
* cell. */
void applyGrandTotalDataFormat(std::vector<LineData> const& rLines,
std::vector<FormatOutputField> const& rOutputFields,
SCCOLROW nGrandTotalPositionToSkip,
std::function<void(SCCOLROW nLinePosition)> const& rApplicator)
{
if (hasSetFields(rOutputFields))
{
// Use FormatType::None to collect all matching lines (both exact and broad)
// because grand totals need to apply to all matches, not just the narrower set
std::vector<std::reference_wrapper<const LineData>> aMatches;
findMatchingLines(rLines, rOutputFields, FormatType::None, aMatches);
applyToMatchedLines(aMatches, nGrandTotalPositionToSkip, rApplicator);
}
else
{
applyToMatchedLines(rLines, nGrandTotalPositionToSkip, rApplicator);
}
}
} // end anonymous namespace
bool FormatOutput::tryHandleGrandTotals(ScDocument& rDocument, sc::FormatOutputEntry const& rEntry)
{
if (rEntry.bGrandRow && rEntry.bGrandColumn && mnGrandTotalRow >= 0 && mnGrandTotalColumn >= 0)
{
rDocument.ApplyPattern(mnGrandTotalColumn, mnGrandTotalRow, *rEntry.onTab,
*rEntry.pPattern);
return true;
}
if (rEntry.bGrandRow && mnGrandTotalRow >= 0)
{
if (rEntry.eType == FormatType::Data)
{
applyGrandTotalDataFormat(maColumnLines, rEntry.aColumnOutputFields, mnGrandTotalColumn,
[&](SCCOLROW nColumnPosition) {
rDocument.ApplyPattern(nColumnPosition, mnGrandTotalRow,
*rEntry.onTab, *rEntry.pPattern);
});
}
else if (rEntry.eType == FormatType::Label)
{
if (rEntry.oOffset && mnTabStartColumn >= 0 && mnDataStartColumn > mnTabStartColumn)
{
// Offset column is relative to the label area
SCCOL nOffsetColumn = rEntry.oOffset->aStart.Col();
SCCOL nLabelCount = mnDataStartColumn - mnTabStartColumn;
SCCOL nTargetColumn
= mnTabStartColumn + std::min(nOffsetColumn, SCCOL(nLabelCount - 1));
rDocument.ApplyPattern(nTargetColumn, mnGrandTotalRow, *rEntry.onTab,
*rEntry.pPattern);
}
else if (mnTabStartColumn >= 0 && mnDataStartColumn > mnTabStartColumn)
{
// No offset: apply to all label columns
for (SCCOL nColumn = mnTabStartColumn; nColumn < mnDataStartColumn; ++nColumn)
{
rDocument.ApplyPattern(nColumn, mnGrandTotalRow, *rEntry.onTab,
*rEntry.pPattern);
}
}
}
return true;
}
if (rEntry.bGrandColumn && mnGrandTotalColumn >= 0)
{
if (rEntry.eType == FormatType::Data)
{
applyGrandTotalDataFormat(maRowLines, rEntry.aRowOutputFields, mnGrandTotalRow,
[&](SCCOLROW nRowPosition) {
rDocument.ApplyPattern(mnGrandTotalColumn, nRowPosition,
*rEntry.onTab, *rEntry.pPattern);
});
}
else if (rEntry.eType == FormatType::Label)
{
// Apply to the column header rows at the grand total column.
// Use the column lines' positions to find header row positions.
std::set<SCROW> aHeaderRows;
for (LineData const& rColLine : maColumnLines)
{
if (rColLine.oPosition)
aHeaderRows.insert(*rColLine.oPosition);
}
for (SCROW nRow : aHeaderRows)
{
rDocument.ApplyPattern(mnGrandTotalColumn, nRow, *rEntry.onTab, *rEntry.pPattern);
}
}
return true;
}
return false;
}
void FormatOutput::apply(ScDocument& rDocument)
{
if (!mpFormats)
return;
for (auto const& rEntry : maFormatOutputEntries)
{
if (!rEntry.onTab || !rEntry.pPattern)
continue;
// Handle grand total formats
if (tryHandleGrandTotals(rDocument, rEntry))
continue;
std::vector<SCCOLROW> aRows;
std::vector<SCCOLROW> aColumns;
bool bHasRowReferences = hasSetFields(rEntry.aRowOutputFields);
bool bHasColumnReferences = hasSetFields(rEntry.aColumnOutputFields);
// For labels, only match the axis that has references to avoid
// wildcard matching applying labels to the wrong axis cells.
bool bMatchRows = (rEntry.eType != FormatType::Label) || bHasRowReferences;
bool bMatchColumns = (rEntry.eType != FormatType::Label) || bHasColumnReferences;
// Column headers start after any field name rows at the top.
// With N column fields, headers occupy the N rows before the data start.
SCROW nColumnHeaderStartRow = mnDataStartRow;
if (!maColumnLines.empty() && !maColumnLines[0].maFields.empty())
nColumnHeaderStartRow -= SCROW(maColumnLines[0].maFields.size());
if (bMatchRows)
{
std::vector<std::reference_wrapper<const LineData>> aRowMatches;
findMatchingLines(maRowLines, rEntry.aRowOutputFields, rEntry.eType, aRowMatches);
// For data formats targeting subtotal intersections (all set fields
// on both axes have bHasSubtotal), single data row groups get the
// format applied to the data row too, not just the subtotal row.
if (rEntry.eType == FormatType::Data && !aRowMatches.empty())
{
bool bAllRowSubtotal = std::all_of(
rEntry.aRowOutputFields.begin(), rEntry.aRowOutputFields.end(),
[](auto const& rField) { return !rField.bSet || rField.bHasSubtotal; });
bool bAllColumnSubtotal = std::all_of(
rEntry.aColumnOutputFields.begin(), rEntry.aColumnOutputFields.end(),
[](auto const& rField) { return !rField.bSet || rField.bHasSubtotal; });
if (bAllRowSubtotal && bAllColumnSubtotal)
{
// Find the index of each matched subtotal in the lines vector,
// then check if the preceding line is a single item group member.
std::vector<std::reference_wrapper<const LineData>> aExtraMatches;
for (LineData const& rMatch : aRowMatches)
{
for (size_t i = 1; i < maRowLines.size(); i++)
{
if (&maRowLines[i] != &rMatch)
continue;
LineData const& rPrev = maRowLines[i - 1];
size_t nFieldIndex = findMatchedFieldIndex(rEntry.aRowOutputFields);
if (nFieldIndex < rPrev.maFields.size()
&& rPrev.maFields[nFieldIndex].bIsMember
&& !rPrev.maFields[nFieldIndex].bSubtotal)
{
aExtraMatches.push_back(std::cref(rPrev));
}
break;
}
}
for (auto const& rExtra : aExtraMatches)
aRowMatches.push_back(rExtra);
}
}
applyMatchedLines(rDocument, aRowMatches, aRows, aColumns, rEntry,
rEntry.aRowOutputFields, FormatResultDirection::ROW, mnTabStartColumn,
nColumnHeaderStartRow, mnDataStartColumn, mnDataStartRow);
}
if (bMatchColumns)
{
std::vector<std::reference_wrapper<const LineData>> aColumnMatches;
findMatchingLines(maColumnLines, rEntry.aColumnOutputFields, rEntry.eType,
aColumnMatches);
applyMatchedLines(rDocument, aColumnMatches, aRows, aColumns, rEntry,
rEntry.aColumnOutputFields, FormatResultDirection::COLUMN,
mnTabStartColumn, nColumnHeaderStartRow, mnDataStartColumn,
mnDataStartRow);
}
if (!aColumns.empty() && !aRows.empty() && rEntry.eType == FormatType::Data)
{
for (SCCOLROW nRow : aRows)
{
for (SCCOLROW nColumn : aColumns)
{
rDocument.ApplyPattern(nColumn, nRow, *rEntry.onTab, *rEntry.pPattern);
}
}
}
}
}
} // end sc
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */