3
\$\begingroup\$

This is just a program that displays summary statistics about discord messages sent to a certain user using a Discord data package. The CSV of messages to a user is "c{idOfChannel}\messages.csv". It's formatted as:

ID,Timestamp,Contents,Attachments

I'm semi-new to programming and have always been bad. I never bothered to make things look good in any way, but I want to improve my problem solving and, more importantly, code writing (so it's readable, maintainable, etc.). This is my first attempt, and I guess the only change I think I should make is maybe putting certain groups of methods into some specific class so it's less convoluted. I'm not sure.

    using CsvHelper;
    using CsvHelper.Configuration;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    
    static class Discord_Data_Analyser
    {
        static string messagesFolderPath;
        static string channelId;
        static string configPath;
        static dynamic records;
        static List<string> words;
        static void Main()
        {
            channelId = GetChannelID();
            configPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\x.config";
            messagesFolderPath = GetMessagesFolderPath() + $"\\c{channelId}\\messages.csv";
            records = GetRecords();
            words = GetWords();
    
            while (true)
            {
                Console.Clear();
                ConsoleKey dataQueryChoice = GetDataQueryChoice();
                switch (dataQueryChoice)
                {
                    case ConsoleKey.D1: // instances of word
                        string wordToCount = GetWordForInstanceQuery();
                        int instancesOfWord = GetInstancesOfWord(wordToCount);
                        Console.Clear();
                        Console.WriteLine($"{wordToCount} appears {instancesOfWord} times.");
                        break;
                    case ConsoleKey.D2: // top used words
                        var wordCounts = GetWordCounts();
                        int wordsToDisplay = GetNumberOfTopWordsToDisplay();
                        PrintTopWords(wordCounts, wordsToDisplay);
                        break;
                    case ConsoleKey.D3: // message increase per month
                        Dictionary<string, int> messagesPerMonth = GetMessagesPerMonth();
                        PrintMessagesPerMonthTable(messagesPerMonth);
                        break;
                }
    
                Console.WriteLine("\nPress enter to restart.");
                Console.ReadLine();
            }
        }
        static void PrintTopWords(IOrderedEnumerable<KeyValuePair<string, int>> wordCounts, int wordsToDisplay)
        {
            Console.Clear();
            int i = 0;
            int sum = 0;
            foreach (var pair in wordCounts)
            {
                i++;
                sum += pair.Value;
                Console.WriteLine($"{pair.Key}: {pair.Value}");
                if (i >= wordsToDisplay && wordsToDisplay != -1)
                    break;
            }
            int percent = (int)Math.Round((double)(100 * sum) / words.Count);
            Console.WriteLine($"Top {wordsToDisplay} words account for {percent}% of all words.");
        }
        static void PrintMessagesPerMonthTable(Dictionary<string, int> messagesPerMonth)
        {
            Console.Clear();
            Console.WriteLine("Date     Messages Sent  Increase/Decrease Since Last Month");
            Console.WriteLine("+-------+--------------+---------------------------------+");
            string previousMonth = null;
            int previousCount = 0;
            int messagesSum = 0;
            foreach (var pair in messagesPerMonth.Reverse())
            {
                messagesSum += pair.Value;
    
                string currentMonth = pair.Key;
                int currentCount = pair.Value;
    
                if (previousMonth != null)
                {
                    char plusSign = new char();
                    double percentChange = Math.Round(((double)currentCount - previousCount) / previousCount * 100);
                    if (percentChange >= 0)
                    {
                        plusSign = '+';
                    }
                    Console.Write($"{currentMonth}  ");
                    Console.Write(currentCount + new string(' ', 15 - currentCount.ToString().Length));
                    Console.Write($"{plusSign}{percentChange}%\n");
                }
                else
                {
                    Console.WriteLine($"{currentMonth}  {currentCount}");
                }
    
                previousMonth = currentMonth;
                previousCount = currentCount;
            }
            Console.WriteLine($"Total:   {messagesSum}");
        }
        static List<dynamic> GetRecords()
        {
            var config = new CsvConfiguration(CultureInfo.InvariantCulture)
            { Delimiter = "," };
    
            List<dynamic> recordsList = new List<dynamic>();
    
            using (StreamReader reader = new StreamReader(messagesFolderPath))
            using (CsvReader csv = new CsvReader(reader, config))
            {
                IEnumerable<dynamic> records = csv.GetRecords<dynamic>();
                recordsList.AddRange(records);
            }
    
            return recordsList;
        }
        static Dictionary<string, int> GetMessagesPerMonth()
        {
            var messagesPerMonth = new Dictionary<string, int>();
            foreach (dynamic record in records)
            {
                string month = record.Timestamp.Substring(0, 7);
                if (messagesPerMonth.ContainsKey(month))
                {
                    messagesPerMonth[month]++;
                }
                else
                {
                    messagesPerMonth.Add(month, 1);
                }
            }
            return messagesPerMonth;
        }
        static string FilterContents(string contents)
        {
            // replace with space otherwise "COCK!^!" turns into "cock^" and fails the "cock" comparison
            return contents
                .Replace("!", " ")
                .Replace("@", " ")
                .Replace("#", " ")
                .Replace("?", " ")
                .Replace("(", " ")
                .Replace(")", " ")
                .Replace("\\", " ")
                .Replace("[", " ")
                .Replace("]", " ")
                .Replace(";", " ")
                .Replace("'", " ")
                .Replace("/", " ");
        }
        static List<string> GetWords()
        {
            var words = new List<string>();
            foreach (dynamic record in records)
            {
                string contents = FilterContents(record.Contents).ToLower();
                string[] wordsInCurrentRecord = contents.Split(new char[] { ' ', ',', '.' }, StringSplitOptions.RemoveEmptyEntries);
                words.AddRange(wordsInCurrentRecord);
            }
            return words;
        }
        static IOrderedEnumerable<KeyValuePair<string, int>> GetWordCounts()
        {
            var wordCounts = new Dictionary<string, int>();
            foreach (string word in words)
            {
                if (wordCounts.ContainsKey(word))
                {
                    wordCounts[word]++;
                }
                else
                {
                    wordCounts[word] = 1;
                }
            }
            return wordCounts.OrderByDescending(pair => pair.Value);
        }
        static int GetInstancesOfWord(string wordToCount)
        {
            int instancesOfWord = 0;
            foreach (string word in words)
            {
                if (word == wordToCount)
                {
                    instancesOfWord++;
                }
            }
            return instancesOfWord;
        }
        static string GetWordForInstanceQuery()
        {
            Console.Clear();
            Console.Write("Word: ");
            return Console.ReadLine().Trim().ToLower();
        }
        static int GetNumberOfTopWordsToDisplay()
        {
            while (true)
            {
                Console.Clear();
                Console.Write("Number of top words (-1 for all): ");
                string input = Console.ReadLine().Trim();
                if (int.TryParse(input, out int numberOfTopWords))
                {
                    return numberOfTopWords;
                }
                else
                {
                    Console.WriteLine("Invalid input. Press Enter.");
                    Console.ReadLine();
                }
            }
        }
        static string GetChannelID()
        {
            while (true)
            {
                Console.Clear();
                Console.Write("Channel ID: ");
                string channelId = Console.ReadLine().Trim();
    
                if (channelId.All(char.IsDigit))
                {
                    return channelId;
                }
                else
                {
                    Console.WriteLine("Invalid input. Press Enter.");
                    Console.ReadLine();
                }
            }
        }
        static string GetMessagesFolderPathFromConfig()
        {
            foreach (string line in File.ReadAllLines(configPath))
            {
                if (line.StartsWith("MessagesFolderPath="))
                {
                    return line.Substring("MessagesFolderPath=".Length);
                }
            }
            return "";
        }
        static void GenerateConfigFile(string data)
        {
            File.Create(configPath).Close();
            File.WriteAllText(configPath, data);
        }
        static ConsoleKey GetDataQueryChoice()
        {
            while (true)
            {
                Console.Clear();
                Console.WriteLine(@"Query:
    [1] Total instances of a word
    [2] Top used words
    [3] Message increase per month
    ");
                ConsoleKey key = Console.ReadKey().Key;
                if (key == ConsoleKey.D1 || key == ConsoleKey.D2 || key == ConsoleKey.D3)
                {
                    return key;
                }
                else
                {
                    Console.Clear();
                    Console.WriteLine("Invalid input. Press Enter.");
                    Console.ReadLine();
                }
            }
        }
        static string GetMessagesFolderPath()
        {
            while (true)
            {
                if (!File.Exists(configPath))
                {
                    Console.Clear();
                    Console.Write("Path to 'messages' folder: ");
                    string messagesFolderPath = Console.ReadLine().Trim().ToLower();
    
                    char lastCharOfPath = messagesFolderPath.Last();
                    if (lastCharOfPath == '\\' || lastCharOfPath == '/')
                    {
                        messagesFolderPath = messagesFolderPath.Remove(messagesFolderPath.Length - 1);
                    }
    
                    if (!Directory.Exists(messagesFolderPath))
                    {
                        Console.WriteLine("Path doesn't exist. Press Enter.");
                        Console.ReadLine();
                        continue;
                    }
                    GenerateConfigFile("Config data for discord data app\n\nMessagesFolderPath=" + messagesFolderPath);
                    return messagesFolderPath;
                }
                else
                {
                    return GetMessagesFolderPathFromConfig();
                }
            }
        }
    }
\$\endgroup\$
2
  • 2
    \$\begingroup\$ Which .NET and C# version are you using? \$\endgroup\$ Commented Apr 25 at 8:08
  • \$\begingroup\$ .NET Framework 4.8 \$\endgroup\$ Commented Apr 26 at 9:10

1 Answer 1

4
\$\begingroup\$

Yes, as you have indicated, the very first step should be to split your code into dedicated modules. Almost all of your methods are used only once. A very natural split could be to encapsulate those methods together that are required for a given calculation. That would give you high cohesion.

In the below XYZProcessor classes I've exposed a Process method which is the only public method. The rest of them are implementation details. Please also note that there are no static methods.

internal class InstancesOfWordProcessor
{
    public void Process(List<string> words)
    {
        string wordToCount = GetWordForInstanceQuery();
        int instancesOfWord = GetInstancesOfWord(words, wordToCount);
        Console.Clear();
        Console.WriteLine($"{wordToCount} appears {instancesOfWord} times.");
    }

    string GetWordForInstanceQuery()
    {
        Console.Clear();
        Console.Write("Word: ");
        return Console.ReadLine().Trim().ToLower();
    }

    int GetInstancesOfWord(List<string> words, string wordToCount)
    {
        int instancesOfWord = 0;
        foreach (string word in words)
        {
            if (word == wordToCount)
            {
                instancesOfWord++;
            }
        }
        return instancesOfWord;
    }
}
internal class TopWordsProcessor
{
    public void Process(List<string> words)
    {
        var wordCounts = GetWordCounts(words);
        int wordsToDisplay = GetNumberOfTopWordsToDisplay();
        PrintTopWords(words.Count, wordCounts, wordsToDisplay);
    }

    IOrderedEnumerable<KeyValuePair<string, int>> GetWordCounts(List<string> words)
    {
        var wordCounts = new Dictionary<string, int>();
        foreach (string word in words)
        {
            if (wordCounts.ContainsKey(word))
            {
                wordCounts[word]++;
            }
            else
            {
                wordCounts[word] = 1;
            }
        }
        return wordCounts.OrderByDescending(pair => pair.Value);
    }

    int GetNumberOfTopWordsToDisplay()
    {
        while (true)
        {
            Console.Clear();
            Console.Write("Number of top words (-1 for all): ");
            string input = Console.ReadLine().Trim();
            if (int.TryParse(input, out int numberOfTopWords))
            {
                return numberOfTopWords;
            }
            else
            {
                Console.WriteLine("Invalid input. Press Enter.");
                Console.ReadLine();
            }
        }
    }

    void PrintTopWords(int wordsCount, IOrderedEnumerable<KeyValuePair<string, int>> wordCounts, int wordsToDisplay)
    {
        Console.Clear();
        int i = 0;
        int sum = 0;
        foreach (var pair in wordCounts)
        {
            i++;
            sum += pair.Value;
            Console.WriteLine($"{pair.Key}: {pair.Value}");
            if (i >= wordsToDisplay && wordsToDisplay != -1)
                break;
        }
        int percent = (int)Math.Round((double)(100 * sum) / wordsCount);
        Console.WriteLine($"Top {wordsToDisplay} words account for {percent}% of all words.");
    }
}
internal class MessagesPerMonthProcessor
{
    public void Process(dynamic records)
    {
        Dictionary<string, int> messagesPerMonth = GetMessagesPerMonth(records);
        PrintMessagesPerMonthTable(messagesPerMonth);
    }

    Dictionary<string, int> GetMessagesPerMonth(dynamic records)
    {
        var messagesPerMonth = new Dictionary<string, int>();
        foreach (dynamic record in records)
        {
            string month = record.Timestamp.Substring(0, 7);
            if (messagesPerMonth.ContainsKey(month))
            {
                messagesPerMonth[month]++;
            }
            else
            {
                messagesPerMonth.Add(month, 1);
            }
        }
        return messagesPerMonth;
    }

    void PrintMessagesPerMonthTable(Dictionary<string, int> messagesPerMonth)
    {
        Console.Clear();
        Console.WriteLine("Date     Messages Sent  Increase/Decrease Since Last Month");
        Console.WriteLine("+-------+--------------+---------------------------------+");
        string previousMonth = null;
        int previousCount = 0;
        int messagesSum = 0;
        foreach (var pair in messagesPerMonth.Reverse())
        {
            messagesSum += pair.Value;

            string currentMonth = pair.Key;
            int currentCount = pair.Value;

            if (previousMonth != null)
            {
                char plusSign = new char();
                double percentChange = Math.Round(((double)currentCount - previousCount) / previousCount * 100);
                if (percentChange >= 0)
                {
                    plusSign = '+';
                }
                Console.Write($"{currentMonth}  ");
                Console.Write(currentCount + new string(' ', 15 - currentCount.ToString().Length));
                Console.Write($"{plusSign}{percentChange}%\n");
            }
            else
            {
                Console.WriteLine($"{currentMonth}  {currentCount}");
            }

            previousMonth = currentMonth;
            previousCount = currentCount;
        }
        Console.WriteLine($"Total:   {messagesSum}");
    }
}

Similarly you can also create one dedicated class for the CSV processing and another for the initial input gathering. With these in your hand the menu handling would be much simpler:

while (true)
{
    Console.Clear();
    switch (GetDataQueryChoice())
    {
        case ConsoleKey.D1:
            new InstancesOfWordProcessor().Process(words);
            break;
        case ConsoleKey.D2:
            new TopWordsProcessor().Process(words);
            break;
        case ConsoleKey.D3:
            new MessagesPerMonthProcessor().Process(records);
            break;
    }

    Console.WriteLine("\nPress enter to restart.");
    Console.ReadLine();
}

Having multiple tiny and focused modules simplifies future extensions and maintenance. After you are done with your code reorganization then you can start refactoring methods/modules/flows to make them more efficient.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ thank you for your time :). I figured the abundance of methods was bad practice but i had no idea how i would break it up into classes. thanks for your reply \$\endgroup\$ Commented Apr 26 at 9:13

Not the answer you're looking for? Browse other questions tagged or ask your own question.