14 Feb 2024

English Dictionary CLI

TL;DR

This CLI tool give you the english definition of a word base, also use a Trie to give you predictions as you type your word.

Table of Contents

Table of Contents
  1. TL;DR
  2. Project Structure
  3. Usage
  4. Roadmap
  5. Contributing
  6. License
  7. Contact
  8. Acknowledgments

CLI App

Normal Funtionality: normal
Pipe Functionality: pipe

Project Structure

This project return the definition of a word in english and use a Trie to give you predictions as you type your word.

The project is divided in two parts:

  • cli: The CLI tool
  • cli.test: The test project

The CLI tool is divide in two files Program.cs and Trie.cs. The Program.cs is the main file that run the CLI tool and the Trie.cs is the data structure that hold the dictionary.

The projecr is structure as follow:

.
├── CliApp.sln
├── LICENSE
├── README.md
├── data
│   └── dictionary_compact.json -- Dictionary
├── cli
│   ├── CliApp.csproj
│   ├── Program.cs -- Program
│   └── Trie.cs -- Data Structure
└── cli.test
    ├── GlobalUsings.cs
    ├── UnitTest1.cs
    └── cli.test.csproj -- Test Project

Prerequisites

  • dotnet 8.0

Code

You can find the repository here

Program.cs

using System.Text.Json;
using cli.Structures;

namespace cli.App
{
    class Program
    {

        static int Main(string[] args)
        {
            CLI app = new CLI();
            app.Run();

            return 0;
        }
    }

    class CLI
    {
        private const uint MAX_PRESDICTION_SHOW = 20;
        private const string FILE_NAME = "data/dictionary_compact.json";
        public EnglishDictionary? myDic;
        private int[] wordPosition = [0, 0];
        private int lastPosition = 0;

        public CLI()
        {
            this.myDic = new EnglishDictionary(FILE_NAME);
        }

        public void Run()
        {
            if (myDic == null)
            {
                throw new NullReferenceException("Dictionary can not be null");
            }

            bool flag_continue = true;

            string word = "";

            Console.Clear();
            Console.WriteLine("Please provide a word:");

            string[] prediction = [];
            do
            {
                try
                {
                    if (!Console.KeyAvailable)
                    {
                        ConsoleKeyInfo keyInfo = Console.ReadKey(true);

                        if (keyInfo.Key == ConsoleKey.Enter || keyInfo.Key == ConsoleKey.Spacebar || keyInfo.Key == ConsoleKey.Escape)
                        {
                            flag_continue = false;
                        }
                        else if (keyInfo.Key == ConsoleKey.Backspace)
                        {
                            if (word.Length > 0)
                            {
                                word = word.Substring(0, word.Length - 1);
                            }
                        }
                        else if (keyInfo.Key == ConsoleKey.Tab)
                        {
                            if (prediction.Length > 0)
                            {
                                word = prediction[0];
                            }
                        }
                        else
                        {
                            word += keyInfo.Key.ToString().ToLower();
                        }
                        // clear the console
                        this.clearPredictions(prediction);
                    }
                    Console.WriteLine(word);
                    this.wordPosition[0] = word.Length;
                    this.wordPosition[1] = Console.CursorTop - 1;

                    prediction = myDic.getPrediction(word);
                    this.printPredictions(prediction);
                    this.lastPosition = Console.CursorTop;
                    Console.SetCursorPosition(this.wordPosition[0], this.wordPosition[1]);
                }
                catch (System.InvalidOperationException e)
                {
                    string? stdIn = Console.ReadLine();
                    if (stdIn != null)
                    {
                        word = stdIn;
                    }
                    else
                    {
                        throw new NullReferenceException("stdIn can not be null");
                    }
                    flag_continue = false;
                }

            } while (flag_continue);

            this.clearPredictions(prediction);
            string definition = myDic.getDefinition(word);
            Console.WriteLine($"Definition of {word}:");
            Console.WriteLine(definition);
        }

        private void printPredictions(string[] prediction)
        {
            if (prediction.Length > 0)
            {
                if (prediction.Length > MAX_PRESDICTION_SHOW)
                {
                    for (int i = 0; i < MAX_PRESDICTION_SHOW; i++)
                    {
                        Console.WriteLine("-> " + prediction[i]);
                    }
                }
                else
                {
                    for (int i = 0; i < prediction.Length; i++)
                    {
                        Console.WriteLine("-> " + prediction[i]);
                    }
                }
            }
        }

        private void clearPredictions(string[] predictions)
        {
            if (predictions.Length > 0)
            {
                Console.SetCursorPosition(0, this.lastPosition);
                if (predictions.Length > MAX_PRESDICTION_SHOW)
                {
                    for (int i = 0; i < MAX_PRESDICTION_SHOW + 1; i++)
                    {
                        Console.SetCursorPosition(0, Console.CursorTop - 1);
                        Console.WriteLine(new string(' ', Console.WindowWidth));
                        Console.SetCursorPosition(0, Console.CursorTop - 1);
                    }
                }
                else
                {
                    for (int i = 0; i < predictions.Length + 1; i++)
                    {
                        Console.SetCursorPosition(0, Console.CursorTop - 1);
                        Console.WriteLine(new string(' ', Console.WindowWidth));
                        Console.SetCursorPosition(0, Console.CursorTop - 1);
                    }
                }
            }
        }
    }


    class EnglishDictionary
    {
        public Dictionary<string, string>? dic;
        private Trie trie;

        public EnglishDictionary(string fileName)
        {
            string filePath = Path.GetFullPath(fileName);
            if (File.Exists(filePath))
            {
                string text = File.ReadAllText(filePath);
                this.dic = JsonSerializer.Deserialize<Dictionary<string, string>>(text);
            }
            else
            {
                Console.WriteLine($"File {filePath} does not exist");
            }
            this.trie = new Trie();
            this.addWordsToTrie();
        }

        public string[] getPrediction(string word)
        {
            string prediction;
            TrieNode node;
            if (word != null)
            {
                (prediction, node) = this.trie.predict(word);
                if (prediction != string.Empty)
                {
                    List<Tuple<string, uint>> words = new List<Tuple<string, uint>>();
                    this.trie.reconstructWords(node, prediction, words);
                    words.Sort((x, y) => y.Item2.CompareTo(x.Item2));
                    return words.Select(x => x.Item1).ToArray();
                }
            }
            return [""];
        }

        public string getDefinition(string word)
        {
            if (this.dic == null)
            {
                throw new NullReferenceException(" Dictionary can not be null");
            }
            if (this.dic.ContainsKey(word))
            {
                return this.dic[word];
            }
            else
            {
                return $"|--- {word} not in the Dictionary ---|";

            }
        }

        private void addWordsToTrie()
        {
            if (this.dic == null)
            {
                throw new NullReferenceException(" Dictionary can not be null");
            }
            List<string> words = new List<string>(this.dic.Keys);
            if (words.Count > 0)
            {
                foreach (string word in words)
                {
                    this.trie.add(word);
                }
            }
        }

    }
}

Trie.cs

namespace cli.Structures
{
    public class Trie
    {
        public TrieNode root;

        public Trie()
        {
            this.root = new TrieNode('.');
        }

        public void add(string word)
        {
            this.addWord(word, this.root);
        }

        public (string, TrieNode) predict(string word)
        {
            string prediction = string.Empty;
            TrieNode node = this.root;
            foreach (char letter in word)
            {
                if (node.children.ContainsKey(letter))
                {
                    node = node.children[letter];
                    prediction += node.letter;
                }
                else
                {
                    return (string.Empty, new TrieNode('.'));
                }
            }
            return (prediction, node);

        }

        public void reconstructWords(TrieNode node, string baseWord, List<Tuple<string, uint>> words)
        {
            if (node.isWordEnd)
            {
                words.Add(new Tuple<string, uint>(baseWord, node.count));
            }
            if (node.children.Count == 0)
            {
                return;
            }
            foreach (KeyValuePair<char, TrieNode> child in node.children)
            {
                string newBaseWord = baseWord + child.Value.letter;
                this.reconstructWords(child.Value, newBaseWord, words);
            }
        }


        private void addWord(string word, TrieNode node)
        {
            if (word.Equals(string.Empty))
            {
                node.isWordEnd = true;
            }
            else
            {
                char letter = word[0];
                if (node.children.ContainsKey(letter))
                {
                    node.children[letter].count++;
                    this.addWord(word.Substring(1), node.children[letter]);
                }
                else
                {
                    TrieNode newNode = new TrieNode(letter);
                    node.children.Add(letter, newNode);
                    this.addWord(word.Substring(1), newNode);
                }
            }
        }
    }

    public class TrieNode
    {
        public char letter;
        public Dictionary<char, TrieNode> children;
        public uint count;
        public bool isWordEnd;

        public TrieNode(char letter)
        {
            this.letter = letter;
            this.children = new Dictionary<char, TrieNode>();
            this.count = 1;
            this.isWordEnd = false;
        }
    }

    public class MyMath
    {
        public static int Add(int a, int b)
        {
            return a + b;
        }
    }
}

UnitTest1.cs

using cli.Structures;

namespace cli.test;

public class UnitTest1
{
    [Fact]
    public void TriePrediction()
    {
        Trie trie = new Trie();
        trie.add("car");
        trie.add("carbone");
        (var pred, var node) = trie.predict("ca");

        Assert.Equal("ca", pred);
        Assert.Equal((uint)2, node.count);
    }

    [Fact]
    public void TrieReconstructWords()
    {
        Trie trie = new Trie();
        trie.add("car");
        trie.add("carbone");
        List<Tuple<string, uint>> words = new List<Tuple<string, uint>>();
        trie.reconstructWords(trie.root, string.Empty, words);

        Assert.Equal(2, words.Count);
        Assert.Equal("car", words[0].Item1);
        Assert.Equal((uint)2, words[0].Item2);
        Assert.Equal("carbone", words[1].Item1);
        Assert.Equal((uint)1, words[1].Item2);
    }
}

Settings

dotnet run --project cli

Testing

dotnet test

Acknowledgements

© 2019 Jsuarez.Dev