Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Попов Захар #18

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6a2688d
Черновой вариант архитектуры.
BlizPerfect Dec 19, 2024
8b6b473
Реализован функционал, отвечающий за отрисовку прямоугольников.
BlizPerfect Dec 29, 2024
dd65959
Релизован функционал расстановки прямоугольников по форме окружности.
BlizPerfect Dec 29, 2024
8a2b6fb
Реализован функционал получения свойств следующего в расстановке прям…
BlizPerfect Dec 29, 2024
2ea4783
Релизован функционал сохранения итогового изображения.
BlizPerfect Dec 29, 2024
ea9307c
Релизован функционал нормализации частоты уникальных слов.
BlizPerfect Dec 29, 2024
39ce03f
Реализован функционал подсчёта уникальных слов.
BlizPerfect Dec 29, 2024
416e7b9
Реализован функционал фильтрации слов.
BlizPerfect Dec 29, 2024
d80488e
Добавлена сущность Tag.
BlizPerfect Dec 29, 2024
cd8e201
Содержимое тестового файла.
BlizPerfect Dec 29, 2024
7013070
Рефакторинг кода.
BlizPerfect Dec 29, 2024
3f63588
Написаны тесты.
BlizPerfect Dec 29, 2024
b99e3ac
Рефакторинг кода FontParser.
BlizPerfect Jan 14, 2025
6d7e951
Бизнес логика из контейнера выделена в отдельные классы.
BlizPerfect Jan 14, 2025
1a21dbe
Добавлены новые аргументы для командной строки.
BlizPerfect Jan 14, 2025
e8506d6
Общий рефакторинг кода.
BlizPerfect Jan 14, 2025
fe52ceb
Рефакторинг опций wordsToIncludeFile и wordsToExcludeFile
BlizPerfect Jan 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Drawing;
using TagCloud.CloudLayouterPainters;

namespace TagCloud.Tests.CloudLayouterPaintersTest
{
internal class CloudLayouterPainterTest
{
private CloudLayouterPainter painter;

[SetUp]
public void SetUp()
{
painter = new CloudLayouterPainter(new Size(1, 1));
}

[Test]
public void Draw_ThrowsArgumentException_WithEmptyTags()
{
var painter = new CloudLayouterPainter(new Size(1, 1));
Assert.Throws<ArgumentException>(() => painter.Draw(new List<Tag>()));
}

[Test]
public void Draw_ThrowsArgumentNullException_WithTagsAsNull()
{
var painter = new CloudLayouterPainter(new Size(1, 1));
Assert.Throws<ArgumentNullException>(() => painter.Draw(null!));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using FluentAssertions;
using System.Drawing;
using TagCloud.CloudLayouterPainters;
using TagCloud.CloudLayouters.CircularCloudLayouter;
using TagCloud.CloudLayouterWorkers;
using TagCloud.ImageSavers;
using TagCloud.Tests.Extensions;

namespace TagCloud.Tests.CloudLayouterTests.CircularCloudLayouterTests
{
[TestFixture]
internal class CircularCloudLayouterMainRequirementsTest
{
private Point center = new Point();
private Rectangle[] rectangles;
private List<Tag> tags;
private readonly string failedTestsDirectory = "FailedTest";

private readonly ImageSaver imageSaver = new ImageSaver();
private readonly CloudLayouterPainter cloudLayouterPainter
= new CloudLayouterPainter(new Size(5000, 5000));

[OneTimeSetUp]
public void Init()
{
Directory.CreateDirectory(failedTestsDirectory);
}

[SetUp]
public void SetUp()
{
var minRectangleWidth = 30;
var maxRectangleWidth = 70;
var minRectangleHeight = 20;
var maxRectangleHeight = 50;
var rectanglesCount = 1000;

tags = new List<Tag>();
var circularCloudLayouter = new CircularCloudLayouter();

var randomWorker = new RandomCloudLayouterWorker(
minRectangleWidth,
maxRectangleWidth,
minRectangleHeight,
maxRectangleHeight);
foreach (var rectangleProperty in randomWorker
.GetNextRectangleProperties().Take(rectanglesCount))
{
tags.Add(
new Tag(
rectangleProperty.word,
circularCloudLayouter.PutNextRectangle(rectangleProperty.size)));
}
rectangles = tags.Select(x => x.Rectangle).ToArray();
}

[TestCase(0.7, 1000)]
[Repeat(10)]
public void ShouldPlaceRectanglesInCircle(double expectedCoverageRatio, int gridSize)
{
var maxRadius = rectangles.Max(
x => x.GetMaxDistanceFromPointToRectangleAngles(center));
var step = 2 * maxRadius / gridSize;

var occupancyGrid = GetOccupancyGrid(gridSize, maxRadius, step);

var actualCoverageRatio = GetOccupancyGridRatio(occupancyGrid, maxRadius, step);
actualCoverageRatio.Should().BeGreaterThanOrEqualTo(expectedCoverageRatio);
}

[TestCase(15)]
[Repeat(10)]
public void ShouldPlaceCenterOfMassOfRectanglesNearCenter(int tolerance)
{
var centerX = rectangles.Average(r => r.Left + r.Width / 2.0);
var centerY = rectangles.Average(r => r.Top + r.Height / 2.0);
var actualCenter = new Point((int)centerX, (int)centerY);

var distance = Math.Sqrt(Math.Pow(actualCenter.X - center.X, 2)
+ Math.Pow(actualCenter.Y - center.Y, 2));

distance.Should().BeLessThanOrEqualTo(tolerance);
}

[Test]
[Repeat(10)]
public void ShouldPlaceRectanglesWithoutOverlap()
{
for (var i = 0; i < rectangles.Length; i++)
{
for (var j = i + 1; j < rectangles.Length; j++)
{
Assert.That(
rectangles[i].IntersectsWith(rectangles[j]) == false,
$"Прямоугольники пересекаются:\n" +
$"{rectangles[i].ToString()}\n" +
$"{rectangles[j].ToString()}");
}
}
}

[TearDown]
public void Cleanup()
{
if (TestContext.CurrentContext.Result.FailCount == 0)
{
return;
}

var name = $"{TestContext.CurrentContext.Test.Name}.png";
var path = Path.Combine(failedTestsDirectory, name);
imageSaver.SaveFile(cloudLayouterPainter.Draw(tags), path);
Console.WriteLine($"Tag cloud visualization saved to file {path}");
}

[OneTimeTearDown]
public void OneTimeCleanup()
{
if (Directory.Exists(failedTestsDirectory)
&& Directory.GetFiles(failedTestsDirectory).Length == 0)
{
Directory.Delete(failedTestsDirectory);
}
}

private (int start, int end) GetGridIndexesInterval(
int rectangleStartValue,
int rectangleCorrespondingSize,
double maxRadius,
double step)
{
var start = (int)((rectangleStartValue - center.X + maxRadius) / step);
var end = (int)((rectangleStartValue
+ rectangleCorrespondingSize - center.X + maxRadius) / step);
return (start, end);
}

private bool[,] GetOccupancyGrid(int gridSize, double maxRadius, double step)
{
var result = new bool[gridSize, gridSize];
foreach (var rect in rectangles)
{
var xInterval = GetGridIndexesInterval(rect.X, rect.Width, maxRadius, step);
var yInterval = GetGridIndexesInterval(rect.Y, rect.Height, maxRadius, step);
for (var x = xInterval.start; x <= xInterval.end; x++)
{
for (var y = yInterval.start; y <= yInterval.end; y++)
{
result[x, y] = true;
}
}
}
return result;
}

private double GetOccupancyGridRatio(bool[,] occupancyGrid, double maxRadius, double step)
{
var totalCellsInsideCircle = 0;
var coveredCellsInsideCircle = 0;
for (var x = 0; x < occupancyGrid.GetLength(0); x++)
{
for (var y = 0; y < occupancyGrid.GetLength(0); y++)
{
var cellCenterX = x * step - maxRadius + center.X;
var cellCenterY = y * step - maxRadius + center.Y;

var distance = Math.Sqrt(
Math.Pow(cellCenterX - center.X, 2) + Math.Pow(cellCenterY - center.Y, 2));

if (distance > maxRadius)
{
continue;
}

totalCellsInsideCircle += 1;
if (occupancyGrid[x, y])
{
coveredCellsInsideCircle += 1;
}
}
}
return (double)coveredCellsInsideCircle / totalCellsInsideCircle;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Drawing;
using TagCloud.CloudLayouters.CircularCloudLayouter;

namespace TagCloud.Tests.CloudLayouterTests.CircularCloudLayouterTests
{
[TestFixture]
internal class CircularCloudLayouterTest
{
[TestCase(0, 100)]
[TestCase(-1, 100)]
[TestCase(100, 0)]
[TestCase(100, -1)]
public void PutNextRectangle_ThrowsArgumentException_OnAnyNegativeOrZeroSize(
int width,
int height)
{
var size = new Size(width, height);
Assert.Throws<ArgumentException>(
() => new CircularCloudLayouter().PutNextRectangle(size));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using FluentAssertions;
using System.Drawing;
using TagCloud.CloudLayouterWorkers;

namespace TagCloud.Tests.CloudLayouterWorkersTests
{
internal class NormalizedFrequencyBasedCloudLayouterWorkerTest
{
private readonly Dictionary<string, double> normalizedValues
= new Dictionary<string, double>
{
{ "three", 0.625 },
{ "one", 0.25 },
{ "two", 0.2917 },
{ "four", 1.0 },
};

[TestCase(0, 100)]
[TestCase(-1, 100)]
[TestCase(100, 0)]
[TestCase(100, -1)]
public void GetNextRectangleSize_ThrowsArgumentException_OnAnyNegativeOrZeroSize(
int width,
int height)
{
Assert.Throws<ArgumentException>(
() => new NormalizedFrequencyBasedCloudLayouterWorker(width, height, normalizedValues));
}

[TestCase(100, 25, false)]
[TestCase(100, 25, true)]
public void GetNextRectangleSize_WorksCorrectly(int width, int height, bool isSortedOrder)
{
var index = 0;
string[]? keys = null;
if (isSortedOrder)
{
keys = normalizedValues.OrderByDescending(x => x.Value).Select(x => x.Key).ToArray();
}
else
{
keys = normalizedValues.Keys.ToArray();
}

var worker = new NormalizedFrequencyBasedCloudLayouterWorker(
width,
height,
normalizedValues,
isSortedOrder);
foreach (var rectangleSize in worker
.GetNextRectangleProperties())
{
var currentValue = normalizedValues[keys[index]];
var expected = new Size((int)(currentValue * width), (int)(currentValue * height));
index += 1;
rectangleSize.size.Should().BeEquivalentTo(expected);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using TagCloud.CloudLayouterWorkers;

namespace TagCloud.Tests.CloudLayouterWorkersTests
{
[TestFixture]
internal class CircularCloudLayouterWorkerTests
{
[TestCase(0, 100)]
[TestCase(-1, 100)]
[TestCase(100, 0)]
[TestCase(100, -1)]
public void GetNextRectangleSize_ThrowsArgumentException_OnAnyNegativeOrZeroSize(
int width,
int height)
{
Assert.Throws<ArgumentException>(
() => new RandomCloudLayouterWorker(width, width, height, height));
}

[TestCase(50, 25, 25, 50)]
[TestCase(25, 50, 50, 25)]
public void GetNextRectangleSize_ThrowsArgumentException_OnNonConsecutiveSizeValues(
int minWidth,
int maxWidth,
int minHeight,
int maxHeight)
{
Assert.Throws<ArgumentException>(
() => new RandomCloudLayouterWorker(minWidth, maxWidth, minHeight, maxHeight));
}
}
}
20 changes: 20 additions & 0 deletions TagCloud.Tests/Extensions/RectangleExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Drawing;

namespace TagCloud.Tests.Extensions
{
internal static class RectangleExtensions
{
public static double GetMaxDistanceFromPointToRectangleAngles(
this Rectangle rectangle,
Point point)
{
var dx = Math.Max(
Math.Abs(rectangle.X - point.X),
Math.Abs(rectangle.X + rectangle.Width - point.X));
var dy = Math.Max(
Math.Abs(rectangle.Y - point.Y),
Math.Abs(rectangle.Y + rectangle.Height - point.Y));
return Math.Sqrt(dx * dx + dy * dy);
}
}
}
10 changes: 10 additions & 0 deletions TagCloud.Tests/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

// Так делать явно плохо
[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.Tests.ImageSaversTests.ImageSaverTest.SaveFile_SavesFile(System.String,System.String)~System.Boolean")]
[assembly: SuppressMessage("Interoperability", "CA1416:Проверка совместимости платформы", Justification = "<Ожидание>", Scope = "member", Target = "~M:TagCloud.Tests.ImageSaversTests.ImageSaverTest.SaveFile_ThrowsArgumentException_WithInvalidFilename(System.String)")]
Loading