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, чтобы не мучаться с кодировкой и с проблемами, когда, например, файл повреждён.
+
+
+
+Очистку стримов решил реализовать через IDisposable интерфейс и вызов Close у обоих потоков.
+
+### Реализация логики анализа статистики вхождений.
+Для реализации хранилища статистики создал специальный интерфейс, подразумевающий разные реализации.
+
+
+
+Для подсчёта количества букв использовал реализацию со словарём, так как у него низкое время поиска элементов + у нас может быть не такое большое количество разных элементов, чтобы беспокоиться о коллизиях.
+
+
+
+**Есть сомнения насчёт логики подсчёта элементов, реализовал её, как просто подсчёт количества подряд идущих повторяющихся букв. Например, для файла с текстом "нНн" результатом будет 2.**
+
+### Работа с консолью.
+
+Сделал красивый вывод с помощью таблицы и с использованием PadRight.
+
+
+
+### Рефакторинг.
+
+Исходные методы имели модификатор private static, тем самым исключая любую возможность их переиспользования в других программах. Также создание объектов внутри конструктора является не очень хорошей практикой, поэтому метод создания ReadOnlyStream был переенсён в отдельную фабрику.
+
+По итогу были совершены следующие изменения:
+1. Вынесены методы, занимающиеся анализом файла через IReadOnlyStream в отдельный LetterAnalysisFacade. Почему не в ReadOnlyStream? Потому что данные методы узконаправлены и выходят за текущий интерефейс.
+2. Метод GetInputStream(string) вынесен в отдельную фабрику IReadOnlyStreamFactory с инъекцией зависимостей.
+3. Метод PrintStatistics(IList\) вынесен в метод ConsoleHelperMethods. Почему сделан static? Потому что метод имеет чёткую привязку к среде выполнения и не меняет внутреннее состояние каких-либо объектов.
+
+
+
+### Диаграмма классов.
+
+Была составлена дигарамма классов для удобства других разработчиков.
+
+
+
+### Unit тесты.
+
+Для создания Unit тестов был создан дополнительный проект TestTaskTest. Для Unit тестирования был выбран фреймворк NUnit, потому что только с этим фреймворком я знаком достаточно, чтобы использовать его в production.
+
+В результате были реализованы тесты для основных алгоритмов в проекте.
+
+
\ 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