diff --git a/TagsCloudVisualization/BitmapExtensions.cs b/TagsCloudVisualization/BitmapExtensions.cs new file mode 100644 index 000000000..88618992e --- /dev/null +++ b/TagsCloudVisualization/BitmapExtensions.cs @@ -0,0 +1,22 @@ +using System.Drawing; +using System.Drawing.Imaging; + +namespace TagsCloudVisualization; + +public static class BitmapExtensions +{ + public static void SaveImage(this Bitmap bitmap, string outputFilePath, ImageFormat imageFormat) + { + outputFilePath = Path.GetFullPath(outputFilePath); + var outputFileName = Path.GetFileName(outputFilePath); + var outputFileDirectory = Path.GetDirectoryName(outputFilePath); + + Directory.CreateDirectory(outputFileDirectory); + + var savePath = Path.Combine(outputFileDirectory, $"{outputFileName}.{imageFormat.ToString().ToLower()}"); + + bitmap.Save(savePath, imageFormat); + + Console.WriteLine($"Image is saved to {savePath}"); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/CustomAttributes/MustBeEnumNameAttribute.cs b/TagsCloudVisualization/CustomAttributes/MustBeEnumNameAttribute.cs new file mode 100644 index 000000000..706c53558 --- /dev/null +++ b/TagsCloudVisualization/CustomAttributes/MustBeEnumNameAttribute.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace TagsCloudVisualization.CustomAttributes; + +public class MustBeEnumNameAttribute : ValidationAttribute +{ + private Type enumType; + + public MustBeEnumNameAttribute(Type enumType) + : base() + { + this.enumType = enumType; + } + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + var enumNames = Enum.GetNames(enumType); + if (value is string valueString && Enum.TryParse(enumType, valueString, true, out var result) + && Enum.IsDefined(enumType, result)) + { + return ValidationResult.Success; + } + + return new ValidationResult($"The value for {validationContext.DisplayName} must be in {enumType}. " + + $"Available: {String.Join(", ", enumNames)}"); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/CustomAttributes/MustBePositiveAttribute.cs b/TagsCloudVisualization/CustomAttributes/MustBePositiveAttribute.cs new file mode 100644 index 000000000..a5eca284d --- /dev/null +++ b/TagsCloudVisualization/CustomAttributes/MustBePositiveAttribute.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace TagsCloudVisualization.CustomAttributes; + +public class MustBePositiveAttribute : ValidationAttribute +{ + public MustBePositiveAttribute() + : base("The value for {0} must be bigger than 0") + { + } + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (int.TryParse(value as string, out var number) && number > 0) + { + return ValidationResult.Success; + } + + return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/DullCheckers/IDullWordChecker.cs b/TagsCloudVisualization/DullCheckers/IDullWordChecker.cs new file mode 100644 index 000000000..8601ee0dc --- /dev/null +++ b/TagsCloudVisualization/DullCheckers/IDullWordChecker.cs @@ -0,0 +1,6 @@ +namespace TagsCloudVisualization; + +public interface IDullWordChecker +{ + public bool Check(WordAnalysis word); +} \ No newline at end of file diff --git a/TagsCloudVisualization/DullCheckers/MystemDullWordChecker.cs b/TagsCloudVisualization/DullCheckers/MystemDullWordChecker.cs new file mode 100644 index 000000000..dbcfbb1b6 --- /dev/null +++ b/TagsCloudVisualization/DullCheckers/MystemDullWordChecker.cs @@ -0,0 +1,34 @@ +using System.Text; + +namespace TagsCloudVisualization; + +public class MystemDullWordChecker : IDullWordChecker +{ + private HashSet removedPartOfSpeech; + private HashSet excludedWords = new(); + + public MystemDullWordChecker(TagLayoutSettings tagLayoutSettings) + { + removedPartOfSpeech = tagLayoutSettings.RemovedPartOfSpeech; + var excludedWordsFile = tagLayoutSettings.ExcludedWordsFile; + if (excludedWordsFile is null) + return; + + try + { + excludedWords = + new HashSet(File.ReadAllText(excludedWordsFile, Encoding.UTF8).Split(Environment.NewLine)); + } + catch (FileNotFoundException e) + { + Console.WriteLine($"Could not find specified excluded words file {excludedWordsFile}. " + + $"No words will be excluded."); + } + } + + public bool Check(WordAnalysis wordAnalysis) + { + return removedPartOfSpeech.Any(dullPart => wordAnalysis.GrammarAnalysis.StartsWith(dullPart)) + || excludedWords.Contains(wordAnalysis.Lexema.ToLower()); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/ExampleImages/ExampleImageFromReadme.png b/TagsCloudVisualization/ExampleImages/ExampleImageFromReadme.png new file mode 100644 index 000000000..5597fbb2d Binary files /dev/null and b/TagsCloudVisualization/ExampleImages/ExampleImageFromReadme.png differ diff --git a/TagsCloudVisualization/ExampleImages/ExampleImageFromReadme2.png b/TagsCloudVisualization/ExampleImages/ExampleImageFromReadme2.png new file mode 100644 index 000000000..0be0360ac Binary files /dev/null and b/TagsCloudVisualization/ExampleImages/ExampleImageFromReadme2.png differ diff --git a/TagsCloudVisualization/IRectangleLayouter.cs b/TagsCloudVisualization/IRectangleLayouter.cs new file mode 100644 index 000000000..941bf103d --- /dev/null +++ b/TagsCloudVisualization/IRectangleLayouter.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public interface IRectangleLayouter +{ + public Rectangle PutNextRectangle(Size rectangleSize); + public Rectangle PutNextRectangle(SizeF rectangleSize); +} \ No newline at end of file diff --git a/TagsCloudVisualization/LayoutDrawer.cs b/TagsCloudVisualization/LayoutDrawer.cs new file mode 100644 index 000000000..3d555c304 --- /dev/null +++ b/TagsCloudVisualization/LayoutDrawer.cs @@ -0,0 +1,86 @@ +using System.Drawing.Imaging; +using System.Drawing; + +namespace TagsCloudVisualization; + +public class LayoutDrawer +{ + private IInterestingWordsParser interestingWordsParser; + private IRectangleLayouter rectangleLayouter; + private IPalette palette; + private Font font; + + public LayoutDrawer(IInterestingWordsParser interestingWordsParser, + IRectangleLayouter rectangleLayouter, + IPalette palette, + Font font) + { + this.interestingWordsParser = interestingWordsParser; + this.rectangleLayouter = rectangleLayouter; + this.palette = palette; + this.font = font; + if (string.Compare(font.OriginalFontName, font.Name, StringComparison.InvariantCultureIgnoreCase) != 0) + Console.WriteLine($"Font \"{font.OriginalFontName}\" was not found. Using \"{font.Name}\" instead"); + } + + public Result CreateLayoutImageFromFile(string inputFilePath, + Size imageSize, + int minimumFontSize) + { + return interestingWordsParser + .GetInterestingWords(inputFilePath) + .Then(GetSortedInterestingWordsCount) + .Then(rectangles => DrawRectangles(imageSize, minimumFontSize, rectangles)) + .RefineError("Can't create layout image"); + } + + private Bitmap DrawRectangles(Size imageSize, + int minimumFontSize, + IOrderedEnumerable<(string Word, int Count)> sortedWordsCount) + { + Bitmap bitmap; + try + { + bitmap = new Bitmap(imageSize.Width, imageSize.Height); + } + catch (ArgumentException e) + { + throw new ArgumentException("Can't create image with negative width or height"); + } + + using var graphics = Graphics.FromImage(bitmap); + var mostWordOccurrencies = sortedWordsCount.Max(arg => arg.Count); + + graphics.Clear(palette.GetBackgroundColor()); + + foreach (var wordCount in sortedWordsCount) + { + var rectangleFont = new Font(font.FontFamily, + Math.Max(font.Size * wordCount.Count / mostWordOccurrencies, minimumFontSize)); + var rectangleSize = graphics.MeasureString(wordCount.Word, rectangleFont); + + var textRectangle = rectangleLayouter.PutNextRectangle(rectangleSize); + var x = textRectangle.X + imageSize.Width / 2; + var y = textRectangle.Y + imageSize.Height / 2; + + if (x < 0 || x > imageSize.Width || y < 0 || y > imageSize.Height) + throw new ApplicationException("Words went out of image boundaries"); + + using var brush = new SolidBrush(palette.GetNextWordColor()); + graphics.DrawString(wordCount.Word, rectangleFont, brush, x, y); + } + + return bitmap; + } + + private IOrderedEnumerable<(string Word, int Count)> GetSortedInterestingWordsCount( + IEnumerable interestingWords) + { + var sortedWordsCount = interestingWords + .GroupBy(s => s) + .Select(group => (Word: group.Key, Count: group.Count())) + .OrderByDescending(wordCount => wordCount.Count); + + return sortedWordsCount; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Palletes/IPalette.cs b/TagsCloudVisualization/Palletes/IPalette.cs new file mode 100644 index 000000000..59472b835 --- /dev/null +++ b/TagsCloudVisualization/Palletes/IPalette.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public interface IPalette +{ + public Color GetNextWordColor(); + + public Color GetBackgroundColor(); +} \ No newline at end of file diff --git a/TagsCloudVisualization/Palletes/Palette.cs b/TagsCloudVisualization/Palletes/Palette.cs new file mode 100644 index 000000000..a53df3e86 --- /dev/null +++ b/TagsCloudVisualization/Palletes/Palette.cs @@ -0,0 +1,27 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public class Palette : IPalette +{ + public Palette(Color[] textColor, Color backgroundColor) + { + TextColor = textColor; + BackgroundColor = backgroundColor; + } + + private Color[] TextColor { get; set; } + private int currentColorId = 0; + private Color BackgroundColor { get; set; } + + public Color GetNextWordColor() + { + if (currentColorId >= TextColor.Length) currentColorId = 0; + return TextColor[currentColorId++]; + } + + public Color GetBackgroundColor() + { + return BackgroundColor; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/PointGenerators/Algorithm.cs b/TagsCloudVisualization/PointGenerators/Algorithm.cs new file mode 100644 index 000000000..3cb0f5db9 --- /dev/null +++ b/TagsCloudVisualization/PointGenerators/Algorithm.cs @@ -0,0 +1,7 @@ +namespace TagsCloudVisualization; + +public enum Algorithm +{ + Spiral, + Square +} \ No newline at end of file diff --git a/TagsCloudVisualization/PointGenerators/IPointGenerator.cs b/TagsCloudVisualization/PointGenerators/IPointGenerator.cs new file mode 100644 index 000000000..1a845776c --- /dev/null +++ b/TagsCloudVisualization/PointGenerators/IPointGenerator.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public interface IPointGenerator +{ + public Algorithm Name { get; } + Point GetNextPoint(); +} \ No newline at end of file diff --git a/TagsCloudVisualization/PointGenerators/LissajousCurvePointGenerator.cs b/TagsCloudVisualization/PointGenerators/LissajousCurvePointGenerator.cs new file mode 100644 index 000000000..988f7f60a --- /dev/null +++ b/TagsCloudVisualization/PointGenerators/LissajousCurvePointGenerator.cs @@ -0,0 +1,31 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public class LissajousCurvePointGenerator : IPointGenerator +{ + private int xAmplitude = 100; + private int yAmplitude = 100; + private int xConstant = 19; + private int yConstant = 20; + private double delta = Math.PI / 2; + private double parameter = 0; + + public Algorithm Name { get; } = Algorithm.Square; + + public Point GetNextPoint() + { + parameter += 0.01; + var x = Math.Round(xAmplitude * Math.Sin(xConstant * parameter + delta)); + var y = Math.Round(yAmplitude * Math.Sin(yConstant * parameter)); + + if (parameter > 20) + { + parameter = 0; + xAmplitude += 20; + yAmplitude += 20; + } + + return new Point((int)x, (int)y); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/PointGenerators/SpiralPointGenerator.cs b/TagsCloudVisualization/PointGenerators/SpiralPointGenerator.cs new file mode 100644 index 000000000..47df14bb0 --- /dev/null +++ b/TagsCloudVisualization/PointGenerators/SpiralPointGenerator.cs @@ -0,0 +1,28 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public class SpiralPointGenerator : IPointGenerator +{ + public Point Center { get; } = new(0, 0); + public int Radius { get; private set; } + public double Angle { get; private set; } + public int RadiusDelta { get; private set; } = 1; + public double AngleDelta { get; private set; } = Math.PI / 60; + + public Algorithm Name { get; } = Algorithm.Spiral; + + public Point GetNextPoint() + { + var x = (int)Math.Round(Center.X + Radius * Math.Cos(Angle)); + var y = (int)Math.Round(Center.Y + Radius * Math.Sin(Angle)); + + var nextAngle = Angle + AngleDelta; + var angleMoreThan2Pi = Math.Abs(nextAngle) >= Math.PI * 2; + + Radius = angleMoreThan2Pi ? Radius + RadiusDelta : Radius; + Angle = angleMoreThan2Pi ? 0 : nextAngle; + + return new Point(x, y); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Program.cs b/TagsCloudVisualization/Program.cs new file mode 100644 index 000000000..d229c0ff1 --- /dev/null +++ b/TagsCloudVisualization/Program.cs @@ -0,0 +1,91 @@ +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using System.Drawing.Imaging; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.DependencyInjection; +using TagsCloudVisualization.CustomAttributes; + +namespace TagsCloudVisualization; + +public class Program +{ + public static int Main(string[] args) => CommandLineApplication.Execute(args); + + [Argument(0, Description = "Path to txt file with words")] + [Required(ErrorMessage = "Expected to get path to file with words as first positional argument." + + "\nExample: C:\\PathTo\\File.txt\nOr relative to exe: PathTo\\File.txt")] + public string InputFilePath { get; set; } + + [Argument(1, Description = "Path to output file")] + [Required(ErrorMessage = "Expected to get output file path as second positional argument." + + "\nExample: C:\\PathTo\\File\nOr relative to exe: PathTo\\File")] + public string OutputFilePath { get; set; } + + [Option("-w", Description = "Image width in pixels")] + [MustBePositive] + private int ImageWidth { get; set; } = 1000; + + [Option("-h", Description = "Image height in pixels")] + [MustBePositive] + private int ImageHeight { get; set; } = 1000; + + [Option("-bg", Description = "Image background color from KnownColor enum")] + [MustBeEnumName(typeof(KnownColor))] + private KnownColor BackgroundColor { get; set; } = KnownColor.Wheat; + + [Option("-tc", Description = "Image words colors sequence array from KnownColor enum. " + + "Can be set multiple times for sequence. Example: -tc black -tc white")] + [MustBeEnumName(typeof(KnownColor))] + private KnownColor[] TextColor { get; set; } = { KnownColor.Black }; + + [Option("-ff", Description = "Font used for words")] + private string FontFamily { get; set; } = "Arial"; + + [Option("-fs", Description = "Max font size in em")] + [MustBePositive] + private int FontSize { get; set; } = 50; + + [Option("-mfs", Description = "Min font size in em")] + [MustBePositive] + private int MinimalFontSize { get; set; } = 1; + + [Option("-img", Description = "Output image format. Choosen from ImageFormat")] + private ImageFormat SaveImageFormat { get; set; } = ImageFormat.Png; + + [Option("-ef", Description = "Txt file with words to exclude. 1 word in line. Words must be lexems.")] + private string ExcludedWordsFile { get; set; } + + [Option("-rp", Description = "Parts of speech abbreviations that are excluded from parsed words. " + + "More info here https://yandex.ru/dev/mystem/doc/ru/grammemes-values")] + private HashSet RemovedPartsOfSpeech { get; set; } = new() + { "ADVPRO", "APRO", "INTJ", "CONJ", "PART", "PR", "SPRO" }; + + [Option("-alg", Description = "Choose algorithm to generate tag cloud. Available: Spiral, Square")] + [MustBeEnumName(typeof(Algorithm))] + private Algorithm Algorithm { get; set; } = Algorithm.Spiral; + + + private void OnExecute() + { + var services = new ServiceCollection(); + services.AddSingleton(new TagLayoutSettings(Algorithm, RemovedPartsOfSpeech, ExcludedWordsFile)); + services.AddScoped(x => new Font(FontFamily, FontSize)); + services.AddScoped(x => new Palette(TextColor.Select(Color.FromKnownColor).ToArray(), + Color.FromKnownColor(BackgroundColor))); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + using var provider = services.BuildServiceProvider(); + + var layoutDrawer = provider.GetRequiredService(); + + layoutDrawer + .CreateLayoutImageFromFile(InputFilePath, new Size(ImageWidth, ImageHeight), MinimalFontSize) + .Then(bitmap => bitmap.SaveImage(OutputFilePath, SaveImageFormat)) + .OnFail(Console.WriteLine); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Readme.md b/TagsCloudVisualization/Readme.md new file mode 100644 index 000000000..b0760c9a4 --- /dev/null +++ b/TagsCloudVisualization/Readme.md @@ -0,0 +1,86 @@ +# TagsCloudVisualization + +✔️ Вместо исключений всюду в программе используй паттерн `Result` + +----------------- + +✔️ - Сделал в виде ошибки, приложение падает и покрыто тестами + +❓ - Сделал в виде сообщения в консоли, приложение не падает, а использует дефолтные настройки и не придумал как покрыть тестами + +❗ - Выдает ошибку еще на этапе запуска, дальше не идет + +----------------- + +- ✔️ Файл не найден / не доступен + +- ❓️ Файл настроек не получилось прочитать + +- ❗ Настройки некорреткны + +- Одна из внешних библиотек дала сбой (не смог найти mystem.exe). В этом случае падает во время +работы, но не придумал как это покрыть тестом. + +- ❓️ Шрифт с таким именем не найден в системе + +- ✔️ Облако тегов не влезло на изображение заданного размера + +--------- + +## Как использовать + +Можно вызвать через терминал находясь в папке с .exe-шником и прописав его название `TagsCloudVisualization` +и вписав путь к .txt файлу со словами которые нужно разложить в облако. Путь может быть как абсолютный, так и +относительный. Но все относительные пути ищутся относительно .exe-шника + +Пример +```TagsCloudVisualization "C:\Path\To\My\File" "C:\Path\To\Output\File"``` + +Так же есть различные опции с помощью которых можно повлиять на результирующее облако. Их полный список можно +увидеть введя ```TagsCloudVisualization --help```. Постарался дать им понятные описания. + +А вот пример ввода каждого из параметров +``` +TagsCloudVisualization "C:\Path\To\My\File" "C:\Path\To\Output\File" + +-w 101 Ширина изображения 101 + +-h 102 Высота изображения 102 + +-bc black Фон изображения Черный + +-tc white -tc grey Цвет слов будет чередоваться белый-черный до бесконечности + +-ff "Century Gothic" Шрифт будет "Century Gothic" + +-fs 100 Максимальный размер шрифта 100 (у самого часто встречающегося слова) + +-mfs 10 Минимальный размер шрифта 10 + +-img jpeg Изображение на выходе будет в формате .jpeg + +-ef "ExcludedWords.txt" Из этого файла будут подтягиваться слова которые надо иключить +> Excluded Words должен содержать слова по 1 в строке в начальной форме +яблоко +груша +петь + +-rp null Ни одна часть речи не будет исключена + +-rp A -rp S Будут исключены прилагательные и существительные + +-square Будет использоваться другой алгоритм генерации + в результате облако будет в форме квадрата +``` + +Вот так выглядела команда для получения картинки ниже + +```TagsCloudVisualization TextSamples\RuText.txt ExampleImages\ExampleImageFromReadme -bc black -tc aquamarine -tc teal -fs 80 -mfs 10 -rp A -rp S -square``` + +![ExampleImageFromReadme.png](ExampleImages%2FExampleImageFromReadme.png) + +А вот так с только необходимыми параметрами + +```TagsCloudVisualization TextSamples\RuText.txt ExampleImages\ExampleImageFromReadme2``` + +![ExampleImageFromReadme2.png](ExampleImages%2FExampleImageFromReadme2.png) \ No newline at end of file diff --git a/TagsCloudVisualization/RectangleLayouter.cs b/TagsCloudVisualization/RectangleLayouter.cs new file mode 100644 index 000000000..fac01096a --- /dev/null +++ b/TagsCloudVisualization/RectangleLayouter.cs @@ -0,0 +1,44 @@ +using System.Drawing; + +namespace TagsCloudVisualization; + +public class RectangleLayouter : IRectangleLayouter +{ + private readonly IPointGenerator pointGenerator; + private readonly List createdRectangles = new(); + + public RectangleLayouter(TagLayoutSettings tagLayoutSettings, IEnumerable pointGenerators) + { + pointGenerator = pointGenerators.First(generator => generator.Name == tagLayoutSettings.Algorithm); + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Height < 0 || rectangleSize.Width < 0) + throw new ArgumentException("Rectangle cant have less than 0 width or height"); + + while (true) + { + var nextPoint = pointGenerator.GetNextPoint(); + + var rectangleLocation = new Point(nextPoint.X - rectangleSize.Width / 2, + nextPoint.Y - rectangleSize.Height / 2); + + var newRectangle = new Rectangle(rectangleLocation, rectangleSize); + if (createdRectangles.Any(rectangle => rectangle.IntersectsWith(newRectangle))) + continue; + + createdRectangles.Add(newRectangle); + return newRectangle; + } + } + + public Rectangle PutNextRectangle(SizeF rectangleSize) + { + var intWidth = (int)Math.Round(rectangleSize.Width); + var intHeight = (int)Math.Round(rectangleSize.Height); + var intSize = new Size(intWidth, intHeight); + + return PutNextRectangle(intSize); + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/Result.cs b/TagsCloudVisualization/Result.cs new file mode 100644 index 000000000..22c48cf24 --- /dev/null +++ b/TagsCloudVisualization/Result.cs @@ -0,0 +1,139 @@ +namespace TagsCloudVisualization +{ + public class None + { + private None() + { + } + } + + public struct Result + { + public Result(string error, T value = default(T)) + { + Error = error; + Value = value; + } + public static implicit operator Result(T v) + { + return Result.Ok(v); + } + + public string Error { get; } + internal T Value { get; } + public T GetValueOrThrow() + { + if (IsSuccess) return Value; + throw new InvalidOperationException($"No value. Only Error {Error}"); + } + public bool IsSuccess => Error == null; + } + + public static class Result + { + public static Result AsResult(this T value) + { + return Ok(value); + } + + public static Result Ok(T value) + { + return new Result(null, value); + } + public static Result Ok() + { + return Ok(null); + } + + public static Result Fail(string e) + { + return new Result(e); + } + + public static Result Of(Func f, string error = null) + { + try + { + return Ok(f()); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result OfAction(Action f, string error = null) + { + try + { + f(); + return Ok(); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result Then( + this Result input, + Func continuation) + { + return input.Then(inp => Of(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Func> continuation) + { + return input.IsSuccess + ? continuation(input.Value) + : Fail(input.Error); + } + + public static Result OnFail( + this Result input, + Action handleError) + { + if (!input.IsSuccess) handleError(input.Error); + return input; + } + + public static Result ReplaceError( + this Result input, + Func replaceError) + { + if (input.IsSuccess) return input; + return Fail(replaceError(input.Error)); + } + + public static Result RefineError( + this Result input, + string errorMessage) + { + return input.ReplaceError(err => errorMessage + ". " + err); + } + + public static Result RefineErrorOnCondition( + this Result input, + bool condition, + string errorMessage) + { + return condition ? input.ReplaceError(err => errorMessage + ". " + err) : input; + } + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagLayoutSettings.cs b/TagsCloudVisualization/TagLayoutSettings.cs new file mode 100644 index 000000000..f49edcc3f --- /dev/null +++ b/TagsCloudVisualization/TagLayoutSettings.cs @@ -0,0 +1,22 @@ +using System.Drawing; +using System.Drawing.Imaging; +using Microsoft.Extensions.DependencyInjection; + +namespace TagsCloudVisualization; + +public class TagLayoutSettings +{ + public TagLayoutSettings(Algorithm algorithm, + HashSet removedPartOfSpeech, + string? excludedWordsFile) + { + Algorithm = algorithm; + RemovedPartOfSpeech = removedPartOfSpeech; + ExcludedWordsFile = excludedWordsFile; + } + + public Algorithm Algorithm { get; } + public HashSet RemovedPartOfSpeech { get; } + public string? ExcludedWordsFile { get; } + +} \ No newline at end of file diff --git a/TagsCloudVisualization/TagsCloudVisualization.csproj b/TagsCloudVisualization/TagsCloudVisualization.csproj new file mode 100644 index 000000000..5b51aef21 --- /dev/null +++ b/TagsCloudVisualization/TagsCloudVisualization.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + enable + + false + false + Exe + latest + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/TagsCloudVisualization/TextSamples/RuText.txt b/TagsCloudVisualization/TextSamples/RuText.txt new file mode 100644 index 000000000..2770040a2 --- /dev/null +++ b/TagsCloudVisualization/TextSamples/RuText.txt @@ -0,0 +1,11 @@ +У Шрека и Фионы медовый месяц. Вернувшись домой, они получают послание от родителей Фионы — короля и королевы Тридевятого королевства. Фиона уговаривает мужа поехать, утверждая, что отец с матерью будут рады их видеть. Шрек не хочет ехать — он привык к другой реакции на себя, к тому же он не уверен, что королевская семья положительно отнесётся к их визиту. В конце концов, пара оставляет дом на попечение сказочных существ и вместе с Ослом (который напросился с ними в попутчики, так как у него временный разлад отношений с Драконихой) уезжает на своей карете-луковице в гости к папе и маме Фионы. + +Но когда король Гарольд видит дочь и зятя, то особой радости не испытывает. Королева Лиллиан, напротив, пытается быть дипломатичной и принять дочь такой, какая она есть, а заодно и её мужа. Но Гарольд, не желая прислушиваться к мнению супруги, винит во всём Шрека, который не дал «его девочке» остаться прекрасной принцессой. Короля также пугают перспективы внуков-огров. В итоге во время ужина между ним и Шреком происходит ссора; расстроенная Фиона в слезах убегает в спальню, а чуть позже сама ссорится со Шреком, когда тот изъявляет желание вернуться домой, ссылаясь на то, что реакция короля на их визит была ожидаемой. + +Далее выясняется, что по первоначальному плану короля Фиону должен был освободить и взять в жёны прекрасный принц Чарминг (от англ. charming — очаровательный), но Шрек его опередил. За спиной принца стоит его мать — Фея-крёстная Фионы, богатая и могущественная владелица крупнейшего зельеварочного завода (она же была инициатором заточения Фионы в башню). Когда-то Крёстная помогла королю Гарольду самому жениться на принцессе Лиллиан, и теперь он — должник Крёстной. По её плану, дочь Гарольда должна была выйти замуж за Чарминга, чтобы тем самым открыть честолюбивой Крёстной путь к завладению королевским наследством. + +Теперь Крёстная шантажом вынуждает короля исполнить уговор и избавиться от Шрека. Ночью Король тайно прибывает в таверну «Ядовитое яблочко» для того, чтобы найти и нанять киллера, который бы смог разобраться с огром. По наводке барменши Дорис — одной из уродливых сестёр Золушки — Гарольд нанимает «головореза». Им оказывается Кот в сапогах. Он берёт заказ на убийство, однако, встретив огра в лесу, проваливает задание, поперхнувшись комком шерсти. Шрек, которого тесть хитростью выманил в лес якобы на утреннюю примирительную охоту, обезоруживает Кота, но оставляет его в живых. В знак благодарности несостоявшийся киллер просит Шрека и Осла позволить сопровождать их, причём сообщает огру, кто его нанял. Шрек ошеломлён таким поворотом событий. + +Шрек с друзьями направляются на волшебную фабрику Крёстной, где хотят получить совет Феи, не догадываясь о её роли в этой истории. Естественно, Крёстная советует огру оставить принцессу в покое для её же блага. Тем не менее, компания похищает у неё со склада один из волшебных эликсиров (попутно разгромив фабрику). По пути Осёл пробует на вкус зелье, а затем его выпивает и Шрек. Начинается дождь, и герои решают заночевать в амбаре, где с ними за ночь происходит волшебная трансформация. Наутро Шрек просыпается в человеческом обличии, Осёл тоже изменился и стал прекрасным белым жеребцом. Чтобы заклятие не потеряло силу и принесло счастье, до полуночи надо встретить суженую и поцеловать её. Шрек добывает себе приличную одежду и едет во дворец к жене, надеясь решить все их проблемы — ведь теперь он стал полноценным человеком и это должно устроить короля и королеву. + +В замке с Фионой тоже происходит метаморфоза, и утром она тоже просыпается в человеческом облике. От появившегося Осла-Жеребца она узнаёт о преображении, но сама при этом ещё не видела Шрека в новом облике. Этим решают воспользоваться Крёстная и её сын: Чарминг проникает в замок и прикидывается преобразившимся Шреком, а Фея встаёт на пути настоящего Шрека и показывает ему издали Фиону и Чарминга: именно он, по мнению Крёстной, достойная для неё пара, а Шрек должен отказаться от Фионы во имя любви и ради её же блага. Подавленный Шрек решает поступить благородно и в компании Осла-Жеребца и Кота покидает замок и идёт в «Ядовитое яблочко», где пытается залить своё горе. \ No newline at end of file diff --git a/TagsCloudVisualization/TextSamples/RuWords.txt b/TagsCloudVisualization/TextSamples/RuWords.txt new file mode 100644 index 000000000..91c43a891 --- /dev/null +++ b/TagsCloudVisualization/TextSamples/RuWords.txt @@ -0,0 +1,20 @@ +яблоки +груши +манго +персиковый +яблочный +красить +петь +свистеть +машина +машинный +леопард +леопардовый +и +я +он +она +и +и +и +и \ No newline at end of file diff --git a/TagsCloudVisualization/WordAnalysis.cs b/TagsCloudVisualization/WordAnalysis.cs new file mode 100644 index 000000000..32cd96151 --- /dev/null +++ b/TagsCloudVisualization/WordAnalysis.cs @@ -0,0 +1,15 @@ +namespace TagsCloudVisualization; + +public class WordAnalysis +{ + public string Word { get; set; } + public string? Lexema { get; set; } + public string? GrammarAnalysis { get; set; } + + public WordAnalysis(string word, string? lexema = null, string? grammarAnalysis = null) + { + Word = word; + Lexema = lexema; + GrammarAnalysis = grammarAnalysis; + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/WordParsers/IInterestingWordsParser.cs b/TagsCloudVisualization/WordParsers/IInterestingWordsParser.cs new file mode 100644 index 000000000..1a1680499 --- /dev/null +++ b/TagsCloudVisualization/WordParsers/IInterestingWordsParser.cs @@ -0,0 +1,6 @@ +namespace TagsCloudVisualization; + +public interface IInterestingWordsParser +{ + Result> GetInterestingWords(string path); +} \ No newline at end of file diff --git a/TagsCloudVisualization/WordParsers/MystemWordsParser.cs b/TagsCloudVisualization/WordParsers/MystemWordsParser.cs new file mode 100644 index 000000000..c11a28185 --- /dev/null +++ b/TagsCloudVisualization/WordParsers/MystemWordsParser.cs @@ -0,0 +1,96 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Text; +using System.Text.Json; + +namespace TagsCloudVisualization; + +public class MystemWordsParser : IInterestingWordsParser +{ + private IDullWordChecker dullWordChecker; + + public MystemWordsParser(IDullWordChecker dullWordChecker) + { + this.dullWordChecker = dullWordChecker; + } + + public Result> GetInterestingWords(string inputFilePath) + { + var path = Path.GetFullPath(inputFilePath); + + return Result.Of(() => File.ReadAllText(path, Encoding.UTF8)) + .RefineErrorOnCondition(!Path.IsPathRooted(inputFilePath), + "Relative paths are searched realative to .exe file. Try giving an absolute path") + .Then(ParseInterestingWords) + .Then(analyses => analyses + .Where(analysis => !dullWordChecker.Check(analysis)) + .Select(analysis => analysis.Lexema.ToLower())) + .RefineError("Can't get interesting words"); + } + + private static Result> ParseInterestingWords(string text) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "mystem.exe"), + Arguments = "-lig --format json", + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + StandardInputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + StandardOutputEncoding = Encoding.UTF8, + } + }; + try + { + process.Start(); + process.StandardInput.Write(text); + process.StandardInput.Close(); + } + catch (Win32Exception e) + { + return Result.Fail>( + $"Can't find mystem.exe in working directory: {AppDomain.CurrentDomain.BaseDirectory}. " + + "You can download it here https://yandex.ru/dev/mystem/"); + } + + var wordsAnalysis = Result.Of(() => DeserializeInterestingWordsAnalysis(process)) + .RefineError("Error occured during deserialization"); + + process.WaitForExit(1); + return wordsAnalysis; + } + + private static List DeserializeInterestingWordsAnalysis(Process process) + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var wordsAnalysis = new List(); + + while (!process.StandardOutput.EndOfStream) + { + var line = process.StandardOutput.ReadLine(); + var deserializedLine = JsonSerializer.Deserialize>(line, options); + foreach (var jsonWordAnalysis in deserializedLine) + { + if (jsonWordAnalysis.Analysis.Count < 1) + continue; + + var unpackedAnalysis = jsonWordAnalysis.Analysis.First(); + wordsAnalysis.Add(new WordAnalysis(jsonWordAnalysis.Text, unpackedAnalysis["lex"], + unpackedAnalysis["gr"])); + } + } + + return wordsAnalysis; + } + + private class JsonWordAnalysis + { + public string Text { get; set; } + public List> Analysis { get; set; } + } +} \ No newline at end of file diff --git a/TagsCloudVisualization/mystem.exe b/TagsCloudVisualization/mystem.exe new file mode 100644 index 000000000..e7158ff1b Binary files /dev/null and b/TagsCloudVisualization/mystem.exe differ diff --git a/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenFileDoesntExist.ForScenario.Absolute path.approved.txt b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenFileDoesntExist.ForScenario.Absolute path.approved.txt new file mode 100644 index 000000000..fab0dbf91 --- /dev/null +++ b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenFileDoesntExist.ForScenario.Absolute path.approved.txt @@ -0,0 +1 @@ +Can't create layout image. Can't get interesting words. Could not find a part of the path 'C:\Absolute\Non\Existing\Path'. \ No newline at end of file diff --git a/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenFileDoesntExist.ForScenario.Relative path.approved.txt b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenFileDoesntExist.ForScenario.Relative path.approved.txt new file mode 100644 index 000000000..27ccdc7a4 --- /dev/null +++ b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenFileDoesntExist.ForScenario.Relative path.approved.txt @@ -0,0 +1 @@ +Can't create layout image. Can't get interesting words. Relative paths are searched realative to .exe file. Try giving an absolute path. Could not find file 'G:\Шпора дз\fp\TagsCloudVisualizationTests\bin\Debug\net6.0\NonExistingPath'. \ No newline at end of file diff --git a/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenImageSizeIsNegative.approved.txt b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenImageSizeIsNegative.approved.txt new file mode 100644 index 000000000..589e981b0 --- /dev/null +++ b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenImageSizeIsNegative.approved.txt @@ -0,0 +1 @@ +Can't create layout image. Can't create image with negative width or height \ No newline at end of file diff --git a/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenTagCloudIsTooBig.approved.txt b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenTagCloudIsTooBig.approved.txt new file mode 100644 index 000000000..75e38a418 --- /dev/null +++ b/TagsCloudVisualizationTests/LayoutDrawer_Should.Fail_WhenTagCloudIsTooBig.approved.txt @@ -0,0 +1 @@ +Can't create layout image. Words went out of image boundaries \ No newline at end of file diff --git a/TagsCloudVisualizationTests/LayoutDrawer_Should.cs b/TagsCloudVisualizationTests/LayoutDrawer_Should.cs new file mode 100644 index 000000000..ace0e6b79 --- /dev/null +++ b/TagsCloudVisualizationTests/LayoutDrawer_Should.cs @@ -0,0 +1,65 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using ApprovalTests; +using ApprovalTests.Namers; +using ApprovalTests.Reporters; +using FluentAssertions; +using TagsCloudVisualization; + +namespace TagsCloudVisualizationTests; + +[TestFixture] +[UseReporter(typeof(DiffReporter))] +public class LayoutDrawer_Should +{ + private IPalette palette; + private IPointGenerator pointGenerator; + private IDullWordChecker dullWordChecker; + private IInterestingWordsParser interestingWordsParser; + private IRectangleLayouter rectangleLayouter; + private TagLayoutSettings tagLayoutSettings; + + [SetUp] + public void SetUp() + { + palette = new Palette(new[] { Color.Aqua }, Color.Black); + pointGenerator = new SpiralPointGenerator(); + tagLayoutSettings = new TagLayoutSettings(Algorithm.Spiral, new HashSet { "S", "CONJ" }, + @"TextFiles\ExcludedWords.txt"); + dullWordChecker = new MystemDullWordChecker(tagLayoutSettings); + interestingWordsParser = new MystemWordsParser(dullWordChecker); + rectangleLayouter = new RectangleLayouter(tagLayoutSettings, new[] { pointGenerator }); + } + + [TestCase(@"C:\Absolute\Non\Existing\Path", TestName = "Absolute path")] + [TestCase("NonExistingPath", TestName = "Relative path")] + [Test] + public void Fail_WhenFileDoesntExist(string inputFilePath) + { + var layoutDrawer = new LayoutDrawer(interestingWordsParser, rectangleLayouter, palette, new Font("Arial", 24)); + var result = layoutDrawer.CreateLayoutImageFromFile(inputFilePath, new Size(1000, 1000), 1); + using (ApprovalResults.ForScenario(TestContext.CurrentContext.Test.Name)) + { + result.IsSuccess.Should().BeFalse(); + Approvals.Verify(result.Error); + } + } + + [Test] + public void Fail_WhenTagCloudIsTooBig() + { + var layoutDrawer = new LayoutDrawer(interestingWordsParser, rectangleLayouter, palette, new Font("Arial", 100)); + var result = layoutDrawer.CreateLayoutImageFromFile("TextFiles/RuWords.txt", new Size(100, 100), 1); + result.IsSuccess.Should().BeFalse(); + Approvals.Verify(result.Error); + } + + [Test] + public void Fail_WhenImageSizeIsNegative() + { + var layoutDrawer = new LayoutDrawer(interestingWordsParser, rectangleLayouter, palette, new Font("Arial", 100)); + var result = layoutDrawer.CreateLayoutImageFromFile("TextFiles/RuWords.txt", new Size(-1, -1), 1); + result.IsSuccess.Should().BeFalse(); + Approvals.Verify(result.Error); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/RectangleLayouter_Should.cs b/TagsCloudVisualizationTests/RectangleLayouter_Should.cs new file mode 100644 index 000000000..aca891df4 --- /dev/null +++ b/TagsCloudVisualizationTests/RectangleLayouter_Should.cs @@ -0,0 +1,68 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloudVisualization; + +namespace TagsCloudVisualizationTests; + +public class RectangleLayouter_Should +{ + private RectangleLayouter? rectangleLayouter; + private TagLayoutSettings tagLayoutSettings; + private IEnumerable pointGenerators; + + [SetUp] + public void SetCircularCloudFieldToNull() + { + rectangleLayouter = null; + tagLayoutSettings = new TagLayoutSettings(Algorithm.Spiral, new HashSet(), null); + pointGenerators = new[] { new SpiralPointGenerator() }; + } + + [TestCase(-1, 0, TestName = "Negative width")] + [TestCase(0, -1, TestName = "Negative height")] + [TestCase(-5, -5, TestName = "Negative width and height")] + public void PutNextRectangleThrowsArgumentException_WhenNegativeParameters(int rectWidth, int rectHeight) + { + rectangleLayouter = new RectangleLayouter(tagLayoutSettings, pointGenerators); + var rectangleSize = new Size(rectWidth, rectHeight); + var rectangleCreation = () => rectangleLayouter.PutNextRectangle(rectangleSize); + rectangleCreation.Should().Throw(); + } + + [TestCaseSource(nameof(GeneratorsAndMaxDistance))] + public void PutNextRectanglePlacesSquaresNear_WithDifferentRealisations( + IPointGenerator pointGenerator, + int closestRectangleMaxDistance) + { + rectangleLayouter = new RectangleLayouter(tagLayoutSettings, pointGenerators); + var squareSide = 20; + var rectangleSize = new Size(squareSide, squareSide); + var rectanglesWithoutCurrent = new List(); + + var firstRectangle = rectangleLayouter.PutNextRectangle(rectangleSize); + rectanglesWithoutCurrent.Add(firstRectangle); + + for (var i = 1; i < 15; i++) + { + var currentRectangle = rectangleLayouter.PutNextRectangle(rectangleSize); + + var closestRectangleDistance = rectanglesWithoutCurrent + .Min(existingRectangle => CalculateDistanceBetweenRectangles(currentRectangle, existingRectangle)); + + closestRectangleDistance.Should().BeLessThan(closestRectangleMaxDistance); + rectanglesWithoutCurrent.Add(currentRectangle); + } + } + + private static object[][] GeneratorsAndMaxDistance = + { + new object[] { new SpiralPointGenerator(), 21 } + }; + + private double CalculateDistanceBetweenRectangles(Rectangle firstRectangle, Rectangle secondRectangle) + { + var xSquare = (firstRectangle.X - secondRectangle.X) * (firstRectangle.X - secondRectangle.X); + var ySquare = (firstRectangle.Y - secondRectangle.Y) * (firstRectangle.Y - secondRectangle.Y); + return Math.Sqrt(xSquare + ySquare); + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/SpiralPointGenerator_Should.cs b/TagsCloudVisualizationTests/SpiralPointGenerator_Should.cs new file mode 100644 index 000000000..d12482abd --- /dev/null +++ b/TagsCloudVisualizationTests/SpiralPointGenerator_Should.cs @@ -0,0 +1,47 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloudVisualization; + +namespace TagsCloudVisualizationTests; + +public class SpiralPointGenerator_Should +{ + private SpiralPointGenerator spiralPointGenerator; + + [SetUp] + public void CreateSpiralGenerator() + { + spiralPointGenerator = new SpiralPointGenerator(); + } + + [Test] + public void GetPointCalculatesCorrectUniqueCoordinates() + { + var existingPoints = new List(); + + var expectedPoints = new Point[] + { + new (0, 0), + new (1, 0), + new (1, 1), + new (0, 1), + new (-1, 1), + new (-1, 0), + new (-1, -1), + new (0, -1), + new (1, -1), + new (2, 0), + }; + + foreach (var expectedPoint in expectedPoints) + { + var actualPoint = spiralPointGenerator.GetNextPoint(); + while (existingPoints.Contains(actualPoint)) + { + actualPoint = spiralPointGenerator.GetNextPoint(); + } + existingPoints.Add(actualPoint); + actualPoint.Should().Be(expectedPoint); + } + } +} \ No newline at end of file diff --git a/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj b/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj new file mode 100644 index 000000000..b4be87f6a --- /dev/null +++ b/TagsCloudVisualizationTests/TagsCloudVisualizationTests.csproj @@ -0,0 +1,45 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + LayoutDrawer_Should.cs + + + LayoutDrawer_Should.cs + + + + diff --git a/TagsCloudVisualizationTests/TextFiles/EmptyExcludedWords.txt b/TagsCloudVisualizationTests/TextFiles/EmptyExcludedWords.txt new file mode 100644 index 000000000..e69de29bb diff --git a/TagsCloudVisualizationTests/TextFiles/ExcludedWords.txt b/TagsCloudVisualizationTests/TextFiles/ExcludedWords.txt new file mode 100644 index 000000000..2668bbb36 --- /dev/null +++ b/TagsCloudVisualizationTests/TextFiles/ExcludedWords.txt @@ -0,0 +1,2 @@ +яблоко +петь \ No newline at end of file diff --git a/TagsCloudVisualizationTests/TextFiles/RuWords.txt b/TagsCloudVisualizationTests/TextFiles/RuWords.txt new file mode 100644 index 000000000..fe85b1ac3 --- /dev/null +++ b/TagsCloudVisualizationTests/TextFiles/RuWords.txt @@ -0,0 +1,10 @@ +яблоки +груши +персиковый +яблочный +красить +петь +и +я +он +она \ No newline at end of file diff --git a/TagsCloudVisualizationTests/Usings.cs b/TagsCloudVisualizationTests/Usings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/TagsCloudVisualizationTests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/TagsCloudVisualizationTests/WordParser_Should.cs b/TagsCloudVisualizationTests/WordParser_Should.cs new file mode 100644 index 000000000..45c1f85e6 --- /dev/null +++ b/TagsCloudVisualizationTests/WordParser_Should.cs @@ -0,0 +1,54 @@ +using FluentAssertions; +using TagsCloudVisualization; + +namespace TagsCloudVisualizationTests; + +public class WordParser_Should +{ + [Test] + public void GetInterestingWords_GetsAllLexems_WhenNoExcludedWordsAndPartsOfSpeech() + { + var tagLayoutSettings = + new TagLayoutSettings(Algorithm.Spiral, new HashSet(), @"TextFiles\EmptyExcludedWords.txt"); + + var wordParser = new MystemWordsParser(new MystemDullWordChecker(tagLayoutSettings)); + var parsedWords = wordParser.GetInterestingWords(@"TextFiles\RuWords.txt").GetValueOrThrow().ToArray(); + + parsedWords.Should() + .BeEquivalentTo("яблоко", "груша", "персиковый", "яблочный", "красить", "петь", "и", "я", "он", "она"); + } + + [Test] + public void GetInterestingWordsExcludeWords_WhenExclusionFileNotEmpty() + { + var tagLayoutSettings = + new TagLayoutSettings(Algorithm.Spiral, new HashSet(), @"TextFiles\ExcludedWords.txt"); + var wordParser = new MystemWordsParser(new MystemDullWordChecker(tagLayoutSettings)); + var parsedWords = wordParser.GetInterestingWords(@"TextFiles\RuWords.txt").GetValueOrThrow().ToArray(); + + parsedWords.Should() + .BeEquivalentTo("груша", "персиковый", "яблочный", "красить", "и", "я", "он", "она"); + } + + [Test] + public void GetInterestingWordsExcludesPartsOfSpeech() + { + var tagLayoutSettings = + new TagLayoutSettings(Algorithm.Spiral, new HashSet { "S", "CONJ" }, @"TextFiles\EmptyExcludedWords.txt"); + var wordParser = new MystemWordsParser(new MystemDullWordChecker(tagLayoutSettings)); + var parsedWords = wordParser.GetInterestingWords(@"TextFiles\RuWords.txt").GetValueOrThrow().ToArray(); + + parsedWords.Should().BeEquivalentTo("персиковый", "яблочный", "красить", "петь"); + } + + [Test] + public void GetInterestingWordsExcludesPartsOfSpeechAndWords() + { + var tagLayoutSettings = + new TagLayoutSettings(Algorithm.Spiral, new HashSet { "S", "CONJ" }, @"TextFiles\ExcludedWords.txt"); + var wordParser = new MystemWordsParser(new MystemDullWordChecker(tagLayoutSettings)); + var parsedWords = wordParser.GetInterestingWords(@"TextFiles\RuWords.txt").GetValueOrThrow().ToArray(); + + parsedWords.Should().BeEquivalentTo("персиковый", "яблочный", "красить"); + } +} \ No newline at end of file diff --git a/fp.sln b/fp.sln index a592ceee3..392f8fbc5 100644 --- a/fp.sln +++ b/fp.sln @@ -10,6 +10,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{754C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Summator", "Summator\Summator.csproj", "{2E64584B-4FBC-4E7D-AEF2-003E8251307A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualization", "TagsCloudVisualization\TagsCloudVisualization.csproj", "{54529126-B7B0-4264-8176-70F194B49717}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudVisualizationTests", "TagsCloudVisualizationTests\TagsCloudVisualizationTests.csproj", "{2896E771-AFB4-43AD-9E5C-1FAC8BA3C44B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +36,14 @@ Global {2E64584B-4FBC-4E7D-AEF2-003E8251307A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E64584B-4FBC-4E7D-AEF2-003E8251307A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E64584B-4FBC-4E7D-AEF2-003E8251307A}.Release|Any CPU.Build.0 = Release|Any CPU + {54529126-B7B0-4264-8176-70F194B49717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54529126-B7B0-4264-8176-70F194B49717}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54529126-B7B0-4264-8176-70F194B49717}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54529126-B7B0-4264-8176-70F194B49717}.Release|Any CPU.Build.0 = Release|Any CPU + {2896E771-AFB4-43AD-9E5C-1FAC8BA3C44B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2896E771-AFB4-43AD-9E5C-1FAC8BA3C44B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2896E771-AFB4-43AD-9E5C-1FAC8BA3C44B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2896E771-AFB4-43AD-9E5C-1FAC8BA3C44B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2E64584B-4FBC-4E7D-AEF2-003E8251307A} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0}