diff --git a/.gitignore b/.gitignore index 445484b62..155692fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -197,3 +197,4 @@ FakesAssemblies/ *.opt *Solved.cs +/TagCloud/Properties diff --git a/TagCloud.Tests/CloudLayouterPaintersTests/CloudLayouterPainterTests.cs b/TagCloud.Tests/CloudLayouterPaintersTests/CloudLayouterPainterTests.cs new file mode 100644 index 000000000..acbfeb89f --- /dev/null +++ b/TagCloud.Tests/CloudLayouterPaintersTests/CloudLayouterPainterTests.cs @@ -0,0 +1,66 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using TagCloud.CloudLayouterPainters; + +namespace TagCloud.Tests.CloudLayouterPaintersTests +{ + [TestFixture] + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal class CloudLayouterPainterTests + { + private static readonly TestCaseData[] invalidTestCases = new TestCaseData[] + { + new TestCaseData( + "Список тегов пуст", + new List()) + .SetArgDisplayNames("EmptyTags"), + new TestCaseData( + "Tags передан как null", + null) + .SetArgDisplayNames("TagsAsNull"), + new TestCaseData( + "Все прямоугольники не помещаются на изображение", + new Tag[] + { + new Tag( + "Test", + new Rectangle(new Point(0, 0), new Size(100, 100))) + }) + .SetArgDisplayNames("TooSmallToFitImage"), + }; + + [TestCaseSource(nameof(invalidTestCases))] + public void Draw_ThrowsException_WithInvalidCases(string errorMessage, IList tags) + { + var painter = new CloudLayouterPainter(new Size(1, 1)); + var expected = Result.Fail(errorMessage); + var actual = painter.Draw(tags); + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Draw_ReturnsBitmapWithCorrectSize() + { + var size = new Size(1000, 1000); + var tags = new List + { + new Tag( + "Test", + new Rectangle( + new Point(size.Width / 2, size.Height / 2), + new Size(size.Width / 10, size.Height / 10))) + }; + + var painter = new CloudLayouterPainter(size); + var result = painter.Draw(tags); + + result.IsSuccess.Should().BeTrue(); + result.GetValueOrThrow().Size.Should().Be(size); + } + } +} \ No newline at end of file diff --git a/TagCloud.Tests/CloudLayouterWorkersTests/NormalizedFrequencyBasedCloudLayouterWorkerTests.cs b/TagCloud.Tests/CloudLayouterWorkersTests/NormalizedFrequencyBasedCloudLayouterWorkerTests.cs new file mode 100644 index 000000000..1ea6b3cf7 --- /dev/null +++ b/TagCloud.Tests/CloudLayouterWorkersTests/NormalizedFrequencyBasedCloudLayouterWorkerTests.cs @@ -0,0 +1,71 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Drawing; +using TagCloud.CloudLayouterWorkers; + +namespace TagCloud.Tests.CloudLayouterWorkersTests +{ + [TestFixture] + internal class NormalizedFrequencyBasedCloudLayouterWorkerTests + { + private readonly Dictionary normalizedValues + = new Dictionary + { + { "three", 0.625 }, + { "one", 0.25 }, + { "two", 0.2917 }, + { "four", 1.0 }, + }; + + [TestCase(0, 100)] + [TestCase(-1, 100)] + [TestCase(100, 0)] + [TestCase(100, -1)] + public void GetNextRectangleSize_ThrowsException_OnAnyNegativeOrZeroSize( + int width, + int height) + { + var expected = Result + .Fail> + ($"Переданное числовое значение должно быть больше 0: \"{(width <= 0 ? width : height)}\""); + + var worker = new NormalizedFrequencyBasedCloudLayouterWorker( + width, + height, + normalizedValues); + var actual = worker.GetNextRectangleProperties(); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase(100, 25, false)] + [TestCase(100, 25, true)] + public void GetNextRectangleSize_WorksCorrectly(int width, int height, bool isSortedOrder) + { + var index = 0; + string[]? keys = null; + if (isSortedOrder) + { + keys = normalizedValues + .OrderByDescending(x => x.Value).Select(x => x.Key).ToArray(); + } + else + { + keys = normalizedValues.Keys.ToArray(); + } + + var worker = new NormalizedFrequencyBasedCloudLayouterWorker( + width, + height, + normalizedValues, + isSortedOrder); + foreach (var rectangleSize in worker + .GetNextRectangleProperties().GetValueOrThrow()) + { + var currentValue = normalizedValues[keys[index]]; + var expected = new Size((int)(currentValue * width), (int)(currentValue * height)); + index += 1; + rectangleSize.size.Should().BeEquivalentTo(expected); + } + } + } +} diff --git a/TagCloud.Tests/CloudLayouterWorkersTests/RandomCloudLayouterWorkerTests.cs b/TagCloud.Tests/CloudLayouterWorkersTests/RandomCloudLayouterWorkerTests.cs new file mode 100644 index 000000000..37e2b3f3e --- /dev/null +++ b/TagCloud.Tests/CloudLayouterWorkersTests/RandomCloudLayouterWorkerTests.cs @@ -0,0 +1,45 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Drawing; +using TagCloud.CloudLayouterWorkers; + +namespace TagCloud.Tests.CloudLayouterWorkersTests +{ + [TestFixture] + internal class CircularCloudLayouterWorkerTests + { + [TestCase(0, 100)] + [TestCase(-1, 100)] + [TestCase(100, 0)] + [TestCase(100, -1)] + public void GetNextRectangleSize_ThrowsException_OnAnyNegativeOrZeroSize( + int width, + int height) + { + var expected = Result + .Fail> + ($"Переданное числовое значение должно быть больше 0: \"{(width <= 0 ? width : height)}\""); + + var worker = new RandomCloudLayouterWorker(width, width, height, height); + var actual = worker.GetNextRectangleProperties(); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase(50, 25, 25, 50)] + [TestCase(25, 50, 50, 25)] + public void GetNextRectangleSize_ThrowsException_OnNonConsecutiveSizeValues( + int minWidth, + int maxWidth, + int minHeight, + int maxHeight) + { + var expected = Result + .Fail> + ("Минимальное значение не может быть больше максимального"); + + var worker = new RandomCloudLayouterWorker(minWidth, maxWidth, minHeight, maxHeight); + var actual = worker.GetNextRectangleProperties(); + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/CloudLayoutersTests/CircularCloudLayouterTests/CircularCloudLayouterTests.cs b/TagCloud.Tests/CloudLayoutersTests/CircularCloudLayouterTests/CircularCloudLayouterTests.cs new file mode 100644 index 000000000..1c3822495 --- /dev/null +++ b/TagCloud.Tests/CloudLayoutersTests/CircularCloudLayouterTests/CircularCloudLayouterTests.cs @@ -0,0 +1,194 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using TagCloud.CloudLayouterPainters; +using TagCloud.CloudLayouters.CircularCloudLayouter; +using TagCloud.ImageSavers; +using TagCloud.Tests.Extensions; +using TagCloud.Tests.Utilities; + +namespace TagCloud.Tests.CloudLayoutersTests.CircularCloudLayouterTests +{ + [TestFixture] + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal class CircularCloudLayouterTests + { + private RectangleSetupper? rectangleSetupper; + private Point center = new Point(); + private readonly string failedTestsDirectory = "FailedTest"; + + private readonly ImageSaver imageSaver = new ImageSaver(); + private readonly CloudLayouterPainter cloudLayouterPainter + = new CloudLayouterPainter(new Size(3000, 3000)); + + [OneTimeSetUp] + public void Init() + => Directory.CreateDirectory(failedTestsDirectory); + + [Test] + [Repeat(10)] + public void ShouldPlaceRectanglesInCircle() + { + rectangleSetupper = new RectangleSetupper(); + var rectangles = rectangleSetupper.Rectangles(); + var expectedCoverageRatio = 0.7; + var gridSize = 1000; + + var maxRadius = rectangles.Max( + x => x.GetMaxDistanceFromPointToRectangleAngles(center)); + var step = 2 * maxRadius / gridSize; + + var occupancyGrid = GetOccupancyGrid(gridSize, maxRadius, step, rectangles); + + var actualCoverageRatio = GetOccupancyGridRatio(occupancyGrid, maxRadius, step); + actualCoverageRatio.Should().BeGreaterThanOrEqualTo(expectedCoverageRatio); + } + + [Test] + [Repeat(10)] + public void ShouldPlaceCenterOfMassOfRectanglesNearCenter() + { + rectangleSetupper = new RectangleSetupper(); + var rectangles = rectangleSetupper.Rectangles(); + var tolerance = 15; + + var centerX = rectangles.Average(r => r.Left + r.Width / 2.0); + var centerY = rectangles.Average(r => r.Top + r.Height / 2.0); + var actualCenter = new Point((int)centerX, (int)centerY); + + var distance = Math.Sqrt(Math.Pow(actualCenter.X - center.X, 2) + + Math.Pow(actualCenter.Y - center.Y, 2)); + + distance.Should().BeLessThanOrEqualTo(tolerance); + } + + [Test] + [Repeat(10)] + public void ShouldPlaceRectanglesWithoutOverlap() + { + rectangleSetupper = new RectangleSetupper(); + var rectangles = rectangleSetupper.Rectangles(); + for (var i = 0; i < rectangles.Length; i++) + { + for (var j = i + 1; j < rectangles.Length; j++) + { + Assert.That( + rectangles[i].IntersectsWith(rectangles[j]), + Is.EqualTo(false), + $"Прямоугольники пересекаются:\n" + + $"{rectangles[i]}\n" + + $"{rectangles[j]}"); + } + } + } + + [TestCase(0, 100)] + [TestCase(-1, 100)] + [TestCase(100, 0)] + [TestCase(100, -1)] + public void PutNextRectangle_ThrowsException_OnAnyNegativeOrZeroSize( + int width, + int height) + { + rectangleSetupper = null; + var size = new Size(width, height); + var expected = Result.Fail( + "Размеры прямоугольника не могут быть меньше либо равны нуля"); + var actual = new CircularCloudLayouter().PutNextRectangle(size); + actual.Should().BeEquivalentTo(expected); + } + + [TearDown] + public void Cleanup() + { + if (TestContext.CurrentContext.Result.FailCount == 0 + || rectangleSetupper is null) + { + return; + } + + var name = $"{TestContext.CurrentContext.Test.Name}.png"; + var path = Path.Combine(failedTestsDirectory, name); + imageSaver.SaveFile( + cloudLayouterPainter.Draw(rectangleSetupper.Tags).GetValueOrThrow(), path); + Console.WriteLine($"Tag cloud visualization saved to file {path}"); + } + + [OneTimeTearDown] + public void OneTimeCleanup() + { + if (Directory.Exists(failedTestsDirectory) + && Directory.GetFiles(failedTestsDirectory).Length == 0) + { + Directory.Delete(failedTestsDirectory); + } + } + + private (int start, int end) GetGridIndexesInterval( + int rectangleStartValue, + int rectangleCorrespondingSize, + double maxRadius, + double step) + { + var start = (int)((rectangleStartValue - center.X + maxRadius) / step); + var end = (int)((rectangleStartValue + + rectangleCorrespondingSize - center.X + maxRadius) / step); + return (start, end); + } + + private bool[,] GetOccupancyGrid( + int gridSize, + double maxRadius, + double step, + Rectangle[] rectangles) + { + var result = new bool[gridSize, gridSize]; + foreach (var rect in rectangles) + { + var xInterval = GetGridIndexesInterval(rect.X, rect.Width, maxRadius, step); + var yInterval = GetGridIndexesInterval(rect.Y, rect.Height, maxRadius, step); + for (var x = xInterval.start; x <= xInterval.end; x++) + { + for (var y = yInterval.start; y <= yInterval.end; y++) + { + result[x, y] = true; + } + } + } + return result; + } + + private double GetOccupancyGridRatio(bool[,] occupancyGrid, double maxRadius, double step) + { + var totalCellsInsideCircle = 0; + var coveredCellsInsideCircle = 0; + for (var x = 0; x < occupancyGrid.GetLength(0); x++) + { + for (var y = 0; y < occupancyGrid.GetLength(0); y++) + { + var cellCenterX = x * step - maxRadius + center.X; + var cellCenterY = y * step - maxRadius + center.Y; + + var distance = Math.Sqrt( + Math.Pow(cellCenterX - center.X, 2) + Math.Pow(cellCenterY - center.Y, 2)); + + if (distance > maxRadius) + { + continue; + } + + totalCellsInsideCircle += 1; + if (occupancyGrid[x, y]) + { + coveredCellsInsideCircle += 1; + } + } + } + return (double)coveredCellsInsideCircle / totalCellsInsideCircle; + } + } +} diff --git a/TagCloud.Tests/Extensions/RectangleExtensions.cs b/TagCloud.Tests/Extensions/RectangleExtensions.cs new file mode 100644 index 000000000..d4c8aa55f --- /dev/null +++ b/TagCloud.Tests/Extensions/RectangleExtensions.cs @@ -0,0 +1,20 @@ +using System.Drawing; + +namespace TagCloud.Tests.Extensions +{ + internal static class RectangleExtensions + { + public static double GetMaxDistanceFromPointToRectangleAngles( + this Rectangle rectangle, + Point point) + { + var dx = Math.Max( + Math.Abs(rectangle.X - point.X), + Math.Abs(rectangle.X + rectangle.Width - point.X)); + var dy = Math.Max( + Math.Abs(rectangle.Y - point.Y), + Math.Abs(rectangle.Y + rectangle.Height - point.Y)); + return Math.Sqrt(dx * dx + dy * dy); + } + } +} diff --git a/TagCloud.Tests/FullProgramExecutionTests.cs b/TagCloud.Tests/FullProgramExecutionTests.cs new file mode 100644 index 000000000..5667fbabd --- /dev/null +++ b/TagCloud.Tests/FullProgramExecutionTests.cs @@ -0,0 +1,60 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; +using TagCloud.Tests.OptionsTests; +using TagCloud.Tests.Utilities; +namespace TagCloud.Tests +{ + [TestFixture] + internal class FullProgramExecutionTests() : BaseOptionTest("FullProgramExecutionTest") + { + [Test] + public void Program_ExecutesSuccessfully_WithValidArguments() + { + var expected = Result.Ok(); + + var wordsToIncludePath = Path.Combine(directoryPath, "ToInclude.txt"); + FileUtilities.CreateDataFile( + directoryPath, + wordsToIncludePath, + new string[] + { + "snow", + "white" + }); + + var wordsToExcludePath = Path.Combine(directoryPath, "ToExclude.txt"); + FileUtilities.CreateDataFile( + directoryPath, + wordsToExcludePath, + new string[] + { + "the" + }); + + var options = new CommandLineOptions + { + BackgroundColor = "Black", + TextColor = "Yellow", + Font = "Calibri", + IsSorted = Boolean.FalseString, + ImageSize = "1000:1000", + MaxRectangleHeight = "100", + MaxRectangleWidth = "200", + ImageFileName = imageFile, + DataFileName = dataFile, + ResultFormat = "bmp", + WordsToIncludeFileName = wordsToIncludePath, + WordsToExcludeFileName = wordsToExcludePath, + }; + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + File.Exists($"{imageFile}.{options.ResultFormat}").Should().BeTrue(); + } + } +} diff --git a/TagCloud.Tests/ImageSaversTests/ImageSaverTests.cs b/TagCloud.Tests/ImageSaversTests/ImageSaverTests.cs new file mode 100644 index 000000000..cc0c4dff4 --- /dev/null +++ b/TagCloud.Tests/ImageSaversTests/ImageSaverTests.cs @@ -0,0 +1,84 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using TagCloud.ImageSavers; +using TagCloud.Tests.Utilities; + +namespace TagCloud.Tests.ImageSaversTests +{ + [TestFixture] + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal class ImageSaverTests + { + private readonly string directoryPath = "TempFilesForImageSaverTests"; + + [OneTimeSetUp] + public void Init() + { + Directory.CreateDirectory(directoryPath); + } + + [Test] + public void SaveFile_ThrowsException_WithNullBitmap() + { + var imageSaver = new ImageSaver(); + var path = Path.Combine(directoryPath, "Test"); + var expected = Result.Fail("Передаваемое изображение не должно быть null"); + var actual = imageSaver.SaveFile(null!, path); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(null)] + public void SaveFile_ThrowsException_WithInvalidFilename(string? filename) + { + var imageSaver = new ImageSaver(); + var dummyImage = new Bitmap(1, 1); + var expected = Result.Fail($"Некорректное имя файла для создания \"{filename}\""); + var actual = imageSaver.SaveFile(dummyImage, filename!); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("abc")] + public void SaveFile_ThrowsException_WithInvalidFormat(string? format) + { + var imageSaver = new ImageSaver(); + var dummyImage = new Bitmap(1, 1); + var filename = "Test"; + var expected = Result.Fail($"Формат \"{format}\" не поддерживается"); + var actual = imageSaver.SaveFile(dummyImage, filename, format!); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("Test", "png", ExpectedResult = true)] + [TestCase("Test", "jpg", ExpectedResult = true)] + [TestCase("Test", "jpeg", ExpectedResult = true)] + [TestCase("Test", "bmp", ExpectedResult = true)] + [TestCase("Test", "gif", ExpectedResult = true)] + [TestCase("Test", "tiff", ExpectedResult = true)] + public bool SaveFile_SavesFile(string filename, string format) + { + var imageSaver = new ImageSaver(); + var dummyImage = new Bitmap(1, 1); + var path = Path.Combine(directoryPath, filename); + + File.Delete($"{path}.{format}"); + imageSaver.SaveFile(dummyImage, path, format); + return File.Exists($"{path}.{format}"); + } + + [OneTimeTearDown] + public void OneTimeCleanup() + { + FileUtilities.DeleteDirectory(directoryPath); + } + } +} diff --git a/TagCloud.Tests/NormalizersTests/NormalizerTests.cs b/TagCloud.Tests/NormalizersTests/NormalizerTests.cs new file mode 100644 index 000000000..d925f5f57 --- /dev/null +++ b/TagCloud.Tests/NormalizersTests/NormalizerTests.cs @@ -0,0 +1,82 @@ +using FileSenderRailway; +using FluentAssertions; +using TagCloud.Normalizers; + +namespace TagCloud.Tests.NormalizersTests +{ + [TestFixture] + internal class NormalizerTests + { + private readonly Normalizer normalizer = new Normalizer(); + private readonly Dictionary values = new Dictionary + { + { "one", 14 }, + { "two", 15 }, + { "three", 23 }, + { "four", 32 }, + }; + private readonly Dictionary expectedResult = new Dictionary + { + { "one", 0.25 }, + { "two",0.29166666666666669 }, + { "three", 0.625 }, + { "four", 1.0 }, + + }; + private readonly int defaultDecimalPlaces = 4; + private readonly double defaultMinCoefficient = 0.25; + + [TestCase(-0.1)] + [TestCase(1.1)] + public void Normalize_ThrowsException_WithInvalidMinCoefficient( + double minCoefficient) + { + var expected = Result.Fail>( + "Минимальный коэффициент нормализации должен быть в диапазоне от 0 до 1"); + var actual = normalizer.Normalize(values, minCoefficient, defaultDecimalPlaces); + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Normalize_ThrowsException_WithEmptyValues() + { + var expected = Result.Fail>( + "Словарь значений не может быть пустым"); + var actual = normalizer.Normalize( + new Dictionary(), defaultMinCoefficient, defaultDecimalPlaces); + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Normalize_ThrowsException_WithValuesAsNull() + { + var expected = Result.Fail>( + "Словарь значений не может быть пустым"); + var actual = normalizer.Normalize(null!, defaultMinCoefficient, defaultDecimalPlaces); + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Normalize_ThrowsException_WithInvalidDecimalPlaces() + { + var expected = Result.Fail>( + "Количество знаков после запятой не может быть отрицательным"); + var actual = normalizer.Normalize(values, defaultMinCoefficient, -1); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase(4)] + [TestCase(2)] + public void Normalize_CalculatesСorrectly(int decimalPlaces) + { + var dict = new Dictionary(); + foreach (var pair in expectedResult) + { + dict[pair.Key] = Math.Round(pair.Value, decimalPlaces); + } + var expected = dict.AsResult(); + var actual = normalizer.Normalize(values, defaultMinCoefficient, decimalPlaces); + actual.GetValueOrThrow().Should().BeEquivalentTo(expected.GetValueOrThrow()); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/BackgroundColorTests.cs b/TagCloud.Tests/OptionsTests/BackgroundColorTests.cs new file mode 100644 index 000000000..86324b81f --- /dev/null +++ b/TagCloud.Tests/OptionsTests/BackgroundColorTests.cs @@ -0,0 +1,27 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class BackgroundColorTests() : BaseOptionTest("BackgroundColor") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void Program_WorksCorrectly_WithInvalidBackgroundColor(string backgroundColor) + { + options.BackgroundColor = backgroundColor; + var expected = Result.Fail($"Неизвестный цвет \"{backgroundColor}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/BaseOptionTest.cs b/TagCloud.Tests/OptionsTests/BaseOptionTest.cs new file mode 100644 index 000000000..8f73e82ea --- /dev/null +++ b/TagCloud.Tests/OptionsTests/BaseOptionTest.cs @@ -0,0 +1,36 @@ +using TagCloud.Tests.Utilities; + +namespace TagCloud.Tests.OptionsTests +{ + internal abstract class BaseOptionTest + { + protected readonly string directoryPath; + protected readonly string dataFile; + protected readonly string imageFile; + protected readonly CommandLineOptions options; + + protected BaseOptionTest(string testName) + { + directoryPath = $"TempFilesFor{testName}Tests"; + dataFile = Path.Combine(directoryPath, "TestData.txt"); + imageFile = Path.Combine(directoryPath, "Test"); + + options = new CommandLineOptions + { + DataFileName = dataFile, + ImageFileName = imageFile, + }; + } + + [OneTimeSetUp] + protected void Init() + => FileUtilities.CreateDataFile( + directoryPath, + dataFile, + ValidValues.ValidDataFileContent); + + [OneTimeTearDown] + protected void OneTimeCleanup() + => FileUtilities.DeleteDirectory(directoryPath); + } +} diff --git a/TagCloud.Tests/OptionsTests/DataFileNameTests.cs b/TagCloud.Tests/OptionsTests/DataFileNameTests.cs new file mode 100644 index 000000000..5cf971a84 --- /dev/null +++ b/TagCloud.Tests/OptionsTests/DataFileNameTests.cs @@ -0,0 +1,88 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; +using TagCloud.Tests.Utilities; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class DataFileNameTests() : BaseOptionTest("DataFileName") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("NonExistingFile.txt")] + public void Program_WorksCorrectly_WithNonExistingFilename(string dataFileName) + { + options.DataFileName = dataFileName; + var expected = Result.Fail($"Файл \"{dataFileName}\" не существует"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Program_WorksCorrectly_WithFileWithMoreThanOneWordInLine() + { + var path = Path.Combine(directoryPath, "InvalidFile_MoreThanOneWordInLine.txt"); + var invalidContent = new string[] + { + "one", + "two", + "three three three", + "four" + }; + FileUtilities.CreateDataFile(directoryPath, path, invalidContent); + + options.DataFileName = path; + var expected = Result.Fail( + $"Файл \"{path}\" содержит строку с двумя и более словами"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Program_WorksCorrectly_WithEmptyFile() + { + var path = Path.Combine(directoryPath, "InvalidFile_MoreThanOneWordInLine.txt"); + FileUtilities.CreateDataFile(directoryPath, path, Array.Empty()); + + options.DataFileName = path; + var expected = Result.Fail($"Файл \"{path}\" пустой"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("FileDoc.doc")] + [TestCase("FileImg.png")] + public void Program_WorksCorrectly_WithNonTxtFile(string dataFileName) + { + var path = Path.Combine(directoryPath, dataFileName); + FileUtilities.CreateDataFile(directoryPath, path, Array.Empty()); + + options.DataFileName = path; + var expected = Result.Fail($"Файл \"{path}\" должен иметь расширение \"txt\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/FontTests.cs b/TagCloud.Tests/OptionsTests/FontTests.cs new file mode 100644 index 000000000..64101ee16 --- /dev/null +++ b/TagCloud.Tests/OptionsTests/FontTests.cs @@ -0,0 +1,27 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class FontTests() : BaseOptionTest("Font") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void Program_WorksCorrectly_WithInvalidFont(string font) + { + options.Font = font; + var expected = Result.Fail($"Неизвестный шрифт \"{font}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/ImageFileNameTests.cs b/TagCloud.Tests/OptionsTests/ImageFileNameTests.cs new file mode 100644 index 000000000..353e842a4 --- /dev/null +++ b/TagCloud.Tests/OptionsTests/ImageFileNameTests.cs @@ -0,0 +1,27 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class ImageFileNameTests() : BaseOptionTest("ImageFileName") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + public void Program_WorksCorrectly_WithInvalidImageFileName(string imageFileName) + { + options.ImageFileName = imageFileName; + var expected = Result.Fail( + $"Некорректное имя файла для создания \"{imageFileName}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/ImageSizeTests.cs b/TagCloud.Tests/OptionsTests/ImageSizeTests.cs new file mode 100644 index 000000000..deb1f853f --- /dev/null +++ b/TagCloud.Tests/OptionsTests/ImageSizeTests.cs @@ -0,0 +1,67 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class ImageSizeTests() : BaseOptionTest("ImageSize") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("100:")] + [TestCase(":100")] + [TestCase("100:100:100")] + [TestCase("abc")] + public void Program_WorksCorrectly_WithIncorrectFormat(string size) + { + options.ImageSize = size; + var expected = Result.Fail( + $"Некорректный формат размера изображения \"{size}\", используйте формат \"Ширина:Высота\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("abc:100")] + [TestCase("100:abc")] + [TestCase("abc:abc")] + public void Program_WorksCorrectly_WithIncorrectInput(string size) + { + options.ImageSize = size; + var expected = Result.Fail($"Передано не число \"abc\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("0:100")] + [TestCase("-1:100")] + [TestCase("100:0")] + [TestCase("100:-1")] + [TestCase("0:0")] + [TestCase("-1:-1")] + public void Program_WorksCorrectly_WithInputLessThanZero(string size) + { + options.ImageSize = size; + var wrongValue = size.Contains("-1") ? "-1" : "0"; + var expected = Result.Fail($"Переданное числовое значение должно быть больше 0: \"{wrongValue}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/IsSortedTests.cs b/TagCloud.Tests/OptionsTests/IsSortedTests.cs new file mode 100644 index 000000000..abc16c114 --- /dev/null +++ b/TagCloud.Tests/OptionsTests/IsSortedTests.cs @@ -0,0 +1,27 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class IsSortedTests() : BaseOptionTest("IsSorted") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void Program_WorksCorrectly_WithInvalidIsSorted(string sorted) + { + options.IsSorted = sorted; + var expected = Result.Fail($"Неизвестный параметр сортировки \"{sorted}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/MaxRectangleHeightTests.cs b/TagCloud.Tests/OptionsTests/MaxRectangleHeightTests.cs new file mode 100644 index 000000000..062b109ed --- /dev/null +++ b/TagCloud.Tests/OptionsTests/MaxRectangleHeightTests.cs @@ -0,0 +1,43 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class MaxRectangleHeightTests() : BaseOptionTest("MaxRectangleHeight") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void Program_WorksCorrectly_WithNotANumber(string maxRectangleHeight) + { + options.MaxRectangleWidth = maxRectangleHeight; + var expected = Result.Fail($"Передано не число \"{maxRectangleHeight}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("0")] + [TestCase("-1")] + public void Program_WorksCorrectly_WithNumberZeroOrLess(string maxRectangleHeight) + { + options.MaxRectangleWidth = maxRectangleHeight; + var expected = Result.Fail( + $"Переданное числовое значение должно быть больше 0: \"{maxRectangleHeight}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/MaxRectangleWidthTests.cs b/TagCloud.Tests/OptionsTests/MaxRectangleWidthTests.cs new file mode 100644 index 000000000..380eb857d --- /dev/null +++ b/TagCloud.Tests/OptionsTests/MaxRectangleWidthTests.cs @@ -0,0 +1,43 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class MaxRectangleWidthTests() : BaseOptionTest("MaxRectangleWidth") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void Program_WorksCorrectly_WithNotANumber(string maxRectangleWidth) + { + options.MaxRectangleWidth = maxRectangleWidth; + var expected = Result.Fail($"Передано не число \"{maxRectangleWidth}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("0")] + [TestCase("-1")] + public void Program_WorksCorrectly_WithNumberZeroOrLess(string maxRectangleWidth) + { + options.MaxRectangleWidth = maxRectangleWidth; + var expected = Result.Fail( + $"Переданное числовое значение должно быть больше 0: \"{maxRectangleWidth}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/ResultFormatTests.cs b/TagCloud.Tests/OptionsTests/ResultFormatTests.cs new file mode 100644 index 000000000..b5a2682bc --- /dev/null +++ b/TagCloud.Tests/OptionsTests/ResultFormatTests.cs @@ -0,0 +1,27 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class ResultFormatTests() : BaseOptionTest("ResultFormat") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void Program_WorksCorrectly_WithInvalidResultFormat(string resultFormat) + { + options.ResultFormat = resultFormat; + var expected = Result.Fail($"Формат \"{resultFormat}\" не поддерживается"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/TextColorTests.cs b/TagCloud.Tests/OptionsTests/TextColorTests.cs new file mode 100644 index 000000000..d1af34b2d --- /dev/null +++ b/TagCloud.Tests/OptionsTests/TextColorTests.cs @@ -0,0 +1,29 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class TextColorTests() : BaseOptionTest("TextColor") + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void Program_WorksCorrectly_WithInvalidTextColor(string textColor) + { + options.TextColor = textColor; + var expected = Result.Fail($"Неизвестный цвет \"{textColor}\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + + var result = executor.Execute(); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/ValidValues.cs b/TagCloud.Tests/OptionsTests/ValidValues.cs new file mode 100644 index 000000000..2dc086d39 --- /dev/null +++ b/TagCloud.Tests/OptionsTests/ValidValues.cs @@ -0,0 +1,17 @@ +namespace TagCloud.Tests.OptionsTests +{ + internal static class ValidValues + { + public static readonly string[] ValidDataFileContent = new string[] + { + "One", + "One", + "Two", + "Three", + "Four", + "Four", + "Four", + "Four" + }; + } +} diff --git a/TagCloud.Tests/OptionsTests/WordsToExcludeFileNameTests.cs b/TagCloud.Tests/OptionsTests/WordsToExcludeFileNameTests.cs new file mode 100644 index 000000000..96b45deef --- /dev/null +++ b/TagCloud.Tests/OptionsTests/WordsToExcludeFileNameTests.cs @@ -0,0 +1,86 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; +using TagCloud.Tests.Utilities; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class WordsToExcludeFileNameTests() : BaseOptionTest("WordsToExcludeFileName") + { + [Test] + public void Program_WorksCorrectly_WithNonExistingFilename() + { + var wordsToExcludeFileName = "NonExistingFile.txt"; + options.WordsToExcludeFileName = wordsToExcludeFileName; + var expected = Result.Fail($"Файл \"{wordsToExcludeFileName}\" не существует"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Program_WorksCorrectly_WithFileWithMoreThanOneWordInLine() + { + var path = Path.Combine(directoryPath, "InvalidFile_MoreThanOneWordInLine.txt"); + var invalidContent = new string[] + { + "one", + "two", + "three three three", + "four" + }; + FileUtilities.CreateDataFile(directoryPath, path, invalidContent); + + options.WordsToExcludeFileName = path; + var expected = Result.Fail( + $"Файл \"{path}\" содержит строку с двумя и более словами"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Program_WorksCorrectly_WithEmptyFile() + { + var path = Path.Combine(directoryPath, "InvalidFile_MoreThanOneWordInLine.txt"); + FileUtilities.CreateDataFile(directoryPath, path, Array.Empty()); + + options.WordsToExcludeFileName = path; + var expected = Result.Fail($"Файл \"{path}\" пустой"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("FileDoc.doc")] + [TestCase("FileImg.png")] + public void Program_WorksCorrectly_WithNonTxtFile(string wordsToExcludeFileName) + { + var path = Path.Combine(directoryPath, wordsToExcludeFileName); + FileUtilities.CreateDataFile(directoryPath, path, Array.Empty()); + + options.WordsToExcludeFileName = path; + var expected = Result.Fail($"Файл \"{path}\" должен иметь расширение \"txt\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/OptionsTests/WordsToIncludeFileNameTests.cs b/TagCloud.Tests/OptionsTests/WordsToIncludeFileNameTests.cs new file mode 100644 index 000000000..f78cd16d4 --- /dev/null +++ b/TagCloud.Tests/OptionsTests/WordsToIncludeFileNameTests.cs @@ -0,0 +1,84 @@ +using Autofac; +using FileSenderRailway; +using FluentAssertions; +using TagCloud.Tests.Utilities; + +namespace TagCloud.Tests.OptionsTests +{ + [TestFixture] + internal class WordsToIncludeFileNameTests() : BaseOptionTest("WordsToIncludeFileName") + { + [Test] + public void Program_WorksCorrectly_WithNonExistingFilename() + { + var wordsToIncludeFileName = "NonExistingFile.txt"; + options.WordsToIncludeFileName = wordsToIncludeFileName; + var expected = Result.Fail($"Файл \"{wordsToIncludeFileName}\" не существует"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + public void Program_WorksCorrectly_WithFileWithMoreThanOneWordInLine() + { + var path = Path.Combine(directoryPath, "InvalidFile_MoreThanOneWordInLine.txt"); + var invalidContent = new string[] + { + "one", + "two", + "three three three", + "four" + }; + FileUtilities.CreateDataFile(directoryPath, path, invalidContent); + + options.WordsToIncludeFileName = path; + var expected = Result.Fail($"Файл \"{path}\" содержит строку с двумя и более словами"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void Program_WorksCorrectly_WithEmptyFile() + { + var path = Path.Combine(directoryPath, "InvalidFile_MoreThanOneWordInLine.txt"); + FileUtilities.CreateDataFile(directoryPath, path, Array.Empty()); + + options.WordsToIncludeFileName = path; + var expected = Result.Fail($"Файл \"{path}\" пустой"); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("FileDoc.doc")] + [TestCase("FileImg.png")] + public void Program_WorksCorrectly_WithNonTxtFile(string wordsToIncludeFileName) + { + var path = Path.Combine(directoryPath, wordsToIncludeFileName); + FileUtilities.CreateDataFile(directoryPath, path, Array.Empty()); + + options.WordsToIncludeFileName = path; + var expected = Result.Fail($"Файл \"{path}\" должен иметь расширение \"txt\""); + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + var actual = executor.Execute(); + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/ParsersTests/BoolParserTests.cs b/TagCloud.Tests/ParsersTests/BoolParserTests.cs new file mode 100644 index 000000000..598a22eb5 --- /dev/null +++ b/TagCloud.Tests/ParsersTests/BoolParserTests.cs @@ -0,0 +1,30 @@ +using FileSenderRailway; +using FluentAssertions; +using TagCloud.Parsers; + +namespace TagCloud.Tests.ParsersTests +{ + [TestFixture] + internal class BoolParserTests + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void BoolParser_ThrowsException_WithInvalidInput(string sorted) + { + var expected = Result.Fail($"Неизвестный параметр сортировки \"{sorted}\""); + var actual = BoolParser.ParseIsSorted(sorted); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("True")] + [TestCase("False")] + public void BoolParser_WorksCorrectly(string sorted) + { + var expected = Convert.ToBoolean(sorted); + var actual = BoolParser.ParseIsSorted(sorted).GetValueOrThrow(); + actual.Should().Be(expected); + } + } +} diff --git a/TagCloud.Tests/ParsersTests/ColorParserTests.cs b/TagCloud.Tests/ParsersTests/ColorParserTests.cs new file mode 100644 index 000000000..99dac59d5 --- /dev/null +++ b/TagCloud.Tests/ParsersTests/ColorParserTests.cs @@ -0,0 +1,35 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Drawing; +using TagCloud.Parsers; + +namespace TagCloud.Tests.ParsersTests +{ + [TestFixture] + internal class ColorParserTests + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void ColorParser_ThrowsException_WithInvalidColor(string color) + { + var expected = Result.Fail($"Неизвестный цвет \"{color}\""); + var actual = ColorParser.ParseColor(color); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("red")] + [TestCase("blue")] + [TestCase("white")] + [TestCase("black")] + [TestCase("green")] + [TestCase("yellow")] + public void ColorParser_WorksCorrectly(string color) + { + var expected = Color.FromName(color); + var actual = ColorParser.ParseColor(color).GetValueOrThrow(); + actual.Should().Be(expected); + } + } +} diff --git a/TagCloud.Tests/ParsersTests/FontParserTests.cs b/TagCloud.Tests/ParsersTests/FontParserTests.cs new file mode 100644 index 000000000..70b96de16 --- /dev/null +++ b/TagCloud.Tests/ParsersTests/FontParserTests.cs @@ -0,0 +1,39 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using TagCloud.Parsers; + +namespace TagCloud.Tests.ParsersTests +{ + [TestFixture] + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal class FontParserTests + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + + public void FontParser_ThrowsException_WithInvalidFont(string font) + { + var expected = Result.Fail($"Неизвестный шрифт \"{font}\""); + var actual = FontParser.ParseFont(font); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("Arial")] + [TestCase("Times New Roman")] + [TestCase("Georgia")] + [TestCase("Verdana")] + public void FontParser_WorksCorrectly(string font) + { + var expected = new FontFamily(font); + var actual = FontParser.ParseFont(font).GetValueOrThrow(); + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/ParsersTests/SizeParserTests.cs b/TagCloud.Tests/ParsersTests/SizeParserTests.cs new file mode 100644 index 000000000..56720349e --- /dev/null +++ b/TagCloud.Tests/ParsersTests/SizeParserTests.cs @@ -0,0 +1,92 @@ +using FileSenderRailway; +using FluentAssertions; +using System.Drawing; +using TagCloud.Parsers; + +namespace TagCloud.Tests.ParsersTests +{ + [TestFixture] + internal class SizeParserTests + { + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("100:")] + [TestCase(":100")] + [TestCase("100:100:100")] + [TestCase("abc")] + public void ParseImageSize_ThrowsException_WithIncorrectFormat(string size) + { + var expected = Result.Fail( + $"Некорректный формат размера изображения \"{size}\", используйте формат \"Ширина:Высота\""); + var actual = SizeParser.ParseImageSize(size); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("abc:100")] + [TestCase("100:abc")] + [TestCase("abc:abc")] + public void ParseImageSize_ThrowsException_WithIncorrectInput(string size) + { + var expected = Result.Fail($"Передано не число \"abc\""); + var actual = SizeParser.ParseImageSize(size); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("0:100")] + [TestCase("-1:100")] + [TestCase("100:0")] + [TestCase("100:-1")] + [TestCase("0:0")] + [TestCase("-1:-1")] + public void ParseImageSize_ThrowsException_WithInputLessThanZero(string size) + { + var wrongValue = size.Contains("-1") ? "-1" : "0"; + var expected = Result.Fail($"Переданное числовое значение должно быть больше 0: \"{wrongValue}\""); + var actual = SizeParser.ParseImageSize(size); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(null!)] + [TestCase("abc")] + public void ParseSizeDimension_ThrowsException_WithIncorrectInputAsString(string size) + { + var expected = Result.Fail($"Передано не число \"{size}\""); + var actual = SizeParser.ParseSizeDimension(size); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("0")] + [TestCase("-1")] + public void ParseSizeDimension_ThrowsException_WithInputLessThanZeroAsString(string size) + { + var expected = Result.Fail($"Переданное числовое значение должно быть больше 0: \"{size}\""); + var actual = SizeParser.ParseSizeDimension(size); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("0")] + [TestCase("-1")] + public void ParseSizeDimension_ThrowsException_WithInputLessThanZeroAsInt(int size) + { + var expected = Result.Fail($"Переданное числовое значение должно быть больше 0: \"{size}\""); + var actual = SizeParser.ParseSizeDimension(size); + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void ParseImageSize_ReturnsCorrectSize() + { + var width = 125; + var height = 55; + var size = $"{width}:{height}"; + var expectedSize = new Size(width, height); + + var result = SizeParser.ParseImageSize(size); + result.IsSuccess.Should().BeTrue(); + result.GetValueOrThrow().Should().Be(expectedSize); + } + } +} diff --git a/TagCloud.Tests/TagCloud.Tests.csproj b/TagCloud.Tests/TagCloud.Tests.csproj new file mode 100644 index 000000000..391e86bc2 --- /dev/null +++ b/TagCloud.Tests/TagCloud.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/TagCloud.Tests/Utilities/FileUtilities.cs b/TagCloud.Tests/Utilities/FileUtilities.cs new file mode 100644 index 000000000..04c9f195c --- /dev/null +++ b/TagCloud.Tests/Utilities/FileUtilities.cs @@ -0,0 +1,19 @@ +namespace TagCloud.Tests.Utilities +{ + internal static class FileUtilities + { + public static void CreateDataFile(string directoryPath, string dataFile, string[] content) + { + Directory.CreateDirectory(directoryPath); + File.WriteAllLines(dataFile, content); + } + + public static void DeleteDirectory(string directoryPath) + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, true); + } + } + } +} diff --git a/TagCloud.Tests/Utilities/RectangleSetupper.cs b/TagCloud.Tests/Utilities/RectangleSetupper.cs new file mode 100644 index 000000000..2e2038103 --- /dev/null +++ b/TagCloud.Tests/Utilities/RectangleSetupper.cs @@ -0,0 +1,37 @@ +using System.Drawing; +using TagCloud.CloudLayouters.CircularCloudLayouter; +using TagCloud.CloudLayouterWorkers; + +namespace TagCloud.Tests.Utilities +{ + internal class RectangleSetupper + { + private readonly List tags = new List(); + public List Tags => tags.ToList(); + public Rectangle[] Rectangles() => tags.Select(x => x.Rectangle).ToArray(); + + public RectangleSetupper( + int minRectangleWidth = 30, + int maxRectangleWidth = 70, + int minRectangleHeight = 20, + int maxRectangleHeight = 50, + int rectanglesCount = 1000) + { + var circularCloudLayouter = new CircularCloudLayouter(); + var randomWorker = new RandomCloudLayouterWorker( + minRectangleWidth, + maxRectangleWidth, + minRectangleHeight, + maxRectangleHeight); + foreach (var rectangleProperty in randomWorker + .GetNextRectangleProperties().GetValueOrThrow().Take(rectanglesCount)) + { + tags.Add( + new Tag( + "Test", + circularCloudLayouter.PutNextRectangle(rectangleProperty.size) + .GetValueOrThrow())); + } + } + } +} diff --git a/TagCloud.Tests/WordCountersTests/WordCounterTests.cs b/TagCloud.Tests/WordCountersTests/WordCounterTests.cs new file mode 100644 index 000000000..eb12fba1d --- /dev/null +++ b/TagCloud.Tests/WordCountersTests/WordCounterTests.cs @@ -0,0 +1,28 @@ +using FluentAssertions; +using TagCloud.Tests.OptionsTests; +using TagCloud.WordCounters; + +namespace TagCloud.Tests.WordCountersTests +{ + [TestFixture] + internal class WordCounterTests + { + [Test] + public void WordCounter_CountsCorrect() + { + var wordCounter = new WordCounter(); + var expected = new Dictionary(); + foreach (var word in ValidValues.ValidDataFileContent) + { + expected.TryGetValue(word, out var count); + expected[word] = count + 1; + } + + foreach (var value in ValidValues.ValidDataFileContent) + { + wordCounter.AddWord(value); + } + wordCounter.Values.Should().BeEquivalentTo(expected); + } + } +} diff --git a/TagCloud.Tests/WordFiltersTests/BannedWordLists.cs b/TagCloud.Tests/WordFiltersTests/BannedWordLists.cs new file mode 100644 index 000000000..5dea13028 --- /dev/null +++ b/TagCloud.Tests/WordFiltersTests/BannedWordLists.cs @@ -0,0 +1,60 @@ +namespace TagCloud.Tests.WordFiltersTests +{ + internal static class BannedWordLists + { + public static string[] CustomBans = new string[] + { + "not", "also", "how", "let" + }; + + public static string[] ToHaveForms = new string[] + { + "have", "has", "had", "having", + }; + + public static string[] ToBeForms = new string[] + { + "am", "is", "are", "was", "were", "be", "been", "being", + }; + + public static string[] Articles = new string[] + { + "a", "an", "the" + }; + + public static string[] Pronouns => new string[] + { + "i", "you", "he", "she", "it", "we", "they", "me", "him", + "her", "us", "them", "my", "your", "his", "its", "our", "their", + "mine", "yours", "hers", "theirs", "myself", "yourself", "himself", + "herself", "itself", "ourselves", "yourselves", "themselves", "this", + "that", "these", "those", "who", "whom", "whose", "what", "which", + "some", "any", "none", "all", "many", "few", "several", + "everyone", "somebody", "anybody", "nobody", "everything", "anything", + "nothing", "each", "every", "either", "neither" + }; + + public static string[] Prepositions => new string[] + { + "about", "above", "across", "after", "against", "along", "amid", "among", + "around", "as", "at", "before", "behind", "below", "beneath", "beside", + "besides", "between", "beyond", "but", "by", "despite", "down", "during", + "except", "for", "from", "in", "inside", "into", "like", "near", "of", "off", + "on", "onto", "out", "outside", "over", "past", "since", "through", "throughout", + "till", "to", "toward", "under", "underneath", "until", "up", "upon", "with", + "within", "without" + }; + + public static string[] Conjunctions => new string[] + { + "and", "but", "or", "nor", "for", "yet", "so", "if", "because", "although", "though", + "since", "until", "unless", "while", "whereas", "when", "where", "before", "after" + }; + + public static string[] Interjections => new string[] + { + "o", "ah", "aha", "alas", "aw", "aye", "eh", "hmm", "huh", "hurrah", "no", "oh", "oops", + "ouch", "ow", "phew", "shh", "tsk", "ugh", "um", "wow", "yay", "yes", "yikes" + }; + } +} diff --git a/TagCloud.Tests/WordFiltersTests/WordFilterChangeBannedWordsTests.cs b/TagCloud.Tests/WordFiltersTests/WordFilterChangeBannedWordsTests.cs new file mode 100644 index 000000000..04e9f06ae --- /dev/null +++ b/TagCloud.Tests/WordFiltersTests/WordFilterChangeBannedWordsTests.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using TagCloud.WordFilters; + +namespace TagCloud.Tests.WordFiltersTests +{ + [TestFixture] + internal class WordFilterChangeBannedWordsTests + { + [Test] + public void Clear_ShouldClearBannedWordList() + { + var wordFilter = new WordFilter(); + wordFilter.Clear(); + wordFilter.BannedWords.Should().BeEmpty(); + } + + [Test] + public void Add_ShouldAddWord_ToBannedWords() + { + var wordFilter = new WordFilter(); + var wordToAdd = "WordToAdd"; + wordFilter.Clear(); + wordFilter.Add(wordToAdd); + wordFilter.BannedWords.Should().Contain(wordToAdd).And.HaveCount(1); + } + + [Test] + public void Remove_ShouldRemoveWord_InBannedWords() + { + var wordFilter = new WordFilter(); + var wordToRemove = "WordToRemove"; + wordFilter.Clear(); + wordFilter.Add(wordToRemove); + wordFilter.Remove(wordToRemove); + wordFilter.BannedWords.Should().NotContain(wordToRemove); + } + } +} diff --git a/TagCloud.Tests/WordFiltersTests/WordFilterDefaultBannedWordsTests.cs b/TagCloud.Tests/WordFiltersTests/WordFilterDefaultBannedWordsTests.cs new file mode 100644 index 000000000..8f06b4ecd --- /dev/null +++ b/TagCloud.Tests/WordFiltersTests/WordFilterDefaultBannedWordsTests.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using TagCloud.WordFilters; + +namespace TagCloud.Tests.WordFiltersTests +{ + [TestFixture] + internal class WordFilterDefaultBannedWordsTests + { + private readonly WordFilter wordFilter = new WordFilter(); + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.CustomBans))] + public void IsCorrectWord_ShouldBeFalse_WithCustomBans(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.ToHaveForms))] + public void IsCorrectWord_ShouldBeFalse_WithToHaveForms(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.ToBeForms))] + public void IsCorrectWord_ShouldBeFalse_WithToBeForms(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.Articles))] + public void IsCorrectWord_ShouldBeFalse_WithArticles(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.Pronouns))] + public void IsCorrectWord_ShouldBeFalse_WithPronouns(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.Prepositions))] + public void IsCorrectWord_ShouldBeFalse_WithPrepositions(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.Conjunctions))] + public void IsCorrectWord_ShouldBeFalse_WithConjunctions(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + + [TestCaseSource(typeof(BannedWordLists), nameof(BannedWordLists.Interjections))] + public void IsCorrectWord_ShouldBeFalse_WithInterjections(string word) + { + wordFilter.IsCorrectWord(word).Should().BeFalse(); + } + } +} diff --git a/TagCloud.Tests/WordReadersTests/WordReaderTests.cs b/TagCloud.Tests/WordReadersTests/WordReaderTests.cs new file mode 100644 index 000000000..62b369eb4 --- /dev/null +++ b/TagCloud.Tests/WordReadersTests/WordReaderTests.cs @@ -0,0 +1,109 @@ +using FileSenderRailway; +using FluentAssertions; +using TagCloud.Tests.OptionsTests; +using TagCloud.Tests.Utilities; +using TagCloud.WordReaders; + +namespace TagCloud.Tests.WordReadersTests +{ + [TestFixture] + internal class WordReaderTests + { + private readonly string directoryPath = "TempFilesForWordReaderTests"; + + private readonly string fileWithCorrectValuesPath = "CorrectFile.txt"; + + private readonly string fileWithMoreThanOneWordInLinePath + = "InvalidFile_MoreThanOneWordInLine.txt"; + private readonly string[] moreThanOneWordInLineValues = new string[] + { + "One", + "Two", + "Three Three", + "Four" + }; + + private readonly string fileEmptyPath = "InvalidFile_Empty.txt"; + + [OneTimeSetUp] + public void Init() + { + FileUtilities + .CreateDataFile( + directoryPath, + Path.Combine(directoryPath, fileWithCorrectValuesPath), + ValidValues.ValidDataFileContent); + + FileUtilities + .CreateDataFile( + directoryPath, + Path.Combine(directoryPath, fileWithMoreThanOneWordInLinePath), + moreThanOneWordInLineValues); + + FileUtilities + .CreateDataFile( + directoryPath, + Path.Combine(directoryPath, fileEmptyPath), + Array.Empty()); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase("NonExistingFile.txt")] + public void WordReader_ThrowsFileNotFoundException_WithInvalidFilename(string filename) + { + var wordReader = new WordReader(); + var path = Path.Combine(directoryPath, filename); + var expected = Result.Fail>($"Файл \"{path}\" не существует"); + var actual = wordReader.ReadByLines(path); + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void WordReader_ThrowsException_WithTwoWordsInOneLine() + { + var wordReader = new WordReader(); + var path = Path.Combine(directoryPath, fileWithMoreThanOneWordInLinePath); + var expected = Result.Fail>($"Файл \"{path}\" содержит строку с двумя и более словами"); + var actual = wordReader.ReadByLines(path); + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void WordReader_ThrowsException_WithEmpty() + { + var wordReader = new WordReader(); + var path = Path.Combine(directoryPath, fileEmptyPath); + var expected = Result.Fail>($"Файл \"{path}\" пустой"); + var actual = wordReader.ReadByLines(path); + actual.Should().BeEquivalentTo(expected); + } + + [TestCase("FileDoc.doc")] + [TestCase("FileImg.png")] + public void WordReader_ThrowsException_WithNonTxt(string filename) + { + var wordReader = new WordReader(); + FileUtilities + .CreateDataFile( + directoryPath, + Path.Combine(directoryPath, filename), + Array.Empty()); + + var path = Path.Combine(directoryPath, filename); + var expected = Result.Fail>( + $"Файл \"{path}\" должен иметь расширение \"txt\""); + var actual = wordReader.ReadByLines(path); + actual.Should().BeEquivalentTo(expected); + } + + [OneTimeTearDown] + public void OneTimeCleanup() + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, true); + } + } + } +} diff --git a/TagCloud/CloudLayouterPainters/CloudLayouterPainter.cs b/TagCloud/CloudLayouterPainters/CloudLayouterPainter.cs new file mode 100644 index 000000000..51b9fe2db --- /dev/null +++ b/TagCloud/CloudLayouterPainters/CloudLayouterPainter.cs @@ -0,0 +1,128 @@ +using FileSenderRailway; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; + +namespace TagCloud.CloudLayouterPainters +{ + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal class CloudLayouterPainter( + Size imageSize, + Color? backgroundColor = null, + Color? textColor = null, + FontFamily? fontName = null) : ICloudLayouterPainter + { + private readonly Color backgroundColor = backgroundColor ?? Color.White; + private readonly Color textColor = textColor ?? Color.Black; + private readonly FontFamily fontName = fontName ?? new FontFamily("Arial"); + + public Result Draw(IList tags) + => ValidateTags(tags) + .Then(validTags => CreateBitmap(validTags)) + .OnFail(error => Result.Fail(error)); + + private Result> ValidateTags(IList tags) + { + if (tags is null) + { + return Result.Fail>("Tags передан как null"); + } + + if (tags.Count == 0) + { + return Result.Fail>("Список тегов пуст"); + } + + if (!DoRectanglesFit(tags)) + { + return Result + .Fail>("Все прямоугольники не помещаются на изображение"); + } + + return tags.AsResult(); + } + + private Result CreateBitmap(IList tags) + => Result.Of(() => + { + var bitmap = new Bitmap(imageSize.Width, imageSize.Height); + + using var graphics = Graphics.FromImage(bitmap); + graphics.Clear(backgroundColor); + + foreach (var tag in tags) + { + var positionOnCanvas = GetPositionOnCanvas(tag.Rectangle); + var rectOnCanvas = new Rectangle( + positionOnCanvas.X, + positionOnCanvas.Y, + tag.Rectangle.Width, + tag.Rectangle.Height); + + DrawText(graphics, rectOnCanvas, tag.Text); + } + + return bitmap; + }); + + private bool DoRectanglesFit(IList tags) + { + var minimums = new Point( + tags.Min(t => t.Rectangle.Left), + tags.Min(t => t.Rectangle.Top)); + + var maximums = new Point( + tags.Max(t => t.Rectangle.Right), + tags.Max(t => t.Rectangle.Bottom)); + + var actualWidth = maximums.X - minimums.X; + var actualHeight = maximums.Y - minimums.Y; + return actualWidth < imageSize.Width && actualHeight < imageSize.Height; + } + + private Point GetPositionOnCanvas(Rectangle rectangle) + => new Point(rectangle.X + imageSize.Width / 2, rectangle.Y + imageSize.Height / 2); + + private void DrawText(Graphics graphics, Rectangle rectangle, string text) + { + var fontSize = FindFittingFontSize(graphics, text, rectangle); + using var fittingFont = new Font(fontName, fontSize, FontStyle.Regular, GraphicsUnit.Pixel); + using var stringFormat = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center + }; + using var brush = new SolidBrush(textColor); + + graphics.DrawString(text, fittingFont, brush, rectangle, stringFormat); + } + + private int FindFittingFontSize(Graphics graphics, string text, Rectangle rectangle) + { + var minSize = 1; + var maxSize = Math.Min(rectangle.Width, rectangle.Height); + var result = minSize; + + while (minSize <= maxSize) + { + var midSize = (minSize + maxSize) / 2; + using var font = new Font(fontName, midSize, FontStyle.Regular, GraphicsUnit.Pixel); + + var textSize = graphics.MeasureString(text, font); + if (textSize.Width <= rectangle.Width && textSize.Height <= rectangle.Height) + { + result = midSize; + minSize = midSize + 1; + } + else + { + maxSize = midSize - 1; + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/TagCloud/CloudLayouterPainters/ICloudLayouterPainter.cs b/TagCloud/CloudLayouterPainters/ICloudLayouterPainter.cs new file mode 100644 index 000000000..71e1a0ce6 --- /dev/null +++ b/TagCloud/CloudLayouterPainters/ICloudLayouterPainter.cs @@ -0,0 +1,11 @@ +using FileSenderRailway; +using System.Drawing; + +namespace TagCloud.CloudLayouterPainters +{ + // Интерфейс отрисовки прямоугольников + internal interface ICloudLayouterPainter + { + public Result Draw(IList tags); + } +} diff --git a/TagCloud/CloudLayouterWorkers/ICloudLayouterWorker.cs b/TagCloud/CloudLayouterWorkers/ICloudLayouterWorker.cs new file mode 100644 index 000000000..f24dd8b3f --- /dev/null +++ b/TagCloud/CloudLayouterWorkers/ICloudLayouterWorker.cs @@ -0,0 +1,13 @@ +using FileSenderRailway; +using System.Drawing; + +namespace TagCloud.CloudLayouterWorkers +{ + // Интерфейс получения свойств следующего прямоугольника + // По хорошему, нужно возвращать IEnumerable, + // для повышения возможности переиспользования + internal interface ICloudLayouterWorker + { + public Result> GetNextRectangleProperties(); + } +} diff --git a/TagCloud/CloudLayouterWorkers/NormalizedFrequencyBasedCloudLayouterWorker.cs b/TagCloud/CloudLayouterWorkers/NormalizedFrequencyBasedCloudLayouterWorker.cs new file mode 100644 index 000000000..4f290a5c5 --- /dev/null +++ b/TagCloud/CloudLayouterWorkers/NormalizedFrequencyBasedCloudLayouterWorker.cs @@ -0,0 +1,56 @@ +using FileSenderRailway; +using System.Drawing; +using TagCloud.Parsers; + +namespace TagCloud.CloudLayouterWorkers +{ + internal class NormalizedFrequencyBasedCloudLayouterWorker : ICloudLayouterWorker + { + private readonly int maxRectangleWidth; + private readonly int maxRectangleHeight; + private readonly Dictionary values; + private readonly string[] keysOrder; + public string[] KeysOrder => keysOrder.ToArray(); + + public NormalizedFrequencyBasedCloudLayouterWorker( + int maxWidth, + int maxHeight, + Dictionary normalizedValues, + bool isSorted = true) + { + maxRectangleWidth = maxWidth; + maxRectangleHeight = maxHeight; + values = normalizedValues; + if (isSorted) + { + keysOrder = values.OrderByDescending(x => x.Value).Select(x => x.Key).ToArray(); + } + else + { + keysOrder = values.Keys.ToArray(); + } + } + + public Result> GetNextRectangleProperties() + => ValidateDimensions() + .Then(_ => GenerateRectangles()) + .OnFail(error => Result.Fail>(error)); + + private Result ValidateDimensions() + => SizeParser.ParseSizeDimension(maxRectangleWidth) + .Then(_ => SizeParser.ParseSizeDimension(maxRectangleHeight)) + .Then(_ => Result.Ok()) + .OnFail(error => Result.Fail(error)); + + private IEnumerable<(string word, Size size)> GenerateRectangles() + { + foreach (var key in keysOrder) + { + var value = values[key]; + var width = (int)(maxRectangleWidth * value); + var height = (int)(maxRectangleHeight * value); + yield return (key, new Size(width, height)); + } + } + } +} \ No newline at end of file diff --git a/TagCloud/CloudLayouterWorkers/RandomCloudLayouterWorker.cs b/TagCloud/CloudLayouterWorkers/RandomCloudLayouterWorker.cs new file mode 100644 index 000000000..43aef0143 --- /dev/null +++ b/TagCloud/CloudLayouterWorkers/RandomCloudLayouterWorker.cs @@ -0,0 +1,52 @@ +using FileSenderRailway; +using System.Drawing; +using TagCloud.Parsers; + +namespace TagCloud.CloudLayouterWorkers +{ + internal class RandomCloudLayouterWorker( + int minRectangleWidth, + int maxRectangleWidth, + int minRectangleHeight, + int maxRectangleHeight) : ICloudLayouterWorker + { + private readonly Random random = new Random(); + public Result> GetNextRectangleProperties() + => ValidateDimensions() + .Then(_ => GenerateRectangles()) + .OnFail(error => Result.Fail>(error)); + + private Result ValidateDimensions() + => AreMinAndMaxSizesAppropriate(minRectangleWidth, maxRectangleWidth) + .Then(_ => AreMinAndMaxSizesAppropriate(minRectangleHeight, maxRectangleHeight)) + .Then(_ => ParseSizes()) + .OnFail(error => Result.Fail(error)); + + private Result ParseSizes() + => SizeParser.ParseSizeDimension(minRectangleWidth) + .Then(_ => SizeParser.ParseSizeDimension(maxRectangleWidth) + .Then(_ => SizeParser.ParseSizeDimension(minRectangleHeight) + .Then(_ => SizeParser.ParseSizeDimension(maxRectangleHeight) + .Then(_ => Result.Ok())))) + .OnFail(error => Result.Fail(error)); + + private static Result AreMinAndMaxSizesAppropriate(int min, int max) + { + if (min > max) + { + return Result.Fail("Минимальное значение не может быть больше максимального"); + } + return true.AsResult(); + } + + private IEnumerable<(string word, Size size)> GenerateRectangles() + { + while (true) + { + var width = random.Next(minRectangleWidth, maxRectangleWidth); + var height = random.Next(minRectangleHeight, maxRectangleHeight); + yield return (string.Empty, new Size(width, height)); + } + } + } +} \ No newline at end of file diff --git a/TagCloud/CloudLayouters/CircularCloudLayouter/Circle.cs b/TagCloud/CloudLayouters/CircularCloudLayouter/Circle.cs new file mode 100644 index 000000000..55198b985 --- /dev/null +++ b/TagCloud/CloudLayouters/CircularCloudLayouter/Circle.cs @@ -0,0 +1,25 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouters.CircularCloudLayouter +{ + internal class Circle(float startRadius = 2.0f) + { + private readonly Point center = new Point(0, 0); + public float Radius { get; set; } = startRadius; + + public IEnumerable GetCoordinatesOnCircle( + int startAngle, + int step = 1) + { + for (var dAngle = 0; dAngle < 360; dAngle += step) + { + var angle = (startAngle + dAngle) % 360; + + double angleInRadians = angle * Math.PI / 180; + var x = (int)(center.X + Radius * Math.Cos(angleInRadians)); + var y = (int)(center.Y + Radius * Math.Sin(angleInRadians)); + yield return new Point(x, y); + } + } + } +} diff --git a/TagCloud/CloudLayouters/CircularCloudLayouter/CircularCloudLayouter.cs b/TagCloud/CloudLayouters/CircularCloudLayouter/CircularCloudLayouter.cs new file mode 100644 index 000000000..ded9e7573 --- /dev/null +++ b/TagCloud/CloudLayouters/CircularCloudLayouter/CircularCloudLayouter.cs @@ -0,0 +1,73 @@ +using FileSenderRailway; +using System.Drawing; + +namespace TagCloud.CloudLayouters.CircularCloudLayouter +{ + // Класс, со старого задания TagCloud, + // который расставляет прямоугольники по окружности + // с постепенно увеличивающимся радиусом. + // Прямоугольники расставляются вокруг точки с координатой (0, 0), + // Затем, в CloudLayouterPainter координат пересчитываются таким образом, + // что бы расположить первый прямоугольник в центре холста. + + internal class CircularCloudLayouter : ICloudLayouter + { + private readonly Circle arrangementСircle = new Circle(); + private readonly Random random = new Random(); + private readonly List rectangles = new List(); + + public Result PutNextRectangle(Size rectangleSize) + => ValidateRectangleSize(rectangleSize) + .Then(size => PlaceRectangle(size)) + .OnFail(error => Result.Fail(error)); + + private static Result ValidateRectangleSize(Size rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) + { + return Result.Fail("Размеры прямоугольника не могут быть меньше либо равны нуля"); + } + + return rectangleSize.AsResult(); + } + + private Result PlaceRectangle(Size rectangleSize) + => Result.Of(() => + { + var result = new Rectangle(); + arrangementСircle.Radius -= 1.0f; + + var isPlaced = false; + while (!isPlaced) + { + var startAngle = random.Next(360); + foreach (var coordinate in arrangementСircle.GetCoordinatesOnCircle(startAngle)) + { + var location = GetRectangleLocation(coordinate, rectangleSize); + var nextRectangle = new Rectangle(location, rectangleSize); + if (!IsIntersectionWithAlreadyPlaced(nextRectangle)) + { + rectangles.Add(nextRectangle); + isPlaced = true; + result = nextRectangle; + break; + } + } + + arrangementСircle.Radius += 1.0f; + } + + return result; + }); + + private bool IsIntersectionWithAlreadyPlaced(Rectangle rectangle) + => rectangles.Any(rect => rect.IntersectsWith(rectangle)); + + private static Point GetRectangleLocation(Point pointOnCircle, Size rectangleSize) + { + var x = pointOnCircle.X - rectangleSize.Width / 2; + var y = pointOnCircle.Y - rectangleSize.Height / 2; + return new Point(x, y); + } + } +} diff --git a/TagCloud/CloudLayouters/ICloudLayouter.cs b/TagCloud/CloudLayouters/ICloudLayouter.cs new file mode 100644 index 000000000..ad6a7eb25 --- /dev/null +++ b/TagCloud/CloudLayouters/ICloudLayouter.cs @@ -0,0 +1,11 @@ +using FileSenderRailway; +using System.Drawing; + +namespace TagCloud.CloudLayouters +{ + // Интерфейс расстановки прямоугольников + internal interface ICloudLayouter + { + public Result PutNextRectangle(Size rectangleSize); + } +} diff --git a/TagCloud/CommandLineOptions.cs b/TagCloud/CommandLineOptions.cs new file mode 100644 index 000000000..902249b9e --- /dev/null +++ b/TagCloud/CommandLineOptions.cs @@ -0,0 +1,79 @@ +using CommandLine; + +namespace TagCloud +{ + public class CommandLineOptions + { + [Option( + "backgroundColor", + Required = false, + HelpText = "Цвет заднего фона изображения, например \"White\".")] + public string BackgroundColor { get; set; } = "White"; + + [Option( + "textColor", + Required = false, + HelpText = "Цвет текста на изображении, например \"Black\".")] + public string TextColor { get; set; } = "Black"; + + [Option( + "font", + Required = false, + HelpText = "Шрифт текста на изображении, например \"Arial\".")] + public string Font { get; set; } = "Arial"; + + [Option( + "nonSorted", + Required = false, + HelpText = "Отключение сортировки слов, например \"False\".")] + public string IsSorted { get; set; } = Boolean.TrueString; + + [Option( + "size", + Required = false, + HelpText = "Размер изображения в формате ШИРИНА:ВЫСОТА, например \"5000:5000\".")] + public string ImageSize { get; set; } = "5000:5000"; + + [Option( + "maxRectangleWidth", + Required = false, + HelpText = "Максимальная ширина прямоугольника, например \"500\".")] + public string MaxRectangleWidth { get; set; } = "500"; + + [Option( + "maxRectangleHeight", + Required = false, + HelpText = "Максимальная высота прямоугольника, например \"200\".")] + public string MaxRectangleHeight { get; set; } = "200"; + + [Option( + "imageFile", + Required = false, + HelpText = "Имя выходного файла изображения, например \"Result\".")] + public string ImageFileName { get; set; } = "Result"; + + [Option( + "dataFile", + Required = true, + HelpText = "Полный путь к файлу с исходными данными, например \"C:\\MyWorkSpace\\Coding\\MyCodes\\CSharp\\PostUniversityEra\\ShporaHomeworks\\Homework.6.TagCloudII\\SnowWhite.txt\".")] + public required string DataFileName { get; set; } + + [Option( + "resultFormat", + Required = false, + HelpText = "Формат создаваемого изображение, например \"png\".")] + public string ResultFormat { get; set; } = "png"; + + [Option( + "wordsToIncludeFile", + Required = false, + HelpText = "Полный путь к файлу со словами для добавления в фильтр \"скучных слов\", например \"C:\\MyWorkSpace\\Coding\\MyCodes\\CSharp\\PostUniversityEra\\ShporaHomeworks\\Homework.6.TagCloudII\\WordsToInclude.txt\".")] + public string? WordsToIncludeFileName { get; set; } = null; //+ + + [Option( + "wordsToExcludeFile", + Required = false, + HelpText = "Полный путь к файлу со словами для исключения из фильтра \"скучных слов\", например \"C:\\MyWorkSpace\\Coding\\MyCodes\\CSharp\\PostUniversityEra\\ShporaHomeworks\\Homework.6.TagCloudII\\WordsToExclude.txt\".")] + public string? WordsToExcludeFileName { get; set; } = null; //+ + } +} \ No newline at end of file diff --git a/TagCloud/DIContainer.cs b/TagCloud/DIContainer.cs new file mode 100644 index 000000000..464a4bc68 --- /dev/null +++ b/TagCloud/DIContainer.cs @@ -0,0 +1,59 @@ +using Autofac; +using TagCloud.CloudLayouters.CircularCloudLayouter; +using TagCloud.CloudLayouters; +using TagCloud.ImageSavers; +using TagCloud.Normalizers; +using TagCloud.WordCounters; +using TagCloud.WordReaders; +using TagCloud.Factories; + +namespace TagCloud +{ + public static class DIContainer + { + public static IContainer ConfigureContainer(CommandLineOptions options) + { + var builder = new ContainerBuilder(); + + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + + RegisterProgramExecutorService(builder, options); + + return builder.Build(); + } + + private static void RegisterSimpleSevice( + ContainerBuilder builder) + where TImplementation : TService + where TService : notnull + => builder + .RegisterType() + .As() + .SingleInstance(); + + private static void RegisterProgramExecutorService( + ContainerBuilder builder, + CommandLineOptions options) + => builder.RegisterType() + .WithParameter("backgroundColor", options.BackgroundColor) + .WithParameter("textColor", options.TextColor) + .WithParameter("font", options.Font) + .WithParameter("isSorted", options.IsSorted) + .WithParameter("imageSize", options.ImageSize) + .WithParameter("maxRectangleWidth", options.MaxRectangleWidth) + .WithParameter("maxRectangleHeight", options.MaxRectangleHeight) + .WithParameter("imageFileName", options.ImageFileName) + .WithParameter("dataFileName", options.DataFileName) + .WithParameter("resultFormat", options.ResultFormat) + .WithParameter("wordsToIncludeFileName", options.WordsToIncludeFileName!) + .WithParameter("wordsToExcludeFileName", options.WordsToExcludeFileName!) + .SingleInstance(); + } +} diff --git a/TagCloud/Factories/CloudLayouterPainterFactory.cs b/TagCloud/Factories/CloudLayouterPainterFactory.cs new file mode 100644 index 000000000..5d733e5d8 --- /dev/null +++ b/TagCloud/Factories/CloudLayouterPainterFactory.cs @@ -0,0 +1,19 @@ +using FileSenderRailway; +using System.Drawing; +using TagCloud.CloudLayouterPainters; + + +namespace TagCloud.Factories +{ + internal class CloudLayouterPainterFactory : ICloudLayouterPainterFactory + { + public Result Create( + Size imageSize, + Color? backgroundColor = null, + Color? textColor = null, + FontFamily? fontName = null) + => Result.Ok( + new CloudLayouterPainter(imageSize, backgroundColor, textColor, fontName)); + + } +} diff --git a/TagCloud/Factories/CloudLayouterWorkerFactory.cs b/TagCloud/Factories/CloudLayouterWorkerFactory.cs new file mode 100644 index 000000000..c3be69c3f --- /dev/null +++ b/TagCloud/Factories/CloudLayouterWorkerFactory.cs @@ -0,0 +1,63 @@ +using FileSenderRailway; +using TagCloud.CloudLayouterWorkers; +using TagCloud.Normalizers; +using TagCloud.WordCounters; +using TagCloud.WordFilters; +using TagCloud.WordReaders; + +namespace TagCloud.Factories +{ + internal class CloudLayouterWorkerFactory( + IWordReader wordReader, + IWordCounter wordCounter, + INormalizer normalizer, + IWordFilterFactory wordFilterFactory) : ICloudLayouterWorkerFactory + { + public Result Create( + string dataFileName, + string? wordsToIncludeFileName, + string? wordsToExcludeFileName, + int maxRectangleWidth, + int maxRectangleHeight, + bool isSorted) + => ReadWords(dataFileName) + .Then(initialWords => CreateFilter( + wordsToIncludeFileName, + wordsToExcludeFileName, + wordReader) + .Then(filter => AddWords(filter, initialWords))) + .Then(_ => normalizer.Normalize(wordCounter.Values)) + .Then(normalizer + => Result.Ok( + new NormalizedFrequencyBasedCloudLayouterWorker( + maxRectangleWidth, + maxRectangleHeight, + normalizer, + isSorted))) + .OnFail(error => Result.Fail(error)); + + private Result> ReadWords(string dataFileName) + => wordReader.ReadByLines(dataFileName); + + private Result CreateFilter( + string? wordsToIncludeFileName, + string? wordsToExcludeFileName, + IWordReader wordReader) + => wordFilterFactory.Create(wordsToIncludeFileName, wordsToExcludeFileName, wordReader); + + private Result AddWords(IWordFilter wordFilter, IEnumerable words) + { + foreach (var word in words) + { + var wordInLowerCase = word.ToLower(); + if (!wordFilter.IsCorrectWord(wordInLowerCase)) + { + continue; + } + + wordCounter.AddWord(wordInLowerCase); + } + return Result.Ok(); + } + } +} \ No newline at end of file diff --git a/TagCloud/Factories/ICloudLayouterPainterFactory.cs b/TagCloud/Factories/ICloudLayouterPainterFactory.cs new file mode 100644 index 000000000..91724bf90 --- /dev/null +++ b/TagCloud/Factories/ICloudLayouterPainterFactory.cs @@ -0,0 +1,15 @@ +using FileSenderRailway; +using System.Drawing; +using TagCloud.CloudLayouterPainters; + +namespace TagCloud.Factories +{ + internal interface ICloudLayouterPainterFactory + { + public Result Create( + Size imageSize, + Color? backgroundColor = null, + Color? textColor = null, + FontFamily? fontName = null); + } +} diff --git a/TagCloud/Factories/ICloudLayouterWorkerFactory.cs b/TagCloud/Factories/ICloudLayouterWorkerFactory.cs new file mode 100644 index 000000000..eff29b50d --- /dev/null +++ b/TagCloud/Factories/ICloudLayouterWorkerFactory.cs @@ -0,0 +1,16 @@ +using FileSenderRailway; +using TagCloud.CloudLayouterWorkers; + +namespace TagCloud.Factories +{ + internal interface ICloudLayouterWorkerFactory + { + public Result Create( + string dataFileName, + string? wordsToIncludeFileName, + string? wordsToExcludeFileName, + int maxRectangleWidth, + int maxRectangleHeight, + bool isSorted); + } +} diff --git a/TagCloud/Factories/IWordFilterFactory.cs b/TagCloud/Factories/IWordFilterFactory.cs new file mode 100644 index 000000000..fcaf1c41b --- /dev/null +++ b/TagCloud/Factories/IWordFilterFactory.cs @@ -0,0 +1,14 @@ +using FileSenderRailway; +using TagCloud.WordFilters; +using TagCloud.WordReaders; + +namespace TagCloud.Factories +{ + internal interface IWordFilterFactory + { + public Result Create( + string? wordsToIncludeFileName, + string? wordsToExcludeFileName, + IWordReader wordReader); + } +} diff --git a/TagCloud/Factories/WordFilterFactory.cs b/TagCloud/Factories/WordFilterFactory.cs new file mode 100644 index 000000000..97a39734a --- /dev/null +++ b/TagCloud/Factories/WordFilterFactory.cs @@ -0,0 +1,66 @@ +using FileSenderRailway; +using TagCloud.CloudLayouterWorkers; +using TagCloud.WordFilters; +using TagCloud.WordReaders; + +namespace TagCloud.Factories +{ + internal class WordFilterFactory : IWordFilterFactory + { + public Result Create( + string? wordsToIncludeFileName, + string? wordsToExcludeFileName, + IWordReader wordReader) + => Result.Ok(new WordFilter()) + .Then(wordFilter => AddWords(wordReader, wordsToIncludeFileName!, wordFilter)) + .Then(wordFilter => RemoveWords(wordReader, wordsToExcludeFileName!, wordFilter)) + .OnFail(error => Result.Fail(error)); + + private static bool IsFileNameSet(string? fileName) + => fileName is not null; + + private static Result AddWords( + IWordReader wordReader, + string wordsToIncludeFileName, + IWordFilter wordFilter) + { + if (IsFileNameSet(wordsToIncludeFileName)) + { + var wordsResult = wordReader.ReadByLines(wordsToIncludeFileName); + if (!wordsResult.IsSuccess) + { + return Result.Fail(wordsResult.Error); + } + + foreach (var word in wordsResult.GetValueOrThrow()) + { + wordFilter.Add(word.ToLower()); + } + } + + return Result.Ok(wordFilter); + } + + private static Result RemoveWords( + IWordReader wordReader, + string wordsToExcludeFileName, + IWordFilter wordFilter) + { + if (IsFileNameSet(wordsToExcludeFileName)) + { + var wordsResult = wordReader.ReadByLines(wordsToExcludeFileName); + if (!wordsResult.IsSuccess) + { + return Result.Fail(wordsResult.Error); + } + + foreach (var word in wordsResult.GetValueOrThrow()) + { + wordFilter.Remove(word.ToLower()); + } + } + + return Result.Ok(wordFilter); + } + } +} \ No newline at end of file diff --git a/TagCloud/ImageSavers/IImageSaver.cs b/TagCloud/ImageSavers/IImageSaver.cs new file mode 100644 index 000000000..aee878dc6 --- /dev/null +++ b/TagCloud/ImageSavers/IImageSaver.cs @@ -0,0 +1,11 @@ +using FileSenderRailway; +using System.Drawing; + +namespace TagCloud.ImageSavers +{ + // Интерфейс сохранения изображения в файл + internal interface IImageSaver + { + public Result SaveFile(Bitmap image, string fileName, string format = "png"); + } +} diff --git a/TagCloud/ImageSavers/ImageSaver.cs b/TagCloud/ImageSavers/ImageSaver.cs new file mode 100644 index 000000000..ed2b9205b --- /dev/null +++ b/TagCloud/ImageSavers/ImageSaver.cs @@ -0,0 +1,63 @@ +using FileSenderRailway; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Drawing.Imaging; + +namespace TagCloud.ImageSavers +{ + // Реализован пункт на перспективу: + // Формат результата. + // Поддерживать разные форматы изображений. + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal class ImageSaver : IImageSaver + { + private readonly Dictionary supportedFormats = + new Dictionary() + { + {"png" , ImageFormat.Png }, + {"jpg" , ImageFormat.Jpeg }, + {"jpeg" , ImageFormat.Jpeg }, + {"bmp" , ImageFormat.Bmp }, + {"gif" , ImageFormat.Gif }, + {"tiff" , ImageFormat.Tiff } + }; + + public Result SaveFile(Bitmap image, string fileName, string format = "png") + => ValidateInput(image, fileName, format) + .Then(_ => SaveImage(image, fileName, format)) + .OnFail(error => Result.Fail(error)); + + private Result ValidateInput(Bitmap image, string fileName, string format) + { + if (image is null) + { + return Result.Fail("Передаваемое изображение не должно быть null"); + } + + if (string.IsNullOrWhiteSpace(fileName)) + { + return Result.Fail($"Некорректное имя файла для создания \"{fileName}\""); + } + + if (string.IsNullOrWhiteSpace(format) || !IsSupportedFormat(format)) + { + return Result.Fail($"Формат \"{format}\" не поддерживается"); + } + + return Result.Ok(); + } + + private Result SaveImage(Bitmap image, string fileName, string format) + { + var imageFormat = supportedFormats[format]; + image.Save($"{fileName}.{format}", imageFormat); + return Result.Ok(); + } + + private bool IsSupportedFormat(string format) + => supportedFormats.ContainsKey(format); + } +} \ No newline at end of file diff --git a/TagCloud/Normalizers/INormalizer.cs b/TagCloud/Normalizers/INormalizer.cs new file mode 100644 index 000000000..cf67bd480 --- /dev/null +++ b/TagCloud/Normalizers/INormalizer.cs @@ -0,0 +1,13 @@ +using FileSenderRailway; + +namespace TagCloud.Normalizers +{ + // Интерфейс нормализации количества каждого слова + internal interface INormalizer + { + public Result> Normalize( + Dictionary values, + double minCoefficient = 0.25, + int decimalPlaces = 4); + } +} diff --git a/TagCloud/Normalizers/Normalizer.cs b/TagCloud/Normalizers/Normalizer.cs new file mode 100644 index 000000000..2f7fb6644 --- /dev/null +++ b/TagCloud/Normalizers/Normalizer.cs @@ -0,0 +1,91 @@ +using FileSenderRailway; + +namespace TagCloud.Normalizers +{ + // Слово, которое встречается чаще всего, будет иметь вес 1.0. + // Это означает, что оно в дальнейшем будет иметь прямоугольник + // с максимальным размером. + // Слово с минимальной частотой будет иметь + // minCoefficient * максимальный размеро прямоугольника. + internal class Normalizer : INormalizer + { + public Result> Normalize( + Dictionary values, + double minCoefficient = 0.25, + int decimalPlaces = 4) + => ValidateInput(values, minCoefficient, decimalPlaces) + .Then(_ => CalculateNormalizedValues(values, minCoefficient, decimalPlaces)) + .OnFail(error => Result.Fail>(error)); + + private static Result ValidateInput( + Dictionary values, + double minCoefficient, + int decimalPlaces) + { + if (values is null || values.Count == 0) + { + return Result.Fail("Словарь значений не может быть пустым"); + } + + if (minCoefficient < 0.0 || minCoefficient > 1.0) + { + return Result.Fail( + "Минимальный коэффициент нормализации должен быть в диапазоне от 0 до 1"); + } + + if (decimalPlaces < 0) + { + return Result.Fail( + "Количество знаков после запятой не может быть отрицательным"); + } + + return Result.Ok(); + } + + private Result> CalculateNormalizedValues( + Dictionary values, + double minCoefficient, + int decimalPlaces) + => Result.Of(() => + { + var result = new Dictionary(); + + var maxValue = values.Values.Max(); + var minValue = values.Values.Min(); + + var scale = 1.0 - minCoefficient; + + foreach (var pair in values) + { + result[pair.Key] = CalculateNormalizedValue( + minCoefficient, + scale, + pair.Value, + minValue, + maxValue, + decimalPlaces); + } + + return result; + }); + + + private static double CalculateNormalizedValue( + double minCoefficient, + double scale, + uint value, + uint minValue, + uint maxValue, + int decimalPlaces) + { + if (minValue == maxValue) + { + return 1.0; + } + + return Math.Round( + minCoefficient + scale * ((double)(value - minValue) / (maxValue - minValue)), + decimalPlaces); + } + } +} \ No newline at end of file diff --git a/TagCloud/Parsers/BoolParser.cs b/TagCloud/Parsers/BoolParser.cs new file mode 100644 index 000000000..086076d6a --- /dev/null +++ b/TagCloud/Parsers/BoolParser.cs @@ -0,0 +1,16 @@ +using FileSenderRailway; + +namespace TagCloud.Parsers +{ + internal static class BoolParser + { + public static Result ParseIsSorted(string value) + { + if (value == bool.FalseString || value == bool.TrueString) + { + return Convert.ToBoolean(value).AsResult(); + } + return Result.Fail($"Неизвестный параметр сортировки \"{value}\""); + } + } +} diff --git a/TagCloud/Parsers/ColorParser.cs b/TagCloud/Parsers/ColorParser.cs new file mode 100644 index 000000000..f12fd05a2 --- /dev/null +++ b/TagCloud/Parsers/ColorParser.cs @@ -0,0 +1,25 @@ +using FileSenderRailway; +using System.Drawing; + +namespace TagCloud.Parsers +{ + internal static class ColorParser + { + public static Result ParseColor(string color) + { + if (string.IsNullOrWhiteSpace(color)) + { + return GetError(color); + } + var result = Color.FromName(color); + if (!result.IsKnownColor) + { + return GetError(color); + } + return result.AsResult(); + } + + private static Result GetError(string color) + => Result.Fail($"Неизвестный цвет \"{color}\""); + } +} diff --git a/TagCloud/Parsers/FontParser.cs b/TagCloud/Parsers/FontParser.cs new file mode 100644 index 000000000..a7f12a7d0 --- /dev/null +++ b/TagCloud/Parsers/FontParser.cs @@ -0,0 +1,30 @@ +using FileSenderRailway; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; + +namespace TagCloud.Parsers +{ + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal static class FontParser + { + public static Result ParseFont(string font) + { + if (string.IsNullOrWhiteSpace(font)) + { + return GetError(font); + } + if (!FontFamily.Families.Any( + x => x.Name.Equals(font, StringComparison.OrdinalIgnoreCase))) + { + return GetError(font); + } + return new FontFamily(font).AsResult(); + } + + private static Result GetError(string font) + => Result.Fail($"Неизвестный шрифт \"{font}\""); + } +} diff --git a/TagCloud/Parsers/SizeParser.cs b/TagCloud/Parsers/SizeParser.cs new file mode 100644 index 000000000..bb8b10650 --- /dev/null +++ b/TagCloud/Parsers/SizeParser.cs @@ -0,0 +1,57 @@ +using FileSenderRailway; +using System.Drawing; + +namespace TagCloud.Parsers +{ + internal static class SizeParser + { + private static Result GetErrorInvalidSize(string size) + => Result.Fail($"Некорректный формат размера изображения \"{size}\", используйте формат \"Ширина:Высота\""); + + public static Result ParseImageSize(string size) + { + if (string.IsNullOrWhiteSpace(size)) + { + return GetErrorInvalidSize(size); + } + + var dimensions = size.Split(':', StringSplitOptions.RemoveEmptyEntries); + if (dimensions.Length != 2) + { + return GetErrorInvalidSize(size); + } + + var width = ParseSizeDimension(dimensions[0]); + if (!width.IsSuccess) + { + return Result.Fail(width.Error); + } + + var height = ParseSizeDimension(dimensions[1]); + if (!height.IsSuccess) + { + return Result.Fail(height.Error); + } + + return new Size(width.GetValueOrThrow(), height.GetValueOrThrow()).AsResult(); + } + + public static Result ParseSizeDimension(string dimension) + { + if (string.IsNullOrWhiteSpace(dimension) || !int.TryParse(dimension, out var result)) + { + return Result.Fail($"Передано не число \"{dimension}\""); + } + return ParseSizeDimension(result); + } + + public static Result ParseSizeDimension(int dimension) + { + if (dimension <= 0) + { + return Result.Fail($"Переданное числовое значение должно быть больше 0: \"{dimension}\""); + } + return dimension.AsResult(); + } + } +} diff --git a/TagCloud/Program.cs b/TagCloud/Program.cs new file mode 100644 index 000000000..bb6718c9d --- /dev/null +++ b/TagCloud/Program.cs @@ -0,0 +1,40 @@ +using Autofac; +using CommandLine; +using FileSenderRailway; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("TagCloud.Tests")] +namespace TagCloud +{ + internal class Program + { + static void Main(string[] args) + { + var parserResult = Parser.Default.ParseArguments(args); + + parserResult.WithParsed(options => + { + Run(DIContainer.ConfigureContainer(options)); + }); + + parserResult.WithNotParsed(errors => + { + Console.WriteLine("Ошибка парсинга аргументов:"); + foreach (var error in errors) + { + Console.WriteLine(error.ToString()); + } + }); + } + + private static void Run(IContainer container) + { + using var scope = container.BeginLifetimeScope(); + var program = scope.Resolve(); + program + .Execute() + .Then(_ => Console.WriteLine("Изображение успешно сгенерировано.")) + .OnFail(error => Console.WriteLine(error)); + } + } +} diff --git a/TagCloud/ProgramExecutor.cs b/TagCloud/ProgramExecutor.cs new file mode 100644 index 000000000..410457747 --- /dev/null +++ b/TagCloud/ProgramExecutor.cs @@ -0,0 +1,105 @@ +using FileSenderRailway; +using TagCloud.CloudLayouters; +using TagCloud.CloudLayouterWorkers; +using TagCloud.ImageSavers; +using TagCloud.Factories; +using TagCloud.Parsers; +using System.Drawing; +using System.Diagnostics.CodeAnalysis; + +namespace TagCloud +{ + [SuppressMessage( + "Interoperability", + "CA1416:Проверка совместимости платформы", + Justification = "Код предназначен для выполнения только на Windows 6.1 и новее")] + internal class ProgramExecutor( + string backgroundColor, + string textColor, + string font, + string isSorted, + string imageSize, + string maxRectangleWidth, + string maxRectangleHeight, + string imageFileName, + string dataFileName, + string resultFormat, + string? wordsToIncludeFileName, + string? wordsToExcludeFileName, + ICloudLayouterWorkerFactory cloudLayouterWorkerFactory, + ICloudLayouterPainterFactory cloudLayouterPainterFactory, + ICloudLayouter layouter, + IImageSaver imageSaver) + { + public Result Execute() + => ProcessTags() + .Then(tags => Draw(tags)) + .Then(image => Save(image)) + .OnFail(error => Result.Fail(error)); + + private Result CreateWorker() + => SizeParser.ParseSizeDimension(maxRectangleWidth) + .Then(width => SizeParser.ParseSizeDimension(maxRectangleHeight) + .Then(height => BoolParser.ParseIsSorted(isSorted) + .Then(isSorted => cloudLayouterWorkerFactory.Create( + dataFileName, + wordsToIncludeFileName, + wordsToExcludeFileName, + width, + height, + isSorted)) + ) + ) + .OnFail(error => Result.Fail(error)); + + private Result> ProcessTagsWithWorker(ICloudLayouterWorker worker) + => worker + .GetNextRectangleProperties() + .Then(rectangleProperties => + { + var tags = new List(); + foreach (var rectangleProperty in rectangleProperties) + { + var tagResult = layouter + .PutNextRectangle(rectangleProperty.size) + .Then(tagSize => new Tag(rectangleProperty.word, tagSize)); + + if (!tagResult.IsSuccess) + { + return Result.Fail>(tagResult.Error); + } + + tags.Add(tagResult.GetValueOrThrow()); + } + return Result.Ok(tags); + }) + .OnFail(error => Result.Fail>(error)); + + private Result> ProcessTags() + => CreateWorker() + .Then(worker => ProcessTagsWithWorker(worker)) + .OnFail(error => Result.Fail>(error)); + + private Result Draw(List tags) + => SizeParser.ParseImageSize(imageSize) + .Then(size => ColorParser.ParseColor(backgroundColor) + .Then(bgColor => ColorParser.ParseColor(textColor) + .Then(textColor => FontParser.ParseFont(font) + .Then(font => cloudLayouterPainterFactory.Create(size, bgColor, textColor, font)) + .Then(painter => painter.Draw(tags)) + ) + ) + ) + .OnFail(error => Result.Fail(error)); + + private Result Save(Bitmap image) + => imageSaver.SaveFile(image, imageFileName, resultFormat) + .Then(_ => Result.Ok()) + .OnFail(error => Result.Fail(error)); + + private Result DrawAndSaveImage(List tags) + => Draw(tags) + .Then(image => Save(image)) + .OnFail(error => Result.Fail(error)); + } +} \ No newline at end of file diff --git a/TagCloud/SnowWhiteContent.txt b/TagCloud/SnowWhiteContent.txt new file mode 100644 index 000000000..239707285 --- /dev/null +++ b/TagCloud/SnowWhiteContent.txt @@ -0,0 +1,2941 @@ +Long +long +ago +in +the +winter +time +when +the +snowflakes +were +falling +like +little +white +feathers +from +the +sky +a +beautiful +Queen +sat +beside +her +window +which +was +framed +in +black +ebony +and +stitched +As +she +worked +she +looked +sometimes +at +the +falling +snow +and +so +it +happened +that +she +pricked +her +finger +with +her +needle +so +that +three +drops +of +blood +fell +upon +the +snow +How +pretty +the +red +blood +looked +upon +the +dazzling +white +The +Queen +said +to +herself +as +she +saw +it +Ah +me +If +only +I +had +a +dear +little +child +as +white +as +the +snow +as +rosy +as +the +blood +and +with +hair +as +black +as +the +ebony +window +frame +Soon +afterwards +a +little +daughter +came +to +her +who +was +white +as +snow +rosy +as +the +blood +and +whose +hair +was +as +black +as +ebony +so +she +was +called +Little +Snow +White +But +alas +When +the +little +one +came +the +good +Queen +died +A +year +passed +away +and +the +King +took +another +wife +She +was +very +beautiful +but +so +proud +and +haughty +that +she +could +not +bear +to +be +surpassed +in +beauty +by +anyone +She +possessed +a +wonderful +mirror +which +could +answer +her +when +she +stood +before +it +and +said +Mirror +mirror +upon +the +wall +Who +is +the +fairest +of +all +The +mirror +answered +Thou +O +Queen +art +the +fairest +of +all +and +the +Queen +was +contented +because +she +knew +the +mirror +could +speak +nothing +but +the +truth +But +as +time +passed +on +Little +Snow +White +grew +more +and +more +beautiful +until +when +she +was +seven +years +old +she +was +as +lovely +as +the +bright +day +and +still +more +lovely +than +the +Queen +herself +so +that +when +the +lady +one +day +asked +her +mirror +Mirror +mirror +upon +the +wall +Who +is +the +fairest +fair +of +all +It +answered +O +Lady +Queen +though +fair +ye +be +Snow +White +is +fairer +far +to +see +The +Queen +was +horrified +and +from +that +moment +envy +and +pride +grew +in +her +heart +like +rank +weeds +until +one +day +she +called +a +huntsman +and +said +Take +the +child +away +into +the +woods +and +kill +her +for +I +can +no +longer +bear +the +sight +of +her +And +when +you +return +bring +with +you +her +heart +that +I +may +know +you +have +obeyed +my +will +The +huntsman +dared +not +disobey +so +he +led +Snow +White +out +into +the +woods +and +placed +an +arrow +in +his +bow +to +pierce +her +innocent +heart +but +the +little +maid +begged +him +to +spare +her +life +and +the +child’s +beauty +touched +his +heart +with +pity +so +that +he +bade +her +run +away +Then +as +a +young +wild +boar +came +rushing +by +he +killed +it +took +out +its +heart +and +carried +it +home +to +the +Queen +Poor +little +Snow +White +was +now +all +alone +in +the +wild +wood +and +so +frightened +was +she +that +she +trembled +at +every +leaf +that +rustled +So +she +began +to +run +and +ran +on +and +on +until +she +came +to +a +little +house +where +she +went +in +to +rest +In +the +little +house +everything +she +saw +was +tiny +but +more +dainty +and +clean +than +words +can +tell +Upon +a +white +covered +table +stood +seven +little +plates +and +upon +each +plate +lay +a +little +spoon +besides +which +there +were +seven +knives +and +forks +and +seven +little +goblets +Against +the +wall +and +side +by +side +stood +seven +little +beds +covered +with +snow +white +sheets +Snow +White +was +so +hungry +and +thirsty +that +she +took +a +little +food +from +each +of +the +seven +plates +and +drank +a +few +drops +of +wine +from +each +goblet +for +she +did +not +wish +to +take +everything +away +from +one +Then +because +she +was +so +tired +she +crept +into +one +bed +after +the +other +seeking +for +rest +but +one +was +too +long +another +too +short +and +so +on +until +she +came +to +the +seventh +which +suited +her +exactly +so +she +said +her +prayers +and +soon +fell +fast +asleep +When +night +fell +the +masters +of +the +little +house +came +home +They +were +seven +dwarfs +who +worked +with +a +pick +axe +and +spade +searching +for +cooper +and +gold +in +the +heart +of +the +mountains +They +lit +their +seven +candles +and +then +saw +that +someone +had +been +to +visit +them +The +first +said +Who +has +been +sitting +on +my +chair +The +second +said +Who +has +been +eating +from +my +plate +The +third +said +Who +has +taken +a +piece +of +my +bread +The +fourth +said +Who +has +taken +some +of +my +vegetables +The +fifth +Who +has +been +using +my +fork +The +sixth +Who +has +been +cutting +with +my +knife +The +seventh +Who +has +been +drinking +out +of +my +goblet +The +first +looked +round +and +saw +that +his +bed +was +rumpled +so +he +said +Who +has +been +getting +into +my +bed +Then +the +others +looked +round +and +each +one +cried +Someone +has +been +on +my +bed +too +But +the +seventh +saw +little +Snow +White +lying +asleep +in +his +bed +and +called +the +others +to +come +and +look +at +her +and +they +cried +aloud +with +surprise +and +fetched +their +seven +little +candles +so +that +they +might +see +her +the +better +and +they +were +so +pleased +with +her +beauty +that +they +let +her +sleep +on +all +night +When +the +sun +rose +Snow +White +awoke +and +oh +How +frightened +she +was +when +she +saw +the +seven +little +dwarfs +But +they +were +very +friendly +and +asked +what +her +name +was +My +name +is +Snow +White +she +answered +And +how +did +you +come +to +get +into +our +house +questioned +the +dwarfs +Then +she +told +them +how +her +cruel +step +mother +had +intended +her +to +be +killed +but +how +the +huntsman +had +spared +her +life +and +she +had +run +on +until +she +reached +the +little +house +And +the +dwarfs +said +If +you +will +take +care +of +our +house +cook +for +us +and +make +the +beds +wash +mend +and +knit +and +keep +everything +neat +and +clean +then +you +may +stay +with +us +altogether +and +you +shall +want +for +nothing +With +all +my +heart +answered +Snow +White +and +so +she +stayed +She +kept +the +house +neat +and +clean +for +the +dwarfs +who +went +off +early +in +the +morning +to +search +for +copper +and +gold +in +the +mountains +and +who +expected +their +meal +to +be +standing +ready +for +them +when +they +returned +at +night +All +day +long +Snow +White +was +alone +and +the +good +little +dwarfs +warned +her +to +be +careful +to +let +no +one +into +the +house +For +said +they +your +step +mother +will +soon +discover +that +you +are +living +here +The +Queen +believing +of +course +that +Snow +White +was +dead +and +that +therefore +she +was +again +the +most +beautiful +lady +in +the +land +went +to +her +mirror +and +said +Mirror +mirror +upon +the +wall +Who +is +the +fairest +fair +of +all +Then +the +mirror +answered +O +Lady +Queen +though +fair +ye +be +Snow +White +is +fairer +far +to +see +Over +the +hills +and +far +away +She +dwells +with +seven +dwarfs +to +day +How +angry +she +was +for +she +knew +that +the +mirror +spoke +the +truth +and +that +the +huntsman +must +have +deceived +her +She +thought +and +thought +how +she +might +kill +Snow +White +for +she +knew +she +would +have +neither +rest +nor +peace +until +she +really +was +the +most +beautiful +lady +in +the +land +At +length +she +decided +what +to +do +She +painted +her +face +and +dressed +herself +like +an +old +peddler +woman +so +that +no +one +could +recognize +her +and +in +this +disguise +she +climbed +the +seven +mountains +that +lay +between +her +and +the +dwarfs’ +house +and +knocked +at +their +door +and +cried +Good +wares +to +sell +very +cheap +today +Snow +White +peeped +from +the +window +and +said +Good +day +good +wife +and +what +are +your +wares +All +sorts +of +pretty +things +my +dear +answered +the +woman +Silken +laces +of +every +colour +and +she +held +up +a +bright +coloured +one +made +of +plaited +silks +Surely +I +might +let +this +honest +old +woman +come +in +thought +Snow +White +and +unbolted +the +door +and +bought +the +pretty +lace +Dear +dear +what +a +figure +you +are +child +said +the +old +woman +come +let +me +lace +you +properly +for +once +Snow +White +had +no +suspicious +thoughts +so +she +placed +herself +in +front +of +the +old +woman +that +she +might +fasten +her +dress +with +the +new +silk +lace +But +in +less +than +no +time +the +wicked +creature +had +laced +her +so +tightly +that +she +could +not +breathe +but +fell +down +upon +the +ground +as +though +she +were +dead +Now +said +the +Queen +I +am +once +more +the +most +beautiful +lady +in +the +land +and +she +went +away +When +the +dwarfs +came +home +they +were +very +grieved +to +find +their +dear +little +Snow +White +lying +upon +the +ground +as +though +she +were +dead +They +lifted +her +gently +and +seeing +that +she +was +too +tightly +laced +they +cut +the +silken +cord +when +she +drew +a +long +breath +and +then +gradually +came +back +to +life +When +the +dwarfs +heard +all +that +had +happened +they +said +The +peddler +woman +was +certainly +the +wicked +Queen +Now +take +care +in +future +that +you +open +the +door +to +none +when +we +are +not +with +you +The +wicked +Queen +had +no +sooner +reached +home +than +she +went +to +her +mirror +and +said +Mirror +mirror +upon +the +wall +Who +is +the +fairest +fair +of +all +And +the +mirror +answered +as +before +O +Lady +Queen +though +fair +ye +be +Snow +White +is +fairer +far +to +see +Over +the +hills +and +far +away +She +dwells +with +seven +dwarfs +to +day +The +blood +rushed +to +her +face +as +she +heard +these +words +for +she +knew +that +Snow +White +must +have +come +to +life +again +But +I +will +manage +to +put +an +end +to +her +yet +she +said +and +then +by +means +of +her +magic +she +made +a +poisonous +comb +Again +she +disguised +herself +climbed +the +seven +mountains +and +knocked +at +the +door +of +the +seven +dwarfs’ +cottage +crying +Good +wares +to +sell +very +cheap +today +Snow +White +looked +out +of +the +window +and +said +Go +away +good +woman +for +I +dare +not +let +you +in +Surely +you +can +look +at +my +goods +answered +the +woman +and +held +up +the +poisonous +comb +which +pleased +Snow +White +so +well +that +she +opened +the +door +and +bought +it +Come +let +me +comb +your +hair +in +the +newest +way +said +the +woman +and +the +poor +unsuspicious +child +let +her +have +her +way +but +no +sooner +did +the +comb +touch +her +hair +than +the +poison +began +to +work +and +she +fell +fainting +to +the +ground +There +you +model +of +beauty +said +the +wicked +woman +as +she +went +away +you +are +done +for +at +last +But +fortunately +it +was +almost +time +for +the +dwarfs +to +come +home +and +as +soon +as +they +came +in +and +found +Snow +White +lying +upon +the +ground +they +guessed +that +her +wicked +step +mother +had +been +there +again +and +set +to +work +to +find +out +what +was +wrong +They +soon +saw +the +poisonous +comb +and +drew +it +out +and +almost +immediately +Snow +White +began +to +recover +and +told +them +what +had +happened +Once +more +they +warned +her +to +be +on +her +guard +and +to +open +the +door +to +no +one +When +the +Queen +reached +home +she +went +straight +to +the +mirror +and +said +Mirror +mirror +on +the +wall +Who +is +the +fairest +fair +of +all +And +the +mirror +answered +O +Lady +Queen +though +fair +ye +be +Snow +White +is +fairer +far +to +see +Over +the +hills +and +far +away +She +dwells +with +seven +dwarfs +to +day +When +the +Queen +heard +these +words +she +shook +with +rage +Snow +White +shall +die +she +cried +even +if +it +costs +me +my +own +life +to +manage +it +She +went +into +a +secret +chamber +where +no +one +else +ever +entered +and +there +she +made +a +poisonous +apple +and +then +she +painted +her +face +and +disguised +herself +as +a +peasant +woman +and +climbed +the +seven +mountains +and +went +to +the +dwarfs’ +house +She +knocked +at +the +door +Snow +White +put +her +head +out +of +the +window +and +said +I +must +not +let +anyone +in +the +seven +dwarfs +have +forbidden +me +to +do +so +It’s +all +the +same +to +me +answered +the +peasant +woman +I +shall +soon +get +rid +of +these +fine +apples +But +before +I +go +I’ll +make +you +a +present +of +one +Oh +No +said +Snow +White +for +I +must +not +take +it +Surely +you +are +not +afraid +of +poison +said +the +woman +See +I +will +cut +one +in +two +the +rosy +cheek +you +shall +take +and +the +white +cheek +I +will +eat +myself +Now +the +apple +had +been +so +cleverly +made +that +only +the +rose +cheeked +side +contained +the +poison +Snow +White +longed +for +the +delicious +looking +fruit +and +when +she +saw +that +the +woman +ate +half +of +it +she +thought +there +could +be +no +danger +and +stretched +out +her +hand +and +took +the +other +part +But +no +sooner +had +she +tasted +it +than +she +fell +down +dead +The +wicked +Queen +laughed +aloud +with +joy +as +she +gazed +at +her +White +as +snow +red +as +blood +black +as +ebony +she +said +this +time +the +dwarfs +cannot +awaken +you +And +she +went +straight +home +and +asked +her +mirror +Mirror +mirror +upon +the +wall +Who +is +the +fairest +fair +of +all +And +at +length +it +answered +Thou +O +Queen +art +fairest +of +all +So +her +envious +heart +had +peace +at +least +so +much +peace +as +an +envious +heart +can +have +When +the +little +dwarfs +came +home +at +night +they +found +Snow +White +lying +upon +the +ground +No +breath +came +from +her +parted +lips +for +she +was +dead +They +lifted +her +tenderly +and +sought +for +some +poisonous +object +which +might +have +caused +the +mischief +unlaced +her +frock +combed +her +hair +and +washed +her +with +wine +and +water +but +all +in +vain +dead +she +was +and +dead +she +remained +They +laid +her +upon +a +bier +and +all +seven +of +them +sat +round +about +it +and +wept +as +though +their +hearts +would +break +for +three +whole +days +When +the +time +came +that +she +should +be +laid +in +the +ground +they +could +not +bear +to +part +from +her +Her +pretty +cheeks +were +still +rosy +red +and +she +looked +just +as +though +she +were +still +living +We +cannot +hide +her +away +in +the +dark +earth +said +the +dwarfs +and +so +they +made +a +transparent +coffin +of +shining +glass +and +laid +her +in +it +and +wrote +her +name +upon +it +in +letters +of +gold +also +they +wrote +that +she +was +a +King’s +daughter +Then +they +placed +the +coffin +upon +the +mountain +top +and +took +it +in +turns +to +watch +beside +it +And +all +the +animals +came +and +wept +for +Snow +White +first +an +owl +then +a +raven +and +then +a +little +dove +For +a +long +long +time +little +Snow +White +lay +in +the +coffin +but +her +form +did +not +wither +she +only +looked +as +though +she +slept +for +she +was +still +as +white +as +snow +as +red +as +blood +and +as +black +as +ebony +It +chanced +that +a +King’s +son +came +into +the +wood +and +went +to +the +dwarfs’ +house +meaning +to +spend +the +night +there +He +saw +the +coffin +upon +the +mountain +top +with +little +Snow +White +lying +within +it +and +he +read +the +words +that +were +written +upon +it +in +letters +of +gold +And +he +said +to +the +dwarfs +If +you +will +but +let +me +have +the +coffin +you +may +ask +of +me +what +you +will +and +I +will +give +it +to +you +But +the +dwarfs +answered +We +would +not +sell +it +for +all +the +gold +in +the +world +Then +said +the +Prince +Let +me +have +it +as +a +gift +I +pray +you +for +I +cannot +live +without +seeing +little +Snow +White +and +I +will +prize +your +gift +as +the +dearest +of +my +possessions +The +good +little +dwarfs +pitied +him +when +they +heard +these +words +and +so +gave +him +the +coffin +The +King’s +son +then +bade +his +servants +place +it +upon +their +shoulders +and +carry +it +away +but +as +they +went +they +stumbled +over +the +stump +of +a +tree +and +the +violent +shaking +shook +the +piece +of +poisonous +apple +which +had +lodged +in +Snow +White’s +throat +out +again +so +that +she +opened +her +eyes +raised +the +lid +of +the +coffin +and +sat +up +alive +once +more +Where +am +I +she +cried +and +the +happy +Prince +answered +Thou +art +with +me +dearest +Then +he +told +her +all +that +had +happened +and +how +he +loved +her +better +than +the +whole +world +and +begged +her +to +go +with +him +to +his +father’s +palace +and +be +his +wife +Snow +White +consented +and +went +with +him +and +the +wedding +was +celebrated +with +great +splendour +and +magnificence +Little +Snow +White’s +wicked +step +mother +was +bidden +to +the +feast +and +when +she +had +arrayed +herself +in +her +most +beautiful +garments +she +stood +before +her +mirror +and +said +Mirror +mirror +upon +the +wall +Who +is +the +fairest +fair +of +all +And +the +mirror +answered +O +Lady +Queen +though +fair +ye +be +The +young +Queen +is +fairer +to +see +Oh +How +angry +the +wicked +woman +was +then +and +so +terrified +too +that +she +scarcely +knew +what +to +do +At +first +she +thought +she +would +not +go +to +the +wedding +at +all +but +then +she +felt +that +she +could +not +rest +until +she +had +seen +the +young +Queen +No +sooner +did +she +enter +the +palace +than +she +recognized +little +Snow +White +and +could +not +move +for +terror +Then +a +pair +of +iron +shoes +was +brought +into +the +room +and +set +before +her +and +these +she +was +forced +to +put +on +and +to +dance +in +them +until +she +could +dance +no +longer +but +fell +down +dead +and +that +was +the +end +of +her \ No newline at end of file diff --git a/TagCloud/Tag.cs b/TagCloud/Tag.cs new file mode 100644 index 000000000..ee37ae006 --- /dev/null +++ b/TagCloud/Tag.cs @@ -0,0 +1,6 @@ +using System.Drawing; + +namespace TagCloud +{ + internal record Tag(string Text, Rectangle Rectangle); +} diff --git a/TagCloud/TagCloud.csproj b/TagCloud/TagCloud.csproj new file mode 100644 index 000000000..f89c5ddcb --- /dev/null +++ b/TagCloud/TagCloud.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/TagCloud/WordCounters/IWordCounter.cs b/TagCloud/WordCounters/IWordCounter.cs new file mode 100644 index 000000000..f41d1763b --- /dev/null +++ b/TagCloud/WordCounters/IWordCounter.cs @@ -0,0 +1,9 @@ +namespace TagCloud.WordCounters +{ + // Интерфейс подсчёта количества каждого уникального слова + internal interface IWordCounter + { + public void AddWord(string word); + public Dictionary Values { get; } + } +} diff --git a/TagCloud/WordCounters/WordCounter.cs b/TagCloud/WordCounters/WordCounter.cs new file mode 100644 index 000000000..39e279357 --- /dev/null +++ b/TagCloud/WordCounters/WordCounter.cs @@ -0,0 +1,14 @@ +namespace TagCloud.WordCounters +{ + internal class WordCounter : IWordCounter + { + private readonly Dictionary counts = new Dictionary(); + public Dictionary Values => counts.ToDictionary(); + + public void AddWord(string word) + { + counts.TryGetValue(word, out uint value); + counts[word] = value + 1; + } + } +} diff --git a/TagCloud/WordFilters/IWordFilter.cs b/TagCloud/WordFilters/IWordFilter.cs new file mode 100644 index 000000000..6a546151a --- /dev/null +++ b/TagCloud/WordFilters/IWordFilter.cs @@ -0,0 +1,10 @@ +namespace TagCloud.WordFilters +{ + // Интерфейс фильтрации "скучных" слов + internal interface IWordFilter + { + public bool Add(string word); + public bool Remove(string word); + public bool IsCorrectWord(string word); + } +} diff --git a/TagCloud/WordFilters/WordFilter.cs b/TagCloud/WordFilters/WordFilter.cs new file mode 100644 index 000000000..aea059b33 --- /dev/null +++ b/TagCloud/WordFilters/WordFilter.cs @@ -0,0 +1,56 @@ +namespace TagCloud.WordFilters +{ + // Реализован пункт на перспективу: + // Предобработка слов. + // Дать возможность влиять на список скучных слов, которые не попадут в облако. + internal class WordFilter : IWordFilter + { + private readonly HashSet bannedWords = new HashSet() + { + // Просто скучные по моему мнению + "not", "also", "how", "let", + // To have + "have", "has", "had", "having", + // To be + "am", "is", "are", "was", "were", "be", "been", "being", + // Артикли + "a", "an", "the", + // Местоимения + "i", "you", "he", "she", "it", "we", "they", "me", "him", + "her", "us", "them", "my", "your", "his", "its", "our", "their", + "mine", "yours", "hers", "theirs", "myself", "yourself", "himself", + "herself", "itself", "ourselves", "yourselves", "themselves", "this", + "that", "these", "those", "who", "whom", "whose", "what", "which", + "some", "any", "none", "all", "many", "few", "several", + "everyone", "somebody", "anybody", "nobody", "everything", "anything", + "nothing", "each", "every", "either", "neither", + // Предлоги + "about", "above", "across", "after", "against", "along", "amid", "among", + "around", "as", "at", "before", "behind", "below", "beneath", "beside", + "besides", "between", "beyond", "but", "by", "despite", "down", "during", + "except", "for", "from", "in", "inside", "into", "like", "near", "of", "off", + "on", "onto", "out", "outside", "over", "past", "since", "through", "throughout", + "till", "to", "toward", "under", "underneath", "until", "up", "upon", "with", + "within", "without", + // Союзы + "and", "but", "or", "nor", "for", "yet", "so", "if", "because", "although", "though", + "since", "until", "unless", "while", "whereas", "when", "where", "before", "after", + // Междометия + "o","ah", "aha", "alas", "aw", "aye", "eh", "hmm", "huh", "hurrah", "no", "oh", "oops", + "ouch", "ow", "phew", "shh", "tsk", "ugh", "um", "wow", "yay", "yes", "yikes" + }; + + public bool Add(string word) => bannedWords.Add(word); + + public bool Remove(string word) => bannedWords.Remove(word); + + public void Clear() => bannedWords.Clear(); + + public HashSet BannedWords => bannedWords.ToHashSet(); + + public bool IsCorrectWord(string word) + { + return !bannedWords.Contains(word); + } + } +} diff --git a/TagCloud/WordReaders/IWordReader.cs b/TagCloud/WordReaders/IWordReader.cs new file mode 100644 index 000000000..37acea4ea --- /dev/null +++ b/TagCloud/WordReaders/IWordReader.cs @@ -0,0 +1,10 @@ +using FileSenderRailway; + +namespace TagCloud.WordReaders +{ + // Интерфейс для построчного чтения содержимого файла + internal interface IWordReader + { + public Result> ReadByLines(string path); + } +} diff --git a/TagCloud/WordReaders/WordReader.cs b/TagCloud/WordReaders/WordReader.cs new file mode 100644 index 000000000..69aa5a77e --- /dev/null +++ b/TagCloud/WordReaders/WordReader.cs @@ -0,0 +1,32 @@ +using FileSenderRailway; + +namespace TagCloud.WordReaders +{ + internal class WordReader : IWordReader + { + public Result> ReadByLines(string path) + { + if (!File.Exists(path)) + { + return Result.Fail>($"Файл \"{path}\" не существует"); + } + + if (path.Split('.')[^1] != "txt") + { + return Result.Fail>($"Файл \"{path}\" должен иметь расширение \"txt\""); + } + + var lines = File.ReadAllLines(path); + if (lines.Length == 0) + { + return Result.Fail>($"Файл \"{path}\" пустой"); + } + if (lines.Any(line => line.Contains(' '))) + { + return Result.Fail>($"Файл \"{path}\" содержит строку с двумя и более словами"); + } + + return lines.AsEnumerable().AsResult(); + } + } +} \ No newline at end of file diff --git a/fp.sln b/fp.sln index d104ab530..6a1522eb2 100644 --- a/fp.sln +++ b/fp.sln @@ -1,14 +1,21 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSenderRailway", "FileSenderRailway\FileSenderRailway.csproj", "{D979A1EA-516A-46BC-BE6C-8845CA10853D}" +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileSenderRailway", "FileSenderRailway\FileSenderRailway.csproj", "{D979A1EA-516A-46BC-BE6C-8845CA10853D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErrorHandling", "ErrorHandling\ErrorHandling.csproj", "{66FAF276-533D-4733-AB2E-A9905D678CF6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ErrorHandling", "ErrorHandling\ErrorHandling.csproj", "{66FAF276-533D-4733-AB2E-A9905D678CF6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{754C1CC8-A8B6-46C6-B35C-8A43B80111A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Summator", "Samples\Summator\Summator.csproj", "{C33F3A5E-A1ED-4657-9B35-968A4CB23AA1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Summator", "Samples\Summator\Summator.csproj", "{C33F3A5E-A1ED-4657-9B35-968A4CB23AA1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConwaysGameOfLife", "Samples\ConwaysGameOfLife\ConwaysGameOfLife.csproj", "{4B77EC28-5FB5-4095-B3D7-127F5C488D6E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConwaysGameOfLife", "Samples\ConwaysGameOfLife\ConwaysGameOfLife.csproj", "{4B77EC28-5FB5-4095-B3D7-127F5C488D6E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloud", "TagCloud\TagCloud.csproj", "{4B940669-E063-446F-AE0C-6DDF3725D758}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloud.Tests", "TagCloud.Tests\TagCloud.Tests.csproj", "{F738E726-1084-490D-A000-BD7B3F50D554}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -32,9 +39,23 @@ Global {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {4B940669-E063-446F-AE0C-6DDF3725D758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B940669-E063-446F-AE0C-6DDF3725D758}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B940669-E063-446F-AE0C-6DDF3725D758}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B940669-E063-446F-AE0C-6DDF3725D758}.Release|Any CPU.Build.0 = Release|Any CPU + {F738E726-1084-490D-A000-BD7B3F50D554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F738E726-1084-490D-A000-BD7B3F50D554}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F738E726-1084-490D-A000-BD7B3F50D554}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F738E726-1084-490D-A000-BD7B3F50D554}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {C33F3A5E-A1ED-4657-9B35-968A4CB23AA1} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0} {4B77EC28-5FB5-4095-B3D7-127F5C488D6E} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {32B979F8-E2E6-47D9-B6B1-02CA478687F8} + EndGlobalSection EndGlobal