All pages
Powered by GitBook
1 of 1

Loading...

4-A: Data Science

I wanted to put our own spin on the Data lab from CollegeBoard for this project. Rather than just following the standard lab, we're going to analyze real-world video game sales data that will give you hands-on experience with classes, interfaces, and data processing in Java.

For this project, we'll be using a comprehensive dataset containing information about video games, including sales figures, critic ratings, user scores, and more. I've sourced this dataset from Kaggle: Video Game Sales and Ratings.

This project will give you practical experience with:

  • Creating and using custom classes

  • Working with interfaces

  • Processing CSV data files

  • Implementing search and analysis algorithms

  • Visualizing data patterns

Activity 1: Getting Started

Our project requires two main Java classes:

  • GameDataRow - Represents a single game record from our dataset

  • GameDataManager - Handles loading and analyzing collections of game data

Let's start by looking at how to set up the GameDataRow class:

Understanding the Class Header and Interfaces

The class header public class GameDataRow implements Comparable<GameDataRow> has several important components:

  1. public - This access modifier makes the class accessible from outside its package.

  2. class GameDataRow - Defines a new class named GameDataRow.

  3. implements Comparable<GameDataRow> - Indicates that this class implements the Comparable interface.

The Comparable Interface

The Comparable interface is part of the Java Collections Framework and enables objects of a class to be compared to each other. When a class implements Comparable, it must provide an implementation of the compareTo() method:

The compareTo() method must return:

  • A negative value if this object should be ordered before the other object

  • Zero if both objects are equal

  • A positive value if this object should be ordered after the other object

In our implementation, we're sorting games by global sales in descending order (highest sales first).

Other Common Interfaces in Java

While we're only using Comparable in this project, here are other common interfaces you might encounter:

  • Serializable - Marks a class as capable of being serialized (converted to a byte stream).

  • Runnable - Used for classes whose instances will be executed by a thread.

  • ActionListener - Used for handling action events, commonly in GUI applications.

  • Iterable - Allows an object to be the target of the "for-each loop" statement.

Properties and Encapsulation

Now, let's determine what properties our GameDataRow class needs. Our CSV file contains 17 columns of data about each game, and we want to represent each data point as instance variables in our class.

Looking at the structure of our CSV file, we need to create properties that match each column:

Encapsulation Review

Remember that encapsulation is one of the four fundamental OOP concepts, along with inheritance, polymorphism, and abstraction. Encapsulation means bundling data and methods that operate on that data within a single unit (the class) and restricting direct access to some of the object's components.

Notice all our instance variables are marked as private. This is a key principle of encapsulation:

  1. Private instance variables: By making our instance variables private, we prevent outside classes from directly accessing or modifying them.

  2. Public accessor methods: We provide controlled access through getter methods (and sometimes setter methods).

  3. Data validation: Through these methods, we can validate any changes to ensure they meet our requirements.

Good encapsulation ensures that the internal representation of an object can't be seen from outside the object's definition, which creates a "black box" effect.

Constructors

We need constructors to initialize our GameDataRow objects. A well-designed class should have multiple constructors to provide flexibility in object creation:

Notice I've included three constructors:

  1. A primary constructor with all fields

  2. A simplified constructor with only essential fields

  3. A copy constructor to create a deep copy of an existing object

Accessor Methods (Getters)

Now we need accessor methods (getters) for all our instance variables. This is a perfect opportunity to use AI responsibly. Rather than typing out all these repetitive methods, you can ask Claude or an IDE's chatbot to generate them for you.

For example, you could say:

Please generate all the accessor methods for the GameDataRow class based on the instance variables.

This isn't plagiarism or cutting corners - it's using a tool to handle the tedious, repetitive aspects of coding so you can focus on the more challenging and creative aspects like designing algorithms and implementing the business logic.

Here's a snippet showing what a few of these accessor methods would look like:

Test-Driven Development

Let's talk about Test-Driven Development (TDD) - a software development approach where you write tests before writing the actual implementation code. This practice has been widely adopted by professional developers because it leads to more reliable, maintainable code.

In TDD, the process follows three simple steps, often called "Red-Green-Refactor":

  1. Write a test that fails (Red)

  2. Write the minimal code to make the test pass (Green)

  3. Improve the code without changing its behavior (Refactor)

For our project, I've prepared a complete main method that serves as a test suite. It will systematically test all the required functionality of your GameDataRow class and tell you exactly what's missing or not working correctly.

Adding GameDataManager and Testing

Now that your GameDataRow class is complete, let's work with collections of games:

  1. Copy the contents of GameDataManager.java file from this link:

  2. Download the video_games.csv file and place it in your project directory. The location should match the path in the GameDataManager.loadFromCSV() method call.

Activity 2: Building Methods

Now that we have our class structure and properties defined, it's time to add meaningful functionality to our GameDataRow class. To complete this class, you'll need to implement several methods that analyze game data in different ways.

I'm providing you with a few utility methods that handle some of the trickier aspects of parsing data from the CSV file, such as handling non-numeric user scores. Let's first look at these provided methods:

I'm giving you these methods because:

  1. The hasUserReviews() and getUserScoreNumeric() methods handle data conversion and error checking that might be challenging

  2. The toString() method is lengthy but straightforward, and implementing it wouldn't teach you much

Now, let's focus on the methods you need to implement:

Method 1: getAverageScore()

Return Type: float

This method calculates the average between critic and user scores. However, there's a challenge: critic scores are on a scale of 0-100, while user scores are on a scale of 0-10.

Requirements:

  • Return a float representing the normalized average score on a scale of 0-10

  • Normalize the critic score to a 0-10 scale by dividing by 10

  • Handle cases where one or both scores are missing/invalid

  • Return the appropriate average or individual score based on what's available

Key Considerations:

  • What constitutes an "available" score? (Hint: check for positive values)

  • How do you handle the case where only one score type is available?

Hints:

You can paste this code into your method as a starting guide:

Method 2: getSalesPercentages()

Return Type: float[]

This method calculates what percentage of the global sales comes from each region (North America, Europe, Japan, and Other).

Requirements:

  • Return an array of float values representing percentages for each region

  • The array should have 4 elements, one for each region

  • Use the defined constants (NA_REGION, EU_REGION, etc.) for array indices

  • Handle the case where globalSales is zero to avoid division by zero

Key Considerations:

  • The formula for calculating percentage is: (regional sales / global sales) * 100

  • If globalSales is zero, return an array of zeros to avoid division by zero

Method 3: hasSalesData()

Return Type: boolean

This method determines whether the game has any sales data available.

Requirements:

  • Return true if any sales data (regional or global) is available

  • Return false if all sales fields are zero

Key Considerations:

  • Check all sales fields (naSales, euSales, jpSales, otherSales, globalSales)

  • Use logical OR operators to efficiently check multiple conditions

Method 4: hasCriticReviews()

Return Type: boolean

This method determines whether the game has critic review data available.

Requirements:

  • Return true if the game has both a critic score and a count of critics

  • Return false if either piece of information is missing or zero

Key Considerations:

  • Both criticScore and criticCount should be positive values

Method 5: getDominantRegion()

Return Type: String

This method determines which region has the highest sales for this game.

Requirements:

  • Compare sales across all regions and find the highest

  • Return a string representing that region ("North America", "Europe", "Japan", or "Other")

  • Handle ties by choosing the first region encountered with the maximum value

Key Considerations:

  • You'll need to track the maximum sales value and the corresponding region

  • You can use a switch statement to convert from region index to region name (see example below)

Method 6: getRegionalSales()

Return Type: float

This method returns the sales figures for a specific region.

Requirements:

  • Accept an int parameter representing the region (use the class constants)

  • Return the sales figure (as a float) for the specified region

  • Handle all four possible regions: NA_REGION, EU_REGION, JP_REGION, OTHER_REGION

Key Considerations:

  • Use a switch statement to select the appropriate sales figure based on the region parameter

Switch Statement Example

A switch statement allows you to select one of many code blocks to be executed. Here's how you might implement the getRegionalSales method using a switch statement:

The switch statement evaluates the regionIndex parameter and executes the code block that matches the case. The default case handles any values that don't match any specified cases.

Method 7: Implementation of Comparable

Return Type: int

You need to implement the compareTo method from the Comparable interface.

Requirements:

  • Override the compareTo method to compare games based on global sales

  • Higher sales should sort before lower sales (descending order)

  • Return negative value if this game has higher sales than the other game

  • Return positive value if this game has lower sales than the other game

Key Considerations:

  • Remember that returning a negative value means "this" comes before "other" in the sorted order

Activity 3: Results

Let's add one final method to our GameDataRow class before moving on to working with collections of game data:

Understanding Static Variables and Methods

This method introduces an important concept in Java: static members. Unlike instance variables and methods that belong to specific objects, static members belong to the class itself.

Here's what you need to know:

  • Static variables (like our gamesAnalyzed counter) are shared across all instances of a class. There's only one copy in memory, regardless of how many objects you create.

  • Static methods operate at the class level rather than the instance level. They can be called using the class name without creating an object: GameDataRow.getGamesAnalyzed().

  • Cannot use this in static context

In our case, we're using the static variable gamesAnalyzed to track how many game objects have been created throughout our program's execution. Each constructor increments this counter, and our static method lets us retrieve the current count.

Creating Your Own Analysis

Now it's time to put your skills to work! Modify the GameDataManager.java file to add your own custom analysis of the video game data.

Add at least one new method to the GameDataManager class that performs an interesting analysis. Here are some ideas:

  1. Year Analysis: Which year had the highest average game quality? Most releases?

  2. Developer Success Rate: For developers with multiple games, which ones have the highest average scores?

  3. Cross-Platform Analysis: Which games appeared on the most platforms? Do multi-platform games sell better?

  4. Sales vs. Ratings Correlation: Do higher-rated games generally sell better? Is there a difference between critic and user ratings?

Once you've added your analysis method(s), modify the main method to call your new method and display the results.

Example Custom Analysis Method

Here's what your custom analysis might look like (don't just copy this - create your own!):

In the main method, you would add a call to your new analysis:

Requirements for Your Custom Analysis

Your analysis should:

  • Process data from multiple games

  • Use loops and conditionals appropriately

  • Output meaningful results

  • Include comments explaining your approach

Activity 4: Recursion

In this final activity, you'll explore recursion - a powerful programming technique where a method calls itself to solve a problem by breaking it down into smaller instances of the same problem.

Understanding Recursion

Recursion is a fundamental concept in computer science that provides elegant solutions to certain types of problems. A recursive method has two essential components:

  1. Base case(s): The simplest scenario(s) that can be solved directly without further recursion

  2. Recursive case(s): Where the method calls itself with a simpler version of the problem

Let's examine a simple example - calculating the factorial of a number:

When calculating factorial(5):

  1. The method first checks if n equals 0 or 1 (our base case) - it doesn't, so we move to the recursive case

  2. It returns 5 * factorial(4)

  3. Now it calculates factorial(4), which is 4 * factorial(3)

Why Use Recursion?

Recursion offers several advantages:

  1. Elegance: Recursive solutions can be concise and elegant for problems that would be complex to solve iteratively

  2. Divide and conquer: Recursion naturally implements divide-and-conquer strategies, breaking down large data sets into smaller pieces

  3. Tree traversal: For hierarchical data structures like trees, recursion provides a natural approach

  4. Problem decomposition: Complex problems become simpler when broken into smaller, similar sub-problems

Implementing Recursion in Game Data Analysis

Looking at our GameDataManager class, several methods could benefit from a recursive approach. For example, finding games within a specific criteria could be implemented using a binary search algorithm rather than the current linear search.

Here's an example of how we could improve the findGamesByTitle method using a recursive binary search (assuming the games list is sorted by title):

The recursive binary search has a time complexity of O(log n), which is much more efficient than the O(n) time complexity of linear search, especially with large datasets.

Another example would be implementing a recursive merge sort:

Your Task

  1. Choose one method from your GameDataRow or GameDataManager class that could benefit from a recursive approach

  2. Reimplement it using recursion

  3. Test your implementation and compare it to the original approach

  4. Document your findings: Was the recursive approach more elegant? More efficient? What challenges did you face?

Alternatively, you can implement a new recursive method that performs a complex analysis that would be difficult to do iteratively. Here are some ideas:

  • Find the most profitable franchise (games with similar names) recursively

  • Implement a recursive algorithm to categorize games into hierarchical genres (e.g., "RPG > Action RPG")

  • Create a recursive method to find "hidden gems" - games with high ratings but low sales

Example: Recursive Method for Maximum Critic Score

Here's an example of how you might implement a recursive method to find the game with the highest critic score within a list of games:

This method uses a divide-and-conquer approach, recursively finding the maximum in each half of the list and then comparing the results. While this specific example might not be more efficient than a simple loop, it demonstrates the recursive pattern that can be applied to more complex problems.

Remember, not all problems are best solved with recursion. Some considerations:

  • Recursion can lead to stack overflow errors with very large inputs

  • Recursive solutions sometimes have higher memory usage

  • Some problems have simpler iterative solutions

Return 0.0f if no scores are available

Return zero if both games have equal sales

: Since static methods don't operate on a specific instance, you cannot use the
this
keyword inside them. There's no "current object" to reference! If you try to use
this
in a static method, you'll get a compiler error.
  • Access limitations: Static methods can only directly access other static members (variables or methods). They cannot directly access instance variables or methods without referencing a specific object.

  • Genre Evolution: How have the popularity of different genres changed over time?

  • Handle edge cases (like missing data)
    This continues until we reach factorial(1), which returns 1 (base case)
  • Then the recursion "unwinds": 1 * 2 * 3 * 4 * 5 = 120

  • https://gist.github.com/dadiletta/2310e45953e3bcb8db3cf151c7c75d06
    https://www.kaggle.com/datasets/thedevastator/video-game-sales-and-ratings?resource=download
    /**
     * The GameDataRow class represents a single video game from the dataset
     * and provides methods for analyzing game data including sales, ratings, and more.
     */
    public class GameDataRow implements Comparable<GameDataRow> {
        // Instance variables will go here
        // Methods will go here
    }
    @Override
    public int compareTo(GameDataRow other) {
        // if you want to implement Comparable, you'll need this sort of thing
        if (this.number > other.number) return -1;
        if (this.number < other.number) return 1;
        return 0;
    }
    public class MyClass implements Serializable { ... }
    public class MyTask implements Runnable {
        public void run() {
            // Code to be executed in a thread
        }
    }
    public class ButtonHandler implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            // Handle button click
        }
    }
    public class MyCollection<T> implements Iterable<T> {
        public Iterator<T> iterator() {
            // Return an iterator
        }
    }
    // Instance variables
    private int index;
    private String name;
    private String platform;
    private float yearOfRelease;
    private String genre;
    private String publisher;
    private float naSales;
    private float euSales;
    private float jpSales;
    private float otherSales;
    private float globalSales;
    private float criticScore;
    private float criticCount;
    private String userScore; // Note: String type, not float
    private float userCount;
    private String developer;
    private String rating;
    
    // Static variable to track number of games analyzed
    private static int gamesAnalyzed = 0;
    
    // Constants for regional sales references
    public static final int NA_REGION = 0;
    public static final int EU_REGION = 1;
    public static final int JP_REGION = 2;
    public static final int OTHER_REGION = 3;
    /**
     * Primary constructor for creating a new GameDataRow object with all fields
     */
    public GameDataRow(int index, String name, String platform, float yearOfRelease, 
                       String genre, String publisher, float naSales, float euSales, 
                       float jpSales, float otherSales, float globalSales, 
                       float criticScore, float criticCount, String userScore, 
                       float userCount, String developer, String rating) {
        this.index = index;
        this.name = name;
        this.platform = platform;
        this.yearOfRelease = yearOfRelease;
        this.genre = genre;
        this.publisher = publisher;
        this.naSales = naSales;
        this.euSales = euSales;
        this.jpSales = jpSales;
        this.otherSales = otherSales;
        this.globalSales = globalSales;
        this.criticScore = criticScore;
        this.criticCount = criticCount;
        this.userScore = userScore;
        this.userCount = userCount;
        this.developer = developer;
        this.rating = rating;
        gamesAnalyzed++;
    }
    
    /**
     * Simplified constructor with essential fields only
     */
    public GameDataRow(String name, String platform, float yearOfRelease, 
                       String genre, String publisher, float globalSales) {
        this.index = -1; // Default value for index
        this.name = name;
        this.platform = platform;
        this.yearOfRelease = yearOfRelease;
        this.genre = genre;
        this.publisher = publisher;
        this.naSales = 0.0f;
        this.euSales = 0.0f;
        this.jpSales = 0.0f;
        this.otherSales = 0.0f;
        this.globalSales = globalSales;
        this.criticScore = 0.0f;
        this.criticCount = 0.0f;
        this.userScore = "0.0";
        this.userCount = 0.0f;
        this.developer = "Unknown";
        this.rating = "Not Rated";
        gamesAnalyzed++;
    }
    
    /**
     * Copy constructor that creates a deep copy of another GameDataRow object
     */
    public GameDataRow(GameDataRow other) {
        this.index = other.index;
        this.name = other.name;
        this.platform = other.platform;
        this.yearOfRelease = other.yearOfRelease;
        this.genre = other.genre;
        this.publisher = other.publisher;
        this.naSales = other.naSales;
        this.euSales = other.euSales;
        this.jpSales = other.jpSales;
        this.otherSales = other.otherSales;
        this.globalSales = other.globalSales;
        this.criticScore = other.criticScore;
        this.criticCount = other.criticCount;
        this.userScore = other.userScore;
        this.userCount = other.userCount;
        this.developer = other.developer;
        this.rating = other.rating;
        gamesAnalyzed++;
    }
    // Accessor methods (getters)
    public int getIndex() { return index; }
    public String getName() { return name; }
    public String getPlatform() { return platform; }
    public float getYearOfRelease() { return yearOfRelease; }
    public String getGenre() { return genre; }
    public String getPublisher() { return publisher; }
    // ... and so on for all instance variables
    /**
     * Main method for testing the GameDataRow class
     */
    public static void main(String[] args) {
        System.out.println("===== GAME DATA ROW TEST SUITE =====\n");
        int testsPassed = 0;
        int totalTests = 9;
        
        try {
            // Test 1: Basic constructor and accessors
            GameDataRow game1 = new GameDataRow(
                1, "Wii Sports", "Wii", 2006.0f, "Sports", "Nintendo", 
                41.49f, 29.02f, 3.77f, 8.46f, 82.74f, 
                76.0f, 51.0f, "8.0", 322.0f, "Nintendo", "E"
            );
            
            if (game1.getName().equals("Wii Sports") && 
                game1.getPlatform().equals("Wii") &&
                game1.getGlobalSales() == 82.74f) {
                System.out.println("βœ“ Test 1 Passed: Basic constructor and accessors working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 1 Failed: Basic constructor or accessors not working correctly");
            }
            
            // Test 2: Simplified constructor
            GameDataRow game2 = new GameDataRow("Super Mario Bros.", "NES", 1985.0f, 
                                               "Platform", "Nintendo", 40.24f);
            
            if (game2.getName().equals("Super Mario Bros.") && 
                game2.getGlobalSales() == 40.24f) {
                System.out.println("βœ“ Test 2 Passed: Simplified constructor working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 2 Failed: Simplified constructor not working correctly");
            }
            
            // Test 3: Copy constructor
            GameDataRow game3 = new GameDataRow(game1);
            
            if (game3.getName().equals(game1.getName()) && 
                game3.getGlobalSales() == game1.getGlobalSales() &&
                game3 != game1) {  // Different objects
                System.out.println("βœ“ Test 3 Passed: Copy constructor working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 3 Failed: Copy constructor not working correctly");
            }
            
            // Test 4: Static counter
            int expectedCount = 3;  // game1, game2, game3
            if (GameDataRow.getGamesAnalyzed() == expectedCount) {
                System.out.println("βœ“ Test 4 Passed: Static counter working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 4 Failed: Static counter expected " + 
                                  expectedCount + " but got " + 
                                  GameDataRow.getGamesAnalyzed());
            }
            
            // Test 5: Get regional sales
            if (Math.abs(game1.getRegionalSales(GameDataRow.NA_REGION) - 41.49f) < 0.01f) {
                System.out.println("βœ“ Test 5 Passed: Regional sales accessor working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 5 Failed: Regional sales accessor not working correctly");
            }
            
            // Test 6: Dominant region
            if (game1.getDominantRegion().equals("North America")) {
                System.out.println("βœ“ Test 6 Passed: Dominant region calculation working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 6 Failed: Dominant region expected 'North America' but got '" + 
                                  game1.getDominantRegion() + "'");
            }
            
            // Test 7: Sales percentages
            float[] percentages = game1.getSalesPercentages();
            if (percentages.length == 4 && 
                Math.abs(percentages[0] - 50.14f) < 0.1f) {  // ~50.14% for NA
                System.out.println("βœ“ Test 7 Passed: Sales percentages calculation working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 7 Failed: Sales percentages not calculated correctly");
            }
            
            // Test 8: User score numeric conversion
            if (Math.abs(game1.getUserScoreNumeric() - 8.0f) < 0.01f) {
                System.out.println("βœ“ Test 8 Passed: User score numeric conversion working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 8 Failed: User score numeric conversion not working correctly");
            }
            
            // Test 9: compareTo implementation (formerly Test 10)
            if (game1.compareTo(game2) < 0 &&  // game1 has higher sales
                game2.compareTo(game1) > 0) {
                System.out.println("βœ“ Test 9 Passed: Comparable implementation working");
                testsPassed++;
            } else {
                System.out.println("βœ— Test 9 Failed: Comparable implementation not working correctly");
            }
            
        } catch (Exception e) {
            System.out.println("\nTest failed with exception: " + e.getMessage());
            e.printStackTrace();
        }
        
        // Summary
        System.out.println("\n===== TEST SUMMARY =====");
        System.out.println("Passed: " + testsPassed + "/" + totalTests + " tests");
        
        if (testsPassed == totalTests) {
            System.out.println("πŸŽ‰ Congratulations! All tests passed!");
        } else {
            System.out.println("❌ Some tests failed. Review the output above to see which tests failed.");
        }
    }
    /**
     * Determines if the game has user reviews
     * @return true if user score and count are available
     */
    public boolean hasUserReviews() {
        try {
            return Float.parseFloat(userScore) > 0 && userCount > 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    
    /**
     * Convert user score from string to float, handling special values
     * @return user score as float or 0.0 if not available
     */
    public float getUserScoreNumeric() {
        try {
            return Float.parseFloat(userScore);
        } catch (NumberFormatException e) {
            return 0.0f; // Return 0 for "tbd" or other non-numeric values
        }
    }
    
    /**
     * Returns a string representation of the game data
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(name).append(" (").append(yearOfRelease).append(")");
        sb.append("\nPlatform: ").append(platform);
        sb.append("\nGenre: ").append(genre);
        sb.append("\nPublisher: ").append(publisher);
        sb.append("\nDeveloper: ").append(developer);
        sb.append("\nRating: ").append(rating);
        sb.append("\nGlobal Sales: ").append(globalSales).append(" million units");
        sb.append("\nDominant Region: ").append(getDominantRegion());
        
        if (hasCriticReviews()) {
            sb.append("\nCritic Score: ").append(criticScore).append("/100 (").append(criticCount).append(" critics)");
        }
        
        if (hasUserReviews()) {
            sb.append("\nUser Score: ").append(userScore).append("/10 (").append(userCount).append(" users)");
        }
        
        return sb.toString();
    }
            // Convert critic score to scale of 10 like user score
    
            // fetch the user score using the built-in converter
    
            // check if any score is 0
                // if not, return the average of the two / 2.0f
                // else return the one with the number
    // Example of how to use a switch statement to return sales for a specific region
    // Example switch statement to convert a day number to a day name
    int dayNumber = 3;
    String dayName;
    
    switch(dayNumber) {
        case 1:
            dayName = "Monday";
            break;
        case 2:
            dayName = "Tuesday";
            break;
        case 3:
            dayName = "Wednesday";
            break;
        case 4:
            dayName = "Thursday";
            break;
        case 5:
            dayName = "Friday";
            break;
        case 6:
            dayName = "Saturday";
            break;
        case 7:
            dayName = "Sunday";
            break;
        default:
            dayName = "Invalid day";
            break;
    }
    // Static method to get count of games analyzed
    public static int getGamesAnalyzed() {
        return gamesAnalyzed;
    }
    /**
     * Analyze which platforms have the highest-rated games on average
     */
    public void analyzePlatformQuality() {
        Map<String, Float> totalScores = new HashMap<String, Float>();
        Map<String, Integer> gameCounts = new HashMap<String, Integer>();
        
        // Collect data
        for (GameDataRow game : games) {
            if (game.hasCriticReviews()) {
                String platform = game.getPlatform();
                totalScores.put(platform, totalScores.getOrDefault(platform, 0.0f) + game.getCriticScore());
                gameCounts.put(platform, gameCounts.getOrDefault(platform, 0) + 1);
            }
        }
        
        // Calculate averages and find the best
        String bestPlatform = "";
        float bestScore = 0.0f;
        
        System.out.println("\n===== PLATFORM QUALITY ANALYSIS =====");
        System.out.println("Platform\tAvg. Critic Score\tGame Count");
        
        for (String platform : totalScores.keySet()) {
            int count = gameCounts.get(platform);
            if (count >= 10) { // Only consider platforms with enough games
                float avgScore = totalScores.get(platform) / count;
                System.out.printf("%s\t\t%.1f\t\t%d\n", platform, avgScore, count);
                
                if (avgScore > bestScore) {
                    bestScore = avgScore;
                    bestPlatform = platform;
                }
            }
        }
        
        System.out.println("\nBest-rated platform: " + bestPlatform + 
                           " with average score " + bestScore);
    }
    // Call your custom analysis
    manager.analyzePlatformQuality();
    /**
     * Calculates the factorial of n (n!)
     * @param n a non-negative integer
     * @return the factorial of n
     */
    public static int factorial(int n) {
        // Base case
        if (n == 0 || n == 1) {
            return 1;
        }
        // Recursive case
        else {
            return n * factorial(n - 1);
        }
    }
    /**
     * Find a game by title using recursive binary search
     * @param title the title to search for
     * @return the game if found, null otherwise
     */
    public GameDataRow findGameByTitle(String title) {
        // Sort the games by title first (if not already sorted)
        Collections.sort(games, (g1, g2) -> g1.getName().compareTo(g2.getName()));
        
        // Call the recursive helper method
        return findGameByTitleHelper(title, 0, games.size() - 1);
    }
    
    /**
     * Recursive helper method for binary search
     */
    private GameDataRow findGameByTitleHelper(String title, int low, int high) {
        // Base case: element not found
        if (low > high) {
            return null;
        }
        
        // Calculate middle index
        int mid = (low + high) / 2;
        
        // Get the game at the middle
        GameDataRow midGame = games.get(mid);
        
        // Compare the middle element
        int comparison = midGame.getName().compareTo(title);
        
        // If found, return it
        if (comparison == 0) {
            return midGame;
        }
        // If the middle element is greater, search the left half
        else if (comparison > 0) {
            return findGameByTitleHelper(title, low, mid - 1);
        }
        // Otherwise, search the right half
        else {
            return findGameByTitleHelper(title, mid + 1, high);
        }
    }
    /**
     * Sort games by a certain criteria using merge sort
     * @param criteria the sorting criteria (e.g., "sales", "score")
     */
    public void sortGamesByMergeSort(String criteria) {
        games = mergeSort(games, criteria, 0, games.size() - 1);
    }
    
    /**
     * Recursive merge sort implementation
     */
    private ArrayList<GameDataRow> mergeSort(ArrayList<GameDataRow> list, String criteria, int left, int right) {
        // Base case: list of size 1 or empty
        if (left >= right) {
            ArrayList<GameDataRow> result = new ArrayList<>();
            if (left == right) {
                result.add(list.get(left));
            }
            return result;
        }
        
        // Find the middle point
        int mid = (left + right) / 2;
        
        // Recursively sort both halves
        ArrayList<GameDataRow> leftHalf = mergeSort(list, criteria, left, mid);
        ArrayList<GameDataRow> rightHalf = mergeSort(list, criteria, mid + 1, right);
        
        // Merge the sorted halves
        return merge(leftHalf, rightHalf, criteria);
    }
    
    /**
     * Merge two sorted lists
     */
    private ArrayList<GameDataRow> merge(ArrayList<GameDataRow> left, ArrayList<GameDataRow> right, String criteria) {
        ArrayList<GameDataRow> result = new ArrayList<>();
        int leftIndex = 0, rightIndex = 0;
        
        // Compare elements from both lists and add the smaller one to result
        while (leftIndex < left.size() && rightIndex < right.size()) {
            GameDataRow leftGame = left.get(leftIndex);
            GameDataRow rightGame = right.get(rightIndex);
            
            boolean addLeft = false;
            
            // Compare based on criteria
            if (criteria.equals("sales")) {
                addLeft = leftGame.getGlobalSales() >= rightGame.getGlobalSales();
            } else if (criteria.equals("score")) {
                addLeft = leftGame.getCriticScore() >= rightGame.getCriticScore();
            } else {  // Default to name
                addLeft = leftGame.getName().compareTo(rightGame.getName()) <= 0;
            }
            
            if (addLeft) {
                result.add(leftGame);
                leftIndex++;
            } else {
                result.add(rightGame);
                rightIndex++;
            }
        }
        
        // Add remaining elements
        while (leftIndex < left.size()) {
            result.add(left.get(leftIndex++));
        }
        
        while (rightIndex < right.size()) {
            result.add(right.get(rightIndex++));
        }
        
        return result;
    }
    /**
     * Find the game with the highest critic score using recursion
     */
    public GameDataRow findGameWithHighestCriticScore() {
        if (games.isEmpty()) {
            return null;
        }
        return findGameWithHighestCriticScoreHelper(0, games.size() - 1);
    }
    
    /**
     * Recursive helper to find the game with highest critic score
     */
    private GameDataRow findGameWithHighestCriticScoreHelper(int start, int end) {
        // Base case: only one game
        if (start == end) {
            return games.get(start);
        }
        
        // Base case: two games
        if (start + 1 == end) {
            if (games.get(start).getCriticScore() >= games.get(end).getCriticScore()) {
                return games.get(start);
            } else {
                return games.get(end);
            }
        }
        
        // Find middle index
        int mid = (start + end) / 2;
        
        // Recursively find max in both halves
        GameDataRow leftMax = findGameWithHighestCriticScoreHelper(start, mid);
        GameDataRow rightMax = findGameWithHighestCriticScoreHelper(mid + 1, end);
        
        // Return the game with higher critic score
        if (leftMax.getCriticScore() >= rightMax.getCriticScore()) {
            return leftMax;
        } else {
            return rightMax;
        }
    }