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/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 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 54% rename from TestTask/IReadOnlyStream.cs rename to TestTask/Interfaces/IReadOnlyStream.cs index 6946745..d70d786 100644 --- a/TestTask/IReadOnlyStream.cs +++ b/TestTask/Interfaces/IReadOnlyStream.cs @@ -1,16 +1,15 @@ -namespace TestTask -{ - /// - /// Интерфейс для работы с файлом в сильно урезаном виде. - /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. - /// - internal interface IReadOnlyStream - { - // TODO : Необходимо доработать данный интерфейс для обеспечения гарантированного закрытия файла, по окончанию работы с таковым! - 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/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/Program.cs b/TestTask/Program.cs index fdf048e..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,70 +20,44 @@ public class Program /// Второй параметр - путь до второго файла. static void Main(string[] args) { - IReadOnlyStream inputStream1 = GetInputStream(args[0]); - 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); - - // TODO : Необжодимо дождаться нажатия клавиши, прежде чем завершать выполнение программы. - } - - /// - /// Ф-ция возвращает экземпляр потока с уже загруженным файлом для последующего посимвольного чтения. - /// - /// Полный путь до файла для чтения - /// Поток для последующего чтения. - private static IReadOnlyStream GetInputStream(string fileFullPath) - { - return new ReadOnlyStream(fileFullPath); - } - - /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения каждой буквы. - /// Статистика РЕГИСТРОЗАВИСИМАЯ! - /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillSingleLetterStats(IReadOnlyStream stream) - { - stream.ResetPositionToStart(); - while (!stream.IsEof) + if (args.Length < 2) { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - регистрозависимый. + Console.WriteLine("Использование: TestTask <путь_к_файлу1> <путь_к_файлу2>"); + return; } - //return ???; - - throw new NotImplementedException(); - } - - /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. - /// В статистику должны попадать только пары из одинаковых букв, например АА, СС, УУ, ЕЕ и т.д. - /// Статистика - НЕ регистрозависимая! - /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillDoubleLetterStats(IReadOnlyStream stream) - { - stream.ResetPositionToStart(); - while (!stream.IsEof) + try { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - НЕ регистрозависимый. + IList singleLetterStats; + IList doubleLetterStats; + + using (IReadOnlyStream inputStream1 = new ReadOnlyStream(args[0])) + { + singleLetterStats = LetterStatsService.FillSingleLetterStats(inputStream1); + } + using (IReadOnlyStream inputStream2 = new ReadOnlyStream(args[1])) + { + doubleLetterStats = LetterStatsService.FillDoubleLetterStats(inputStream2); + } + + RemoveCharStatsByType(singleLetterStats, CharType.Vowel); + RemoveCharStatsByType(doubleLetterStats, CharType.Consonants); + + LetterStatisticsPrintHelper.PrintStatisticSorted(singleLetterStats); + LetterStatisticsPrintHelper.PrintStatisticSorted(doubleLetterStats); + } + catch (FileNotFoundException e) + { + Console.WriteLine($"Файл не найден: {e.FileName}"); + } + catch (Exception e) + { + Console.WriteLine($"Ошибка: {e.Message}"); + } + finally + { + Console.ReadKey(); } - - //return ???; - - throw new NotImplementedException(); } /// @@ -91,39 +69,9 @@ private static IList FillDoubleLetterStats(IReadOnlyStream stream) /// Тип букв для анализа private static void RemoveCharStatsByType(IList letters, CharType charType) { - // TODO : Удалить статистику по запрошенному типу букв. - switch (charType) - { - case CharType.Consonants: - break; - case CharType.Vowel: - break; - } - + var strategy = CharFilterStrategyFactory.GetStrategy(charType); + letters.RemoveElementsByStrategy(strategy); } - /// - /// Ф-ция выводит на экран полученную статистику в формате "{Буква} : {Кол-во}" - /// Каждая буква - с новой строки. - /// Выводить на экран необходимо предварительно отсортировав набор по алфавиту. - /// В конце отдельная строчка с ИТОГО, содержащая в себе общее кол-во найденных букв/пар - /// - /// Коллекция со статистикой - private static void PrintStatistic(IEnumerable letters) - { - // TODO : Выводить на экран статистику. Выводить предварительно отсортировав по алфавиту! - throw new NotImplementedException(); - } - - /// - /// Метод увеличивает счётчик вхождений по переданной структуре. - /// - /// - private static void IncStatistic(LetterStats letterStats) - { - letterStats.Count++; - } - - } } 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/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 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/ReadOnlyStream.cs b/TestTask/Services/ReadOnlyStream.cs similarity index 53% rename from TestTask/ReadOnlyStream.cs rename to TestTask/Services/ReadOnlyStream.cs index a51a61e..568402a 100644 --- a/TestTask/ReadOnlyStream.cs +++ b/TestTask/Services/ReadOnlyStream.cs @@ -1,60 +1,59 @@ -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.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; + } + } +} 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/TestTask.csproj b/TestTask/TestTask.csproj index dc470b0..2da0747 100644 --- a/TestTask/TestTask.csproj +++ b/TestTask/TestTask.csproj @@ -1,53 +1,12 @@ - - - - - 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 + latest + enable + enable + true + true + \ No newline at end of file