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;
+ }
}
}