diff --git a/ConsoleApp/CommandLineParser.cs b/ConsoleApp/CommandLineParser.cs new file mode 100644 index 000000000..e3eee0253 --- /dev/null +++ b/ConsoleApp/CommandLineParser.cs @@ -0,0 +1,43 @@ +using CommandLine; +using ConsoleApp.Handlers; +using ConsoleApp.Options; + +namespace ConsoleApp; + +public class CommandLineParser: ICommandLineParser +{ + private readonly IOptionsHandler[] handlers; + private readonly IOptions[] options; + + public CommandLineParser(IOptionsHandler[] handlers, IOptions[] options) + { + this.handlers = handlers; + this.options = options; + } + + public void ParseFromConsole() + { + var types = options + .Select(opt => opt.GetType()) + .ToArray(); + + Console.WriteLine("Доступные команды \"--help\""); + while (true) + { + var input = Console.ReadLine(); + var args = input.Split(); + Parser.Default.ParseArguments(args, types) + .WithParsed(Parse); + } + } + + private void Parse(T options) where T : IOptions + { + var handler = handlers.FirstOrDefault(h => h.CanParse(options)); + if (handler is null) + throw new Exception("Обработчик параметров не найден."); + + var message = handler.WithParsed(options); + Console.WriteLine(message); + } +} \ No newline at end of file diff --git a/ConsoleApp/ConsoleApp.csproj b/ConsoleApp/ConsoleApp.csproj new file mode 100644 index 000000000..065e94d7b --- /dev/null +++ b/ConsoleApp/ConsoleApp.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + diff --git a/ConsoleApp/Handlers/ExitOptionsHandler.cs b/ConsoleApp/Handlers/ExitOptionsHandler.cs new file mode 100644 index 000000000..94cd789a9 --- /dev/null +++ b/ConsoleApp/Handlers/ExitOptionsHandler.cs @@ -0,0 +1,17 @@ +using ConsoleApp.Options; + +namespace ConsoleApp.Handlers; + +public class ExitOptionsHandler : IOptionsHandler +{ + public bool CanParse(IOptions options) + { + return options is ExitOptions; + } + + public string WithParsed(IOptions options) + { + Environment.Exit(0); + return "Завершение выполнения программы."; + } +} \ No newline at end of file diff --git a/ConsoleApp/Handlers/GenerateCloudOptionsHandler.cs b/ConsoleApp/Handlers/GenerateCloudOptionsHandler.cs new file mode 100644 index 000000000..a08ec5898 --- /dev/null +++ b/ConsoleApp/Handlers/GenerateCloudOptionsHandler.cs @@ -0,0 +1,59 @@ +using ConsoleApp.Options; +using MyStemWrapper; +using TagsCloudContainer; +using TagsCloudContainer.Settings; + +namespace ConsoleApp.Handlers; + +public class GenerateCloudOptionsHandler : IOptionsHandler +{ + private readonly MyStem myStem; + private readonly IAppSettings appSettings; + private readonly IAnalyseSettings analyseSettings; + private readonly ITagsCloudContainer cloudContainer; + + public GenerateCloudOptionsHandler(IAppSettings appSettings, MyStem myStem, IAnalyseSettings analyseSettings, + ITagsCloudContainer cloudContainer) + { + this.appSettings = appSettings; + this.myStem = myStem; + this.analyseSettings = analyseSettings; + this.cloudContainer = cloudContainer; + } + + public bool CanParse(IOptions options) + { + return options is GenerateCloudOptions; + } + + public string WithParsed(IOptions options) + { + Map(options); + return Execute(); + } + + private void Map(IOptions options) + { + if (options is GenerateCloudOptions opts) + Map(opts); + else + throw new ArgumentException(nameof(options)); + } + + private void Map(GenerateCloudOptions options) + { + appSettings.InputFile = options.InputFile; + appSettings.OutputFile = options.OutputFile; + + if (!string.IsNullOrWhiteSpace(options.AnalyseParameters)) + myStem.Parameters = "-" + options.AnalyseParameters; + if (options.ValidSpeechParts.Any()) + analyseSettings.ValidSpeechParts = options.ValidSpeechParts.ToArray(); + } + + private string Execute() + { + cloudContainer.GenerateImageToFile(appSettings.InputFile, appSettings.OutputFile); + return $"Успешно сохранено в файл - \"{appSettings.OutputFile}\"."; + } +} \ No newline at end of file diff --git a/ConsoleApp/Handlers/IOptionsHandler.cs b/ConsoleApp/Handlers/IOptionsHandler.cs new file mode 100644 index 000000000..fe5348bf2 --- /dev/null +++ b/ConsoleApp/Handlers/IOptionsHandler.cs @@ -0,0 +1,10 @@ +using ConsoleApp.Options; + +namespace ConsoleApp.Handlers; + +public interface IOptionsHandler +{ + public bool CanParse(IOptions options); + + public string WithParsed(IOptions options); +} \ No newline at end of file diff --git a/ConsoleApp/Handlers/SetImageOptionsHandler.cs b/ConsoleApp/Handlers/SetImageOptionsHandler.cs new file mode 100644 index 000000000..34fb6d1fd --- /dev/null +++ b/ConsoleApp/Handlers/SetImageOptionsHandler.cs @@ -0,0 +1,48 @@ +using ConsoleApp.Options; +using SixLabors.ImageSharp; +using TagsCloudContainer.Settings; + +namespace ConsoleApp.Handlers; + +public class SetImageOptionsHandler : IOptionsHandler +{ + private readonly IImageSettings imageSettings; + + public SetImageOptionsHandler(IImageSettings imageSettings) + { + this.imageSettings = imageSettings; + } + + private void Map(SetImageOptions options) + { + if (options.PrimaryColor != default) + imageSettings.PrimaryColor = options.PrimaryColor; + if (options.BackgroundColor != default) + imageSettings.BackgroundColor = options.BackgroundColor; + if (options.Width != default) + imageSettings.ImageSize = new Size(options.Width, imageSettings.ImageSize.Height); + if (options.Height != default) + imageSettings.ImageSize = new Size(imageSettings.ImageSize.Width, options.Height); + if (options.Font is not null) + imageSettings.TextOptions.Font = options.Font; + } + + private void Map(IOptions options) + { + if (options is SetImageOptions opts) + Map(opts); + else + throw new ArgumentException(nameof(options)); + } + + public bool CanParse(IOptions options) + { + return options is SetImageOptions; + } + + public string WithParsed(IOptions options) + { + Map(options); + return "Настройки изображения установлены."; + } +} \ No newline at end of file diff --git a/ConsoleApp/ICommandLineParser.cs b/ConsoleApp/ICommandLineParser.cs new file mode 100644 index 000000000..0146fe314 --- /dev/null +++ b/ConsoleApp/ICommandLineParser.cs @@ -0,0 +1,6 @@ +namespace ConsoleApp; + +public interface ICommandLineParser +{ + public void ParseFromConsole(); +} \ No newline at end of file diff --git a/ConsoleApp/Options/ExitOptions.cs b/ConsoleApp/Options/ExitOptions.cs new file mode 100644 index 000000000..fb6f1249c --- /dev/null +++ b/ConsoleApp/Options/ExitOptions.cs @@ -0,0 +1,8 @@ +using CommandLine; + +namespace ConsoleApp.Options; + +[Verb("exit", HelpText = "Закончить выполнение программы")] +public class ExitOptions: IOptions +{ +} \ No newline at end of file diff --git a/ConsoleApp/Options/GenerateCloudOptions.cs b/ConsoleApp/Options/GenerateCloudOptions.cs new file mode 100644 index 000000000..f7c033b9e --- /dev/null +++ b/ConsoleApp/Options/GenerateCloudOptions.cs @@ -0,0 +1,19 @@ +using CommandLine; + +namespace ConsoleApp.Options; + +[Verb("generate", HelpText = "Предобработка слов")] +public class GenerateCloudOptions: IOptions +{ + [Option('i', "input", Required = true, HelpText = "Путь к файлу текста для анализа.")] + public string InputFile { get; set; } + + [Option('o', "output", Required = true, HelpText = "Путь к сохранению изображения.")] + public string OutputFile { get; set; } + + [Option('p', "params", HelpText = "Параметры вывода MyStem")] + public string AnalyseParameters { get; set; } + + [Value(1, Max = 14, HelpText = "Части речи, которые буду задействованы при анализе.")] + public IEnumerable ValidSpeechParts { get; set; } = new string[0]; +} \ No newline at end of file diff --git a/ConsoleApp/Options/IOptions.cs b/ConsoleApp/Options/IOptions.cs new file mode 100644 index 000000000..3efcfa1e9 --- /dev/null +++ b/ConsoleApp/Options/IOptions.cs @@ -0,0 +1,5 @@ +namespace ConsoleApp.Options; + +public interface IOptions +{ +} \ No newline at end of file diff --git a/ConsoleApp/Options/SetImageOptions.cs b/ConsoleApp/Options/SetImageOptions.cs new file mode 100644 index 000000000..55818af3f --- /dev/null +++ b/ConsoleApp/Options/SetImageOptions.cs @@ -0,0 +1,24 @@ +using CommandLine; +using SixLabors.Fonts; +using SixLabors.ImageSharp; + +namespace ConsoleApp.Options; + +[Verb("image", HelpText = "Настройка изображения")] +public class SetImageOptions: IOptions +{ + [Option('c', "color", HelpText = "Основной цвет")] + public Color PrimaryColor { get; set; } + + [Option('b', "background", HelpText = "Цвет заднего фона")] + public Color BackgroundColor { get; set; } + + [Option('w', "width", HelpText = "Ширина")] + public int Width { get; set; } + + [Option('h', "height", HelpText = "Высота")] + public int Height { get; set; } + + [Option('f', "font",HelpText = "Шрифт")] + public Font Font { get; set; } +} \ No newline at end of file diff --git a/ConsoleApp/Program.cs b/ConsoleApp/Program.cs new file mode 100644 index 000000000..d04b96289 --- /dev/null +++ b/ConsoleApp/Program.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Autofac; +using MyStemWrapper; +using TagsCloudContainer; +using TagsCloudContainer.Settings; + +namespace ConsoleApp; + +public class Program +{ + public static void Main() + { + var builder = new ContainerBuilder(); + ConfigureService(builder); + var container = builder.Build(); + + using var scope = container.BeginLifetimeScope(); + var commandLineReader = scope.Resolve(); + commandLineReader.ParseFromConsole(); + } + + public static void ConfigureService(ContainerBuilder builder) + { + RegisterAssemblyTypes(builder, typeof(Tag).GetTypeInfo().Assembly); + RegisterAssemblyTypes(builder, typeof(CommandLineParser).GetTypeInfo().Assembly); + + var location = Assembly.GetExecutingAssembly().Location; + var path = Path.GetDirectoryName(location); + var myStem = new MyStem + { + PathToMyStem = $"{path}\\mystem.exe", + Parameters = "-nli", + }; + builder.RegisterInstance(myStem).AsSelf().SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + } + + private static void RegisterAssemblyTypes(ContainerBuilder builder, Assembly assembly) + { + builder.RegisterAssemblyTypes(assembly) + .AsImplementedInterfaces(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Tests/CloudLayouters/CircularCloudLayouterTests.cs b/TagsCloudContainer.Tests/CloudLayouters/CircularCloudLayouterTests.cs new file mode 100644 index 000000000..d1f507137 --- /dev/null +++ b/TagsCloudContainer.Tests/CloudLayouters/CircularCloudLayouterTests.cs @@ -0,0 +1,119 @@ +using System.Drawing; +using FluentAssertions; +using NUnit.Framework.Internal; +using TagsCloudContainer.CloudLayouters; +using TagsCloudContainer.Settings; + +namespace TagsCloudContainer.Tests.CloudLayouters; + +[TestFixture] +[TestOf(typeof(CircularCloudLayouter))] +public class CircularCloudLayouterTests +{ + private Point center; + private CircularCloudLayouter sut; + private ImageSettings imageSettings; + + private Randomizer Random => TestContext.CurrentContext.Random; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + imageSettings = new ImageSettings(); + center = new Point(imageSettings.ImageSize.Width / 2, imageSettings.ImageSize.Height / 2); + } + + [SetUp] + public void SetUp() + { + sut = new CircularCloudLayouter(imageSettings); + } + + [Test] + public void PutNextRectangle_ShouldThrow_WhenSizeNotPositive() + { + var action = () => sut.PutNextRectangle(new Size(0, 0)); + + action.Should().Throw(); + } + + [Test] + public void PutNextRectangle_ShouldReturnRectangleWithGivenSize() + { + var size = new Size(10, 10); + + var rectangle = sut.PutNextRectangle(size); + + rectangle.Size.Should().Be(size); + } + + [Test] + public void PutNextRectangle_ShouldPlaceFirstRectangleInCenter() + { + var size = new Size(50, 50); + var offsetCenter = center - size / 2; + + var rectangle = sut.PutNextRectangle(size); + + rectangle.Location.Should().Be(offsetCenter); + } + + [Test] + public void PutNextRectangle_ShouldReturnNotIntersectingRectangles() + { + var sizes = GetRandomSizes(70); + + var actualRectangles = sizes.Select(x => sut.PutNextRectangle(x)).ToList(); + + for (var i = 0; i < actualRectangles.Count - 1; i++) + { + for (var j = i + 1; j < actualRectangles.Count; j++) + { + actualRectangles[i].IntersectsWith(actualRectangles[j]).Should().BeFalse(); + } + } + } + + [Test] + public void PutNextRectangle_ShouldCreateLayoutCloseToCircle() + { + var sizes = GetRandomSizes(50); + + var rectangles = sizes.Select(size => sut.PutNextRectangle(size)).ToList(); + var rectanglesSquare = rectangles + .Select(x => x.Width * x.Height) + .Sum(); + var circleSquare = CalculateBoundingCircleSquare(rectangles); + + rectanglesSquare.Should().BeInRange((int)(circleSquare * 0.7), (int)(circleSquare * 1.3)); + } + + private Size GetRandomSize() + { + //return new Size(Random.Next(30, 40), Random.Next(30, 40)); + return new Size(Random.Next(100, 120), Random.Next(30, 90)); + } + + private IEnumerable GetRandomSizes(int count) + { + var sizes = new List(count); + for (var i = 0; i < count; i++) + sizes.Add(GetRandomSize()); + return sizes; + } + + private double CalculateBoundingCircleSquare(List rectangles) + { + var rect = rectangles + .Where(x => x.Contains(x.X, center.Y)) + .MaxBy(x => Math.Abs(x.X - center.X)); + var width = Math.Abs(rect.X - center.X); + + rect = rectangles + .Where(x => x.Contains(center.X, x.Y)) + .MaxBy(x => Math.Abs(x.Y - center.Y)); + var height = Math.Abs(rect.Y - center.Y); + + return Math.Max(width, height) * Math.Max(width, height) * Math.PI; + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Tests/GlobalUsings.cs b/TagsCloudContainer.Tests/GlobalUsings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/TagsCloudContainer.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj b/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj new file mode 100644 index 000000000..504f33217 --- /dev/null +++ b/TagsCloudContainer.Tests/TagsCloudContainer.Tests.csproj @@ -0,0 +1,27 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/TagsCloudContainer.Tests/TagsCloudGeneratorTests.cs b/TagsCloudContainer.Tests/TagsCloudGeneratorTests.cs new file mode 100644 index 000000000..3eef1774a --- /dev/null +++ b/TagsCloudContainer.Tests/TagsCloudGeneratorTests.cs @@ -0,0 +1,58 @@ +using System.Drawing; +using Autofac; +using FluentAssertions; +using TagsCloudContainer.CloudGenerators; +using TagsCloudContainer.Settings; +using Program = ConsoleApp.Program; + +namespace TagsCloudContainer.Tests; + +[TestFixture] +[TestOf(typeof(TagsCloudGenerator))] +public class TagsCloudGeneratorTests +{ + private TagsCloudGenerator sut; + private ImageSettings imageSettings; + + [OneTimeSetUp] + public void Setup() + { + var builder = new ContainerBuilder(); + Program.ConfigureService(builder); + builder.RegisterType().AsSelf(); + var scope = builder.Build(); + sut = scope.Resolve(); + imageSettings = new ImageSettings(); + } + + [Test] + public void Should_LocatePopularWordInCenter() + { + var popularWord = "bbb"; + var center = new Point(imageSettings.ImageSize.Width / 2, imageSettings.ImageSize.Height / 2); + var wordsDetails = new[] + { + new WordDetails("aaa", 4), + new WordDetails(popularWord, 10), + new WordDetails("ccc", 4), + }; + + var cloud = sut.Generate(wordsDetails); + + var tag = cloud.Tags.First(tag => tag.Word == popularWord); + tag.Rectangle.Contains(center).Should().BeTrue(); + } + + [Test] + public void Should_ContainsAllWords() + { + var words = new[] { "a", "b", "c", "d", "e", "f", "g" }; + var wordsDetails = words + .Select(word => new WordDetails(word)) + .ToArray(); + + var cloud = sut.Generate(wordsDetails); + + cloud.Tags.Select(tag => tag.Word).Should().BeEquivalentTo(words); + } +} \ No newline at end of file diff --git a/TagsCloudContainer.Tests/TextPreprocessorTests.cs b/TagsCloudContainer.Tests/TextPreprocessorTests.cs new file mode 100644 index 000000000..b46820c8f --- /dev/null +++ b/TagsCloudContainer.Tests/TextPreprocessorTests.cs @@ -0,0 +1,67 @@ +using Autofac; +using ConsoleApp; +using FluentAssertions; +using TagsCloudContainer.TextAnalysers; + +namespace TagsCloudContainer.Tests; + +[TestFixture] +[TestOf(typeof(TextPreprocessor))] +public class TextPreprocessorTests +{ + private TextPreprocessor sut; + + [OneTimeSetUp] + public void Setup() + { + var builder = new ContainerBuilder(); + Program.ConfigureService(builder); + builder.RegisterType().AsSelf(); + + var scope = builder.Build(); + sut = scope.Resolve(); + } + + [Test] + public void Should_NoContainsEmptyWords() + { + var text = "собака . ! "; + + var wordsDetails = sut.Preprocess(text); + + wordsDetails.Should().HaveCount(1); + wordsDetails.Should().BeEquivalentTo(new[] {new WordDetails("собака", speechPart: "S")}); + } + + [Test] + public void Should_TransformToLowerCase() + { + var text = "Собака Любить Кость"; + var expected = text.Split().Select(word => word.ToLower()); + + var wordsDetails = sut.Preprocess(text); + + wordsDetails.Select(word => word.Word).Should().BeEquivalentTo(expected); + } + + [Test] + public void Should_TransformToInitialForm() + { + var text = "собаки любят красивых собак"; + var expected = new[] { "собака", "любить", "красивый" }; + + var wordsDetails = sut.Preprocess(text); + + wordsDetails.Select(word => word.Word).Should().BeEquivalentTo(expected); + } + + [Test] + public void Should_IgnoreBoringWords() + { + var text = "я не ваш только с от к мы нашему кому"; + + var wordsDetails = sut.Preprocess(text); + + wordsDetails.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Assets/ImagesResources/25_rect.jpeg b/TagsCloudContainer/Assets/ImagesResources/25_rect.jpeg new file mode 100644 index 000000000..557e5bac9 Binary files /dev/null and b/TagsCloudContainer/Assets/ImagesResources/25_rect.jpeg differ diff --git a/TagsCloudContainer/Assets/ImagesResources/70_squares.jpeg b/TagsCloudContainer/Assets/ImagesResources/70_squares.jpeg new file mode 100644 index 000000000..a01855350 Binary files /dev/null and b/TagsCloudContainer/Assets/ImagesResources/70_squares.jpeg differ diff --git a/TagsCloudContainer/Assets/ImagesResources/75_rect.jpeg b/TagsCloudContainer/Assets/ImagesResources/75_rect.jpeg new file mode 100644 index 000000000..23b01d4a1 Binary files /dev/null and b/TagsCloudContainer/Assets/ImagesResources/75_rect.jpeg differ diff --git a/TagsCloudContainer/Assets/ImagesResources/TagsCloud.png b/TagsCloudContainer/Assets/ImagesResources/TagsCloud.png new file mode 100644 index 000000000..1a096e775 Binary files /dev/null and b/TagsCloudContainer/Assets/ImagesResources/TagsCloud.png differ diff --git a/TagsCloudContainer/CloudGenerators/ITagsCloudGenerator.cs b/TagsCloudContainer/CloudGenerators/ITagsCloudGenerator.cs new file mode 100644 index 000000000..f6d860cbc --- /dev/null +++ b/TagsCloudContainer/CloudGenerators/ITagsCloudGenerator.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.CloudGenerators; + +public interface ITagsCloudGenerator +{ + public ITagCloud Generate(IEnumerable wordsDetails); +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudGenerators/TagsCloudGenerator.cs b/TagsCloudContainer/CloudGenerators/TagsCloudGenerator.cs new file mode 100644 index 000000000..d6b273008 --- /dev/null +++ b/TagsCloudContainer/CloudGenerators/TagsCloudGenerator.cs @@ -0,0 +1,42 @@ +using TagsCloudContainer.CloudLayouters; +using TagsCloudContainer.TextMeasures; + +namespace TagsCloudContainer.CloudGenerators; + +public class TagsCloudGenerator: ITagsCloudGenerator +{ + private readonly ITagTextMeasurer tagTextProvider; + private readonly ICloudLayouterProvider cloudLayouterProvider; + + public TagsCloudGenerator(ICloudLayouterProvider cloudLayouterProvider, ITagTextMeasurer tagTextProvider) + { + this.cloudLayouterProvider = cloudLayouterProvider; + this.tagTextProvider = tagTextProvider; + } + + public ITagCloud Generate(IEnumerable wordsDetails) + { + var cloudLayouter = cloudLayouterProvider.Get(); + var sorted = SortWords(wordsDetails); + + var tags = new List(); + foreach (var word in sorted) + { + var size = tagTextProvider.Measure(word); + var rect = cloudLayouter.PutNextRectangle(size); + tags.Add(new Tag(rect, word)); + } + + return new TagsCloud + { + Tags = tags + }; + } + + private IEnumerable SortWords(IEnumerable wordsDetails) + { + return wordsDetails + .OrderByDescending(x => x.Frequency) + .Select(x => x.Word); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/CircularCloudLayouter.cs b/TagsCloudContainer/CloudLayouters/CircularCloudLayouter.cs new file mode 100644 index 000000000..affe9f73f --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/CircularCloudLayouter.cs @@ -0,0 +1,99 @@ +using System.Drawing; +using TagsCloudContainer.Settings; + +namespace TagsCloudContainer.CloudLayouters; + +public class CircularCloudLayouter : ICloudLayouter +{ + private readonly List rectangles; + private Point center; + private double spiralStep; + private double angle; + private const double DefaultAngleStep = Math.PI / 10; + private const double DefaultSpiralStep = 1; + private const double FullCircle = Math.PI * 2; + private const int SpiralStepThreshold = 10; + + public CircularCloudLayouter(IImageSettings imageSettings) + { + center = new Point(imageSettings.ImageSize.Width / 2, imageSettings.ImageSize.Height / 2); + rectangles = new List(); + spiralStep = 1; + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Width == 0 || rectangleSize.Height == 0) + throw new ArgumentException($"{nameof(rectangleSize)} should be with positive width and height"); + + var location = GetPosition(rectangleSize); + var rectangle = new Rectangle(location, rectangleSize); + rectangles.Add(rectangle); + return rectangle; + } + + private Point GetPosition(Size rectangleSize) + { + if (rectangles.Count == 0) + { + center.Offset(new Point(rectangleSize / -2)); + return center; + } + + return FindApproximatePosition(rectangleSize); + } + + private Point FindApproximatePosition(Size rectangleSize) + { + var currentAngle = angle; + while (true) + { + var candidateLocation = new Point(center.X + (int)(spiralStep * Math.Cos(currentAngle)), + center.Y + (int)(spiralStep * Math.Sin(currentAngle))); + var candidateRectangle = new Rectangle(candidateLocation, rectangleSize); + + if (!IntersectsWithAny(candidateRectangle)) + { + rectangles.Add(candidateRectangle); + angle = currentAngle; + + return candidateRectangle.Location; + } + + currentAngle = CalculateAngle(currentAngle); + } + } + + private bool IntersectsWithAny(Rectangle candidateRectangle) + { + return rectangles + .Any(candidateRectangle.IntersectsWith); + } + + private double CalculateAngle(double currentAngle) + { + currentAngle += GetAngleStep(); + if (currentAngle > FullCircle) + { + currentAngle %= FullCircle; + UpdateSpiral(); + } + + return currentAngle; + } + + private void UpdateSpiral() + { + spiralStep += DefaultSpiralStep; + } + + private double GetAngleStep() + { + var angleStep = DefaultAngleStep; + var stepCount = (int)spiralStep / SpiralStepThreshold; + if (stepCount > 0) + angleStep /= stepCount; + + return angleStep; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/CloudLayouterProvider.cs b/TagsCloudContainer/CloudLayouters/CloudLayouterProvider.cs new file mode 100644 index 000000000..25a431c8d --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/CloudLayouterProvider.cs @@ -0,0 +1,18 @@ +using Autofac; + +namespace TagsCloudContainer.CloudLayouters; + +public class CloudLayouterProvider: ICloudLayouterProvider +{ + private readonly ILifetimeScope scope; + + public CloudLayouterProvider(ILifetimeScope scope) + { + this.scope = scope; + } + + public ICloudLayouter Get() + { + return scope.Resolve(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/ICloudLayouter.cs b/TagsCloudContainer/CloudLayouters/ICloudLayouter.cs new file mode 100644 index 000000000..070f32bb3 --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/ICloudLayouter.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloudContainer.CloudLayouters; + +public interface ICloudLayouter +{ + public Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/TagsCloudContainer/CloudLayouters/ICloudLayouterProvider.cs b/TagsCloudContainer/CloudLayouters/ICloudLayouterProvider.cs new file mode 100644 index 000000000..de61f931e --- /dev/null +++ b/TagsCloudContainer/CloudLayouters/ICloudLayouterProvider.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.CloudLayouters; + +public interface ICloudLayouterProvider +{ + public ICloudLayouter Get(); +} \ No newline at end of file diff --git a/TagsCloudContainer/FileProviders/FileReader.cs b/TagsCloudContainer/FileProviders/FileReader.cs new file mode 100644 index 000000000..00a607457 --- /dev/null +++ b/TagsCloudContainer/FileProviders/FileReader.cs @@ -0,0 +1,9 @@ +namespace TagsCloudContainer.FileProviders; + +public class FileReader: IFileReader +{ + public string ReadFile(string filePath) + { + return File.ReadAllText(filePath); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/FileProviders/IFileReader.cs b/TagsCloudContainer/FileProviders/IFileReader.cs new file mode 100644 index 000000000..adc20dbc5 --- /dev/null +++ b/TagsCloudContainer/FileProviders/IFileReader.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.FileProviders; + +public interface IFileReader +{ + public string ReadFile(string filePath); +} \ No newline at end of file diff --git a/TagsCloudContainer/FileProviders/IImageProvider.cs b/TagsCloudContainer/FileProviders/IImageProvider.cs new file mode 100644 index 000000000..6881eb043 --- /dev/null +++ b/TagsCloudContainer/FileProviders/IImageProvider.cs @@ -0,0 +1,8 @@ +using SixLabors.ImageSharp; + +namespace TagsCloudContainer.FileProviders; + +public interface IImageProvider +{ + public void SaveImage(Image image, string filePath); +} \ No newline at end of file diff --git a/TagsCloudContainer/FileProviders/ImageProvider.cs b/TagsCloudContainer/FileProviders/ImageProvider.cs new file mode 100644 index 000000000..1b554d08b --- /dev/null +++ b/TagsCloudContainer/FileProviders/ImageProvider.cs @@ -0,0 +1,11 @@ +using SixLabors.ImageSharp; + +namespace TagsCloudContainer.FileProviders; + +public class ImageProvider: IImageProvider +{ + public void SaveImage(Image image, string filePath) + { + image.Save(filePath); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/ITagCloud.cs b/TagsCloudContainer/ITagCloud.cs new file mode 100644 index 000000000..00cc97e6e --- /dev/null +++ b/TagsCloudContainer/ITagCloud.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer; + +public interface ITagCloud +{ + public List Tags { get; set; } +} \ No newline at end of file diff --git a/TagsCloudContainer/ITagsCloudContainer.cs b/TagsCloudContainer/ITagsCloudContainer.cs new file mode 100644 index 000000000..b68bc9828 --- /dev/null +++ b/TagsCloudContainer/ITagsCloudContainer.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer; + +public interface ITagsCloudContainer +{ + public void GenerateImageToFile(string inputFile, string outputFile); +} \ No newline at end of file diff --git a/TagsCloudContainer/README.md b/TagsCloudContainer/README.md new file mode 100644 index 000000000..cea9a9f26 --- /dev/null +++ b/TagsCloudContainer/README.md @@ -0,0 +1,17 @@ +# Примеры визуализации облака тегов + +| ![Облако с квадратными тегами](./Assets/ImagesResources/25_rect.jpeg) | +|:----------------------------------------------------------------------:| +| *Визуализация облака с квадратными тегами* | + +| ![Облако с прямоугольными тегами](./Assets/ImagesResources/70_squares.jpeg) | +|:----------------------------------------------------------------------------:| +| *Визуализация облака с прямоугольными тегами* | + +| ![Облако с различными тегам](./Assets/ImagesResources/75_rect.jpeg) | +|:--------------------------------------------------------------------:| +| *Визуализация облака с различными тегами* | + +| ![Облако из слов](./Assets/ImagesResources/TagsCloud.png) | +|:----------------------------------------------------------:| +| *Визуализация облака из слов* | \ No newline at end of file diff --git a/TagsCloudContainer/Settings/AnalyseSettings.cs b/TagsCloudContainer/Settings/AnalyseSettings.cs new file mode 100644 index 000000000..1e04ff518 --- /dev/null +++ b/TagsCloudContainer/Settings/AnalyseSettings.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.Settings; + +public class AnalyseSettings: IAnalyseSettings +{ + public string[] ValidSpeechParts { get; set; } = { "V", "S", "A", "ADV", "NUM" }; +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/AppSettings.cs b/TagsCloudContainer/Settings/AppSettings.cs new file mode 100644 index 000000000..7d1bf0c70 --- /dev/null +++ b/TagsCloudContainer/Settings/AppSettings.cs @@ -0,0 +1,8 @@ +namespace TagsCloudContainer.Settings; + +public class AppSettings: IAppSettings +{ + public string InputFile { get; set; } + + public string OutputFile { get; set; } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/IAnalyseSettings.cs b/TagsCloudContainer/Settings/IAnalyseSettings.cs new file mode 100644 index 000000000..c624d2d03 --- /dev/null +++ b/TagsCloudContainer/Settings/IAnalyseSettings.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.Settings; + +public interface IAnalyseSettings +{ + public string[] ValidSpeechParts { get; set; } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/IAppSettings.cs b/TagsCloudContainer/Settings/IAppSettings.cs new file mode 100644 index 000000000..ea906f0c8 --- /dev/null +++ b/TagsCloudContainer/Settings/IAppSettings.cs @@ -0,0 +1,8 @@ +namespace TagsCloudContainer.Settings; + +public interface IAppSettings +{ + public string InputFile { get; set; } + + public string OutputFile { get; set; } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/IImageSettings.cs b/TagsCloudContainer/Settings/IImageSettings.cs new file mode 100644 index 000000000..0aaf1c38b --- /dev/null +++ b/TagsCloudContainer/Settings/IImageSettings.cs @@ -0,0 +1,18 @@ +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace TagsCloudContainer.Settings; + +public interface IImageSettings +{ + public TextOptions TextOptions { get; set; } + + public Size ImageSize { get; set; } + + public Color PrimaryColor { get; set; } + + public Color SecondaryColor { get; set; } + + public Color BackgroundColor { get; set; } +} \ No newline at end of file diff --git a/TagsCloudContainer/Settings/ImageSettings.cs b/TagsCloudContainer/Settings/ImageSettings.cs new file mode 100644 index 000000000..5c54bfcf7 --- /dev/null +++ b/TagsCloudContainer/Settings/ImageSettings.cs @@ -0,0 +1,17 @@ +using SixLabors.Fonts; +using SixLabors.ImageSharp; + +namespace TagsCloudContainer.Settings; + +public class ImageSettings: IImageSettings +{ + public Color PrimaryColor { get; set; } = Color.Yellow; + + public Color SecondaryColor { get; set; } = Color.Blue; + + public Color BackgroundColor { get; set; } = Color.Black; + + public Size ImageSize { get; set; } = new(1000, 1000); + + public TextOptions TextOptions { get; set; } = new(SystemFonts.CreateFont("Segoe UI", 30, FontStyle.Regular)); +} \ No newline at end of file diff --git a/TagsCloudContainer/Tag.cs b/TagsCloudContainer/Tag.cs new file mode 100644 index 000000000..445609816 --- /dev/null +++ b/TagsCloudContainer/Tag.cs @@ -0,0 +1,16 @@ +using System.Drawing; + +namespace TagsCloudContainer; + +public struct Tag +{ + public string Word { get; } + + public Rectangle Rectangle { get; } + + public Tag(Rectangle rectangle, string word) + { + Rectangle = rectangle; + Word = word; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/TagsCloud.cs b/TagsCloudContainer/TagsCloud.cs new file mode 100644 index 000000000..7170431fd --- /dev/null +++ b/TagsCloudContainer/TagsCloud.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer; + +public class TagsCloud: ITagCloud +{ + public List Tags { get; set; } +} \ No newline at end of file diff --git a/TagsCloudContainer/TagsCloudContainer.cs b/TagsCloudContainer/TagsCloudContainer.cs new file mode 100644 index 000000000..9584a958e --- /dev/null +++ b/TagsCloudContainer/TagsCloudContainer.cs @@ -0,0 +1,34 @@ +using TagsCloudContainer.CloudGenerators; +using TagsCloudContainer.FileProviders; +using TagsCloudContainer.TextAnalysers; +using TagsCloudContainer.Visualizers; + +namespace TagsCloudContainer; + +public class TagsCloudContainer : ITagsCloudContainer +{ + private readonly IFileReader fileReader; + private readonly ICloudVisualizer visualizer; + private readonly ITagsCloudGenerator cloudGenerator; + private readonly ITextPreprocessor textPreprocessor; + private readonly IImageProvider imageProvider; + + public TagsCloudContainer(ITagsCloudGenerator cloudGenerator, ICloudVisualizer visualizer, + ITextPreprocessor textPreprocessor, IFileReader fileReader, IImageProvider imageProvider) + { + this.cloudGenerator = cloudGenerator; + this.visualizer = visualizer; + this.textPreprocessor = textPreprocessor; + this.fileReader = fileReader; + this.imageProvider = imageProvider; + } + + public void GenerateImageToFile(string inputFile, string outputFile) + { + var text = fileReader.ReadFile(inputFile); + var preprocessedText = textPreprocessor.Preprocess(text); + var cloud = cloudGenerator.Generate(preprocessedText); + using var image = visualizer.DrawImage(cloud); + imageProvider.SaveImage(image, outputFile); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/TagsCloudContainer.csproj b/TagsCloudContainer/TagsCloudContainer.csproj new file mode 100644 index 000000000..02a57f87b --- /dev/null +++ b/TagsCloudContainer/TagsCloudContainer.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/TagsCloudContainer/TextAnalysers/FrequencyCalculator.cs b/TagsCloudContainer/TextAnalysers/FrequencyCalculator.cs new file mode 100644 index 000000000..9f77102de --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/FrequencyCalculator.cs @@ -0,0 +1,19 @@ +namespace TagsCloudContainer.TextAnalysers; + +public class FrequencyCalculator: IFrequencyCalculator +{ + public IEnumerable CalculateFrequency(IEnumerable wordsDetails) + { + var wordsDictionary = new Dictionary(); + foreach (var details in wordsDetails) + { + if (!wordsDictionary.ContainsKey(details.Word)) + wordsDictionary.TryAdd(details.Word, details); + else + wordsDictionary[details.Word].Frequency++; + } + + return wordsDictionary + .Select(pair => pair.Value); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/TextAnalysers/IFrequencyCalculator.cs b/TagsCloudContainer/TextAnalysers/IFrequencyCalculator.cs new file mode 100644 index 000000000..fe6ca50e7 --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/IFrequencyCalculator.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.TextAnalysers; + +public interface IFrequencyCalculator +{ + public IEnumerable CalculateFrequency(IEnumerable wordsDetails); +} \ No newline at end of file diff --git a/TagsCloudContainer/TextAnalysers/IMyStemParser.cs b/TagsCloudContainer/TextAnalysers/IMyStemParser.cs new file mode 100644 index 000000000..652d7e6d2 --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/IMyStemParser.cs @@ -0,0 +1,8 @@ +namespace TagsCloudContainer.TextAnalysers; + +public interface IMyStemParser +{ + public bool CanParse(string wordInfo); + + public WordDetails Parse(string wordInfo); +} \ No newline at end of file diff --git a/TagsCloudContainer/TextAnalysers/ITextPreprocessor.cs b/TagsCloudContainer/TextAnalysers/ITextPreprocessor.cs new file mode 100644 index 000000000..90cd18573 --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/ITextPreprocessor.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.TextAnalysers; + +public interface ITextPreprocessor +{ + public WordDetails[] Preprocess(string text); +} \ No newline at end of file diff --git a/TagsCloudContainer/TextAnalysers/MyStemParser.cs b/TagsCloudContainer/TextAnalysers/MyStemParser.cs new file mode 100644 index 000000000..51080a778 --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/MyStemParser.cs @@ -0,0 +1,34 @@ +namespace TagsCloudContainer.TextAnalysers; + +public class MyStemParser: IMyStemParser +{ + public bool CanParse(string wordInfo) + { + var wordInfoSegments = ParseWordInfo(wordInfo); + if (wordInfoSegments is null) + return false; + + var word = wordInfoSegments[0]; + return !word.Contains("??"); + } + + public WordDetails Parse(string wordInfo) + { + var wordInfoSegments = ParseWordInfo(wordInfo); + if (wordInfoSegments is null) + throw new ArgumentException(nameof(wordInfo)); + + var word = wordInfoSegments[0]; + if (word.Contains("??")) + throw new ArgumentException(nameof(wordInfo)); + + var speechPart = wordInfoSegments[1].Split(',').First(); + return new WordDetails(word, speechPart: speechPart); + } + + private string[]? ParseWordInfo(string wordInfo) + { + var wordInfoSegments = wordInfo.Split('='); + return wordInfoSegments.Length >= 2 ? wordInfoSegments : null; + } +} \ No newline at end of file diff --git a/TagsCloudContainer/TextAnalysers/TextPreprocessor.cs b/TagsCloudContainer/TextAnalysers/TextPreprocessor.cs new file mode 100644 index 000000000..74d3265c4 --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/TextPreprocessor.cs @@ -0,0 +1,35 @@ +using MyStemWrapper; +using TagsCloudContainer.TextAnalysers.WordsFilters; + +namespace TagsCloudContainer.TextAnalysers; + +public class TextPreprocessor: ITextPreprocessor +{ + private readonly MyStem myStem; + private readonly IMyStemParser myStemParser; + private readonly IWordsFilter wordsFilter; + private readonly IFrequencyCalculator frequencyCalculator; + + public TextPreprocessor(MyStem myStem, IMyStemParser myStemParser, IWordsFilter wordsFilter, IFrequencyCalculator frequencyCalculator) + { + this.myStem = myStem; + this.myStemParser = myStemParser; + this.wordsFilter = wordsFilter; + this.frequencyCalculator = frequencyCalculator; + } + + public WordDetails[] Preprocess(string text) + { + var analyzed = myStem.Analysis(text); + var wordInfos = analyzed.Split('\n'); + var wordsDetails = new List(wordInfos.Length); + foreach (var wordInfo in wordInfos) + { + if (myStemParser.CanParse(wordInfo)) + wordsDetails.Add(myStemParser.Parse(wordInfo)); + } + + return wordsFilter.Filter(frequencyCalculator.CalculateFrequency(wordsDetails)) + .ToArray(); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/TextAnalysers/WordsFilters/IWordsFilter.cs b/TagsCloudContainer/TextAnalysers/WordsFilters/IWordsFilter.cs new file mode 100644 index 000000000..4e820a809 --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/WordsFilters/IWordsFilter.cs @@ -0,0 +1,6 @@ +namespace TagsCloudContainer.TextAnalysers.WordsFilters; + +public interface IWordsFilter +{ + public IEnumerable Filter(IEnumerable wordDetails); +} \ No newline at end of file diff --git a/TagsCloudContainer/TextAnalysers/WordsFilters/WordsFilter.cs b/TagsCloudContainer/TextAnalysers/WordsFilters/WordsFilter.cs new file mode 100644 index 000000000..b05e33340 --- /dev/null +++ b/TagsCloudContainer/TextAnalysers/WordsFilters/WordsFilter.cs @@ -0,0 +1,19 @@ +using TagsCloudContainer.Settings; + +namespace TagsCloudContainer.TextAnalysers.WordsFilters; + +public class WordsFilter: IWordsFilter +{ + private readonly IAnalyseSettings analyseSettings; + + public WordsFilter(IAnalyseSettings analyseSettings) + { + this.analyseSettings = analyseSettings; + } + + public IEnumerable Filter(IEnumerable wordDetails) + { + return wordDetails + .Where(word => analyseSettings.ValidSpeechParts.Contains(word.SpeechPart)); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/TextMeasures/ITagTextMeasurer.cs b/TagsCloudContainer/TextMeasures/ITagTextMeasurer.cs new file mode 100644 index 000000000..328790019 --- /dev/null +++ b/TagsCloudContainer/TextMeasures/ITagTextMeasurer.cs @@ -0,0 +1,9 @@ + +using System.Drawing; + +namespace TagsCloudContainer.TextMeasures; + +public interface ITagTextMeasurer +{ + public Size Measure(string text); +} \ No newline at end of file diff --git a/TagsCloudContainer/TextMeasures/TagTextMeasurer.cs b/TagsCloudContainer/TextMeasures/TagTextMeasurer.cs new file mode 100644 index 000000000..af34853e3 --- /dev/null +++ b/TagsCloudContainer/TextMeasures/TagTextMeasurer.cs @@ -0,0 +1,21 @@ +using System.Drawing; +using SixLabors.Fonts; +using TagsCloudContainer.Settings; + +namespace TagsCloudContainer.TextMeasures; + +public class TagTextMeasurer: ITagTextMeasurer +{ + private readonly IImageSettings imageSettings; + + public TagTextMeasurer(IImageSettings imageSettings) + { + this.imageSettings = imageSettings; + } + + public Size Measure(string text) + { + var fontRectangle = TextMeasurer.MeasureSize(text, imageSettings.TextOptions); + return new Size((int)fontRectangle.Width, (int)fontRectangle.Height); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Visualizers/CloudVisualizer.cs b/TagsCloudContainer/Visualizers/CloudVisualizer.cs new file mode 100644 index 000000000..b42dc048b --- /dev/null +++ b/TagsCloudContainer/Visualizers/CloudVisualizer.cs @@ -0,0 +1,40 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using TagsCloudContainer.Settings; + +namespace TagsCloudContainer.Visualizers; + +public class CloudVisualizer: ICloudVisualizer +{ + private readonly IImageSettings settings; + + public CloudVisualizer(IImageSettings settings) + { + this.settings = settings; + } + + public Image DrawImage(ITagCloud cloud) + { + var image = new Image(settings.ImageSize.Width, settings.ImageSize.Height); + DrawBackground(image); + foreach (var tag in cloud.Tags) + { + DrawTag(image, tag); + } + + return image; + } + + private void DrawTag(Image image, Tag tag) + { + var location = new PointF(tag.Rectangle.Location.X, tag.Rectangle.Location.Y); + image.Mutate(ctx => { ctx.DrawText(tag.Word, settings.TextOptions.Font, settings.PrimaryColor, location); }); + } + + private void DrawBackground(Image image) + { + image.Mutate(ctx => { ctx.Fill(settings.BackgroundColor); }); + } +} \ No newline at end of file diff --git a/TagsCloudContainer/Visualizers/ICloudVisualizer.cs b/TagsCloudContainer/Visualizers/ICloudVisualizer.cs new file mode 100644 index 000000000..e20bd017d --- /dev/null +++ b/TagsCloudContainer/Visualizers/ICloudVisualizer.cs @@ -0,0 +1,8 @@ +using SixLabors.ImageSharp; + +namespace TagsCloudContainer.Visualizers; + +public interface ICloudVisualizer +{ + public Image DrawImage(ITagCloud cloud); +} \ No newline at end of file diff --git a/TagsCloudContainer/WordDetails.cs b/TagsCloudContainer/WordDetails.cs new file mode 100644 index 000000000..9933e7d5c --- /dev/null +++ b/TagsCloudContainer/WordDetails.cs @@ -0,0 +1,28 @@ +namespace TagsCloudContainer; + +public class WordDetails +{ + public string Word { get; } + + public int Frequency { get; set; } + + public string? SpeechPart { get; } + + public WordDetails(string word, int frequency = 1, string? speechPart = null) + { + Word = word; + Frequency = frequency; + SpeechPart = speechPart; + } + + public static bool CanMap(string wordInfo) + { + var data = wordInfo.Split('='); + if (data.Length < 2) + return false; + + var word = data[0]; + + return !word.Contains("??"); + } +} \ No newline at end of file diff --git a/di.sln b/di.sln index b27b7c05d..dceb208a6 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}") = "FractalPainter", "FractalPainter\FractalPainter.csproj", "{4D70883B-6F8B-4166-802F-8EDC9BE93199}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudContainer", "TagsCloudContainer\TagsCloudContainer.csproj", "{78F8856D-FEBF-4EE4-9482-AA6718084B59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "ConsoleApp\ConsoleApp.csproj", "{18D5D5E7-F49F-430B-B4C4-703583791AED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudContainer.Tests", "TagsCloudContainer.Tests\TagsCloudContainer.Tests.csproj", "{CCE40A47-2632-4CF5-BEAE-7A3D213FEBE5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +18,17 @@ Global {4D70883B-6F8B-4166-802F-8EDC9BE93199}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D70883B-6F8B-4166-802F-8EDC9BE93199}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D70883B-6F8B-4166-802F-8EDC9BE93199}.Release|Any CPU.Build.0 = Release|Any CPU + {78F8856D-FEBF-4EE4-9482-AA6718084B59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78F8856D-FEBF-4EE4-9482-AA6718084B59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78F8856D-FEBF-4EE4-9482-AA6718084B59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78F8856D-FEBF-4EE4-9482-AA6718084B59}.Release|Any CPU.Build.0 = Release|Any CPU + {18D5D5E7-F49F-430B-B4C4-703583791AED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18D5D5E7-F49F-430B-B4C4-703583791AED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18D5D5E7-F49F-430B-B4C4-703583791AED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18D5D5E7-F49F-430B-B4C4-703583791AED}.Release|Any CPU.Build.0 = Release|Any CPU + {CCE40A47-2632-4CF5-BEAE-7A3D213FEBE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCE40A47-2632-4CF5-BEAE-7A3D213FEBE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCE40A47-2632-4CF5-BEAE-7A3D213FEBE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCE40A47-2632-4CF5-BEAE-7A3D213FEBE5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal