Skip to content

Commit 6b77caa

Browse files
committed
Merge branch 'master' into feature/k-nearest-neighbors
2 parents a486ee2 + 7467fe4 commit 6b77caa

21 files changed

+747
-100
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using NUnit.Framework;
2+
using Algorithms.MachineLearning;
3+
using System;
4+
5+
namespace Algorithms.Tests.MachineLearning;
6+
7+
[TestFixture]
8+
public class LogisticRegressionTests
9+
{
10+
[Test]
11+
public void Fit_ThrowsOnEmptyInput()
12+
{
13+
var model = new LogisticRegression();
14+
Assert.Throws<ArgumentException>(() => model.Fit(Array.Empty<double[]>(), Array.Empty<int>()));
15+
}
16+
17+
[Test]
18+
public void Fit_ThrowsOnMismatchedLabels()
19+
{
20+
var model = new LogisticRegression();
21+
double[][] X = { new double[] { 1, 2 } };
22+
int[] y = { 1, 0 };
23+
Assert.Throws<ArgumentException>(() => model.Fit(X, y));
24+
}
25+
26+
[Test]
27+
public void FitAndPredict_WorksOnSimpleData()
28+
{
29+
// Simple AND logic
30+
double[][] X =
31+
{
32+
new[] { 0.0, 0.0 },
33+
new[] { 0.0, 1.0 },
34+
new[] { 1.0, 0.0 },
35+
new[] { 1.0, 1.0 }
36+
};
37+
int[] y = { 0, 0, 0, 1 };
38+
var model = new LogisticRegression();
39+
model.Fit(X, y, epochs: 2000, learningRate: 0.1);
40+
Assert.That(model.Predict(new double[] { 0, 0 }), Is.EqualTo(0));
41+
Assert.That(model.Predict(new double[] { 0, 1 }), Is.EqualTo(0));
42+
Assert.That(model.Predict(new double[] { 1, 0 }), Is.EqualTo(0));
43+
Assert.That(model.Predict(new double[] { 1, 1 }), Is.EqualTo(1));
44+
}
45+
46+
[Test]
47+
public void PredictProbability_ThrowsOnFeatureMismatch()
48+
{
49+
var model = new LogisticRegression();
50+
double[][] X = { new double[] { 1, 2 } };
51+
int[] y = { 1 };
52+
model.Fit(X, y);
53+
Assert.Throws<ArgumentException>(() => model.PredictProbability(new double[] { 1 }));
54+
}
55+
56+
[Test]
57+
public void FeatureCount_ReturnsCorrectValue()
58+
{
59+
var model = new LogisticRegression();
60+
double[][] X = { new double[] { 1, 2, 3 } };
61+
int[] y = { 1 };
62+
model.Fit(X, y);
63+
Assert.That(model.FeatureCount, Is.EqualTo(3));
64+
}
65+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Algorithms.Problems.JobScheduling;
4+
5+
namespace Algorithms.Tests.Problems.JobScheduling;
6+
7+
public class IntervalSchedulingSolverTests
8+
{
9+
[Test]
10+
public void Schedule_ReturnsEmpty_WhenNoJobs()
11+
{
12+
var result = IntervalSchedulingSolver.Schedule(new List<Job>());
13+
Assert.That(result, Is.Empty);
14+
}
15+
16+
[Test]
17+
public void Schedule_ReturnsSingleJob_WhenOnlyOneJob()
18+
{
19+
var jobs = new List<Job> { new Job(1, 3) };
20+
var result = IntervalSchedulingSolver.Schedule(jobs);
21+
Assert.That(result, Has.Count.EqualTo(1));
22+
Assert.That(result[0], Is.EqualTo(jobs[0]));
23+
}
24+
25+
[Test]
26+
public void Schedule_ThrowsArgumentNullException_WhenJobsIsNull()
27+
{
28+
Assert.Throws<ArgumentNullException>(() => IntervalSchedulingSolver.Schedule(jobs: null!));
29+
}
30+
31+
[Test]
32+
public void Schedule_SelectsJobsWithEqualEndTime()
33+
{
34+
var jobs = new List<Job>
35+
{
36+
new Job(1, 4),
37+
new Job(2, 4),
38+
new Job(4, 6)
39+
};
40+
var result = IntervalSchedulingSolver.Schedule(jobs);
41+
Assert.That(result, Has.Count.EqualTo(2));
42+
Assert.That(result, Does.Contain(new Job(1, 4)));
43+
Assert.That(result, Does.Contain(new Job(4, 6)));
44+
}
45+
46+
[Test]
47+
public void Schedule_SelectsJobStartingAtLastEnd()
48+
{
49+
var jobs = new List<Job>
50+
{
51+
new Job(1, 3),
52+
new Job(3, 5),
53+
new Job(5, 7)
54+
};
55+
var result = IntervalSchedulingSolver.Schedule(jobs);
56+
Assert.That(result, Has.Count.EqualTo(3));
57+
Assert.That(result[0], Is.EqualTo(jobs[0]));
58+
Assert.That(result[1], Is.EqualTo(jobs[1]));
59+
Assert.That(result[2], Is.EqualTo(jobs[2]));
60+
}
61+
62+
[Test]
63+
public void Schedule_HandlesJobsWithNegativeTimes()
64+
{
65+
var jobs = new List<Job>
66+
{
67+
new Job(-5, -3),
68+
new Job(-2, 1),
69+
new Job(0, 2)
70+
};
71+
var result = IntervalSchedulingSolver.Schedule(jobs);
72+
Assert.That(result, Has.Count.EqualTo(2));
73+
Assert.That(result, Does.Contain(new Job(-5, -3)));
74+
Assert.That(result, Does.Contain(new Job(-2, 1)));
75+
}
76+
77+
[Test]
78+
public void Schedule_SelectsNonOverlappingJobs()
79+
{
80+
var jobs = new List<Job>
81+
{
82+
new Job(1, 4),
83+
new Job(3, 5),
84+
new Job(0, 6),
85+
new Job(5, 7),
86+
new Job(8, 9),
87+
new Job(5, 9)
88+
};
89+
var result = IntervalSchedulingSolver.Schedule(jobs);
90+
// Expected: (1,4), (5,7), (8,9)
91+
Assert.That(result, Has.Count.EqualTo(3));
92+
Assert.That(result, Does.Contain(new Job(1, 4)));
93+
Assert.That(result, Does.Contain(new Job(5, 7)));
94+
Assert.That(result, Does.Contain(new Job(8, 9)));
95+
}
96+
97+
[Test]
98+
public void Schedule_HandlesFullyOverlappingJobs()
99+
{
100+
var jobs = new List<Job>
101+
{
102+
new Job(1, 5),
103+
new Job(2, 6),
104+
new Job(3, 7)
105+
};
106+
var result = IntervalSchedulingSolver.Schedule(jobs);
107+
// Only one job can be selected
108+
Assert.That(result, Has.Count.EqualTo(1));
109+
}
110+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using Algorithms.Problems.TravelingSalesman;
2+
using NUnit.Framework;
3+
4+
namespace Algorithms.Tests.Problems.TravelingSalesman;
5+
6+
/// <summary>
7+
/// Unit tests for TravelingSalesmanSolver. Covers brute-force and nearest neighbor methods, including edge cases and invalid input.
8+
/// </summary>
9+
[TestFixture]
10+
public class TravelingSalesmanSolverTests
11+
{
12+
/// <summary>
13+
/// Tests brute-force TSP solver on a 4-city symmetric distance matrix with known optimal route.
14+
/// </summary>
15+
[Test]
16+
public void SolveBruteForce_KnownOptimalRoute_ReturnsCorrectResult()
17+
{
18+
// Distance matrix for 4 cities (symmetric, triangle inequality holds)
19+
double[,] matrix =
20+
{
21+
{ 0, 10, 15, 20 },
22+
{ 10, 0, 35, 25 },
23+
{ 15, 35, 0, 30 },
24+
{ 20, 25, 30, 0 }
25+
};
26+
var (route, distance) = TravelingSalesmanSolver.SolveBruteForce(matrix);
27+
// Optimal route: 0 -> 1 -> 3 -> 2 -> 0, total distance = 80
28+
Assert.That(distance, Is.EqualTo(80));
29+
Assert.That(route, Is.EquivalentTo(new[] { 0, 1, 3, 2, 0 }));
30+
}
31+
32+
/// <summary>
33+
/// Tests nearest neighbor heuristic on the same matrix. May not be optimal.
34+
/// </summary>
35+
[Test]
36+
public void SolveNearestNeighbor_Heuristic_ReturnsFeasibleRoute()
37+
{
38+
double[,] matrix =
39+
{
40+
{ 0, 10, 15, 20 },
41+
{ 10, 0, 35, 25 },
42+
{ 15, 35, 0, 30 },
43+
{ 20, 25, 30, 0 }
44+
};
45+
var (route, distance) = TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0);
46+
// Route: 0 -> 1 -> 3 -> 2 -> 0, total distance = 80
47+
Assert.That(route.Length, Is.EqualTo(5));
48+
Assert.That(route.First(), Is.EqualTo(0));
49+
Assert.That(route.Last(), Is.EqualTo(0));
50+
Assert.That(distance, Is.GreaterThanOrEqualTo(80)); // Heuristic may be optimal or suboptimal
51+
}
52+
53+
/// <summary>
54+
/// Tests nearest neighbor with invalid start index.
55+
/// </summary>
56+
[Test]
57+
public void SolveNearestNeighbor_InvalidStart_ThrowsException()
58+
{
59+
double[,] matrix =
60+
{
61+
{ 0, 1 },
62+
{ 1, 0 }
63+
};
64+
Assert.Throws<ArgumentOutOfRangeException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, -1));
65+
Assert.Throws<ArgumentOutOfRangeException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 2));
66+
}
67+
68+
/// <summary>
69+
/// Tests nearest neighbor when no unvisited cities remain (should throw InvalidOperationException).
70+
/// </summary>
71+
[Test]
72+
public void SolveNearestNeighbor_NoUnvisitedCities_ThrowsException()
73+
{
74+
// Construct a matrix where one city cannot be reached (simulate unreachable city)
75+
double[,] matrix =
76+
{
77+
{ 0, double.MaxValue, 1 },
78+
{ double.MaxValue, 0, double.MaxValue },
79+
{ 1, double.MaxValue, 0 }
80+
};
81+
// Start at city 0, city 1 is unreachable from both 0 and 2
82+
Assert.Throws<InvalidOperationException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0));
83+
}
84+
85+
/// <summary>
86+
/// Tests brute-force and nearest neighbor with non-square matrix.
87+
/// </summary>
88+
[Test]
89+
public void NonSquareMatrix_ThrowsException()
90+
{
91+
double[,] matrix = new double[2, 3];
92+
Assert.Throws<ArgumentException>(() => TravelingSalesmanSolver.SolveBruteForce(matrix));
93+
Assert.Throws<ArgumentException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0));
94+
}
95+
96+
/// <summary>
97+
/// Tests brute-force with less than two cities (invalid case).
98+
/// </summary>
99+
[Test]
100+
public void SolveBruteForce_TooFewCities_ThrowsException()
101+
{
102+
double[,] matrix = { { 0 } };
103+
Assert.Throws<ArgumentException>(() => TravelingSalesmanSolver.SolveBruteForce(matrix));
104+
}
105+
106+
/// <summary>
107+
/// Tests nearest neighbor with only two cities (trivial case).
108+
/// </summary>
109+
[Test]
110+
public void SolveNearestNeighbor_TwoCities_ReturnsCorrectRoute()
111+
{
112+
double[,] matrix =
113+
{
114+
{ 0, 5 },
115+
{ 5, 0 }
116+
};
117+
var (route, distance) = TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0);
118+
Assert.That(route, Is.EquivalentTo(new[] { 0, 1, 0 }));
119+
Assert.That(distance, Is.EqualTo(10));
120+
}
121+
}

Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,71 @@ public void PredictRating_WithOtherUserHavingRatedTargetItem_ShouldCalculateSimi
8989
Assert.That(predictedRating, Is.Not.EqualTo(0.0d));
9090
Assert.That(predictedRating, Is.EqualTo(3.5d).Within(0.01));
9191
}
92+
93+
[Test]
94+
public void PredictRating_TargetUserNotExist_ThrowsOrReturnsZero()
95+
{
96+
Assert.Throws<KeyNotFoundException>(() => recommender!.PredictRating("item1", "nonexistentUser", testRatings));
97+
}
98+
99+
[Test]
100+
public void PredictRating_RatingsEmpty_ReturnsZero()
101+
{
102+
var emptyRatings = new Dictionary<string, Dictionary<string, double>>();
103+
Assert.Throws<KeyNotFoundException>(() => recommender!.PredictRating("item1", "user1", emptyRatings));
104+
}
105+
106+
[Test]
107+
public void PredictRating_NoOtherUserRatedTargetItem_ReturnsZero()
108+
{
109+
var ratings = new Dictionary<string, Dictionary<string, double>>
110+
{
111+
["user1"] = new() { ["item1"] = 5.0 },
112+
["user2"] = new() { ["item2"] = 4.0 }
113+
};
114+
var recommenderLocal = new CollaborativeFiltering(mockSimilarityCalculator!.Object);
115+
var result = recommenderLocal.PredictRating("item2", "user1", ratings);
116+
Assert.That(result, Is.EqualTo(0));
117+
}
118+
119+
[Test]
120+
public void CalculateSimilarity_EmptyDictionaries_ReturnsZero()
121+
{
122+
var recommenderLocal = new CollaborativeFiltering(mockSimilarityCalculator!.Object);
123+
var result = recommenderLocal.CalculateSimilarity(new Dictionary<string, double>(), new Dictionary<string, double>());
124+
Assert.That(result, Is.EqualTo(0));
125+
}
126+
127+
[Test]
128+
public void CalculateSimilarity_OneCommonItem_ReturnsZero()
129+
{
130+
var recommenderLocal = new CollaborativeFiltering(mockSimilarityCalculator!.Object);
131+
var dict1 = new Dictionary<string, double> { ["item1"] = 5.0 };
132+
var dict2 = new Dictionary<string, double> { ["item1"] = 5.0 };
133+
var result = recommenderLocal.CalculateSimilarity(dict1, dict2);
134+
Assert.That(result, Is.EqualTo(0));
135+
}
136+
137+
[Test]
138+
public void PredictRating_MultipleUsersWeightedSum_CorrectCalculation()
139+
{
140+
var ratings = new Dictionary<string, Dictionary<string, double>>
141+
{
142+
["user1"] = new() { ["item1"] = 5.0 },
143+
["user2"] = new() { ["item1"] = 2.0 },
144+
["user3"] = new() { ["item1"] = 8.0 }
145+
};
146+
var mockSim = new Mock<ISimilarityCalculator>();
147+
mockSim.Setup(s => s.CalculateSimilarity(It.IsAny<Dictionary<string, double>>(), ratings["user2"]))
148+
.Returns(-0.5);
149+
mockSim.Setup(s => s.CalculateSimilarity(It.IsAny<Dictionary<string, double>>(), ratings["user3"]))
150+
.Returns(1.0);
151+
var recommenderLocal = new CollaborativeFiltering(mockSim.Object);
152+
var result = recommenderLocal.PredictRating("item1", "user1", ratings);
153+
// weightedSum = (-0.5*2.0) + (1.0*8.0) = -1.0 + 8.0 = 7.0
154+
// totalSimilarity = 0.5 + 1.0 = 1.5
155+
// result = 7.0 / 1.5 = 4.666...
156+
Assert.That(result, Is.EqualTo(4.666).Within(0.01));
157+
}
92158
}
93159
}

Algorithms.Tests/Sequences/DivisorsCountSequenceTests.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ public class DivisorsCountSequenceTests
88
public void First10ElementsCorrect()
99
{
1010
// These values are taken from https://oeis.org/A000005 for comparison.
11-
var oeisSource = new BigInteger[]
12-
{
13-
1, 2, 2, 3, 2, 4, 2, 4, 3, 4, 2, 6, 2,
14-
4, 4, 5, 2, 6, 2, 6, 4, 4, 2, 8, 3, 4,
15-
4, 6, 2, 8, 2, 6, 4, 4, 4, 9, 2, 4, 4,
16-
8, 2, 8, 2, 6, 6, 4, 2, 10, 3, 6, 4, 6,
17-
2, 8, 4, 8, 4, 4, 2, 12, 2, 4, 6, 7, 4,
18-
8, 2, 6, 4, 8, 2, 12, 2, 4, 6, 6, 4, 8,
19-
2, 10, 5, 4, 2, 12, 4, 4, 4, 8, 2, 12, 4,
20-
6, 4, 4, 4, 12, 2, 6, 6, 9, 2, 8, 2, 8,
21-
};
11+
BigInteger[] oeisSource =
12+
[
13+
1, 2, 2, 3, 2, 4, 2, 4, 3, 4, 2, 6, 2,
14+
4, 4, 5, 2, 6, 2, 6, 4, 4, 2, 8, 3, 4,
15+
4, 6, 2, 8, 2, 6, 4, 4, 4, 9, 2, 4, 4,
16+
8, 2, 8, 2, 6, 6, 4, 2, 10, 3, 6, 4, 6,
17+
2, 8, 4, 8, 4, 4, 2, 12, 2, 4, 6, 7, 4,
18+
8, 2, 6, 4, 8, 2, 12, 2, 4, 6, 6, 4, 8,
19+
2, 10, 5, 4, 2, 12, 4, 4, 4, 8, 2, 12, 4,
20+
6, 4, 4, 4, 12, 2, 6, 6, 9, 2, 8, 2, 8,
21+
];
2222

2323
var sequence = new DivisorsCountSequence().Sequence.Take(oeisSource.Length);
2424
sequence.SequenceEqual(oeisSource).Should().BeTrue();

0 commit comments

Comments
 (0)