diff --git a/Images/Main.png b/Images/Main.png new file mode 100644 index 0000000..fc525d2 Binary files /dev/null and b/Images/Main.png differ diff --git "a/Images/\320\222\321\213\320\262\320\276\320\264.png" "b/Images/\320\222\321\213\320\262\320\276\320\264.png" new file mode 100644 index 0000000..5a5b95d Binary files /dev/null and "b/Images/\320\222\321\213\320\262\320\276\320\264.png" differ diff --git "a/Images/\320\224\320\270\320\260\320\263\321\200\320\260\320\274\320\274\320\260.png" "b/Images/\320\224\320\270\320\260\320\263\321\200\320\260\320\274\320\274\320\260.png" new file mode 100644 index 0000000..8c26ca3 Binary files /dev/null and "b/Images/\320\224\320\270\320\260\320\263\321\200\320\260\320\274\320\274\320\260.png" differ diff --git "a/Images/\320\236\320\261\320\265\321\200\321\202\320\272\320\260.png" "b/Images/\320\236\320\261\320\265\321\200\321\202\320\272\320\260.png" new file mode 100644 index 0000000..2ba78e9 Binary files /dev/null and "b/Images/\320\236\320\261\320\265\321\200\321\202\320\272\320\260.png" differ diff --git "a/Images/\320\237\320\276\320\264\321\201\321\207\321\221\321\202.png" "b/Images/\320\237\320\276\320\264\321\201\321\207\321\221\321\202.png" new file mode 100644 index 0000000..1c206cf Binary files /dev/null and "b/Images/\320\237\320\276\320\264\321\201\321\207\321\221\321\202.png" differ diff --git "a/Images/\320\242\320\265\321\201\321\202\321\213.png" "b/Images/\320\242\320\265\321\201\321\202\321\213.png" new file mode 100644 index 0000000..61fa37b Binary files /dev/null and "b/Images/\320\242\320\265\321\201\321\202\321\213.png" differ diff --git "a/Images/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265.png" "b/Images/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265.png" new file mode 100644 index 0000000..3266905 Binary files /dev/null and "b/Images/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265.png" differ diff --git a/README.md b/README.md index 1036790..d7111c9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Тестовое задание. +# Тестовое задание. +``` Доброго дня. Данный репозиторий представляет из себя тестовое задание для выполнения перед собеседованием на вакансию разработчика C# в компанию Катрен. @@ -20,3 +21,68 @@ __Для разработки настоятельно рекомендуетс И по готовности результат отдать на ревью методом Pull Request'a.__ Если данный способ по каким-то причинам Вам не представляется реализуемым, можете выслать результат в виде архива на почту кадровому агенту. +``` +# Обзор проекта. + +**Тип проекта** - консольное приложение с версией .NET фреймворка 4.6.1.
+**Назначение (кратко)** - консольное приложение, анализирующее содержимое файла через обертку ReadOnlyStream и выводящее результаты анализа в консоль.
+**Ввод** - название файлов, передаваемых через аргументы командой строки.
+**Вывод** - статистика вхождений одиночных элементов в первом файле, а также количество повторяющихся элементов во втором файле.
+**Узкие места проекта** - *синхронное* чтение файлов без полной загрузки в память. + +# План: +1. Реализация синхронной обертки чтения файлов - ReadOnlyStream с обработкой ошибок. +2. Реализация логики анализа статистики анализа вхождений (скорее всего с помощью Dictionary). +3. Оформление структурированного вывода в консоль. +4. Написание Unit-тестов (если останется время).* + +# Реализация: + +### Обертка над потоком данных файла. +Принял решение реализовать чтение в ReadOnlyStream через StreamReader, чтобы не мучаться с кодировкой и с проблемами, когда, например, файл повреждён. + +![Конструктор класса](Images/Обертка.png) + +Очистку стримов решил реализовать через IDisposable интерфейс и вызов Close у обоих потоков. + +### Реализация логики анализа статистики вхождений. +Для реализации хранилища статистики создал специальный интерфейс, подразумевающий разные реализации. + +![Конструктор класса](Images/Хранилище.png) + +Для подсчёта количества букв использовал реализацию со словарём, так как у него низкое время поиска элементов + у нас может быть не такое большое количество разных элементов, чтобы беспокоиться о коллизиях. + +![Конструктор класса](Images/Подсчёт.png) + +**Есть сомнения насчёт логики подсчёта элементов, реализовал её, как просто подсчёт количества подряд идущих повторяющихся букв. Например, для файла с текстом "нНн" результатом будет 2.** + +### Работа с консолью. + +Сделал красивый вывод с помощью таблицы и с использованием PadRight. + +![Конструктор класса](Images/Вывод.png) + +### Рефакторинг. + +Исходные методы имели модификатор private static, тем самым исключая любую возможность их переиспользования в других программах. Также создание объектов внутри конструктора является не очень хорошей практикой, поэтому метод создания ReadOnlyStream был переенсён в отдельную фабрику.
+ +По итогу были совершены следующие изменения: +1. Вынесены методы, занимающиеся анализом файла через IReadOnlyStream в отдельный LetterAnalysisFacade. Почему не в ReadOnlyStream? Потому что данные методы узконаправлены и выходят за текущий интерефейс. +2. Метод GetInputStream(string) вынесен в отдельную фабрику IReadOnlyStreamFactory с инъекцией зависимостей. +3. Метод PrintStatistics(IList\) вынесен в метод ConsoleHelperMethods. Почему сделан static? Потому что метод имеет чёткую привязку к среде выполнения и не меняет внутреннее состояние каких-либо объектов. + +![Конструктор класса](Images/Main.png) + +### Диаграмма классов. + +Была составлена дигарамма классов для удобства других разработчиков. + +![Диаграмма классов](Images/Диаграмма.png) + +### Unit тесты. + +Для создания Unit тестов был создан дополнительный проект TestTaskTest. Для Unit тестирования был выбран фреймворк NUnit, потому что только с этим фреймворком я знаком достаточно, чтобы использовать его в production. +

+В результате были реализованы тесты для основных алгоритмов в проекте. + +![Тесты](Images/Тесты.png) \ No newline at end of file diff --git a/TestTask/CharType.cs b/TestTask/LetterAnalysis/CharType.cs similarity index 94% rename from TestTask/CharType.cs rename to TestTask/LetterAnalysis/CharType.cs index 899564d..587a031 100644 --- a/TestTask/CharType.cs +++ b/TestTask/LetterAnalysis/CharType.cs @@ -1,18 +1,18 @@ -namespace TestTask -{ - /// - /// Тип букв - /// - public enum CharType - { - /// - /// Гласные - /// - Vowel, - - /// - /// Согласные - /// - Consonants - } -} +namespace TestTask +{ + /// + /// Тип букв + /// + public enum CharType + { + /// + /// Гласные + /// + Vowel, + + /// + /// Согласные + /// + Consonants + } +} diff --git a/TestTask/LetterAnalysis/DictionaryLetterStorage.cs b/TestTask/LetterAnalysis/DictionaryLetterStorage.cs new file mode 100644 index 0000000..ef72c40 --- /dev/null +++ b/TestTask/LetterAnalysis/DictionaryLetterStorage.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TestTask +{ + /// + /// Контейнер для подсчёта количества вхождений текста с помощью словаря + /// + public sealed class DictionaryLetterStorage : ILetterAnalysisStorage + { + private Dictionary _data = new Dictionary(); + + /// + /// Обрабатывает статистику по введённому тексту + /// + /// + public void Add(string text) + { + if (_data.TryGetValue(text, out LetterStats stats)) + { + IncStatistic(text, stats); + } + else + { + stats = new LetterStats + { + Letter = text.ToString(), + Count = 1 + }; + _data.Add(text, stats); + } + } + + /// + /// Метод увеличивает счётчик вхождений по переданной структуре. + /// + /// + private void IncStatistic(string key, LetterStats value) + { + value.Count++; + _data[key] = value; + } + + /// + /// Возвращает список с данными вхождений текста + /// + /// Список с данными вхождений текста + public IList GetStatistics() => _data.Values.ToList(); + } +} \ No newline at end of file diff --git a/TestTask/LetterAnalysis/ILetterAnalysisStorage.cs b/TestTask/LetterAnalysis/ILetterAnalysisStorage.cs new file mode 100644 index 0000000..cde3f2b --- /dev/null +++ b/TestTask/LetterAnalysis/ILetterAnalysisStorage.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace TestTask +{ + /// + /// Интерфейс контейнера для подсчёта количества вхождений текста + /// + internal interface ILetterAnalysisStorage + { + void Add(string text); + + IList GetStatistics(); + } +} diff --git a/TestTask/LetterAnalysis/LetterAnalysisFacade.cs b/TestTask/LetterAnalysis/LetterAnalysisFacade.cs new file mode 100644 index 0000000..01f67fc --- /dev/null +++ b/TestTask/LetterAnalysis/LetterAnalysisFacade.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TestTask +{ + /// + /// Фасад для анализа файлов + /// + internal class LetterAnalysisFacade + { + private const string VOWELS = "AEIOUYАОЕЯЁЭЫУИЮaeiouyаоеяёэыуию"; + + /// + /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения каждой буквы. + /// Статистика РЕГИСТРОЗАВИСИМАЯ! + /// + /// Стрим для считывания символов для последующего анализа + /// Коллекция статистик по каждой букве, что была прочитана из стрима. + public IList FillSingleLetterStats(IReadOnlyStream stream) + { + stream.ResetPositionToStart(); + ILetterAnalysisStorage storage = new DictionaryLetterStorage(); + + while (!stream.IsEof) + { + char c = stream.ReadNextChar(); + + if (!char.IsLetter(c)) + { + continue; + } + + string letterText = c.ToString(); + storage.Add(letterText); + } + + return storage.GetStatistics(); + } + + /// + /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. + /// В статистику должны попадать только пары из одинаковых букв, например АА, СС, УУ, ЕЕ и т.д. + /// Статистика - НЕ регистрозависимая! + /// + /// Стрим для считывания символов для последующего анализа + /// Коллекция статистик по каждой букве, что была прочитана из стрима. + public IList FillDoubleLetterStats(IReadOnlyStream stream) + { + stream.ResetPositionToStart(); + ILetterAnalysisStorage storage = new DictionaryLetterStorage(); + + bool hasPair = false; + char previousChar = ' '; + + while (!stream.IsEof) + { + char c = stream.ReadNextChar(); + if (!char.IsLetter(c)) + { + hasPair = false; + continue; + } + + if (!hasPair) + { + previousChar = char.ToUpper(c); + hasPair = true; + continue; + } + + char upperFirstChar = previousChar; + char upperSecondChar = char.ToUpper(c); + previousChar = upperSecondChar; + + if (upperFirstChar != upperSecondChar) + { + continue; + } + + string letterText = string.Concat(upperFirstChar, upperSecondChar); + storage.Add(letterText); + } + + return storage.GetStatistics(); + } + + /// + /// Ф-ция перебирает все найденные буквы/парные буквы, содержащие в себе только гласные или согласные буквы. + /// (Тип букв для перебора определяется параметром charType) + /// Все найденные буквы/пары соответствующие параметру поиска - удаляются из переданной коллекции статистик. + /// + /// Коллекция со статистиками вхождения букв/пар + /// Тип букв для анализа + public IList RemoveCharStatsByType(IList letters, CharType charType) + { + switch (charType) + { + case CharType.Consonants: + { + return letters.Where(x => VOWELS.Contains(x.Letter[0])).ToList(); + } + case CharType.Vowel: + { + return letters.Where(x => !VOWELS.Contains(x.Letter[0])).ToList(); + } + default: + { + return letters; + } + } + } + } +} diff --git a/TestTask/LetterStats.cs b/TestTask/LetterAnalysis/LetterStats.cs similarity index 96% rename from TestTask/LetterStats.cs rename to TestTask/LetterAnalysis/LetterStats.cs index aa10728..90611af 100644 --- a/TestTask/LetterStats.cs +++ b/TestTask/LetterAnalysis/LetterStats.cs @@ -1,18 +1,18 @@ -namespace TestTask -{ - /// - /// Статистика вхождения буквы/пары букв - /// - public struct LetterStats - { - /// - /// Буква/Пара букв для учёта статистики. - /// - public string Letter; - - /// - /// Кол-во вхождений буквы/пары. - /// - public int Count; - } -} +namespace TestTask +{ + /// + /// Статистика вхождения буквы/пары букв + /// + public struct LetterStats + { + /// + /// Буква/Пара букв для учёта статистики. + /// + public string Letter; + + /// + /// Кол-во вхождений буквы/пары. + /// + public int Count; + } +} diff --git a/TestTask/Program.cs b/TestTask/Program.cs index fdf048e..146b19d 100644 --- a/TestTask/Program.cs +++ b/TestTask/Program.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; namespace TestTask { public class Program { - /// /// Программа принимает на входе 2 пути до файлов. /// Анализирует в первом файле кол-во вхождений каждой буквы (регистрозависимо). Например А, б, Б, Г и т.д. @@ -16,114 +16,59 @@ public class Program /// Второй параметр - путь до второго файла. static void Main(string[] args) { - IReadOnlyStream inputStream1 = GetInputStream(args[0]); - IReadOnlyStream inputStream2 = GetInputStream(args[1]); + try + { + IReadOnlyStreamFactory factory = new BaseReadOnlyStreamFactory(); + LetterAnalysisFacade facade = new LetterAnalysisFacade(); - IList singleLetterStats = FillSingleLetterStats(inputStream1); - IList doubleLetterStats = FillDoubleLetterStats(inputStream2); + using (IReadOnlyStream inputStream1 = factory.GetInputStream(args[0])) + { + IList singleLetterStats = facade.FillSingleLetterStats(inputStream1); + singleLetterStats = facade.RemoveCharStatsByType(singleLetterStats, CharType.Vowel); - RemoveCharStatsByType(singleLetterStats, CharType.Vowel); - RemoveCharStatsByType(doubleLetterStats, CharType.Consonants); + Console.WriteLine($"Результаты анализа файла {args[0]}\n"); + ConsoleHelperMethods.PrintStatistic(singleLetterStats); + } - PrintStatistic(singleLetterStats); - PrintStatistic(doubleLetterStats); + Console.WriteLine("\n"); - // TODO : Необжодимо дождаться нажатия клавиши, прежде чем завершать выполнение программы. - } + using (IReadOnlyStream inputStream2 = factory.GetInputStream(args[1])) + { + IList doubleLetterStats = facade.FillDoubleLetterStats(inputStream2); + doubleLetterStats = facade.RemoveCharStatsByType(doubleLetterStats, CharType.Consonants); - /// - /// Ф-ция возвращает экземпляр потока с уже загруженным файлом для последующего посимвольного чтения. - /// - /// Полный путь до файла для чтения - /// Поток для последующего чтения. - private static IReadOnlyStream GetInputStream(string fileFullPath) - { - return new ReadOnlyStream(fileFullPath); - } - - /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения каждой буквы. - /// Статистика РЕГИСТРОЗАВИСИМАЯ! - /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillSingleLetterStats(IReadOnlyStream stream) - { - stream.ResetPositionToStart(); - while (!stream.IsEof) + Console.WriteLine($"Результаты анализа файла {args[1]}\n"); + ConsoleHelperMethods.PrintStatistic(doubleLetterStats); + } + } + catch (IndexOutOfRangeException) { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - регистрозависимый. + Console.WriteLine($"Ошибка, передано {args.Length} аргументов командой строки вместо 2!"); } - - //return ???; - - throw new NotImplementedException(); - } - - /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. - /// В статистику должны попадать только пары из одинаковых букв, например АА, СС, УУ, ЕЕ и т.д. - /// Статистика - НЕ регистрозависимая! - /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillDoubleLetterStats(IReadOnlyStream stream) - { - stream.ResetPositionToStart(); - while (!stream.IsEof) + catch (NullReferenceException ex) { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - НЕ регистрозависимый. + Console.WriteLine(ex.Message); } - - //return ???; - - throw new NotImplementedException(); - } - - /// - /// Ф-ция перебирает все найденные буквы/парные буквы, содержащие в себе только гласные или согласные буквы. - /// (Тип букв для перебора определяется параметром charType) - /// Все найденные буквы/пары соответствующие параметру поиска - удаляются из переданной коллекции статистик. - /// - /// Коллекция со статистиками вхождения букв/пар - /// Тип букв для анализа - private static void RemoveCharStatsByType(IList letters, CharType charType) - { - // TODO : Удалить статистику по запрошенному типу букв. - switch (charType) + catch (ArgumentNullException ex) { - case CharType.Consonants: - break; - case CharType.Vowel: - break; + Console.WriteLine(ex.Message); + } + catch (DirectoryNotFoundException ex) + { + Console.WriteLine(ex.Message); + } + catch (FileNotFoundException ex) + { + Console.WriteLine(ex.Message); + } + catch (Exception ex) + { + Console.WriteLine("Возникла непредвиденная ошибка! \nТекст ошибки: " + ex.ToString()); } - - } - - /// - /// Ф-ция выводит на экран полученную статистику в формате "{Буква} : {Кол-во}" - /// Каждая буква - с новой строки. - /// Выводить на экран необходимо предварительно отсортировав набор по алфавиту. - /// В конце отдельная строчка с ИТОГО, содержащая в себе общее кол-во найденных букв/пар - /// - /// Коллекция со статистикой - private static void PrintStatistic(IEnumerable letters) - { - // TODO : Выводить на экран статистику. Выводить предварительно отсортировав по алфавиту! - throw new NotImplementedException(); - } - /// - /// Метод увеличивает счётчик вхождений по переданной структуре. - /// - /// - private static void IncStatistic(LetterStats letterStats) - { - letterStats.Count++; + Console.WriteLine("\n"); + Console.Write("Нажмите на любую кнопку, чтобы закрыть программу..."); + Console.ReadKey(); } - - } } diff --git a/TestTask/Properties/AssemblyInfo.cs b/TestTask/Properties/AssemblyInfo.cs index aecb854..e6eb01c 100644 --- a/TestTask/Properties/AssemblyInfo.cs +++ b/TestTask/Properties/AssemblyInfo.cs @@ -13,6 +13,7 @@ [assembly: AssemblyCopyright("Copyright © 2018")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("TestTaskTest")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from diff --git a/TestTask/ReadOnlyStream/BaseReadOnlyStreamFactory.cs b/TestTask/ReadOnlyStream/BaseReadOnlyStreamFactory.cs new file mode 100644 index 0000000..c66dfb9 --- /dev/null +++ b/TestTask/ReadOnlyStream/BaseReadOnlyStreamFactory.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; + +namespace TestTask +{ + /// + /// Простая фабрика для создания потоков для чтения + /// + internal class BaseReadOnlyStreamFactory : IReadOnlyStreamFactory + { + /// + /// Ф-ция возвращает экземпляр потока с уже загруженным файлом для последующего посимвольного чтения. + /// + /// Полный путь до файла для чтения + /// Выкидывается в случае передачи пустой строки + /// Выкидывается в случае передачи несуществующей директории + /// Выкидывается в случае, если файл не существует + /// Выкидывается в случае, если не удалось создать итоговый объект + /// Поток для последующего чтения. + public virtual IReadOnlyStream GetInputStream(string fileFullPath) + { + if (string.IsNullOrWhiteSpace(fileFullPath)) + { + throw new ArgumentNullException("Передано null вместо названия файла"); + } + + if (!Directory.Exists(Path.GetDirectoryName(fileFullPath))) + { + throw new DirectoryNotFoundException($"Директории с файлом {fileFullPath} не существует"); + } + + if (!File.Exists(fileFullPath)) + { + throw new FileNotFoundException($"Файла в пути {fileFullPath} не существует"); + } + + IReadOnlyStream stream = null; + Stream dataStream = null; + StreamReader reader = null; + try + { + dataStream = new FileStream(fileFullPath, FileMode.Open); + reader = new StreamReader(dataStream); + stream = new ReadOnlyStream(dataStream, reader); + } + catch (Exception ex) + { + if (dataStream != null) + { + dataStream.Dispose(); + } + if (reader != null) + { + reader.Dispose(); + } + throw new NullReferenceException($"Ошибка, не удалось создать объект ReadOnlyStream\nТекст ошибки {ex.Message}"); + } + + return stream; + } + } +} diff --git a/TestTask/IReadOnlyStream.cs b/TestTask/ReadOnlyStream/IReadOnlyStream.cs similarity index 54% rename from TestTask/IReadOnlyStream.cs rename to TestTask/ReadOnlyStream/IReadOnlyStream.cs index 6946745..e615414 100644 --- a/TestTask/IReadOnlyStream.cs +++ b/TestTask/ReadOnlyStream/IReadOnlyStream.cs @@ -1,16 +1,17 @@ -namespace TestTask -{ - /// - /// Интерфейс для работы с файлом в сильно урезаном виде. - /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. - /// - internal interface IReadOnlyStream - { - // TODO : Необходимо доработать данный интерфейс для обеспечения гарантированного закрытия файла, по окончанию работы с таковым! - char ReadNextChar(); - - void ResetPositionToStart(); - - bool IsEof { get; } - } -} +using System; + +namespace TestTask +{ + /// + /// Интерфейс для работы с файлом в сильно урезаном виде. + /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. + /// + internal interface IReadOnlyStream : IDisposable + { + char ReadNextChar(); + + void ResetPositionToStart(); + + bool IsEof { get; } + } +} diff --git a/TestTask/ReadOnlyStream/IReadOnlyStreamFactory.cs b/TestTask/ReadOnlyStream/IReadOnlyStreamFactory.cs new file mode 100644 index 0000000..2e18bd9 --- /dev/null +++ b/TestTask/ReadOnlyStream/IReadOnlyStreamFactory.cs @@ -0,0 +1,10 @@ +namespace TestTask +{ + /// + /// Интерфейс фабрики классов для создания потоков для чтения + /// + internal interface IReadOnlyStreamFactory + { + IReadOnlyStream GetInputStream(string fileFullPath); + } +} diff --git a/TestTask/ReadOnlyStream.cs b/TestTask/ReadOnlyStream/ReadOnlyStream.cs similarity index 58% rename from TestTask/ReadOnlyStream.cs rename to TestTask/ReadOnlyStream/ReadOnlyStream.cs index a51a61e..c970627 100644 --- a/TestTask/ReadOnlyStream.cs +++ b/TestTask/ReadOnlyStream/ReadOnlyStream.cs @@ -1,60 +1,88 @@ -using System; -using System.IO; - -namespace TestTask -{ - public class ReadOnlyStream : IReadOnlyStream - { - private Stream _localStream; - - /// - /// Конструктор класса. - /// Т.к. происходит прямая работа с файлом, необходимо - /// обеспечить ГАРАНТИРОВАННОЕ закрытие файла после окончания работы с таковым! - /// - /// Полный путь до файла для чтения - public ReadOnlyStream(string fileFullPath) - { - IsEof = true; - - // TODO : Заменить на создание реального стрима для чтения файла! - _localStream = null; - } - - /// - /// Флаг окончания файла. - /// - public bool IsEof - { - get; // TODO : Заполнять данный флаг при достижении конца файла/стрима при чтении - private set; - } - - /// - /// Ф-ция чтения следующего символа из потока. - /// Если произведена попытка прочитать символ после достижения конца файла, метод - /// должен бросать соответствующее исключение - /// - /// Считанный символ. - public char ReadNextChar() - { - // TODO : Необходимо считать очередной символ из _localStream - throw new NotImplementedException(); - } - - /// - /// Сбрасывает текущую позицию потока на начало. - /// - public void ResetPositionToStart() - { - if (_localStream == null) - { - IsEof = true; - return; - } - - _localStream.Position = 0; - IsEof = false; - } - } -} +using System.IO; + +namespace TestTask +{ + public class ReadOnlyStream : IReadOnlyStream + { + private Stream _localStream; + private StreamReader _localReader; + + private bool _isEof = false; + + /// + /// Конструктор класса. + /// Т.к. происходит прямая работа с файлом, необходимо + /// обеспечить ГАРАНТИРОВАННОЕ закрытие файла после окончания работы с таковым! + /// + /// Полный путь до файла для чтения + public ReadOnlyStream(Stream localStream, StreamReader localReader) + { + IsEof = false; + + _localStream = localStream; + _localReader = localReader; + } + + /// + /// Флаг окончания файла. + /// + public bool IsEof + { + get => _isEof; + private set => _isEof = value; + } + + /// + /// Ф-ция чтения следующего символа из потока. + /// Если произведена попытка прочитать символ после достижения конца файла, метод + /// должен бросать соответствующее исключение + /// + /// Считанный символ. + public char ReadNextChar() + { + if (IsEof) + { + throw new EndOfStreamException("Попытка чтения файла после достижения границы"); + } + + int symbol = _localReader.Read(); + if (symbol == -1) + { + IsEof = true; + } + + return (char)symbol; + } + + /// + /// Сбрасывает текущую позицию потока на начало. + /// + public void ResetPositionToStart() + { + if (_localStream == null) + { + IsEof = true; + return; + } + + _localStream.Position = 0; + _localReader.DiscardBufferedData(); + IsEof = false; + } + + /// + /// Закрывает потоки для работы с файлом + /// + public void Dispose() + { + if (_localReader != null) + { + _localReader.Close(); + } + if (_localStream != null) + { + _localStream.Close(); + } + } + } +} diff --git a/TestTask/TestTask.csproj b/TestTask/TestTask.csproj index dc470b0..1471f81 100644 --- a/TestTask/TestTask.csproj +++ b/TestTask/TestTask.csproj @@ -11,6 +11,21 @@ v4.6.1 512 true + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true AnyCPU @@ -31,6 +46,10 @@ prompt 4 + + + + @@ -42,12 +61,27 @@ - - - + + + + + + + + - + + + + + + + + False + .NET Framework 3.5 SP1 + false + \ No newline at end of file diff --git a/TestTask/TestTask.sln b/TestTask/TestTask.sln index 494bc7e..94e34f5 100644 --- a/TestTask/TestTask.sln +++ b/TestTask/TestTask.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2011 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36623.8 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestTask", "TestTask.csproj", "{6E9922F2-AF75-4807-B6E4-F74499317C39}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestTaskTest", "..\TestTaskTest\TestTaskTest.csproj", "{B17352FD-EE61-442A-BD62-3ECBDB910806}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {6E9922F2-AF75-4807-B6E4-F74499317C39}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E9922F2-AF75-4807-B6E4-F74499317C39}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E9922F2-AF75-4807-B6E4-F74499317C39}.Release|Any CPU.Build.0 = Release|Any CPU + {B17352FD-EE61-442A-BD62-3ECBDB910806}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B17352FD-EE61-442A-BD62-3ECBDB910806}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B17352FD-EE61-442A-BD62-3ECBDB910806}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B17352FD-EE61-442A-BD62-3ECBDB910806}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TestTask/Utilities/ConsoleHelperMethods.cs b/TestTask/Utilities/ConsoleHelperMethods.cs new file mode 100644 index 0000000..3def127 --- /dev/null +++ b/TestTask/Utilities/ConsoleHelperMethods.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TestTask +{ + /// + /// Методы для работы с вводом/выводом + /// + internal class ConsoleHelperMethods + { + /// + /// Ф-ция выводит на экран полученную статистику в формате "{Буква} : {Кол-во}" + /// Каждая буква - с новой строки. + /// Выводить на экран необходимо предварительно отсортировав набор по алфавиту. + /// В конце отдельная строчка с ИТОГО, содержащая в себе общее кол-во найденных букв/пар + /// + /// Коллекция со статистикой + public static void PrintStatistic(IEnumerable letters, int columnLength = 15) + { + IEnumerable sortedLetters = letters.OrderBy(x => x.Letter); + + Console.WriteLine($"|{new string('-', columnLength)}|{new string('-', columnLength)}|"); + Console.WriteLine($"|{"Буква".PadRight(columnLength)}|{"Кол-во".PadRight(columnLength)}|"); + foreach (LetterStats letterData in sortedLetters) + { + Console.WriteLine($"|{new string('-', columnLength)}|{new string('-', columnLength)}|"); + string letterText = letterData.Letter; + string countText = letterData.Count.ToString(); + + Console.WriteLine($"|{letterText.PadRight(columnLength)}|{countText.PadRight(columnLength)}|"); + } + + Console.WriteLine($"|{new string('-', columnLength)}|{new string('-', columnLength)}|"); + } + } +} diff --git a/TestTaskTest/Fakes/ArrayReadOnlyStream.cs b/TestTaskTest/Fakes/ArrayReadOnlyStream.cs new file mode 100644 index 0000000..686d0a2 --- /dev/null +++ b/TestTaskTest/Fakes/ArrayReadOnlyStream.cs @@ -0,0 +1,54 @@ +using System.IO; +using TestTask; + +namespace TestTaskTest +{ + /// + /// Симулирует чтение из файла с помощью массива + /// + public class ArrayReadOnlyStream : IReadOnlyStream + { + private char[] _text; + private bool _isEOF = false; + private int _position = 0; + + public bool IsEof + { + get => _isEOF; + private set => _isEOF = value; + } + + public ArrayReadOnlyStream(char[] text) + { + _text = text; + _position = 0; + IsEof = _position == _text.Length; + } + + public char ReadNextChar() + { + if (IsEof) + { + throw new EndOfStreamException("Попытка чтения файла после достижения границы"); + } + + char symbol = _text[_position++]; + if (_position == _text.Length) + { + _isEOF = true; + } + + return symbol; + } + + public void ResetPositionToStart() + { + _position = 0; + } + + public void Dispose() + { + + } + } +} diff --git a/TestTaskTest/TestTaskTest.csproj b/TestTaskTest/TestTaskTest.csproj new file mode 100644 index 0000000..c0c2ec5 --- /dev/null +++ b/TestTaskTest/TestTaskTest.csproj @@ -0,0 +1,22 @@ + + + + net461 + + false + true + + + + + + + + + + + + + + + diff --git a/TestTaskTest/Tests/BaseReadOnlyStreamFactoryTests.cs b/TestTaskTest/Tests/BaseReadOnlyStreamFactoryTests.cs new file mode 100644 index 0000000..55cfe01 --- /dev/null +++ b/TestTaskTest/Tests/BaseReadOnlyStreamFactoryTests.cs @@ -0,0 +1,86 @@ +using NUnit.Framework; +using TestTask; +using System.Collections.Generic; +using System; +using System.IO; + +namespace TestTaskTest +{ + /// + /// Тесты для тестирования проверки фабрики обертки над потоками + /// + public class BaseReadOnlyStreamFactoryTests + { + private BaseReadOnlyStreamFactory _factory = new BaseReadOnlyStreamFactory(); + private string createdFileName = "./temp.txt"; + + [OneTimeTearDown] + public void TearDown() + { + if (File.Exists(createdFileName)) + { + File.Delete(createdFileName); + } + } + + [Test] + public void GetInputStream_WhiteSpacePath_Throws() + { + // ARRANGE + string fileName = " "; + + // ACT + + // ASERT + Assert.Throws(() => _factory.GetInputStream(fileName)); + } + + [Test] + public void GetInputStream_NonExistingFile_Throws() + { + // ARRANGE + string fileName = "./a.txt"; + + if (File.Exists(fileName)) + { + Assert.Ignore("Для запуска теста необходимо убрать файл a.txt из папки проекта"); + } + // ACT + + // ASERT + Assert.Throws(() => _factory.GetInputStream(fileName)); + } + + [Test] + public void GetInputStream_ExistingFile_CreatesInstance() + { + // ARRANGE + try + { + FileStream test = File.Create(createdFileName); + test.Close(); + } + catch + { + Assert.Ignore($"Не удалось создать файл {createdFileName}, проверьте среду выполнения!"); + } + + // ACT + IReadOnlyStream instance = _factory.GetInputStream(createdFileName); + + // ASERT + try + { + Assert.That(instance != null); + } + catch + { + throw; + } + finally + { + instance.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/TestTaskTest/Tests/LetterAnalysisFacadeTests.cs b/TestTaskTest/Tests/LetterAnalysisFacadeTests.cs new file mode 100644 index 0000000..e4882e9 --- /dev/null +++ b/TestTaskTest/Tests/LetterAnalysisFacadeTests.cs @@ -0,0 +1,235 @@ +using NUnit.Framework; +using TestTask; +using System.Collections.Generic; + +namespace TestTaskTest +{ + + /// + /// Тесты для проверки алгоритмов анализа файлов + /// + public class LetterAnalysisFacadeTests + { + private LetterAnalysisFacade _facade = new LetterAnalysisFacade(); + + [Test] + public void FillSingleLetterStats_1_IgnoreNonLetters() + { + // ARRANGE + char[] testData = "1".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillSingleLetterStats(fake); + + // ASERT + Assert.That(result.Count == 0); + } + + [Test] + public void FillSingleLetterStats_1Letter_Have1Value() + { + // ARRANGE + char[] testData = "A".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillSingleLetterStats(fake); + + // ASERT + Assert.That(result.Count == 1); + } + + [Test] + public void FillSingleLetterStats_1LetterA_ResultCount1() + { + // ARRANGE + char[] testData = "A".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillSingleLetterStats(fake); + + // ASERT + Assert.That(result[0].Count == 1); + } + + [Test] + public void FillSingleLetterStats_aabbb_returns2a3b() + { + // ARRANGE + char[] testData = "aabbb".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillSingleLetterStats(fake); + + // ASERT + Assert.That(result.Count == 2); + LetterStats firstElement = result[0]; + + if (firstElement.Letter == "a") + { + Assert.That(firstElement.Count == 2); + } + else + { + Assert.That(firstElement.Letter == "b"); + Assert.That(firstElement.Count == 3); + } + + LetterStats secondElement = result[1]; + if (firstElement.Letter == "a") + { + Assert.That(secondElement.Letter == "b"); + Assert.That(secondElement.Count == 3); + } + else + { + Assert.That(secondElement.Letter == "a"); + Assert.That(secondElement.Count == 2); + } + } + + [Test] + public void FillDoubleLetterStats_1_IgnoreNonLetters() + { + // ARRANGE + char[] testData = "1".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillDoubleLetterStats(fake); + + // ASERT + Assert.That(result.Count == 0); + } + + [Test] + public void FillDoubleLetterStats_2DifferentLetters_NotCount() + { + // ARRANGE + char[] testData = "AB".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillDoubleLetterStats(fake); + + // ASERT + Assert.That(result.Count == 0); + } + + [Test] + public void FillDoubleLetterStats_2SameLetterA_ResultCount1() + { + // ARRANGE + char[] testData = "AA".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillDoubleLetterStats(fake); + + // ASERT + Assert.That(result[0].Count == 1); + } + + [Test] + public void FillDoubleLetterStats_aabbb_returns1AA2BB() + { + // ARRANGE + char[] testData = "aabbb".ToCharArray(); + + IReadOnlyStream fake = new ArrayReadOnlyStream(testData); + + // ACT + IList result = _facade.FillDoubleLetterStats(fake); + + // ASERT + Assert.That(result.Count == 2); + LetterStats firstElement = result[0]; + + if (firstElement.Letter == "AA") + { + Assert.That(firstElement.Count == 1); + } + else + { + Assert.That(firstElement.Letter == "BB"); + Assert.That(firstElement.Count == 2); + } + + LetterStats secondElement = result[1]; + if (firstElement.Letter == "AA") + { + Assert.That(secondElement.Letter == "BB"); + Assert.That(secondElement.Count == 2); + } + else + { + Assert.That(secondElement.Letter == "AA"); + Assert.That(secondElement.Count == 1); + } + } + + [Test] + public void RemoveCharStatsByType_a1b1CharTypeVowel_b1Returns() + { + // ARRANGE + IList stats = new List + { + new LetterStats + { + Letter = "a", + Count = 1 + }, + new LetterStats + { + Letter = "b", + Count = 1 + } + }; + + // ACT + IList result = _facade.RemoveCharStatsByType(stats, CharType.Vowel); + + // ASSERT + Assert.That(result.Count == 1); + Assert.That(result[0].Letter == "b"); + Assert.That(result[0].Count == 1); + } + + [Test] + public void RemoveCharStatsByType_a1b1CharTypeConsonant_a1Returns() + { + // ARRANGE + IList stats = new List + { + new LetterStats + { + Letter = "a", + Count = 1 + }, + new LetterStats + { + Letter = "b", + Count = 1 + } + }; + + // ACT + IList result = _facade.RemoveCharStatsByType(stats, CharType.Consonants); + + // ASSERT + Assert.That(result.Count == 1); + Assert.That(result[0].Letter == "a"); + Assert.That(result[0].Count == 1); + } + } +} \ No newline at end of file