Introduction

Welcome to today's lesson on applying data filtering and aggregation in a real-world scenario using a user management system. We'll start by building a foundational structure that can handle basic user operations. Then, we'll expand it by introducing more advanced functionalities that allow filtering and aggregating user data.

Starter Task Methods

In our starter task, we will implement a class that manages basic operations on a collection of user data, specifically handling adding new users, retrieving user profiles, and updating user profiles.

Here are the starter task methods:

  • addUser(userId, age, country, subscribed) - adds a new user with the specified attributes. Returns true if the user was added successfully and false if a user with the same userId already exists.
  • getUser(userId) - returns the user's profile as an object if the user exists; otherwise, returns null.
  • updateUser(userId, age, country, subscribed) - updates the user's profile based on non-null parameters. Returns true if the user exists and was updated, false otherwise.
Solution for the Starter Task

The JavaScript implementation of our starter task is shown below:

class UserManager {
    constructor() {
        this.users = {};  // Initialize an empty object to store user data
    }
    
    // Method to add a new user
    addUser(userId, age, country, subscribed) {
        if (this.users[userId]) {
            return false;  // Return false if userId already exists
        }
        this.users[userId] = { age, country, subscribed };  // Add user to the users object
        return true;  // Return true to indicate successful addition
    }

    // Method to retrieve a user's profile
    getUser(userId) {
        return this.users[userId] || null;  // Return the user profile or null if the user does not exist
    }

    // Method to update a user's profile
    updateUser(userId, age, country, subscribed) {
        if (!this.users[userId]) {
            return false;  // Return false if the user does not exist
        }
        if (age !== null) {
            this.users[userId].age = age;  // Update age if provided
        }
        if (country !== null) {
            this.users[userId].country = country;  // Update country if provided
        }
        if (subscribed !== null) {
            this.users[userId].subscribed = subscribed;  // Update subscription status if provided
        }
        return true;  // Return true to indicate successful update
    }
}

// Example usage
const um = new UserManager();
console.log(um.addUser("u1", 25, "USA", true));  // true
console.log(um.addUser("u2", 30, "Canada", false));  // true
console.log(um.addUser("u1", 22, "Mexico", true));  // false
console.log(um.getUser("u1"));  // { age: 25, country: "USA", subscribed: true }
console.log(um.updateUser("u1", 26, null, null));  // true
console.log(um.updateUser("u3", 19, "UK", false));  // false

This implementation covers all our starter methods. Let's move forward and introduce more complex functionalities.

Introducing New Methods for Data Filtering and Aggregation

With our foundational structure in place, it's time to add functionalities for filtering user data and aggregating statistics.

Here are new methods to implement:

  • filterUsers(minAge, maxAge, country, subscribed):
    • Returns the list of user IDs that match the specified criteria. Criteria can be null, meaning that the criterion should not be applied during filtering.
  • aggregateStats() - returns statistics in the form of an object:
    • totalUsers: Total number of users
    • averageAge: Average age of all users (rounded down to the nearest integer)
    • subscribedRatio: Ratio of subscribed users to total users (as a float with two decimals)
Step-by-Step Implementation: Step 1: Adding `filterUsers` Method

This method filters users based on the criteria provided. Let's see how it works:

class UserManager {
    // Existing methods...

    // Method to filter users based on criteria
    filterUsers(minAge, maxAge, country, subscribed) {
        const filteredUsers = [];
        for (const [userId, profile] of Object.entries(this.users)) {
            // Check minimum age criterion
            if (minAge !== null && profile.age < minAge) {
                continue;
            }
            // Check maximum age criterion
            if (maxAge !== null && profile.age > maxAge) {
                continue;
            }
            // Check country criterion
            if (country !== null && profile.country !== country) {
                continue;
            }
            // Check subscription status criterion
            if (subscribed !== null && profile.subscribed !== subscribed) {
                continue;
            }
            // Add userId to filteredUsers if all criteria are met
            filteredUsers.push(userId);
        }
        return filteredUsers;  // Return the list of filtered user IDs
    }
}

// Example usage of the new method
const um = new UserManager();
um.addUser("u1", 25, "USA", true);
um.addUser("u2", 30, "Canada", false);
um.addUser("u3", 22, "USA", true);
console.log(um.filterUsers(20, 30, "USA", true));  // ["u1", "u3"]
console.log(um.filterUsers(null, 28, null, null));  // ["u1", "u3"]
console.log(um.filterUsers(null, null, "Canada", false));  // ["u2"]
  • The filterUsers method filters users based on minAge, maxAge, country, and subscribed status criteria.
  • It iterates over the users dictionary and checks each user's profile against the provided criteria.
  • Users who meet all the criteria are added to the filteredUsers list, which is then returned.
  • The example usage shows how to add users and filter them based on different criteria.
Step 2: Adding `aggregateStats` Method

This method aggregates statistics from the user profiles. Let's implement it:

class UserManager {
    // Existing methods...

    // Method to aggregate statistics from user profiles
    aggregateStats() {
        const totalUsers = Object.keys(this.users).length;  // Get the total number of users
        if (totalUsers === 0) {  // If no users, return zeroed statistics
            return { totalUsers: 0, averageAge: 0, subscribedRatio: 0.00 };
        }
        
        // Calculate total age by summing ages of all users
        const totalAge = Object.values(this.users).reduce((sum, profile) => sum + profile.age, 0);
        // Count number of subscribed users
        const subscribedUsers = Object.values(this.users).filter(profile => profile.subscribed).length;
        
        // Calculate average age (rounded down)
        const averageAge = Math.floor(totalAge / totalUsers);
        // Calculate subscribed ratio (to two decimals)
        const subscribedRatio = (subscribedUsers / totalUsers).toFixed(2);
        
        return { totalUsers, averageAge, subscribedRatio: parseFloat(subscribedRatio) };  // Return statistics object
    }
}

// Using `um` from the previous section
console.log(um.aggregateStats());  // { totalUsers: 3, averageAge: 25, subscribedRatio: 0.67 }

This aggregateStats method calculates and returns aggregate statistics about the users in the form of an object. It first determines totalUsers, the total number of users. If there are no users, it returns an object with zeroed statistics. Otherwise, it calculates totalAge by summing the ages of all users and counts subscribedUsers who are subscribed. It then computes averageAge by performing integer division of totalAge by totalUsers and calculates subscribedRatio by dividing subscribedUsers by totalUsers and rounding to two decimals. The resulting statistics object includes totalUsers, averageAge, and subscribedRatio.

The Final Solution

Here's the complete UserManager class with all methods, including the new ones for filtering and aggregation:

class UserManager {
    constructor() {
        this.users = {};  // Initialize an empty object to store user data
    }

    // Method to add a new user
    addUser(userId, age, country, subscribed) {
        if (this.users[userId]) {
            return false;  // Return false if userId already exists
        }
        this.users[userId] = { age, country, subscribed };  // Add user to the users object
        return true;  // Return true to indicate successful addition
    }

    // Method to retrieve a user's profile
    getUser(userId) {
        return this.users[userId] || null;  // Return the user profile or null if the user does not exist
    }

    // Method to update a user's profile
    updateUser(userId, age, country, subscribed) {
        if (!this.users[userId]) {
            return false;  // Return false if the user does not exist
        }
        if (age !== null) {
            this.users[userId].age = age;  // Update age if provided
        }
        if (country !== null) {
            this.users[userId].country = country;  // Update country if provided
        }
        if (subscribed !== null) {
            this.users[userId].subscribed = subscribed;  // Update subscription status if provided
        }
        return true;  // Return true to indicate successful update
    }

    // Method to filter users based on criteria
    filterUsers(minAge, maxAge, country, subscribed) {
        const filteredUsers = [];
        for (const [userId, profile] of Object.entries(this.users)) {
            // Check minimum age criterion
            if (minAge !== null && profile.age < minAge) {
                continue;
            }
            // Check maximum age criterion
            if (maxAge !== null && profile.age > maxAge) {
                continue;
            }
            // Check country criterion
            if (country !== null && profile.country !== country) {
                continue;
            }
            // Check subscription status criterion
            if (subscribed !== null && profile.subscribed !== subscribed) {
                continue;
            }
            // Add userId to filteredUsers if all criteria are met
            filteredUsers.push(userId);
        }
        return filteredUsers;  // Return the list of filtered user IDs
    }

    // Method to aggregate statistics from user profiles
    aggregateStats() {
        const totalUsers = Object.keys(this.users).length;  // Get the total number of users
        if (totalUsers === 0) {  // If no users, return zeroed statistics
            return { totalUsers: 0, averageAge: 0, subscribedRatio: 0.00 };
        }
        
        // Calculate total age by summing ages of all users
        const totalAge = Object.values(this.users).reduce((sum, profile) => sum + profile.age, 0);
        // Count number of subscribed users
        const subscribedUsers = Object.values(this.users).filter(profile => profile.subscribed).length;
        
        // Calculate average age (rounded down)
        const averageAge = Math.floor(totalAge / totalUsers);
        // Calculate subscribed ratio (to two decimals)
        const subscribedRatio = (subscribedUsers / totalUsers).toFixed(2);
        
        return { totalUsers, averageAge, subscribedRatio: parseFloat(subscribedRatio) };  // Return statistics object
    }
}

// Example usage
const um = new UserManager();
um.addUser("u1", 25, "USA", true);
um.addUser("u2", 30, "Canada", false);
um.addUser("u3", 22, "USA", true);

console.log(um.filterUsers(20, 30, "USA", true));  // ["u1", "u3"]
console.log(um.filterUsers(null, 28, null, null));  // ["u1", "u3"]
console.log(um.filterUsers(null, null, "Canada", false));  // ["u2"]

console.log(um.aggregateStats());  // { totalUsers: 3, averageAge: 25, subscribedRatio: 0.67 }
Lesson Summary

Great job! Today, you've learned how to effectively handle user data by implementing advanced functionalities like filtering and aggregation on top of a basic system. This is a critical skill in real-life software development, where you often need to extend existing systems to meet new requirements.

I encourage you to practice solving similar challenges to solidify your understanding of data filtering and aggregation. Happy coding, and see you in the next lesson!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal