diff --git a/ErrorHandling/ActionStatus.cs b/ErrorHandling/ActionStatus.cs new file mode 100644 index 000000000..7240396a5 --- /dev/null +++ b/ErrorHandling/ActionStatus.cs @@ -0,0 +1,17 @@ +namespace ErrorHandling; + +public readonly struct ActionStatus(string error) +{ + public string Error { get; } = error; + public bool IsSuccess => Error == null; + + public static ActionStatus Ok() + { + return new ActionStatus(null); + } + + public static ActionStatus Fail(string e) + { + return new ActionStatus(e); + } +} \ No newline at end of file diff --git a/ErrorHandling/Result.cs b/ErrorHandling/Result.cs index 2e8db379b..ca110487d 100644 --- a/ErrorHandling/Result.cs +++ b/ErrorHandling/Result.cs @@ -9,16 +9,10 @@ private None() } } -public struct Result +public readonly struct Result(string error, T value = default) { - public Result(string error, T value = default(T)) - { - Error = error; - Value = value; - } - - public string Error { get; } - internal T Value { get; } + public string Error { get; } = error; + internal T Value { get; } = value; public T GetValueOrThrow() { @@ -62,20 +56,32 @@ public static Result Then( this Result input, Func continuation) { - throw new NotImplementedException(); + return input.Then(x => Of(() => continuation(x))); } 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) return new Result(input.Error, input.Value); + handleError(input.Error); + return Fail(input.Error); + } + + public static Result ReplaceError(this Result input, Func replacement) + { + return input.IsSuccess ? input : Fail(replacement(input.Error)); + } + + public static Result RefineError(this Result input, string addition) + { + return input.ReplaceError(error => $"{addition}. {error}"); } } \ No newline at end of file diff --git a/TagCloud.GUI/Controls/DropdownList.cs b/TagCloud.GUI/Controls/DropdownList.cs new file mode 100644 index 000000000..4762d81e5 --- /dev/null +++ b/TagCloud.GUI/Controls/DropdownList.cs @@ -0,0 +1,43 @@ +namespace TagCloudGUI.Controls; + +public class DropdownList : TagCloudTableLayoutPanel +{ + private readonly TagCloudLabel _heading; + public event Action? TextHasBeenChanged; + public string SelectedText { get; private set; } + protected readonly TagCloudConfigurationForm ParentForm; + public readonly ErrorInformation ErrorMessage = new(); + + public DropdownList(string heading, IEnumerable list, TagCloudConfigurationForm parentForm) + { + ParentForm = parentForm; + _heading = new TagCloudLabel(heading); + Margin = new Padding(0, 0, 0, 8); + + DropDownList.Items.AddRange(list.ToArray()); + + ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 35)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 35)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 20)); + + Controls.Add(_heading, 0, 0); + Controls.Add(DropDownList, 0, 1); + Controls.Add(ErrorMessage, 0, 2); + + DropDownList.TextChanged += SelectionHasBeenChanged; + } + + private void SelectionHasBeenChanged(object? sender, EventArgs args) + { + SelectedText = DropDownList.Text; + TextHasBeenChanged?.Invoke(sender, args); + } + + protected ComboBox DropDownList { get; } = new() + { + Dock = DockStyle.Fill, + Margin = new Padding(0, 0, 0, 5), + Padding = new Padding(0), + }; +} \ No newline at end of file diff --git a/TagCloud.GUI/Controls/ErrorInformation.cs b/TagCloud.GUI/Controls/ErrorInformation.cs new file mode 100644 index 000000000..e48e4b708 --- /dev/null +++ b/TagCloud.GUI/Controls/ErrorInformation.cs @@ -0,0 +1,14 @@ +namespace TagCloudGUI.Controls; + +public sealed class ErrorInformation : Label +{ + public ErrorInformation() + { + ForeColor = Color.Red; + Dock = DockStyle.Fill; + Margin = new Padding(0); + Padding = new Padding(0); + BorderStyle = BorderStyle.Fixed3D; + Font = new Font(Font.FontFamily, 12, FontStyle.Bold, GraphicsUnit.Pixel); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Controls/TagCloudButton.cs b/TagCloud.GUI/Controls/TagCloudButton.cs new file mode 100644 index 000000000..f29ee5091 --- /dev/null +++ b/TagCloud.GUI/Controls/TagCloudButton.cs @@ -0,0 +1,14 @@ +namespace TagCloudGUI.Controls; + +public sealed class TagCloudButton : Button +{ + public TagCloudButton() + { + FlatStyle = FlatStyle.Flat; + Padding = new Padding(0); + Margin = new Padding(0); + Height = 40; + TextAlign = ContentAlignment.TopCenter; + Font = new Font(Font.FontFamily, 20, FontStyle.Bold, GraphicsUnit.Pixel); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Controls/TagCloudLabel.cs b/TagCloud.GUI/Controls/TagCloudLabel.cs new file mode 100644 index 000000000..ed268bf83 --- /dev/null +++ b/TagCloud.GUI/Controls/TagCloudLabel.cs @@ -0,0 +1,11 @@ +namespace TagCloudGUI.Controls; + +public sealed class TagCloudLabel : Label +{ + public TagCloudLabel(string text) + { + Text = text; + Dock = DockStyle.Fill; + Margin = new Padding(0, 0, 0, 5); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Controls/TagCloudTableLayoutPanel.cs b/TagCloud.GUI/Controls/TagCloudTableLayoutPanel.cs new file mode 100644 index 000000000..6bb5d6ff4 --- /dev/null +++ b/TagCloud.GUI/Controls/TagCloudTableLayoutPanel.cs @@ -0,0 +1,11 @@ +namespace TagCloudGUI.Controls; + +public class TagCloudTableLayoutPanel : TableLayoutPanel +{ + public TagCloudTableLayoutPanel() + { + Dock = DockStyle.Fill; + Margin = new Padding(0); + Padding = new Padding(0); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Controls/TagCloudTextBox.cs b/TagCloud.GUI/Controls/TagCloudTextBox.cs new file mode 100644 index 000000000..a0b140acc --- /dev/null +++ b/TagCloud.GUI/Controls/TagCloudTextBox.cs @@ -0,0 +1,12 @@ +namespace TagCloudGUI.Controls; + +public sealed class TagCloudTextBox : TextBox +{ + public TagCloudTextBox() + { + Dock = DockStyle.Fill; + TextAlign = HorizontalAlignment.Center; + Margin = new Padding(0); + Padding = new Padding(0); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Controls/TagCloudTreeView.cs b/TagCloud.GUI/Controls/TagCloudTreeView.cs new file mode 100644 index 000000000..2c5ceb15d --- /dev/null +++ b/TagCloud.GUI/Controls/TagCloudTreeView.cs @@ -0,0 +1,39 @@ +namespace TagCloudGUI.Controls; + +public class TagCloudTreeView : TagCloudTableLayoutPanel +{ + private readonly TagCloudLabel _heading; + + protected readonly TreeView TreeView = new() + { + Dock = DockStyle.Fill, + Margin = new Padding(0), + Padding = new Padding(0), + BorderStyle = BorderStyle.None, + CheckBoxes = true, + ShowLines = false + }; + + protected readonly TagCloudConfigurationForm ParentForm; + + public TagCloudTreeView(string heading, TagCloudConfigurationForm parentForm) + { + Dock = DockStyle.Fill; + ParentForm = parentForm; + Margin = new Padding(0, 0, 0, 8); + this._heading = new TagCloudLabel(heading); + ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 30)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 270)); + + Controls.Add(this._heading, 0, 0); + Controls.Add(TreeView, 0, 1); + } + + public IEnumerable GetSelectedValues() + { + for (var i = 0; i < TreeView.Nodes.Count; i++) + if (TreeView.Nodes[i].Checked) + yield return TreeView.Nodes[i].Text; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/FirstColumn/ColorSettings.cs b/TagCloud.GUI/Layout/FirstColumn/ColorSettings.cs new file mode 100644 index 000000000..18dc80cb3 --- /dev/null +++ b/TagCloud.GUI/Layout/FirstColumn/ColorSettings.cs @@ -0,0 +1,24 @@ +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.FirstColumn; + +public class ColorSettings : DropdownList +{ + public ColorSettings(TagCloudConfigurationForm parentForm) : base( + "Алгоритм расцветки слов:", + parentForm.VisualizationProvider.Settings.ColoringAlgorithm.NamesOfColoringAlgorithms, + parentForm) + { + DropDownList.SelectedItem = parentForm.VisualizationProvider.Settings.ColoringAlgorithm.SelectedAlgorithm; + TextHasBeenChanged += ColoringAlgorithmsIsSelected; + parentForm.VisualizationProvider.Settings.ColoringAlgorithm.ValueChanged += (_, message) => + { + ErrorMessage.Text = message; + }; + } + + private void ColoringAlgorithmsIsSelected(object? sender, EventArgs e) + { + ParentForm.VisualizationProvider.Settings.ColoringAlgorithm.SelectedAlgorithm = SelectedText; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/FirstColumn/ConfiguringCloudCompressionRatio.cs b/TagCloud.GUI/Layout/FirstColumn/ConfiguringCloudCompressionRatio.cs new file mode 100644 index 000000000..9c7543bb4 --- /dev/null +++ b/TagCloud.GUI/Layout/FirstColumn/ConfiguringCloudCompressionRatio.cs @@ -0,0 +1,57 @@ +using TagCloud.ImageGeneration; +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.FirstColumn; + +public class ConfiguringCloudCompressionRatio : TagCloudTableLayoutPanel +{ + private readonly TagCloudTextBox _coefficient = new(); + + private readonly TagCloudLabel _heading = new("Коэф. сжатия облака:") + { + TextAlign = ContentAlignment.MiddleLeft + }; + + private readonly ErrorInformation _errorMessage = new(); + + private readonly IVisualizationProvider _visualizationProvider; + + public ConfiguringCloudCompressionRatio(IVisualizationProvider visualizationProvider) + { + _visualizationProvider = visualizationProvider; + _coefficient.Text = $"{Math.Round(visualizationProvider.Settings.CompressionRatio.GetValueOrThrow(), 2)}"; + _coefficient.TextChanged += CoefficientHasChanged; + + RowStyles.Add(new RowStyle(SizeType.Absolute, 35)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 20)); + + Controls.Add(CreateControlGrid(), 0, 0); + Controls.Add(_errorMessage, 0, 1); + + visualizationProvider.Settings.CompressionRatio.ValueChanged += (_, message) => + { + _errorMessage.Text = message; + }; + } + + private TagCloudTableLayoutPanel CreateControlGrid() + { + var controlGrid = new TagCloudTableLayoutPanel + { + Margin = new Padding(0, 0, 0, 5) + }; + + controlGrid.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + controlGrid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 260)); + controlGrid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 87)); + controlGrid.Controls.Add(_heading, 0, 0); + controlGrid.Controls.Add(_coefficient, 1, 0); + + return controlGrid; + } + + private void CoefficientHasChanged(object? sender, EventArgs e) + { + _visualizationProvider.Settings.CompressionRatio.Value = _coefficient.Text; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/FirstColumn/FirstColumn.cs b/TagCloud.GUI/Layout/FirstColumn/FirstColumn.cs new file mode 100644 index 000000000..cc80202f1 --- /dev/null +++ b/TagCloud.GUI/Layout/FirstColumn/FirstColumn.cs @@ -0,0 +1,25 @@ +using TagCloud.ImageGeneration; +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.FirstColumn; + +public class FirstColumn : TagCloudTableLayoutPanel +{ + public FirstColumn(IVisualizationProvider visualizationProvider, TagCloudConfigurationForm parentForm) + { + ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 133)); + for (var i = 0; i < 4; i++) + RowStyles.Add(new RowStyle(SizeType.Absolute, 98)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 55)); + RowStyles.Add(new RowStyle(SizeType.AutoSize)); + + Controls.Add(new ImageSizeSettings(visualizationProvider), 0, 0); + Controls.Add(new FontSettings(visualizationProvider, parentForm), 0, 1); + Controls.Add(new ColorSettings(parentForm), 0, 2); + Controls.Add(new SettingUpLayoutAlgorithm(parentForm), 0, 3); + Controls.Add(new SettingTextType(parentForm), 0, 4); + Controls.Add(new ConfiguringCloudCompressionRatio(visualizationProvider), 0, 5); + Controls.Add(new Panel { Dock = DockStyle.Fill }, 0, 6); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/FirstColumn/FontSettings.cs b/TagCloud.GUI/Layout/FirstColumn/FontSettings.cs new file mode 100644 index 000000000..7d3e2a843 --- /dev/null +++ b/TagCloud.GUI/Layout/FirstColumn/FontSettings.cs @@ -0,0 +1,31 @@ +using System.Drawing.Text; +using TagCloud.ImageGeneration; +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.FirstColumn; + +public sealed class FontSettings : DropdownList +{ + private static readonly IEnumerable FontFamilies = new InstalledFontCollection().Families + .Select(family => family.Name); + + private readonly IVisualizationProvider _visualizationProvider; + + public FontSettings(IVisualizationProvider visualizationProvider, TagCloudConfigurationForm parentForm) + : base("Шрифт:", FontFamilies, parentForm) + { + Dock = DockStyle.Fill; + DropDownList.SelectedItem = visualizationProvider.Settings.FontFamily.Name; + _visualizationProvider = visualizationProvider; + TextHasBeenChanged += FontIsSelected; + visualizationProvider.Settings.FontFamily.ValueChanged += (_, message) => + { + ErrorMessage.Text = message; + }; + } + + private void FontIsSelected(object? sender, EventArgs args) + { + _visualizationProvider.Settings.FontFamily.Name = SelectedText; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/FirstColumn/ImageSizeSettings.cs b/TagCloud.GUI/Layout/FirstColumn/ImageSizeSettings.cs new file mode 100644 index 000000000..efce9b27f --- /dev/null +++ b/TagCloud.GUI/Layout/FirstColumn/ImageSizeSettings.cs @@ -0,0 +1,79 @@ +using TagCloud.ImageGeneration; +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.FirstColumn; + +public class ImageSizeSettings : TagCloudTableLayoutPanel +{ + private static readonly Padding ItemsMargin = new(0, 0, 0, 5); + + private readonly TagCloudLabel _heightLabel = new("Высота:"); + + private readonly TagCloudTextBox _heightTextBox = new(); + + private readonly TagCloudLabel _heightUnitsOfMeasurement = new("px."); + + private readonly IVisualizationProvider _visualizationProvider; + + private readonly TagCloudLabel _widthLabel = new("Ширина:") { Margin = ItemsMargin }; + + private readonly TagCloudTextBox _widthTextBox = new(); + + private readonly TagCloudLabel _widthUnitsOfMeasurement = new("px.") { Margin = ItemsMargin }; + + private readonly ErrorInformation _errorMessage = new(); + public TagCloudLabel Heading { get; } = new("Размеры изображения:"); + + public ImageSizeSettings(IVisualizationProvider visualizationProvider) + { + Margin = new Padding(0, 0, 0, 8); + _widthTextBox.Text = visualizationProvider.Settings.ImageSize.Width.ToString(); + _heightTextBox.Text = visualizationProvider.Settings.ImageSize.Height.ToString(); + this._visualizationProvider = visualizationProvider; + RowStyles.Add(new RowStyle(SizeType.Absolute, 35)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 70)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 20)); + ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + + Controls.Add(Heading, 0, 0); + Controls.Add(CreateNestedTable(), 0, 1); + Controls.Add(_errorMessage, 0, 2); + + _heightTextBox.TextChanged += HeightHasChanged; + _widthTextBox.TextChanged += WidthHasChanged; + visualizationProvider.Settings.ImageSize.ValueChanged += (_, message) => + { + _errorMessage.Text = message; + }; + } + + private void WidthHasChanged(object? sender, EventArgs args) + { + _visualizationProvider.Settings.ImageSize.Width = _widthTextBox.Text; + } + + private void HeightHasChanged(object? sender, EventArgs args) + { + _visualizationProvider.Settings.ImageSize.Height = _heightTextBox.Text; + } + + private TagCloudTableLayoutPanel CreateNestedTable() + { + var table = new TagCloudTableLayoutPanel { Margin = new Padding(0, 0, 0, 5)}; + table.RowStyles.Add(new RowStyle(SizeType.Absolute, 35)); + table.RowStyles.Add(new RowStyle(SizeType.Absolute, 30)); + table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 115)); + table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 95)); + table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 66)); + + table.Controls.Add(_widthLabel, 0, 0); + table.Controls.Add(_widthTextBox, 1, 0); + table.Controls.Add(_widthUnitsOfMeasurement, 2, 0); + + table.Controls.Add(_heightLabel, 0, 1); + table.Controls.Add(_heightTextBox, 1, 1); + table.Controls.Add(_heightUnitsOfMeasurement, 2, 1); + + return table; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/FirstColumn/SettingTextType.cs b/TagCloud.GUI/Layout/FirstColumn/SettingTextType.cs new file mode 100644 index 000000000..62137a1de --- /dev/null +++ b/TagCloud.GUI/Layout/FirstColumn/SettingTextType.cs @@ -0,0 +1,25 @@ +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.FirstColumn; + +public sealed class SettingTextType : DropdownList +{ + public SettingTextType(TagCloudConfigurationForm parentForm) : base( + "Структура содержания файла", + parentForm.ParserProvider.GetTypesParsers, + parentForm) + { + Dock = DockStyle.Fill; + DropDownList.SelectedText = parentForm.ParserProvider.SlectedParser; + TextHasBeenChanged += TextTypeIsSelected; + ParentForm.ParserProvider.ValueChanged += (_, message) => + { + ErrorMessage.Text = message; + }; + } + + private void TextTypeIsSelected(object? sender, EventArgs e) + { + var status = ParentForm.ParserProvider.SlectedParser = SelectedText; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/FirstColumn/SettingUpLayoutAlgorithm.cs b/TagCloud.GUI/Layout/FirstColumn/SettingUpLayoutAlgorithm.cs new file mode 100644 index 000000000..34437ffe5 --- /dev/null +++ b/TagCloud.GUI/Layout/FirstColumn/SettingUpLayoutAlgorithm.cs @@ -0,0 +1,24 @@ +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.FirstColumn; + +public class SettingUpLayoutAlgorithm : DropdownList +{ + public SettingUpLayoutAlgorithm(TagCloudConfigurationForm parentForm) : base( + "Алгоритм генерации раскладки:", + parentForm.VisualizationProvider.Settings.LayoutAlgorithm.NamesOfLayoutAlgorithms, + parentForm) + { + DropDownList.SelectedItem = parentForm.VisualizationProvider.Settings.LayoutAlgorithm.SelectedAlgorithm; + TextHasBeenChanged += LayoutProviderIsSelected; + parentForm.VisualizationProvider.Settings.LayoutAlgorithm.ValueChanged += (_, message) => + { + ErrorMessage.Text = message; + }; + } + + private void LayoutProviderIsSelected(object? sender, EventArgs e) + { + ParentForm.VisualizationProvider.Settings.LayoutAlgorithm.SelectedAlgorithm = SelectedText; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/PushButtonPanel.cs b/TagCloud.GUI/Layout/PushButtonPanel.cs new file mode 100644 index 000000000..d7c0fe7b7 --- /dev/null +++ b/TagCloud.GUI/Layout/PushButtonPanel.cs @@ -0,0 +1,112 @@ +using TagCloud; +using TagCloud.ImageGeneration; +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout; + +public sealed class PushButtonPanel : TagCloudTableLayoutPanel +{ + public static event Action? SetupIsFinished; + public static event Action? TextIsUploaded; + + private readonly TagCloudButton _cloudGenerationButton = new() + { + Text = "Сгенерировать", + Width = 220 + }; + + private readonly TagCloudConfigurationForm _parentForm; + + private readonly TagCloudButton _textUploadTagCloudButton = new() + { + Text = "Загрузить текст", + Width = 230 + }; + + private readonly IVisualizationProvider _visualizationProvider; + + public PushButtonPanel(IVisualizationProvider visualizationProvider, TagCloudConfigurationForm parentForm) + { + this._parentForm = parentForm; + _visualizationProvider = visualizationProvider; + + ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 200)); + ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 190)); + + Controls.Add(_textUploadTagCloudButton, 0, 0); + Controls.Add(new Panel { Dock = DockStyle.Fill }, 1, 0); + Controls.Add(_cloudGenerationButton, 2, 0); + + _textUploadTagCloudButton.Click += SelectFile; + _cloudGenerationButton.Click += GenerateImage; + _cloudGenerationButton.Enabled = false; + _textUploadTagCloudButton.Enabled = parentForm.ParserProvider.IsCorrect; + + parentForm.ParserProvider.ValueChanged += ContentTypeChanged; + _visualizationProvider.Settings.ValueChanged += SettingsCorrectnessStatusChanged; + } + + private void SettingsCorrectnessStatusChanged(ICrrectnessChecker settings, string _) + { + _cloudGenerationButton.Enabled = settings.IsCorrect; + } + + private void ContentTypeChanged(ICrrectnessChecker parserProvider, string __) + { + _textUploadTagCloudButton.Enabled = parserProvider.IsCorrect; + } + + private void SelectFile(object? sender, EventArgs e) + { + var extensions = _parentForm.ReaderProvider.GetSupportedExtensions() + .Select(extension => $"*{extension}") + .ToArray(); + var openFileDialog = new OpenFileDialog + { + Filter = "(" + string.Join(", ", extensions) + ")|" + string.Join(";", extensions), + RestoreDirectory = true + }; + + if (openFileDialog.ShowDialog() != DialogResult.OK) return; + var filePath = openFileDialog.FileName; + var requestResult = _parentForm.ReaderProvider.GetReader(filePath); + if (!requestResult.IsSuccess) + { + SecondColumn.SecondColumn.ErrorMessage.Text = requestResult.Error; + return; + } + var reader = requestResult.GetValueOrThrow(); + var parser = _parentForm.ParserProvider.GetParser(); + var status = parser(reader); + if (status.IsSuccess) + { + _visualizationProvider.Settings.WordsList.Value = status.GetValueOrThrow(); + TextIsUploaded?.Invoke(); + } + SecondColumn.SecondColumn.ErrorMessage.Text = status.Error; + } + + private void GenerateImage(object? sender, EventArgs e) + { + var filePath = GetPathToSave(); + if (filePath == null) return; + + SetupIsFinished?.Invoke(); + + var status = _visualizationProvider.CreateImage(); + if (status.IsSuccess) + status.GetValueOrThrow().Save(filePath); + SecondColumn.SecondColumn.ErrorMessage.Text = status.Error; + } + + private string? GetPathToSave() + { + var saveFileDialog = new SaveFileDialog(); + saveFileDialog.Filter = "Изображение (*.png, *.jpeg, *.bmp)|*.png;*.jpeg;*.bmp"; + saveFileDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + saveFileDialog.Title = "Сохранение файла"; + + return saveFileDialog.ShowDialog() == DialogResult.OK ? saveFileDialog.FileName : null; + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/SecondColumn/PartOfSpeechFilter.cs b/TagCloud.GUI/Layout/SecondColumn/PartOfSpeechFilter.cs new file mode 100644 index 000000000..1347bcf21 --- /dev/null +++ b/TagCloud.GUI/Layout/SecondColumn/PartOfSpeechFilter.cs @@ -0,0 +1,23 @@ +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.SecondColumn; + +public class PartOfSpeechFilter : TagCloudTreeView +{ + public PartOfSpeechFilter(TagCloudConfigurationForm parentForm) + : base("Исключить части речи:", parentForm) + { + Margin = new Padding(0, 0, 0, 8); + PushButtonPanel.TextIsUploaded += FillTreeView; + } + + private void FillTreeView() + { + var partsOfSpeech = ParentForm.VisualizationProvider.Settings.WordsList.Value + .Select(wordInfo => wordInfo.PartOfSpeach) + .ToHashSet(); + TreeView.Nodes.Clear(); + foreach (var partOfSpeech in partsOfSpeech) + TreeView.Nodes.Add(partOfSpeech); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/SecondColumn/SecondColumn.cs b/TagCloud.GUI/Layout/SecondColumn/SecondColumn.cs new file mode 100644 index 000000000..0b3111cce --- /dev/null +++ b/TagCloud.GUI/Layout/SecondColumn/SecondColumn.cs @@ -0,0 +1,37 @@ +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.SecondColumn; + +public sealed class SecondColumn : TagCloudTableLayoutPanel +{ + private readonly TagCloudConfigurationForm _parentForm; + private readonly PartOfSpeechFilter _partOfSpeechFilter; + private readonly WordFilter _wordFilter; + + public static readonly ErrorInformation ErrorMessage = new(); + + public SecondColumn(TagCloudConfigurationForm parentForm) + { + _parentForm = parentForm; + _wordFilter = new WordFilter(parentForm); + _partOfSpeechFilter = new PartOfSpeechFilter(parentForm); + + ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 316)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 324)); + RowStyles.Add(new RowStyle(SizeType.Absolute, 44)); + Controls.Add(_partOfSpeechFilter, 0, 0); + Controls.Add(_wordFilter, 0, 1); + Controls.Add(ErrorMessage, 0, 2); + + PushButtonPanel.SetupIsFinished += FilterData; + } + + private void FilterData() + { + var excludedWords = _wordFilter.GetSelectedValues().ToHashSet(); + var excludedPartsOfSpeech = _partOfSpeechFilter.GetSelectedValues().ToHashSet(); + _parentForm.VisualizationProvider.Settings.WordsList.Value = _parentForm.VisualizationProvider.Settings.WordsList.Value? + .Where(word => !excludedPartsOfSpeech.Contains(word.PartOfSpeach) && !excludedWords.Contains(word.Word)); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/SecondColumn/WordFilter.cs b/TagCloud.GUI/Layout/SecondColumn/WordFilter.cs new file mode 100644 index 000000000..1ca1137b8 --- /dev/null +++ b/TagCloud.GUI/Layout/SecondColumn/WordFilter.cs @@ -0,0 +1,21 @@ +using TagCloudGUI.Controls; + +namespace TagCloudGUI.Layout.SecondColumn; + +public class WordFilter : TagCloudTreeView +{ + public WordFilter(TagCloudConfigurationForm parentForm) + : base("Исключить слова:", parentForm) + { + PushButtonPanel.TextIsUploaded += FillTreeView; + } + + private void FillTreeView() + { + var words = ParentForm.VisualizationProvider.Settings.WordsList.Value + .Select(wordInfo => wordInfo.Word); + TreeView.Nodes.Clear(); + foreach (var word in words) + TreeView.Nodes.Add(word); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Layout/SettingsTable.cs b/TagCloud.GUI/Layout/SettingsTable.cs new file mode 100644 index 000000000..af682be9c --- /dev/null +++ b/TagCloud.GUI/Layout/SettingsTable.cs @@ -0,0 +1,20 @@ +using TagCloud.ImageGeneration; +using TagCloudGUI.Controls; +using TagCloudGUI.Layout.FirstColumn; + +namespace TagCloudGUI.Layout; + +public class SettingsTable : TagCloudTableLayoutPanel +{ + public SettingsTable(IVisualizationProvider visualizationProvider, TagCloudConfigurationForm parentForm, SettingTextType textType) + { + RowStyles.Add(new RowStyle(SizeType.AutoSize)); + ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 348)); + ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 49)); + ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 523)); + + Controls.Add(new FirstColumn.FirstColumn(visualizationProvider, parentForm), 0, 0); + Controls.Add(new Panel { Dock = DockStyle.Fill }, 1, 0); + Controls.Add(new SecondColumn.SecondColumn(parentForm), 2, 0); + } +} \ No newline at end of file diff --git a/TagCloud.GUI/Program.cs b/TagCloud.GUI/Program.cs new file mode 100644 index 000000000..783f4cb89 --- /dev/null +++ b/TagCloud.GUI/Program.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using TagCloud.ImageGeneration; +using TagCloud.ImageGeneration.Settings; +using TagCloud.ImageGeneration.Settings.DTO; +using TagCloud.Parsing; +using TagCloud.ReadingFiles; + +namespace TagCloudGUI; + +internal static class Program +{ + [STAThread] + private static void Main() + { + ApplicationConfiguration.Initialize(); + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + + var partialSupplier = services.BuildServiceProvider(); + services.AddSingleton(_ => new RenderingSettings( + new ImageSizeDto(1080, 1080), + new FontFamilyDto("Arial"), + new CompressionRatioDto(2f), + partialSupplier.GetService(), + partialSupplier.GetService(), + new WordsListDto())); + + var provider = services.BuildServiceProvider(); + var form = provider.GetService
(); + Application.Run(form); + } +} diff --git a/TagCloud.GUI/TagCloud.GUI.csproj b/TagCloud.GUI/TagCloud.GUI.csproj new file mode 100644 index 000000000..29ccc6440 --- /dev/null +++ b/TagCloud.GUI/TagCloud.GUI.csproj @@ -0,0 +1,19 @@ + + + + WinExe + net8.0-windows + enable + true + enable + TagCloudGUI + + + + + + + + + + diff --git a/TagCloud.GUI/TagCloudConfigurationForm.Designer.cs b/TagCloud.GUI/TagCloudConfigurationForm.Designer.cs new file mode 100644 index 000000000..a06d938b0 --- /dev/null +++ b/TagCloud.GUI/TagCloudConfigurationForm.Designer.cs @@ -0,0 +1,42 @@ +namespace TagCloudGUI; + +partial class TagCloudConfigurationForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.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; + MaximizeBox = false; + Size = new Size(1018, 847); + FormBorderStyle = FormBorderStyle.FixedSingle; + Padding = new Padding(40, 35, 40, 35); + Text = "Облака тегов"; + } + + #endregion +} \ No newline at end of file diff --git a/TagCloud.GUI/TagCloudConfigurationForm.cs b/TagCloud.GUI/TagCloudConfigurationForm.cs new file mode 100644 index 000000000..0441e30d7 --- /dev/null +++ b/TagCloud.GUI/TagCloudConfigurationForm.cs @@ -0,0 +1,43 @@ +using TagCloud.ImageGeneration; +using TagCloud.Parsing; +using TagCloud.ReadingFiles; +using TagCloudGUI.Controls; +using TagCloudGUI.Layout; +using TagCloudGUI.Layout.FirstColumn; + +namespace TagCloudGUI; + +public partial class TagCloudConfigurationForm : Form +{ + public IParserProvider ParserProvider { get; } + public IReaderProvider ReaderProvider { get; } + public IVisualizationProvider VisualizationProvider { get; } + + public TagCloudConfigurationForm( + IParserProvider parserProvider, + IReaderProvider readerProvider, + IVisualizationProvider visualizationProvider) + { + InitializeComponent(); + ParserProvider = parserProvider; + ReaderProvider = readerProvider; + VisualizationProvider = visualizationProvider; + Font = new Font("Arial", 20, FontStyle.Regular, GraphicsUnit.Pixel); + var table = new TagCloudTableLayoutPanel(); + table.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + table.RowStyles.Add(new RowStyle(SizeType.Absolute, 676)); + table.RowStyles.Add(new RowStyle(SizeType.Absolute, 14)); + table.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); + + var textTypeSettings = new SettingTextType(this); + table.Controls.Add( + new SettingsTable(visualizationProvider, this, textTypeSettings) + { + Dock = DockStyle.Fill + }, 0, 0); + table.Controls.Add(new Panel { Dock = DockStyle.Fill }, 0, 1); + table.Controls.Add(new PushButtonPanel(visualizationProvider, this), 0, 2); + + Controls.Add(table); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/CircularCloudTests.cs b/TagCloud.Tests/CircularCloudTests.cs new file mode 100644 index 000000000..414ce0c64 --- /dev/null +++ b/TagCloud.Tests/CircularCloudTests.cs @@ -0,0 +1,73 @@ +using System.Drawing; +using FluentAssertions; +using FluentAssertions.Extensions; +using TagCloud.ImageGeneration; + +namespace TagCloud.Tests; + +[TestFixture] +public class CircularCloudTests +{ + private readonly List _listRectangles = []; + + [Test] + public void CircularCloud_CorrectInitialization_NoExceptions() + { + var createAConstructor = () => new CircularCloud(); + + createAConstructor + .Should() + .NotThrow(); + } + + [Test] + public void PutNextRectangle_RandomSizes_MustBeRightSize() + { + var random = new Random(); + var placemarker = new CircularCloud(); + + for (var i = 0; i < 50; i++) + { + var width = random.Next(30, 200); + var actualSize = new SizeF(width, random.Next(width / 6, width / 3)); + + var rectangle = placemarker.PutNextRectangle(actualSize); + + actualSize.Should().Be(rectangle.Size); + } + } + + [Test] + public void PutNextRectangle_RandomSizes_ShouldNotIntersect() + { + var random = new Random(); + var placemarker = new CircularCloud(); + + for (var i = 0; i < 100; i++) + { + var width = random.Next(30, 200); + + var rectangle = placemarker.PutNextRectangle(new SizeF(width, random.Next(width / 6, width / 3))); + + _listRectangles.Any(rect => rect.IntersectsWith(rectangle)) + .Should() + .BeFalse("Прямоугольники не должны пересекаться"); + + _listRectangles.Add(rectangle); + } + } + + [Test] + public void PutNextRectangle_DegenerateRectangle_ShouldNotGoIntoEndlessLoop() + { + var call = () => + { + var placemarker = new CircularCloud(); + placemarker.PutNextRectangle(new SizeF(10, 50)); + placemarker.PutNextRectangle(new SizeF(0, 50)); + placemarker.PutNextRectangle(new SizeF(10, 0)); + }; + + call.ExecutionTime().Should().BeLessThanOrEqualTo(3000.Milliseconds()); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/GeneratingTestData.cs b/TagCloud.Tests/GeneratingTestData.cs new file mode 100644 index 000000000..57226a872 --- /dev/null +++ b/TagCloud.Tests/GeneratingTestData.cs @@ -0,0 +1,35 @@ +using System.Collections.Immutable; + +namespace TagCloud.Tests; + +public class GeneratingTestData +{ + public static readonly ImmutableDictionary FrequencyDictionary = ImmutableDictionary + .CreateRange(new KeyValuePair[] + { + new("привет", 5), + new("морозный", 7), + new("быстрый", 3), + new("я", 20), + new("человек", 2), + new("отчаянно", 8) + }); + + public TItems[] Shuffle(TItems[] source) + { + var lines = source.ToArray(); + new Random().Shuffle(lines); + + return lines; + } + + public string[] CreateArrayOfWords(IDictionary frequencyDictionary) + { + var list = new List(); + foreach (var pair in frequencyDictionary) + for (var i = 0; i < pair.Value; i++) + list.Add(pair.Key); + + return list.ToArray(); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/Images/(1080x1080)CheckingCount.bmp b/TagCloud.Tests/Images/(1080x1080)CheckingCount.bmp new file mode 100644 index 000000000..6ac4c7d99 Binary files /dev/null and b/TagCloud.Tests/Images/(1080x1080)CheckingCount.bmp differ diff --git a/TagCloud.Tests/Images/(1080x1080)GeeseAndSwans.png b/TagCloud.Tests/Images/(1080x1080)GeeseAndSwans.png new file mode 100644 index 000000000..5e5f94502 Binary files /dev/null and b/TagCloud.Tests/Images/(1080x1080)GeeseAndSwans.png differ diff --git a/TagCloud.Tests/Images/(1080x1080)Morozko.jpeg b/TagCloud.Tests/Images/(1080x1080)Morozko.jpeg new file mode 100644 index 000000000..58d674cc6 Binary files /dev/null and b/TagCloud.Tests/Images/(1080x1080)Morozko.jpeg differ diff --git a/TagCloud.Tests/Images/(1280x720)CheckingCount.bmp b/TagCloud.Tests/Images/(1280x720)CheckingCount.bmp new file mode 100644 index 000000000..1c0b16c15 Binary files /dev/null and b/TagCloud.Tests/Images/(1280x720)CheckingCount.bmp differ diff --git a/TagCloud.Tests/Images/(1280x720)GeeseAndSwans.png b/TagCloud.Tests/Images/(1280x720)GeeseAndSwans.png new file mode 100644 index 000000000..a23e3226b Binary files /dev/null and b/TagCloud.Tests/Images/(1280x720)GeeseAndSwans.png differ diff --git a/TagCloud.Tests/Images/(1280x720)Morozko.jpeg b/TagCloud.Tests/Images/(1280x720)Morozko.jpeg new file mode 100644 index 000000000..a1e3cbaba Binary files /dev/null and b/TagCloud.Tests/Images/(1280x720)Morozko.jpeg differ diff --git a/TagCloud.Tests/LiteraryTextParserTests.cs b/TagCloud.Tests/LiteraryTextParserTests.cs new file mode 100644 index 000000000..87c1e4685 --- /dev/null +++ b/TagCloud.Tests/LiteraryTextParserTests.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using FluentAssertions; +using TagCloud.Parsing; + +namespace TagCloud.Tests; + +[TestFixture] +public class LiteraryTextParserTests +{ + private readonly LiteraryTextParser _literaryTextParser = new(); + private ImmutableArray _testLines; + + private readonly HashSet _russianAlphabet = []; + + [SetUp] + public void SetUp() + { + for (var symbol = 'а'; symbol <= 'я'; symbol++) + _russianAlphabet.Add(symbol); + _russianAlphabet.Add('ё'); + + var generator = new GeneratingTestData(); + _testLines = [..generator.Shuffle(generator.CreateArrayOfWords(GeneratingTestData.FrequencyDictionary))]; + } + + [TestCase("привет", "ПрИвЕт", "Привет", "ПРИВЕТ")] + public void PerformPreprocessing_Text_AllCharactersMustBeInLowercase(params string[] lines) + { + var result = _literaryTextParser.Parse(() => lines); + + result.IsSuccess.Should().BeTrue(); + CheckCharactersOfWords(result.GetValueOrThrow(), symbol => char.IsLower(symbol) || symbol == '-'); + } + + [TestCase("python?", "java!", "C#", "языки-", "программирования", "пriveт", "из-за")] + public void PerformPreprocessing_Text_OnlyRussianLettersShouldRemainInWords(params string[] lines) + { + var result = _literaryTextParser.Parse(() => lines); + + result.IsSuccess.Should().BeTrue(); + CheckCharactersOfWords(result.GetValueOrThrow(), symbol => _russianAlphabet.Contains(symbol) || symbol == '-'); + } + + private void CheckCharactersOfWords(IEnumerable words, Func check) + { + foreach (var wordInfo in words) + wordInfo.Word.All(check).Should().BeTrue(); + } + + [Test] + public void PerformPreprocessing_Text_CorrectWordCount() + { + var result = _literaryTextParser.Parse(() => _testLines); + + result.IsSuccess.Should().BeTrue(); + result.GetValueOrThrow() + .All(wordInfo => GeneratingTestData.FrequencyDictionary[wordInfo.Word] == wordInfo.NumberInText) + .Should() + .BeTrue(); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/ReaderPickerTests.cs b/TagCloud.Tests/ReaderPickerTests.cs new file mode 100644 index 000000000..29ad730fc --- /dev/null +++ b/TagCloud.Tests/ReaderPickerTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using TagCloud.ReadingFiles; + +namespace TagCloud.Tests; + +[TestFixture] +public class ReaderPickerTests +{ + private readonly ReaderPicker _readerProvider = new([ new TxtReader() ]); + private readonly string _pathToFileFolder = Path.Combine(Directory.GetCurrentDirectory(), "TestsFiles"); + + [Test] + public void Constructor_Null_ThrowArgumentNullException() + { + var initialization = () => new ReaderPicker(null); + + initialization.Should().Throw(); + } + + [Test] + public void GetSupportedExtensions_CorrectNumberOfFormats() + { + var expected = new List { ".txt" }; + + var actual = _readerProvider.GetSupportedExtensions(); + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void GetReader_PngFile_ThrowException() + { + var path = Path.Combine(_pathToFileFolder, "Morozko.png"); + + var status = _readerProvider.GetReader(path); + + status.IsSuccess.Should().BeFalse(); + } + + [Test] + public void GetReader_CorrectFileExtension_NotThrow() + { + var path = Path.Combine(_pathToFileFolder, "Morozko.txt"); + + var call = () => _readerProvider.GetReader(path); + + call.Should().NotThrow(); + } + + [Test] + public void GetReader_CorrectPath_СorrectFunction() + { + var reader = new TxtReader(); + var path = Path.Combine(_pathToFileFolder, "Morozko.txt"); + var expected = () => reader.ReadTextLineByLine(path); + + var actual = _readerProvider.GetReader(path).GetValueOrThrow(); + + actual().Should().BeEquivalentTo(expected()); + } + + [Test] + public void GetReader_UnExistingFile_ThrowFileNotFoundException() + { + var status = _readerProvider.GetReader("UnExistingFile.txt"); + + status.IsSuccess.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/SettingsTests/CompressionRatioDtoTests.cs b/TagCloud.Tests/SettingsTests/CompressionRatioDtoTests.cs new file mode 100644 index 000000000..b4d0bcb55 --- /dev/null +++ b/TagCloud.Tests/SettingsTests/CompressionRatioDtoTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using TagCloud.ImageGeneration.Settings.DTO; + +namespace TagCloud.Tests.SettingsTests; + +[TestFixture] +public class CompressionRatioDtoTests +{ + private readonly CompressionRatioDto _coefficient = new(5); + + [TestCase(-1)] + [TestCase(11)] + public void CompressionRatioDto_InvalidValueDuringInitialization_ThrowException(float coefficient) + { + var action = () => new CompressionRatioDto(coefficient); + + action.Should().Throw(); + } + + [TestCase(0.1f)] + [TestCase(10)] + [TestCase(3)] + public void CompressionRatioDto_CorrectInitialization_ThrowException(float coefficient) + { + var action = () => new CompressionRatioDto(coefficient); + + action.Should().NotThrow(); + } + + [TestCase(-1)] + [TestCase(11)] + public void GetValueOrThrow_IncorrectValue_ThrowException(float coefficient) + { + _coefficient.Value = coefficient.ToString(); + var action = () => _coefficient.GetValueOrThrow(); + + action.Should().Throw(); + } + + [TestCase(0.1f)] + [TestCase(10)] + [TestCase(7)] + public void GetValueOrThrow_CorrectValue_NotThrowException(float coefficient) + { + _coefficient.Value = coefficient.ToString(); + var action = () => _coefficient.GetValueOrThrow(); + + action.Should().NotThrow(); + } + + [TestCase(0)] + [TestCase(-1000f)] + [TestCase(1000f)] + [TestCase(10.01f)] + public void CompressionRatioDto_ChangeToIncorrectValue(float coefficient) + { + _coefficient.Value = coefficient.ToString(); + + _coefficient.IsCorrect.Should().BeFalse(); + _coefficient.Value.Should().Be(coefficient.ToString()); + } + + [TestCase(2)] + [TestCase(10f)] + [TestCase(0.1f)] + public void CompressionRatioDto_ChangeToCorrectValue(float coefficient) + { + _coefficient.Value = coefficient.ToString(); + + _coefficient.IsCorrect.Should().BeTrue(); + _coefficient.Value.Should().Be(coefficient.ToString()); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/SettingsTests/FontFamilyDtoTests.cs b/TagCloud.Tests/SettingsTests/FontFamilyDtoTests.cs new file mode 100644 index 000000000..8981356d9 --- /dev/null +++ b/TagCloud.Tests/SettingsTests/FontFamilyDtoTests.cs @@ -0,0 +1,80 @@ +using FluentAssertions; +using TagCloud.ImageGeneration.Settings.DTO; + +namespace TagCloud.Tests.SettingsTests; + +[TestFixture] +public class FontFamilyDtoTests +{ + private readonly FontFamilyDto _fontFamily = new("Calibri"); + + [TestCase("Arial")] + [TestCase("Calibri")] + public void FontFamilyDto_CorrectInitialization_NotThrowsException(string fontName) + { + var action = () => new FontFamilyDto(fontName); + + action.Should().NotThrow(); + } + + [Test] + public void FontFamilyDto_NullDuringInitialization_ThrowException() + { + var action = () => new FontFamilyDto(null); + + action.Should().Throw(); + } + + [TestCase("")] + public void FontFamilyDto_EmptyValueDuringInitialization_ThrowException(string fontName) + { + var action = () => new FontFamilyDto(fontName); + + action.Should().Throw(); + } + + [TestCase("ms")] + public void FontFamilyDto_NonExistentFontDuringInitialization_ThrowException(string fontName) + { + var action = () => new FontFamilyDto(fontName); + + action.Should().Throw(); + } + + [TestCase("ms")] + public void FontFamilyDto_ChangingToNonExistentFont(string fontName) + { + _fontFamily.Name = fontName; + + _fontFamily.IsCorrect.Should().BeFalse(); + _fontFamily.Name.Should().Be(fontName); + } + + [TestCase("Arial")] + public void FontFamilyDto_ChangingToSystemFont(string fontName) + { + _fontFamily.Name = fontName; + + _fontFamily.IsCorrect.Should().BeTrue(); + _fontFamily.Name.Should().Be(fontName); + } + + [TestCase("ms")] + public void GetValueOrThrow_NonExistentFont_ThrowException(string fontName) + { + _fontFamily.Name = fontName; + var action = () => _fontFamily.GetValueOrThrow(); + + action.Should().Throw(); + } + + [TestCase("Arial")] + [TestCase("Calibri")] + public void GetValueOrThrow_SystemFont_NotThrowException(string fontName) + { + _fontFamily.Name = fontName; + var action = () => _fontFamily.GetValueOrThrow(); + + action.Should().NotThrow(); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/SettingsTests/ImageSizeDtoTests.cs b/TagCloud.Tests/SettingsTests/ImageSizeDtoTests.cs new file mode 100644 index 000000000..fff2518f6 --- /dev/null +++ b/TagCloud.Tests/SettingsTests/ImageSizeDtoTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using TagCloud.ImageGeneration.Settings.DTO; + +namespace TagCloud.Tests.SettingsTests; + +[TestFixture] +public class ImageSizeDtoTests +{ + private readonly ImageSizeDto _imageSize = new(1080, 1080); + + [TestCase("540", "200")] + [TestCase("100", "1080")] + public void ImageSizeDto_PositiveValues(string width, string height) + { + _imageSize.Width = width; + _imageSize.Height = height; + + _imageSize.IsCorrect.Should().BeTrue(); + _imageSize.Width.Should().Be(width); + _imageSize.Height.Should().Be(height); + } + + [TestCase("-100", "100")] + [TestCase("100", "-100")] + [TestCase("0", "100")] + [TestCase("100", "0")] + public void ImageSizeDto_NotPositiveValues_ValueMustBeIncorrect(string width, string height) + { + CheckForInaccuracy(width, height); + } + + [TestCase("hmm", "100")] + [TestCase("100", "hmm")] + [TestCase("hmm", "nnh")] + + public void ImageSizeDto_NotNumber_ValueMustBeIncorrect(string width, string height) + { + CheckForInaccuracy(width, height); + } + + [TestCase("1080", "1920")] + public void GetValueOrThrow_CorrectValue_NotThrowException(string width, string height) + { + _imageSize.Width = width; + _imageSize.Height = height; + var action = () => _imageSize.GetValueOrThrow(); + + action.Should().NotThrow(); + } + + [TestCase("1920", "-1")] + [TestCase("-1", "1920")] + [TestCase("1080", "hh")] + [TestCase("-h", "1080")] + public void GetValueOrThrow_IncorrectValue_ThrowException(string width, string height) + { + _imageSize.Width = width; + _imageSize.Height = height; + var action = () => _imageSize.GetValueOrThrow(); + + action.Should().Throw(); + } + + private void CheckForInaccuracy(string width, string height) + { + _imageSize.Width = width; + _imageSize.Height = height; + + _imageSize.IsCorrect.Should().BeFalse(); + _imageSize.Width.Should().Be(width); + _imageSize.Height.Should().Be(height); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/TagCloud.Tests.csproj b/TagCloud.Tests/TagCloud.Tests.csproj new file mode 100644 index 000000000..289be27f9 --- /dev/null +++ b/TagCloud.Tests/TagCloud.Tests.csproj @@ -0,0 +1,52 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + diff --git a/TagCloud.Tests/TestsFiles/CheckingCount.txt b/TagCloud.Tests/TestsFiles/CheckingCount.txt new file mode 100644 index 000000000..2fb170c6b --- /dev/null +++ b/TagCloud.Tests/TestsFiles/CheckingCount.txt @@ -0,0 +1,45 @@ +я +я +я +отчаянно +я +человек +я +отчаянно +отчаянно +я +я +я +я +морозный +морозный +привет +отчаянно +я +я +я +я +быстрый +я +морозный +я +быстрый +привет +морозный +я +человек +морозный +отчаянно +я +привет +отчаянно +быстрый +я +отчаянно +морозный +я +отчаянно +привет +морозный +я +привет diff --git a/TagCloud.Tests/TestsFiles/EmptyFile.txt b/TagCloud.Tests/TestsFiles/EmptyFile.txt new file mode 100644 index 000000000..e69de29bb diff --git a/TagCloud.Tests/TestsFiles/GeeseAndSwans.txt b/TagCloud.Tests/TestsFiles/GeeseAndSwans.txt new file mode 100644 index 000000000..a70d3f0ed --- /dev/null +++ b/TagCloud.Tests/TestsFiles/GeeseAndSwans.txt @@ -0,0 +1,158 @@ +Гуси-лебеди + +Русская народная сказка + +в обработке А. Н. Толстого + + + + + + +Гуси-лебеди + + + + + +Жили мужик да баба. У них была дочка да сынок маленький. + +— Доченька, — говорит мать, — мы пойдём на работу — береги братца! Не ходи со двора, будь умницей — мы купим тебе платочек. + +Отец с матерью ушли, а дочка позабыла, что ей приказывали: посадила братца на травке под окошко, сама побежала на улицу, заигралась, загулялась. + +Налетели гуси-лебеди, подхватили мальчика, унесли на крыльях. + +Вернулась девочка, глядь — братца нету! Ахнула, кинулась туда-сюда — нету! + +Она его кликала, слезами заливалась, причитывала, что худо будет от отца с матерью, — братец не откликнулся. + +Выбежала она в чистое поле и только видела: метнулись вдалеке гуси-лебеди и пропали за тёмным лесом. Тут она догадалась, что они унесли её братца. Про гусей-лебедей давно шла дурная слава — что они пошаливали: маленьких детей уносили. + +Бросилась девочка догонять их. Бежала, бежала, увидела — стоит печь. + + + +— Печка, печка, скажи, куда гуси-лебеди полетели? + +Печка ей отвечает: + +— Съешь моего ржаного пирожка — скажу. + +— Стану я ржаной пирог есть! У моего батюшки и пшеничные не едятся… + +Печка ей не сказала. Побежала девочка дальше — стоит яблоня. + + + +— Яблоня, яблоня, скажи, куда гуси-лебеди полетели? + +— Поешь моего лесного яблочка — скажу. + +— У моего батюшки и садовые не едятся… + +Яблоня ей не сказала. Побежала девочка дальше. Течёт молочная река в кисельных берегах. + +— Молочная река, кисельные берега, куда гуси-лебеди полетели? + + + +— Поешь моего простого киселька с молочком — скажу. + +— У моего батюшки и сливочки не едятся… + +Долго она бегала по полям, по лесам. День клонится к вечеру, делать нечего — надо идти домой. Вдруг видит — стоит избушка на курьих ножках, об одном окошке, кругом себя поворачивается. + + + +В избушке старая баба-яга прядёт кудель. А на лавочке сидит братец, играет серебряными яблочками. + +Девочка вошла в избушку: + +— Здравствуй, бабушка! + +— Здравствуй, девица! Зачем на глаза явилась? + +— Я по мхам, по болотам ходила, платье измочила, пришла погреться. + + + +— Садись покуда кудель прясть. + +Баба-яга дала ей веретено, а сама ушла. + +Девочка прядёт — вдруг из-под печки выбегает мышка и говорит ей: + +— Девица, девица, дай мне кашки, я тебе добренькое скажу. + +Девица дала ей кашки. + +Мышка ей сказала: + +— Не дожидайся, бери братца, беги, а я за тебя кудель попряду. + +Девочка взяла братца и побежала. + +А баба-яга подойдёт к окошку и спрашивает: + + + +— Девица, прядёшь ли? + +Мышка ей отвечает: + +— Пряду, бабушка… + +Баба-яга вошла в избушку. А там и нет никого! + +Баба-яга закричала: + +— Гуси-лебеди, летите в погоню! Сестра братца унесла! + +Сестра с братцем добежала до молочной речки. + +Видит — летят гуси-лебеди. + +— Речка, матушка, спрячь меня! + +— Поешь моего простого киселька. + +Девочка поела и спасибо сказала. + + + +Река укрыла её под кисельным бережком. + +Гуси-лебеди не увидали, пролетели мимо. + +Девочка с братцем опять побежала. А гуси-лебеди воротились, летят навстречу, вот-вот увидят. Что делать? Беда! + +Стоит яблоня… + +— Яблоня, матушка, спрячь меня! + +— Поешь моего лесного яблочка. + + + +Девочка поскорее съела и спасибо сказала. + +Яблоня её заслонила ветвями, прикрыла листами. + +Гуси-лебеди не увидали, пролетели мимо. + +Девочка опять побежала. Бежит, бежит, уж недалеко осталось. Тут гуси-лебеди увидали её, загоготали. Налетают, крыльями бьют — того гляди, братца из рук вырвут. + +Добежала девочка до печки: + +— Печка, матушка, спрячь меня! + +— Поешь моего ржаного пирожка. + +Девочка скорее пирожок в рот, а сама с братцем — в печь, села в устьице. + +Гуси-лебеди полетали-полетали, покричали-покричали и ни с чем улетели к бабе-яге. + +Девочка сказала печи спасибо и вместе с братцем прибежала домой. + +А тут и отец с матерью пришли. \ No newline at end of file diff --git a/TagCloud.Tests/TestsFiles/Morozko.png b/TagCloud.Tests/TestsFiles/Morozko.png new file mode 100644 index 000000000..b1a39372e Binary files /dev/null and b/TagCloud.Tests/TestsFiles/Morozko.png differ diff --git a/TagCloud.Tests/TestsFiles/Morozko.txt b/TagCloud.Tests/TestsFiles/Morozko.txt new file mode 100644 index 000000000..1386bd007 --- /dev/null +++ b/TagCloud.Tests/TestsFiles/Morozko.txt @@ -0,0 +1,102 @@ +МОРОЗКО + + + + +Жили-были дед и баба. У деда была дочка, и у бабы была дочка. Все знают, как за мачехой жить: перевернешься — бита и недовернешься — бита. А родная дочь что ни сделает — за все гладят по головке: умница. Падчерица и скотину поила-кормила, дрова и воду в избу носила, печь топила, избу мела — еще до свету… Ничем старухе не угодишь — все не так, все худо. Ветер хоть пошумит, да затихнет, а старая баба расходится — не скоро уймется. Вот мачеха и придумала падчерицу со свету сжить. + +— Вези, вези ее, старик, — говорит мужу, — куда хочешь, чтобы мои глаза ее не видали! Вези ее в лес, на трескучий мороз. + +Старик затужил, заплакал, однако делать нечего, бабы не переспоришь. Запряг лошадь: + +— Садись, мила дочь, в сани. + +Повез бездомную в лес, свалил в сугроб под большую ель и уехал. Девушка сидит под елью, дрожит, озноб ее пробирает. Вдруг слышит — невдалеке Морозко по елкам потрескивает, с елки на елку поскакивает, пощелкивает. Очутился на той ели, под которой девица сидит, и сверху ее спрашивает: + +— Тепло ли тебе, девица? + +Она чуть дух переводит: + +— Тепло, Морозушко, тепло, батюшка. + +Морозко стал ниже спускаться, сильнее потрескивает, пощелкивает: + +— Тепло ли тебе, девица? Тепло ли тебе, красная? + +Она чуть дух переводит: + +— Тепло, Морозушко, тепло, батюшка. + +Морозко еще ниже спустился, пуще затрещал, сильнее защелкал: + +— Тепло ли тебе, девица? Тепло ли тебе, красная? Тепло ли тебе, лапушка? + +Девица окостеневать стала, чуть-чуть языком шевелит: + +— Ой, тепло, голубчик Морозушко! + +Тут Морозко сжалился над девицей; окутал ее теплыми шубами, отогрел пуховыми одеялами. + +А мачеха по ней поминки справляет, печет блины и кричит мужу: + +— Ступай, старый хрыч, вези свою дочь хоронить! + +Поехал старик в лес, доезжает до того места, — под большою елью сидит его дочь, веселая, румяная, в собольей шубе, вся в золоте-серебре, а около — короб с богатыми подарками. + +Старик обрадовался, положил все добро в сани, посадил дочь, повез домой. + +А дома старуха печет блины, а собачка под столом: + +— Тяф, тяф! Старикову дочь в злате, в серебре везут, а старухину замуж не берут. + +Старуха бросит ей блин: + +— Не так тявкаешь! Говори: «Старухину дочь замуж берут, а стариковой дочери косточки везут…» Собака съест блин и опять: + +— Тяф, тяф! Старикову дочь в злате, в серебре везут, а старухину замуж не берут. + +Старуха блины ей кидала и била ее, собачка — все свое… + +Вдруг заскрипели ворота, отворилась дверь, в избу идет падчерица — в злате-серебре, так и сияет. А за ней несут короб высокий, тяжелый. Старуха глянула — и руки врозь… + +— Запрягай, старый хрыч, другую лошадь! Вези, вези мою дочь в лес на то же место… + +Старик посадил старухину дочь в сани, повез ее в лес на то же место, вывалил в сугроб под высокой елью и уехал. + +Старухина дочь сидит, зубами стучит. А Морозко по лесу потрескивает, с елки на елку поскакивает, пощелкивает, на старухину дочь поглядывает: + +— Тепло ли тебе, девица? + +А она ему: + +— Ой, студено! Не скрипи, не трещи, Морозко… + +Морозко стал ниже спускаться, пуще потрескивать, пощелкивать: + +— Тепло ли тебе, девица? Тепло ли тебе, красная? + +— Ой, руки, ноги отмерзли! Уйди, Морозко… + +Еще ниже спустился Морозко, сильнее приударил, затрещал, защелкал: + +— Тепло ли тебе, девица? Тепло ли тебе, красная? + +— Ой, совсем застудил! Сгинь, пропади, проклятый Морозко! + +Рассердился Морозко да так хватил, что старухина дочь окостенела. + +Чуть свет старуха посылает мужа: + +— Запрягай скорее, старый хрыч, поезжай за дочерью, привези ее в злате-серебре… + +Старик уехал. А собачка под столом: + +— Тяв, тяв! Старикову дочь женихи возьмут, а старухиной дочери в мешке косточки везут. Старуха кинула ей пирог: + +— Не так тявкаешь! Скажи: «Старухину дочь в злате-серебре везут…» + +А собачка — все свое: + +— Тяв, тяв! Старухиной дочери в мешке косточки везут… + +Заскрипели ворота, старуха кинулась встречать дочь. Рогожу отвернула, а дочь лежит в санях мертвая. Заголосила старуха, да поздно. diff --git a/TagCloud.Tests/TxtReaderTests.cs b/TagCloud.Tests/TxtReaderTests.cs new file mode 100644 index 000000000..dc2f6881a --- /dev/null +++ b/TagCloud.Tests/TxtReaderTests.cs @@ -0,0 +1,45 @@ +using FluentAssertions; +using TagCloud.ReadingFiles; + +namespace TagCloud.Tests; + +[TestFixture] +public class TxtReaderTests +{ + private readonly TxtReader _reader = new(); + private readonly string _pathToFileFolder = Path.Combine(Directory.GetCurrentDirectory(), "TestsFiles"); + + [Test] + public void ReadTextLineByLine_UnExistingFile_ThrowFileNotFoundException() + { + var calling = () => _reader.ReadTextLineByLine("UnExistingFile.txt").ToArray(); + + calling.Should().Throw(); + } + + [Test] + public void ReadTextLineByLine_EmptyFile_EmptyCollectionOfWords() + { + var pathToFile = Path.Combine(_pathToFileFolder, "EmptyFile.txt"); + + var actual = _reader.ReadTextLineByLine(pathToFile); + + actual.Should().BeEmpty(); + } + + [Test] + public void ReadTextLineByLine_Text_CorrectWordCount() + { + var pathToFile = Path.Combine(_pathToFileFolder, "CheckingCount.txt"); + var generator = new GeneratingTestData(); + var lines = generator.Shuffle(generator.CreateArrayOfWords(GeneratingTestData.FrequencyDictionary)); + File.WriteAllLines(pathToFile, lines); + + var result = _reader.ReadTextLineByLine(pathToFile); + + result + .All(line => GeneratingTestData.FrequencyDictionary[line] == result.Count(l => l == line)) + .Should() + .BeTrue(); + } +} diff --git a/TagCloud.Tests/VisualizationCloudLayoutTests.cs b/TagCloud.Tests/VisualizationCloudLayoutTests.cs new file mode 100644 index 000000000..b6981395c --- /dev/null +++ b/TagCloud.Tests/VisualizationCloudLayoutTests.cs @@ -0,0 +1,148 @@ +using System.Drawing; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using TagCloud.ImageGeneration; +using TagCloud.ImageGeneration.Settings; +using TagCloud.ImageGeneration.Settings.DTO; +using TagCloud.Parsing; +using TagCloud.ReadingFiles; + +namespace TagCloud.Tests; + +[TestFixture] +public class VisualizationCloudLayoutTests +{ + private const string TypeLiteraryText = "Литературный текст"; + private const string WordListType = "Список слов (по одному в строке)"; + private IParserProvider _parserProvider; + private IReaderProvider _readerProvider; + private IVisualizationProvider _visualizationProvider; + private readonly HashSet _partOfSpeechForFiltering = + [ + "местоимение-прилагательное", + "союз", + "междометие", + "частица", + "предлог", + "местоимение-существительное", + ]; + + [Test] + public void VisualizationCloudLayout_ChangeSettings_SettingsShouldChange() + { + const float cloudCompressionRatio = 1.5f; + var imageSize = new Size(1920, 540); + var fontName = "Calibri"; + + _visualizationProvider.Settings.ImageSize.Width = imageSize.Width.ToString(); + _visualizationProvider.Settings.ImageSize.Height = imageSize.Height.ToString(); + + _visualizationProvider.Settings.FontFamily.Name = fontName; + _visualizationProvider.Settings.CompressionRatio.Value = cloudCompressionRatio.ToString(); + + _visualizationProvider.Settings.ImageSize.GetValueOrThrow().Should().Be(imageSize); + _visualizationProvider.Settings.FontFamily.Name.Should().Be(fontName); + _visualizationProvider.Settings.CompressionRatio.GetValueOrThrow().Should().Be(cloudCompressionRatio); + } + + [TestCase("Morozko.txt", "Morozko.jpeg", TypeLiteraryText, 2.6f)] + [TestCase("GeeseAndSwans.txt", "GeeseAndSwans.png", TypeLiteraryText, 2.8f)] + [TestCase("CheckingCount.txt", "CheckingCount.bmp", WordListType, 0.4f)] + public void CreateImage_ImageSizeMustMatchSettings(string fileName, string imageName, + string structure, float cloudCompressionRatio) + { + var sourceFile = Path.Combine("TestsFiles", fileName); + var reader = _readerProvider.GetReader(sourceFile).GetValueOrThrow(); + _parserProvider.SlectedParser = structure; + var parser = _parserProvider.GetParser(); + var preprocessingStatus = parser(reader); + preprocessingStatus.IsSuccess.Should().BeTrue(); + var words = preprocessingStatus.GetValueOrThrow() + .Where(info => !_partOfSpeechForFiltering.Contains(info.PartOfSpeach)); + _visualizationProvider.Settings.WordsList.Value = words; + + _visualizationProvider.Settings.CompressionRatio.Value = cloudCompressionRatio.ToString(); + + _visualizationProvider.Settings.ImageSize.Width = "1080"; + _visualizationProvider.Settings.ImageSize.Height = "1080"; + CheckSizeMatching(imageName); + _visualizationProvider.Settings.ImageSize.Width = "1280"; + _visualizationProvider.Settings.ImageSize.Height = "720"; + CheckSizeMatching(imageName); + } + + private void CheckSizeMatching(string imageName) + { + var image = GenerateImage(imageName); + image.Size.Should().Be(_visualizationProvider.Settings.ImageSize.GetValueOrThrow()); + } + + private Bitmap GenerateImage(string imageName) + { + imageName = $"({_visualizationProvider.Settings.ImageSize.Width}" + + $"x{_visualizationProvider.Settings.ImageSize.Height})" + + $"{imageName}"; + var pathToImage = $"../../../Images/{imageName}"; + + var image = _visualizationProvider.CreateImage().GetValueOrThrow(); + image.Save(pathToImage); + + return image; + } + + [TestCase("CheckingCount.txt", "CheckingCount.bmp", WordListType, 10f)] + public void CreateImage_FailedSettings_ImageWillNotBeGenerated(string fileName, string imageName, + string structure, float cloudCompressionRatio) + { + var sourceFile = Path.Combine("TestsFiles", fileName); + var reader = _readerProvider.GetReader(sourceFile).GetValueOrThrow(); + _parserProvider.SlectedParser = structure; + var parser = _parserProvider.GetParser(); + var preprocessingStatus = parser(reader); + preprocessingStatus.IsSuccess.Should().BeTrue(); + _visualizationProvider.Settings.WordsList.Value = preprocessingStatus.GetValueOrThrow(); + + var status = _visualizationProvider.CreateImage(); + + status.IsSuccess.Should().BeFalse(); + } + + [Test] + public void CreateImage_WordsAreNotLoaded_ImageWillNotBeGenerated() + { + var status = _visualizationProvider.CreateImage(); + + status.IsSuccess.Should().BeFalse(); + } + + [SetUp] + public void PrepareEnvironment() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + var partialSupplier = services.BuildServiceProvider(); + services.AddSingleton(_ => new RenderingSettings( + new ImageSizeDto(1080, 1080), + new FontFamilyDto("Arial"), + new CompressionRatioDto(2f), + partialSupplier.GetService(), + partialSupplier.GetService(), + new WordsListDto())); + var provider = services.BuildServiceProvider(); + + _parserProvider = provider.GetService(); + _readerProvider = provider.GetService(); + _visualizationProvider = provider.GetService(); + } +} \ No newline at end of file diff --git a/TagCloud.Tests/WordInfoTests.cs b/TagCloud.Tests/WordInfoTests.cs new file mode 100644 index 000000000..f9d5afec7 --- /dev/null +++ b/TagCloud.Tests/WordInfoTests.cs @@ -0,0 +1,35 @@ +using FluentAssertions; +using TagCloud.Parsing; + +namespace TagCloud.Tests; + +[TestFixture] +public class WordInfoTests +{ + [Test] + public void WordInfo_CorrectPartsOfSpeech() + { + const int count = 1; + const string word = "привет"; + + foreach (var res in WordInfo.PartsOfSpeech + .Select(partOfSpeech => WordInfo.Create(word, partOfSpeech, count))) + { + res.IsSuccess.Should().BeTrue(); + } + } + + [Test] + public void WordInfo_IncorrectPartsOfSpeech() + { + const int count = 1; + const string word = "привет"; + var incorrectPartsOfSpeech = new[] { "0", "999", "a", word, "причастие", "деепричастие", "слово" }; + + foreach (var res in incorrectPartsOfSpeech + .Select(partOfSpeech => WordInfo.Create(word, partOfSpeech, count))) + { + res.IsSuccess.Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/TagCloud/CrrectnessCheckerBase.cs b/TagCloud/CrrectnessCheckerBase.cs new file mode 100644 index 000000000..2a4331ca7 --- /dev/null +++ b/TagCloud/CrrectnessCheckerBase.cs @@ -0,0 +1,13 @@ +namespace TagCloud; + +public abstract class CrrectnessCheckerBase : ICrrectnessChecker +{ + public bool IsCorrect { get; private set; } + public event Action? ValueChanged; + + protected void ChangeValue(bool isCorrect, string message = "") + { + IsCorrect = isCorrect; + ValueChanged?.Invoke(this, message); + } +} \ No newline at end of file diff --git a/TagCloud/ICrrectnessChecker.cs b/TagCloud/ICrrectnessChecker.cs new file mode 100644 index 000000000..b3173e4e2 --- /dev/null +++ b/TagCloud/ICrrectnessChecker.cs @@ -0,0 +1,7 @@ +namespace TagCloud; + +public interface ICrrectnessChecker +{ + public bool IsCorrect { get; } + public event Action? ValueChanged; +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/CircularCloud.cs b/TagCloud/ImageGeneration/CircularCloud.cs new file mode 100644 index 000000000..6f4967e9c --- /dev/null +++ b/TagCloud/ImageGeneration/CircularCloud.cs @@ -0,0 +1,66 @@ +using System.Drawing; + +namespace TagCloud.ImageGeneration; + +public class CircularCloud : ILayoutProvider +{ + private const double AngleChangeStep = Math.PI / 180; + + private readonly LinkedList _cloudOfRectangles = []; + private int DistanceBetweenTurns { get; set; } = 30; + private int InitialRadiusOfSpiral { get; set; } + private double AngleOfRotationInRadians { get; set; } + public string Name => "Круглая форма"; + + public Point Center { get; set; } + + public RectangleF PutNextRectangle(SizeF rectangleSize) + { + var halfOfMinSide = (int)(Math.Min(rectangleSize.Width, rectangleSize.Height) / 2); + if (halfOfMinSide > 0) + DistanceBetweenTurns = Math.Min(DistanceBetweenTurns, halfOfMinSide); + + if (_cloudOfRectangles.Count == 0) InitialRadiusOfSpiral = halfOfMinSide; + + var rectangle = ChooseLocationForRectangle(rectangleSize); + _cloudOfRectangles.AddFirst(rectangle); + + return rectangle; + } + + public void ResetLayout() + { + _cloudOfRectangles.Clear(); + AngleOfRotationInRadians = 0; + } + + private RectangleF ChooseLocationForRectangle(SizeF rectangleSize) + { + var currentPoint = GetNewPoint(); + var rectangle = GetNewRectangle(currentPoint, rectangleSize); + + while (_cloudOfRectangles.Any(rect => rect.IntersectsWith(rectangle))) + { + AngleOfRotationInRadians += AngleChangeStep; + currentPoint = GetNewPoint(); + rectangle = GetNewRectangle(currentPoint, rectangleSize); + } + + return rectangle; + } + + private RectangleF GetNewRectangle(PointF centerPoint, SizeF rectangleSize) + { + return new RectangleF(new PointF(centerPoint.X - rectangleSize.Width / 2, + centerPoint.Y - rectangleSize.Height / 2), rectangleSize); + } + + private PointF GetNewPoint() + { + var coefficient = InitialRadiusOfSpiral + AngleOfRotationInRadians * DistanceBetweenTurns; + var x = (float)(coefficient * Math.Cos(AngleOfRotationInRadians) + Center.X); + var y = (float)(coefficient * Math.Sin(AngleOfRotationInRadians) + Center.Y); + + return new PointF(x, y); + } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/ColorPicker.cs b/TagCloud/ImageGeneration/ColorPicker.cs new file mode 100644 index 000000000..ce04e9d7c --- /dev/null +++ b/TagCloud/ImageGeneration/ColorPicker.cs @@ -0,0 +1,14 @@ +using System.Drawing; +using TagCloud.Parsing; + +namespace TagCloud.ImageGeneration; + +public class ColorPicker : IColorProvider +{ + public string Name { get; } = "Однотонный красный"; + + public Color GetColorForWord(WordInfo word) + { + return Color.Red; + } +} diff --git a/TagCloud/ImageGeneration/IColorProvider.cs b/TagCloud/ImageGeneration/IColorProvider.cs new file mode 100644 index 000000000..ea52ee167 --- /dev/null +++ b/TagCloud/ImageGeneration/IColorProvider.cs @@ -0,0 +1,10 @@ +using System.Drawing; +using TagCloud.Parsing; + +namespace TagCloud.ImageGeneration; + +public interface IColorProvider +{ + public string Name { get; } + public Color GetColorForWord(WordInfo word); +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/ILayoutProvider.cs b/TagCloud/ImageGeneration/ILayoutProvider.cs new file mode 100644 index 000000000..b453e2dcf --- /dev/null +++ b/TagCloud/ImageGeneration/ILayoutProvider.cs @@ -0,0 +1,11 @@ +using System.Drawing; + +namespace TagCloud.ImageGeneration; + +public interface ILayoutProvider +{ + public string Name { get; } + public Point Center { get; set; } + public void ResetLayout(); + public RectangleF PutNextRectangle(SizeF rectangleSize); +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/IVisualizationProvider.cs b/TagCloud/ImageGeneration/IVisualizationProvider.cs new file mode 100644 index 000000000..3baaaa2ed --- /dev/null +++ b/TagCloud/ImageGeneration/IVisualizationProvider.cs @@ -0,0 +1,11 @@ +using System.Drawing; +using ErrorHandling; +using TagCloud.ImageGeneration.Settings; + +namespace TagCloud.ImageGeneration; + +public interface IVisualizationProvider +{ + public Result CreateImage(); + public RenderingSettings Settings { get; } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/Settings/DTO/ColoringAlgorithmDTO.cs b/TagCloud/ImageGeneration/Settings/DTO/ColoringAlgorithmDTO.cs new file mode 100644 index 000000000..b82fe9615 --- /dev/null +++ b/TagCloud/ImageGeneration/Settings/DTO/ColoringAlgorithmDTO.cs @@ -0,0 +1,34 @@ +using Castle.Core.Internal; + +namespace TagCloud.ImageGeneration.Settings.DTO; + +public class ColoringAlgorithmDto : CrrectnessCheckerBase +{ + private readonly Dictionary _coloringAlgorithms; + public IEnumerable NamesOfColoringAlgorithms => _coloringAlgorithms.Keys; + public IColorProvider? GetColorProvider => IsCorrect ? _coloringAlgorithms[_selectedAlgorithm] : null; + + private string _selectedAlgorithm; + public string SelectedAlgorithm { + get => _selectedAlgorithm; + set + { + _selectedAlgorithm = value; + if (_selectedAlgorithm.IsNullOrEmpty()) + ChangeValue(false, "Значение не должно быть пустым"); + else if (!_coloringAlgorithms.ContainsKey(value)) + ChangeValue(false, "Алгоритм расцветки не найден"); + else ChangeValue(true); + } + } + + public ColoringAlgorithmDto(IEnumerable coloringAlgorithms, IColorProvider defaultAlgorithm) + { + ArgumentNullException.ThrowIfNull(coloringAlgorithms); + _coloringAlgorithms = coloringAlgorithms.ToDictionary(cp => cp.Name, cp => cp); + ArgumentNullException.ThrowIfNull(defaultAlgorithm); + if (!_coloringAlgorithms.ContainsKey(defaultAlgorithm.Name)) + throw new ArgumentException($"{nameof(defaultAlgorithm)} должен быть указан в {nameof(coloringAlgorithms)}"); + SelectedAlgorithm = defaultAlgorithm.Name; + } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/Settings/DTO/CompressionRatioDTO.cs b/TagCloud/ImageGeneration/Settings/DTO/CompressionRatioDTO.cs new file mode 100644 index 000000000..69b8cbda3 --- /dev/null +++ b/TagCloud/ImageGeneration/Settings/DTO/CompressionRatioDTO.cs @@ -0,0 +1,48 @@ +namespace TagCloud.ImageGeneration.Settings.DTO; + +public class CompressionRatioDto : ICrrectnessChecker +{ + public const float MaxValue = 10f; + public const float MinValue = 0.1f; + public bool IsCorrect { get; private set; } + public event Action? ValueChanged; + + private string _value; + public string Value + { + get => _value; + set + { + _value = value; + if (float.TryParse(value, out var number)) + { + IsCorrect = CheckCorrectness(number); + ValueChanged?.Invoke(this, IsCorrect ? string.Empty : $"{MinValue} <= коэффициент <= {MaxValue}"); + } + else + { + IsCorrect = false; + ValueChanged?.Invoke(this, "Не является числом"); + } + } + } + + public CompressionRatioDto(float ratio) + { + if (!CheckCorrectness(ratio)) + throw new ArgumentException($"{MinValue} <= {nameof(ratio)} <= {MaxValue}"); + Value = $"{ratio}"; + } + + private bool CheckCorrectness(float coefficient) + { + return coefficient is >= MinValue - float.Epsilon and <= MaxValue + float.Epsilon; + } + + public float GetValueOrThrow() + { + if (float.TryParse(_value, out var number) && CheckCorrectness(number)) + return number; + throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/Settings/DTO/FontFamilyDTO.cs b/TagCloud/ImageGeneration/Settings/DTO/FontFamilyDTO.cs new file mode 100644 index 000000000..c17c662c1 --- /dev/null +++ b/TagCloud/ImageGeneration/Settings/DTO/FontFamilyDTO.cs @@ -0,0 +1,45 @@ +using System.Drawing; +using System.Drawing.Text; +using Castle.Core.Internal; + +namespace TagCloud.ImageGeneration.Settings.DTO; + +public class FontFamilyDto : CrrectnessCheckerBase +{ + private readonly HashSet _availableFontFamilies = new InstalledFontCollection().Families + .Select(family => family.Name) + .ToHashSet(); + + private string _name; + + public FontFamily GetValueOrThrow() + { + if (IsCorrect) + return new FontFamily(_name); + throw new InvalidOperationException(); + } + + public string Name + { + get => _name; + set + { + _name = value; + if (value.IsNullOrEmpty()) + ChangeValue(false, "Значение не должно быть пустым"); + else if (!_availableFontFamilies.Contains(value)) + ChangeValue(false, $"Шрифт {value} не найден"); + else ChangeValue(true); + } + } + + public FontFamilyDto(string fontName) + { + if (fontName.IsNullOrEmpty()) + throw new ArgumentException("Значение не должно быть пустым", nameof(fontName)); + if (!_availableFontFamilies.Contains(fontName)) + throw new ArgumentException($"Шрифт {fontName} не найден в списке системных шрифтов"); + + Name = fontName; + } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/Settings/DTO/ImageSizeDTO.cs b/TagCloud/ImageGeneration/Settings/DTO/ImageSizeDTO.cs new file mode 100644 index 000000000..0dbcdfeae --- /dev/null +++ b/TagCloud/ImageGeneration/Settings/DTO/ImageSizeDTO.cs @@ -0,0 +1,71 @@ +using System.Drawing; + +namespace TagCloud.ImageGeneration.Settings.DTO; + +public class ImageSizeDto : ICrrectnessChecker +{ + public bool IsCorrect => _widthIsCorrect && _heightIsCorrect; + public event Action? ValueChanged; + private const string ErrorMessage = "Размеры должны быть положительными"; + + public Size GetValueOrThrow() + { + if (IsCorrect) + return new Size(int.Parse(_width), int.Parse(_height)); + throw new InvalidOperationException(); + } + + private bool _widthIsCorrect; + private bool _heightIsCorrect; + + private string _width; + public string Width + { + get => _width; + set + { + _width = value; + if (int.TryParse(value, out var number)) + { + _widthIsCorrect = number > 0; + ValueChanged?.Invoke(this, _widthIsCorrect ? string.Empty : ErrorMessage); + } + else + { + _widthIsCorrect = false; + ValueChanged?.Invoke(this, "Оба параметра должны быть целыми числами"); + } + } + } + + private string _height; + public string Height + { + get => _height; + set + { + _height = value; + if (int.TryParse(value, out var number)) + { + _heightIsCorrect = number > 0; + ValueChanged?.Invoke(this, _heightIsCorrect ? string.Empty : ErrorMessage); + } + else + { + _heightIsCorrect = false; + ValueChanged?.Invoke(this, "Оба параметра должны быть целыми числами"); + } + } + } + + public ImageSizeDto(int width, int height) + { + if (width <= 0) + throw new ArgumentException("Ширина должна быть положительной", nameof(width)); + if (height <= 0) + throw new ArgumentException("Высота должна быть положительной", nameof(height)); + + Width = width.ToString(); + Height = height.ToString(); + } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/Settings/DTO/LayoutAlgorithmDTO.cs b/TagCloud/ImageGeneration/Settings/DTO/LayoutAlgorithmDTO.cs new file mode 100644 index 000000000..0d8175312 --- /dev/null +++ b/TagCloud/ImageGeneration/Settings/DTO/LayoutAlgorithmDTO.cs @@ -0,0 +1,40 @@ +using Castle.Core.Internal; + +namespace TagCloud.ImageGeneration.Settings.DTO; + +public class LayoutAlgorithmDto : CrrectnessCheckerBase +{ + private readonly Dictionary _layoutAlgorithms; + public IEnumerable NamesOfLayoutAlgorithms => _layoutAlgorithms.Keys; + + public ILayoutProvider GetValueOrThrow() + { + if (IsCorrect) + return _layoutAlgorithms[_selectedAlgorithm]; + throw new InvalidOperationException(); + } + + private string _selectedAlgorithm; + public string SelectedAlgorithm { + get => _selectedAlgorithm; + set + { + _selectedAlgorithm = value; + if (_selectedAlgorithm.IsNullOrEmpty()) + ChangeValue(false, "Значение не должно быть пустым"); + else if (!_layoutAlgorithms.ContainsKey(value)) + ChangeValue(false, "Алгоритм раскладки не найден"); + else ChangeValue(true); + } + } + + public LayoutAlgorithmDto(IEnumerable layoutAlgorithms, ILayoutProvider defaultAlgorithm) + { + ArgumentNullException.ThrowIfNull(layoutAlgorithms); + _layoutAlgorithms = layoutAlgorithms.ToDictionary(lp => lp.Name, lp => lp); + ArgumentNullException.ThrowIfNull(defaultAlgorithm); + if (!_layoutAlgorithms.ContainsKey(defaultAlgorithm.Name)) + throw new ArgumentException($"{nameof(defaultAlgorithm)} должен быть указан в {nameof(layoutAlgorithms)}"); + SelectedAlgorithm = defaultAlgorithm.Name; + } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/Settings/DTO/WordsListDto.cs b/TagCloud/ImageGeneration/Settings/DTO/WordsListDto.cs new file mode 100644 index 000000000..08e4446cd --- /dev/null +++ b/TagCloud/ImageGeneration/Settings/DTO/WordsListDto.cs @@ -0,0 +1,21 @@ +using TagCloud.Parsing; + +namespace TagCloud.ImageGeneration.Settings.DTO; + +public class WordsListDto : ICrrectnessChecker +{ + public bool IsCorrect { get; private set; } + public event Action? ValueChanged; + + private IEnumerable? _value; + public IEnumerable? Value + { + get => _value; + set + { + _value = value; + IsCorrect = value is not null && value.Any(); + ValueChanged?.Invoke(this, IsCorrect ? string.Empty : "Загрузите слова"); + } + } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/Settings/RenderingSettings.cs b/TagCloud/ImageGeneration/Settings/RenderingSettings.cs new file mode 100644 index 000000000..0fbe09b3f --- /dev/null +++ b/TagCloud/ImageGeneration/Settings/RenderingSettings.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using TagCloud.ImageGeneration.Settings.DTO; + +namespace TagCloud.ImageGeneration.Settings; + +public class RenderingSettings : ICrrectnessChecker +{ + public ImageSizeDto ImageSize { get; } + public FontFamilyDto FontFamily { get; } + public WordsListDto WordsList { get; } + public CompressionRatioDto CompressionRatio { get; } + public LayoutAlgorithmDto LayoutAlgorithm { get; } + public ColoringAlgorithmDto ColoringAlgorithm { get; } + + private readonly ImmutableArray _arrayFields; + + public RenderingSettings(ImageSizeDto imageSize, FontFamilyDto fontFamily, CompressionRatioDto compressionRatio, + LayoutAlgorithmDto layoutAlgorithm, ColoringAlgorithmDto coloringAlgorithm, WordsListDto wordsList) + { + ImageSize = imageSize ?? throw new ArgumentNullException(nameof(imageSize)); + FontFamily = fontFamily ?? throw new ArgumentNullException(nameof(fontFamily)); + CompressionRatio = compressionRatio ?? throw new ArgumentNullException(nameof(compressionRatio)); + LayoutAlgorithm = layoutAlgorithm ?? throw new ArgumentNullException(nameof(layoutAlgorithm)); + ColoringAlgorithm = coloringAlgorithm ?? throw new ArgumentNullException(nameof(coloringAlgorithm)); + WordsList = wordsList ?? throw new ArgumentNullException(nameof(wordsList)); + _arrayFields = ImmutableArray.CreateRange([ + ImageSize, + FontFamily, + CompressionRatio, + LayoutAlgorithm, + ColoringAlgorithm, + WordsList ]); + foreach (var field in _arrayFields) + field.ValueChanged += FieldValueHasChanged; + } + + private void FieldValueHasChanged(ICrrectnessChecker _, string message) + { + if (IsCorrect == _arrayFields.All(checker => checker.IsCorrect)) return; + IsCorrect = !IsCorrect; + ValueChanged?.Invoke(this, string.Empty); + } + + public event Action? ValueChanged; + public bool IsCorrect { get; private set; } +} \ No newline at end of file diff --git a/TagCloud/ImageGeneration/VisualizationCloudLayout.cs b/TagCloud/ImageGeneration/VisualizationCloudLayout.cs new file mode 100644 index 000000000..4ddb378d3 --- /dev/null +++ b/TagCloud/ImageGeneration/VisualizationCloudLayout.cs @@ -0,0 +1,52 @@ +using System.Drawing; +using ErrorHandling; +using TagCloud.ImageGeneration.Settings; + +namespace TagCloud.ImageGeneration; + +public class VisualizationCloudLayout( + RenderingSettings settings) + : IVisualizationProvider +{ + private float _coefficient; + public RenderingSettings Settings { get; } = settings; + + public Result CreateImage() + { + if (!Settings.WordsList.IsCorrect) + return Result.Fail("Загрузите слова для генерации изображения"); + if (!Settings.IsCorrect) + return Result.Fail("Значения настроек некорректны"); + + var imageSize = Settings.ImageSize.GetValueOrThrow(); + Settings.LayoutAlgorithm.GetValueOrThrow().ResetLayout(); + var center = new Point(imageSize.Width / 2, imageSize.Height / 2); + Settings.LayoutAlgorithm.GetValueOrThrow().Center = center; + var numberOUniqueWords = Settings.WordsList.Value.Sum(wordInfo => wordInfo.NumberInText); + _coefficient = imageSize.Height * Settings.CompressionRatio.GetValueOrThrow() / numberOUniqueWords; + var image = new Bitmap(imageSize.Width, imageSize.Height); + var status = DrawСloudOfWords(Graphics.FromImage(image)); + + return status.IsSuccess ? Result.Ok(image) : Result.Fail(status.Error); + } + + private ActionStatus DrawСloudOfWords(Graphics graphics) + { + var rectOfCanvas = new RectangleF(Point.Empty, Settings.ImageSize.GetValueOrThrow()); + + foreach (var word in Settings.WordsList.Value) + { + var color = Settings.ColoringAlgorithm.GetColorProvider.GetColorForWord(word); + var height = word.NumberInText * _coefficient; + var font = new Font(Settings.FontFamily.GetValueOrThrow(), height, GraphicsUnit.Pixel); + var size = graphics.MeasureString(word.Word, font); + var rectOfWord = Settings.LayoutAlgorithm.GetValueOrThrow().PutNextRectangle(size); + if (!rectOfCanvas.Contains(rectOfWord)) + return ActionStatus.Fail("Не удалось уместить все слова на холст"); + + graphics.DrawString(word.Word, font, new SolidBrush(color), rectOfWord); + } + + return ActionStatus.Ok(); + } +} \ No newline at end of file diff --git a/TagCloud/Parsing/IParser.cs b/TagCloud/Parsing/IParser.cs new file mode 100644 index 000000000..c06acd5ac --- /dev/null +++ b/TagCloud/Parsing/IParser.cs @@ -0,0 +1,9 @@ +using ErrorHandling; + +namespace TagCloud.Parsing; + +public interface IParser +{ + public string TypeOfParsing { get; } + public Result Parse(Func> getTextLineByLine); +} \ No newline at end of file diff --git a/TagCloud/Parsing/IParserProvider.cs b/TagCloud/Parsing/IParserProvider.cs new file mode 100644 index 000000000..54f7e38f3 --- /dev/null +++ b/TagCloud/Parsing/IParserProvider.cs @@ -0,0 +1,8 @@ +namespace TagCloud.Parsing; + +public interface IParserProvider : ICrrectnessChecker +{ + public string SlectedParser { get; set; } + public IEnumerable GetTypesParsers { get; } + public ParsingFunction? GetParser(); +} diff --git a/TagCloud/Parsing/LiteraryTextParser.cs b/TagCloud/Parsing/LiteraryTextParser.cs new file mode 100644 index 000000000..c80ab8724 --- /dev/null +++ b/TagCloud/Parsing/LiteraryTextParser.cs @@ -0,0 +1,107 @@ +using System.Diagnostics; +using System.Text; +using Castle.Core.Internal; +using ErrorHandling; + +namespace TagCloud.Parsing; + +public class LiteraryTextParser : IParser +{ + public string TypeOfParsing => "Литературный текст"; + private readonly ProcessStartInfo _startInfo = new() + { + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardInput = false, + RedirectStandardOutput = true, + FileName = "Parsing/Mystem.exe", + }; + private readonly Dictionary _decryptionGrammems = new() + { + { "A", "прилагательное" }, + { "ADV", "наречие" }, + { "ADVPRO", "местоименное наречие" }, + { "ANUM", "числительное-прилагательное" }, + { "APRO", "местоимение-прилагательное" }, + { "COM", "часть композита - сложного слова" }, + { "CONJ", "союз" }, + { "INTJ", "междометие" }, + { "NUM", "числительное" }, + { "PART", "частица" }, + { "PR", "предлог" }, + { "S", "существительное" }, + { "SPRO", "местоимение-существительное" }, + { "V", "глагол" } + }; + private readonly Dictionary, int> _countingDictionary = new(); + + private ActionStatus ParseText(Func> getTextLineByLine) + { + var tempFile = Path.ChangeExtension(Path.GetTempFileName(), ".txt"); + WriteLinesToFile(getTextLineByLine(), tempFile); + _startInfo.Arguments = $"-ling {tempFile}"; + + using var process = Process.Start(_startInfo); + using var reader = new StreamReader(process.StandardOutput.BaseStream, Encoding.UTF8); + var stopwatch = Stopwatch.StartNew(); + while (reader.ReadLine() is { } line && stopwatch.ElapsedMilliseconds < 5000) + { + if (TryExtractWordAndPartOfSpeech(line, out var wordAndPartOfSpeech)) + AddInfoAboutWord(wordAndPartOfSpeech); + } + stopwatch.Stop(); + var errors = process.StandardError.ReadToEnd(); + + process.Kill(); + process.WaitForExit(); + File.Delete(tempFile); + + if (stopwatch.ElapsedMilliseconds >= 5000) + return ActionStatus.Fail("обработка текста длится слишком долго"); + return errors.IsNullOrEmpty() ? ActionStatus.Ok() : ActionStatus.Fail(errors); + } + + private void WriteLinesToFile(IEnumerable lines, string fileName) + { + using var writer = new StreamWriter(fileName); + foreach (var line in lines) + writer.WriteLine(line); + } + + public Result Parse(Func> getTextLineByLine) + { + _countingDictionary.Clear(); + var result = new List(); + var parsingStatus = ParseText(getTextLineByLine); + if (!parsingStatus.IsSuccess) + return Result.Fail("Литературная обработка текста не доступна. " + + $"Ошибка работы стороннего приложения Mystem: {parsingStatus.Error}"); + + foreach (var pair in _countingDictionary) + { + var res = WordInfo.Create(pair.Key.Item1, _decryptionGrammems[pair.Key.Item2], pair.Value); + if (!res.IsSuccess) + return Result.Fail($"Встретилась {res.Error}"); + result.Add(res.GetValueOrThrow()); + } + + return result.Count == 0 ? Result.Fail("Файл оказался пустым") : Result.Ok(result.ToArray()); + } + + private bool TryExtractWordAndPartOfSpeech(string wordAnalysis, out Tuple? result) + { + result = null; + var info = wordAnalysis.Split('='); + + if (info.Length < 2 || info[0].Contains('?')) return false; + result = Tuple.Create(info[0], info[1].Split(',')[0]); + return true; + } + + private void AddInfoAboutWord(Tuple wordAndPartOfSpeech) + { + if (!_countingDictionary.TryAdd(wordAndPartOfSpeech, 1)) + _countingDictionary[wordAndPartOfSpeech]++; + } +} diff --git a/TagCloud/Parsing/ParserProvider.cs b/TagCloud/Parsing/ParserProvider.cs new file mode 100644 index 000000000..ad8c5e70d --- /dev/null +++ b/TagCloud/Parsing/ParserProvider.cs @@ -0,0 +1,37 @@ +using Castle.Core.Internal; + +namespace TagCloud.Parsing; + +public class ParserProvider : CrrectnessCheckerBase, IParserProvider +{ + private readonly Dictionary _parsers; + + private string _selectedParser; + public string SlectedParser + { + get => _selectedParser; + set + { + _selectedParser = value; + if (_selectedParser.IsNullOrEmpty()) + ChangeValue(false, "Значение не должно быть пустым"); + else if (!_parsers.ContainsKey(_selectedParser)) + ChangeValue(false, "Тип содержания не найден"); + else ChangeValue(true); + } + } + + public IEnumerable GetTypesParsers => _parsers.Keys; + + public ParsingFunction? GetParser() => IsCorrect ? new ParsingFunction(_parsers[_selectedParser].Parse) : null; + + public ParserProvider(IEnumerable parsers, IParser defaultParser) + { + ArgumentNullException.ThrowIfNull(parsers); + _parsers = parsers.ToDictionary(parser => parser.TypeOfParsing, parser => parser); + ArgumentNullException.ThrowIfNull(defaultParser); + if (!_parsers.ContainsKey(defaultParser.TypeOfParsing)) + throw new AggregateException($"{nameof(defaultParser)} должен быть указан в {nameof(parsers)}"); + SlectedParser = defaultParser.TypeOfParsing; + } +} \ No newline at end of file diff --git a/TagCloud/Parsing/ParsingFunction.cs b/TagCloud/Parsing/ParsingFunction.cs new file mode 100644 index 000000000..a30e7b188 --- /dev/null +++ b/TagCloud/Parsing/ParsingFunction.cs @@ -0,0 +1,5 @@ +using ErrorHandling; + +namespace TagCloud.Parsing; + +public delegate Result ParsingFunction(Func> getTextLineByLine); \ No newline at end of file diff --git a/TagCloud/Parsing/WordInfo.cs b/TagCloud/Parsing/WordInfo.cs new file mode 100644 index 000000000..fd9dc4a64 --- /dev/null +++ b/TagCloud/Parsing/WordInfo.cs @@ -0,0 +1,49 @@ +using System.Collections.Immutable; +using ErrorHandling; + +namespace TagCloud.Parsing; + +public record WordInfo +{ + public static ImmutableHashSet PartsOfSpeech { get; } = + [ + "прилагательное", + "наречие", + "местоименное наречие", + "числительное-прилагательное", + "местоимение-прилагательное", + "часть композита - сложного слова", + "союз", + "междометие", + "числительное", + "частица", + "предлог", + "существительное", + "местоимение-существительное", + "глагол", + "нет данных" + ]; + + private WordInfo(string word, string partOfSpeach, int numberInText) + { + Word = word; + PartOfSpeach = partOfSpeach; + NumberInText = numberInText; + } + + public static Result Create(string word, int numberInText) + { + return Result.Ok(new WordInfo(word, "нет данных", numberInText)); + } + + public static Result Create(string word, string partOfSpeach, int numberInText) + { + if (!PartsOfSpeech.Contains(partOfSpeach)) + return Result.Fail($"неизвестная чаcть речи: {partOfSpeach}"); + return Result.Ok(new WordInfo(word, partOfSpeach, numberInText)); + } + + public string Word { get; } + public string PartOfSpeach { get; } + public int NumberInText { get; } +} \ No newline at end of file diff --git a/TagCloud/Parsing/WordListParser.cs b/TagCloud/Parsing/WordListParser.cs new file mode 100644 index 000000000..281dfdd74 --- /dev/null +++ b/TagCloud/Parsing/WordListParser.cs @@ -0,0 +1,30 @@ +using ErrorHandling; + +namespace TagCloud.Parsing; + +public class WordListParser : IParser +{ + public string TypeOfParsing => "Список слов (по одному в строке)"; + + public Result Parse(Func> getTextLineByLine) + { + var countingDictionary = new Dictionary(); + + foreach (var line in getTextLineByLine()) + { + if (line.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length > 1) + return Result.Fail("Формат содержания файла не соответствует настройкам." + + " Ожидалось, что в файле будут слова по одному в строке"); + var word = line.Trim().ToLower(); + if (word == string.Empty) continue; + if (!countingDictionary.TryAdd(word, 1)) + countingDictionary[word]++; + } + + var result = countingDictionary + .Select(pair => WordInfo.Create(pair.Key, pair.Value).GetValueOrThrow()) + .ToArray(); + + return result.Length == 0 ? Result.Fail("Файл оказался пустым") : Result.Ok(result); + } +} \ No newline at end of file diff --git a/TagCloud/Parsing/mystem.exe b/TagCloud/Parsing/mystem.exe new file mode 100644 index 000000000..e7158ff1b Binary files /dev/null and b/TagCloud/Parsing/mystem.exe differ diff --git a/TagCloud/ReadingFiles/IReader.cs b/TagCloud/ReadingFiles/IReader.cs new file mode 100644 index 000000000..fe43e6dac --- /dev/null +++ b/TagCloud/ReadingFiles/IReader.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; +using ErrorHandling; + +namespace TagCloud.ReadingFiles; + +public interface IReader +{ + public ImmutableHashSet SupportedExtensions { get; } + public IEnumerable ReadTextLineByLine(string pathToFile); + public ActionStatus PerformFileReadValidation(string pathToFile); +} \ No newline at end of file diff --git a/TagCloud/ReadingFiles/IReaderProvider.cs b/TagCloud/ReadingFiles/IReaderProvider.cs new file mode 100644 index 000000000..966d9a3b1 --- /dev/null +++ b/TagCloud/ReadingFiles/IReaderProvider.cs @@ -0,0 +1,9 @@ +using ErrorHandling; + +namespace TagCloud.ReadingFiles; + +public interface IReaderProvider +{ + public IEnumerable GetSupportedExtensions(); + public Result>> GetReader(string pathToFile); +} \ No newline at end of file diff --git a/TagCloud/ReadingFiles/ReaderPicker.cs b/TagCloud/ReadingFiles/ReaderPicker.cs new file mode 100644 index 000000000..3ddb60f79 --- /dev/null +++ b/TagCloud/ReadingFiles/ReaderPicker.cs @@ -0,0 +1,31 @@ +using ErrorHandling; + +namespace TagCloud.ReadingFiles; + +public class ReaderPicker : IReaderProvider +{ + private readonly Dictionary _readers = new(); + + public ReaderPicker(IEnumerable readers) + { + ArgumentNullException.ThrowIfNull(readers); + foreach (var reader in readers) + foreach (var extension in reader.SupportedExtensions) + _readers[extension] = reader; + } + + public IEnumerable GetSupportedExtensions() => _readers.Keys; + + public Result>> GetReader(string pathToFile) + { + var fileExtension = Path.GetExtension(pathToFile); + if (!_readers.TryGetValue(fileExtension, out var reader)) + return Result.Fail>>($"Не найден подходящий {nameof(IReader)} " + + $"для файла с расширением {fileExtension}"); + + var validationResult = reader.PerformFileReadValidation(pathToFile); + return !validationResult.IsSuccess + ? Result.Fail>>(validationResult.Error) + : Result.Ok>>(() => reader.ReadTextLineByLine(pathToFile)); + } +} \ No newline at end of file diff --git a/TagCloud/ReadingFiles/TxtReader.cs b/TagCloud/ReadingFiles/TxtReader.cs new file mode 100644 index 000000000..9cff167d9 --- /dev/null +++ b/TagCloud/ReadingFiles/TxtReader.cs @@ -0,0 +1,38 @@ +using System.Collections.Immutable; +using ErrorHandling; + +namespace TagCloud.ReadingFiles; + +public class TxtReader : IReader +{ + public ImmutableHashSet SupportedExtensions { get; } = [ ".txt" ]; + + public IEnumerable ReadTextLineByLine(string pathToFile) + { + using var reader = new StreamReader(pathToFile); + while (reader.ReadLine() is { } line) + yield return line.Trim(); + } + + public ActionStatus PerformFileReadValidation(string pathToFile) + { + var fileExtension = Path.GetExtension(pathToFile); + if (!SupportedExtensions.Contains(fileExtension)) + ActionStatus.Fail($"{nameof(TxtReader)} не поддерживает {fileExtension} формат файлов"); + if (!Path.Exists(pathToFile)) + ActionStatus.Fail($"Файл {pathToFile} не существует или поврежден"); + + try + { + foreach (var _ in ReadTextLineByLine(pathToFile)) + { + } + } + catch (Exception ex) + { + return ActionStatus.Fail($"Ошибка чтения файла: {ex.Message}"); + } + + return ActionStatus.Ok(); + } +} \ No newline at end of file diff --git a/TagCloud/TagCloud.csproj b/TagCloud/TagCloud.csproj new file mode 100644 index 000000000..20de80d42 --- /dev/null +++ b/TagCloud/TagCloud.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + + Always + + + + + + + + diff --git a/fp.sln b/fp.sln index d104ab530..f8b0dde2c 100644 --- a/fp.sln +++ b/fp.sln @@ -10,6 +10,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Summator", "Samples\Summato EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConwaysGameOfLife", "Samples\ConwaysGameOfLife\ConwaysGameOfLife.csproj", "{4B77EC28-5FB5-4095-B3D7-127F5C488D6E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TagCloud", "TagCloud", "{B83F517C-F000-41C9-A493-0BB0C797F66C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud", "TagCloud\TagCloud.csproj", "{90C0F363-183D-413C-B316-6C1B6EA39F27}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud.GUI", "TagCloud.GUI\TagCloud.GUI.csproj", "{C808C033-A649-472B-8557-A09F013ECE3E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloud.Tests", "TagCloud.Tests\TagCloud.Tests.csproj", "{3BF8A576-A605-404D-8617-6421BEACD34F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,9 +40,24 @@ Global {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {90C0F363-183D-413C-B316-6C1B6EA39F27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90C0F363-183D-413C-B316-6C1B6EA39F27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90C0F363-183D-413C-B316-6C1B6EA39F27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90C0F363-183D-413C-B316-6C1B6EA39F27}.Release|Any CPU.Build.0 = Release|Any CPU + {C808C033-A649-472B-8557-A09F013ECE3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C808C033-A649-472B-8557-A09F013ECE3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C808C033-A649-472B-8557-A09F013ECE3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C808C033-A649-472B-8557-A09F013ECE3E}.Release|Any CPU.Build.0 = Release|Any CPU + {3BF8A576-A605-404D-8617-6421BEACD34F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BF8A576-A605-404D-8617-6421BEACD34F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BF8A576-A605-404D-8617-6421BEACD34F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BF8A576-A605-404D-8617-6421BEACD34F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {C33F3A5E-A1ED-4657-9B35-968A4CB23AA1} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0} {4B77EC28-5FB5-4095-B3D7-127F5C488D6E} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0} + {90C0F363-183D-413C-B316-6C1B6EA39F27} = {B83F517C-F000-41C9-A493-0BB0C797F66C} + {C808C033-A649-472B-8557-A09F013ECE3E} = {B83F517C-F000-41C9-A493-0BB0C797F66C} + {3BF8A576-A605-404D-8617-6421BEACD34F} = {B83F517C-F000-41C9-A493-0BB0C797F66C} EndGlobalSection EndGlobal