diff --git a/TestTask/IReadOnlyStream.cs b/TestTask/IReadOnlyStream.cs index 6946745..8e0a9b0 100644 --- a/TestTask/IReadOnlyStream.cs +++ b/TestTask/IReadOnlyStream.cs @@ -1,11 +1,14 @@ -namespace TestTask +using System; + +namespace TestTask { /// /// Интерфейс для работы с файлом в сильно урезаном виде. /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. /// - internal interface IReadOnlyStream + internal interface IReadOnlyStream : IDisposable { + // Интерфейс пополнили IDisposable, чтобы клиенты могли гарантированно закрывать файл после окончания работы. // TODO : Необходимо доработать данный интерфейс для обеспечения гарантированного закрытия файла, по окончанию работы с таковым! char ReadNextChar(); diff --git a/TestTask/Program.cs b/TestTask/Program.cs index fdf048e..092d92d 100644 --- a/TestTask/Program.cs +++ b/TestTask/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; namespace TestTask { @@ -16,19 +17,29 @@ public class Program /// Второй параметр - путь до второго файла. static void Main(string[] args) { - IReadOnlyStream inputStream1 = GetInputStream(args[0]); - IReadOnlyStream inputStream2 = GetInputStream(args[1]); + // если пути не были переданы, запрашиваем у пользователя + if (args.Length < 2) + { + Console.WriteLine("Usage: TestTask "); + return; + } - IList singleLetterStats = FillSingleLetterStats(inputStream1); - IList doubleLetterStats = FillDoubleLetterStats(inputStream2); + using (IReadOnlyStream inputStream1 = GetInputStream(args[0])) + using (IReadOnlyStream inputStream2 = GetInputStream(args[1])) + { + IList singleLetterStats = FillSingleLetterStats(inputStream1); + IList doubleLetterStats = FillDoubleLetterStats(inputStream2); - RemoveCharStatsByType(singleLetterStats, CharType.Vowel); - RemoveCharStatsByType(doubleLetterStats, CharType.Consonants); + RemoveCharStatsByType(singleLetterStats, CharType.Vowel); + RemoveCharStatsByType(doubleLetterStats, CharType.Consonants); - PrintStatistic(singleLetterStats); - PrintStatistic(doubleLetterStats); + PrintStatistic(singleLetterStats); + PrintStatistic(doubleLetterStats); + } - // TODO : Необжодимо дождаться нажатия клавиши, прежде чем завершать выполнение программы. + // дождаться нажатия любой клавиши перед закрытием + Console.WriteLine("Нажмите любую клавишу для выхода..."); + Console.ReadKey(true); } /// @@ -49,16 +60,39 @@ private static IReadOnlyStream GetInputStream(string fileFullPath) /// Коллекция статистик по каждой букве, что была прочитана из стрима. private static IList FillSingleLetterStats(IReadOnlyStream stream) { + var stats = new Dictionary(); + stream.ResetPositionToStart(); - while (!stream.IsEof) + while (true) { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - регистрозависимый. - } + if (stream.IsEof) + break; - //return ???; + char c; + try + { + c = stream.ReadNextChar(); + } + catch (EndOfStreamException) + { + // достигли конца - просто прерываем цикл + break; + } + + if (!char.IsLetter(c)) + continue; + + string key = c.ToString(); + if (!stats.TryGetValue(key, out LetterStats letterStats)) + { + letterStats = new LetterStats { Letter = key, Count = 0 }; + } - throw new NotImplementedException(); + IncStatistic(ref letterStats); // регистрозависимо используем сам символ + stats[key] = letterStats; + } + + return new List(stats.Values); } /// @@ -70,16 +104,52 @@ private static IList FillSingleLetterStats(IReadOnlyStream stream) /// Коллекция статистик по каждой букве, что была прочитана из стрима. private static IList FillDoubleLetterStats(IReadOnlyStream stream) { + // используем словарь с игнорированием регистра для ключей + var stats = new Dictionary(StringComparer.OrdinalIgnoreCase); + stream.ResetPositionToStart(); - while (!stream.IsEof) + char? prevChar = null; + while (true) { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - НЕ регистрозависимый. - } + if (stream.IsEof) + break; - //return ???; + char c; + try + { + c = stream.ReadNextChar(); + } + catch (EndOfStreamException) + { + break; + } + + if (!char.IsLetter(c)) + { + prevChar = null; + continue; + } + + if (prevChar.HasValue) + { + // сравниваем без учёта регистра + if (char.ToUpperInvariant(prevChar.Value) == char.ToUpperInvariant(c)) + { + string key = new string(new[] { char.ToUpperInvariant(c), char.ToUpperInvariant(c) }); + if (!stats.TryGetValue(key, out LetterStats letterStats)) + { + letterStats = new LetterStats { Letter = key, Count = 0 }; + } + + IncStatistic(ref letterStats); + stats[key] = letterStats; + } + } + + prevChar = c; + } - throw new NotImplementedException(); + return new List(stats.Values); } /// @@ -91,15 +161,36 @@ private static IList FillDoubleLetterStats(IReadOnlyStream stream) /// Тип букв для анализа private static void RemoveCharStatsByType(IList letters, CharType charType) { - // TODO : Удалить статистику по запрошенному типу букв. - switch (charType) + // Удаляем элементы из списка по префиксу соответствующему типу + bool IsVowelChar(char ch) { - case CharType.Consonants: - break; - case CharType.Vowel: - break; + // базовый набор русских гласных (и их латинские аналоги, если встретятся) + const string vowels = "AEIOUYАЕЁИОУЫЭЮЯaeiouyаеёиоуыэюя"; + return vowels.IndexOf(ch) >= 0; + } + + for (int i = letters.Count - 1; i >= 0; i--) + { + string str = letters[i].Letter; + if (string.IsNullOrEmpty(str)) + continue; + + bool allVowels = true; + bool allConsonants = true; + foreach (char ch in str) + { + if (!char.IsLetter(ch)) + continue; // игнорируем несравнимые символы + if (IsVowelChar(ch)) + allConsonants = false; + else + allVowels = false; + } + + bool remove = charType == CharType.Vowel ? allVowels : allConsonants; + if (remove) + letters.RemoveAt(i); } - } /// @@ -111,15 +202,26 @@ private static void RemoveCharStatsByType(IList letters, CharType c /// Коллекция со статистикой private static void PrintStatistic(IEnumerable letters) { - // TODO : Выводить на экран статистику. Выводить предварительно отсортировав по алфавиту! - throw new NotImplementedException(); + var sorted = new List(letters); + sorted.Sort((a, b) => string.CompareOrdinal(a.Letter, b.Letter)); + + int total = 0; + foreach (var ls in sorted) + { + Console.WriteLine($"{ls.Letter} : {ls.Count}"); + total += ls.Count; + } + + Console.WriteLine($"ИТОГО : {total}"); } /// /// Метод увеличивает счётчик вхождений по переданной структуре. + /// Принципиально принимаем структуру по ссылке, чтобы изменения + /// увидел вызывающий код (struct - value type). /// /// - private static void IncStatistic(LetterStats letterStats) + private static void IncStatistic(ref LetterStats letterStats) { letterStats.Count++; } diff --git a/TestTask/ReadOnlyStream.cs b/TestTask/ReadOnlyStream.cs index a51a61e..6f36011 100644 --- a/TestTask/ReadOnlyStream.cs +++ b/TestTask/ReadOnlyStream.cs @@ -5,7 +5,8 @@ namespace TestTask { public class ReadOnlyStream : IReadOnlyStream { - private Stream _localStream; + // использовали StreamReader потому, что оно корректно работает с `char` и кодировкой + private StreamReader _localStream; /// /// Конструктор класса. @@ -15,10 +16,9 @@ public class ReadOnlyStream : IReadOnlyStream /// Полный путь до файла для чтения public ReadOnlyStream(string fileFullPath) { - IsEof = true; - - // TODO : Заменить на создание реального стрима для чтения файла! - _localStream = null; + // при создании открываем файл на чтение + _localStream = new StreamReader(fileFullPath); + IsEof = false; } /// @@ -26,7 +26,7 @@ public ReadOnlyStream(string fileFullPath) /// public bool IsEof { - get; // TODO : Заполнять данный флаг при достижении конца файла/стрима при чтении + get; private set; } @@ -38,8 +38,19 @@ public bool IsEof /// Считанный символ. public char ReadNextChar() { - // TODO : Необходимо считать очередной символ из _localStream - throw new NotImplementedException(); + if (_localStream == null) + { + throw new ObjectDisposedException(nameof(ReadOnlyStream)); + } + + int result = _localStream.Read(); + if (result == -1) + { + IsEof = true; + throw new EndOfStreamException("Attempted to read past end of stream"); + } + + return (char)result; } /// @@ -53,8 +64,19 @@ public void ResetPositionToStart() return; } - _localStream.Position = 0; + _localStream.BaseStream.Seek(0, SeekOrigin.Begin); + _localStream.DiscardBufferedData(); IsEof = false; } + + /// + /// Закрывает внутренний поток. + /// + public void Dispose() + { + _localStream?.Dispose(); + _localStream = null; + IsEof = true; + } } }