A Beginner’s Guide to C++: Implementing Excel-Style Data Grouping Functionality

A Beginner's Guide to C++: Implementing Excel-Style Data Grouping Functionality

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:

  1. Hierarchical Structure: Allows for the creation of multi-level groups
  2. Expand/Collapse: Toggle the display of details at any time
  3. 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

  1. 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:

    1. Implement grouping by value range (e.g., 0-10, 10-20, etc.)
    2. Add drag-and-drop functionality for groups (to change grouping hierarchy)
    3. Implement saving and loading of grouped data
    4. 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.

    Leave a Comment