Hello everyone! Today, I will guide you through implementing a very useful data grouping feature in C++ similar to that in Excel.
1. Basic Features of Excel’s Grouping Function
The grouping function in Excel has three main features:
- Hierarchical Structure: Allows for the creation of multi-level groups
- Expand/Collapse: Toggle the display of details at any time
- Automatic Summary: Can perform statistics on grouped data
To implement this functionality in C++, we need:
- Tree Structure: To manage the grouping hierarchy
- State Management: To record expand/collapse states
- Data Aggregation: To calculate summary values for groups
2. Basic Data Structure Design
1. Group Node Class
#include <vector>
#include <string>
#include <memory>
class GroupNode {
private:
std::string name; // Group name
bool isExpanded = true; // Is expanded
std::vector<int> itemIndices; // Indices of contained data items
std::vector<std::shared_ptr<GroupNode>> children; // Child groups
public:
GroupNode(const std::string& groupName) : name(groupName) {}
void addItemIndex(int index) {
itemIndices.push_back(index);
}
void addChild(std::shared_ptr<GroupNode> child) {
children.push_back(child);
}
void toggleExpanded() {
isExpanded = !isExpanded;
}
// Get visible items (considering expand state)
void getVisibleItems(std::vector<int>& visibleItems) const {
if(isExpanded) {
for(int index : itemIndices) {
visibleItems.push_back(index);
}
for(auto& child : children) {
child->getVisibleItems(visibleItems);
}
}
}
};
2. Data Manager
class DataGroupManager {
private:
std::vector<std::string> rawData; // Raw data
std::shared_ptr<GroupNode> rootGroup; // Root group
public:
DataGroupManager(const std::vector<std::string>& data)
: rawData(data), rootGroup(std::make_shared<GroupNode>("All Data")) {
// Initially, all data is in the root group
for(int i = 0; i < rawData.size(); ++i) {
rootGroup->addItemIndex(i);
}
}
// Create group
void createGroup(const std::vector<int>& indices, const std::string& groupName) {
auto newGroup = std::make_shared<GroupNode>(groupName);
for(int index : indices) {
newGroup->addItemIndex(index);
// Remove these items from the parent group
// (Actual implementation needs to find the parent group of these items)
}
rootGroup->addChild(newGroup);
}
// Get currently visible data
std::vector<std::string> getVisibleData() const {
std::vector<int> visibleIndices;
rootGroup->getVisibleItems(visibleIndices);
std::vector<std::string> result;
for(int index : visibleIndices) {
result.push_back(rawData[index]);
}
return result;
}
};
Tips:
- Use
<span>shared_ptr</span>
to manage the lifecycle of group nodes - Reference raw data by index to avoid data copying
- Can extend to support multi-level grouping
3. Console Interface Implementation
1. Display Grouped Data
void displayGroupedData(const DataGroupManager& manager) {
auto visibleData = manager.getVisibleData();
std::cout << "Current visible data (" << visibleData.size() << " items):\n";
for(const auto& item : visibleData) {
std::cout << "- " << item << "\n";
}
}
2. Simple Interaction Control
#include <conio.h> // For _getch()
void handleGroupInput(DataGroupManager& manager) {
std::cout << "\nOperation: (E) Expand / (C) Collapse / (G) Create Group / (Q) Quit\n";
char input = _getch();
switch(toupper(input)) {
case 'E':
// Implement expand logic
break;
case 'C':
// Implement collapse logic
break;
case 'G':
// Implement create group logic
break;
case 'Q':
exit(0);
}
}
4. Grouping Algorithm Implementation
1. Group by Value
void groupByValue(DataGroupManager& manager) {
// Example: Group by first letter
std::unordered_map<char, std::vector<int>> groups;
const auto& data = manager.getRawData();
for(int i = 0; i < data.size(); ++i) {
char firstChar = data[i].empty() ? ' ' : toupper(data[i][0]);
groups[firstChar].push_back(i);
}
// Create groups
for(auto& [key, indices] : groups) {
std::string groupName = "Letter ";
groupName += key;
manager.createGroup(indices, groupName);
}
}
2. Group by Condition
void groupByCondition(DataGroupManager& manager,
const std::function<bool(const std::string&)>& condition,
const std::string& groupName) {
std::vector<int> matchingIndices;
const auto& data = manager.getRawData();
for(int i = 0; i < data.size(); ++i) {
if(condition(data[i])) {
matchingIndices.push_back(i);
}
}
manager.createGroup(matchingIndices, groupName);
}
5. Enhanced Functionality Implementation
1. Group Summary Statistics
struct GroupSummary {
std::string groupName;
int itemCount;
double average;
// Other statistical information...
};
std::vector<GroupSummary> getGroupSummaries(const DataGroupManager& manager) {
std::vector<GroupSummary> summaries;
// Recursively traverse the group tree
std::function<void(const std::shared_ptr<GroupNode>&)> traverse;
traverse = [&](const std::shared_ptr<GroupNode>& node) {
if(!node->getItemIndices().empty()) {
GroupSummary summary;
summary.groupName = node->getName();
summary.itemCount = node->getItemIndices().size();
// Calculate average and other statistics
// ...
summaries.push_back(summary);
}
for(auto& child : node->getChildren()) {
traverse(child);
}
};
traverse(manager.getRootGroup());
return summaries;
}
2. Multi-Level Grouping Support
void createMultiLevelGrouping(DataGroupManager& manager) {
// First level grouping: by first letter
groupByValue(manager);
// Second level grouping: within each letter group, group by length
auto& root = manager.getRootGroup();
for(auto& letterGroup : root->getChildren()) {
std::unordered_map<int, std::vector<int>> lengthGroups;
for(int index : letterGroup->getItemIndices()) {
int length = manager.getRawData()[index].length();
lengthGroups[length].push_back(index);
}
// Create sub-groups
for(auto& [len, indices] : lengthGroups) {
auto lenGroup = std::make_shared<GroupNode>("Length " + std::to_string(len));
for(int index : indices) {
lenGroup->addItemIndex(index);
}
letterGroup->addChild(lenGroup);
}
// Clear direct items from parent group
letterGroup->clearItemIndices();
}
}
6. Considerations
-
Performance Optimization:
- Consider using indices instead of copying data for large datasets
- Use smart pointers to manage memory
Extension Suggestions:
// Add group ID for quick lookup
std::unordered_map<std::string, std::weak_ptr<GroupNode>> groupIndex;
Thread Safety:
#include <mutex>
mutable std::mutex mtx;
void addItemToGroup(int index, const std::string& groupName) {
std::lock_guard<std::mutex> lock(mtx);
// ...operate on group data...
}
7. Hands-On Practice
Try these interesting exercises:
- Implement grouping by value range (e.g., 0-10, 10-20, etc.)
- Add drag-and-drop functionality for groups (to change grouping hierarchy)
- Implement saving and loading of grouped data
- Create a function to group by date
Conclusion
Today we learned:
- Using a tree structure to manage grouped data
- The core logic for implementing expand/collapse functionality
- Implementation of various grouping algorithms
- Grouping summary statistics functionality
Data grouping is an important technique for improving data readability, and mastering this functionality can make your programs more professional. It is recommended to start with simple single-level grouping and gradually implement more complex features.