diff --git a/TagCloud/CloudDrawers/ICloudDrawer.cs b/TagCloud/CloudDrawers/ICloudDrawer.cs new file mode 100644 index 00000000..a8801f82 --- /dev/null +++ b/TagCloud/CloudDrawers/ICloudDrawer.cs @@ -0,0 +1,9 @@ +using System.Drawing; +using TagCloud; + +namespace TagCloudTests; + +public interface ICloudDrawer +{ + void Draw(List rectangle); +} \ No newline at end of file diff --git a/TagCloud/CloudDrawers/TagCloudDrawer.cs b/TagCloud/CloudDrawers/TagCloudDrawer.cs new file mode 100644 index 00000000..dcfb09de --- /dev/null +++ b/TagCloud/CloudDrawers/TagCloudDrawer.cs @@ -0,0 +1,57 @@ +using System.Drawing; +using TagCloud.Extensions; +using TagCloudTests; + +namespace TagCloud; + +public class TagCloudDrawer : ICloudDrawer +{ + private readonly string path; + private readonly IColorSelector selector; + private readonly string name; + private readonly Font font; + + private TagCloudDrawer(string path, string name, IColorSelector selector, Font font) + { + this.path = path; + this.selector = selector; + this.font = font; + this.name = name; + } + + public void Draw(List rectangles) + { + if (rectangles.Count == 0) + throw new ArgumentException("Empty rectangles list"); + + var containingRect = rectangles + .Select(r => r.Rectangle) + .GetMinimalContainingRectangle(); + + using var bitmap = new Bitmap(containingRect.Width + 2, containingRect.Height + 2); + using var graphics = Graphics.FromImage(bitmap); + + graphics.DrawStrings( + selector, + rectangles + .Select(rect => rect.OnLocation(-containingRect.X + rect.X, -containingRect.Y + rect.Y)) + .ToArray() + ); + + SaveToFile(bitmap); + } + + private void SaveToFile(Bitmap bitmap) + { + var pathToFile = @$"{path}\{name}"; + bitmap.Save(pathToFile); + Console.WriteLine($"Tag cloud visualization saved to file {path}"); + } + + public static TagCloudDrawer Create(string path, string name, Font font, IColorSelector selector) + { + if (!Directory.Exists(path)) + throw new ArgumentException("Directory does not exist"); + return new TagCloudDrawer(path, name, selector, font); + } +} \ No newline at end of file diff --git a/TagCloud/CloudLayouter.cs b/TagCloud/CloudLayouter.cs new file mode 100644 index 00000000..db736d29 --- /dev/null +++ b/TagCloud/CloudLayouter.cs @@ -0,0 +1,36 @@ +using System.Drawing; + +namespace TagCloud; + +public class CloudLayouter +{ + private List rectangles; + private ICloudShaper shaper; + + public IReadOnlyList Rectangles => rectangles; + + public CloudLayouter(ICloudShaper shaper) + { + rectangles = new List(); + this.shaper = shaper; + } + + public Rectangle PutNextRectangle(Size size) + { + if (size.Width <= 0) + throw new ArgumentException("Size width must be positive number"); + if (size.Height <= 0) + throw new ArgumentException("Size height must be positive number"); + + Rectangle rectangle = Rectangle.Empty; + foreach (var point in shaper.GetPossiblePoints()) + { + rectangle = new Rectangle(point, size); + if (!Rectangles.Any(rect => rect.IntersectsWith(rectangle))) + break; + } + + rectangles.Add(rectangle); + return rectangle; + } +} \ No newline at end of file diff --git a/TagCloud/CloudSharper/ICloudShaper.cs b/TagCloud/CloudSharper/ICloudShaper.cs new file mode 100644 index 00000000..67c8e063 --- /dev/null +++ b/TagCloud/CloudSharper/ICloudShaper.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloud; + +public interface ICloudShaper +{ + IEnumerable GetPossiblePoints(); +} \ No newline at end of file diff --git a/TagCloud/CloudSharper/SpiralCloudShaper.cs b/TagCloud/CloudSharper/SpiralCloudShaper.cs new file mode 100644 index 00000000..6a27f187 --- /dev/null +++ b/TagCloud/CloudSharper/SpiralCloudShaper.cs @@ -0,0 +1,53 @@ +using System.Drawing; + +namespace TagCloud; + +public class SpiralCloudShaper : ICloudShaper +{ + private Point center; + private double coefficient; + private double deltaAngle; + + private SpiralCloudShaper(Point center, double coefficient, double deltaAngle) + { + this.center = center; + this.deltaAngle = deltaAngle; + this.coefficient = coefficient; + } + + public IEnumerable GetPossiblePoints() + { + var currentAngle = 0D; + var position = center; + var previousPoint = position; + while(true) + { + while (position == previousPoint) + { + currentAngle += deltaAngle; + previousPoint = position; + position = CalculatePointByCurrentAngle(currentAngle); + } + yield return position; + previousPoint = position; + position = CalculatePointByCurrentAngle(currentAngle); + } + } + + private Point CalculatePointByCurrentAngle(double angle) + { + return new Point( + center.X + (int)(coefficient * angle * Math.Cos(angle)), + center.Y + (int)(coefficient * angle * Math.Sin(angle)) + ); + } + + public static SpiralCloudShaper Create(Point center, double coefficient = 0.1, double deltaAngle = 0.1) + { + if (coefficient <= 0) + throw new ArgumentException("Spiral coefficient must be positive number"); + if (deltaAngle <= 0) + throw new ArgumentException("Spiral delta angle must be positive number"); + return new SpiralCloudShaper(center, coefficient, deltaAngle); + } +} \ No newline at end of file diff --git a/TagCloud/ColorSelectors/ConstantColorSelector.cs b/TagCloud/ColorSelectors/ConstantColorSelector.cs new file mode 100644 index 00000000..84c88328 --- /dev/null +++ b/TagCloud/ColorSelectors/ConstantColorSelector.cs @@ -0,0 +1,15 @@ +using System.Drawing; +using TagCloudTests; + +namespace TagCloud; + +public class ConstantColorSelector : IColorSelector +{ + private Color color; + public ConstantColorSelector(Color color) + { + this.color = color; + } + + public Color SetColor() => color; +} \ No newline at end of file diff --git a/TagCloud/ColorSelectors/GrayScaleColorSelector.cs b/TagCloud/ColorSelectors/GrayScaleColorSelector.cs new file mode 100644 index 00000000..1359efdc --- /dev/null +++ b/TagCloud/ColorSelectors/GrayScaleColorSelector.cs @@ -0,0 +1,14 @@ +using System.Drawing; + +namespace TagCloudTests; + +public class GrayScaleColorSelector : IColorSelector +{ + private readonly Random random = new(DateTime.Now.Microsecond); + + public Color SetColor() + { + var gray = random.Next(100, 200); + return Color.FromArgb(gray, gray, gray); + } +} \ No newline at end of file diff --git a/TagCloud/ColorSelectors/IColorSelector.cs b/TagCloud/ColorSelectors/IColorSelector.cs new file mode 100644 index 00000000..2301dcf3 --- /dev/null +++ b/TagCloud/ColorSelectors/IColorSelector.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloudTests; + +public interface IColorSelector +{ + Color SetColor(); +} \ No newline at end of file diff --git a/TagCloud/ColorSelectors/RandomColorSelector.cs b/TagCloud/ColorSelectors/RandomColorSelector.cs new file mode 100644 index 00000000..debe99c1 --- /dev/null +++ b/TagCloud/ColorSelectors/RandomColorSelector.cs @@ -0,0 +1,15 @@ +using System.Drawing; + +namespace TagCloud.ColorSelectors; + +public class RandomColorSelector +{ + private readonly Random random = new(DateTime.Now.Microsecond); + + public Color SetColor() + { + var color = random.Next(0, 255); + return Color.FromArgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)); + } + +} \ No newline at end of file diff --git a/TagCloud/Extensions/ColorConverter.cs b/TagCloud/Extensions/ColorConverter.cs new file mode 100644 index 00000000..c5cf84ee --- /dev/null +++ b/TagCloud/Extensions/ColorConverter.cs @@ -0,0 +1,22 @@ +using System.Drawing; +using System.Text.RegularExpressions; + +namespace TagCloud.Extensions; + +public static class ColorConverter +{ + public static bool TryConvert(string hexString, out Color color) + { + color = default; + var colorHexRegExp = new Regex(@"^#([A-Fa-f0-9]{6})$"); + if (colorHexRegExp.Count(hexString) != 1) + return false; + var rgbValue = hexString + .Replace("#", "") + .Chunk(2) + .Select(chars => Convert.ToInt32(new string(chars), 16)) + .ToArray(); + color = Color.FromArgb(rgbValue[0], rgbValue[1], rgbValue[2]); + return true; + } +} \ No newline at end of file diff --git a/TagCloud/Extensions/GraphicsExtensions.cs b/TagCloud/Extensions/GraphicsExtensions.cs new file mode 100644 index 00000000..f5284f46 --- /dev/null +++ b/TagCloud/Extensions/GraphicsExtensions.cs @@ -0,0 +1,17 @@ +using System.Drawing; +using TagCloudTests; + +namespace TagCloud.Extensions; + +public static class GraphicsExtensions +{ + public static void DrawStrings(this Graphics graphics, IColorSelector selector, TextRectangle[] rectangles) + { + using var brush = new SolidBrush(selector.SetColor()); + foreach (var rectangle in rectangles) + { + graphics.DrawString(rectangle.Text, rectangle.Font, brush, rectangle.X, rectangle.Y); + brush.Color = selector.SetColor(); + } + } +} \ No newline at end of file diff --git a/TagCloud/Extensions/PointExtension.cs b/TagCloud/Extensions/PointExtension.cs new file mode 100644 index 00000000..2503b7ae --- /dev/null +++ b/TagCloud/Extensions/PointExtension.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagCloud.Extensions; + +public static class PointExtension +{ + public static double GetDistanceTo(this Point first, Point second) + { + return Math.Sqrt((first.X - second.X) * (first.X - second.X) + (first.Y - second.Y) * (first.Y - second.Y)); + } +} \ No newline at end of file diff --git a/TagCloud/Extensions/RectangleEnumerableExtensions.cs b/TagCloud/Extensions/RectangleEnumerableExtensions.cs new file mode 100644 index 00000000..6286fd4e --- /dev/null +++ b/TagCloud/Extensions/RectangleEnumerableExtensions.cs @@ -0,0 +1,36 @@ +using System.Drawing; + +namespace TagCloud.Extensions; + +public static class RectangleEnumerableExtensions +{ + public static bool HasIntersectedRectangles(this IEnumerable rectangles) + { + return rectangles + .SelectMany( + (x, i) => rectangles.Skip(i + 1), + (x, y) => Tuple.Create(x, y) + ) + .Any(tuple => tuple.Item1.IntersectsWith(tuple.Item2)); + } + + public static Rectangle GetMinimalContainingRectangle(this IEnumerable rectangles) + { + int minX = int.MaxValue, minY = int.MaxValue; + int maxX = int.MinValue, maxY = int.MinValue; + + foreach (var rectangle in rectangles) + { + if (rectangle.X < minX) + minX = rectangle.X; + if (rectangle.Y < minY) + minY = rectangle.Y; + if (rectangle.Right > maxX) + maxX = rectangle.Right; + if (rectangle.Bottom > maxY) + maxY = rectangle.Bottom; + } + + return new Rectangle(minX, minY, maxX - minX, maxY- minY); + } +} \ No newline at end of file diff --git a/TagCloud/Option.cs b/TagCloud/Option.cs new file mode 100644 index 00000000..976b0155 --- /dev/null +++ b/TagCloud/Option.cs @@ -0,0 +1,61 @@ +using CommandLine; + +namespace TagCloudApplication; + +public class Options +{ + [Option('d', "destination", HelpText = "Set destination path.", Default = @"..\..\..\Results")] + public string DestinationPath { get; set; } + + [Option('s', "source", HelpText = "Set source path.", Default = @"..\..\..\Results\text.txt")] + public string SourcePath { get; set; } + + [Option('n', "name", HelpText = "Set name.", Default = "default.png")] + public string Name { get; set; } + + [Option('c', "color", + HelpText = """ + Set color. + random - Random colors + #F0F0F0 - Color hex code + """, + Default = "random")] + public string ColorScheme { get; set; } + + [Option('f', "font", HelpText = "Set font.", Default = "Arial")] + public string Font { get; set; } + + [Option("size", HelpText = "Set font size.", Default = 20)] + public int FontSize { get; set; } + + [Option("unusedParts", + HelpText = """ + Set unused parts of speech. + A - прилагательное + ADV - наречие + ADVPRO - местоименное наречие + ANUM - числительное-прилагательное + APRO - местоимение-прилагательное + COM - часть композита - сложного слова + CONJ - союз + INTJ - междометие + NUM - числительное + PART - частица + PR - предлог + S - существительное + SPRO - местоимение-существительное + V - глагол + """, + Default = new[] { "PR", "PART", "CONJ", "INTJ" })] + public string[] UnusedPartsOfSpeech { get; set; } + + [Option("density", HelpText = "Set density.", Default = 0.1)] + public double Density { get; set; } + + [Option("width", HelpText = "Set width.", Default = 100)] + public int Width { get; set; } + + + [Option("height", HelpText = "Set height.", Default = 100)] + public int Height { get; set; } +} \ No newline at end of file diff --git a/TagCloud/Program.cs b/TagCloud/Program.cs new file mode 100644 index 00000000..e5dff12b --- /dev/null +++ b/TagCloud/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/TagCloud/TagCloud.csproj b/TagCloud/TagCloud.csproj new file mode 100644 index 00000000..78c394f0 --- /dev/null +++ b/TagCloud/TagCloud.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/TagCloud/TagCloudGenerator.cs b/TagCloud/TagCloudGenerator.cs new file mode 100644 index 00000000..72cc771e --- /dev/null +++ b/TagCloud/TagCloudGenerator.cs @@ -0,0 +1,37 @@ +using TagCloud.TextHandlers; +using TagCloudTests; + +namespace TagCloud; + +public class TagCloudGenerator +{ + private readonly ITextHandler handler; + private readonly CloudLayouter layouter; + private readonly ICloudDrawer drawer; + private readonly TextMeasurer measurer; + + public TagCloudGenerator(ITextHandler handler, CloudLayouter layouter, ICloudDrawer drawer, TextMeasurer measurer) + { + this.handler = handler; + this.layouter = layouter; + this.drawer = drawer; + this.measurer = measurer; + } + + public void Generate() + { + var rectangles = new List(); + + foreach (var word in handler.Handle().OrderByDescending(pair => pair.Value)) + { + var (size, font) = measurer.GetTextRectangleSize(word.Value, word.Weight); + rectangles.Add(new TextRectangle( + layouter.PutNextRectangle(size), + word.Value, + font + )); + } + + drawer.Draw(rectangles); + } +} \ No newline at end of file diff --git a/TagCloud/TagCloudServicesFactory.cs b/TagCloud/TagCloudServicesFactory.cs new file mode 100644 index 00000000..b8a75605 --- /dev/null +++ b/TagCloud/TagCloudServicesFactory.cs @@ -0,0 +1,61 @@ +using System.Drawing; +using Autofac; +using TagCloud.ColorSelectors; +using TagCloud.Excluders; +using TagCloud.TextHandlers; +using TagCloud.WordFilters; +using TagCloudApplication; +using TagCloudTests; +using ColorConverter = TagCloud.Extensions.ColorConverter; + +namespace TagCloud; + +public static class TagCloudServicesFactory +{ + public static IContainer ConfigureService(Options options) + { + var builder = new ContainerBuilder(); + + builder.RegisterType().AsSelf().SingleInstance(); + + builder.Register(provider => SpiralCloudShaper.Create(new Point(0, 0), coefficient: options.Density)).SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + + builder.Register(provider => new Font(new FontFamily(options.Font), options.FontSize)).As().InstancePerLifetimeScope(); + builder.RegisterType().AsSelf().SingleInstance(); + + builder.Register(provider => TagCloudDrawer.Create( + options.DestinationPath, + options.Name, + provider.Resolve(), + provider.Resolve() + )).As().SingleInstance(); + + if (options.ColorScheme == "gray") + builder.RegisterType().As().SingleInstance(); + if (options.ColorScheme == "random") + builder.RegisterType().As().SingleInstance(); + else if (ColorConverter.TryConvert(options.ColorScheme, out var color)) + builder.Register(provider => new ConstantColorSelector(color)).As().SingleInstance(); + else + builder.Register(provider => new ConstantColorSelector(Color.Black)).As().SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + + builder.Register(provider => + new FileTextHandler( + stream: File.Open(options.SourcePath, FileMode.Open), + filter: provider.Resolve() + ) + ).As().SingleInstance(); + + return builder.Build(); + } + + public static bool ConfigureServiceAndTryGet(Options option, out T value) + { + using var container = ConfigureService(option); + value = container.Resolve(); + return value != null; + } +} \ No newline at end of file diff --git a/TagCloud/TextHandlers/FileTextHandler.cs b/TagCloud/TextHandlers/FileTextHandler.cs new file mode 100644 index 00000000..f20fcc8b --- /dev/null +++ b/TagCloud/TextHandlers/FileTextHandler.cs @@ -0,0 +1,36 @@ +using MyStemWrapper; +using TagCloud.Excluders; + +namespace TagCloud.TextHandlers; + +public class FileTextHandler : ITextHandler +{ + private readonly Stream stream; + private readonly IWordFilter filter; + + public FileTextHandler(Stream stream, IWordFilter filter) + { + this.stream = stream; + this.filter = filter; + } + + public IEnumerable Handle() + { + var wordCounts = new Dictionary(); + using var sr = new StreamReader(stream); + + while (!sr.EndOfStream) + { + var word = sr.ReadLine()?.ToLower(); + if (word == null) + continue; + wordCounts.TryAdd(word, 0); + wordCounts[word]++; + } + + var data = wordCounts.Select(pair => new TextData() { Value = pair.Key, Weight = pair.Value }); + data = filter.ExcludeWords(data); + + return data; + } +} \ No newline at end of file diff --git a/TagCloud/TextHandlers/ITextHandler.cs b/TagCloud/TextHandlers/ITextHandler.cs new file mode 100644 index 00000000..ec9610bc --- /dev/null +++ b/TagCloud/TextHandlers/ITextHandler.cs @@ -0,0 +1,6 @@ +namespace TagCloud.TextHandlers; + +public interface ITextHandler +{ + IEnumerable Handle(); +} \ No newline at end of file diff --git a/TagCloud/TextHandlers/TextData.cs b/TagCloud/TextHandlers/TextData.cs new file mode 100644 index 00000000..c99db847 --- /dev/null +++ b/TagCloud/TextHandlers/TextData.cs @@ -0,0 +1,7 @@ +namespace TagCloud.TextHandlers; + +public class TextData +{ + public string Value { get; set; } + public int Weight { get; set; } +} \ No newline at end of file diff --git a/TagCloud/TextMeasurer.cs b/TagCloud/TextMeasurer.cs new file mode 100644 index 00000000..1d94426b --- /dev/null +++ b/TagCloud/TextMeasurer.cs @@ -0,0 +1,21 @@ +using System.Drawing; + +namespace TagCloud; + +public class TextMeasurer +{ + private readonly Font font; + + public TextMeasurer(Font font) + { + this.font = font; + } + + public (Size, Font) GetTextRectangleSize(string text, int size) + { + using var graphics = Graphics.FromImage(new Bitmap(1, 1)); + var textFont = new Font(font.FontFamily, size * font.Size); + var sizeF = graphics.MeasureString(text, textFont); + return (new Size((int)sizeF.Width, (int)sizeF.Height), textFont); + } +} \ No newline at end of file diff --git a/TagCloud/TextRectangle.cs b/TagCloud/TextRectangle.cs new file mode 100644 index 00000000..9a383565 --- /dev/null +++ b/TagCloud/TextRectangle.cs @@ -0,0 +1,25 @@ +using System.Drawing; + +namespace TagCloud; + +public struct TextRectangle +{ + public string Text { get; } + public Font Font { get; } + public Rectangle Rectangle { get; private set; } + + public TextRectangle(Rectangle rectangle, string text, Font font) + { + Rectangle = rectangle; + Text = text; + Font = font; + } + + public int X => Rectangle.X; + public int Y => Rectangle.Y; + + public TextRectangle OnLocation(int x, int y) + { + return new TextRectangle(Rectangle with { X = x, Y = y }, Text, Font); + } +} \ No newline at end of file diff --git a/TagCloud/WordFilters/IWordFilter.cs b/TagCloud/WordFilters/IWordFilter.cs new file mode 100644 index 00000000..8cd48db4 --- /dev/null +++ b/TagCloud/WordFilters/IWordFilter.cs @@ -0,0 +1,8 @@ +using TagCloud.TextHandlers; + +namespace TagCloud.Excluders; + +public interface IWordFilter +{ + IEnumerable ExcludeWords(IEnumerable data); +} \ No newline at end of file diff --git a/TagCloud/WordFilters/MyStemWordFilter.cs b/TagCloud/WordFilters/MyStemWordFilter.cs new file mode 100644 index 00000000..9610587c --- /dev/null +++ b/TagCloud/WordFilters/MyStemWordFilter.cs @@ -0,0 +1,38 @@ +using MyStemWrapper; +using TagCloud.Excluders; +using TagCloud.TextHandlers; + +namespace TagCloud.WordFilters; + +public class MyStemWordFilter : IWordFilter +{ + private static readonly string[] ForbidenSpeechParts = new[] + { + "PR", // предлог + "PART", // частица + "CONJ", // союз + "INTJ" // междометие + }; + + public IEnumerable ExcludeWords(IEnumerable data) + { + var stem = new MyStem(); + stem.Parameters = "-lig"; + foreach (var word in data) + { + var analysis = stem.Analysis(word.Value); + if (string.IsNullOrEmpty(analysis)) + continue; + + analysis = analysis.Substring(1, analysis.Length - 2); + var analysisResults = analysis.Split(","); + var partsOfSpeech = analysisResults[0] + .Split("=|") + .Select(part => part.Split("=")[1]); + + if (partsOfSpeech.Any(ForbidenSpeechParts.Contains)) + continue; + yield return word; + } + } +} \ No newline at end of file diff --git a/TagCloudApp/Program.cs b/TagCloudApp/Program.cs new file mode 100644 index 00000000..803c1c54 --- /dev/null +++ b/TagCloudApp/Program.cs @@ -0,0 +1,19 @@ +using CommandLine; +using TagCloud; + +namespace TagCloudApplication; + +class Program +{ + static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(option => + { + if (TagCloudServicesFactory.ConfigureServiceAndTryGet(option, out var generator)) + generator.Generate(); + else + throw new Exception("Can't configure service"); + }); + } +} \ No newline at end of file diff --git a/TagCloudApp/Results/default.png b/TagCloudApp/Results/default.png new file mode 100644 index 00000000..481b2a25 Binary files /dev/null and b/TagCloudApp/Results/default.png differ diff --git a/TagCloudApp/Results/text.txt b/TagCloudApp/Results/text.txt new file mode 100644 index 00000000..34b2b08c --- /dev/null +++ b/TagCloudApp/Results/text.txt @@ -0,0 +1,51 @@ +Разнообразный +и +богатый +опыт +курс +на +социально-ориентированный +национальный +проект +играет +важную +роль +в +формировании +форм +воздействия +Значимость +этих +проблем +настолько +очевидна +что +новая +модель +организационной +деятельности +в +значительной +степени +обуславливает +создание +системы +масштабного +изменения +ряда +параметров +Разнообразный +и +богатый +опыт +сложившаяся +структура +организации +позволяет +оценить +значение +системы +масштабного +изменения +ряда +параметров \ No newline at end of file diff --git a/TagCloudApp/TagCloudApp.csproj b/TagCloudApp/TagCloudApp.csproj new file mode 100644 index 00000000..0e3430ac --- /dev/null +++ b/TagCloudApp/TagCloudApp.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/TagCloudTests/CloudLayouterTests.cs b/TagCloudTests/CloudLayouterTests.cs new file mode 100644 index 00000000..c1e3c13d --- /dev/null +++ b/TagCloudTests/CloudLayouterTests.cs @@ -0,0 +1,103 @@ +using System.Drawing; +using TagCloud; +using TagCloud.Extensions; + +namespace TagCloudTests; + +public class CloudLayouterTests +{ + private CloudLayouter layouter; + private ICloudDrawer drawer; + private Point center; + + [SetUp] + public void Setup() + { + center = new Point(0, 0); + layouter = new CloudLayouter(SpiralCloudShaper.Create(center)); + } + + [Test] + public void Rectangles_ReturnEmptyList_WhenCreated() + { + layouter.Rectangles.Should().BeEmpty(); + } + + [Test] + public void Rectangles_ReturnOneElementList_WhenAddOne() + { + layouter.PutNextRectangle(new Size(1, 1)); + layouter.Rectangles.Count().Should().Be(1); + } + + [Test] + public void Rectangles_ReturnTwoElementList_WhenAddTwo() + { + layouter.PutNextRectangle(new Size(1, 1)); + layouter.PutNextRectangle(new Size(1, 1)); + layouter.Rectangles.Count().Should().Be(2); + NotIntersectedAssertion(layouter.Rectangles); + } + + [TestCase(1, 1, 500, 0.77D, TestName = "WithSquareShape")] + [TestCase(20, 10, 500, 0.67D, TestName = "WithRectangleShape")] + public void Layouter_ShouldLocateConstantSizeRectangles_ByCircleShapeAndWithoutIntersection(int width, int height, int count, double accuracy) + { + var size = new Size(width, height); + for (int i = 0; i < count; i++) + layouter.PutNextRectangle(size); + + layouter.Rectangles.Count().Should().Be(count); + NotIntersectedAssertion(layouter.Rectangles); + CircleShapeAssertion(layouter.Rectangles, accuracy); + } + + [Test] + public void Layouter_ShouldLocateVariableSizeRectangles_ByCircleShapeAndWithoutIntersection() + { + var rnd = new Random(DateTime.Now.Microsecond); + + for (int i = 0; i < 200; i++) + { + var size = new Size(rnd.Next(20, 40), rnd.Next(20, 40)); + layouter.PutNextRectangle(size); + } + + layouter.Rectangles.Count().Should().Be(200); + NotIntersectedAssertion(layouter.Rectangles); + CircleShapeAssertion(layouter.Rectangles, 0.6D); + } + + [TestCase(-1, 1, TestName = "WithNegativeWidth")] + [TestCase(1, -1, TestName = "WithNegativeHeight")] + [TestCase(-1, -1, TestName = "WithNegativeWidthAndHeight")] + public void PutNextRectangle_ShouldThrow_ThenTryPutRectangle(int width, int height) + { + Assert.Throws(() => layouter.PutNextRectangle(new Size(width, height))); + } + + private void CircleShapeAssertion(IEnumerable rectangles, double accuracy) + { + var circleRadius = rectangles + .SelectMany(rect => new[] + { + rect.Location, + rect.Location with { X = rect.Right }, + rect.Location with { Y = rect.Bottom }, + rect.Location with { X = rect.Right, Y = rect.Bottom } + }) + .Max(point => point.GetDistanceTo(center)); + Console.WriteLine(circleRadius); + var containingCircleRadius = Math.PI * circleRadius * circleRadius; + var rectanglesTotalArea = rectangles.Sum(rect => rect.Width * rect.Height); + Assert.GreaterOrEqual(rectanglesTotalArea / containingCircleRadius, accuracy); + } + + public static void NotIntersectedAssertion(IEnumerable rectangles) + { + rectangles + .HasIntersectedRectangles() + .Should() + .BeFalse(); + } +} \ No newline at end of file diff --git a/TagCloudTests/ColorConverterTests.cs b/TagCloudTests/ColorConverterTests.cs new file mode 100644 index 00000000..c93ea651 --- /dev/null +++ b/TagCloudTests/ColorConverterTests.cs @@ -0,0 +1,16 @@ +using System.Drawing; +using TagCloudTests.TestData; +using ColorConverter = TagCloud.Extensions.ColorConverter; + +namespace TagCloudTests; + +public class ColorConverterTests +{ + [TestCaseSource(typeof(ColorConverterTestData), nameof(ColorConverterTestData.RightCases))] + public Color Converter_ShouldConvertStringToColor_WhenStringInRightFormat(string hexString) + { + if (ColorConverter.TryConvert(hexString, out var color)) + return color; + throw new Exception(); + } +} \ No newline at end of file diff --git a/TagCloudTests/FileTextHandlerTests.cs b/TagCloudTests/FileTextHandlerTests.cs new file mode 100644 index 00000000..64386f29 --- /dev/null +++ b/TagCloudTests/FileTextHandlerTests.cs @@ -0,0 +1,21 @@ +using System.Text; +using TagCloud.Excluders; +using TagCloud.TextHandlers; +using TagCloud.WordFilters; +using TagCloudTests.TestData; + +namespace TagCloudTests; + +public class FileTextHandlerTests +{ + private FileTextHandler handler; + + [TestCaseSource(typeof(FileTextHandlerTestData), nameof(FileTextHandlerTestData.Data))] + public void Handle(string input, List output) + { + handler = new FileTextHandler(new MemoryStream(Encoding.UTF8.GetBytes(input)), new MyStemWordFilter()); + handler.Handle() + .Should() + .BeEquivalentTo(output); + } +} \ No newline at end of file diff --git a/TagCloudTests/GlobalUsings.cs b/TagCloudTests/GlobalUsings.cs new file mode 100644 index 00000000..25c79abf --- /dev/null +++ b/TagCloudTests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using NUnit.Framework; +global using FluentAssertions; \ No newline at end of file diff --git a/TagCloudTests/HasIntersectedRectangleTests.cs b/TagCloudTests/HasIntersectedRectangleTests.cs new file mode 100644 index 00000000..fc8d1bee --- /dev/null +++ b/TagCloudTests/HasIntersectedRectangleTests.cs @@ -0,0 +1,28 @@ +using System.Drawing; +using TagCloud.Extensions; +using TagCloudTests.TestData; + +namespace TagCloudTests; + +public class HasIntersectedRectangleTests +{ + [TestCaseSource(typeof(IntersectedRectanglesTestCases), nameof(IntersectedRectanglesTestCases.Data))] + [TestCaseSource(typeof(NotIntersectedRectanglesTestCases), nameof(NotIntersectedRectanglesTestCases.Data))] + public bool ReturnValue(Rectangle first, Rectangle second) + { + return new[] { first, second }.HasIntersectedRectangles() + && new[] { second, first }.HasIntersectedRectangles(); + } + + [Test] + public void ReturnFalse_ThenConainsOneElement() + { + new[] { new Rectangle(0, 0, 1, 1) }.HasIntersectedRectangles().Should().BeFalse(); + } + + [Test] + public void ReturnFalse_ThenEmpty() + { + Array.Empty().HasIntersectedRectangles().Should().BeFalse(); + } +} \ No newline at end of file diff --git a/TagCloudTests/SpiralCloudShaperTests.cs b/TagCloudTests/SpiralCloudShaperTests.cs new file mode 100644 index 00000000..52a0a05d --- /dev/null +++ b/TagCloudTests/SpiralCloudShaperTests.cs @@ -0,0 +1,32 @@ +using System.Drawing; +using TagCloud; + +namespace TagCloudTests; + +public class SpiralCloudShaperTests +{ + private Point center; + private SpiralCloudShaper shaper; + + [SetUp] + public void SetUp() + { + center = new Point(0, 0); + shaper = SpiralCloudShaper.Create(center); + } + + [TestCase(-1, 1, TestName = "NegativeCoefficient")] + [TestCase(1, -1, TestName = "NegativeDeltaAngle")] + [TestCase(-1, -1, TestName = "NegativeDeltaAngleAndCoefficient")] + public void Throw_OnCreationWith(double coefficient, double deltaAngle) + { + Assert.Throws(() => SpiralCloudShaper.Create(center, coefficient, deltaAngle)); + } + + [TestCase(1, 1, TestName = "Integer coefficients")] + [TestCase(0.1D, 0.1D, TestName = "Double coefficients")] + public void NotThrow_OnCreationWith(double coefficient, double deltaAngle) + { + Assert.DoesNotThrow(() => SpiralCloudShaper.Create(center, coefficient, deltaAngle)); + } +} \ No newline at end of file diff --git a/TagCloudTests/TagCloudDrawerTests.cs b/TagCloudTests/TagCloudDrawerTests.cs new file mode 100644 index 00000000..ca29df44 --- /dev/null +++ b/TagCloudTests/TagCloudDrawerTests.cs @@ -0,0 +1,72 @@ +using System.Drawing; +using TagCloud; + +namespace TagCloudTests; + +public class TagCloudDrawerTests +{ + private const string RelativePathToTestDirectory = @"..\..\..\Test"; + + private string path; + private DirectoryInfo directory; + + private TagCloudDrawer drawer; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + if (!Path.Exists(RelativePathToTestDirectory)) + Directory.CreateDirectory(RelativePathToTestDirectory); + + path = Path.GetFullPath(RelativePathToTestDirectory); + directory = new DirectoryInfo(path); + + foreach (var file in directory.EnumerateFiles()) + file.Delete(); + } + + [SetUp] + public void SetUp() + { + drawer = TagCloudDrawer.Create( + path, + TestContext.CurrentContext.Test.Name, + new Font(FontFamily.GenericSerif, 1), + new ConstantColorSelector(Color.Black) + ); + } + + [Test] + public void Throw_ThenDrawEmptyList() + { + Assert.Throws(() => drawer.Draw(new List())); + } + + [Test] + public void Throw_ThenDirectoryDoesNotExist() + { + Assert.Throws(() => TagCloudDrawer.Create( + path: "PathDontExist", + name: "xxx", + new Font(FontFamily.GenericSerif, 1), + new ConstantColorSelector(Color.Black)) + ); + } + + [Test] + public void DrawOneTundredRectangles() + { + var testRect = new TextRectangle( + new Rectangle(0, 0, 1, 1), + text: "abc", + new Font(FontFamily.GenericSerif, 10) + ); + var textRectangles = Enumerable.Repeat(testRect, 100).ToList(); + drawer.Draw(textRectangles); + directory + .EnumerateFiles() + .Count(file => file.Name.Contains(nameof(DrawOneTundredRectangles))) + .Should() + .Be(1); + } +} \ No newline at end of file diff --git a/TagCloudTests/TagCloudTests.csproj b/TagCloudTests/TagCloudTests.csproj new file mode 100644 index 00000000..85d0052e --- /dev/null +++ b/TagCloudTests/TagCloudTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/TagCloudTests/Test/DrawOneTundredRectangles b/TagCloudTests/Test/DrawOneTundredRectangles new file mode 100644 index 00000000..9d89799c Binary files /dev/null and b/TagCloudTests/Test/DrawOneTundredRectangles differ diff --git a/TagCloudTests/TestData/ColorConverterTestData.cs b/TagCloudTests/TestData/ColorConverterTestData.cs new file mode 100644 index 00000000..c6a38568 --- /dev/null +++ b/TagCloudTests/TestData/ColorConverterTestData.cs @@ -0,0 +1,18 @@ +using System.Drawing; + +namespace TagCloudTests.TestData; + +public class ColorConverterTestData +{ + public static IEnumerable RightCases + { + get + { + yield return new TestCaseData("#FFFFFF").Returns(Color.FromArgb(255, 255, 255)); + yield return new TestCaseData("#FF0000").Returns(Color.FromArgb(255, 0, 0)); + yield return new TestCaseData("#808080").Returns(Color.FromArgb(128, 128, 128)); + yield return new TestCaseData("#123456").Returns(Color.FromArgb(18, 52, 86)); + } + } + +} \ No newline at end of file diff --git a/TagCloudTests/TestData/FileTextHandlerTestData.cs b/TagCloudTests/TestData/FileTextHandlerTestData.cs new file mode 100644 index 00000000..f59a6dde --- /dev/null +++ b/TagCloudTests/TestData/FileTextHandlerTestData.cs @@ -0,0 +1,32 @@ +using TagCloud.TextHandlers; + +namespace TagCloudTests.TestData; + +public class FileTextHandlerTestData +{ + public static IEnumerable Data + { + get + { + yield return new TestCaseData( + "два\nодин\nдва\nа\nтри\nтри\nтри", + new List + { + new() { Value = "один", Weight = 1 }, new() { Value = "два", Weight = 2 }, + new() { Value = "три", Weight = 3 } + } + ) + .SetName("ShouldExcludeConjAndNotExcludeOtherWords"); + yield return new TestCaseData("и а или", new List()) + .SetName("ShouldExcludeAllConj"); + yield return new TestCaseData( + "КАПС\nКаПС\nКАПс", + new List + { + new() { Value = "капс", Weight = 3 } + } + ) + .SetName("ShouldTransformToLowerCase"); + } + } +} \ No newline at end of file diff --git a/TagCloudTests/TestData/IntersectedRectanglesTestCases.cs b/TagCloudTests/TestData/IntersectedRectanglesTestCases.cs new file mode 100644 index 00000000..4a0dee0c --- /dev/null +++ b/TagCloudTests/TestData/IntersectedRectanglesTestCases.cs @@ -0,0 +1,29 @@ +using System.Drawing; + +namespace TagCloudTests.TestData; + +public class IntersectedRectanglesTestCases +{ + public static IEnumerable Data + { + get + { + yield return new TestCaseData(new Rectangle(1, 1, 1, 1), new Rectangle(1, 1, 1, 1)) + .SetName("WhenFirstEqualsSecond").Returns(true); + yield return new TestCaseData(new Rectangle(1, 1, 1, 1), new Rectangle(0, 0, 3, 3)) + .SetName("WhenFirstInSecond").Returns(true); + yield return new TestCaseData(new Rectangle(-1, 1, 2, 1), new Rectangle(0, 0, 3, 3)) + .SetName("WhenFirstEnterInSecond_ByLeftSide").Returns(true); + yield return new TestCaseData(new Rectangle(1, 1, 2, 1), new Rectangle(0, 0, 3, 3)) + .SetName("WhenFirstEnterInSecond_ByRightSide").Returns(true); + yield return new TestCaseData(new Rectangle(1, 1, 1, 2), new Rectangle(0, 0, 3, 3)) + .SetName("WhenFirstEnterInSecond_ByTopSide").Returns(true); + yield return new TestCaseData(new Rectangle(1, -1, 1, 2), new Rectangle(0, 0, 3, 3)) + .SetName("WhenFirstEnterInSecond_ByBottomSide").Returns(true); + yield return new TestCaseData(new Rectangle(0, 0, 2, 2), new Rectangle(1, 1, 2, 2)) + .SetName("WhenFirstRightTopAngle_IntersectWithSecondLeftBottomAngle").Returns(true); + yield return new TestCaseData(new Rectangle(0, 1, 2, 2), new Rectangle(0, 1, 2, 2)) + .SetName("WhenFirstRightBottomAngle_IntersectWithSecondLeftTopAngle").Returns(true); + } + } +} \ No newline at end of file diff --git a/TagCloudTests/TestData/NotIntersectedRectanglesTestCases.cs b/TagCloudTests/TestData/NotIntersectedRectanglesTestCases.cs new file mode 100644 index 00000000..9a1de361 --- /dev/null +++ b/TagCloudTests/TestData/NotIntersectedRectanglesTestCases.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagCloudTests.TestData; + +public class NotIntersectedRectanglesTestCases +{ + public static IEnumerable Data + { + get + { + yield return new TestCaseData(new Rectangle(0, 0, 1, 1), new Rectangle(1, 0, 1, 1)) + .SetName("WhenFirstRightBorder_СoncidesWithSecondLeftBorder").Returns(false); + yield return new TestCaseData(new Rectangle(0, 0, 1, 1), new Rectangle(0, 1, 1, 1)) + .SetName("WhenFirstTopBorder_СoncidesWithSecondBottomBorder").Returns(false); + } + } +} \ No newline at end of file diff --git a/di.sln b/di.sln index a50991da..2b37aeb6 100644 --- a/di.sln +++ b/di.sln @@ -2,6 +2,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter/di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud", "TagCloud\TagCloud.csproj", "{31580084-C883-4D29-BC58-C4BD60C5AC3F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudTests", "TagCloudTests\TagCloudTests.csproj", "{361973CC-5AFE-461D-BBE3-91D00162B7B9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudApp", "TagCloudApp\TagCloudApp.csproj", "{698B15A2-1214-41BD-B2F3-9CC77CA1BDE8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +18,17 @@ 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 + {31580084-C883-4D29-BC58-C4BD60C5AC3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31580084-C883-4D29-BC58-C4BD60C5AC3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31580084-C883-4D29-BC58-C4BD60C5AC3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31580084-C883-4D29-BC58-C4BD60C5AC3F}.Release|Any CPU.Build.0 = Release|Any CPU + {361973CC-5AFE-461D-BBE3-91D00162B7B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {361973CC-5AFE-461D-BBE3-91D00162B7B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {361973CC-5AFE-461D-BBE3-91D00162B7B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {361973CC-5AFE-461D-BBE3-91D00162B7B9}.Release|Any CPU.Build.0 = Release|Any CPU + {698B15A2-1214-41BD-B2F3-9CC77CA1BDE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {698B15A2-1214-41BD-B2F3-9CC77CA1BDE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {698B15A2-1214-41BD-B2F3-9CC77CA1BDE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {698B15A2-1214-41BD-B2F3-9CC77CA1BDE8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal