From 9a0b5f5a35b7e1075fb31d6b14a84b5561529f10 Mon Sep 17 00:00:00 2001 From: Duc Thanh Nguyen Date: Tue, 22 Oct 2024 00:20:57 +0700 Subject: [PATCH 1/8] Recommender Systems by User-based Collaborative Filtering --- .../CollaborativeFilteringTests.cs | 76 ++++++++++++++++++ .../CollaborativeFiltering.cs | 80 +++++++++++++++++++ README.md | 2 + 3 files changed, 158 insertions(+) create mode 100644 Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs create mode 100644 Algorithms/RecommenderSystem/CollaborativeFiltering.cs diff --git a/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs new file mode 100644 index 00000000..b46dae92 --- /dev/null +++ b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs @@ -0,0 +1,76 @@ +using Algorithms.RecommenderSystem; +using NUnit.Framework; +using System.Collections.Generic; + +namespace Algorithms.Tests.RecommenderSystem +{ + [TestFixture] + public class CollaborativeFilteringTests + { + private CollaborativeFiltering recommender = new(); + private Dictionary> testRatings = null!; + + [SetUp] + public void Setup() + { + recommender = new CollaborativeFiltering(); + + testRatings = new Dictionary> + { + ["user1"] = new() + { + ["item1"] = 5.0, + ["item2"] = 3.0, + ["item3"] = 4.0 + }, + ["user2"] = new() + { + ["item1"] = 4.0, + ["item2"] = 2.0, + ["item3"] = 5.0 + }, + ["user3"] = new() + { + ["item1"] = 3.0, + ["item2"] = 4.0, + ["item4"] = 3.0 + } + }; + } + + [Test] + [TestCase("item1", 4.0, 5.0)] + [TestCase("item2", 2.0, 4.0)] + public void CalculateSimilarity_WithValidInputs_ReturnsExpectedResults( + string commonItem, + double rating1, + double rating2) + { + var user1Ratings = new Dictionary { [commonItem] = rating1 }; + var user2Ratings = new Dictionary { [commonItem] = rating2 }; + + var similarity = recommender.CalculateSimilarity(user1Ratings, user2Ratings); + + Assert.That(similarity, Is.InRange(-1.0, 1.0)); + } + + [Test] + public void CalculateSimilarity_WithNoCommonItems_ReturnsZero() + { + var user1Ratings = new Dictionary { ["item1"] = 5.0 }; + var user2Ratings = new Dictionary { ["item2"] = 4.0 }; + + var similarity = recommender.CalculateSimilarity(user1Ratings, user2Ratings); + + Assert.That(similarity, Is.EqualTo(0)); + } + + [Test] + public void PredictRating_WithNonexistentItem_ReturnsZero() + { + var predictedRating = recommender.PredictRating("nonexistentItem", "user1", testRatings); + + Assert.That(predictedRating, Is.EqualTo(0)); + } + } +} diff --git a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs new file mode 100644 index 00000000..cb5b5bd1 --- /dev/null +++ b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Algorithms.RecommenderSystem +{ + public class CollaborativeFiltering + { + /// + /// Method to calculate similarity between two users using Pearson correlation. + /// + /// Rating of User 1. + /// Rating of User 2. + /// double value to reflect the index of similarity between two users. + public double CalculateSimilarity(Dictionary user1Ratings, Dictionary user2Ratings) + { + var commonItems = user1Ratings.Keys.Intersect(user2Ratings.Keys).ToList(); + if (commonItems.Count == 0) + { + return 0; + } + + var user1Scores = commonItems.Select(item => user1Ratings[item]).ToArray(); + var user2Scores = commonItems.Select(item => user2Ratings[item]).ToArray(); + + var avgUser1 = user1Scores.Average(); + var avgUser2 = user2Scores.Average(); + + double numerator = 0; + double sumSquare1 = 0; + double sumSquare2 = 0; + + for (var i = 0; i < commonItems.Count; i++) + { + var diff1 = user1Scores[i] - avgUser1; + var diff2 = user2Scores[i] - avgUser2; + + numerator += diff1 * diff2; + sumSquare1 += diff1 * diff1; + sumSquare2 += diff2 * diff2; + } + + var denominator = Math.Sqrt(sumSquare1 * sumSquare2); + return denominator == 0 ? 0 : numerator / denominator; + } + + /// + /// Predict a rating for a specific item by a target user. + /// + /// The item for which the rating needs to be predicted. + /// The user for whom the rating is being predicted. + /// + /// A dictionary containing user ratings where: + /// - The key is the user's identifier (string). + /// - The value is another dictionary where the key is the item identifier (string), and the value is the rating given by the user (double). + /// + /// The predicted rating for the target item by the target user. + /// If there is insufficient data to predict a rating, the method returns 0. + /// + public double PredictRating(string targetItem, string targetUser, Dictionary> ratings) + { + var targetUserRatings = ratings[targetUser]; + double totalSimilarity = 0; + double weightedSum = 0; + + foreach (var otherUser in ratings.Keys.Where(u => u != targetUser)) + { + var otherUserRatings = ratings[otherUser]; + if (otherUserRatings.ContainsKey(targetItem)) + { + var similarity = CalculateSimilarity(targetUserRatings, otherUserRatings); + totalSimilarity += Math.Abs(similarity); + weightedSum += similarity * otherUserRatings[targetItem]; + } + } + + return totalSimilarity == 0 ? 0 : weightedSum / totalSimilarity; + } + } +} diff --git a/README.md b/README.md index f13516de..cefdef6b 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ find more than one implementation for the same objective but using different alg * [Josephus Problem](./Algorithms/Numeric/JosephusProblem.cs) * [Newton's Square Root Calculation](./Algorithms/NewtonSquareRoot.cs) * [SoftMax Function](./Algorithms/Numeric/SoftMax.cs) + * [RecommenderSystem](./Algorithms/RecommenderSystem) + * [CollaborativeFiltering](./Algorithms/RecommenderSystem/CollaborativeFiltering) * [Searches](./Algorithms/Search) * [A-Star](./Algorithms/Search/AStar/) * [Binary Search](./Algorithms/Search/BinarySearcher.cs) From d8b51719c26a46da6bfe49ecf11090c637258d7f Mon Sep 17 00:00:00 2001 From: Duc Thanh Nguyen Date: Tue, 22 Oct 2024 00:36:19 +0700 Subject: [PATCH 2/8] To handle floating-point precision issues in C# --- Algorithms/RecommenderSystem/CollaborativeFiltering.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs index cb5b5bd1..79f0f634 100644 --- a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs +++ b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs @@ -29,6 +29,7 @@ public double CalculateSimilarity(Dictionary user1Ratings, Dicti double numerator = 0; double sumSquare1 = 0; double sumSquare2 = 0; + double epsilon = 1e-10; for (var i = 0; i < commonItems.Count; i++) { @@ -41,7 +42,7 @@ public double CalculateSimilarity(Dictionary user1Ratings, Dicti } var denominator = Math.Sqrt(sumSquare1 * sumSquare2); - return denominator == 0 ? 0 : numerator / denominator; + return Math.Abs(denominator) < epsilon ? 0 : numerator / denominator; } /// @@ -62,6 +63,7 @@ public double PredictRating(string targetItem, string targetUser, Dictionary u != targetUser)) { @@ -74,7 +76,7 @@ public double PredictRating(string targetItem, string targetUser, Dictionary Date: Wed, 23 Oct 2024 09:01:23 +0700 Subject: [PATCH 3/8] add some test case --- Algorithms.Tests/Algorithms.Tests.csproj | 1 + .../CollaborativeFilteringTests.cs | 29 +++++++++++++++---- .../CollaborativeFiltering.cs | 9 +++++- .../ISimilarityCalculator.cs | 13 +++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 Algorithms/RecommenderSystem/ISimilarityCalculator.cs diff --git a/Algorithms.Tests/Algorithms.Tests.csproj b/Algorithms.Tests/Algorithms.Tests.csproj index 893564d8..d6d63755 100644 --- a/Algorithms.Tests/Algorithms.Tests.csproj +++ b/Algorithms.Tests/Algorithms.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs index b46dae92..208de6fb 100644 --- a/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs +++ b/Algorithms.Tests/RecommenderSystem/CollaborativeFilteringTests.cs @@ -1,4 +1,5 @@ using Algorithms.RecommenderSystem; +using Moq; using NUnit.Framework; using System.Collections.Generic; @@ -7,13 +8,15 @@ namespace Algorithms.Tests.RecommenderSystem [TestFixture] public class CollaborativeFilteringTests { - private CollaborativeFiltering recommender = new(); + private Mock? mockSimilarityCalculator; + private CollaborativeFiltering? recommender; private Dictionary> testRatings = null!; [SetUp] public void Setup() { - recommender = new CollaborativeFiltering(); + mockSimilarityCalculator = new Mock(); + recommender = new CollaborativeFiltering(mockSimilarityCalculator.Object); testRatings = new Dictionary> { @@ -49,7 +52,7 @@ public void CalculateSimilarity_WithValidInputs_ReturnsExpectedResults( var user1Ratings = new Dictionary { [commonItem] = rating1 }; var user2Ratings = new Dictionary { [commonItem] = rating2 }; - var similarity = recommender.CalculateSimilarity(user1Ratings, user2Ratings); + var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); Assert.That(similarity, Is.InRange(-1.0, 1.0)); } @@ -60,7 +63,7 @@ public void CalculateSimilarity_WithNoCommonItems_ReturnsZero() var user1Ratings = new Dictionary { ["item1"] = 5.0 }; var user2Ratings = new Dictionary { ["item2"] = 4.0 }; - var similarity = recommender.CalculateSimilarity(user1Ratings, user2Ratings); + var similarity = recommender?.CalculateSimilarity(user1Ratings, user2Ratings); Assert.That(similarity, Is.EqualTo(0)); } @@ -68,9 +71,25 @@ public void CalculateSimilarity_WithNoCommonItems_ReturnsZero() [Test] public void PredictRating_WithNonexistentItem_ReturnsZero() { - var predictedRating = recommender.PredictRating("nonexistentItem", "user1", testRatings); + var predictedRating = recommender?.PredictRating("nonexistentItem", "user1", testRatings); Assert.That(predictedRating, Is.EqualTo(0)); } + + [Test] + public void PredictRating_WithOtherUserHavingRatedTargetItem_ShouldCalculateSimilarityAndWeightedSum() + { + var targetItem = "item1"; + var targetUser = "user1"; + + mockSimilarityCalculator? + .Setup(s => s.CalculateSimilarity(It.IsAny>(), It.IsAny>())) + .Returns(0.8); + + var predictedRating = recommender?.PredictRating(targetItem, targetUser, testRatings); + + Assert.That(predictedRating, Is.Not.EqualTo(0.0d)); + Assert.That(predictedRating, Is.EqualTo(3.5d).Within(0.01)); + } } } diff --git a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs index 79f0f634..8da58b8a 100644 --- a/Algorithms/RecommenderSystem/CollaborativeFiltering.cs +++ b/Algorithms/RecommenderSystem/CollaborativeFiltering.cs @@ -6,6 +6,13 @@ namespace Algorithms.RecommenderSystem { public class CollaborativeFiltering { + private readonly ISimilarityCalculator similarityCalculator; + + public CollaborativeFiltering(ISimilarityCalculator similarityCalculator) + { + this.similarityCalculator = similarityCalculator; + } + /// /// Method to calculate similarity between two users using Pearson correlation. /// @@ -70,7 +77,7 @@ public double PredictRating(string targetItem, string targetUser, Dictionary user1Ratings, Dictionary user2Ratings); + } +} From 333a1971afb4b5a6d67b7b622b198cf0496d270e Mon Sep 17 00:00:00 2001 From: Duc Thanh Nguyen Date: Thu, 24 Oct 2024 08:57:37 +0700 Subject: [PATCH 4/8] Add the Geohash --- Algorithms.Tests/Other/GeohashTests.cs | 59 ++++++++++++++++++ Algorithms/Other/Geohash.cs | 84 ++++++++++++++++++++++++++ README.md | 1 + 3 files changed, 144 insertions(+) create mode 100644 Algorithms.Tests/Other/GeohashTests.cs create mode 100644 Algorithms/Other/Geohash.cs diff --git a/Algorithms.Tests/Other/GeohashTests.cs b/Algorithms.Tests/Other/GeohashTests.cs new file mode 100644 index 00000000..bf2cced4 --- /dev/null +++ b/Algorithms.Tests/Other/GeohashTests.cs @@ -0,0 +1,59 @@ +using Algorithms.Other; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.Tests.Other +{ + [TestFixture] + public class GeohashTests + { + [Test] + public void Encode_ShouldReturnCorrectGeohash_ForHoChiMinhCity() + { + double latitude = 10.8231; + double longitude = 106.6297; + string result = Geohash.Encode(latitude, longitude); + Assert.That(result, Is.EqualTo("w3gvd6m3hh54")); + } + + [Test] + public void Encode_ShouldReturnCorrectGeohash_ForHanoi() + { + double latitude = 21.0285; + double longitude = 105.8542; + string result = Geohash.Encode(latitude, longitude); + Assert.That(result, Is.EqualTo("w7er8u0evss2")); + } + + [Test] + public void Encode_ShouldReturnCorrectGeohash_ForDaNang() + { + double latitude = 16.0544; + double longitude = 108.2022; + string result = Geohash.Encode(latitude, longitude); + Assert.That(result, Is.EqualTo("w6ugq4w7wj04")); + } + + [Test] + public void Encode_ShouldReturnCorrectGeohash_ForNhaTrang() + { + double latitude = 12.2388; + double longitude = 109.1967; + string result = Geohash.Encode(latitude, longitude); + Assert.That(result, Is.EqualTo("w6jtsu485t8v")); + } + + [Test] + public void Encode_ShouldReturnCorrectGeohash_ForVungTau() + { + double latitude = 10.3460; + double longitude = 107.0843; + string result = Geohash.Encode(latitude, longitude); + Assert.That(result, Is.EqualTo("w3u4ug2mv41m")); + } + } +} diff --git a/Algorithms/Other/Geohash.cs b/Algorithms/Other/Geohash.cs new file mode 100644 index 00000000..4ef8923c --- /dev/null +++ b/Algorithms/Other/Geohash.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.Other +{ + public class Geohash + { + private static readonly string Base32Characters = "0123456789bcdefghjkmnpqrstuvwxyz"; // Convert latitude and longitude coordinates into a concise string + private static readonly int GeohashLength = 12; // ± 1.86 cm + + /// + /// Encodes the provided latitude and longitude coordinates into a Geohash string. + /// Geohashing is a method to encode geographic coordinates (latitude, longitude). + /// into a short string of letters and digits. Each character in the resulting Geohash . + /// string adds more precision to the location. The longer the Geohash, the smaller the area. + /// + /// The latitude of the location to encode. It must be a value between -90 and 90. + /// The longitude of the location to encode. It must be a value between -180 and 180. + /// + /// A Geohash string of length 12 representing the location with high precision. + /// A longer Geohash provides higher precision in terms of geographic area. + /// and a 12-character Geohash can be accurate down to around 1.86 cm. + /// + public static string Encode(double latitude, double longitude) + { + double[] latitudeRange = new[] { -90.0, 90.0 }; + double[] longitudeRange = new[] { -180.0, 180.0 }; + bool isEncodingLongitude = true; + int currentBit = 0; + int base32Index = 0; + StringBuilder geohashResult = new StringBuilder(); + + while (geohashResult.Length < GeohashLength) + { + double midpoint; + + if (isEncodingLongitude) + { + midpoint = (longitudeRange[0] + longitudeRange[1]) / 2; + if (longitude > midpoint) + { + base32Index |= 1 << (4 - currentBit); + longitudeRange[0] = midpoint; + } + else + { + longitudeRange[1] = midpoint; + } + } + else + { + midpoint = (latitudeRange[0] + latitudeRange[1]) / 2; + if (latitude > midpoint) + { + base32Index |= 1 << (4 - currentBit); + latitudeRange[0] = midpoint; + } + else + { + latitudeRange[1] = midpoint; + } + } + + isEncodingLongitude = !isEncodingLongitude; + + if (currentBit < 4) + { + currentBit++; + } + else + { + geohashResult.Append(Base32Characters[base32Index]); + currentBit = 0; + base32Index = 0; + } + } + + return geohashResult.ToString(); + } + } +} diff --git a/README.md b/README.md index f67c4356..c392e0d0 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ find more than one implementation for the same objective but using different alg * [Welford's Variance](./Algorithms/Other/WelfordsVariance.cs) * [Julian Easter](./Algorithms/Other/JulianEaster.cs) * [Pollard's Rho](./Algorithms/Other/PollardsRhoFactorizing.cs) + * [GeoLocation Hash](./Algorithms/Other/Geohash.cs) * [Problems](./Algorithms/Problems) * [Stable Marriage](./Algorithms/Problems/StableMarriage) * [Gale-Shapley](./Algorithms/Problems/StableMarriage/GaleShapley.cs) From a80e22fdc07b5a7ca354a683c9968dbbedec2fa6 Mon Sep 17 00:00:00 2001 From: Duc Thanh Nguyen Date: Thu, 24 Oct 2024 09:18:33 +0700 Subject: [PATCH 5/8] Add the Geohash and improve some code --- Algorithms/Other/Geohash.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Algorithms/Other/Geohash.cs b/Algorithms/Other/Geohash.cs index 4ef8923c..53507f85 100644 --- a/Algorithms/Other/Geohash.cs +++ b/Algorithms/Other/Geohash.cs @@ -6,10 +6,10 @@ namespace Algorithms.Other { - public class Geohash + public static class Geohash { - private static readonly string Base32Characters = "0123456789bcdefghjkmnpqrstuvwxyz"; // Convert latitude and longitude coordinates into a concise string - private static readonly int GeohashLength = 12; // ± 1.86 cm + private const string Base32Characters = "0123456789bcdefghjkmnpqrstuvwxyz"; // Convert latitude and longitude coordinates into a concise string + private const int GeohashLength = 12; // ± 1.86 cm /// /// Encodes the provided latitude and longitude coordinates into a Geohash string. From ef1dc8e25d61b5623b2a72916b60e5a2cf4e97b7 Mon Sep 17 00:00:00 2001 From: Duc Thanh Nguyen Date: Thu, 24 Oct 2024 13:24:27 +0700 Subject: [PATCH 6/8] Add the geofence boundary logic --- Algorithms.Tests/Other/GeofenceTests.cs | 66 +++++++++++++++++++++++++ Algorithms/Other/Geofence.cs | 37 ++++++++++++++ README.md | 1 + 3 files changed, 104 insertions(+) create mode 100644 Algorithms.Tests/Other/GeofenceTests.cs create mode 100644 Algorithms/Other/Geofence.cs diff --git a/Algorithms.Tests/Other/GeofenceTests.cs b/Algorithms.Tests/Other/GeofenceTests.cs new file mode 100644 index 00000000..30dcfff4 --- /dev/null +++ b/Algorithms.Tests/Other/GeofenceTests.cs @@ -0,0 +1,66 @@ +using Algorithms.Other; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.Tests.Other +{ + [TestFixture] + public class GeofenceTests + { + private Geofence? geofence; + + [SetUp] + public void Setup() + { + geofence = new Geofence(10.8231, 106.6297, 500); + } + + [Test] + public void IsInside_ShouldReturnTrue_WhenUserIsInsideGeofence() + { + double userLat = 10.8221; + double userLon = 106.6289; + + bool? result = geofence?.IsInside(userLat, userLon); + + Assert.That(result, Is.True); + } + + [Test] + public void IsInside_ShouldReturnFalse_WhenUserIsOutsideGeofence() + { + double userLat = 10.8300; + double userLon = 106.6400; + + bool? result = geofence?.IsInside(userLat, userLon); + + Assert.That(result, Is.False); + } + + [Test] + public void IsInside_ShouldReturnTrue_WhenUserIsExactlyOnGeofenceBoundary() + { + double userLat = 10.8231; + double userLon = 106.6297; + + bool? result = geofence?.IsInside(userLat, userLon); + + Assert.That(result, Is.True); + } + + [Test] + public void IsInside_ShouldReturnFalse_WhenUserIsFarFromGeofence() + { + double userLat = 20.0000; + double userLon = 100.0000; + + bool? result = geofence?.IsInside(userLat, userLon); + + Assert.That(result, Is.False); + } + } +} diff --git a/Algorithms/Other/Geofence.cs b/Algorithms/Other/Geofence.cs new file mode 100644 index 00000000..90fb9626 --- /dev/null +++ b/Algorithms/Other/Geofence.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.Other +{ + public class Geofence + { + public double Latitude { get; set; } + + public double Longitude { get; set; } + + public double RadiusInMeters { get; set; } + + public Geofence(double latitude, double longitude, double radiusInMeters) + { + Latitude = latitude; + Longitude = longitude; + RadiusInMeters = radiusInMeters; + } + + /// + /// Checks whether the provided user location (latitude and longitude) is within the geofence boundary. + /// The geofence is defined by a center point (latitude, longitude) and a radius in meters. + /// + /// The latitude of the user's current location. + /// The longitude of the user's current location. + /// Returns true if the user is inside the geofence, otherwise returns false. + public bool IsInside(double userLatitude, double userLongitude) + { + double distance = GeoLocation.CalculateDistanceFromLatLng(Latitude, Longitude, userLatitude, userLongitude); + return distance <= RadiusInMeters; + } + } +} diff --git a/README.md b/README.md index ddf2c1cc..a57b5826 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,7 @@ find more than one implementation for the same objective but using different alg * [Julian Easter](./Algorithms/Other/JulianEaster.cs) * [Pollard's Rho](./Algorithms/Other/PollardsRhoFactorizing.cs) * [GeoLocation Hash](./Algorithms/Other/Geohash.cs) + * [Geofencing](./Algorithms/Other/Geofence.cs) * [Problems](./Algorithms/Problems) * [Stable Marriage](./Algorithms/Problems/StableMarriage) * [Gale-Shapley](./Algorithms/Problems/StableMarriage/GaleShapley.cs) From b1751cea85a6437e18ea7ae30082df14a3fc3bcf Mon Sep 17 00:00:00 2001 From: Duc Thanh Nguyen Date: Thu, 24 Oct 2024 16:49:33 +0700 Subject: [PATCH 7/8] Add the Triangulation Algorithm for Estimating Your Location --- Algorithms.Tests/Other/TriangulatorTests.cs | 62 ++++++++++++++++++++ Algorithms/Other/Triangulator.cs | 65 +++++++++++++++++++++ README.md | 1 + 3 files changed, 128 insertions(+) create mode 100644 Algorithms.Tests/Other/TriangulatorTests.cs create mode 100644 Algorithms/Other/Triangulator.cs diff --git a/Algorithms.Tests/Other/TriangulatorTests.cs b/Algorithms.Tests/Other/TriangulatorTests.cs new file mode 100644 index 00000000..39014684 --- /dev/null +++ b/Algorithms.Tests/Other/TriangulatorTests.cs @@ -0,0 +1,62 @@ +using Algorithms.Other; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.Tests.Other +{ + [TestFixture] + public class TriangulatorTests + { + [Test] + public void CalculatePosition_ValidCoordinatesAndDistances_ReturnsExpectedPosition() + { + var triangulator = new Triangulator(); + var baseLocations = new List<(double Latitude, double Longitude)> + { + (16.054407, 108.202167), + (16.049807, 108.218991), + (16.063597, 108.215553) + }; + + var distances = new List { 0.5, 0.7, 0.6 }; + + var expectedPosition = (Latitude: 16.054, Longitude: 108.210); + var result = triangulator.CalculatePosition(baseLocations, distances); + + Assert.That(result.Latitude, Is.EqualTo(expectedPosition.Latitude).Within(0.01)); + Assert.That(result.Longitude, Is.EqualTo(expectedPosition.Longitude).Within(0.01)); + } + + [Test] + public void CalculatePosition_InvalidBaseLocations_ThrowsArgumentException() + { + var triangulator = new Triangulator(); + var baseLocations = new List<(double Latitude, double Longitude)> + { + (10.762622, 106.660172) + }; + var distances = new List { 1.0 }; + + Assert.That(() => triangulator.CalculatePosition(baseLocations, distances), Throws.ArgumentException); + } + + [Test] + public void CalculatePosition_InvalidDistances_ThrowsArgumentException() + { + var triangulator = new Triangulator(); + var baseLocations = new List<(double Latitude, double Longitude)> + { + (10.762622, 106.660172), + (10.774981, 106.665504), + (10.771817, 106.681179) + }; + var distances = new List { 1.0 }; + + Assert.That(() => triangulator.CalculatePosition(baseLocations, distances), Throws.ArgumentException); + } + } +} diff --git a/Algorithms/Other/Triangulator.cs b/Algorithms/Other/Triangulator.cs new file mode 100644 index 00000000..9142a1d1 --- /dev/null +++ b/Algorithms/Other/Triangulator.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.Other +{ + public class Triangulator + { + public (double Latitude, double Longitude) CalculatePosition(List<(double Latitude, double Longitude)> baseLocations, List distances) + { + if (baseLocations.Count < 3 || distances.Count < 3) + { + throw new ArgumentException("At least three points and corresponding distances are required."); + } + + // Convert distances from kilometers to degrees (approximately) + // 1 degree is approximately 111.32 km at the equator + const double kmPerDegree = 111.32; + var distancesInDegrees = distances.Select(d => d / kmPerDegree).ToList(); + + // Get the coordinates of the three base stations + double lat1 = baseLocations[0].Latitude; + double lon1 = baseLocations[0].Longitude; + double lat2 = baseLocations[1].Latitude; + double lon2 = baseLocations[1].Longitude; + double lat3 = baseLocations[2].Latitude; + double lon3 = baseLocations[2].Longitude; + + // Convert coordinates to radians + lat1 = ToRadians(lat1); + lon1 = ToRadians(lon1); + lat2 = ToRadians(lat2); + lon2 = ToRadians(lon2); + lat3 = ToRadians(lat3); + lon3 = ToRadians(lon3); + + // Distances in radians + double r1 = distancesInDegrees[0] * Math.PI / 180; + double r2 = distancesInDegrees[1] * Math.PI / 180; + double r3 = distancesInDegrees[2] * Math.PI / 180; + + // Calculate the center point + double centerLat = (lat1 + lat2 + lat3) / 3; + double centerLon = (lon1 + lon2 + lon3) / 3; + + // Convert back to degrees + centerLat = ToDegrees(centerLat); + centerLon = ToDegrees(centerLon); + + return (centerLat, centerLon); + } + + private double ToRadians(double degrees) + { + return degrees * Math.PI / 180; + } + + private double ToDegrees(double radians) + { + return radians * 180 / Math.PI; + } + } +} diff --git a/README.md b/README.md index a57b5826..8085ccab 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ find more than one implementation for the same objective but using different alg * [Pollard's Rho](./Algorithms/Other/PollardsRhoFactorizing.cs) * [GeoLocation Hash](./Algorithms/Other/Geohash.cs) * [Geofencing](./Algorithms/Other/Geofence.cs) + * [Triangulation Algorithm](./Algorithms/Other/Triangulator.cs) * [Problems](./Algorithms/Problems) * [Stable Marriage](./Algorithms/Problems/StableMarriage) * [Gale-Shapley](./Algorithms/Problems/StableMarriage/GaleShapley.cs) From d3fc4b0112f1e68422031b96b31d9b63a8f72d93 Mon Sep 17 00:00:00 2001 From: Duc Thanh Nguyen Date: Thu, 24 Oct 2024 16:53:20 +0700 Subject: [PATCH 8/8] Revert "Add the Triangulation Algorithm for Estimating Your Location" This reverts commit b1751cea85a6437e18ea7ae30082df14a3fc3bcf. --- Algorithms.Tests/Other/TriangulatorTests.cs | 62 -------------------- Algorithms/Other/Triangulator.cs | 65 --------------------- README.md | 1 - 3 files changed, 128 deletions(-) delete mode 100644 Algorithms.Tests/Other/TriangulatorTests.cs delete mode 100644 Algorithms/Other/Triangulator.cs diff --git a/Algorithms.Tests/Other/TriangulatorTests.cs b/Algorithms.Tests/Other/TriangulatorTests.cs deleted file mode 100644 index 39014684..00000000 --- a/Algorithms.Tests/Other/TriangulatorTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Algorithms.Other; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Algorithms.Tests.Other -{ - [TestFixture] - public class TriangulatorTests - { - [Test] - public void CalculatePosition_ValidCoordinatesAndDistances_ReturnsExpectedPosition() - { - var triangulator = new Triangulator(); - var baseLocations = new List<(double Latitude, double Longitude)> - { - (16.054407, 108.202167), - (16.049807, 108.218991), - (16.063597, 108.215553) - }; - - var distances = new List { 0.5, 0.7, 0.6 }; - - var expectedPosition = (Latitude: 16.054, Longitude: 108.210); - var result = triangulator.CalculatePosition(baseLocations, distances); - - Assert.That(result.Latitude, Is.EqualTo(expectedPosition.Latitude).Within(0.01)); - Assert.That(result.Longitude, Is.EqualTo(expectedPosition.Longitude).Within(0.01)); - } - - [Test] - public void CalculatePosition_InvalidBaseLocations_ThrowsArgumentException() - { - var triangulator = new Triangulator(); - var baseLocations = new List<(double Latitude, double Longitude)> - { - (10.762622, 106.660172) - }; - var distances = new List { 1.0 }; - - Assert.That(() => triangulator.CalculatePosition(baseLocations, distances), Throws.ArgumentException); - } - - [Test] - public void CalculatePosition_InvalidDistances_ThrowsArgumentException() - { - var triangulator = new Triangulator(); - var baseLocations = new List<(double Latitude, double Longitude)> - { - (10.762622, 106.660172), - (10.774981, 106.665504), - (10.771817, 106.681179) - }; - var distances = new List { 1.0 }; - - Assert.That(() => triangulator.CalculatePosition(baseLocations, distances), Throws.ArgumentException); - } - } -} diff --git a/Algorithms/Other/Triangulator.cs b/Algorithms/Other/Triangulator.cs deleted file mode 100644 index 9142a1d1..00000000 --- a/Algorithms/Other/Triangulator.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Algorithms.Other -{ - public class Triangulator - { - public (double Latitude, double Longitude) CalculatePosition(List<(double Latitude, double Longitude)> baseLocations, List distances) - { - if (baseLocations.Count < 3 || distances.Count < 3) - { - throw new ArgumentException("At least three points and corresponding distances are required."); - } - - // Convert distances from kilometers to degrees (approximately) - // 1 degree is approximately 111.32 km at the equator - const double kmPerDegree = 111.32; - var distancesInDegrees = distances.Select(d => d / kmPerDegree).ToList(); - - // Get the coordinates of the three base stations - double lat1 = baseLocations[0].Latitude; - double lon1 = baseLocations[0].Longitude; - double lat2 = baseLocations[1].Latitude; - double lon2 = baseLocations[1].Longitude; - double lat3 = baseLocations[2].Latitude; - double lon3 = baseLocations[2].Longitude; - - // Convert coordinates to radians - lat1 = ToRadians(lat1); - lon1 = ToRadians(lon1); - lat2 = ToRadians(lat2); - lon2 = ToRadians(lon2); - lat3 = ToRadians(lat3); - lon3 = ToRadians(lon3); - - // Distances in radians - double r1 = distancesInDegrees[0] * Math.PI / 180; - double r2 = distancesInDegrees[1] * Math.PI / 180; - double r3 = distancesInDegrees[2] * Math.PI / 180; - - // Calculate the center point - double centerLat = (lat1 + lat2 + lat3) / 3; - double centerLon = (lon1 + lon2 + lon3) / 3; - - // Convert back to degrees - centerLat = ToDegrees(centerLat); - centerLon = ToDegrees(centerLon); - - return (centerLat, centerLon); - } - - private double ToRadians(double degrees) - { - return degrees * Math.PI / 180; - } - - private double ToDegrees(double radians) - { - return radians * 180 / Math.PI; - } - } -} diff --git a/README.md b/README.md index 8085ccab..a57b5826 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,6 @@ find more than one implementation for the same objective but using different alg * [Pollard's Rho](./Algorithms/Other/PollardsRhoFactorizing.cs) * [GeoLocation Hash](./Algorithms/Other/Geohash.cs) * [Geofencing](./Algorithms/Other/Geofence.cs) - * [Triangulation Algorithm](./Algorithms/Other/Triangulator.cs) * [Problems](./Algorithms/Problems) * [Stable Marriage](./Algorithms/Problems/StableMarriage) * [Gale-Shapley](./Algorithms/Problems/StableMarriage/GaleShapley.cs)