From 3feb8c2fed53070bcb26674d14f84657a37f41db Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Thu, 26 Feb 2026 14:31:42 +0700 Subject: [PATCH 01/10] update project files --- TestTask/Properties/AssemblyInfo.cs | 36 ---------------- TestTask/TestTask.csproj | 66 ++++++----------------------- 2 files changed, 14 insertions(+), 88 deletions(-) delete mode 100644 TestTask/Properties/AssemblyInfo.cs diff --git a/TestTask/Properties/AssemblyInfo.cs b/TestTask/Properties/AssemblyInfo.cs deleted file mode 100644 index aecb854..0000000 --- a/TestTask/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("TestTask")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TestTask")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// 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 -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("6e9922f2-af75-4807-b6e4-f74499317c39")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TestTask/TestTask.csproj b/TestTask/TestTask.csproj index dc470b0..05a0b26 100644 --- a/TestTask/TestTask.csproj +++ b/TestTask/TestTask.csproj @@ -1,53 +1,15 @@ - - - - - Debug - AnyCPU - {6E9922F2-AF75-4807-B6E4-F74499317C39} - Exe - TestTask - TestTask - v4.6.1 - 512 - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - + + + + Exe + net8.0 + 10 + enable + disable + true + true + + + + \ No newline at end of file From c425cd0f4ac6ab43ee7003ae026b73ed89161384 Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 13:09:27 +0700 Subject: [PATCH 02/10] update stream reader --- TestTask/IReadOnlyStream.cs | 3 +-- TestTask/ReadOnlyStream.cs | 42 +++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/TestTask/IReadOnlyStream.cs b/TestTask/IReadOnlyStream.cs index 6946745..ae7cfe2 100644 --- a/TestTask/IReadOnlyStream.cs +++ b/TestTask/IReadOnlyStream.cs @@ -4,9 +4,8 @@ /// Интерфейс для работы с файлом в сильно урезаном виде. /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. /// - internal interface IReadOnlyStream + internal interface IReadOnlyStream : IDisposable { - // TODO : Необходимо доработать данный интерфейс для обеспечения гарантированного закрытия файла, по окончанию работы с таковым! char ReadNextChar(); void ResetPositionToStart(); diff --git a/TestTask/ReadOnlyStream.cs b/TestTask/ReadOnlyStream.cs index a51a61e..9175864 100644 --- a/TestTask/ReadOnlyStream.cs +++ b/TestTask/ReadOnlyStream.cs @@ -1,34 +1,29 @@ using System; using System.IO; +using System.Text; namespace TestTask { public class ReadOnlyStream : IReadOnlyStream { - private Stream _localStream; + private StreamReader _localStream; /// /// Конструктор класса. - /// Т.к. происходит прямая работа с файлом, необходимо - /// обеспечить ГАРАНТИРОВАННОЕ закрытие файла после окончания работы с таковым! /// /// Полный путь до файла для чтения public ReadOnlyStream(string fileFullPath) { - IsEof = true; - - // TODO : Заменить на создание реального стрима для чтения файла! - _localStream = null; + var fileStream = File.Open(fileFullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + _localStream = new StreamReader(fileStream, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + if(_localStream == null) + throw new EndOfStreamException(); } - + /// /// Флаг окончания файла. /// - public bool IsEof - { - get; // TODO : Заполнять данный флаг при достижении конца файла/стрима при чтении - private set; - } + public bool IsEof => _localStream == null || _localStream.EndOfStream; /// /// Ф-ция чтения следующего символа из потока. @@ -38,8 +33,14 @@ public bool IsEof /// Считанный символ. public char ReadNextChar() { - // TODO : Необходимо считать очередной символ из _localStream - throw new NotImplementedException(); + try + { + return (char)_localStream.Read(); + } + catch (EndOfStreamException e) + { + throw new EndOfStreamException(e.Message); + } } /// @@ -49,12 +50,17 @@ public void ResetPositionToStart() { if (_localStream == null) { - IsEof = true; return; } - _localStream.Position = 0; - IsEof = false; + _localStream.DiscardBufferedData(); + _localStream.BaseStream.Position = 0; + } + + public void Dispose() + { + _localStream?.Dispose(); + _localStream = null; } } } From 56bde84a427baf61f0d831959c43d711102ba32d Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 13:11:22 +0700 Subject: [PATCH 03/10] add letter classification --- TestTask/CharCompareExtension.cs | 12 ++++++++++++ TestTask/ConsonantVowelClassificator.cs | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 TestTask/CharCompareExtension.cs create mode 100644 TestTask/ConsonantVowelClassificator.cs diff --git a/TestTask/CharCompareExtension.cs b/TestTask/CharCompareExtension.cs new file mode 100644 index 0000000..27b2427 --- /dev/null +++ b/TestTask/CharCompareExtension.cs @@ -0,0 +1,12 @@ +namespace TestTask; + +public static class CharInvariantCompareExtension +{ + public static bool EqualsSensitiveCase(this char c1, char c2, bool ignoreCase = false) + { + if (ignoreCase) + return char.ToLowerInvariant(c1) == char.ToLowerInvariant(c2); + + return c1 == c2; + } +} \ No newline at end of file diff --git a/TestTask/ConsonantVowelClassificator.cs b/TestTask/ConsonantVowelClassificator.cs new file mode 100644 index 0000000..fd4513a --- /dev/null +++ b/TestTask/ConsonantVowelClassificator.cs @@ -0,0 +1,21 @@ +namespace TestTask; + +public static class ConsonantVowelClassificator +{ + private static readonly HashSet Vowels = new HashSet("аеёиоуыьъэюяАЕЁИОУЫЬЪЭЮЯaeiouAEIOU"); + private static readonly HashSet NonLetterChars = new HashSet("ьъЬЪ"); + + public static bool IsVowel(char c) + { + if (!char.IsLetter(c)) + return false; + if (NonLetterChars.Contains(c)) + return false; + return Vowels.Contains(c); + } + + public static bool IsConsonant(char c) + { + return !IsVowel(c); + } +} \ No newline at end of file From 20a832968abd4a7722b6ae745138eb96cdb520de Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 13:11:54 +0700 Subject: [PATCH 04/10] add letter statistics --- TestTask/LetterStatisticsPrinter.cs | 15 ++++++ TestTask/Program.cs | 76 ++++++++++++++++++----------- TestTask/StatisticRemover.cs | 37 ++++++++++++++ 3 files changed, 99 insertions(+), 29 deletions(-) create mode 100644 TestTask/LetterStatisticsPrinter.cs create mode 100644 TestTask/StatisticRemover.cs diff --git a/TestTask/LetterStatisticsPrinter.cs b/TestTask/LetterStatisticsPrinter.cs new file mode 100644 index 0000000..47f9fe9 --- /dev/null +++ b/TestTask/LetterStatisticsPrinter.cs @@ -0,0 +1,15 @@ +namespace TestTask; + +public static class LetterStatisticsPrinter +{ + public static void PrintStatisticSorted(IList letters) + { + var sorted = letters.OrderBy(x => char.ToLowerInvariant(x.Letter[0])); + foreach (var letter in sorted) + { + Console.WriteLine($"{letter.Letter} : {letter.Count}"); + } + + Console.WriteLine($"ИТОГО : {letters.Count()}"); + } +} \ No newline at end of file diff --git a/TestTask/Program.cs b/TestTask/Program.cs index fdf048e..50297be 100644 --- a/TestTask/Program.cs +++ b/TestTask/Program.cs @@ -16,8 +16,8 @@ public class Program /// Второй параметр - путь до второго файла. static void Main(string[] args) { - IReadOnlyStream inputStream1 = GetInputStream(args[0]); - IReadOnlyStream inputStream2 = GetInputStream(args[1]); + using IReadOnlyStream inputStream1 = GetInputStream(args[0]); + using IReadOnlyStream inputStream2 = GetInputStream(args[1]); IList singleLetterStats = FillSingleLetterStats(inputStream1); IList doubleLetterStats = FillDoubleLetterStats(inputStream2); @@ -28,7 +28,7 @@ static void Main(string[] args) PrintStatistic(singleLetterStats); PrintStatistic(doubleLetterStats); - // TODO : Необжодимо дождаться нажатия клавиши, прежде чем завершать выполнение программы. + Console.ReadKey(); } /// @@ -50,20 +50,29 @@ private static IReadOnlyStream GetInputStream(string fileFullPath) private static IList FillSingleLetterStats(IReadOnlyStream stream) { stream.ResetPositionToStart(); + var stats = new Dictionary(); while (!stream.IsEof) { char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - регистрозависимый. + + if(!char.IsLetter(c)) + continue; + + stats.TryAdd(c, 0); + stats[c]++; } - //return ???; - - throw new NotImplementedException(); + var result = new List(); + foreach (var stat in stats) + { + result.Add(new LetterStats(){Letter = stat.Key.ToString(), Count = stat.Value}); + } + return result; } /// /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. - /// В статистику должны попадать только пары из одинаковых букв, например АА, СС, УУ, ЕЕ и т.д. + /// В статистику должны попадать только пары из одинаковых букв, например АА, cС, УУ, ee и т.д. /// Статистика - НЕ регистрозависимая! /// /// Стрим для считывания символов для последующего анализа @@ -71,15 +80,33 @@ private static IList FillSingleLetterStats(IReadOnlyStream stream) private static IList FillDoubleLetterStats(IReadOnlyStream stream) { stream.ResetPositionToStart(); + var stats = new Dictionary(); + if(stream.IsEof) + return new List(); + + char prevChar = stream.ReadNextChar(); + while (!stream.IsEof) { char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - НЕ регистрозависимый. + if (!prevChar.EqualsSensitiveCase(c, true)) + { + prevChar = c; + continue; + } + + var combination = $"{prevChar}{c}"; + stats.TryAdd(combination, 0); + stats[combination]++; + prevChar = c; } - //return ???; - - throw new NotImplementedException(); + var result = new List(); + foreach (var stat in stats) + { + result.Add(new LetterStats(){Letter = stat.Key, Count = stat.Value}); + } + return result; } /// @@ -91,39 +118,30 @@ private static IList FillDoubleLetterStats(IReadOnlyStream stream) /// Тип букв для анализа private static void RemoveCharStatsByType(IList letters, CharType charType) { - // TODO : Удалить статистику по запрошенному типу букв. switch (charType) { case CharType.Consonants: + letters.TrimConsonant(); break; case CharType.Vowel: + letters.TrimVowels(); + break; + default: break; } - } /// - /// Ф-ция выводит на экран полученную статистику в формате "{Буква} : {Кол-во}" - /// Каждая буква - с новой строки. + /// Ф-ция выводит на экран полученную статистику в формате "{Буква/пара} : {Кол-во}" + /// Каждая буква/пара - с новой строки. /// Выводить на экран необходимо предварительно отсортировав набор по алфавиту. /// В конце отдельная строчка с ИТОГО, содержащая в себе общее кол-во найденных букв/пар /// /// Коллекция со статистикой - private static void PrintStatistic(IEnumerable letters) + private static void PrintStatistic(IList letters) { - // TODO : Выводить на экран статистику. Выводить предварительно отсортировав по алфавиту! - throw new NotImplementedException(); + LetterStatisticsPrinter.PrintStatisticSorted(letters); } - /// - /// Метод увеличивает счётчик вхождений по переданной структуре. - /// - /// - private static void IncStatistic(LetterStats letterStats) - { - letterStats.Count++; - } - - } } diff --git a/TestTask/StatisticRemover.cs b/TestTask/StatisticRemover.cs new file mode 100644 index 0000000..3e20874 --- /dev/null +++ b/TestTask/StatisticRemover.cs @@ -0,0 +1,37 @@ +namespace TestTask; + +public static class LetterStatsExtension +{ + public static void TrimConsonant(this IList letterStats) + { + TrimByPredicate(letterStats, IsConsonant); + } + + public static void TrimVowels(this IList letterStats) + { + TrimByPredicate(letterStats, IsVowel); + } + + private static void TrimByPredicate(IList letterStats, Func predicate) + { + for(int i = 0; i < letterStats.Count; i++) + { + if(!predicate(letterStats[i].Letter)) + { + continue; + } + letterStats.Remove(letterStats[i]); + i--; + } + } + + private static bool IsConsonant(string letter) + { + return string.IsNullOrEmpty(letter) || ConsonantVowelClassificator.IsConsonant(letter[0]); + } + + private static bool IsVowel(string letter) + { + return string.IsNullOrEmpty(letter) || ConsonantVowelClassificator.IsVowel(letter[0]); + } +} \ No newline at end of file From 1571be44279ca8c14487a10a90b82f20326b0036 Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 16:00:57 +0700 Subject: [PATCH 05/10] comments, updates --- TestTask/CharCompareExtension.cs | 12 --- TestTask/ConsonantVowelClassificator.cs | 21 ----- .../CharInvariantCompareExtension.cs | 28 +++++++ .../Extensions/ConsonantVowelClassificator.cs | 36 ++++++++ TestTask/{ => Interfaces}/IReadOnlyStream.cs | 30 +++---- TestTask/LetterStatisticsPrinter.cs | 15 ---- TestTask/LetterStats.cs | 18 ---- TestTask/{ => Models}/CharType.cs | 36 ++++---- TestTask/Models/LetterStats.cs | 7 ++ .../Services/LetterStatisticsPrintHelper.cs | 26 ++++++ TestTask/Services/LetterStatsService.cs | 82 +++++++++++++++++++ 11 files changed, 212 insertions(+), 99 deletions(-) delete mode 100644 TestTask/CharCompareExtension.cs delete mode 100644 TestTask/ConsonantVowelClassificator.cs create mode 100644 TestTask/Extensions/CharInvariantCompareExtension.cs create mode 100644 TestTask/Extensions/ConsonantVowelClassificator.cs rename TestTask/{ => Interfaces}/IReadOnlyStream.cs (81%) delete mode 100644 TestTask/LetterStatisticsPrinter.cs delete mode 100644 TestTask/LetterStats.cs rename TestTask/{ => Models}/CharType.cs (87%) create mode 100644 TestTask/Models/LetterStats.cs create mode 100644 TestTask/Services/LetterStatisticsPrintHelper.cs create mode 100644 TestTask/Services/LetterStatsService.cs diff --git a/TestTask/CharCompareExtension.cs b/TestTask/CharCompareExtension.cs deleted file mode 100644 index 27b2427..0000000 --- a/TestTask/CharCompareExtension.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TestTask; - -public static class CharInvariantCompareExtension -{ - public static bool EqualsSensitiveCase(this char c1, char c2, bool ignoreCase = false) - { - if (ignoreCase) - return char.ToLowerInvariant(c1) == char.ToLowerInvariant(c2); - - return c1 == c2; - } -} \ No newline at end of file diff --git a/TestTask/ConsonantVowelClassificator.cs b/TestTask/ConsonantVowelClassificator.cs deleted file mode 100644 index fd4513a..0000000 --- a/TestTask/ConsonantVowelClassificator.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace TestTask; - -public static class ConsonantVowelClassificator -{ - private static readonly HashSet Vowels = new HashSet("аеёиоуыьъэюяАЕЁИОУЫЬЪЭЮЯaeiouAEIOU"); - private static readonly HashSet NonLetterChars = new HashSet("ьъЬЪ"); - - public static bool IsVowel(char c) - { - if (!char.IsLetter(c)) - return false; - if (NonLetterChars.Contains(c)) - return false; - return Vowels.Contains(c); - } - - public static bool IsConsonant(char c) - { - return !IsVowel(c); - } -} \ No newline at end of file diff --git a/TestTask/Extensions/CharInvariantCompareExtension.cs b/TestTask/Extensions/CharInvariantCompareExtension.cs new file mode 100644 index 0000000..a81642c --- /dev/null +++ b/TestTask/Extensions/CharInvariantCompareExtension.cs @@ -0,0 +1,28 @@ +namespace TestTask.Extensions; + +public static class CharInvariantCompareExtension +{ + /// + /// Проверка символов без учета регистра. + /// + /// Символ, относительно которого сравниваем. + /// Символ сравнения. + /// Учитывать ли регистр. + public static bool EqualsWithCaseOption(this char c1, char c2, bool ignoreCase = false) + { + if (ignoreCase) + return c1.ToLowerInvariant().Equals(c2.ToLowerInvariant()); + + return c1.Equals(c2); + } + + /// + /// Приведение к нижнему регистру. + /// + /// Символ приведения. + /// + public static char ToLowerInvariant(this char c) + { + return char.ToLowerInvariant(c); + } +} \ No newline at end of file diff --git a/TestTask/Extensions/ConsonantVowelClassificator.cs b/TestTask/Extensions/ConsonantVowelClassificator.cs new file mode 100644 index 0000000..7b22687 --- /dev/null +++ b/TestTask/Extensions/ConsonantVowelClassificator.cs @@ -0,0 +1,36 @@ +namespace TestTask.Extensions; +/// +/// Классификатор гласных и согласных. Определяет русские и английские буквы. Откидывает мягкий и твердый знаки. +/// +public static class ConsonantVowelClassificator +{ + private static readonly HashSet Vowels = new HashSet("аеёиоуыэюяАЕЁИОУЫЭЮЯaeiouAEIOU"); + private static readonly HashSet UncategorizedChars = new HashSet("ьъЬЪ"); + + /// + /// Является ли гласной. + /// + /// Проверяемый символ + /// + public static bool IsVowel(char c) + { + if (!char.IsLetter(c)) + return false; + if (UncategorizedChars.Contains(c)) + return false; + return Vowels.Contains(c); + } + /// + /// Является ли согласной. + /// + /// Проверяемый символ + /// + public static bool IsConsonant(char c) + { + if (!char.IsLetter(c)) + return false; + if (UncategorizedChars.Contains(c)) + return false; + return !Vowels.Contains(c); + } +} \ No newline at end of file diff --git a/TestTask/IReadOnlyStream.cs b/TestTask/Interfaces/IReadOnlyStream.cs similarity index 81% rename from TestTask/IReadOnlyStream.cs rename to TestTask/Interfaces/IReadOnlyStream.cs index ae7cfe2..d70d786 100644 --- a/TestTask/IReadOnlyStream.cs +++ b/TestTask/Interfaces/IReadOnlyStream.cs @@ -1,15 +1,15 @@ -namespace TestTask -{ - /// - /// Интерфейс для работы с файлом в сильно урезаном виде. - /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. - /// - internal interface IReadOnlyStream : IDisposable - { - char ReadNextChar(); - - void ResetPositionToStart(); - - bool IsEof { get; } - } -} +namespace TestTask.Interfaces +{ + /// + /// Интерфейс для работы с файлом в сильно урезаном виде. + /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. + /// + public interface IReadOnlyStream : IDisposable + { + char ReadNextChar(); + + void ResetPositionToStart(); + + bool IsEof { get; } + } +} diff --git a/TestTask/LetterStatisticsPrinter.cs b/TestTask/LetterStatisticsPrinter.cs deleted file mode 100644 index 47f9fe9..0000000 --- a/TestTask/LetterStatisticsPrinter.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace TestTask; - -public static class LetterStatisticsPrinter -{ - public static void PrintStatisticSorted(IList letters) - { - var sorted = letters.OrderBy(x => char.ToLowerInvariant(x.Letter[0])); - foreach (var letter in sorted) - { - Console.WriteLine($"{letter.Letter} : {letter.Count}"); - } - - Console.WriteLine($"ИТОГО : {letters.Count()}"); - } -} \ No newline at end of file diff --git a/TestTask/LetterStats.cs b/TestTask/LetterStats.cs deleted file mode 100644 index aa10728..0000000 --- a/TestTask/LetterStats.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TestTask -{ - /// - /// Статистика вхождения буквы/пары букв - /// - public struct LetterStats - { - /// - /// Буква/Пара букв для учёта статистики. - /// - public string Letter; - - /// - /// Кол-во вхождений буквы/пары. - /// - public int Count; - } -} diff --git a/TestTask/CharType.cs b/TestTask/Models/CharType.cs similarity index 87% rename from TestTask/CharType.cs rename to TestTask/Models/CharType.cs index 899564d..82b3cb3 100644 --- a/TestTask/CharType.cs +++ b/TestTask/Models/CharType.cs @@ -1,18 +1,18 @@ -namespace TestTask -{ - /// - /// Тип букв - /// - public enum CharType - { - /// - /// Гласные - /// - Vowel, - - /// - /// Согласные - /// - Consonants - } -} +namespace TestTask.Models +{ + /// + /// Тип букв + /// + public enum CharType + { + /// + /// Гласные + /// + Vowel, + + /// + /// Согласные + /// + Consonants + } +} diff --git a/TestTask/Models/LetterStats.cs b/TestTask/Models/LetterStats.cs new file mode 100644 index 0000000..69f97f9 --- /dev/null +++ b/TestTask/Models/LetterStats.cs @@ -0,0 +1,7 @@ +namespace TestTask.Models +{ + /// + /// Статистика вхождения буквы/пары букв + /// + public record struct LetterStats (string Letter, int Count = 0); +} diff --git a/TestTask/Services/LetterStatisticsPrintHelper.cs b/TestTask/Services/LetterStatisticsPrintHelper.cs new file mode 100644 index 0000000..b486499 --- /dev/null +++ b/TestTask/Services/LetterStatisticsPrintHelper.cs @@ -0,0 +1,26 @@ +using TestTask.Models; + +namespace TestTask.Services; + +public static class LetterStatisticsPrintHelper +{ + /// + /// Ф-ция выводит на экран полученную статистику в формате "{Буква/пара} : {Кол-во}" + /// Каждая буква/пара - с новой строки. + /// Выводить на экран необходимо предварительно отсортировав набор по алфавиту. + /// В конце отдельная строчка с ИТОГО, содержащая в себе общее кол-во найденных букв/пар + /// + /// Коллекция со статистикой + public static void PrintStatisticSorted(IList letters) + { + var sorted = letters.OrderBy(x => x.Letter.ToLowerInvariant()); + int total = 0; + foreach (var letter in sorted) + { + Console.WriteLine($"{letter.Letter} : {letter.Count}"); + total += letter.Count; + } + + Console.WriteLine($"ИТОГО : {total}"); + } +} \ No newline at end of file diff --git a/TestTask/Services/LetterStatsService.cs b/TestTask/Services/LetterStatsService.cs new file mode 100644 index 0000000..502e4fd --- /dev/null +++ b/TestTask/Services/LetterStatsService.cs @@ -0,0 +1,82 @@ +using TestTask.Extensions; +using TestTask.Interfaces; +using TestTask.Models; + +namespace TestTask.Services; + +/// +/// Сервис для сбора статистики букв из потока. +/// +public static class LetterStatsService +{ + /// + /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения каждой буквы. + /// Статистика РЕГИСТРОЗАВИСИМАЯ! + /// + /// Стрим для считывания символов для последующего анализа + /// Коллекция статистик по каждой букве, что была прочитана из стрима. + public static IList FillSingleLetterStats(IReadOnlyStream stream) + { + stream.ResetPositionToStart(); + var stats = new Dictionary(); + + while (!stream.IsEof) + { + char c = stream.GetNextLetter(); + if (!char.IsLetter(c)) break; + + stats[c] = stats.GetValueOrDefault(c) + 1; + } + + return stats + .Select(s => new LetterStats(s.Key.ToString(), s.Value)) + .ToList(); + } + + /// + /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. + /// В статистику должны попадать только пары из одинаковых букв, например АА, сС, УУ, ee и т.д. + /// Статистика - НЕ регистрозависимая! + /// + /// Стрим для считывания символов для последующего анализа + /// Коллекция статистик по каждой букве, что была прочитана из стрима. + public static IList FillDoubleLetterStats(IReadOnlyStream stream) + { + stream.ResetPositionToStart(); + var stats = new Dictionary(); + + if (stream.IsEof) + return new List(); + + char prevChar = stream.GetNextLetter(); + + if (!char.IsLetter(prevChar)) + return new List(); + + while (!stream.IsEof ) + { + char c = stream.ReadNextChar(); + + if (!char.IsLetter(c)) + { + prevChar = '\0'; + continue; + } + + if (!prevChar.EqualsWithCaseOption(c, true)) + { + prevChar = c; + continue; + } + + var combination = new string(new[] { prevChar.ToLowerInvariant(), c.ToLowerInvariant() }); + + stats[combination] = stats.GetValueOrDefault(combination) + 1; + prevChar = c; + } + + return stats + .Select(s => new LetterStats(s.Key, s.Value)) + .ToList(); + } +} \ No newline at end of file From 59825a5e8f0086f1cfc0d998aea57404d0293a2b Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 16:01:20 +0700 Subject: [PATCH 06/10] next letter extension --- TestTask/Extensions/CharStreamExtension.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 TestTask/Extensions/CharStreamExtension.cs diff --git a/TestTask/Extensions/CharStreamExtension.cs b/TestTask/Extensions/CharStreamExtension.cs new file mode 100644 index 0000000..6aa84f4 --- /dev/null +++ b/TestTask/Extensions/CharStreamExtension.cs @@ -0,0 +1,21 @@ +using TestTask.Interfaces; + +namespace TestTask.Extensions; + +internal static class CharStreamExtension +{ + /// + /// Проходит по потоку и находит первый символ являющийся буквой по Unicode. + /// + /// Поток текста. + /// Буква или символ новой строки(конец потока). + public static char GetNextLetter(this IReadOnlyStream stream) + { + char c = '\0'; + + while (!stream.IsEof && !char.IsLetter(c)) + c = stream.ReadNextChar(); + + return c; + } +} \ No newline at end of file From c86acdbe308141a7cb05257429801e02fc28def2 Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 16:01:39 +0700 Subject: [PATCH 07/10] stream fix --- TestTask/{ => Services}/ReadOnlyStream.cs | 125 ++++++++++------------ 1 file changed, 59 insertions(+), 66 deletions(-) rename TestTask/{ => Services}/ReadOnlyStream.cs (77%) diff --git a/TestTask/ReadOnlyStream.cs b/TestTask/Services/ReadOnlyStream.cs similarity index 77% rename from TestTask/ReadOnlyStream.cs rename to TestTask/Services/ReadOnlyStream.cs index 9175864..568402a 100644 --- a/TestTask/ReadOnlyStream.cs +++ b/TestTask/Services/ReadOnlyStream.cs @@ -1,66 +1,59 @@ -using System; -using System.IO; -using System.Text; - -namespace TestTask -{ - public class ReadOnlyStream : IReadOnlyStream - { - private StreamReader _localStream; - - /// - /// Конструктор класса. - /// - /// Полный путь до файла для чтения - public ReadOnlyStream(string fileFullPath) - { - var fileStream = File.Open(fileFullPath, FileMode.Open, FileAccess.Read, FileShare.Read); - _localStream = new StreamReader(fileStream, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: true); - if(_localStream == null) - throw new EndOfStreamException(); - } - - /// - /// Флаг окончания файла. - /// - public bool IsEof => _localStream == null || _localStream.EndOfStream; - - /// - /// Ф-ция чтения следующего символа из потока. - /// Если произведена попытка прочитать символ после достижения конца файла, метод - /// должен бросать соответствующее исключение - /// - /// Считанный символ. - public char ReadNextChar() - { - try - { - return (char)_localStream.Read(); - } - catch (EndOfStreamException e) - { - throw new EndOfStreamException(e.Message); - } - } - - /// - /// Сбрасывает текущую позицию потока на начало. - /// - public void ResetPositionToStart() - { - if (_localStream == null) - { - return; - } - - _localStream.DiscardBufferedData(); - _localStream.BaseStream.Position = 0; - } - - public void Dispose() - { - _localStream?.Dispose(); - _localStream = null; - } - } -} +using System.Text; +using TestTask.Interfaces; + +namespace TestTask.Services +{ + public sealed class ReadOnlyStream : IReadOnlyStream + { + private StreamReader _localStream; + + /// + /// Конструктор класса. + /// + /// Полный путь до файла для чтения + public ReadOnlyStream(string fileFullPath) + { + var fileStream = File.Open(fileFullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + _localStream = new StreamReader(fileStream, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + } + + /// + /// Флаг окончания файла. + /// + public bool IsEof => _localStream == null || _localStream.EndOfStream; + + /// + /// Ф-ция чтения следующего символа из потока. + /// Если произведена попытка прочитать символ после достижения конца файла, метод + /// должен бросать соответствующее исключение + /// + /// Считанный символ. + public char ReadNextChar() + { + if(IsEof) + throw new EndOfStreamException(); + + return (char)_localStream.Read(); + } + + /// + /// Сбрасывает текущую позицию потока на начало. + /// + public void ResetPositionToStart() + { + if (_localStream == null) + { + return; + } + + _localStream.DiscardBufferedData(); + _localStream.BaseStream.Position = 0; + } + + public void Dispose() + { + _localStream?.Dispose(); + _localStream = null; + } + } +} From cc738a31bf6882f969d4f6ff9b9119b42133ebc1 Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 16:02:25 +0700 Subject: [PATCH 08/10] add letter filter strategy --- .../CharFilterStrategyFactory.cs | 26 +++++++++++++ .../LetterStrategy/ConsonantFilterStrategy.cs | 15 ++++++++ .../LetterStrategy/ICharFilterStrategy.cs | 14 +++++++ .../LetterStrategy/VowelFilterStrategy.cs | 15 ++++++++ TestTask/Services/StatisticRemover.cs | 25 +++++++++++++ TestTask/StatisticRemover.cs | 37 ------------------- 6 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 TestTask/Services/LetterStrategy/CharFilterStrategyFactory.cs create mode 100644 TestTask/Services/LetterStrategy/ConsonantFilterStrategy.cs create mode 100644 TestTask/Services/LetterStrategy/ICharFilterStrategy.cs create mode 100644 TestTask/Services/LetterStrategy/VowelFilterStrategy.cs create mode 100644 TestTask/Services/StatisticRemover.cs delete mode 100644 TestTask/StatisticRemover.cs diff --git a/TestTask/Services/LetterStrategy/CharFilterStrategyFactory.cs b/TestTask/Services/LetterStrategy/CharFilterStrategyFactory.cs new file mode 100644 index 0000000..8b38a76 --- /dev/null +++ b/TestTask/Services/LetterStrategy/CharFilterStrategyFactory.cs @@ -0,0 +1,26 @@ +using TestTask.Models; + +namespace TestTask.Services.LetterStrategy; + +public static class CharFilterStrategyFactory +{ + private static readonly Dictionary Strategies = new() + { + { CharType.Vowel, new VowelFilterStrategy() }, + { CharType.Consonants, new ConsonantFilterStrategy() }, + }; + + /// + /// Возвращает стратегию фильтрации для указанного типа символов. + /// + /// Тип символов. + /// Соответствующая стратегия. + /// Если тип не поддерживается. + public static ICharFilterStrategy GetStrategy(CharType charType) + { + if (!Strategies.TryGetValue(charType, out var strategy)) + throw new ArgumentOutOfRangeException(nameof(charType), $"Неизвестный тип: {charType}"); + + return strategy; + } +} \ No newline at end of file diff --git a/TestTask/Services/LetterStrategy/ConsonantFilterStrategy.cs b/TestTask/Services/LetterStrategy/ConsonantFilterStrategy.cs new file mode 100644 index 0000000..be4db7f --- /dev/null +++ b/TestTask/Services/LetterStrategy/ConsonantFilterStrategy.cs @@ -0,0 +1,15 @@ +using TestTask.Extensions; + +namespace TestTask.Services.LetterStrategy; + +/// +/// Стратегия удаления согласных букв из статистики. +/// +public class ConsonantFilterStrategy : ICharFilterStrategy +{ + public bool ShouldRemove(string letter) + { + if (string.IsNullOrEmpty(letter)) return false; + return ConsonantVowelClassificator.IsConsonant(letter[0]); + } +} \ No newline at end of file diff --git a/TestTask/Services/LetterStrategy/ICharFilterStrategy.cs b/TestTask/Services/LetterStrategy/ICharFilterStrategy.cs new file mode 100644 index 0000000..a8a9402 --- /dev/null +++ b/TestTask/Services/LetterStrategy/ICharFilterStrategy.cs @@ -0,0 +1,14 @@ +namespace TestTask.Services.LetterStrategy; + +/// +/// Стратегия выбора фильтров букв из статистики +/// +public interface ICharFilterStrategy +{ + /// + /// Определяет, надо ли убрать букву/пару из статистики + /// + /// Буква или пара букв для проверки + /// true - если надо удалить + bool ShouldRemove(string letter); +} \ No newline at end of file diff --git a/TestTask/Services/LetterStrategy/VowelFilterStrategy.cs b/TestTask/Services/LetterStrategy/VowelFilterStrategy.cs new file mode 100644 index 0000000..fc5d402 --- /dev/null +++ b/TestTask/Services/LetterStrategy/VowelFilterStrategy.cs @@ -0,0 +1,15 @@ +using TestTask.Extensions; + +namespace TestTask.Services.LetterStrategy; + +/// +/// Стратегия удаления гласных букв из статистики. +/// +public class VowelFilterStrategy : ICharFilterStrategy +{ + public bool ShouldRemove(string letter) + { + if (string.IsNullOrEmpty(letter)) return false; + return ConsonantVowelClassificator.IsVowel(letter[0]); + } +} \ No newline at end of file diff --git a/TestTask/Services/StatisticRemover.cs b/TestTask/Services/StatisticRemover.cs new file mode 100644 index 0000000..dd7f326 --- /dev/null +++ b/TestTask/Services/StatisticRemover.cs @@ -0,0 +1,25 @@ +using TestTask.Models; +using TestTask.Services.LetterStrategy; + +namespace TestTask.Services; + +public static class LetterStatsExtension +{ + /// + /// Удалить записи по типу стратегии. + /// + /// Список статистики. + /// Тип стратегии. + public static void RemoveElementsByStrategy(this IList letterStats, ICharFilterStrategy strategy) + { + for(int i = 0; i < letterStats.Count; i++) + { + if(!strategy.ShouldRemove(letterStats[i].Letter)) + { + continue; + } + letterStats.RemoveAt(i); + i--; + } + } +} \ No newline at end of file diff --git a/TestTask/StatisticRemover.cs b/TestTask/StatisticRemover.cs deleted file mode 100644 index 3e20874..0000000 --- a/TestTask/StatisticRemover.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace TestTask; - -public static class LetterStatsExtension -{ - public static void TrimConsonant(this IList letterStats) - { - TrimByPredicate(letterStats, IsConsonant); - } - - public static void TrimVowels(this IList letterStats) - { - TrimByPredicate(letterStats, IsVowel); - } - - private static void TrimByPredicate(IList letterStats, Func predicate) - { - for(int i = 0; i < letterStats.Count; i++) - { - if(!predicate(letterStats[i].Letter)) - { - continue; - } - letterStats.Remove(letterStats[i]); - i--; - } - } - - private static bool IsConsonant(string letter) - { - return string.IsNullOrEmpty(letter) || ConsonantVowelClassificator.IsConsonant(letter[0]); - } - - private static bool IsVowel(string letter) - { - return string.IsNullOrEmpty(letter) || ConsonantVowelClassificator.IsVowel(letter[0]); - } -} \ No newline at end of file From 6e326f97322d9d448af5bd6cac1576f6443837f2 Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 16:02:43 +0700 Subject: [PATCH 09/10] update program --- TestTask/Program.cs | 142 +++++++++++--------------------------------- 1 file changed, 36 insertions(+), 106 deletions(-) diff --git a/TestTask/Program.cs b/TestTask/Program.cs index 50297be..3c4b849 100644 --- a/TestTask/Program.cs +++ b/TestTask/Program.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; +using TestTask.Extensions; +using TestTask.Interfaces; +using TestTask.Models; +using TestTask.Services; +using TestTask.Services.LetterStrategy; namespace TestTask { - public class Program + internal static class Program { - /// /// Программа принимает на входе 2 пути до файлов. /// Анализирует в первом файле кол-во вхождений каждой буквы (регистрозависимо). Например А, б, Б, Г и т.д. @@ -16,97 +20,44 @@ public class Program /// Второй параметр - путь до второго файла. static void Main(string[] args) { - 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); - - PrintStatistic(singleLetterStats); - PrintStatistic(doubleLetterStats); - - Console.ReadKey(); - } - - /// - /// Ф-ция возвращает экземпляр потока с уже загруженным файлом для последующего посимвольного чтения. - /// - /// Полный путь до файла для чтения - /// Поток для последующего чтения. - private static IReadOnlyStream GetInputStream(string fileFullPath) - { - return new ReadOnlyStream(fileFullPath); - } - - /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения каждой буквы. - /// Статистика РЕГИСТРОЗАВИСИМАЯ! - /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillSingleLetterStats(IReadOnlyStream stream) - { - stream.ResetPositionToStart(); - var stats = new Dictionary(); - while (!stream.IsEof) + if (args.Length < 2) { - char c = stream.ReadNextChar(); - - if(!char.IsLetter(c)) - continue; - - stats.TryAdd(c, 0); - stats[c]++; + Console.WriteLine("Использование: TestTask <путь_к_файлу1> <путь_к_файлу2>"); + return; } - var result = new List(); - foreach (var stat in stats) + try { - result.Add(new LetterStats(){Letter = stat.Key.ToString(), Count = stat.Value}); - } - return result; - } - - /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. - /// В статистику должны попадать только пары из одинаковых букв, например АА, cС, УУ, ee и т.д. - /// Статистика - НЕ регистрозависимая! - /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillDoubleLetterStats(IReadOnlyStream stream) - { - stream.ResetPositionToStart(); - var stats = new Dictionary(); - if(stream.IsEof) - return new List(); + IList singleLetterStats; + IList doubleLetterStats; - char prevChar = stream.ReadNextChar(); - - while (!stream.IsEof) - { - char c = stream.ReadNextChar(); - if (!prevChar.EqualsSensitiveCase(c, true)) + using (IReadOnlyStream inputStream1 = new ReadOnlyStream(args[0])) { - prevChar = c; - continue; + singleLetterStats = LetterStatsService.FillSingleLetterStats(inputStream1); } - - var combination = $"{prevChar}{c}"; - stats.TryAdd(combination, 0); - stats[combination]++; - prevChar = c; - } + using (IReadOnlyStream inputStream2 = new ReadOnlyStream(args[1])) + { + doubleLetterStats = LetterStatsService.FillDoubleLetterStats(inputStream2); + } + + RemoveCharStatsByType(singleLetterStats, CharType.Vowel); + RemoveCharStatsByType(doubleLetterStats, CharType.Consonants); - var result = new List(); - foreach (var stat in stats) + LetterStatisticsPrintHelper.PrintStatisticSorted(singleLetterStats); + LetterStatisticsPrintHelper.PrintStatisticSorted(doubleLetterStats); + } + catch (FileNotFoundException e) + { + Console.WriteLine($"Файл не найден: {e.FileName}"); + } + catch (Exception e) + { + Console.WriteLine($"Ошибка: {e.Message}"); + } + finally { - result.Add(new LetterStats(){Letter = stat.Key, Count = stat.Value}); + Console.ReadKey(); } - return result; } /// @@ -118,29 +69,8 @@ private static IList FillDoubleLetterStats(IReadOnlyStream stream) /// Тип букв для анализа private static void RemoveCharStatsByType(IList letters, CharType charType) { - switch (charType) - { - case CharType.Consonants: - letters.TrimConsonant(); - break; - case CharType.Vowel: - letters.TrimVowels(); - break; - default: - break; - } - } - - /// - /// Ф-ция выводит на экран полученную статистику в формате "{Буква/пара} : {Кол-во}" - /// Каждая буква/пара - с новой строки. - /// Выводить на экран необходимо предварительно отсортировав набор по алфавиту. - /// В конце отдельная строчка с ИТОГО, содержащая в себе общее кол-во найденных букв/пар - /// - /// Коллекция со статистикой - private static void PrintStatistic(IList letters) - { - LetterStatisticsPrinter.PrintStatisticSorted(letters); + var strategy = CharFilterStrategyFactory.GetStrategy(charType); + letters.RemoveElementsByStrategy(strategy); } } From 4f5303ec9aa0789fd286860abcefc57255014db9 Mon Sep 17 00:00:00 2001 From: Alina Svintsova Date: Fri, 27 Feb 2026 16:02:59 +0700 Subject: [PATCH 10/10] some fix --- TestTask/TestTask.csproj | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/TestTask/TestTask.csproj b/TestTask/TestTask.csproj index 05a0b26..2da0747 100644 --- a/TestTask/TestTask.csproj +++ b/TestTask/TestTask.csproj @@ -3,13 +3,10 @@ Exe net8.0 - 10 + latest enable - disable + enable true true - - - \ No newline at end of file