first commit

This commit is contained in:
William Dillon 2025-06-04 20:51:49 -04:00
commit 553fa7d95e
5 changed files with 295 additions and 0 deletions

28
SalmonCache.sln Normal file
View File

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalmonCache", "SalmonCache\SalmonCache.csproj", "{554D35BB-451F-484B-A959-CF3438F951EA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalmonCacheTest", "SalmonCacheTest\SalmonCacheTest.csproj", "{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{554D35BB-451F-484B-A959-CF3438F951EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{554D35BB-451F-484B-A959-CF3438F951EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{554D35BB-451F-484B-A959-CF3438F951EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{554D35BB-451F-484B-A959-CF3438F951EA}.Release|Any CPU.Build.0 = Release|Any CPU
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

146
SalmonCache/SalmonCache.cs Normal file
View File

@ -0,0 +1,146 @@
namespace SalmonCache;
public class SalmonCache<K, V> where K : notnull
{
private class SalmonCacheValue
{
public K Key { set; get; }
public V Value { set; get; }
public SalmonCacheValue(K key, V value)
{
Key = key;
Value = value;
}
}
private int _capacity;
private bool _logRemovals;
private readonly ReaderWriterLockSlim _lock;
private readonly LinkedList<SalmonCacheValue> _storage;
private readonly Dictionary<K, LinkedListNode<SalmonCacheValue>> _cache;
public SalmonCache(int capacity, bool logRemovals = false)
{
_capacity = capacity;
_logRemovals = logRemovals;
_lock = new();
_storage = new();
_cache = new(_capacity);
}
public int Count
{
get
{
lock (_lock) { return _storage.Count; }
}
}
public int Capacity
{
set
{
Resize(value);
}
get
{
lock (_lock) { return _capacity; }
}
}
private SalmonCacheValue? LockedRemoveLast()
{
var node = _storage.Last;
if (node == null)
return null;
var results = node.Value;
_storage.Remove(node);
_cache.Remove(results.Key);
return results;
}
public int Resize(int capacity)
{
if (capacity <= 0) throw new ArgumentOutOfRangeException("capacity must be >= 0");
int removedCount = 0;
lock (_lock)
{
_capacity = capacity;
while (_storage.Count > _capacity)
{
var removed = LockedRemoveLast();
if (removed != null)
{
if (_logRemovals)
Console.WriteLine($"SalmonCache::Resize removed key \"{removed.Key}\"");
removedCount++;
}
}
}
return removedCount;
}
public (V?, bool) Lookup(K key)
{
lock (_lock)
{
LinkedListNode<SalmonCacheValue>? node;
if (_cache.TryGetValue(key, out node))
{
_storage.Remove(node);
_storage.AddFirst(node);
return (node.Value.Value, true);
}
else
{
return (default, false);
}
}
}
public bool Insert(K key, V value)
{
lock (_lock)
{
LinkedListNode<SalmonCacheValue>? node;
if (_cache.TryGetValue(key, out node))
{
_storage.Remove(node);
_storage.AddFirst(node);
return false;
}
else
{
while (_storage.Count + 1 > _capacity)
{
var removed = LockedRemoveLast();
if (removed != null && _logRemovals)
{
Console.WriteLine($"SalmonCache::Insert removed key \"{removed.Key}\"");
}
}
node = _storage.AddFirst(new SalmonCacheValue(key, value));
_cache[key] = node;
return true;
}
}
}
public (V?, bool) Remove(K key)
{
lock (_lock)
{
LinkedListNode<SalmonCacheValue>? node;
if (_cache.TryGetValue(key, out node))
{
var stored = node.Value;
_storage.Remove(node);
_cache.Remove(key);
return (stored.Value, true);
}
else
{
return (default, false);
}
}
}
public void Clear()
{
lock (_lock)
{
_storage.Clear();
_cache.Clear();
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,84 @@
namespace SalmonCacheTest;
public class SalmonCacheTests
{
private readonly List<string> _words = new();
[SetUp]
public void Setup()
{
StreamReader reader = new("/usr/share/dict/words");
Dictionary<string, bool> Words = new();
var line = reader.ReadLine();
while (line != null)
{
var word = line.Trim().ToLower();
if (!Words.ContainsKey(word)) Words.Add(word, true);
}
reader.Close();
_words.AddRange(Words.Keys.ToArray());
_words.Sort();
}
[Test]
public void TestBasicInsertAndLookup()
{
var cache = new SalmonCache.SalmonCache<string, string>(_words.Count);
Assert.That(cache.Capacity, Is.EqualTo(_words.Count));
foreach (var word in _words)
{
var inserted = cache.Insert(word, word.ToUpper());
Assert.That(inserted, Is.True);
var (stored, found) = cache.Lookup(word);
Assert.That(found, Is.True);
Assert.That(stored, Is.EqualTo(word.ToUpper()));
}
Assert.That(cache.Count, Is.EqualTo(_words.Count));
}
[Test]
public void TestRemove()
{
var cache = new SalmonCache.SalmonCache<string, string>(_words.Count);
foreach (var word in _words)
{
Assert.That(cache.Insert(word, word.ToUpper()), Is.True);
}
Assert.That(cache.Count, Is.EqualTo(_words.Count));
foreach (var word in _words)
{
var (stored, found) = cache.Remove(word);
Assert.That(found, Is.True);
Assert.That(stored, Is.EqualTo(word.ToUpper()));
}
}
[Test]
public void TestResizeAndClear()
{
var cache = new SalmonCache.SalmonCache<string, string>(_words.Count);
foreach (var word in _words)
{
Assert.That(cache.Insert(word, word.ToUpper()), Is.True);
}
Assert.That(cache.Count, Is.EqualTo(_words.Count));
var capacity = _words.Count / 2;
Assert.That(cache.Count, Is.EqualTo(capacity));
// the first (_words.Count - capacity) words will have been removed
for (var i = 0; i < _words.Count - capacity; i++)
{
var word = _words.ElementAt(i);
var (stored, found) = cache.Lookup(word);
Assert.That(found, Is.False);
Assert.That(stored, Is.Empty);
}
for (var i = _words.Count - capacity; i < _words.Count; i++)
{
var word = _words.ElementAt(i);
var (stored, found) = cache.Lookup(word);
Assert.That(found, Is.True);
Assert.That(stored, Is.EqualTo(word.ToUpper()));
}
cache.Clear();
Assert.That(cache.Count, Is.Empty);
}
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SalmonCache\SalmonCache.csproj" />
</ItemGroup>
</Project>