diff --git a/.github/workflows/TestTask-tests-workflow.yml b/.github/workflows/TestTask-tests-workflow.yml new file mode 100644 index 0000000..ae31fa5 --- /dev/null +++ b/.github/workflows/TestTask-tests-workflow.yml @@ -0,0 +1,21 @@ +name: TestTask Build and Test # Имя workflow, отображается в интерфейсе GitHub Actions +on: + push: # Запускается при каждом push в репозиторий + branches: + - master # ветки, для которых нужно запускать тесты + +jobs: # Определяет набор задач (jobs), которые будут выполнены + service-build: # Имя задачи + runs-on: windows-latest # на каком окружении будет выполняться задача + steps: # Список шагов, которые будут выполнены в рамках задачи + - name: Checkout repository + uses: actions/checkout@v3 + - name: Restore NuGet packages + run: | + nuget restore TestTask\TestTask.sln # Ваш sln-файл решения + - name: Build solution + run: | + & "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\amd64\msbuild.exe" /m TestTask\TestTask.sln # Сборка решения + - name: Run tests + working-directory: TestTask.UnitTests + run: dotnet test TestTask.UnitTests.csproj --verbosity normal # Выполняет команду dotnet test для запуска юнит-тестов \ No newline at end of file diff --git a/README.md b/README.md index 1036790..8d999a2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Тестовое задание. +# ТЗ. Доброго дня. Данный репозиторий представляет из себя тестовое задание для выполнения перед собеседованием на вакансию разработчика C# в компанию Катрен. @@ -20,3 +20,43 @@ __Для разработки настоятельно рекомендуетс И по готовности результат отдать на ревью методом Pull Request'a.__ Если данный способ по каким-то причинам Вам не представляется реализуемым, можете выслать результат в виде архива на почту кадровому агенту. + +# РЕШЕНИЕ +## Что сделано? +Всё, согласно ТЗ, все `TODO` (9 шт). Код исправлен и доработан. + +### Дополнительно +- Написаны юнит-тесты, +- Настроен автозапуск тестов на `GitHub Actions` при каждом коммите в удаленный репозиторий. + +### Структура Решения +Решение состоит из 2-х проектов +- TestTask (исходный проект), +- TestTask.UnitTests (юнит-тесты). + +``` +. +├──📂TestTask Исходный проект +│ ├──📂Helpers +│ │ ├──📄HelperFillStat.cs Логика для заполнения статистики +│ │ ├──📄HelperMain.cs Логика для основного метода Main +│ │ ├──📄HelperPrintStat.cs Логика для печати статистики +│ │ └──📄HelperRemoveStat.cs Логика для удаления статистики +│ ├──📄CharType.cs +│ ├──📄IReadOnlyStream.cs +│ ├──📄LetterStats.cs +│ ├──📄Program.cs Точка входа в программу +│ └──📄ReadOnlyStream.cs +│ +└──📂TestTask.UnitTests Проект с юнит-тестами + ├──📂Fakes + │ └──📄FakeReadOnlyStream.cs Фейковая реализация ReadOnlyStream + ├──📂InputData + │ └──📄InputObjectData.cs Входные данные для тестов + ├──📄FillStatsTests.cs Юнит-тесты для HelperFillStat + ├──📄HelperMainTests.cs Юнит-тесты для HelperMain + ├──📄PrintStatsTests.cs Юнит-тесты для HelperPrintStat + └──📄RemoveStatsTests.cs Юнит-тесты для HelperRemoveStat +``` + +Исполняемые файлы с программой [здесь](https://github.com/Certalarm/TestTask/tree/master/_assets/TestTask.zip). Не требует установки. diff --git a/TestTask.UnitTests/Fakes/FakeReadOnlyStream.cs b/TestTask.UnitTests/Fakes/FakeReadOnlyStream.cs new file mode 100644 index 0000000..966d8da --- /dev/null +++ b/TestTask.UnitTests/Fakes/FakeReadOnlyStream.cs @@ -0,0 +1,25 @@ +using System.IO; +using System.Text; + +namespace TestTask.UnitTests.Fakes +{ + public class FakeReadOnlyStream : ReadOnlyStream + { + public FakeReadOnlyStream(string inputData, Encoding encoding = default) : base(inputData, encoding) + { + } + + public override Stream CreateStream(string param) + { + try + { + byte[] bytes = Encoding.GetBytes(param); + return new MemoryStream(bytes); + } + catch + { + return Stream.Null; + } + } + } +} diff --git a/TestTask.UnitTests/FillStatsTests.cs b/TestTask.UnitTests/FillStatsTests.cs new file mode 100644 index 0000000..a995f4e --- /dev/null +++ b/TestTask.UnitTests/FillStatsTests.cs @@ -0,0 +1,77 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.IO; +using TestTask.Helpers; +using TestTask.UnitTests.Fakes; +using TestTask.UnitTests.InputData; +using Xunit; + +namespace TestTask.UnitTests +{ + public class FillStatsTests + { + // format: inputData + public static List DataWithNoLetters = InputObjectData.DataWithNoLetters; + + // format: inputData, singleStatCount, doubleStatCount + public static List DataWithLettersOnly = InputObjectData.DataWithLettersOnlyFillStat; + + [Fact] + public void create_reader_with_null_param_will_be_exception() + { + FakeReadOnlyStream sut; + + Assert.Throws(() => sut = new FakeReadOnlyStream(null)); + } + + [Fact] + public void fill_stats_with_control_symbols_only_will_be_exception() + { + string stream = "\0\0\0\0\0\0\0\0\0\0\t\t\t\r\n\r\n"; + + IList singleResult = new List(); + IList doubleResult = new List(); + + using (IReadOnlyStream sut = new FakeReadOnlyStream(stream)) + { + Assert.Throws(() => singleResult = HelperFillStat.FillSingleLetterStats(sut)); + Assert.Throws(() => doubleResult = HelperFillStat.FillDoubleLetterStats(sut)); + } + } + + [Theory] + [MemberData(nameof(DataWithNoLetters))] + public void fill_stats_with_no_letters_symbols_only_will_be_empty_stats(string inputData) + { + IList singleResult = new List(); + IList doubleResult = new List(); + + using (IReadOnlyStream sut = new FakeReadOnlyStream(inputData)) + { + singleResult = HelperFillStat.FillSingleLetterStats(sut); + doubleResult = HelperFillStat.FillDoubleLetterStats(sut); + } + + singleResult.Should().HaveCount(0); + doubleResult.Should().HaveCount(0); + } + + [Theory] + [MemberData(nameof(DataWithLettersOnly))] + public void fill_stats_with_letters_symbols_only_will_has_count(string inputData, int singleStatsCount, int doubleStatsCount) + { + IList singleResult = new List(); + IList doubleResult = new List(); + + using (IReadOnlyStream sut = new FakeReadOnlyStream(inputData)) + { + singleResult = HelperFillStat.FillSingleLetterStats(sut); + doubleResult = HelperFillStat.FillDoubleLetterStats(sut); + } + + singleResult.Should().HaveCount(singleStatsCount); + doubleResult.Should().HaveCount(doubleStatsCount); + } + } +} diff --git a/TestTask.UnitTests/HelperMainTests.cs b/TestTask.UnitTests/HelperMainTests.cs new file mode 100644 index 0000000..a035037 --- /dev/null +++ b/TestTask.UnitTests/HelperMainTests.cs @@ -0,0 +1,22 @@ +using FluentAssertions; +using System.Collections.Generic; +using TestTask.Helpers; +using TestTask.UnitTests.InputData; +using Xunit; + +namespace TestTask.UnitTests +{ + public class HelperMainTests + { + public static List DataWithBadPaths = InputObjectData.DataWithBadPaths; + + [Theory] + [MemberData(nameof(DataWithBadPaths))] + public void isBadArgs_with_bad_args_will_be_true(string[] args) + { + var result = HelperMain.IsBadArgs(args); + + result.Should().BeTrue(); + } + } +} diff --git a/TestTask.UnitTests/InputData/InputObjectData.cs b/TestTask.UnitTests/InputData/InputObjectData.cs new file mode 100644 index 0000000..5ec013d --- /dev/null +++ b/TestTask.UnitTests/InputData/InputObjectData.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; + +namespace TestTask.UnitTests.InputData +{ + public static class InputObjectData + { + private static readonly List _inputData = new List() + { + "ААБББВВВВГДЕЖЗЯЯЯ", + "аабббввввгдежзяяя", + "аАБбБвВВвГдЕЖзяЯЯ", + }; + + + // format: inputData + public static List DataWithNoLetters = new List() + { + new object[] { "" }, + new object[] { "123454321708090706,-./" }, + new object[] { "12345\0\0\0\0\0\0\0\0\0\0\t\t\t\r\n\r\n"}, + }; + + // format: inputData, singleStatCount, doubleStatCount + public static List DataWithLettersOnlyFillStat = new List() + { + new object[] { _inputData[0], 9, 4 }, + new object[] { _inputData[1], 9, 4 }, + new object[] { _inputData[2], 13, 4}, + }; + + // format: inputData, singleStatCount, doubleStatCount + public static List DataWithLettersOnlyRemoveVowelStat = new List() + { + new object[] { _inputData[0], 6, 2 }, + new object[] { _inputData[1], 6, 2 }, + new object[] { _inputData[2], 8, 2 }, + }; + + // format: inputData, singleStatCount, doubleStatCount + public static List DataWithLettersOnlyRemoveConsonantStat = new List() + { + new object[] { _inputData[0], 3, 2 }, + new object[] { _inputData[1], 3, 2 }, + new object[] { _inputData[2], 5, 2 }, + }; + + + // format: inputData, singleStatLineCount + public static List DataWithLettersOnlySinglePrintStat = new List() + { + new object[] { _inputData[0], 7 }, + new object[] { _inputData[1], 7 }, + new object[] { _inputData[2], 9 }, + }; + + // format: inputData, doubleStatLineCount + public static List DataWithLettersOnlyDoublePrintStat = new List() + { + new object[] { _inputData[0], 3 }, + new object[] { _inputData[1], 3 }, + new object[] { _inputData[2], 3 }, + }; + + // format: args[] + public static List DataWithBadPaths = new List() + { + new object[] { null }, + new object[] { new object[] { null, null } }, + new object[] { new object[] { "", "" } }, + new object[] { new object[] { null, "" } }, + new object[] { new object[] { "", null } }, + new object[] { new object[] { null, "1" } }, + new object[] { new object[] { "1", null } }, + new object[] { new object[] { "", "1" } }, + new object[] { new object[] { "1", "" } }, + new object[] { new object[] { "1", "" } }, + new object[] { new object[] { "1" } }, + new object[] { new object[] { "" } }, + new object[] { new object[] { null } }, + }; + } +} diff --git a/TestTask.UnitTests/PrintStatsTests.cs b/TestTask.UnitTests/PrintStatsTests.cs new file mode 100644 index 0000000..ca1b977 --- /dev/null +++ b/TestTask.UnitTests/PrintStatsTests.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using TestTask.Helpers; +using TestTask.UnitTests.Fakes; +using TestTask.UnitTests.InputData; +using Xunit; + +namespace TestTask.UnitTests +{ + public class PrintStatsTests + { + public static List DataWithLettersOnlySingle = InputObjectData.DataWithLettersOnlySinglePrintStat; + public static List DataWithLettersOnlyDouble = InputObjectData.DataWithLettersOnlyDoublePrintStat; + + [Theory] + [MemberData(nameof(DataWithLettersOnlySingle))] + public void report_single_stats_will_be_have_line_count(string inputData, int singleStatsLineCount) + { + var stream = new FakeReadOnlyStream(inputData); + var singleStat = HelperFillStat.FillSingleLetterStats(stream); + HelperRemoveStat.RemoveCharStatsByType(singleStat, CharType.Vowel); + + var linesCount = HelperPrintStat.BuildReport(singleStat) + .Split(new string[] { Environment.NewLine }, StringSplitOptions.None) + .Count(); + + linesCount.Should().Be(singleStatsLineCount); + } + + [Theory] + [MemberData(nameof(DataWithLettersOnlyDouble))] + public void report_double_stats_will_be_have_line_count(string inputData, int doubleStatsLineCount) + { + var stream = new FakeReadOnlyStream(inputData); + var doubleStat = HelperFillStat.FillDoubleLetterStats(stream); + HelperRemoveStat.RemoveCharStatsByType(doubleStat, CharType.Consonants); + + var linesCount = HelperPrintStat.BuildReport(doubleStat) + .Split(new string[] { Environment.NewLine }, StringSplitOptions.None) + .Count(); + + linesCount.Should().Be(doubleStatsLineCount); + } + } +} diff --git a/TestTask.UnitTests/Properties/AssemblyInfo.cs b/TestTask.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7140d2e --- /dev/null +++ b/TestTask.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Общие сведения об этой сборке предоставляются следующим набором +// набора атрибутов. Измените значения этих атрибутов для изменения сведений, +// связанные со сборкой. +[assembly: AssemblyTitle("TestTask.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestTask.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми +// для компонентов COM. Если необходимо обратиться к типу в этой сборке через +// COM, задайте атрибуту ComVisible значение TRUE для этого типа. +[assembly: ComVisible(false)] + +// Следующий GUID служит для идентификации библиотеки типов, если этот проект будет видимым для COM +[assembly: Guid("99a1b5e9-b57d-4591-b8df-0e6796c45d4c")] + +// Сведения о версии сборки состоят из указанных ниже четырех значений: +// +// Основной номер версии +// Дополнительный номер версии +// Номер сборки +// Редакция +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/TestTask.UnitTests/RemoveStatsTests.cs b/TestTask.UnitTests/RemoveStatsTests.cs new file mode 100644 index 0000000..bfd4523 --- /dev/null +++ b/TestTask.UnitTests/RemoveStatsTests.cs @@ -0,0 +1,63 @@ +using FluentAssertions; +using System.Collections.Generic; +using TestTask.Helpers; +using TestTask.UnitTests.Fakes; +using TestTask.UnitTests.InputData; +using Xunit; + +namespace TestTask.UnitTests +{ + public class RemoveStatsTests + { + public static List DataWithLettersOnlyVowel = InputObjectData.DataWithLettersOnlyRemoveVowelStat; + public static List DataWithLettersOnlyConsonant = InputObjectData.DataWithLettersOnlyRemoveConsonantStat; + + [Theory] + [MemberData(nameof(DataWithLettersOnlyVowel))] + public void after_remove_vowels_single_stats_will_be_have_count(string inputData, int singleStatsCount, int _) + { + var stream = new FakeReadOnlyStream(inputData); + var singleStat = HelperFillStat.FillSingleLetterStats(stream); + + HelperRemoveStat.RemoveCharStatsByType(singleStat, CharType.Vowel); + + singleStat.Should().HaveCount(singleStatsCount); + } + + [Theory] + [MemberData(nameof(DataWithLettersOnlyVowel))] + public void after_remove_vowels_double_stats_will_be_have_count(string inputData, int _, int doubleStatsCount) + { + var stream = new FakeReadOnlyStream(inputData); + var doubleStat = HelperFillStat.FillDoubleLetterStats(stream); + + HelperRemoveStat.RemoveCharStatsByType(doubleStat, CharType.Vowel); + + doubleStat.Should().HaveCount(doubleStatsCount); + } + + [Theory] + [MemberData(nameof(DataWithLettersOnlyConsonant))] + public void after_remove_consonants_single_stats_will_be_have_count(string inputData, int singleStatsCount, int _) + { + var stream = new FakeReadOnlyStream(inputData); + var singleStat = HelperFillStat.FillSingleLetterStats(stream); + + HelperRemoveStat.RemoveCharStatsByType(singleStat, CharType.Consonants); + + singleStat.Should().HaveCount(singleStatsCount); + } + + [Theory] + [MemberData(nameof(DataWithLettersOnlyConsonant))] + public void after_remove_consonants_double_stats_will_be_have_count(string inputData, int _, int doubleStatsCount) + { + var stream = new FakeReadOnlyStream(inputData); + var doubleStat = HelperFillStat.FillDoubleLetterStats(stream); + + HelperRemoveStat.RemoveCharStatsByType(doubleStat, CharType.Consonants); + + doubleStat.Should().HaveCount(doubleStatsCount); + } + } +} diff --git a/TestTask.UnitTests/TestTask.UnitTests.csproj b/TestTask.UnitTests/TestTask.UnitTests.csproj new file mode 100644 index 0000000..bacaf93 --- /dev/null +++ b/TestTask.UnitTests/TestTask.UnitTests.csproj @@ -0,0 +1,169 @@ + + + + + + + + + + Debug + AnyCPU + {99A1B5E9-B57D-4591-B8DF-0E6796C45D4C} + Library + Properties + TestTask.UnitTests + TestTask.UnitTests + v4.8 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x86 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\TestTask\packages\FluentAssertions.8.8.0\lib\net47\FluentAssertions.dll + + + ..\TestTask\packages\Microsoft.ApplicationInsights.2.23.0\lib\net46\Microsoft.ApplicationInsights.dll + + + ..\TestTask\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\TestTask\packages\Microsoft.Testing.Platform.MSBuild.1.9.1\lib\netstandard2.0\Microsoft.Testing.Extensions.MSBuild.dll + + + ..\TestTask\packages\Microsoft.Testing.Extensions.Telemetry.1.9.1\lib\netstandard2.0\Microsoft.Testing.Extensions.Telemetry.dll + + + ..\TestTask\packages\Microsoft.Testing.Extensions.TrxReport.Abstractions.1.9.1\lib\netstandard2.0\Microsoft.Testing.Extensions.TrxReport.Abstractions.dll + + + ..\TestTask\packages\Microsoft.Testing.Platform.1.9.1\lib\netstandard2.0\Microsoft.Testing.Platform.dll + + + ..\TestTask\packages\Microsoft.TestPlatform.ObjectModel.17.13.0\lib\net462\Microsoft.TestPlatform.CoreUtilities.dll + + + ..\TestTask\packages\Microsoft.TestPlatform.ObjectModel.17.13.0\lib\net462\Microsoft.TestPlatform.PlatformAbstractions.dll + + + ..\TestTask\packages\Microsoft.TestPlatform.ObjectModel.17.13.0\lib\net462\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll + + + ..\TestTask\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll + + + + ..\TestTask\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\TestTask\packages\System.Collections.Immutable.6.0.0\lib\net461\System.Collections.Immutable.dll + + + + + ..\TestTask\packages\System.Diagnostics.DiagnosticSource.6.0.0\lib\net461\System.Diagnostics.DiagnosticSource.dll + + + ..\TestTask\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + ..\TestTask\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\TestTask\packages\System.Reflection.Metadata.1.6.0\lib\netstandard2.0\System.Reflection.Metadata.dll + + + + ..\TestTask\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + + ..\TestTask\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll + + + ..\TestTask\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll + + + ..\TestTask\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + + + + + + + False + ..\TestTask\bin\Debug\TestTask.exe + + + ..\TestTask\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll + True + + + ..\TestTask\packages\xunit.assert.2.9.3\lib\netstandard1.1\xunit.assert.dll + + + ..\TestTask\packages\xunit.extensibility.core.2.9.3\lib\net452\xunit.core.dll + + + ..\TestTask\packages\xunit.extensibility.execution.2.9.3\lib\net452\xunit.execution.desktop.dll + + + + + + + + + + + + + + + + + + + + + + + Данный проект ссылается на пакеты NuGet, отсутствующие на этом компьютере. Используйте восстановление пакетов NuGet, чтобы скачать их. Дополнительную информацию см. по адресу: http://go.microsoft.com/fwlink/?LinkID=322105. Отсутствует следующий файл: {0}. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestTask.UnitTests/app.config b/TestTask.UnitTests/app.config new file mode 100644 index 0000000..cb74baa --- /dev/null +++ b/TestTask.UnitTests/app.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestTask.UnitTests/packages.config b/TestTask.UnitTests/packages.config new file mode 100644 index 0000000..4b993ee --- /dev/null +++ b/TestTask.UnitTests/packages.config @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestTask/Helpers/HelperFillStat.cs b/TestTask/Helpers/HelperFillStat.cs new file mode 100644 index 0000000..afa8cd2 --- /dev/null +++ b/TestTask/Helpers/HelperFillStat.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; + +namespace TestTask.Helpers +{ + public static class HelperFillStat + { + /// + /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения каждой буквы. + /// Статистика РЕГИСТРОЗАВИСИМАЯ! + /// + /// Стрим для считывания символов для последующего анализа + /// Коллекция статистик по каждой букве, что была прочитана из стрима. + public static IList FillSingleLetterStats(IReadOnlyStream stream) + { + stream.ResetPositionToStart(); + Dictionary letterCountsMap = new Dictionary(); + while (!stream.IsEof) + { + char c = stream.ReadNextChar(); + // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - регистрозависимый. (+) + FillSingleStatMap(c, letterCountsMap); + } + return new List(letterCountsMap.Values); + } + + /// + /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. + /// В статистику должны попадать только пары из одинаковых букв, например АА, СС, УУ, ЕЕ и т.д. + /// Статистика - НЕ регистрозависимая! + /// + /// Стрим для считывания символов для последующего анализа + /// Коллекция статистик по каждой букве, что была прочитана из стрима. + public static IList FillDoubleLetterStats(IReadOnlyStream stream) + { + stream.ResetPositionToStart(); + Dictionary pairCountsMap = new Dictionary(); + char prevC = '\0'; + while (!stream.IsEof) + { + char c = stream.ReadNextChar(); + // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - НЕ регистрозависимый. (+) + FillDoubleStatMap(prevC, c, pairCountsMap); + prevC = c; + } + return new List(pairCountsMap.Values); + } + + /// + /// Метод добавляет в статистику по одиночным буквам, прочитанный символ. + /// + /// Символ для добаления в статистику. + /// Словарь статистики. + private static void FillSingleStatMap(char c, Dictionary map) + { + if (IsGoodSingleChar(c)) + { + FillStatMap(BuildSingleKey(c), map); + } + } + + /// + /// Метод добавляет в статистику по парным буквам, прочитанную пару символов. + /// + /// Первый символ для добаления в статистику. + /// Второй символ для добаления в статистику. + /// Словарь статистики. + private static void FillDoubleStatMap(char c1, char c2, Dictionary map) + { + if (IsGoodDoubleChar(c1, c2)) + { + FillStatMap(BuildDoubleKey(c1, c2), map); + } + } + + /// + /// Метод строит ключ для одиночной статистики. + /// + /// Символ для построения ключа. + /// Ключ для одиночной статистики. + private static string BuildSingleKey(char c) => c.ToString(); + + /// + /// Метод строит ключ для парной статистики. + /// + /// Первый символ для построения ключа. + /// Второй символ для построения ключа. + /// Ключ для парной статистики. + private static string BuildDoubleKey(char c1, char c2) => $"{c1}{c2}".ToUpper(); + + /// + /// Метод проверяет символ на принадлежность к букве. + /// + /// Символ для проверки. + /// Был ли символ буквой. + private static bool IsGoodSingleChar(char c) => + char.IsLetter(c); + + /// + /// Метод проверяет пару символов на принадлежность к буквам и на равенство букв. + /// + /// Первый символ для проверки. + /// Второй символ для проверки. + /// Является ли пара символов парой букв и одинаковы ли они. + private static bool IsGoodDoubleChar(char c1, char c2) => + IsLetterBoth(c1, c2) && IsEquallyBoth(c1, c2); + + /// + /// Метод проверяет пару символов на принадлежность к буквам. + /// + /// Первый символ для проверки. + /// Второй символ для проверки. + /// Является ли пара символов парой букв. + private static bool IsLetterBoth(char c1, char c2) => + char.IsLetter(c1) && char.IsLetter(c2); + + /// + /// Метод проверяет пару символов на равенство без учета регистра. + /// + /// Первый символ для проверки. + /// Второй символ для проверки. + /// Одинаковы ли символы. + private static bool IsEquallyBoth(char c1, char c2) => + char.ToUpperInvariant(c1) == char.ToUpperInvariant(c2); + + /// + /// Метод увеличивает статистику по существующему ключу или созает новую запись. + /// + /// Ключ для проверки. + /// Словарь со статистикой. + private static void FillStatMap(string key, Dictionary map) + { + if (map.TryGetValue(key, out LetterStats existingStat)) + { + IncStatistic(ref existingStat); + map[key] = existingStat; + } + else + { + map[key] = CreateLetterStats(key); + } + } + + /// + /// Метод создает статистику для впервые встретившегося ключа. + /// + /// Ключ статистики. + /// Статистика. + private static LetterStats CreateLetterStats(string key) => + new LetterStats + { + Letter = key, + Count = 1 + }; + + /// + /// Метод увеличивает счётчик вхождений по переданной структуре. + /// + /// + private static void IncStatistic(ref LetterStats letterStats) + { + letterStats.Count++; + } + } +} diff --git a/TestTask/Helpers/HelperMain.cs b/TestTask/Helpers/HelperMain.cs new file mode 100644 index 0000000..316fdf3 --- /dev/null +++ b/TestTask/Helpers/HelperMain.cs @@ -0,0 +1,40 @@ +using System; + +namespace TestTask.Helpers +{ + public static class HelperMain + { + private const string __help = "Пожалуйста введите 2 аргумента командной строки! Пути до файлов для обработки."; + private const string __pressAnyKey = "Для закрытия окна, нажмите любую клавишу..."; + + /// + /// Метод проверяет подхолит ли массив аргументов. + /// + /// Входные аргументы. + /// Является ли аргументы не подходящими. + public static bool IsBadArgs(string[] args) => + args == null || args.Length < 2 || string.IsNullOrWhiteSpace(args[0]) || string.IsNullOrWhiteSpace(args[1]); + + /// + /// Метод печатает в консоли подсказку по использованию программы. + /// + public static void PrintHelp() => PrintToConsole(__help); + + /// + /// Метод печатает в консоли сообщение о нажатии клавиши и ждет от пользователя нажатия клавишы. + /// + public static void WaitPressKey() + { + PrintToConsole(string.Empty); + PrintToConsole(__pressAnyKey); + Console.ReadKey(); + } + + /// + /// Метод печатает в консоль переданную строку. + /// + /// Строка, которую нужно напечатать. + public static void PrintToConsole(string report) => + Console.WriteLine(report); + } +} diff --git a/TestTask/Helpers/HelperPrintStat.cs b/TestTask/Helpers/HelperPrintStat.cs new file mode 100644 index 0000000..412b9c9 --- /dev/null +++ b/TestTask/Helpers/HelperPrintStat.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TestTask.Helpers +{ + public static class HelperPrintStat + { + private const string __space = " "; + private const string __colon = ":"; + private const string __total = "ИТОГО"; + private const string __empty = "[ПУСТО]"; + private static readonly string __rn = Environment.NewLine; + + /// + /// Метод отчет по статистике. + /// + /// Коллекция статистик. + /// Строковый отчет. + public static string BuildReport(IEnumerable letters) + { + if (!letters.Any()) + return __empty; + var sortedLetters = SortedLetters(letters); + var body = string.Join(__rn, BuildLines(sortedLetters)); + var total = BuildTotal(letters.Count()); + return $"{body}{__rn}{total}"; + } + + /// + /// Метод сортирует коллекцию статистик по алфавиту. + /// + /// Коллекция статистик. + /// Отсортированная коллекция статистик. + private static IEnumerable SortedLetters(IEnumerable letters) => + letters + .OrderBy(x => x.Letter); + + /// + /// Метод строит коллекцию строк из коллекции статистик. + /// + /// Входная коллекция статистик. + /// Коллекция строковых статистик. + private static IEnumerable BuildLines(IEnumerable letters) => + letters + .Select(x => BuildLine(x)); + + /// + /// Метод строит строку из статистики. + /// + /// Статистика. + /// Строковая статистика. + private static string BuildLine(LetterStats stat) => $"{stat.Letter}{BuildBreak()}{stat.Count}"; + + /// + /// Метод строит строку ИТОГО. + /// + /// Количество для ИТОГО. + /// Строка ИТОГО со значением. + private static string BuildTotal(int count) => $"{__total}{BuildBreak()}{count}"; + + /// + /// Метод строит строку для отделения значений. + /// + /// строка с разделителями. + private static string BuildBreak() => $"{__space}{__colon}{__space}"; + } +} diff --git a/TestTask/Helpers/HelperRemoveStat.cs b/TestTask/Helpers/HelperRemoveStat.cs new file mode 100644 index 0000000..f8e4daf --- /dev/null +++ b/TestTask/Helpers/HelperRemoveStat.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TestTask.Helpers +{ + public static class HelperRemoveStat + { + private static readonly HashSet __vowelsEnRu = + new HashSet("aeiouAEIOUаеёиоуыэюяАЕЁИОУЫЭЮЯ".ToCharArray()); + + /// + /// Ф-ция перебирает все найденные буквы/парные буквы, содержащие в себе только гласные или согласные буквы. + /// (Тип букв для перебора определяется параметром charType) + /// Все найденные буквы/пары соответствующие параметру поиска - удаляются из переданной коллекции статистик. + /// + /// Коллекция со статистиками вхождения букв/пар + /// Тип букв для анализа + public static void RemoveCharStatsByType(IList letters, CharType charType) + { + // TODO : Удалить статистику по запрошенному типу букв. (+) + switch (charType) + { + case CharType.Consonants: + RemoveConsonants(letters); + break; + case CharType.Vowel: + RemoveVowels(letters); + break; + default: + break; + } + } + + /// + /// Метод удаляет статистику по гласным буквам. + /// + /// Статистика. + private static void RemoveVowels(IList letters) + { + for (int i = letters.Count - 1; i >= 0; i--) + { + if (__vowelsEnRu.Contains(letters[i].Letter.First())) + letters.RemoveAt(i); + } + } + + /// + /// Метод удаляет статистику по согласным буквам. + /// + /// Статистика. + private static void RemoveConsonants(IList letters) + { + for (int i = letters.Count - 1; i >= 0; i--) + { + if (!__vowelsEnRu.Contains(letters[i].Letter.First())) + letters.RemoveAt(i); + } + } + } +} diff --git a/TestTask/IReadOnlyStream.cs b/TestTask/IReadOnlyStream.cs index 6946745..35dfc83 100644 --- a/TestTask/IReadOnlyStream.cs +++ b/TestTask/IReadOnlyStream.cs @@ -1,12 +1,14 @@ -namespace TestTask +using System; + +namespace TestTask { /// /// Интерфейс для работы с файлом в сильно урезаном виде. /// Умеет всего 2 вещи: прочитать символ, и перемотать стрим на начало. /// - internal interface IReadOnlyStream + public interface IReadOnlyStream: IDisposable { - // TODO : Необходимо доработать данный интерфейс для обеспечения гарантированного закрытия файла, по окончанию работы с таковым! + // TODO : Необходимо доработать данный интерфейс (+) char ReadNextChar(); void ResetPositionToStart(); diff --git a/TestTask/Program.cs b/TestTask/Program.cs index fdf048e..1a78a44 100644 --- a/TestTask/Program.cs +++ b/TestTask/Program.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using TestTask.Helpers; namespace TestTask { public class Program { - /// /// Программа принимает на входе 2 пути до файлов. /// Анализирует в первом файле кол-во вхождений каждой буквы (регистрозависимо). Например А, б, Б, Г и т.д. @@ -16,90 +15,79 @@ 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); + if (HelperMain.IsBadArgs(args)) + { + HelperMain.PrintHelp(); + } + else + { + ProcessingSingleStats(args[0]); + ProcessingDoubleStats(args[1]); + } + // TODO : Необжодимо дождаться нажатия клавиши, прежде чем завершать выполнение программы. (+) + HelperMain.WaitPressKey(); + } - // TODO : Необжодимо дождаться нажатия клавиши, прежде чем завершать выполнение программы. + /// + /// Метод запускает обработку файла для одиночных букв. + /// + /// Полный путь до файла для чтения + private static void ProcessingSingleStats(string fileFullPath) + { + var singleStats = GetSingleLetterStats(fileFullPath); + HelperRemoveStat.RemoveCharStatsByType(singleStats, CharType.Vowel); + PrintStatistic(singleStats); } /// - /// Ф-ция возвращает экземпляр потока с уже загруженным файлом для последующего посимвольного чтения. + /// Метод запускает обработку файла для парных букв. /// /// Полный путь до файла для чтения - /// Поток для последующего чтения. - private static IReadOnlyStream GetInputStream(string fileFullPath) + private static void ProcessingDoubleStats(string fileFullPath) { - return new ReadOnlyStream(fileFullPath); + var doubleStats = GetDoubleLetterStats(fileFullPath); + HelperRemoveStat.RemoveCharStatsByType(doubleStats, CharType.Consonants); + PrintStatistic(doubleStats); } /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения каждой буквы. - /// Статистика РЕГИСТРОЗАВИСИМАЯ! + /// Метод получает статистику по одиночным буквам. /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillSingleLetterStats(IReadOnlyStream stream) + /// Полный путь до файла для чтения + /// Коллекция статистик. + private static IList GetSingleLetterStats(string fileFullPath) { - stream.ResetPositionToStart(); - while (!stream.IsEof) + IList singleLetterStats = new List(); + using (IReadOnlyStream inputStream = GetInputStream(fileFullPath)) { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - регистрозависимый. + singleLetterStats = HelperFillStat.FillSingleLetterStats(inputStream); } - - //return ???; - - throw new NotImplementedException(); + return singleLetterStats; } /// - /// Ф-ция считывающая из входящего потока все буквы, и возвращающая коллекцию статистик вхождения парных букв. - /// В статистику должны попадать только пары из одинаковых букв, например АА, СС, УУ, ЕЕ и т.д. - /// Статистика - НЕ регистрозависимая! + /// Метод получает статистику по парным буквам. /// - /// Стрим для считывания символов для последующего анализа - /// Коллекция статистик по каждой букве, что была прочитана из стрима. - private static IList FillDoubleLetterStats(IReadOnlyStream stream) + /// Полный путь до файла для чтения + /// Коллекция статистик. + private static IList GetDoubleLetterStats(string fileFullPath) { - stream.ResetPositionToStart(); - while (!stream.IsEof) + IList doubleLetterStats = new List(); + using (IReadOnlyStream inputStream = GetInputStream(fileFullPath)) { - char c = stream.ReadNextChar(); - // TODO : заполнять статистику с использованием метода IncStatistic. Учёт букв - НЕ регистрозависимый. + doubleLetterStats = HelperFillStat.FillDoubleLetterStats(inputStream); } - - //return ???; - - throw new NotImplementedException(); + return doubleLetterStats; } /// - /// Ф-ция перебирает все найденные буквы/парные буквы, содержащие в себе только гласные или согласные буквы. - /// (Тип букв для перебора определяется параметром charType) - /// Все найденные буквы/пары соответствующие параметру поиска - удаляются из переданной коллекции статистик. + /// Ф-ция возвращает экземпляр потока с уже загруженным файлом для последующего посимвольного чтения. /// - /// Коллекция со статистиками вхождения букв/пар - /// Тип букв для анализа - private static void RemoveCharStatsByType(IList letters, CharType charType) + /// Полный путь до файла для чтения + /// Поток для последующего чтения. + private static IReadOnlyStream GetInputStream(string fileFullPath) { - // TODO : Удалить статистику по запрошенному типу букв. - switch (charType) - { - case CharType.Consonants: - break; - case CharType.Vowel: - break; - } - + return new ReadOnlyStream(fileFullPath); } /// @@ -109,21 +97,11 @@ private static void RemoveCharStatsByType(IList letters, CharType c /// В конце отдельная строчка с ИТОГО, содержащая в себе общее кол-во найденных букв/пар /// /// Коллекция со статистикой - private static void PrintStatistic(IEnumerable letters) + public static void PrintStatistic(IEnumerable letters) { - // TODO : Выводить на экран статистику. Выводить предварительно отсортировав по алфавиту! - throw new NotImplementedException(); + // TODO : Выводить на экран статистику. Выводить предварительно отсортировав по алфавиту! (+) + var report = HelperPrintStat.BuildReport(letters); + HelperMain.PrintToConsole(report); } - - /// - /// Метод увеличивает счётчик вхождений по переданной структуре. - /// - /// - private static void IncStatistic(LetterStats letterStats) - { - letterStats.Count++; - } - - } } diff --git a/TestTask/ReadOnlyStream.cs b/TestTask/ReadOnlyStream.cs index a51a61e..41376b0 100644 --- a/TestTask/ReadOnlyStream.cs +++ b/TestTask/ReadOnlyStream.cs @@ -1,33 +1,51 @@ using System; using System.IO; +using System.Text; namespace TestTask { public class ReadOnlyStream : IReadOnlyStream { - private Stream _localStream; + private const string __errorNoMoreSymbols = "Нет символов для чтения"; + private const string __errorStreamSeekNotSupport = "Файловый поток не поддерживает перемещение"; + private const string __errorFileOpen = "Ошибка открытия файла '{0}'."; + private const string __errorNullStream = "Ошибка создания потока."; + + private readonly Stream _localStream; + private bool _disposed = false; + + /// + /// Кодировка для чтения символа из файла. + /// + public Encoding Encoding { get; } + + /// + /// Флаг окончания файла. + /// + // TODO : Заполнять данный флаг при достижении конца файла/стрима при чтении (+) + public bool IsEof => _localStream.Position >= _localStream.Length; /// /// Конструктор класса. - /// Т.к. происходит прямая работа с файлом, необходимо - /// обеспечить ГАРАНТИРОВАННОЕ закрытие файла после окончания работы с таковым! /// /// Полный путь до файла для чтения - public ReadOnlyStream(string fileFullPath) + public ReadOnlyStream(string fileFullPath, Encoding encoding = default) { - IsEof = true; - - // TODO : Заменить на создание реального стрима для чтения файла! - _localStream = null; + Encoding = DefineEncoding(encoding); + // TODO : Заменить на создание реального стрима для чтения файла! (+) + _localStream = CreateStream(fileFullPath); + CheckStream(); } - + /// - /// Флаг окончания файла. + /// Метод открывает Stream по имени файла (можно переопределять). /// - public bool IsEof + /// Полный путь до файла для чтения + /// Открытый поток. + public virtual Stream CreateStream(string param) { - get; // TODO : Заполнять данный флаг при достижении конца файла/стрима при чтении - private set; + CheckPath(param); + return TryOpenStream(param); } /// @@ -38,8 +56,13 @@ public bool IsEof /// Считанный символ. public char ReadNextChar() { - // TODO : Необходимо считать очередной символ из _localStream - throw new NotImplementedException(); + CheckDisposed(); + // TODO : Необходимо считать очередной символ из _localStream (+) + byte[] buffer = new byte[sizeof(char)]; + int bytesRead = _localStream.Read(buffer, 0, sizeof(char)); + if (bytesRead != sizeof(char)) + throw new EndOfStreamException(__errorNoMoreSymbols); + return Encoding.GetChars(buffer)[0]; } /// @@ -47,14 +70,77 @@ public char ReadNextChar() /// public void ResetPositionToStart() { - if (_localStream == null) + if (!_localStream.CanSeek) + throw new InvalidOperationException(__errorStreamSeekNotSupport); + + _localStream.Position = 0; + } + + /// + /// Метод для очистки неуправляемых ресурсов. + /// + public void Dispose() + { + if (!_disposed) { - IsEof = true; - return; + _localStream.Dispose(); + _disposed = true; } + } - _localStream.Position = 0; - IsEof = false; + /// + /// Метод выбрасывает исключение, если ресурс уже освобожден. + /// + private void CheckDisposed() + { + if (_disposed) + throw new ObjectDisposedException(GetType().Name); } + + /// + /// Метод проверяет внутренний стрим на null, выбрасывает исключение. + /// + private void CheckStream() + { + if (_localStream == null || _localStream == Stream.Null) + throw new ArgumentNullException(nameof(Stream), __errorNullStream); + } + + /// + /// Метод пытается открыть файловый поток, выбрасывает исключение при невозможности. + /// + /// Полный путь до файла для чтения. + /// Открытый файловый поток. + private static Stream TryOpenStream(string fileFullPath) + { + try + { + return File.OpenRead(fileFullPath); + } + catch (IOException ex) + { + throw new IOException(string.Format(__errorFileOpen, fileFullPath), ex); + } + } + + /// + /// Метод проверяет путь к файлу на null и пустоту. + /// + /// Полный путь до файла + private static void CheckPath(string fileFullPath) + { + if (string.IsNullOrWhiteSpace(fileFullPath)) + throw new ArgumentNullException(nameof(fileFullPath)); + } + + /// + /// Метод проверяет Encoding на null, возвращает UTF8 при положительной проверке. + /// + /// Задаваемая кодировка + /// Кодировка Encoding. + private static Encoding DefineEncoding(Encoding encoding) => + encoding == default + ? Encoding.UTF8 + : encoding; } } diff --git a/TestTask/TestTask.csproj b/TestTask/TestTask.csproj index dc470b0..e964224 100644 --- a/TestTask/TestTask.csproj +++ b/TestTask/TestTask.csproj @@ -8,12 +8,13 @@ Exe TestTask TestTask - v4.6.1 + v4.8 512 true + - AnyCPU + x86 true full false @@ -21,6 +22,7 @@ DEBUG;TRACE prompt 4 + false AnyCPU @@ -43,11 +45,18 @@ + + + + + + + \ No newline at end of file diff --git a/TestTask/TestTask.sln b/TestTask/TestTask.sln index 494bc7e..3540f1b 100644 --- a/TestTask/TestTask.sln +++ b/TestTask/TestTask.sln @@ -1,10 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2011 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36429.23 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}") = "TestTask.UnitTests", "..\TestTask.UnitTests\TestTask.UnitTests.csproj", "{99A1B5E9-B57D-4591-B8DF-0E6796C45D4C}" + ProjectSection(ProjectDependencies) = postProject + {6E9922F2-AF75-4807-B6E4-F74499317C39} = {6E9922F2-AF75-4807-B6E4-F74499317C39} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +20,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 + {99A1B5E9-B57D-4591-B8DF-0E6796C45D4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99A1B5E9-B57D-4591-B8DF-0E6796C45D4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99A1B5E9-B57D-4591-B8DF-0E6796C45D4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99A1B5E9-B57D-4591-B8DF-0E6796C45D4C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TestTask/app.config b/TestTask/app.config new file mode 100644 index 0000000..3e0e37c --- /dev/null +++ b/TestTask/app.config @@ -0,0 +1,3 @@ + + + diff --git a/_assets/TestTask.zip b/_assets/TestTask.zip new file mode 100644 index 0000000..1c090de Binary files /dev/null and b/_assets/TestTask.zip differ diff --git a/_assets/readme.md b/_assets/readme.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/_assets/readme.md @@ -0,0 +1 @@ +