diff --git a/TagCloud.Tests/CloudLayouterPaintersTest/CloudLayouterPainterTest.cs b/TagCloud.Tests/CloudLayouterPaintersTest/CloudLayouterPainterTest.cs new file mode 100644 index 00000000..549ca468 --- /dev/null +++ b/TagCloud.Tests/CloudLayouterPaintersTest/CloudLayouterPainterTest.cs @@ -0,0 +1,30 @@ +using System.Drawing; +using TagCloud.CloudLayouterPainters; + +namespace TagCloud.Tests.CloudLayouterPaintersTest +{ + internal class CloudLayouterPainterTest + { + private CloudLayouterPainter painter; + + [SetUp] + public void SetUp() + { + painter = new CloudLayouterPainter(new Size(1, 1)); + } + + [Test] + public void Draw_ThrowsArgumentException_WithEmptyTags() + { + var painter = new CloudLayouterPainter(new Size(1, 1)); + Assert.Throws(() => painter.Draw(new List())); + } + + [Test] + public void Draw_ThrowsArgumentNullException_WithTagsAsNull() + { + var painter = new CloudLayouterPainter(new Size(1, 1)); + Assert.Throws(() => painter.Draw(null!)); + } + } +} diff --git a/TagCloud.Tests/CloudLayouterTests/CircularCloudLayouterTests/CircularCloudLayouterMainRequirementsTest.cs b/TagCloud.Tests/CloudLayouterTests/CircularCloudLayouterTests/CircularCloudLayouterMainRequirementsTest.cs new file mode 100644 index 00000000..cc2f2dfc --- /dev/null +++ b/TagCloud.Tests/CloudLayouterTests/CircularCloudLayouterTests/CircularCloudLayouterMainRequirementsTest.cs @@ -0,0 +1,185 @@ +using FluentAssertions; +using System.Drawing; +using TagCloud.CloudLayouterPainters; +using TagCloud.CloudLayouters.CircularCloudLayouter; +using TagCloud.CloudLayouterWorkers; +using TagCloud.ImageSavers; +using TagCloud.Tests.Extensions; + +namespace TagCloud.Tests.CloudLayouterTests.CircularCloudLayouterTests +{ + [TestFixture] + internal class CircularCloudLayouterMainRequirementsTest + { + private Point center = new Point(); + private Rectangle[] rectangles; + private List tags; + private readonly string failedTestsDirectory = "FailedTest"; + + private readonly ImageSaver imageSaver = new ImageSaver(); + private readonly CloudLayouterPainter cloudLayouterPainter + = new CloudLayouterPainter(new Size(5000, 5000)); + + [OneTimeSetUp] + public void Init() + { + Directory.CreateDirectory(failedTestsDirectory); + } + + [SetUp] + public void SetUp() + { + var minRectangleWidth = 30; + var maxRectangleWidth = 70; + var minRectangleHeight = 20; + var maxRectangleHeight = 50; + var rectanglesCount = 1000; + + tags = new List(); + var circularCloudLayouter = new CircularCloudLayouter(); + + var randomWorker = new RandomCloudLayouterWorker( + minRectangleWidth, + maxRectangleWidth, + minRectangleHeight, + maxRectangleHeight); + foreach (var rectangleProperty in randomWorker + .GetNextRectangleProperties().Take(rectanglesCount)) + { + tags.Add( + new Tag( + rectangleProperty.word, + circularCloudLayouter.PutNextRectangle(rectangleProperty.size))); + } + rectangles = tags.Select(x => x.Rectangle).ToArray(); + } + + [TestCase(0.7, 1000)] + [Repeat(10)] + public void ShouldPlaceRectanglesInCircle(double expectedCoverageRatio, int gridSize) + { + var maxRadius = rectangles.Max( + x => x.GetMaxDistanceFromPointToRectangleAngles(center)); + var step = 2 * maxRadius / gridSize; + + var occupancyGrid = GetOccupancyGrid(gridSize, maxRadius, step); + + var actualCoverageRatio = GetOccupancyGridRatio(occupancyGrid, maxRadius, step); + actualCoverageRatio.Should().BeGreaterThanOrEqualTo(expectedCoverageRatio); + } + + [TestCase(15)] + [Repeat(10)] + public void ShouldPlaceCenterOfMassOfRectanglesNearCenter(int tolerance) + { + 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() + { + for (var i = 0; i < rectangles.Length; i++) + { + for (var j = i + 1; j < rectangles.Length; j++) + { + Assert.That( + rectangles[i].IntersectsWith(rectangles[j]) == false, + $"Прямоугольники пересекаются:\n" + + $"{rectangles[i].ToString()}\n" + + $"{rectangles[j].ToString()}"); + } + } + } + + [TearDown] + public void Cleanup() + { + if (TestContext.CurrentContext.Result.FailCount == 0) + { + return; + } + + var name = $"{TestContext.CurrentContext.Test.Name}.png"; + var path = Path.Combine(failedTestsDirectory, name); + imageSaver.SaveFile(cloudLayouterPainter.Draw(tags), 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) + { + 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/CloudLayouterTests/CircularCloudLayouterTests/CircularCloudLayouterTest.cs b/TagCloud.Tests/CloudLayouterTests/CircularCloudLayouterTests/CircularCloudLayouterTest.cs new file mode 100644 index 00000000..8a5ed0e7 --- /dev/null +++ b/TagCloud.Tests/CloudLayouterTests/CircularCloudLayouterTests/CircularCloudLayouterTest.cs @@ -0,0 +1,22 @@ +using System.Drawing; +using TagCloud.CloudLayouters.CircularCloudLayouter; + +namespace TagCloud.Tests.CloudLayouterTests.CircularCloudLayouterTests +{ + [TestFixture] + internal class CircularCloudLayouterTest + { + [TestCase(0, 100)] + [TestCase(-1, 100)] + [TestCase(100, 0)] + [TestCase(100, -1)] + public void PutNextRectangle_ThrowsArgumentException_OnAnyNegativeOrZeroSize( + int width, + int height) + { + var size = new Size(width, height); + Assert.Throws( + () => new CircularCloudLayouter().PutNextRectangle(size)); + } + } +} diff --git a/TagCloud.Tests/CloudLayouterWorkersTests/NormalizedFrequencyBasedCloudLayouterWorkerTest.cs b/TagCloud.Tests/CloudLayouterWorkersTests/NormalizedFrequencyBasedCloudLayouterWorkerTest.cs new file mode 100644 index 00000000..a6d75e2f --- /dev/null +++ b/TagCloud.Tests/CloudLayouterWorkersTests/NormalizedFrequencyBasedCloudLayouterWorkerTest.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using System.Drawing; +using TagCloud.CloudLayouterWorkers; + +namespace TagCloud.Tests.CloudLayouterWorkersTests +{ + internal class NormalizedFrequencyBasedCloudLayouterWorkerTest + { + 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_ThrowsArgumentException_OnAnyNegativeOrZeroSize( + int width, + int height) + { + Assert.Throws( + () => new NormalizedFrequencyBasedCloudLayouterWorker(width, height, normalizedValues)); + } + + [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()) + { + 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/RandomCloudLayouterWorkerTest.cs b/TagCloud.Tests/CloudLayouterWorkersTests/RandomCloudLayouterWorkerTest.cs new file mode 100644 index 00000000..81f71e26 --- /dev/null +++ b/TagCloud.Tests/CloudLayouterWorkersTests/RandomCloudLayouterWorkerTest.cs @@ -0,0 +1,32 @@ +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_ThrowsArgumentException_OnAnyNegativeOrZeroSize( + int width, + int height) + { + Assert.Throws( + () => new RandomCloudLayouterWorker(width, width, height, height)); + } + + [TestCase(50, 25, 25, 50)] + [TestCase(25, 50, 50, 25)] + public void GetNextRectangleSize_ThrowsArgumentException_OnNonConsecutiveSizeValues( + int minWidth, + int maxWidth, + int minHeight, + int maxHeight) + { + Assert.Throws( + () => new RandomCloudLayouterWorker(minWidth, maxWidth, minHeight, maxHeight)); + } + } +} diff --git a/TagCloud.Tests/Extensions/RectangleExtensions.cs b/TagCloud.Tests/Extensions/RectangleExtensions.cs new file mode 100644 index 00000000..d4c8aa55 --- /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/GlobalSuppressions.cs b/TagCloud.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..cdd580ff --- /dev/null +++ b/TagCloud.Tests/GlobalSuppressions.cs @@ -0,0 +1,10 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +// Так делать явно плохо +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.Tests.ImageSaversTests.ImageSaverTest.SaveFile_SavesFile(System.String,System.String)~System.Boolean")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.Tests.ImageSaversTests.ImageSaverTest.SaveFile_ThrowsArgumentException_WithInvalidFilename(System.String)")] diff --git a/TagCloud.Tests/ImageSaversTests/ImageSaverTest.cs b/TagCloud.Tests/ImageSaversTests/ImageSaverTest.cs new file mode 100644 index 00000000..5a4b1bb5 --- /dev/null +++ b/TagCloud.Tests/ImageSaversTests/ImageSaverTest.cs @@ -0,0 +1,62 @@ +using System.Drawing; +using TagCloud.ImageSavers; + +namespace TagCloud.Tests.ImageSaversTests +{ + [TestFixture] + internal class ImageSaverTest + { + private string directoryPath = "TempFilesForImageSaverTests"; + private ImageSaver imageSaver; + + [OneTimeSetUp] + public void Init() + { + Directory.CreateDirectory(directoryPath); + } + + [SetUp] + public void SetUp() + { + imageSaver = new ImageSaver(); + } + + [TestCase("Test")] + public void SaveFile_ArgumentNullException_WithNullBitmap(string filename) + { + var path = Path.Combine(directoryPath, filename); + Assert.Throws(() => imageSaver.SaveFile(null!, path)); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void SaveFile_ThrowsArgumentException_WithInvalidFilename(string? filename) + { + var dummyImage = new Bitmap(1, 1); + Assert.Throws(() => imageSaver.SaveFile(dummyImage, filename!)); + } + + [TestCase("Test", "png", ExpectedResult = true)] + [TestCase("Test", "bmp", ExpectedResult = true)] + public bool SaveFile_SavesFile(string filename, string format) + { + var dummyImage = new Bitmap(1, 1); + var path = Path.Combine(directoryPath, filename); + + File.Delete(path); + imageSaver.SaveFile(dummyImage, path, format); + return File.Exists($"{path}.{format}"); + } + + + [OneTimeTearDown] + public void OneTimeCleanup() + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, true); + } + } + } +} diff --git a/TagCloud.Tests/MainTest.cs b/TagCloud.Tests/MainTest.cs new file mode 100644 index 00000000..8673add3 --- /dev/null +++ b/TagCloud.Tests/MainTest.cs @@ -0,0 +1,64 @@ +using Autofac; +using FluentAssertions; +namespace TagCloud.Tests +{ + [TestFixture] + internal class MainTest + { + private static string directoryPath = "TempFilesForMainTest"; + private static readonly string dataFile = Path.Combine(directoryPath, "TestData.txt"); + private readonly string imageFile = Path.Combine(directoryPath, "Test"); + + [OneTimeSetUp] + public void Init() + { + Directory.CreateDirectory(directoryPath); + File.WriteAllLines(dataFile, new string[] + { + "One", + "One", + "Two", + "Three", + "Four", + "Four", + "Four", + "Four" + }); + } + + [TestCase("bmp")] + public void Program_ExecutesSuccessfully_WithValidArguments(string format) + { + var options = new CommandLineOptions + { + BackgroundColor = "Red", + TextColor = "Blue", + Font = "Calibri", + IsSorted = true.ToString(), + ImageSize = "1000:1000", + MaxRectangleHeight = 100, + MaxRectangleWidth = 200, + ImageFileName = imageFile, + DataFileName = dataFile, + ResultFormat = format + }; + + var container = DIContainer.ConfigureContainer(options); + using var scope = container.BeginLifetimeScope(); + var executor = scope.Resolve(); + + Assert.DoesNotThrow(() => executor.Execute()); + + File.Exists($"{imageFile}.{format}").Should().BeTrue(); + } + + [OneTimeTearDown] + public void OneTimeCleanup() + { + if (Directory.Exists(directoryPath)) + { + Directory.Delete(directoryPath, true); + } + } + } +} diff --git a/TagCloud.Tests/NormalizersTest/NormalizerTest.cs b/TagCloud.Tests/NormalizersTest/NormalizerTest.cs new file mode 100644 index 00000000..92121b77 --- /dev/null +++ b/TagCloud.Tests/NormalizersTest/NormalizerTest.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using TagCloud.Normalizers; + +namespace TagCloud.Tests.WordCountersTests +{ + [TestFixture] + internal class NormalizerTest + { + 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)] + public void Normalize_ThrowsArgumentException_WithMinCoefficientLessThanZero( + double minCoefficient) + { + Assert.Throws(() + => normalizer.Normalize(values, minCoefficient, defaultDecimalPlaces)); + } + + [TestCase(0.25, 4)] + [TestCase(0.25, 2)] + public void Normalize_CalculatesСorrectly(double minCoefficient, int decimalPlaces) + { + foreach (var pair in expectedResult) + { + expectedResult[pair.Key] = Math.Round(pair.Value, decimalPlaces); + } + var actual = normalizer.Normalize(values, minCoefficient, decimalPlaces); + + actual.Should().BeEquivalentTo(expectedResult); + } + } +} diff --git a/TagCloud.Tests/TagCloud.Tests.csproj b/TagCloud.Tests/TagCloud.Tests.csproj new file mode 100644 index 00000000..391e86bc --- /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/WordCountersTests/WordCounterTest.cs b/TagCloud.Tests/WordCountersTests/WordCounterTest.cs new file mode 100644 index 00000000..5a2a698f --- /dev/null +++ b/TagCloud.Tests/WordCountersTests/WordCounterTest.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using TagCloud.WordCounters; + +namespace TagCloud.Tests.WordCountersTests +{ + [TestFixture] + internal class WordCounterTest + { + private WordCounter wordCounter; + + [SetUp] + public void SetUp() + { + wordCounter = new WordCounter(); + } + + [Test] + public void WordCounter_CountsCorrect() + { + var expected = new Dictionary() + { + { "One", 2 }, + { "Two", 1 }, + { "Three", 1 }, + { "Four", 4 }, + }; + var values = new string[] + { + "One", + "One", + "Two", + "Three", + "Four", + "Four", + "Four", + "Four" + }; + + foreach (var value in values) + { + 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 00000000..5dea1302 --- /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/WordFilterChangeBannedWordsTest.cs b/TagCloud.Tests/WordFiltersTests/WordFilterChangeBannedWordsTest.cs new file mode 100644 index 00000000..01dbdbfd --- /dev/null +++ b/TagCloud.Tests/WordFiltersTests/WordFilterChangeBannedWordsTest.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using TagCloud.WordFilters; + +namespace TagCloud.Tests.WordFiltersTests +{ + [TestFixture] + internal class WordFilterChangeBannedWordsTest + { + private WordFilter wordFilter; + + [SetUp] + public void SetUp() + { + wordFilter = new WordFilter(); + } + + [Test] + public void Clear_ShouldClearBannedWordList() + { + wordFilter.Clear(); + wordFilter.BannedWords.Should().BeEmpty(); + } + + [TestCase("WordToAdd")] + public void Add_ShouldAddWord_ToBannedWords(string word) + { + wordFilter.Clear(); + wordFilter.Add(word); + wordFilter.BannedWords.Should().Contain(word).And.HaveCount(1); + } + + [TestCase("WordToRemove")] + public void Remove_ShouldRemoveWord_InBannedWords(string word) + { + wordFilter.Clear(); + wordFilter.Add(word); + wordFilter.Remove(word); + wordFilter.BannedWords.Should().NotContain(word); + } + } +} diff --git a/TagCloud.Tests/WordFiltersTests/WordFilterDefaultBannedWordsTest.cs b/TagCloud.Tests/WordFiltersTests/WordFilterDefaultBannedWordsTest.cs new file mode 100644 index 00000000..6d6f9a43 --- /dev/null +++ b/TagCloud.Tests/WordFiltersTests/WordFilterDefaultBannedWordsTest.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using TagCloud.WordFilters; + +namespace TagCloud.Tests.WordFiltersTests +{ + [TestFixture] + internal class WordFilterDefaultBannedWordsTest + { + 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/WordReaderTest.cs b/TagCloud.Tests/WordReadersTests/WordReaderTest.cs new file mode 100644 index 00000000..1f0aea18 --- /dev/null +++ b/TagCloud.Tests/WordReadersTests/WordReaderTest.cs @@ -0,0 +1,80 @@ +using TagCloud.WordReaders; + +namespace TagCloud.Tests.WordReadersTests +{ + [TestFixture] + internal class WordReaderTest + { + private readonly string directoryPath = "TempFilesForWordReaderTests"; + + private readonly string fileWithCorrectValuesPath = "correctFile.txt"; + private readonly string[] correctValues = new string[] + { + "One", + "One", + "Two", + "Three", + "Four", + "Four", + "Four", + "Four" + }; + + private readonly string fileWithIncorrectValuesPath = "incorrectFile.txt"; + private readonly string[] incorrectValues = new string[] + { + "One", + "Two", + "Three Three", + "Four" + }; + + private WordReader wordReader; + + [OneTimeSetUp] + public void Init() + { + Directory.CreateDirectory(directoryPath); + File.WriteAllLines( + Path.Combine( + directoryPath, + fileWithCorrectValuesPath), + correctValues); + File.WriteAllLines + (Path.Combine( + directoryPath, + fileWithIncorrectValuesPath), + incorrectValues); + } + + [SetUp] + public void SetUp() + { + wordReader = new WordReader(); + } + + [TestCase(" ")] + [TestCase("ThisFileDoesNotExist.txt")] + public void WordReader_ThrowsFileNotFoundException_WithInvalidFilename(string filename) + { + var path = Path.Combine(directoryPath, filename); + Assert.Throws(() => wordReader.ReadByLines(path).ToArray()); + } + + [Test] + public void WordReader_ThrowsException_WithTwoWordsInOneLine() + { + var path = Path.Combine(directoryPath, fileWithIncorrectValuesPath); + Assert.Throws(() => wordReader.ReadByLines(path).ToArray()); + } + + [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 00000000..ca883812 --- /dev/null +++ b/TagCloud/CloudLayouterPainters/CloudLayouterPainter.cs @@ -0,0 +1,87 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouterPainters +{ + 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 Bitmap Draw(IList tags) + { + ArgumentNullException.ThrowIfNull(tags); + + if (tags.Count == 0) + { + throw new ArgumentException("Список тегов пуст"); + } + + var result = new Bitmap(imageSize.Width, imageSize.Height); + + using var graphics = Graphics.FromImage(result); + 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 result; + } + + 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); + 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; + } + } +} diff --git a/TagCloud/CloudLayouterPainters/ICloudLayouterPainter.cs b/TagCloud/CloudLayouterPainters/ICloudLayouterPainter.cs new file mode 100644 index 00000000..38fe25ab --- /dev/null +++ b/TagCloud/CloudLayouterPainters/ICloudLayouterPainter.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouterPainters +{ + // Интерфейс отрисовки прямоугольников + internal interface ICloudLayouterPainter + { + public Bitmap Draw(IList tags); + } +} diff --git a/TagCloud/CloudLayouterWorkers/ICloudLayouterWorker.cs b/TagCloud/CloudLayouterWorkers/ICloudLayouterWorker.cs new file mode 100644 index 00000000..befcb3a4 --- /dev/null +++ b/TagCloud/CloudLayouterWorkers/ICloudLayouterWorker.cs @@ -0,0 +1,12 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouterWorkers +{ + // Интерфейс получения свойств следующего прямоугольника + // По хорошему, нужно возвращать IEnumerable, + // для повышения возможности переиспользования + internal interface ICloudLayouterWorker + { + public IEnumerable<(string word, Size size)> GetNextRectangleProperties(); + } +} diff --git a/TagCloud/CloudLayouterWorkers/NormalizedFrequencyBasedCloudLayouterWorker.cs b/TagCloud/CloudLayouterWorkers/NormalizedFrequencyBasedCloudLayouterWorker.cs new file mode 100644 index 00000000..8bda8ce4 --- /dev/null +++ b/TagCloud/CloudLayouterWorkers/NormalizedFrequencyBasedCloudLayouterWorker.cs @@ -0,0 +1,49 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouterWorkers +{ + internal class NormalizedFrequencyBasedCloudLayouterWorker : ICloudLayouterWorker + { + public readonly int MaxRectangleWidth; + public readonly int MaxRectangleHeight; + private readonly Dictionary values; + private readonly string[] keysOrder; + public string[] KeysOrder => keysOrder; + + public NormalizedFrequencyBasedCloudLayouterWorker( + int maxRectangleWidth, + int maxRectangleHeight, + Dictionary normalizedValues, + bool isSorted = true) + { + if (maxRectangleWidth <= 0 || maxRectangleHeight <= 0) + { + throw new ArgumentException( + "Ширина или высота прямоугольника должна быть положительной"); + } + + MaxRectangleWidth = maxRectangleWidth; + MaxRectangleHeight = maxRectangleHeight; + values = normalizedValues; + if (isSorted) + { + keysOrder = values.OrderByDescending(x => x.Value).Select(x => x.Key).ToArray(); + } + else + { + keysOrder = values.Keys.ToArray(); + } + } + + public IEnumerable<(string word, Size size)> GetNextRectangleProperties() + { + 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)); + } + } + } +} diff --git a/TagCloud/CloudLayouterWorkers/RandomCloudLayouterWorker.cs b/TagCloud/CloudLayouterWorkers/RandomCloudLayouterWorker.cs new file mode 100644 index 00000000..b05b6a47 --- /dev/null +++ b/TagCloud/CloudLayouterWorkers/RandomCloudLayouterWorker.cs @@ -0,0 +1,52 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouterWorkers +{ + // Класс, со старого задания TagCloud, + // выдающий случайный размер прямоугольника + // Оставил его для пары тестов. + internal class RandomCloudLayouterWorker : ICloudLayouterWorker + { + private Random random = new Random(); + public readonly int MinRectangleWidth; + public readonly int MaxRectangleWidth; + public readonly int MinRectangleHeight; + public readonly int MaxRectangleHeight; + + public RandomCloudLayouterWorker( + int minRectangleWidth, + int maxRectangleWidth, + int minRectangleHeight, + int maxRectangleHeight) + { + if (minRectangleWidth <= 0 || maxRectangleWidth <= 0 + || minRectangleHeight <= 0 || maxRectangleHeight <= 0) + { + throw new ArgumentException( + "Ширина или высота прямоугольника должна быть положительной"); + } + + if (minRectangleWidth > maxRectangleWidth + || minRectangleHeight > maxRectangleHeight) + { + throw new ArgumentException( + "Минимальное значение ширины или высоты не может быть больше максимального"); + } + + MinRectangleWidth = minRectangleWidth; + MaxRectangleWidth = maxRectangleWidth; + MinRectangleHeight = minRectangleHeight; + MaxRectangleHeight = maxRectangleHeight; + } + + public IEnumerable<(string word, Size size)> GetNextRectangleProperties() + { + while (true) + { + var width = random.Next(MinRectangleWidth, MaxRectangleWidth); + var height = random.Next(MinRectangleHeight, MaxRectangleHeight); + yield return (string.Empty, new Size(width, height)); + } + } + } +} diff --git a/TagCloud/CloudLayouters/CircularCloudLayouter/Circle.cs b/TagCloud/CloudLayouters/CircularCloudLayouter/Circle.cs new file mode 100644 index 00000000..55198b98 --- /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 00000000..7809e045 --- /dev/null +++ b/TagCloud/CloudLayouters/CircularCloudLayouter/CircularCloudLayouter.cs @@ -0,0 +1,75 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouters.CircularCloudLayouter +{ + // Класс, со старого задания TagCloud, + // который расставляет прямоугольники по окружности + // с постепенно увеличивающимся радиусом. + // Прямоугольники расставляются вокруг точки с координатой (0, 0), + // Затем, в CloudLayouterPainter координат пересыитываются таким образом, + // что бы расположить первый прямоугольник в центре холста. + // Можно создать интерфейс IShape, который через GetCoordinates + // будет возвращать координаты линии формы. + // Тогда Circle можно заменить на IShape и ввести новые формы расстановки. + + internal class CircularCloudLayouter : ICloudLayouter + { + private readonly Circle arrangementСircle = new Circle(); + private readonly Random random = new Random(); + private readonly List rectangles = new List(); + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) + { + throw new ArgumentException( + "Размеры прямоугольника не могут быть меньше либо равны нуля."); + } + + 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) + { + foreach (var rect in rectangles) + { + if (rect.IntersectsWith(rectangle)) + { + return true; + } + } + + return false; + } + + private 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 00000000..49abc795 --- /dev/null +++ b/TagCloud/CloudLayouters/ICloudLayouter.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace TagCloud.CloudLayouters +{ + // Интерфейс расстановки прямоугольников + internal interface ICloudLayouter + { + public Rectangle PutNextRectangle(Size rectangleSize); + } +} diff --git a/TagCloud/CommandLineOptions.cs b/TagCloud/CommandLineOptions.cs new file mode 100644 index 00000000..457b37b3 --- /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; } = true.ToString(); + + [Option( + "size", + Required = false, + HelpText = "Размер изображения в формате ШИРИНА:ВЫСОТА, например 5000:5000.")] + public string ImageSize { get; set; } = "5000:5000"; + + [Option( + "maxRectangleWidth", + Required = false, + HelpText = "Максимальная ширина прямоугольника.")] + public int MaxRectangleWidth { get; set; } = 500; + + [Option( + "maxRectangleHeight", + Required = false, + HelpText = "Максимальная высота прямоугольника.")] + public int MaxRectangleHeight { get; set; } = 200; + + [Option( + "imageFile", + Required = false, + HelpText = "Имя выходного файла изображения.")] + public string ImageFileName { get; set; } = "Result"; + + [Option( + "dataFile", + Required = true, + HelpText = "Имя файла с исходными данными.")] + public required string DataFileName { get; set; } + + [Option( + "resultFormat", + Required = false, + HelpText = "Формат создаваемого изображение, например png.")] + public string ResultFormat { get; set; } = "png"; + + [Option( + "wordsToIncludeFile", + Required = false, + HelpText = "Имя файла со словами для добавления в фильтр \"скучных слов\".")] + public string? WordsToIncludeFileName { get; set; } = null; + + [Option( + "wordsToExcludeFile", + Required = false, + HelpText = "Имя файла со словами для исключения из фильтра \"скучных слов\".")] + 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 00000000..a4f41961 --- /dev/null +++ b/TagCloud/DIContainer.cs @@ -0,0 +1,122 @@ +using Autofac; +using TagCloud.CloudLayouterPainters; +using TagCloud.CloudLayouters.CircularCloudLayouter; +using TagCloud.CloudLayouters; +using TagCloud.CloudLayouterWorkers; +using TagCloud.ImageSavers; +using TagCloud.Normalizers; +using TagCloud.WordCounters; +using TagCloud.WordFilters; +using TagCloud.WordReaders; +using TagCloud.Parsers; +using TagCloud.Factories; +using System.Drawing; + +namespace TagCloud +{ + public static class DIContainer + { + public static IContainer ConfigureContainer(CommandLineOptions options) + { + var builder = new ContainerBuilder(); + + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterIWordFillterSevice(builder, options); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + RegisterSimpleSevice(builder); + + var imageSize = SizeParser.ParseImageSize(options.ImageSize); + RegisterICloudLayouterPainterSevice(builder, options, imageSize); + + RegisterICloudLayouterWorkerSevice(builder, options); + + RegisterProgramExecutorService(builder, options, imageSize); + + return builder.Build(); + } + + private static void RegisterSimpleSevice(ContainerBuilder builder) + where TImplementation : TService + where TService : notnull + { + builder + .RegisterType() + .As() + .SingleInstance(); + } + + private static void RegisterSimpleSevice(ContainerBuilder builder) + where TImplementation : notnull + { + builder + .RegisterType() + .AsSelf() + .SingleInstance(); + } + + private static void RegisterICloudLayouterPainterSevice( + ContainerBuilder builder, + CommandLineOptions options, + Size imageSize) + { + var backgroundColor = ColorParser.ParseColor(options.BackgroundColor); + var textColor = ColorParser.ParseColor(options.TextColor); + var font = FontParser.ParseFont(options.Font); + builder.RegisterType() + .As() + .WithParameter("imageSize", imageSize) + .WithParameter("backgroundColor", backgroundColor) + .WithParameter("textColor", textColor) + .WithParameter("fontName", font) + .SingleInstance(); + } + + private static void RegisterICloudLayouterWorkerSevice( + ContainerBuilder builder, + CommandLineOptions options) + { + builder.Register(c => + { + var factory = c.Resolve(); + return factory.Create( + options.DataFileName, + options.MaxRectangleWidth, + options.MaxRectangleHeight, + BoolParser.ParseIsSorted(options.IsSorted)); + }).As().SingleInstance(); + } + + private static void RegisterIWordFillterSevice( + ContainerBuilder builder, + CommandLineOptions options) + { + builder.Register(c => + { + var factory = c.Resolve(); + return factory.Create( + options.WordsToIncludeFileName, + options.WordsToExcludeFileName, + c.Resolve()); + }).As().SingleInstance(); + } + + private static void RegisterProgramExecutorService( + ContainerBuilder builder, + CommandLineOptions options, + Size imageSize) + { + builder.RegisterType() + .WithParameter("size", imageSize) + .WithParameter("resultFormat", options.ResultFormat) + .WithParameter("maxRectangleWidth", options.MaxRectangleWidth) + .WithParameter("maxRectangleHeight", options.MaxRectangleHeight) + .WithParameter("imageFileName", options.ImageFileName) + .WithParameter("dataFileName", options.DataFileName) + .SingleInstance(); + } + } +} diff --git a/TagCloud/Factories/CloudLayouterWorkerFactory.cs b/TagCloud/Factories/CloudLayouterWorkerFactory.cs new file mode 100644 index 00000000..e1de1a15 --- /dev/null +++ b/TagCloud/Factories/CloudLayouterWorkerFactory.cs @@ -0,0 +1,39 @@ +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, + IWordFilter wordFilter) + { + public ICloudLayouterWorker Create( + string dataFileName, + int maxRectangleWidth, + int maxRectangleHeight, + bool isSorted) + { + foreach (var word in wordReader.ReadByLines(dataFileName)) + { + var wordInLowerCase = word.ToLower(); + if (!wordFilter.IsCorrectWord(wordInLowerCase)) + { + continue; + } + wordCounter.AddWord(wordInLowerCase); + } + + var normalizedValues = normalizer.Normalize(wordCounter.Values); + return new NormalizedFrequencyBasedCloudLayouterWorker( + maxRectangleWidth, + maxRectangleHeight, + normalizedValues, + isSorted); + } + } +} diff --git a/TagCloud/Factories/WordFilterFactory.cs b/TagCloud/Factories/WordFilterFactory.cs new file mode 100644 index 00000000..51ec5852 --- /dev/null +++ b/TagCloud/Factories/WordFilterFactory.cs @@ -0,0 +1,53 @@ +using TagCloud.WordFilters; +using TagCloud.WordReaders; + +namespace TagCloud.Factories +{ + internal class WordFilterFactory + { + public IWordFilter Create( + string? wordsToIncludeFileName, + string? wordsToExcludeFileName, + IWordReader wordReader) + { + var result = new WordFilter(); + + if (IsFileNameCorrect(wordsToIncludeFileName)) + { + AddWords(wordReader, wordsToIncludeFileName!, result); + } + + if (IsFileNameCorrect(wordsToExcludeFileName)) + { + RemoveWords(wordReader, wordsToExcludeFileName!, result); + } + + return result; + } + + private bool IsFileNameCorrect(string? fileName) + => !string.IsNullOrEmpty(fileName) && !string.IsNullOrWhiteSpace(fileName); + + private void AddWords( + IWordReader wordReader, + string wordsToIncludeFileName, + WordFilter wordFilter) + { + foreach (var word in wordReader.ReadByLines(wordsToIncludeFileName)) + { + wordFilter.Add(word.ToLower()); + } + } + + private void RemoveWords( + IWordReader wordReader, + string wordsToExcludeFileName, + WordFilter wordFilter) + { + foreach (var word in wordReader.ReadByLines(wordsToExcludeFileName)) + { + wordFilter.Remove(word.ToLower()); + } + } + } +} diff --git a/TagCloud/GlobalSuppressions.cs b/TagCloud/GlobalSuppressions.cs new file mode 100644 index 00000000..3069505e --- /dev/null +++ b/TagCloud/GlobalSuppressions.cs @@ -0,0 +1,17 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + + +// Так делать явно плохо +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.CloudLayouterPainters.CloudLayouterPainter.DrawText(System.Drawing.Graphics,System.Drawing.Rectangle,System.String)")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.CloudLayouterPainters.CloudLayouterPainter.Draw(System.Collections.Generic.IList{TagCloud.Tag})~System.Drawing.Bitmap")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.CloudLayouterPainters.CloudLayouterPainter.FindFittingFontSize(System.Drawing.Graphics,System.String,System.Drawing.Rectangle)~System.Int32")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:Program.ParseFont(System.String)~System.String")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.ImageSavers.ImageSaver.SaveFile(System.Drawing.Bitmap,System.String,System.String)")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.Parsers.FontParser.ParseFont(System.String)~System.String")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.Parsers.FontParser.ParseFont(System.String)~System.Drawing.FontFamily")] +[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~F:TagCloud.CloudLayouterPainters.CloudLayouterPainter.fontName")] diff --git a/TagCloud/ImageSavers/IImageSaver.cs b/TagCloud/ImageSavers/IImageSaver.cs new file mode 100644 index 00000000..794e0bc0 --- /dev/null +++ b/TagCloud/ImageSavers/IImageSaver.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace TagCloud.ImageSavers +{ + // Интерфейс сохранения изображения в файл + internal interface IImageSaver + { + public void 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 00000000..f5d2a082 --- /dev/null +++ b/TagCloud/ImageSavers/ImageSaver.cs @@ -0,0 +1,25 @@ +using System.Drawing; + +namespace TagCloud.ImageSavers +{ + // Реализован пункт на перспективу: + // Формат результата. + // Поддерживать разные форматы изображений. + internal class ImageSaver : IImageSaver + { + public void SaveFile(Bitmap image, string fileName, string format = "png") + { + if (image is null) + { + throw new ArgumentNullException("Передаваемое изображение не должно быть null"); + } + + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentException("Некорректное имя файла для создания"); + } + + image.Save($"{fileName}.{format}"); + } + } +} diff --git a/TagCloud/Normalizers/INormalizer.cs b/TagCloud/Normalizers/INormalizer.cs new file mode 100644 index 00000000..050a7450 --- /dev/null +++ b/TagCloud/Normalizers/INormalizer.cs @@ -0,0 +1,11 @@ +namespace TagCloud.Normalizers +{ + // Интерфейс нормализации количества каждого слова + internal interface INormalizer + { + public Dictionary 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 00000000..6f7cd329 --- /dev/null +++ b/TagCloud/Normalizers/Normalizer.cs @@ -0,0 +1,58 @@ +namespace TagCloud.Normalizers +{ + // Слово, которое встречается чаще всего, будет иметь вес 1.0. + // Это означает, что оно в дальнейшем будет иметь прямоугольник + // с максимальным размером. + // Слово с минимальной частотой будет иметь + // minCoefficient * максимальный размеро прямоугольника. + internal class Normalizer : INormalizer + { + public Dictionary Normalize( + Dictionary values, + double minCoefficient = 0.25, + int decimalPlaces = 4) + { + if (minCoefficient < 0) + { + throw new ArgumentException("Минимальный коэффициент не может быть меньше 0"); + } + + 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 double CalculateNormalizedValue( + double minCoefficient, + double scale, + double value, + uint minValue, + uint maxValue, + int decimalPlaces) + { + if (minValue == maxValue) + { + return 1.0; + } + return Math.Round( + minCoefficient + scale * ((double)(value - minValue) / (maxValue - minValue)), + decimalPlaces); + } + } +} diff --git a/TagCloud/Parsers/BoolParser.cs b/TagCloud/Parsers/BoolParser.cs new file mode 100644 index 00000000..419a6e24 --- /dev/null +++ b/TagCloud/Parsers/BoolParser.cs @@ -0,0 +1,14 @@ +namespace TagCloud.Parsers +{ + internal static class BoolParser + { + public static bool ParseIsSorted(string value) + { + if (value == bool.FalseString || value == bool.TrueString) + { + return Convert.ToBoolean(value); + } + throw new ArgumentException($"Неизвестный параметр сортировки {value}"); + } + } +} diff --git a/TagCloud/Parsers/ColorParser.cs b/TagCloud/Parsers/ColorParser.cs new file mode 100644 index 00000000..0d7ed14e --- /dev/null +++ b/TagCloud/Parsers/ColorParser.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagCloud.Parsers +{ + internal static class ColorParser + { + public static Color ParseColor(string color) + { + var result = Color.FromName(color); + if (!result.IsKnownColor) + { + throw new ArgumentException($"Неизвестный цвет {color}"); + } + return result; + } + } +} diff --git a/TagCloud/Parsers/FontParser.cs b/TagCloud/Parsers/FontParser.cs new file mode 100644 index 00000000..4564662d --- /dev/null +++ b/TagCloud/Parsers/FontParser.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagCloud.Parsers +{ + internal static class FontParser + { + public static FontFamily ParseFont(string font) + { + if (!FontFamily.Families.Any( + x => x.Name.Equals(font, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException($"Неизвестный шрифт {font}"); + } + return new FontFamily(font); + } + } +} diff --git a/TagCloud/Parsers/SizeParser.cs b/TagCloud/Parsers/SizeParser.cs new file mode 100644 index 00000000..1ad249b3 --- /dev/null +++ b/TagCloud/Parsers/SizeParser.cs @@ -0,0 +1,21 @@ +using System.Drawing; + +namespace TagCloud.Parsers +{ + internal static class SizeParser + { + public static Size ParseImageSize(string size) + { + var dimensions = size.Split(':'); + if (dimensions.Length != 2 + || !int.TryParse(dimensions[0], out var width) + || !int.TryParse(dimensions[1], out var height)) + { + throw new ArgumentException( + $"Некорректный формат размера изображения: {size}." + + $" Используйте формат Ширина:Высота, например 5000:5000."); + } + return new Size(width, height); + } + } +} diff --git a/TagCloud/Program.cs b/TagCloud/Program.cs new file mode 100644 index 00000000..d67ab880 --- /dev/null +++ b/TagCloud/Program.cs @@ -0,0 +1,34 @@ +using Autofac; +using CommandLine; +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(); + } + } +} diff --git a/TagCloud/ProgramExecutor.cs b/TagCloud/ProgramExecutor.cs new file mode 100644 index 00000000..24d4f246 --- /dev/null +++ b/TagCloud/ProgramExecutor.cs @@ -0,0 +1,28 @@ +using TagCloud.CloudLayouters; +using TagCloud.CloudLayouterPainters; +using TagCloud.CloudLayouterWorkers; +using TagCloud.ImageSavers; + +namespace TagCloud +{ + internal class ProgramExecutor( + string imageFileName, + string resultFormat, + ICloudLayouter layouter, + ICloudLayouterPainter painter, + ICloudLayouterWorker worker, + IImageSaver imageSaver) + { + public void Execute() + { + var tags = new List(); + foreach (var rectangleProperty in worker.GetNextRectangleProperties()) + { + var tagSize = layouter.PutNextRectangle(rectangleProperty.size); + var newTag = new Tag(rectangleProperty.word, tagSize); + tags.Add(newTag); + } + imageSaver.SaveFile(painter.Draw(tags), imageFileName, resultFormat); + } + } +} diff --git a/TagCloud/SnowWhiteContent.txt b/TagCloud/SnowWhiteContent.txt new file mode 100644 index 00000000..23970728 --- /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 00000000..ee37ae00 --- /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 00000000..113b51b7 --- /dev/null +++ b/TagCloud/TagCloud.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/TagCloud/WordCounters/IWordCounter.cs b/TagCloud/WordCounters/IWordCounter.cs new file mode 100644 index 00000000..f41d1763 --- /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 00000000..c5953a1f --- /dev/null +++ b/TagCloud/WordCounters/WordCounter.cs @@ -0,0 +1,18 @@ +namespace TagCloud.WordCounters +{ + internal class WordCounter : IWordCounter + { + private readonly Dictionary counts = new Dictionary(); + public Dictionary Values => counts; + + public void AddWord(string word) + { + if (!counts.ContainsKey(word)) + { + counts[word] = 1; + return; + } + counts[word] += 1; + } + } +} diff --git a/TagCloud/WordFilters/IWordFilter.cs b/TagCloud/WordFilters/IWordFilter.cs new file mode 100644 index 00000000..4e175d56 --- /dev/null +++ b/TagCloud/WordFilters/IWordFilter.cs @@ -0,0 +1,8 @@ +namespace TagCloud.WordFilters +{ + // Интерфейс фильтрации "скучных" слов + internal interface IWordFilter + { + public bool IsCorrectWord(string word); + } +} diff --git a/TagCloud/WordFilters/WordFilter.cs b/TagCloud/WordFilters/WordFilter.cs new file mode 100644 index 00000000..81900028 --- /dev/null +++ b/TagCloud/WordFilters/WordFilter.cs @@ -0,0 +1,75 @@ +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 WordFilter(IList? toAdd = null, IList? toExclude = null) + { + if (toAdd is not null) + { + foreach (var word in toAdd) + { + Add(word); + } + } + + if (toExclude is not null) + { + foreach (var word in toExclude) + { + Remove(word); + } + } + } + + 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 00000000..5384df2a --- /dev/null +++ b/TagCloud/WordReaders/IWordReader.cs @@ -0,0 +1,8 @@ +namespace TagCloud.WordReaders +{ + // Интерфейс для построчного чтения содержимого файла + internal interface IWordReader + { + public IEnumerable ReadByLines(string path); + } +} diff --git a/TagCloud/WordReaders/WordReader.cs b/TagCloud/WordReaders/WordReader.cs new file mode 100644 index 00000000..4443209a --- /dev/null +++ b/TagCloud/WordReaders/WordReader.cs @@ -0,0 +1,22 @@ +namespace TagCloud.WordReaders +{ + internal class WordReader : IWordReader + { + public IEnumerable ReadByLines(string path) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Файл {path} не существует"); + } + + foreach (var line in File.ReadAllLines(path)) + { + if (line.Contains(' ')) + { + throw new Exception($"Файл {path} содержит строку с двумя и более словами"); + } + yield return line; + } + } + } +} diff --git a/di.sln b/di.sln index a50991da..70f2aa48 100644 --- a/di.sln +++ b/di.sln @@ -1,6 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter/di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "di", "FractalPainter\di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud", "TagCloud\TagCloud.csproj", "{DBAE2C15-6BFD-46B8-955E-C169F7BA6FB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud.Tests", "TagCloud.Tests\TagCloud.Tests.csproj", "{99BFD9ED-99EC-41F6-97AA-CCF638BB9ED6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,5 +19,16 @@ Global {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.Build.0 = Release|Any CPU + {DBAE2C15-6BFD-46B8-955E-C169F7BA6FB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBAE2C15-6BFD-46B8-955E-C169F7BA6FB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBAE2C15-6BFD-46B8-955E-C169F7BA6FB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBAE2C15-6BFD-46B8-955E-C169F7BA6FB7}.Release|Any CPU.Build.0 = Release|Any CPU + {99BFD9ED-99EC-41F6-97AA-CCF638BB9ED6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99BFD9ED-99EC-41F6-97AA-CCF638BB9ED6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99BFD9ED-99EC-41F6-97AA-CCF638BB9ED6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99BFD9ED-99EC-41F6-97AA-CCF638BB9ED6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal