diff --git a/ErrorHandling/Result.cs b/ErrorHandling/Result.cs index 9d058ff55..4427a38c3 100644 --- a/ErrorHandling/Result.cs +++ b/ErrorHandling/Result.cs @@ -58,21 +58,36 @@ public static Result Then( this Result input, Func continuation) { - throw new NotImplementedException(); + return input.Then(inut => Of(() => continuation(inut))); } public static Result Then( this Result input, Func> continuation) { - throw new NotImplementedException(); + return input.IsSuccess ? continuation(input.Value) : Fail(input.Error); } public static Result OnFail( this Result input, Action handleError) { - throw new NotImplementedException(); + if (!input.IsSuccess) { + handleError(input.Error); + } + + return input; + } + + public static Result RefineError(this Result input, string error) + { + return input.IsSuccess ? input : input.ReplaceError(e => $"{error}. {input.Error}"); + } + + + public static Result ReplaceError(this Result input, Func error) + { + return input.IsSuccess ? input : Fail(error(input.Error)); } } } \ No newline at end of file diff --git a/ErrorHandling/Result_should.cs b/ErrorHandling/Result_should.cs index 1fabb5dcf..f87afe726 100644 --- a/ErrorHandling/Result_should.cs +++ b/ErrorHandling/Result_should.cs @@ -2,6 +2,7 @@ using FakeItEasy; using FluentAssertions; using NUnit.Framework; +using ResultOf; namespace ResultOfTask { @@ -141,7 +142,6 @@ public void RunThen_WhenOk_ComplexScenario() .Then(hex => parsed.GetValueOrThrow() + " -> " + Guid.Parse(hex + hex + hex + hex)); res.Should().BeEquivalentTo(Result.Ok("1358571172 -> 50fa26a4-50fa-26a4-50fa-26a450fa26a4")); } -/* [Test] public void ReplaceError_IfFail() { @@ -175,6 +175,5 @@ public void RefineError_AddErrorMessageBeforePreviousErrorText() .RefineError("Posting results to db") .Should().BeEquivalentTo(Result.Fail("Posting results to db. No connection")); } - */ } } \ No newline at end of file diff --git a/FileSenderRailway/Dependencies.cs b/FileSenderRailway/Dependencies.cs index 4335d7aae..3253391b0 100644 --- a/FileSenderRailway/Dependencies.cs +++ b/FileSenderRailway/Dependencies.cs @@ -1,5 +1,6 @@ using System; using System.Security.Cryptography.X509Certificates; +using ResultOf; namespace FileSenderRailway { @@ -10,13 +11,11 @@ public interface ICryptographer public interface IRecognizer { - /// Not recognized - Document Recognize(FileContent file); + Result Recognize(FileContent file); } public interface ISender { - /// Can't send void Send(Document document); } @@ -30,10 +29,10 @@ public Document(string name, byte[] content, DateTime created, string format) Content = content; } - public string Name { get; set; } - public DateTime Created { get; set; } - public string Format { get; set; } - public byte[] Content { get; set; } + public string Name { get;} + public DateTime Created { get; } + public string Format { get; } + public byte[] Content { get; } } public class FileContent @@ -59,5 +58,7 @@ public FileSendResult(FileContent file, string error = null) public FileContent File { get; } public string Error { get; } public bool IsSuccess => Error == null; + + } } \ No newline at end of file diff --git a/FileSenderRailway/FileSender.cs b/FileSenderRailway/FileSender.cs index eecd19780..34bce412d 100644 --- a/FileSenderRailway/FileSender.cs +++ b/FileSenderRailway/FileSender.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography.X509Certificates; +using ResultOf; namespace FileSenderRailway { @@ -23,42 +25,41 @@ public FileSender( this.now = now; } + private Result PrepareFileToSend(FileContent file, X509Certificate certificate) + { + var doc = recognizer.Recognize(file) + .Then(IsValidFormatVersion) + .Then(IsValidTimestamp) + .Then(doc => new Document(doc.Name, + cryptographer.Sign(doc.Content, certificate), doc.Created, + doc.Format)) + .RefineError("Can't prepare file to send"); + return doc; + } + public IEnumerable SendFiles(FileContent[] files, X509Certificate certificate) { - foreach (var file in files) - { - string errorMessage = null; - try - { - Document doc = recognizer.Recognize(file); - if (!IsValidFormatVersion(doc)) - throw new FormatException("Invalid format version"); - if (!IsValidTimestamp(doc)) - throw new FormatException("Too old document"); - doc.Content = cryptographer.Sign(doc.Content, certificate); - sender.Send(doc); - } - catch (FormatException e) - { - errorMessage = "Can't prepare file to send. " + e.Message; - } - catch (InvalidOperationException e) - { - errorMessage = "Can't send. " + e.Message; - } - yield return new FileSendResult(file, errorMessage); - } + return from file in files + let doc = PrepareFileToSend(file, certificate).Then(doc => sender.Send(doc)) + let errorMessage = doc.Error + select new FileSendResult(file, errorMessage); + } + + private static Result IsValidFormatVersion(Document doc) + { + return CheckDocument(doc, d => d.Format == "4.0" || d.Format == "3.1", "Invalid format version"); + ; } - private bool IsValidFormatVersion(Document doc) + private static Result CheckDocument(Document doc, Func usl, string error) { - return doc.Format == "4.0" || doc.Format == "3.1"; + return usl(doc) ? doc : Result.Fail(error); } - private bool IsValidTimestamp(Document doc) + private Result IsValidTimestamp(Document doc) { var oneMonthBefore = now().AddMonths(-1); - return doc.Created > oneMonthBefore; + return CheckDocument(doc, d => doc.Created > oneMonthBefore, "Too old document"); } } } \ No newline at end of file diff --git a/FileSenderRailway/FileSender_Should.cs b/FileSenderRailway/FileSender_Should.cs index 8a034050c..7f1cec710 100644 --- a/FileSenderRailway/FileSender_Should.cs +++ b/FileSenderRailway/FileSender_Should.cs @@ -7,6 +7,7 @@ using FakeItEasy; using FluentAssertions; using NUnit.Framework; +using ResultOf; namespace FileSenderRailway { @@ -51,7 +52,7 @@ public void BeOk_WhenGoodFormat( public void Fail_WhenNotRecognized() { A.CallTo(() => recognizer.Recognize(file)) - .Throws(new FormatException("Can't recognize")); + .Returns(Result.Fail("Can't recognize")); VerifyErrorOnPrepareFile(file, certificate); } diff --git a/TagsCloud/App/Actions/CircleCloudAction.cs b/TagsCloud/App/Actions/CircleCloudAction.cs new file mode 100644 index 000000000..78227afc2 --- /dev/null +++ b/TagsCloud/App/Actions/CircleCloudAction.cs @@ -0,0 +1,38 @@ +using TagsCloud.App.Infrastructure; +using TagsCloud.CloudLayouter; +using TagsCloud.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud.App.Actions; + +public class CircleCloudAction : IUiAction +{ + private readonly IImageHolder imageHolder; + private readonly TagCloudPainter painter; + private readonly AppSettings settings; + private readonly Spiral spiral; + + public CircleCloudAction( + AppSettings appSettings, IImageHolder imageHolder, TagCloudPainter painter, Spiral spiral) + { + settings = appSettings; + this.imageHolder = imageHolder; + this.painter = painter; + this.spiral = spiral; + } + + public MenuCategory Category => MenuCategory.Types; + public string Name => "Круг"; + public string Description => ""; + + public void Perform() + { + if (settings.File == null) + { + ErrorHandler.HandleError("сначала загрузи файл"); + return; + } + + painter.Paint(settings.File.FullName, spiral).OnFail(ErrorHandler.HandleError); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Actions/FlowerCloudAction.cs b/TagsCloud/App/Actions/FlowerCloudAction.cs new file mode 100644 index 000000000..714623ac8 --- /dev/null +++ b/TagsCloud/App/Actions/FlowerCloudAction.cs @@ -0,0 +1,35 @@ +using TagsCloud.App.Infrastructure; +using TagsCloud.CloudLayouter; +using TagsCloud.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud.App.Actions; + +public class FlowerCloudAction : IUiAction +{ + private readonly FlowerSpiral flowerSpiral; + private readonly TagCloudPainter painter; + private readonly AppSettings settings; + + public FlowerCloudAction(TagCloudPainter painter, AppSettings settings, FlowerSpiral spiral) + { + flowerSpiral = spiral; + this.settings = settings; + this.painter = painter; + } + + public MenuCategory Category => MenuCategory.Types; + public string Name => "Цветок"; + public string Description => ""; + + public void Perform() + { + if (settings.File == null) + { + ErrorHandler.HandleError("сначала загрузи файл"); + return; + } + + painter.Paint(settings.File.FullName, flowerSpiral).OnFail(ErrorHandler.HandleError); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Actions/ImageSettingsAction.cs b/TagsCloud/App/Actions/ImageSettingsAction.cs new file mode 100644 index 000000000..a9239e9a1 --- /dev/null +++ b/TagsCloud/App/Actions/ImageSettingsAction.cs @@ -0,0 +1,29 @@ +using TagsCloud.App.Infrastructure; +using TagsCloud.App.Settings; +using TagsCloud.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud.App.Actions; + +public class ImageSettingsAction : IUiAction +{ + private readonly IImageHolder imageHolder; + private readonly ImageSettings imageSettings; + + public ImageSettingsAction(IImageHolder imageHolder, + ImageSettings imageSettings) + { + this.imageHolder = imageHolder; + this.imageSettings = imageSettings; + } + + public MenuCategory Category => MenuCategory.Settings; + public string Name => "Изображение..."; + public string Description => "Размеры изображения"; + + public void Perform() + { + SettingsForm.For(imageSettings).ShowDialog(); + imageHolder.RecreateImage(imageSettings).OnFail(ErrorHandler.HandleError); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Actions/SaveFileAction.cs b/TagsCloud/App/Actions/SaveFileAction.cs new file mode 100644 index 000000000..86b1b2712 --- /dev/null +++ b/TagsCloud/App/Actions/SaveFileAction.cs @@ -0,0 +1,36 @@ +using System.Windows.Forms; +using TagsCloud.App.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud.App.Actions; + +public class SaveFileAction : IUiAction +{ + private readonly IImageHolder imageHolder; + + public SaveFileAction(IImageHolder imageHolder) + { + this.imageHolder = imageHolder; + } + + public MenuCategory Category => MenuCategory.File; + public string Name => "Сохранить"; + public string Description => "Сохранить изображение в файл"; + + public void Perform() + { + var imagesDirectoryPath = Path.GetFullPath("..//..//..//images"); + if (!Directory.Exists(imagesDirectoryPath)) Directory.CreateDirectory(imagesDirectoryPath); + var dialog = new SaveFileDialog + { + CheckFileExists = false, + InitialDirectory = Path.GetFullPath(imagesDirectoryPath), + DefaultExt = "png", + FileName = "image.png", + Filter = "Изображения (*.png)|*.png" + }; + var res = dialog.ShowDialog(); + if (res == DialogResult.OK) + imageHolder.SaveImage(dialog.FileName).OnFail(ErrorHandler.HandleError); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Actions/TagSettingsAction.cs b/TagsCloud/App/Actions/TagSettingsAction.cs new file mode 100644 index 000000000..dd6632ff8 --- /dev/null +++ b/TagsCloud/App/Actions/TagSettingsAction.cs @@ -0,0 +1,32 @@ +using System.Drawing.Text; +using TagsCloud.App.Infrastructure; +using TagsCloud.App.Settings; +using TagsCloud.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud.App.Actions; + +public class TagSettingsAction : IUiAction +{ + private readonly HashSet fonts; + private readonly TagSettings tag; + + public TagSettingsAction(TagSettings tag) + { + this.tag = tag; + fonts = new InstalledFontCollection().Families.Select(f => f.Name).ToHashSet(); + } + + public MenuCategory Category => MenuCategory.Settings; + public string Name => "Облако тегов..."; + public string Description => ""; + + public void Perform() + { + SettingsForm.For(tag).ShowDialog(); + + if (!fonts.Contains(tag.FontFamily)) ErrorHandler.HandleError($"Шрифт {tag.FontFamily} не найден"); + + if (tag.Size < 0) ErrorHandler.HandleError("Размер шрифта не должен быть отрицательным"); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Actions/UploadFileAction.cs b/TagsCloud/App/Actions/UploadFileAction.cs new file mode 100644 index 000000000..da950e52e --- /dev/null +++ b/TagsCloud/App/Actions/UploadFileAction.cs @@ -0,0 +1,32 @@ +using System.Windows.Forms; +using TagsCloud.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud.App.Actions; + +public class UploadFileAction : IUiAction +{ + private readonly AppSettings settings; + + public UploadFileAction(AppSettings settings) + { + this.settings = settings; + } + + public MenuCategory Category => MenuCategory.File; + public string Name => "Загрузить"; + public string Description => ""; + + public void Perform() + { + var openFileDialog = new OpenFileDialog + { + Title = "Выберите файл", + Filter = "Текстовые файлы (*.txt)|*.txt|Документы (*.doc;*.docx)|*.doc;*.docx|Все файлы (*.*)|*.*", + FilterIndex = 1, + RestoreDirectory = true + }; + if (openFileDialog.ShowDialog() != DialogResult.OK) return; + settings.File = new FileInfo(openFileDialog.FileName); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Actions/WordAnalyzerSettingsAction.cs b/TagsCloud/App/Actions/WordAnalyzerSettingsAction.cs new file mode 100644 index 000000000..055839899 --- /dev/null +++ b/TagsCloud/App/Actions/WordAnalyzerSettingsAction.cs @@ -0,0 +1,24 @@ +using TagsCloud.App.Settings; +using TagsCloud.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud.App.Actions; + +public class WordAnalyzerSettingsAction : IUiAction +{ + private readonly WordAnalyzerSettings wordAnalyzerSettings; + + public WordAnalyzerSettingsAction(WordAnalyzerSettings wordAnalyzerSettings) + { + this.wordAnalyzerSettings = wordAnalyzerSettings; + } + + public MenuCategory Category => MenuCategory.Settings; + public string Name => "Анализатор..."; + public string Description => ""; + + public void Perform() + { + SettingsForm.For(wordAnalyzerSettings).ShowDialog(); + } +} \ No newline at end of file diff --git a/TagsCloud/App/CloudForm.Designer.cs b/TagsCloud/App/CloudForm.Designer.cs new file mode 100644 index 000000000..a1deb3404 --- /dev/null +++ b/TagsCloud/App/CloudForm.Designer.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; + +namespace TagsCloud; + +partial class CloudForm +{ + /// + /// Required designer variable. + /// + private IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Text = "CloudForm"; + } + + #endregion +} \ No newline at end of file diff --git a/TagsCloud/App/CloudForm.cs b/TagsCloud/App/CloudForm.cs new file mode 100644 index 000000000..e2d134676 --- /dev/null +++ b/TagsCloud/App/CloudForm.cs @@ -0,0 +1,32 @@ +using System.Drawing; +using System.Windows.Forms; +using TagsCloud.App.Infrastructure; +using TagsCloud.App.Settings; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud; + +public partial class CloudForm : Form +{ + public CloudForm(IUiAction[] actions, + PictureBoxImageHolder pictureBox, + ImageSettings imageSettings) + { + InitializeComponent(); + ClientSize = new Size(imageSettings.Width, imageSettings.Height); + + var mainMenu = new MenuStrip(); + mainMenu.Items.AddRange(actions.ToMenuItems()); + Controls.Add(mainMenu); + + pictureBox.RecreateImage(imageSettings); + pictureBox.Dock = DockStyle.Fill; + Controls.Add(pictureBox); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + Text = "TagCloud Painter"; + } +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/AppSettings.cs b/TagsCloud/App/Infrastructure/AppSettings.cs new file mode 100644 index 000000000..f03b8bafb --- /dev/null +++ b/TagsCloud/App/Infrastructure/AppSettings.cs @@ -0,0 +1,6 @@ +namespace TagsCloud.Infrastructure; + +public class AppSettings +{ + public FileInfo File { get; set; } +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/ErrorHandler.cs b/TagsCloud/App/Infrastructure/ErrorHandler.cs new file mode 100644 index 000000000..9569148cb --- /dev/null +++ b/TagsCloud/App/Infrastructure/ErrorHandler.cs @@ -0,0 +1,11 @@ +using System.Windows.Forms; + +namespace TagsCloud.App.Infrastructure; + +public static class ErrorHandler +{ + public static void HandleError(string error) + { + MessageBox.Show(error, "Ошибочная ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/IImageHolder.cs b/TagsCloud/App/Infrastructure/IImageHolder.cs new file mode 100644 index 000000000..5d912ff64 --- /dev/null +++ b/TagsCloud/App/Infrastructure/IImageHolder.cs @@ -0,0 +1,13 @@ +using System.Drawing; +using TagsCloud.App.Settings; + +namespace TagsCloud.App.Infrastructure; + +public interface IImageHolder +{ + Result GetImageSize(); + Result StartDrawing(); + void UpdateUi(); + Result RecreateImage(ImageSettings settings); + Result SaveImage(string fileName); +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/PictureBoxImageHolder.cs b/TagsCloud/App/Infrastructure/PictureBoxImageHolder.cs new file mode 100644 index 000000000..83000a7ec --- /dev/null +++ b/TagsCloud/App/Infrastructure/PictureBoxImageHolder.cs @@ -0,0 +1,46 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Windows.Forms; +using TagsCloud.App.Settings; + +namespace TagsCloud.App.Infrastructure; + +public class PictureBoxImageHolder : PictureBox, IImageHolder +{ + public Result GetImageSize() + { + return FailIfNotInitialized().Then(_ => Image.Size); + } + + public Result StartDrawing() + { + return FailIfNotInitialized().Then(_ => Graphics.FromImage(Image)); + } + + public void UpdateUi() + { + Refresh(); + Application.DoEvents(); + } + + public Result RecreateImage(ImageSettings imageSettings) + { + if (imageSettings.Height <= 0 || imageSettings.Width <= 0) + return Result.Fail("размеры изображения не могут быть отрицательными или равными нулю"); + + Image = new Bitmap(imageSettings.Width, imageSettings.Height, PixelFormat.Format24bppRgb); + return Result.Ok(); + } + + public Result SaveImage(string fileName) + { + return FailIfNotInitialized().Then(_ => Image.Save(fileName)); + } + + private Result FailIfNotInitialized() + { + return Image == null + ? Result.Fail("Call PictureBoxImageHolder.RecreateImage before other method call!") + : Result.Ok(); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/SettingsForm.cs b/TagsCloud/App/Infrastructure/SettingsForm.cs new file mode 100644 index 000000000..c46c78e6a --- /dev/null +++ b/TagsCloud/App/Infrastructure/SettingsForm.cs @@ -0,0 +1,37 @@ +using System.Windows.Forms; + +namespace TagsCloud.Infrastructure; + +public static class SettingsForm +{ + public static SettingsForm For(TSettings settings) + { + return new SettingsForm(settings); + } +} + +public class SettingsForm : Form +{ + public SettingsForm(TSettings settings) + { + var okButton = new Button + { + Text = "OK", + DialogResult = DialogResult.OK, + Dock = DockStyle.Bottom + }; + Controls.Add(okButton); + Controls.Add(new PropertyGrid + { + SelectedObject = settings, + Dock = DockStyle.Fill + }); + AcceptButton = okButton; + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + Text = "Настройки"; + } +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/UiActions/EnumExtensions.cs b/TagsCloud/App/Infrastructure/UiActions/EnumExtensions.cs new file mode 100644 index 000000000..ed3ce1d2c --- /dev/null +++ b/TagsCloud/App/Infrastructure/UiActions/EnumExtensions.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; + +namespace TagsCloud.Infrastructure.UiActions; + +public static class EnumExtensions +{ + public static string GetDescription(this Enum enumValue) + { + var fieldInfo = enumValue.GetType().GetField(enumValue.ToString()); + var description = fieldInfo + .GetCustomAttributes(typeof(DescriptionAttribute), false) + .Cast() + .FirstOrDefault()?.Description; + + return description ?? enumValue.ToString(); + } +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/UiActions/IUiAction.cs b/TagsCloud/App/Infrastructure/UiActions/IUiAction.cs new file mode 100644 index 000000000..5423d0d45 --- /dev/null +++ b/TagsCloud/App/Infrastructure/UiActions/IUiAction.cs @@ -0,0 +1,9 @@ +namespace TagsCloud.Infrastructure.UiActions; + +public interface IUiAction +{ + MenuCategory Category { get; } + string Name { get; } + string Description { get; } + void Perform(); +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/UiActions/MenuCategory.cs b/TagsCloud/App/Infrastructure/UiActions/MenuCategory.cs new file mode 100644 index 000000000..c67a8609f --- /dev/null +++ b/TagsCloud/App/Infrastructure/UiActions/MenuCategory.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace TagsCloud.Infrastructure.UiActions; + +public enum MenuCategory +{ + [Description("Файл")] File = 0, + + [Description("Типы Облаков")] Types = 1, + + [Description("Настойки")] Settings = 2 +} \ No newline at end of file diff --git a/TagsCloud/App/Infrastructure/UiActions/UiActionExtensions.cs b/TagsCloud/App/Infrastructure/UiActions/UiActionExtensions.cs new file mode 100644 index 000000000..fa436374e --- /dev/null +++ b/TagsCloud/App/Infrastructure/UiActions/UiActionExtensions.cs @@ -0,0 +1,32 @@ +using System.Windows.Forms; + +namespace TagsCloud.Infrastructure.UiActions; + +public static class UiActionExtensions +{ + public static ToolStripItem[] ToMenuItems(this IUiAction[] actions) + { + var items = actions.GroupBy(a => a.Category) + .OrderBy(a => a.Key) + .Select(g => CreateTopLevelMenuItem(g.Key, g.ToList())) + .Cast() + .ToArray(); + return items; + } + + private static ToolStripMenuItem CreateTopLevelMenuItem(MenuCategory category, IList items) + { + var menuItems = items.Select(a => a.ToMenuItem()).ToArray(); + return new ToolStripMenuItem(category.GetDescription(), null, menuItems); + } + + public static ToolStripItem ToMenuItem(this IUiAction action) + { + return + new ToolStripMenuItem(action.Name, null, (sender, args) => action.Perform()) + { + ToolTipText = action.Description, + Tag = action + }; + } +} \ No newline at end of file diff --git a/TagsCloud/App/Settings/ImageSettings.cs b/TagsCloud/App/Settings/ImageSettings.cs new file mode 100644 index 000000000..3ed378cb8 --- /dev/null +++ b/TagsCloud/App/Settings/ImageSettings.cs @@ -0,0 +1,7 @@ +namespace TagsCloud.App.Settings; + +public class ImageSettings +{ + public int Width { get; set; } = 1000; + public int Height { get; set; } = 1000; +} \ No newline at end of file diff --git a/TagsCloud/App/Settings/TagSettings.cs b/TagsCloud/App/Settings/TagSettings.cs new file mode 100644 index 000000000..2f22d3f84 --- /dev/null +++ b/TagsCloud/App/Settings/TagSettings.cs @@ -0,0 +1,10 @@ +using System.Drawing; + +namespace TagsCloud.App.Settings; + +public class TagSettings +{ + public string FontFamily { get; set; } = "Arial"; + public int Size { get; set; } = 1; + public Color Color { get; set; } = Color.DarkBlue; +} \ No newline at end of file diff --git a/TagsCloud/App/Settings/WordAnalyzerSettings.cs b/TagsCloud/App/Settings/WordAnalyzerSettings.cs new file mode 100644 index 000000000..1af0b4442 --- /dev/null +++ b/TagsCloud/App/Settings/WordAnalyzerSettings.cs @@ -0,0 +1,15 @@ +using TagsCloud.WordAnalyzer; + +namespace TagsCloud.App.Settings; + +public class WordAnalyzerSettings +{ + public List BoringWords { get; set; } = new(); + + public List ExcludedSpeeches { get; set; } = new() + { + PartSpeech.Interjection, PartSpeech.Preposition + }; + + public List SelectedSpeeches { get; set; } = new(); +} \ No newline at end of file diff --git a/TagsCloud/App/TagCloudPainter.cs b/TagsCloud/App/TagCloudPainter.cs new file mode 100644 index 000000000..45510a2cf --- /dev/null +++ b/TagsCloud/App/TagCloudPainter.cs @@ -0,0 +1,48 @@ +using System.Drawing; +using TagsCloud.App.Infrastructure; +using TagsCloud.App.Settings; +using TagsCloud.CloudLayouter; +using TagsCloud.CloudVisualizer; +using TagsCloud.WordAnalyzer; + +namespace TagsCloud.App; + +public class TagCloudPainter +{ + private readonly FileReader fileReader; + + private readonly IImageHolder imageHolder; + private readonly TagSettings tagSettings; + private readonly WordAnalyzer.WordAnalyzer wordAnalyzer; + + public TagCloudPainter(IImageHolder imageHolder, TagSettings tagSettings, WordAnalyzer.WordAnalyzer wordAnalyzer, + FileReader reader) + { + this.imageHolder = imageHolder; + this.wordAnalyzer = wordAnalyzer; + fileReader = reader; + this.tagSettings = tagSettings; + } + + public Result Paint(string filePath, ISpiral spiral) + { + var result = fileReader.GetWords(filePath) + .Then(words => wordAnalyzer.GetFrequencyList(words)) + .Then(frequencyList => imageHolder.GetImageSize() + .Then(sizeImage => DrawTagCloud(spiral, frequencyList, sizeImage))); + if (result.IsSuccess) + imageHolder.UpdateUi(); + return result; + } + + private Result DrawTagCloud(ISpiral spiral, IEnumerable frequencyList, Size sizeImage) + { + var painter = new TagCloudVisualizer(tagSettings, sizeImage); + var cloudLayouter = + new CloudLayouter.CloudLayouter(spiral, new Point(sizeImage.Width / 2, sizeImage.Height / 2)); + var background = new SolidBrush(Color.Black); + using var graphic = imageHolder.StartDrawing().GetValueOrThrow(); + graphic.FillRectangle(background, new Rectangle(0, 0, sizeImage.Width, sizeImage.Height)); + return painter.DrawTags(frequencyList, graphic, cloudLayouter); + } +} \ No newline at end of file diff --git a/TagsCloud/CloudLayouter/CloudLayouter.cs b/TagsCloud/CloudLayouter/CloudLayouter.cs new file mode 100644 index 000000000..0a509f984 --- /dev/null +++ b/TagsCloud/CloudLayouter/CloudLayouter.cs @@ -0,0 +1,46 @@ +using System.Drawing; + +namespace TagsCloud.CloudLayouter; + +public class CloudLayouter : ICloudLayouter +{ + private readonly IEnumerator iterator; + + public CloudLayouter(ISpiral spiral, Point center) + { + iterator = spiral.GetPoints(center).GetEnumerator(); + Rectangles = new List(); + } + + public List Rectangles { get; } + + public Result PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Height <= 0 || rectangleSize.Width <= 0) + return Result.Fail("Sides of the rectangle should not be non-positive"); + var rectangles = CreateNextRectangle(rectangleSize); + Rectangles.Add(rectangles); + return rectangles; + } + + private Rectangle CreateNextRectangle(Size rectangleSize) + { + iterator.MoveNext(); + var rectangle = new Rectangle(iterator.Current, rectangleSize); + while (!HasNoIntersections(rectangle)) + { + iterator.MoveNext(); + rectangle = new Rectangle(iterator.Current, rectangleSize); + } + + return rectangle; + } + + private bool HasNoIntersections(Rectangle rectangles) + { + for (var i = Rectangles.Count - 1; i >= 0; i--) + if (Rectangles[i].IntersectsWith(rectangles)) + return false; + return true; + } +} \ No newline at end of file diff --git a/TagsCloud/CloudLayouter/CloudLayouterExtension.cs b/TagsCloud/CloudLayouter/CloudLayouterExtension.cs new file mode 100644 index 000000000..933aaa0ae --- /dev/null +++ b/TagsCloud/CloudLayouter/CloudLayouterExtension.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagsCloud.CloudLayouter; + +public static class CloudLayouterExtension +{ + public static List GetRectanglesLocation(this ICloudLayouter layouter) + { + return layouter.Rectangles.Select(rectangle => rectangle.Location).ToList(); + } +} \ No newline at end of file diff --git a/TagsCloud/CloudLayouter/FlowerSpiral.cs b/TagsCloud/CloudLayouter/FlowerSpiral.cs new file mode 100644 index 000000000..9fa4fc27d --- /dev/null +++ b/TagsCloud/CloudLayouter/FlowerSpiral.cs @@ -0,0 +1,36 @@ +using System.Drawing; + +namespace TagsCloud.CloudLayouter; + +public class FlowerSpiral : ISpiral +{ + private readonly int petalCount; + private readonly double petalLength; + private readonly float step; + private int counter; + + public FlowerSpiral(double petalLength = 0.5, int petalCount = 4, float step = 0.1f) + { + this.petalLength = petalLength; + this.petalCount = petalCount; + if (this.petalCount < 0 || petalLength < 0) + throw new ArgumentException($"{nameof(petalCount)} or {nameof(petalLength)} must not be less than 0"); + if (step == 0) + throw new ArgumentException($"the {nameof(step)} must not be equal to 0"); + this.step = step; + } + + public IEnumerable GetPoints(Point center) + { + counter = 0; + while (true) + { + var angle = step * counter; + var radius = angle * petalLength * Math.Sin(petalCount * angle); + var xOffset = (float)(radius * Math.Cos(angle)); + var yOffset = (float)(radius * Math.Sin(angle)); + yield return new Point((int)Math.Round(center.X + xOffset), (int)Math.Round(center.Y + yOffset)); + counter += 1; + } + } +} \ No newline at end of file diff --git a/TagsCloud/CloudLayouter/ICloudLayouter.cs b/TagsCloud/CloudLayouter/ICloudLayouter.cs new file mode 100644 index 000000000..0cc0f28c0 --- /dev/null +++ b/TagsCloud/CloudLayouter/ICloudLayouter.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagsCloud.CloudLayouter; + +public interface ICloudLayouter +{ + List Rectangles { get; } + Result PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/TagsCloud/CloudLayouter/ISpiral.cs b/TagsCloud/CloudLayouter/ISpiral.cs new file mode 100644 index 000000000..13c7af68c --- /dev/null +++ b/TagsCloud/CloudLayouter/ISpiral.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagsCloud.CloudLayouter; + +public interface ISpiral +{ + IEnumerable GetPoints(Point start); +} \ No newline at end of file diff --git a/TagsCloud/CloudLayouter/Spiral.cs b/TagsCloud/CloudLayouter/Spiral.cs new file mode 100644 index 000000000..4029627fa --- /dev/null +++ b/TagsCloud/CloudLayouter/Spiral.cs @@ -0,0 +1,29 @@ +using System.Drawing; + +namespace TagsCloud.CloudLayouter; + +public class Spiral : ISpiral +{ + private readonly float step; + + public Spiral(float step = 0.1f) + { + if (step == 0) + throw new ArgumentException("the step must not be equal to 0"); + this.step = step; + } + + public IEnumerable GetPoints(Point start) + { + var counter = 0; + while (true) + { + var angle = step * counter; + var xOffset = (float)(step * angle * Math.Cos(angle)); + var yOffset = (float)(step * angle * Math.Sin(angle)); + var point = new Point((int)Math.Round(start.X + xOffset), (int)Math.Round(start.Y + yOffset)); + yield return point; + counter += 1; + } + } +} \ No newline at end of file diff --git a/TagsCloud/CloudVisualizer/Tag.cs b/TagsCloud/CloudVisualizer/Tag.cs new file mode 100644 index 000000000..8c6448dc4 --- /dev/null +++ b/TagsCloud/CloudVisualizer/Tag.cs @@ -0,0 +1,19 @@ +using System.Drawing; + +namespace TagsCloud.CloudPainter; + +public class Tag +{ + public readonly Font Font; + public readonly string Word; + public Color Color; + public Rectangle Rectangle; + + public Tag(Font font, string word, Rectangle rectangle, Color color) + { + Color = color; + Font = font; + Word = word; + Rectangle = rectangle; + } +} \ No newline at end of file diff --git a/TagsCloud/CloudVisualizer/TagCloudVisualizer.cs b/TagsCloud/CloudVisualizer/TagCloudVisualizer.cs new file mode 100644 index 000000000..1639285cc --- /dev/null +++ b/TagsCloud/CloudVisualizer/TagCloudVisualizer.cs @@ -0,0 +1,61 @@ +using System.Drawing; +using System.Windows.Forms; +using TagsCloud.App.Settings; +using TagsCloud.CloudLayouter; +using TagsCloud.CloudPainter; +using TagsCloud.WordAnalyzer; + +namespace TagsCloud.CloudVisualizer; + +public class TagCloudVisualizer +{ + public const int Border = 35; + private readonly string Link = "https://github.com/lepeap/DeepMorphy/blob/master/README.md"; + private readonly Size sizeImage; + private readonly TagSettings tagSettings; + + public TagCloudVisualizer(TagSettings tagSettings, Size imageSize) + { + sizeImage = imageSize; + this.tagSettings = tagSettings; + } + + private Tag GetTag(WordInfo wordInfo, ICloudLayouter cloudLayouter) + { + var font = new Font(tagSettings.FontFamily, wordInfo.Count * tagSettings.Size); + var textSize = TextRenderer.MeasureText(wordInfo.Word, font); + var textRectangle = cloudLayouter.PutNextRectangle(new Size(textSize.Width, textSize.Height)); + return new Tag(font, wordInfo.Word, textRectangle.GetValueOrThrow(), tagSettings.Color); + } + + private Rectangle MakeNeedImageSize(Rectangle rectangle, Rectangle imageRectangle) + { + var leftmost = Math.Min(imageRectangle.Left, rectangle.Left); + var rightmost = Math.Max(imageRectangle.Right, rectangle.Right); + var topmost = Math.Min(imageRectangle.Top, rectangle.Top); + var bottommost = Math.Max(imageRectangle.Bottom, rectangle.Bottom); + return new Rectangle(leftmost, topmost, rightmost - leftmost, bottommost - topmost); + } + + private bool IsRectangleOutOfBounds(Rectangle expectedImage) + { + return sizeImage.Height < expectedImage.Height || sizeImage.Width < expectedImage.Width; + } + + public Result DrawTags(IEnumerable words, Graphics graphics, ICloudLayouter cloudLayouter) + { + var imageRectangle = new Rectangle(new Point(0, 0), sizeImage); + foreach (var word in words) + { + var tag = GetTag(word, cloudLayouter); + var brush = new SolidBrush(tag.Color); + imageRectangle = MakeNeedImageSize(tag.Rectangle, imageRectangle); + graphics.DrawString(tag.Word, tag.Font, brush, tag.Rectangle.Location); + } + + return IsRectangleOutOfBounds(imageRectangle) + ? Result.Fail( + $"Облако тегов вышло за границы изображения. Поставь размер {imageRectangle.Width + Border * 2}x{imageRectangle.Height + Border * 2}") + : Result.Ok(); + } +} \ No newline at end of file diff --git a/TagsCloud/FileReader/DocParser.cs b/TagsCloud/FileReader/DocParser.cs new file mode 100644 index 000000000..e844b9f46 --- /dev/null +++ b/TagsCloud/FileReader/DocParser.cs @@ -0,0 +1,14 @@ +using Spire.Doc; + +namespace TagsCloud; + +public class DocParser : IParser +{ + public string FileType => "doc"; + + public IEnumerable GetWordList(string filePath) + { + var document = new Document(filePath, FileFormat.Doc); + return ParserHelper.GetTextParagraph(document); + } +} \ No newline at end of file diff --git a/TagsCloud/FileReader/DocxParser.cs b/TagsCloud/FileReader/DocxParser.cs new file mode 100644 index 000000000..a75c59e55 --- /dev/null +++ b/TagsCloud/FileReader/DocxParser.cs @@ -0,0 +1,14 @@ +using Spire.Doc; + +namespace TagsCloud; + +public class DocxParser : IParser +{ + public string FileType => "docx"; + + public IEnumerable GetWordList(string filePath) + { + var document = new Document(filePath, FileFormat.Docx); + return ParserHelper.GetTextParagraph(document); + } +} \ No newline at end of file diff --git a/TagsCloud/FileReader/FileReader.cs b/TagsCloud/FileReader/FileReader.cs new file mode 100644 index 000000000..e0c89ee58 --- /dev/null +++ b/TagsCloud/FileReader/FileReader.cs @@ -0,0 +1,23 @@ +namespace TagsCloud; + +public class FileReader +{ + private readonly Dictionary parsers; + + public FileReader(IEnumerable parsers) + { + this.parsers = parsers.ToDictionary(parser => parser.FileType); + } + + public Result> GetWords(string filePath) + { + var types = string.Join(", ", parsers.Select(p => p.Key)); + return File.Exists(filePath) + ? parsers.TryGetValue(Path.GetExtension(filePath).Trim('.'), out var parser) + ? Result.Ok>(parser.GetWordList(filePath)) + : Result.Fail>( + $"К сожалению, эта программа поддерживает только файлы с расширениями: {types}.\n " + + $"Попробуйте сконвертировать ваш файл с расширением {Path.GetExtension(filePath).Trim('.')} в один из указанных типов.") + : Result.Fail>($"Файл по пути '{filePath}' не найден"); + } +} \ No newline at end of file diff --git a/TagsCloud/FileReader/IParser.cs b/TagsCloud/FileReader/IParser.cs new file mode 100644 index 000000000..db7a8bbed --- /dev/null +++ b/TagsCloud/FileReader/IParser.cs @@ -0,0 +1,8 @@ +namespace TagsCloud; + +public interface IParser +{ + public string FileType { get; } + + public IEnumerable GetWordList(string filePath); +} \ No newline at end of file diff --git a/TagsCloud/FileReader/ParserHelper.cs b/TagsCloud/FileReader/ParserHelper.cs new file mode 100644 index 000000000..011cf75a9 --- /dev/null +++ b/TagsCloud/FileReader/ParserHelper.cs @@ -0,0 +1,17 @@ +using System.Text.RegularExpressions; +using Spire.Doc.Interface; + +namespace TagsCloud; + +public static class ParserHelper +{ + private static Regex SelectAllWordsRegex => new(@"([\w]+)", RegexOptions.Compiled); + + public static IEnumerable GetTextParagraph(IDocument document) + { + var section = document.Sections[0]; + for (var i = 0; i < section.Paragraphs.Count; i++) + foreach (var word in SelectAllWordsRegex.Matches(section.Paragraphs[i].Text)) + yield return word.ToString().Trim().ToLower(); + } +} \ No newline at end of file diff --git a/TagsCloud/FileReader/TxtParser.cs b/TagsCloud/FileReader/TxtParser.cs new file mode 100644 index 000000000..6f5b9827e --- /dev/null +++ b/TagsCloud/FileReader/TxtParser.cs @@ -0,0 +1,14 @@ +using Spire.Doc; + +namespace TagsCloud; + +public class TxtParser : IParser +{ + public string FileType => "txt"; + + public IEnumerable GetWordList(string filePath) + { + var document = new Document(filePath, FileFormat.Txt); + return ParserHelper.GetTextParagraph(document); + } +} \ No newline at end of file diff --git a/TagsCloud/Folder.DotSettings b/TagsCloud/Folder.DotSettings new file mode 100644 index 000000000..39df687ec --- /dev/null +++ b/TagsCloud/Folder.DotSettings @@ -0,0 +1,5 @@ + + False \ No newline at end of file diff --git a/TagsCloud/Program.cs b/TagsCloud/Program.cs new file mode 100644 index 000000000..5bdc447d0 --- /dev/null +++ b/TagsCloud/Program.cs @@ -0,0 +1,18 @@ +using System.Windows.Forms; +using Autofac; + +namespace TagsCloud; + +public class Program +{ + [STAThread] + public static void Main(string[] args) + { + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + var program = ProgramConstructor.ConstructProgram(); + var cloudForm = program.Resolve(); + Application.Run(cloudForm); + } +} \ No newline at end of file diff --git a/TagsCloud/ProgramConstructor.cs b/TagsCloud/ProgramConstructor.cs new file mode 100644 index 000000000..99fd67d24 --- /dev/null +++ b/TagsCloud/ProgramConstructor.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using Autofac; +using TagsCloud.App; +using TagsCloud.App.Infrastructure; +using TagsCloud.App.Settings; +using TagsCloud.CloudLayouter; +using TagsCloud.Infrastructure; +using TagsCloud.Infrastructure.UiActions; + +namespace TagsCloud; + +public static class ProgramConstructor +{ + public static IContainer ConstructProgram() + { + var containerBuilder = new ContainerBuilder(); + containerBuilder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) + .Where(t => typeof(IUiAction).IsAssignableFrom(t)) + .AsImplementedInterfaces(); + containerBuilder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) + .Where(t => typeof(IParser).IsAssignableFrom(t)) + .AsImplementedInterfaces(); + containerBuilder.RegisterType().AsSelf().SingleInstance(); + containerBuilder.RegisterType(); + containerBuilder.RegisterType().As() + .SingleInstance(); + containerBuilder.RegisterType().AsSelf().SingleInstance(); + containerBuilder.RegisterType().AsSelf().SingleInstance(); + containerBuilder.RegisterType().AsSelf().SingleInstance(); + containerBuilder.RegisterType(); + containerBuilder.RegisterType(); + containerBuilder.RegisterType().AsSelf().SingleInstance(); + containerBuilder.RegisterType(); + containerBuilder.RegisterType(); + return containerBuilder.Build(); + } +} \ No newline at end of file diff --git a/TagsCloud/README.md b/TagsCloud/README.md new file mode 100644 index 000000000..eda34029e --- /dev/null +++ b/TagsCloud/README.md @@ -0,0 +1,4 @@ +![10000 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/10000rect.png) +![1000 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/1000rect.png) +![100 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/100rect.png) +![10 random rectangles](https://github.com/PavelUd/tdd/blob/master/cs/tagsCloud/images/10rect.png) \ No newline at end of file diff --git a/TagsCloud/Result.cs b/TagsCloud/Result.cs new file mode 100644 index 000000000..d2577445b --- /dev/null +++ b/TagsCloud/Result.cs @@ -0,0 +1,134 @@ +namespace TagsCloud; + +public class None +{ + private None() + { + } +} + +public struct Result +{ + public Result(string error, T value = default) + { + 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); + } +} \ No newline at end of file diff --git a/TagsCloud/TagsCloud.csproj b/TagsCloud/TagsCloud.csproj new file mode 100644 index 000000000..7d53b2496 --- /dev/null +++ b/TagsCloud/TagsCloud.csproj @@ -0,0 +1,34 @@ + + + + net6-windows + enable + enable + Exe + TagsCloud + + + + + + + + + + + + ..\..\..\..\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\3.1.32\System.Windows.Forms.dll + + + + + + + + + + Form + + + + diff --git a/TagsCloud/TagsCloud.csproj.DotSettings b/TagsCloud/TagsCloud.csproj.DotSettings new file mode 100644 index 000000000..58ac9814c --- /dev/null +++ b/TagsCloud/TagsCloud.csproj.DotSettings @@ -0,0 +1,7 @@ + + True + True \ No newline at end of file diff --git a/TagsCloud/WordAnalyzer/PartSpeech.cs b/TagsCloud/WordAnalyzer/PartSpeech.cs new file mode 100644 index 000000000..4d907fecc --- /dev/null +++ b/TagsCloud/WordAnalyzer/PartSpeech.cs @@ -0,0 +1,13 @@ +namespace TagsCloud.WordAnalyzer; + +public enum PartSpeech +{ + Noun, + Pronoun, + Verb, + Adjective, + Adverb, + Preposition, + Conjunction, + Interjection +} \ No newline at end of file diff --git a/TagsCloud/WordAnalyzer/WordAnalyzer.cs b/TagsCloud/WordAnalyzer/WordAnalyzer.cs new file mode 100644 index 000000000..4ac4b4716 --- /dev/null +++ b/TagsCloud/WordAnalyzer/WordAnalyzer.cs @@ -0,0 +1,63 @@ +using System.Diagnostics.CodeAnalysis; +using DeepMorphy; +using DeepMorphy.Model; +using TagsCloud.App.Settings; + +namespace TagsCloud.WordAnalyzer; + +public class WordAnalyzer +{ + private readonly string Link = "https://github.com/lepeap/DeepMorphy/blob/master/README.md"; + private readonly WordAnalyzerSettings Settings; + + public WordAnalyzer(WordAnalyzerSettings settings) + { + Settings = settings; + } + + [SuppressMessage("ReSharper.DPA", "DPA0002: Excessive memory allocations in SOH", + MessageId = "type: System.String; size: 127MB")] + private Result> GetFilteredWords(IEnumerable words) + { + var morphAnalyzer = new MorphAnalyzer(true); + var morphInfos = Result.Of(() => morphAnalyzer.Parse(words)); + + if (!morphInfos.IsSuccess) + { + var g = new Uri(Link); + return Result.Fail>( + $"Ошибка внутренней библиотеки Deep Morphy: {morphInfos.Error} +\n\n\n" + + $"Посмотри документацию по ссылке: {Link}"); + } + + return Result.Ok(morphInfos.Value + .Where(IsCurrentWordForAnalysis) + .Select(info => info.BestTag.HasLemma ? info.BestTag.Lemma : info.Text)); + } + + private bool IsCurrentWordForAnalysis(MorphInfo morphInfo) + { + var excludedSpeeches = WordAnalyzerHelper.GetConvertedSpeeches(Settings.ExcludedSpeeches); + var selectedSpeeches = WordAnalyzerHelper.GetConvertedSpeeches(Settings.SelectedSpeeches); + + return !Settings.BoringWords.Any(item => + item.Equals(morphInfo.BestTag.Lemma, StringComparison.OrdinalIgnoreCase)) && + !excludedSpeeches.Contains(morphInfo.BestTag["чр"]) && + (selectedSpeeches.Count == 0 || + selectedSpeeches.Contains(morphInfo.BestTag["чр"])); + } + + public Result> GetFrequencyList(IEnumerable words) + { + var parsedWords = new Dictionary(); + foreach (var word in GetFilteredWords(words).GetValueOrThrow()) + { + parsedWords.TryAdd(word, 0); + parsedWords[word]++; + } + + return Result.Ok>(parsedWords.Select(x => new WordInfo(x.Key, x.Value)) + .OrderByDescending(info => info.Count) + .ThenBy(info => info.Word)); + } +} \ No newline at end of file diff --git a/TagsCloud/WordAnalyzer/WordAnalyzerHelper.cs b/TagsCloud/WordAnalyzer/WordAnalyzerHelper.cs new file mode 100644 index 000000000..92a5fadd4 --- /dev/null +++ b/TagsCloud/WordAnalyzer/WordAnalyzerHelper.cs @@ -0,0 +1,23 @@ +using TagsCloud.WordAnalyzer; + +namespace TagsCloud; + +public static class WordAnalyzerHelper +{ + private static readonly Dictionary converter = new() + { + { PartSpeech.Noun, "сущ" }, + { PartSpeech.Pronoun, "мест" }, + { PartSpeech.Verb, "гл" }, + { PartSpeech.Adjective, "прил" }, + { PartSpeech.Conjunction, "союз" }, + { PartSpeech.Adverb, "нареч" }, + { PartSpeech.Preposition, "предл" }, + { PartSpeech.Interjection, "межд" } + }; + + public static List GetConvertedSpeeches(IEnumerable speeches) + { + return speeches.Select(speech => converter[speech]).ToList(); + } +} \ No newline at end of file diff --git a/TagsCloud/WordAnalyzer/WordInfo.cs b/TagsCloud/WordAnalyzer/WordInfo.cs new file mode 100644 index 000000000..cbefdaefc --- /dev/null +++ b/TagsCloud/WordAnalyzer/WordInfo.cs @@ -0,0 +1,18 @@ +namespace TagsCloud.WordAnalyzer; + +public class WordInfo +{ + public readonly int Count; + public readonly string Word; + + public WordInfo(string word, int count) + { + Word = word; + Count = count; + } + + public static WordInfo Create(string word, int count) + { + return new WordInfo(word, count); + } +} \ No newline at end of file diff --git a/TagsCloud/images/flower.png b/TagsCloud/images/flower.png new file mode 100644 index 000000000..97b037e0f Binary files /dev/null and b/TagsCloud/images/flower.png differ diff --git a/TagsCloud/images/image.png b/TagsCloud/images/image.png new file mode 100644 index 000000000..33721be42 Binary files /dev/null and b/TagsCloud/images/image.png differ diff --git a/TagsCloud/images/taras.png b/TagsCloud/images/taras.png new file mode 100644 index 000000000..5aff207d9 Binary files /dev/null and b/TagsCloud/images/taras.png differ diff --git a/TagsCloudTests/AnalyzerTests.cs b/TagsCloudTests/AnalyzerTests.cs new file mode 100644 index 000000000..eae97bf13 --- /dev/null +++ b/TagsCloudTests/AnalyzerTests.cs @@ -0,0 +1,100 @@ +using FluentAssertions; +using TagsCloud; +using TagsCloud.App.Settings; +using TagsCloud.WordAnalyzer; + +namespace TagsCloudTests; + +public class AnalyzerTests +{ + private WordAnalyzer sut; + private WordAnalyzerSettings settings; + + [SetUp] + public void SetUp() + { + settings = new WordAnalyzerSettings(); + sut = new WordAnalyzer(settings); + } + + [Test] + public void WordAnalyzer_WhenEmptyCollection_ShouldBeEmpty() + { + sut.GetFrequencyList(new List()).GetValueOrThrow().Should().BeEmpty(); + } + + [Test] + public void WordAnalyzer_WhenWithSelectedPartSpeech_ShouldBeOnlySelectedPartSpeech() + { + var words = new List { "он", "плохой", "человек" }; + settings.SelectedSpeeches = new List { PartSpeech.Noun }; + sut.GetFrequencyList(words).GetValueOrThrow().Should().BeEquivalentTo(new List { new((string)"человек", (int)1) }); + } + + [Test] + public void WordAnalyzer_WhenWithSomeBoringWords_ShouldBeWithoutBoringWords() + { + var words = new List { "он", "плохой", "человек" }; + settings.BoringWords = new List { "плохой" }; + sut.GetFrequencyList(words).GetValueOrThrow().Should().BeEquivalentTo(new List { new((string)"он", (int)1), new((string)"человек", (int)1) }); + } + + [Test] + public void WordAnalyzer_WhenWithExcludedSpeeches_ShouldBeWithoutExcludedSpeeches() + { + var words = new List { "он", "плохой", "человек" }; + settings.ExcludedSpeeches = new List { PartSpeech.Noun }; + sut.GetFrequencyList(words).GetValueOrThrow().Should().BeEquivalentTo(new List { new((string)"он", (int)1), new((string)"плохой", (int)1) }); + } + + [Test] + public void WordAnalyzer_WhenDifferentCasesOfWord_ShouldBeWordInInitialForm() + { + var words = new List + { + "человек", + "человека", + "человеку", + "человека", + "человеком", + "человеке", + "читать", + "читая", + "читал", + "читала", + "читало", + "читали" + }; + sut.GetFrequencyList(words).GetValueOrThrow().Should().BeEquivalentTo(new List { new((string)"человек", (int)6), new((string)"читать", (int)6) }); + } + + [Test] + public void WordAnalyzer_WhenDifferentCountWords_ShouldBeSortedByDescending() + { + var words = new List + { + "человек", + "человека", + "человеку", + "человека", + "человеком", + "красивый", + "красивая", + "красивое", + "красивые" + }; + sut.GetFrequencyList(words).GetValueOrThrow().Should().BeInDescendingOrder(info => info.Count); + } + + [Test] + public void WordAnalyzer_WhenEqualCountWords_ShouldBeSortedByAscendingOrder() + { + var words = new List + { + "человек", + "был", + "красивым" + }; + sut.GetFrequencyList(words).GetValueOrThrow().Should().BeInAscendingOrder(info => info.Word); + } +} \ No newline at end of file diff --git a/TagsCloudTests/FileReaderTestData.cs b/TagsCloudTests/FileReaderTestData.cs new file mode 100644 index 000000000..3d63fa670 --- /dev/null +++ b/TagsCloudTests/FileReaderTestData.cs @@ -0,0 +1,23 @@ +using Spire.Doc; + +namespace TagsCloudTests; + +public static class FileReaderTestData +{ + public static IEnumerable ConstructSomeFileTypes => new[] + { + new TestCaseData("TEST.txt", FileFormat.Txt, "They \n call", new[] { "they", "call" }).SetName( + "WhenReadTxtFile"), + new TestCaseData("TEST.doc", FileFormat.Doc, "They \n call", new[] { "they", "call" }).SetName( + "WhenReadDocFile"), + new TestCaseData("TEST.docx", FileFormat.Docx, "ask \n cash \n out", new[] { "ask", "cash", "out" }).SetName( + "WhenReadDocxFile"), + new TestCaseData("litr.txt", FileFormat.Txt, "Straight from .the. hip, cut to the chase\n" + + "Play? no games, say no names", + new[] + { + "straight", "from", "the", "hip", "cut", "to", "the", "chase", "play", "no", "games", "say", "no", + "names" + }).SetName("WhenReadLiteratureText") + }; +} \ No newline at end of file diff --git a/TagsCloudTests/FileReaderTests.cs b/TagsCloudTests/FileReaderTests.cs new file mode 100644 index 000000000..93d8ec338 --- /dev/null +++ b/TagsCloudTests/FileReaderTests.cs @@ -0,0 +1,81 @@ +using System.Reflection; +using Autofac; +using FluentAssertions; +using Spire.Doc; +using TagsCloud; + +namespace TagsCloudTests; + +public class FileReaderTests +{ + private Document documentWriter; + private FileReader sut; + private List parsers; + public string FileName; + + [SetUp] + public void SetUp() + { + documentWriter = new Document(); + parsers = new List() { new DocParser(), new TxtParser(), new DocxParser() }; + sut = new FileReader(parsers); + } + + [TearDown] + public void TearDown() + { + documentWriter.Close(); + if (File.Exists(FileName)) + File.Delete(FileName); + } + + [TestCaseSource(typeof(FileReaderTestData), nameof(FileReaderTestData.ConstructSomeFileTypes))] + public void FileReader_GetWordList(string fileName, FileFormat format, string text, string[] expected) + { + FileName = fileName; + File.Create(FileName).Close(); + var paragraph1 = documentWriter.AddSection().AddParagraph(); + paragraph1.Text = text; + documentWriter.SaveToFile(FileName, FileFormat.Txt); + documentWriter.Close(); + var wordList = sut.GetWords(FileName).GetValueOrThrow().ToArray()[11..]; + wordList.Should().BeEquivalentTo(expected); + + } + + [Test] + public void FileReader_WhenReadEmptyFile_ShouldBeNotTrow() + { + FileName = "Empty.txt"; + var paragraph1 = documentWriter.AddSection(); + File.Create(FileName).Close(); + documentWriter.SaveToFile(FileName, FileFormat.Txt); + documentWriter.Close(); + var action = () => + { + var wordList = sut.GetWords(FileName); + }; + action.Should().NotThrow(); + } + [Test] + public void FileReader_WhenFileTypeParserDoesNotExist_ShouldBeTrow() + { + FileName = "NotFound.pdf"; + var textTypes = string.Join(", ", parsers.Select(x => x.FileType)); + File.Create(FileName).Close(); + documentWriter.SaveToFile(FileName, FileFormat.PDF); + var result = sut.GetWords(FileName); + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be($"К сожалению, эта программа поддерживает только файлы с расширениями: {textTypes}.\n " + + $"Попробуйте сконвертировать ваш файл с расширением {nameof(FileFormat.PDF).ToLower()} в один из указанных типов."); + } + + [Test] + public void FileReader_WhenFileDoesNotExist_ShouldBeTrow() + { + FileName = "NotFound.pdf"; + var result = sut.GetWords(FileName); + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be($"Файл по пути '{FileName}' не найден"); + } +} \ No newline at end of file diff --git a/TagsCloudTests/FlowerSpiralTests.cs b/TagsCloudTests/FlowerSpiralTests.cs new file mode 100644 index 000000000..902e34046 --- /dev/null +++ b/TagsCloudTests/FlowerSpiralTests.cs @@ -0,0 +1,49 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloud.CloudLayouter; + +namespace TagsCloudTests; + +public class FlowerSpiralTests +{ + private Point center; + + [SetUp] + public void Setup() + { + center = new Point(10, 10); + } + + private static IEnumerable ConstructorSpiralPoints => new[] + { + new TestCaseData(0, 6, 2.5f, + new Point[] { new(10, 10), new(10, 10), new(10, 10) }) + .SetName("petal Length equals 0"), + new TestCaseData(6, 0, 2.5f, + new Point[] { new(10, 10), new(10, 10), new(10, 10) }) + .SetName("petal Count equals 0"), + new TestCaseData(4, 4, 2.5f, + new Point[] { new(10, 10), new(14, 7), new(15, -8), new(0, -18), new(-15, -6), new(-3, 11) }) + .SetName("AngleStep is positive"), + new TestCaseData(4, 4, -2.5f, + new Point[] { new(10, 10), new(14, 13), new(15, 28), new(0, 38), new(-15, 26), new(-3, 9) }) + .SetName("AngleStep is negative") + }; + + [Test] + public void FlowerSpiral_WhenPetalCountAndPetalLengthLessThat0_ShouldBeThrowException() + { + Action action = () => new FlowerSpiral(-1, -1); + action.Should().Throw() + .WithMessage("petalCount or petalLength must not be less than 0"); + } + + [TestCaseSource(nameof(ConstructorSpiralPoints))] + public void FlowerSpiral_GetNextPoint_CreatePointsWithCustomAngle_ReturnsCorrectPoints(double petalLength, + int petalCount, float angleStep, + Point[] expectedPoints) + { + var spiral = new FlowerSpiral(petalLength, petalCount, angleStep); + spiral.GetPoints(center).Take(expectedPoints.Length).Should().BeEquivalentTo(expectedPoints); + } +} \ No newline at end of file diff --git a/TagsCloudTests/GlobalUsings.cs b/TagsCloudTests/GlobalUsings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/TagsCloudTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/TagsCloudTests/LayouterTests.cs b/TagsCloudTests/LayouterTests.cs new file mode 100644 index 000000000..790db1a3d --- /dev/null +++ b/TagsCloudTests/LayouterTests.cs @@ -0,0 +1,66 @@ +using System.Drawing; +using System.Drawing.Imaging; +using FluentAssertions; +using NUnit.Framework.Interfaces; +using TagsCloud; +using TagsCloud.CloudLayouter; + +namespace TagsCloudTests; + +public class LayouterTests +{ + private ISpiral spiral; + private ICloudLayouter sut; + + [SetUp] + public void SetUp() + { + spiral = new Spiral(); + sut = new CloudLayouter(spiral, new Point(10, 10)); + } + + + private static bool IsRectanglesIntersect(List rectangles) => + rectangles.Any(rectangle => rectangles.Any(nextRectangle => + nextRectangle.IntersectsWith(rectangle) && !rectangle.Equals(nextRectangle))); + + + [Test] + public void GetLocationAfterInitialization_ShouldBeEmpty() + { + var location = sut.GetRectanglesLocation(); + location.Should().BeEmpty(); + } + + [TestCase(-1, 10, TestName = "width is negative")] + [TestCase(1, -10, TestName = "height is negative")] + [TestCase(1, 0, TestName = "Zero height, correct width")] + [TestCase(0, 10, TestName = "Zero width, correct height")] + public void PutRectangleWithNegativeParams_ShouldBeThrowException(int width, int height) + { + var size = new Size(width, height); + var result = sut.PutNextRectangle(size); + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be("Sides of the rectangle should not be non-positive"); + } + + [Test] + public void PutOneRectangle_IsNotEmpty() + { + var rectangle = sut.PutNextRectangle(new Size(10, 10)); + var location = sut.GetRectanglesLocation(); + location.Should().NotBeEmpty(); + } + + [Test] + public void Put1000Rectangles_RectanglesShouldNotIntersect() + { + for (var i = 0; i < 1000; i++) + { + var size = Utils.GetRandomSize(); + var rectangle = sut.PutNextRectangle(size); + } + + IsRectanglesIntersect(sut.Rectangles).Should().BeFalse(); + } +} \ No newline at end of file diff --git a/TagsCloudTests/SpiralTests.cs b/TagsCloudTests/SpiralTests.cs new file mode 100644 index 000000000..c84272616 --- /dev/null +++ b/TagsCloudTests/SpiralTests.cs @@ -0,0 +1,42 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloud.CloudLayouter; + +namespace TagsCloudTests; + +public class SpiralTests +{ + private Point center; + + [SetUp] + public void Setup() + { + center = new Point(10, 10); + } + + private static IEnumerable ConstructorSpiralPoints => new[] + { + new TestCaseData(2.5f, + new Point[] { new(10, 10), new(5, 14), new(14, -2), new(16, 28), new(-11, -4), new(41, 8) }) + .SetName("AngleStep is positive"), + new TestCaseData(-2.5f, + new Point[] { new(10, 10), new(5, 6), new(14, 22), new(16, -8), new(-11, 24), new(41, 12) }) + .SetName("AngleStep is negative") + }; + + [Test] + public void Spiral_StepAngleEquals0_ShouldBeThrowException() + { + Action action = () => new Spiral(0); + action.Should().Throw() + .WithMessage("the step must not be equal to 0"); + } + + [TestCaseSource(nameof(ConstructorSpiralPoints))] + public void Spiral_GetNextPoint_CreatePointsWithCustomAngle_ReturnsCorrectPoints(float angleStep, + Point[] expectedPoints) + { + var spiral = new Spiral(angleStep); + spiral.GetPoints(center).Take(expectedPoints.Length).Should().BeEquivalentTo(expectedPoints); + } +} \ No newline at end of file diff --git a/TagsCloudTests/TagCloudVisualizerTest.cs b/TagsCloudTests/TagCloudVisualizerTest.cs new file mode 100644 index 000000000..1b07be4bf --- /dev/null +++ b/TagsCloudTests/TagCloudVisualizerTest.cs @@ -0,0 +1,34 @@ +using System.Drawing; +using FluentAssertions; +using TagsCloud.App.Settings; +using TagsCloud.CloudLayouter; +using TagsCloud.CloudVisualizer; +using TagsCloud.WordAnalyzer; + +namespace TagsCloudTests; + +public class TagCloudVisualizerTest +{ + [Test] + public void TagCloudVisualizer_WhenTagCloudOutOfBounds_ShouldBeTrowException() + { + var size = new Size(10, 10); + var sut = new TagCloudVisualizer(new TagSettings(), size); + var words = new List { new("hello", 5), new("gg", 6) }; + var cloudLayouter = new CloudLayouter(new Spiral(), new Point(-1, -1)); + var result = sut.DrawTags(words, Graphics.FromImage(new Bitmap(size.Width, size.Height)), + cloudLayouter); + var expectedImageSize = GetImageSize(cloudLayouter.Rectangles, TagCloudVisualizer.Border); + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be($"Облако тегов вышло за границы изображения. Поставь размер {expectedImageSize.Width}x{expectedImageSize.Height}"); + } + + private Size GetImageSize(List rectangles, int border) + { + var leftmost = Math.Min(rectangles.Min(x => x.Left), 0); + var rightmost = rectangles.Max(x => x.Right); + var topmost = Math.Min(rectangles.Min(x => x.Top), 0); + var bottommost = rectangles.Max(x => x.Bottom); + return new Size(rightmost - leftmost + border * 2, bottommost - topmost + border * 2); + } +} \ No newline at end of file diff --git a/TagsCloudTests/TagsCloudTests.csproj b/TagsCloudTests/TagsCloudTests.csproj new file mode 100644 index 000000000..5a54db322 --- /dev/null +++ b/TagsCloudTests/TagsCloudTests.csproj @@ -0,0 +1,22 @@ + + + + net6-windows + enable + enable + + false + true + + + + + + + + + + + + + diff --git a/TagsCloudTests/Utils.cs b/TagsCloudTests/Utils.cs new file mode 100644 index 000000000..6e0b9ff17 --- /dev/null +++ b/TagsCloudTests/Utils.cs @@ -0,0 +1,13 @@ +using System.Drawing; + +namespace TagsCloudTests; + +public static class Utils +{ + private static readonly Random Random = new(); + private const int MinSize = 1; + private const int MaxSize = 50; + + public static Size GetRandomSize() => + new Size(Random.Next(MinSize, MaxSize), Random.Next(MinSize, MaxSize)); +} \ No newline at end of file diff --git a/fp.sln b/fp.sln index a592ceee3..0a959f2c9 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}") = "TagsCloud", "TagsCloud\TagsCloud.csproj", "{CC7BCB08-BE8B-48FE-B2D7-5BFB3569EA82}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagsCloudTests", "TagsCloudTests\TagsCloudTests.csproj", "{24A29697-788C-482D-BBDD-D35EDDAA5307}" +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 + {CC7BCB08-BE8B-48FE-B2D7-5BFB3569EA82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC7BCB08-BE8B-48FE-B2D7-5BFB3569EA82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC7BCB08-BE8B-48FE-B2D7-5BFB3569EA82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC7BCB08-BE8B-48FE-B2D7-5BFB3569EA82}.Release|Any CPU.Build.0 = Release|Any CPU + {24A29697-788C-482D-BBDD-D35EDDAA5307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24A29697-788C-482D-BBDD-D35EDDAA5307}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24A29697-788C-482D-BBDD-D35EDDAA5307}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24A29697-788C-482D-BBDD-D35EDDAA5307}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2E64584B-4FBC-4E7D-AEF2-003E8251307A} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0}