diff --git a/csharp/Platform.Data.Doublets.Tests/LinkFrequenciesCacheTests.cs b/csharp/Platform.Data.Doublets.Tests/LinkFrequenciesCacheTests.cs new file mode 100644 index 000000000..19603ff62 --- /dev/null +++ b/csharp/Platform.Data.Doublets.Tests/LinkFrequenciesCacheTests.cs @@ -0,0 +1,335 @@ +using System; +using System.IO; +using System.Linq; +using Xunit; +using Platform.Data.Doublets.Sequences.Frequencies.Cache; +using Moq; +using System.Collections.Generic; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Tests +{ + public class LinkFrequenciesCacheTests + { + private readonly Mock> _mockLinks; + private readonly Mock> _mockCounter; + private readonly LinkFrequenciesCache _cache; + + public LinkFrequenciesCacheTests() + { + _mockLinks = new Mock>(); + _mockCounter = new Mock>(); + _cache = new LinkFrequenciesCache(_mockLinks.Object, _mockCounter.Object); + } + + [Fact] + public void IncrementFrequency_NewDoublet_CreatesNewEntry() + { + // Arrange + ulong source = 1; + ulong target = 2; + ulong expectedLink = 10; + + _mockLinks.Setup(x => x.SearchOrDefault(source, target)).Returns(expectedLink); + _mockCounter.Setup(x => x.Count(expectedLink)).Returns(5UL); + + // Act + var result = _cache.IncrementFrequency(source, target); + + // Assert + Assert.NotNull(result); + Assert.Equal(6UL, result.Frequency); // 1 + 5 from counter + Assert.Equal(expectedLink, result.Link); + } + + [Fact] + public void IncrementFrequency_ExistingDoublet_IncrementsFrequency() + { + // Arrange + ulong source = 1; + ulong target = 2; + ulong expectedLink = 10; + + _mockLinks.Setup(x => x.SearchOrDefault(source, target)).Returns(expectedLink); + _mockCounter.Setup(x => x.Count(expectedLink)).Returns(5UL); + + // Act - increment twice + _cache.IncrementFrequency(source, target); + var result = _cache.IncrementFrequency(source, target); + + // Assert + Assert.Equal(7UL, result.Frequency); // 1 + 5 + 1 + } + + [Fact] + public void GetFrequency_ExistingDoublet_ReturnsCorrectFrequency() + { + // Arrange + ulong source = 1; + ulong target = 2; + ulong expectedLink = 10; + + _mockLinks.Setup(x => x.SearchOrDefault(source, target)).Returns(expectedLink); + _mockCounter.Setup(x => x.Count(expectedLink)).Returns(5UL); + + _cache.IncrementFrequency(source, target); + + // Act + var result = _cache.GetFrequency(source, target); + + // Assert + Assert.NotNull(result); + Assert.Equal(6UL, result.Frequency); + Assert.Equal(expectedLink, result.Link); + } + + [Fact] + public void GetFrequency_NonExistingDoublet_ReturnsNull() + { + // Act + var result = _cache.GetFrequency(999, 888); + + // Assert + Assert.Null(result); + } + + [Fact] + public void IncrementFrequencies_Sequence_IncrementsAllPairs() + { + // Arrange + var sequence = new List { 1, 2, 3, 4 }; + + _mockLinks.Setup(x => x.SearchOrDefault(It.IsAny(), It.IsAny())).Returns(0UL); + _mockCounter.Setup(x => x.Count(It.IsAny())).Returns(0UL); + + // Act + _cache.IncrementFrequencies(sequence); + + // Assert + Assert.Equal(3, _cache.Count); // (1,2), (2,3), (3,4) + + var freq12 = _cache.GetFrequency(1, 2); + var freq23 = _cache.GetFrequency(2, 3); + var freq34 = _cache.GetFrequency(3, 4); + + Assert.NotNull(freq12); + Assert.NotNull(freq23); + Assert.NotNull(freq34); + Assert.Equal(1UL, freq12.Frequency); + Assert.Equal(1UL, freq23.Frequency); + Assert.Equal(1UL, freq34.Frequency); + } + + [Fact] + public void SerializeToJson_WithData_ReturnsValidJson() + { + // Arrange + _mockLinks.Setup(x => x.SearchOrDefault(It.IsAny(), It.IsAny())).Returns(0UL); + _mockCounter.Setup(x => x.Count(It.IsAny())).Returns(0UL); + + _cache.IncrementFrequency(1, 2); + _cache.IncrementFrequency(2, 3); + + // Act + var json = _cache.SerializeToJson(); + + // Assert + Assert.NotNull(json); + Assert.Contains("1,2", json); + Assert.Contains("2,3", json); + Assert.Contains("Frequency", json); + Assert.Contains("Link", json); + } + + [Fact] + public void DeserializeFromJson_ValidJson_RestoresCache() + { + // Arrange + var json = @"{ + ""1,2"": { + ""Frequency"": ""5"", + ""Link"": ""10"" + }, + ""2,3"": { + ""Frequency"": ""3"", + ""Link"": ""20"" + } + }"; + + // Act + _cache.DeserializeFromJson(json); + + // Assert + Assert.Equal(2, _cache.Count); + + var freq12 = _cache.GetFrequency(1, 2); + var freq23 = _cache.GetFrequency(2, 3); + + Assert.NotNull(freq12); + Assert.NotNull(freq23); + Assert.Equal(5UL, freq12.Frequency); + Assert.Equal(10UL, freq12.Link); + Assert.Equal(3UL, freq23.Frequency); + Assert.Equal(20UL, freq23.Link); + } + + [Fact] + public void SerializeToFile_AndDeserializeFromFile_RoundTrip() + { + // Arrange + var tempFile = Path.GetTempFileName(); + + _mockLinks.Setup(x => x.SearchOrDefault(It.IsAny(), It.IsAny())).Returns(0UL); + _mockCounter.Setup(x => x.Count(It.IsAny())).Returns(0UL); + + _cache.IncrementFrequency(1, 2); + _cache.IncrementFrequency(2, 3); + var originalCount = _cache.Count; + + try + { + // Act + _cache.SerializeToFile(tempFile); + _cache.Clear(); + Assert.Equal(0, _cache.Count); + + _cache.DeserializeFromFile(tempFile); + + // Assert + Assert.Equal(originalCount, _cache.Count); + + var freq12 = _cache.GetFrequency(1, 2); + var freq23 = _cache.GetFrequency(2, 3); + + Assert.NotNull(freq12); + Assert.NotNull(freq23); + Assert.Equal(1UL, freq12.Frequency); + Assert.Equal(1UL, freq23.Frequency); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + [Fact] + public void DumpToLinksStorage_WithCachedData_CreatesLinksInStorage() + { + // Arrange + _mockLinks.Setup(x => x.SearchOrDefault(It.IsAny(), It.IsAny())).Returns(0UL); + _mockLinks.Setup(x => x.GetOrCreate(It.IsAny(), It.IsAny())).Returns((ulong source, ulong target) => source * 10 + target); + _mockCounter.Setup(x => x.Count(It.IsAny())).Returns(0UL); + + _cache.IncrementFrequency(1, 2); + _cache.IncrementFrequency(2, 3); + + // Act + var createdCount = _cache.DumpToLinksStorage(); + + // Assert + Assert.Equal(2, createdCount); + _mockLinks.Verify(x => x.GetOrCreate(1, 2), Times.Once); + _mockLinks.Verify(x => x.GetOrCreate(2, 3), Times.Once); + + var freq12 = _cache.GetFrequency(1, 2); + var freq23 = _cache.GetFrequency(2, 3); + + Assert.Equal(12UL, freq12.Link); // 1 * 10 + 2 + Assert.Equal(23UL, freq23.Link); // 2 * 10 + 3 + } + + [Fact] + public void Clear_WithData_RemovesAllEntries() + { + // Arrange + _mockLinks.Setup(x => x.SearchOrDefault(It.IsAny(), It.IsAny())).Returns(0UL); + _mockCounter.Setup(x => x.Count(It.IsAny())).Returns(0UL); + + _cache.IncrementFrequency(1, 2); + _cache.IncrementFrequency(2, 3); + Assert.Equal(2, _cache.Count); + + // Act + _cache.Clear(); + + // Assert + Assert.Equal(0, _cache.Count); + } + + [Fact] + public void GetAllEntries_WithData_ReturnsAllCachedEntries() + { + // Arrange + _mockLinks.Setup(x => x.SearchOrDefault(It.IsAny(), It.IsAny())).Returns(0UL); + _mockCounter.Setup(x => x.Count(It.IsAny())).Returns(0UL); + + _cache.IncrementFrequency(1, 2); + _cache.IncrementFrequency(2, 3); + + // Act + var entries = _cache.GetAllEntries(); + + // Assert + Assert.Equal(2, entries.Count()); + } + } + + public class LinkFrequencyTests + { + [Fact] + public void Constructor_SetsProperties() + { + // Act + var frequency = new LinkFrequency(5UL, 10UL); + + // Assert + Assert.Equal(5UL, frequency.Frequency); + Assert.Equal(10UL, frequency.Link); + } + + [Fact] + public void IncrementFrequency_IncrementsValue() + { + // Arrange + var frequency = new LinkFrequency(5UL, 10UL); + + // Act + frequency.IncrementFrequency(); + + // Assert + Assert.Equal(6UL, frequency.Frequency); + } + + [Fact] + public void ToString_ReturnsFormattedString() + { + // Arrange + var frequency = new LinkFrequency(5UL, 10UL); + + // Act + var result = frequency.ToString(); + + // Assert + Assert.Equal("Link: 10, Frequency: 5", result); + } + } + + public class DefaultCounterTests + { + [Fact] + public void Count_AlwaysReturnsZero() + { + // Arrange + var counter = DefaultCounter.Instance; + + // Act & Assert + Assert.Equal(0UL, counter.Count("test")); + Assert.Equal(0UL, counter.Count("another")); + Assert.Equal(0UL, counter.Count("null")); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets.Tests/Platform.Data.Doublets.Tests.csproj b/csharp/Platform.Data.Doublets.Tests/Platform.Data.Doublets.Tests.csproj index 27af73fe0..cb6d9148a 100644 --- a/csharp/Platform.Data.Doublets.Tests/Platform.Data.Doublets.Tests.csproj +++ b/csharp/Platform.Data.Doublets.Tests/Platform.Data.Doublets.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/csharp/Platform.Data.Doublets/Examples/LinkFrequenciesCacheExample.cs b/csharp/Platform.Data.Doublets/Examples/LinkFrequenciesCacheExample.cs new file mode 100644 index 000000000..f43856a3d --- /dev/null +++ b/csharp/Platform.Data.Doublets/Examples/LinkFrequenciesCacheExample.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using Platform.Data.Doublets.Sequences.Frequencies.Cache; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Examples +{ + /// + /// Example demonstrating the usage of LinkFrequenciesCache with serialization/deserialization + /// and dumping to Links storage functionality. + /// + public class LinkFrequenciesCacheExample + { + public static void RunExample() + { + Console.WriteLine("=== LinkFrequenciesCache Example ===\n"); + + // Mock implementation for demonstration purposes + var mockLinks = new MockLinks(); + var mockCounter = new DefaultCounter(); + + // Create the cache + var cache = new LinkFrequenciesCache(mockLinks, mockCounter); + + Console.WriteLine("1. Building frequency cache from sequences..."); + + // Simulate processing some sequences + var sequence1 = new ulong[] { 1, 2, 3, 4 }; + var sequence2 = new ulong[] { 2, 3, 5 }; + var sequence3 = new ulong[] { 1, 2, 6, 7 }; + + cache.IncrementFrequencies(sequence1); + cache.IncrementFrequencies(sequence2); + cache.IncrementFrequencies(sequence3); + + Console.WriteLine($"Cache contains {cache.Count} frequency entries."); + + // Print some frequencies + Console.WriteLine("\n2. Current frequencies:"); + cache.PrintFrequency(1, 2); // Should appear twice + cache.PrintFrequency(2, 3); // Should appear twice + cache.PrintFrequency(3, 4); // Should appear once + cache.PrintFrequency(5, 6); // Should not exist + + Console.WriteLine("\n3. Serializing cache to JSON..."); + var json = cache.SerializeToJson(); + Console.WriteLine($"JSON length: {json.Length} characters"); + + Console.WriteLine("\n4. Saving to file..."); + var filePath = Path.Combine(Path.GetTempPath(), "frequencies_cache.json"); + cache.SerializeToFile(filePath); + Console.WriteLine($"Saved to: {filePath}"); + + Console.WriteLine("\n5. Clearing cache and deserializing..."); + cache.Clear(); + Console.WriteLine($"Cache cleared, count: {cache.Count}"); + + cache.DeserializeFromFile(filePath); + Console.WriteLine($"Cache restored, count: {cache.Count}"); + + Console.WriteLine("\n6. Verifying restored frequencies:"); + cache.PrintFrequency(1, 2); + cache.PrintFrequency(2, 3); + + Console.WriteLine("\n7. Dumping cache to Links storage..."); + var createdLinks = cache.DumpToLinksStorage(); + Console.WriteLine($"Created {createdLinks} links in storage"); + + Console.WriteLine("\n8. All entries in cache:"); + foreach (var entry in cache.GetAllEntries()) + { + var doublet = entry.Key; + var freq = entry.Value; + Console.WriteLine($" ({doublet.Source},{doublet.Target}) -> Frequency: {freq.Frequency}, Link: {freq.Link}"); + } + + // Cleanup + if (File.Exists(filePath)) + { + File.Delete(filePath); + Console.WriteLine($"\nCleaned up temporary file: {filePath}"); + } + + Console.WriteLine("\n=== Example Complete ==="); + } + + /// + /// Simple mock implementation of ILinks for demonstration + /// + private class MockLinks : ILinks + { + private ulong _nextId = 100; + + public LinksConstants Constants { get; } = new LinksConstants(true, 1, 2, 3); + + public ulong Count(IList restriction) + { + return 0; + } + + public ulong Each(Func, ulong> handler, IList restriction) + { + return Constants.Continue; + } + + public ulong Update(IList restriction, IList substitution, WriteHandler handler) + { + return _nextId++; + } + + public ulong SearchOrDefault(ulong source, ulong target) + { + // Return 0 to indicate link doesn't exist yet + return 0; + } + + public ulong CreateAndUpdate(ulong source, ulong target) + { + return _nextId++; + } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/DefaultCounter.cs b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/DefaultCounter.cs new file mode 100644 index 000000000..98c5e06c0 --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/DefaultCounter.cs @@ -0,0 +1,30 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences.Frequencies.Cache +{ + /// + /// Default implementation of ICounter that always returns zero. + /// This can be used when no specific counting logic is needed. + /// + public class DefaultCounter : ICounter where TOutput : IUnsignedNumber + { + /// + /// Gets the default instance of the counter. + /// + public static readonly DefaultCounter Instance = new DefaultCounter(); + + /// + /// Counts the occurrences of the specified input. + /// + /// The input to count. + /// Always returns zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TOutput Count(TInput input) + { + return TOutput.Zero; + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/ICounter.cs b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/ICounter.cs new file mode 100644 index 000000000..739f64bef --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/ICounter.cs @@ -0,0 +1,21 @@ +using System.Numerics; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences.Frequencies.Cache +{ + /// + /// Provides a counter interface for counting occurrences. + /// + /// The type of input to count. + /// The type of output count. + public interface ICounter where TOutput : IUnsignedNumber + { + /// + /// Counts the occurrences of the specified input. + /// + /// The input to count. + /// The count of occurrences. + TOutput Count(TInput input); + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/LinkFrequenciesCache.cs b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/LinkFrequenciesCache.cs new file mode 100644 index 000000000..ab638e42b --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/LinkFrequenciesCache.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Numerics; +using System.IO; +using System.Text.Json; +using System.Linq; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences.Frequencies.Cache +{ + /// + /// Can be used to operate with many CompressingConverters (to keep global frequencies data between them). + /// TODO: Extract interface to implement frequencies storage inside Links storage + /// + public class LinkFrequenciesCache : LinksOperatorBase where TLink : IUnsignedNumber + { + private static readonly EqualityComparer _equalityComparer = EqualityComparer.Default; + private static readonly Comparer _comparer = Comparer.Default; + + private readonly Dictionary, LinkFrequency> _doubletsCache; + private readonly ICounter _frequencyCounter; + + public LinkFrequenciesCache(ILinks links, ICounter frequencyCounter) + : base(links) + { + _doubletsCache = new Dictionary, LinkFrequency>(4096, DoubletComparer.Default); + _frequencyCounter = frequencyCounter; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LinkFrequency GetFrequency(TLink source, TLink target) + { + var doublet = new Doublet(source, target); + return GetFrequency(ref doublet); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LinkFrequency GetFrequency(ref Doublet doublet) + { + _doubletsCache.TryGetValue(doublet, out LinkFrequency data); + return data; + } + + public void IncrementFrequencies(IList sequence) + { + for (var i = 1; i < sequence.Count; i++) + { + IncrementFrequency(sequence[i - 1], sequence[i]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LinkFrequency IncrementFrequency(TLink source, TLink target) + { + var doublet = new Doublet(source, target); + return IncrementFrequency(ref doublet); + } + + public void PrintFrequencies(IList sequence) + { + for (var i = 1; i < sequence.Count; i++) + { + PrintFrequency(sequence[i - 1], sequence[i]); + } + } + + public void PrintFrequency(TLink source, TLink target) + { + var frequency = GetFrequency(source, target); + var number = frequency != null ? frequency.Frequency : TLink.Zero; + Console.WriteLine("({0},{1}) - {2}", source, target, number); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LinkFrequency IncrementFrequency(ref Doublet doublet) + { + if (_doubletsCache.TryGetValue(doublet, out LinkFrequency data)) + { + data.IncrementFrequency(); + } + else + { + var link = Links.SearchOrDefault(doublet.Source, doublet.Target); + data = new LinkFrequency(TLink.One, link); + if (!_equalityComparer.Equals(link, default)) + { + data.Frequency = data.Frequency + _frequencyCounter.Count(link); + } + _doubletsCache.Add(doublet, data); + } + return data; + } + + public void ValidateFrequencies() + { + foreach (var entry in _doubletsCache) + { + var value = entry.Value; + var linkIndex = value.Link; + if (!_equalityComparer.Equals(linkIndex, default)) + { + var frequency = value.Frequency; + var count = _frequencyCounter.Count(linkIndex); + // TODO: Why `frequency` always greater than `count` by 1? + if (((_comparer.Compare(frequency, count) > 0) && (_comparer.Compare(frequency - count, TLink.One) > 0)) + || ((_comparer.Compare(count, frequency) > 0) && (_comparer.Compare(count - frequency, TLink.One) > 0))) + { + throw new InvalidOperationException("Frequencies validation failed."); + } + } + } + } + + /// + /// Serializes the cache data to JSON format. + /// + /// JSON string representation of the cache. + public string SerializeToJson() + { + var cacheData = _doubletsCache.ToDictionary( + kvp => $"{kvp.Key.Source},{kvp.Key.Target}", + kvp => new { Frequency = kvp.Value.Frequency.ToString(), Link = kvp.Value.Link.ToString() } + ); + + var options = new JsonSerializerOptions + { + WriteIndented = true + }; + + return JsonSerializer.Serialize(cacheData, options); + } + + /// + /// Serializes the cache data to a file. + /// + /// The path where to save the serialized data. + public void SerializeToFile(string filePath) + { + var json = SerializeToJson(); + File.WriteAllText(filePath, json); + } + + /// + /// Deserializes cache data from JSON format. + /// + /// JSON string containing the cache data. + public void DeserializeFromJson(string json) + { + _doubletsCache.Clear(); + + var cacheData = JsonSerializer.Deserialize>(json); + + if (cacheData != null) + { + foreach (var kvp in cacheData) + { + var parts = kvp.Key.Split(','); + if (parts.Length == 2 && + TLink.TryParse(parts[0], null, out TLink source) && + TLink.TryParse(parts[1], null, out TLink target)) + { + var frequencyStr = kvp.Value.GetProperty("Frequency").GetString(); + var linkStr = kvp.Value.GetProperty("Link").GetString(); + + if (frequencyStr != null && linkStr != null && + TLink.TryParse(frequencyStr, null, out TLink frequency) && + TLink.TryParse(linkStr, null, out TLink link)) + { + var doublet = new Doublet(source, target); + var linkFrequency = new LinkFrequency(frequency, link); + _doubletsCache[doublet] = linkFrequency; + } + } + } + } + } + + /// + /// Deserializes cache data from a file. + /// + /// The path to the file containing serialized data. + public void DeserializeFromFile(string filePath) + { + var json = File.ReadAllText(filePath); + DeserializeFromJson(json); + } + + /// + /// Dumps the cache data into the Links storage by creating actual links. + /// + /// Number of links created in the storage. + public int DumpToLinksStorage() + { + int createdLinks = 0; + + foreach (var entry in _doubletsCache) + { + var doublet = entry.Key; + var frequency = entry.Value; + + // Only create links that don't already exist + if (_equalityComparer.Equals(frequency.Link, default)) + { + // Create the link in storage + var newLinkId = Links.GetOrCreate(doublet.Source, doublet.Target); + frequency.Link = newLinkId; + createdLinks++; + } + } + + return createdLinks; + } + + /// + /// Gets the total number of cached frequency entries. + /// + public int Count => _doubletsCache.Count; + + /// + /// Gets all cached doublets and their frequencies. + /// + /// Enumerable of all cached entries. + public IEnumerable, LinkFrequency>> GetAllEntries() + { + return _doubletsCache.AsEnumerable(); + } + + /// + /// Clears all cached frequency data. + /// + public void Clear() + { + _doubletsCache.Clear(); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/LinkFrequency.cs b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/LinkFrequency.cs new file mode 100644 index 000000000..8d14db2f6 --- /dev/null +++ b/csharp/Platform.Data.Doublets/Sequences/Frequencies/Cache/LinkFrequency.cs @@ -0,0 +1,54 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Platform.Data.Doublets.Sequences.Frequencies.Cache +{ + /// + /// Represents a frequency count for a specific link. + /// + public class LinkFrequency where TLink : IUnsignedNumber + { + /// + /// Gets or sets the frequency count. + /// + public TLink Frequency { get; set; } + + /// + /// Gets or sets the link identifier. + /// + public TLink Link { get; set; } + + /// + /// Initializes a new instance of the LinkFrequency class. + /// + /// The initial frequency count. + /// The link identifier. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public LinkFrequency(TLink frequency, TLink link) + { + Frequency = frequency; + Link = link; + } + + /// + /// Increments the frequency count by one. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void IncrementFrequency() + { + Frequency = Frequency + TLink.One; + } + + /// + /// Returns a string representation of the LinkFrequency. + /// + /// A string containing the link and frequency information. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() + { + return $"Link: {Link}, Frequency: {Frequency}"; + } + } +} \ No newline at end of file