Added Ellise.Common
This commit is contained in:
parent
40f8c5f409
commit
7eb4be66ec
26 changed files with 1273 additions and 6 deletions
19
Ellie.sln
19
Ellie.sln
|
@ -26,10 +26,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Coordinator", "src\El
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Generators.Strings", "src\Ellie.Bot.Generators.Strings\Ellie.Bot.Generators.Strings.csproj", "{11DE9EB6-2793-4540-BE66-701D2D02903A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Generators.Strings", "src\Ellie.Bot.Generators.Strings\Ellie.Bot.Generators.Strings.csproj", "{11DE9EB6-2793-4540-BE66-701D2D02903A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.VotesApi", "src\Ellie.VotesApi\Ellie.VotesApi.csproj", "{8D996036-52D1-4B11-B7D7-6F853A907EDD}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.VotesApi", "src\Ellie.VotesApi\Ellie.VotesApi.csproj", "{8D996036-52D1-4B11-B7D7-6F853A907EDD}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Ellie.Marmalade\Ellie.Marmalade.csproj", "{D6CF9ABE-205E-4699-90CA-0F18ED236490}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Ellie.Marmalade\Ellie.Marmalade.csproj", "{D6CF9ABE-205E-4699-90CA-0F18ED236490}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellise.Common", "src\Ellise.Common\Ellise.Common.csproj", "{227F78CC-633E-4B1F-A12B-DF8BFF30549C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -48,10 +50,6 @@ Global
|
||||||
{6A8CE149-3808-474F-A2E6-B89825BB5DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{6A8CE149-3808-474F-A2E6-B89825BB5DC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6A8CE149-3808-474F-A2E6-B89825BB5DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{6A8CE149-3808-474F-A2E6-B89825BB5DC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6A8CE149-3808-474F-A2E6-B89825BB5DC2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{6A8CE149-3808-474F-A2E6-B89825BB5DC2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{44BE7271-BABE-46BE-BB41-A5B6F1116C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{44BE7271-BABE-46BE-BB41-A5B6F1116C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{44BE7271-BABE-46BE-BB41-A5B6F1116C21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{44BE7271-BABE-46BE-BB41-A5B6F1116C21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{44BE7271-BABE-46BE-BB41-A5B6F1116C21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{44BE7271-BABE-46BE-BB41-A5B6F1116C21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
@ -64,6 +62,14 @@ Global
|
||||||
{8D996036-52D1-4B11-B7D7-6F853A907EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8D996036-52D1-4B11-B7D7-6F853A907EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8D996036-52D1-4B11-B7D7-6F853A907EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8D996036-52D1-4B11-B7D7-6F853A907EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8D996036-52D1-4B11-B7D7-6F853A907EDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8D996036-52D1-4B11-B7D7-6F853A907EDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D6CF9ABE-205E-4699-90CA-0F18ED236490}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{227F78CC-633E-4B1F-A12B-DF8BFF30549C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{227F78CC-633E-4B1F-A12B-DF8BFF30549C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{227F78CC-633E-4B1F-A12B-DF8BFF30549C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{227F78CC-633E-4B1F-A12B-DF8BFF30549C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -73,10 +79,11 @@ Global
|
||||||
{5284415D-A43F-4539-9483-410124199743} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
{5284415D-A43F-4539-9483-410124199743} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||||
{34E6D136-B151-4B6E-A8E7-7A2FB7B06CA3} = {5284415D-A43F-4539-9483-410124199743}
|
{34E6D136-B151-4B6E-A8E7-7A2FB7B06CA3} = {5284415D-A43F-4539-9483-410124199743}
|
||||||
{6A8CE149-3808-474F-A2E6-B89825BB5DC2} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
{6A8CE149-3808-474F-A2E6-B89825BB5DC2} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||||
{D6CF9ABE-205E-4699-90CA-0F18ED236490} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
|
||||||
{44BE7271-BABE-46BE-BB41-A5B6F1116C21} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
{44BE7271-BABE-46BE-BB41-A5B6F1116C21} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||||
{11DE9EB6-2793-4540-BE66-701D2D02903A} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
{11DE9EB6-2793-4540-BE66-701D2D02903A} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||||
{8D996036-52D1-4B11-B7D7-6F853A907EDD} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
{8D996036-52D1-4B11-B7D7-6F853A907EDD} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||||
|
{D6CF9ABE-205E-4699-90CA-0F18ED236490} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||||
|
{227F78CC-633E-4B1F-A12B-DF8BFF30549C} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {878761F1-C7B5-4D38-A00D-3377D703EBBA}
|
SolutionGuid = {878761F1-C7B5-4D38-A00D-3377D703EBBA}
|
||||||
|
|
19
src/Ellise.Common/AsyncLazy.cs
Normal file
19
src/Ellise.Common/AsyncLazy.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public class AsyncLazy<T> : Lazy<Task<T>>
|
||||||
|
{
|
||||||
|
public AsyncLazy(Func<T> valueFactory)
|
||||||
|
: base(() => Task.Run(valueFactory))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncLazy(Func<Task<T>> taskFactory)
|
||||||
|
: base(() => Task.Run(taskFactory))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public TaskAwaiter<T> GetAwaiter()
|
||||||
|
=> Value.GetAwaiter();
|
||||||
|
}
|
46
src/Ellise.Common/Cache/BotCacheExtensions.cs
Normal file
46
src/Ellise.Common/Cache/BotCacheExtensions.cs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public static class BotCacheExtensions
|
||||||
|
{
|
||||||
|
public static async ValueTask<T?> GetOrDefaultAsync<T>(this IBotCache cache, TypedKey<T> key)
|
||||||
|
{
|
||||||
|
var result = await cache.GetAsync(key);
|
||||||
|
if (result.TryGetValue(out var val))
|
||||||
|
return val;
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypedKey<byte[]> GetImgKey(Uri uri)
|
||||||
|
=> new($"image:{uri}");
|
||||||
|
|
||||||
|
public static ValueTask SetImageDataAsync(this IBotCache c, string key, byte[] data)
|
||||||
|
=> c.SetImageDataAsync(new Uri(key), data);
|
||||||
|
public static async ValueTask SetImageDataAsync(this IBotCache c, Uri key, byte[] data)
|
||||||
|
=> await c.AddAsync(GetImgKey(key), data, expiry: TimeSpan.FromHours(48));
|
||||||
|
|
||||||
|
public static async ValueTask<OneOf<byte[], None>> GetImageDataAsync(this IBotCache c, Uri key)
|
||||||
|
=> await c.GetAsync(GetImgKey(key));
|
||||||
|
|
||||||
|
public static async Task<TimeSpan?> GetRatelimitAsync(
|
||||||
|
this IBotCache c,
|
||||||
|
TypedKey<long> key,
|
||||||
|
TimeSpan length)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var nowB = now.ToBinary();
|
||||||
|
|
||||||
|
var cachedValue = await c.GetOrAddAsync(key,
|
||||||
|
() => Task.FromResult(now.ToBinary()),
|
||||||
|
expiry: length);
|
||||||
|
|
||||||
|
if (cachedValue == nowB)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var diff = now - DateTime.FromBinary(cachedValue);
|
||||||
|
return length - diff;
|
||||||
|
}
|
||||||
|
}
|
47
src/Ellise.Common/Cache/IBotCache.cs
Normal file
47
src/Ellise.Common/Cache/IBotCache.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public interface IBotCache
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key to add</param>
|
||||||
|
/// <param name="value">Value to add to the cache</param>
|
||||||
|
/// <param name="expiry">Optional expiry</param>
|
||||||
|
/// <param name="overwrite">Whether old value should be overwritten</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>Returns whether add was sucessful. Always true unless ovewrite = false</returns>
|
||||||
|
ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get an element from the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>Either a value or <see cref="None"/></returns>
|
||||||
|
ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a key from the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key to remove</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>Whether there was item</returns>
|
||||||
|
ValueTask<bool> RemoveAsync<T>(TypedKey<T> key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the key if it exists or add a new one
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key to get and potentially add</param>
|
||||||
|
/// <param name="createFactory">Value creation factory</param>
|
||||||
|
/// <param name="expiry">Optional expiry</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>The retrieved or newly added value</returns>
|
||||||
|
ValueTask<T?> GetOrAddAsync<T>(
|
||||||
|
TypedKey<T> key,
|
||||||
|
Func<Task<T?>> createFactory,
|
||||||
|
TimeSpan? expiry = null);
|
||||||
|
}
|
71
src/Ellise.Common/Cache/MemoryBotCache.cs
Normal file
71
src/Ellise.Common/Cache/MemoryBotCache.cs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
// ReSharper disable InconsistentlySynchronizedField
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public sealed class MemoryBotCache : IBotCache
|
||||||
|
{
|
||||||
|
// needed for overwrites and Delete return value
|
||||||
|
private readonly object _cacheLock = new object();
|
||||||
|
private readonly MemoryCache _cache;
|
||||||
|
|
||||||
|
public MemoryBotCache()
|
||||||
|
{
|
||||||
|
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
||||||
|
{
|
||||||
|
if (overwrite)
|
||||||
|
{
|
||||||
|
using var item = _cache.CreateEntry(key.Key);
|
||||||
|
item.Value = value;
|
||||||
|
item.AbsoluteExpirationRelativeToNow = expiry;
|
||||||
|
return new(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(key.Key, out var old) && old is not null)
|
||||||
|
return new(false);
|
||||||
|
|
||||||
|
using var item = _cache.CreateEntry(key.Key);
|
||||||
|
item.Value = value;
|
||||||
|
item.AbsoluteExpirationRelativeToNow = expiry;
|
||||||
|
return new(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<T?> GetOrAddAsync<T>(
|
||||||
|
TypedKey<T> key,
|
||||||
|
Func<Task<T?>> createFactory,
|
||||||
|
TimeSpan? expiry = null)
|
||||||
|
=> await _cache.GetOrCreateAsync(key.Key,
|
||||||
|
async ce =>
|
||||||
|
{
|
||||||
|
ce.AbsoluteExpirationRelativeToNow = expiry;
|
||||||
|
var val = await createFactory();
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
public ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
|
||||||
|
{
|
||||||
|
if (!_cache.TryGetValue(key.Key, out var val) || val is null)
|
||||||
|
return new(new None());
|
||||||
|
|
||||||
|
return new((T)val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
||||||
|
{
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
var toReturn = _cache.TryGetValue(key.Key, out var old) && old is not null;
|
||||||
|
_cache.Remove(key.Key);
|
||||||
|
return new(toReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
src/Ellise.Common/Collections/ConcurrentHashSet.cs
Normal file
88
src/Ellise.Common/Collections/ConcurrentHashSet.cs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace System.Collections.Generic;
|
||||||
|
|
||||||
|
[DebuggerDisplay("{_backingStore.Count}")]
|
||||||
|
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T> where T : notnull
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<T, bool> _backingStore;
|
||||||
|
|
||||||
|
public ConcurrentHashSet()
|
||||||
|
=> _backingStore = new();
|
||||||
|
|
||||||
|
public ConcurrentHashSet(IEnumerable<T> values, IEqualityComparer<T>? comparer = null)
|
||||||
|
=> _backingStore = new(values.Select(x => new KeyValuePair<T, bool>(x, true)), comparer);
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
=> _backingStore.Keys.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified item to the <see cref="ConcurrentHashSet{T}" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to add.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true if the items was added to the <see cref="ConcurrentHashSet{T}" />
|
||||||
|
/// successfully; false if it already exists.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="T:System.OverflowException">
|
||||||
|
/// The <see cref="ConcurrentHashSet{T}" />
|
||||||
|
/// contains too many items.
|
||||||
|
/// </exception>
|
||||||
|
public bool Add(T item)
|
||||||
|
=> _backingStore.TryAdd(item, true);
|
||||||
|
|
||||||
|
void ICollection<T>.Add(T item)
|
||||||
|
=> Add(item);
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
=> _backingStore.Clear();
|
||||||
|
|
||||||
|
public bool Contains(T item)
|
||||||
|
=> _backingStore.ContainsKey(item);
|
||||||
|
|
||||||
|
public void CopyTo(T[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(array);
|
||||||
|
|
||||||
|
if (arrayIndex < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
|
||||||
|
|
||||||
|
if (arrayIndex >= array.Length)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
|
||||||
|
|
||||||
|
CopyToInternal(array, arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyToInternal(T[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
var len = array.Length;
|
||||||
|
foreach (var (k, _) in _backingStore)
|
||||||
|
{
|
||||||
|
if (arrayIndex >= len)
|
||||||
|
throw new IndexOutOfRangeException(nameof(arrayIndex));
|
||||||
|
|
||||||
|
array[arrayIndex++] = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ICollection<T>.Remove(T item)
|
||||||
|
=> TryRemove(item);
|
||||||
|
|
||||||
|
public bool TryRemove(T item)
|
||||||
|
=> _backingStore.TryRemove(item, out _);
|
||||||
|
|
||||||
|
public void RemoveWhere(Func<T, bool> predicate)
|
||||||
|
{
|
||||||
|
foreach (var elem in this.Where(predicate))
|
||||||
|
TryRemove(elem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
=> _backingStore.Count;
|
||||||
|
|
||||||
|
public bool IsReadOnly
|
||||||
|
=> false;
|
||||||
|
}
|
148
src/Ellise.Common/Collections/IndexedCollection.cs
Normal file
148
src/Ellise.Common/Collections/IndexedCollection.cs
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public interface IIndexed
|
||||||
|
{
|
||||||
|
int Index { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IndexedCollection<T> : IList<T>
|
||||||
|
where T : class, IIndexed
|
||||||
|
{
|
||||||
|
public List<T> Source { get; }
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
=> Source.Count;
|
||||||
|
|
||||||
|
public bool IsReadOnly
|
||||||
|
=> false;
|
||||||
|
|
||||||
|
public virtual T this[int index]
|
||||||
|
{
|
||||||
|
get => Source[index];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
value.Index = index;
|
||||||
|
Source[index] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _locker = new();
|
||||||
|
|
||||||
|
public IndexedCollection()
|
||||||
|
=> Source = new();
|
||||||
|
|
||||||
|
public IndexedCollection(IEnumerable<T> source)
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
Source = source.OrderBy(x => x.Index).ToList();
|
||||||
|
UpdateIndexes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int IndexOf(T item)
|
||||||
|
=> item?.Index ?? -1;
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
=> Source.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> Source.GetEnumerator();
|
||||||
|
|
||||||
|
public void Add(T item)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
item.Index = Source.Count;
|
||||||
|
Source.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Clear()
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
Source.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(T item)
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
return Source.Contains(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(T[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
Source.CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool Remove(T item)
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
if (Source.Remove(item))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Source.Count; i++)
|
||||||
|
{
|
||||||
|
if (Source[i].Index != i)
|
||||||
|
Source[i].Index = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Insert(int index, T item)
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
Source.Insert(index, item);
|
||||||
|
for (var i = index; i < Source.Count; i++)
|
||||||
|
Source[i].Index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void RemoveAt(int index)
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
Source.RemoveAt(index);
|
||||||
|
for (var i = index; i < Source.Count; i++)
|
||||||
|
Source[i].Index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateIndexes()
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Source.Count; i++)
|
||||||
|
{
|
||||||
|
if (Source[i].Index != i)
|
||||||
|
Source[i].Index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator List<T>(IndexedCollection<T> x)
|
||||||
|
=> x.Source;
|
||||||
|
|
||||||
|
public List<T> ToList()
|
||||||
|
=> Source.ToList();
|
||||||
|
}
|
69
src/Ellise.Common/EllieRandom.cs
Normal file
69
src/Ellise.Common/EllieRandom.cs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
#nullable disable
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public class EllieRandom : Random
|
||||||
|
{
|
||||||
|
private readonly RandomNumberGenerator _rng;
|
||||||
|
|
||||||
|
public EllieRandom()
|
||||||
|
=> _rng = RandomNumberGenerator.Create();
|
||||||
|
|
||||||
|
public override int Next()
|
||||||
|
{
|
||||||
|
var bytes = new byte[sizeof(int)];
|
||||||
|
_rng.GetBytes(bytes);
|
||||||
|
return Math.Abs(BitConverter.ToInt32(bytes, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Next(int maxValue)
|
||||||
|
{
|
||||||
|
if (maxValue <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(maxValue));
|
||||||
|
var bytes = new byte[sizeof(int)];
|
||||||
|
_rng.GetBytes(bytes);
|
||||||
|
return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Next(int minValue, int maxValue)
|
||||||
|
{
|
||||||
|
if (minValue > maxValue)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(maxValue));
|
||||||
|
if (minValue == maxValue)
|
||||||
|
return minValue;
|
||||||
|
var bytes = new byte[sizeof(int)];
|
||||||
|
_rng.GetBytes(bytes);
|
||||||
|
var sign = Math.Sign(BitConverter.ToInt32(bytes, 0));
|
||||||
|
return (sign * BitConverter.ToInt32(bytes, 0) % (maxValue - minValue)) + minValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long NextLong(long minValue, long maxValue)
|
||||||
|
{
|
||||||
|
if (minValue > maxValue)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(maxValue));
|
||||||
|
if (minValue == maxValue)
|
||||||
|
return minValue;
|
||||||
|
var bytes = new byte[sizeof(long)];
|
||||||
|
_rng.GetBytes(bytes);
|
||||||
|
var sign = Math.Sign(BitConverter.ToInt64(bytes, 0));
|
||||||
|
return (sign * BitConverter.ToInt64(bytes, 0) % (maxValue - minValue)) + minValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void NextBytes(byte[] buffer)
|
||||||
|
=> _rng.GetBytes(buffer);
|
||||||
|
|
||||||
|
protected override double Sample()
|
||||||
|
{
|
||||||
|
var bytes = new byte[sizeof(double)];
|
||||||
|
_rng.GetBytes(bytes);
|
||||||
|
return Math.Abs((BitConverter.ToDouble(bytes, 0) / double.MaxValue) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double NextDouble()
|
||||||
|
{
|
||||||
|
var bytes = new byte[sizeof(double)];
|
||||||
|
_rng.GetBytes(bytes);
|
||||||
|
return BitConverter.ToDouble(bytes, 0);
|
||||||
|
}
|
||||||
|
}
|
17
src/Ellise.Common/Ellise.Common.csproj
Normal file
17
src/Ellise.Common/Ellise.Common.csproj
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||||
|
<PackageReference Include="NonBlocking" Version="2.1.1" />
|
||||||
|
<PackageReference Include="OneOf" Version="3.0.243" />
|
||||||
|
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
51
src/Ellise.Common/Extensions/ArrayExtensions.cs
Normal file
51
src/Ellise.Common/Extensions/ArrayExtensions.cs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
// made for expressions because they almost never get added
|
||||||
|
// and they get looped through constantly
|
||||||
|
public static class ArrayExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new array from the old array + new element at the end
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Input array</param>
|
||||||
|
/// <param name="added">Item to add to the end of the output array</param>
|
||||||
|
/// <typeparam name="T">Type of the array</typeparam>
|
||||||
|
/// <returns>A new array with the new element at the end</returns>
|
||||||
|
public static T[] With<T>(this T[] input, T added)
|
||||||
|
{
|
||||||
|
var newExprs = new T[input.Length + 1];
|
||||||
|
Array.Copy(input, 0, newExprs, 0, input.Length);
|
||||||
|
newExprs[input.Length] = added;
|
||||||
|
return newExprs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new array by applying the specified function to every element in the input array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="arr">Array to modify</param>
|
||||||
|
/// <param name="f">Function to apply</param>
|
||||||
|
/// <typeparam name="TIn">Orignal type of the elements in the array</typeparam>
|
||||||
|
/// <typeparam name="TOut">Output type of the elements of the array</typeparam>
|
||||||
|
/// <returns>New array with updated elements</returns>
|
||||||
|
public static TOut[] Map<TIn, TOut>(this TIn[] arr, Func<TIn, TOut> f)
|
||||||
|
=> Array.ConvertAll(arr, x => f(x));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new array by applying the specified function to every element in the input array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="col">Array to modify</param>
|
||||||
|
/// <param name="f">Function to apply</param>
|
||||||
|
/// <typeparam name="TIn">Orignal type of the elements in the array</typeparam>
|
||||||
|
/// <typeparam name="TOut">Output type of the elements of the array</typeparam>
|
||||||
|
/// <returns>New array with updated elements</returns>
|
||||||
|
public static TOut[] Map<TIn, TOut>(this IReadOnlyCollection<TIn> col, Func<TIn, TOut> f)
|
||||||
|
{
|
||||||
|
var toReturn = new TOut[col.Count];
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
foreach (var item in col)
|
||||||
|
toReturn[i++] = f(item);
|
||||||
|
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
}
|
109
src/Ellise.Common/Extensions/EnumerableExtensions.cs
Normal file
109
src/Ellise.Common/Extensions/EnumerableExtensions.cs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public static class EnumerableExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Concatenates the members of a collection, using the specified separator between each member.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Collection to join</param>
|
||||||
|
/// <param name="separator">
|
||||||
|
/// The character to use as a separator. separator is included in the returned string only if
|
||||||
|
/// values has more than one element.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="func">Optional transformation to apply to each element before concatenation.</param>
|
||||||
|
/// <typeparam name="T">The type of the members of values.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// A string that consists of the members of values delimited by the separator character. -or- Empty if values has
|
||||||
|
/// no elements.
|
||||||
|
/// </returns>
|
||||||
|
public static string Join<T>(this IEnumerable<T> data, char separator, Func<T, string>? func = null)
|
||||||
|
=> string.Join(separator, data.Select(func ?? (x => x?.ToString() ?? string.Empty)));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Concatenates the members of a collection, using the specified separator between each member.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Collection to join</param>
|
||||||
|
/// <param name="separator">
|
||||||
|
/// The string to use as a separator.separator is included in the returned string only if values
|
||||||
|
/// has more than one element.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="func">Optional transformation to apply to each element before concatenation.</param>
|
||||||
|
/// <typeparam name="T">The type of the members of values.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// A string that consists of the members of values delimited by the separator character. -or- Empty if values has
|
||||||
|
/// no elements.
|
||||||
|
/// </returns>
|
||||||
|
public static string Join<T>(this IEnumerable<T> data, string separator, Func<T, string>? func = null)
|
||||||
|
=> string.Join(separator, data.Select(func ?? (x => x?.ToString() ?? string.Empty)));
|
||||||
|
|
||||||
|
|
||||||
|
// todo have 2 different shuffles
|
||||||
|
/// <summary>
|
||||||
|
/// Randomize element order by performing the Fisher-Yates shuffle
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Item type</typeparam>
|
||||||
|
/// <param name="items">Items to shuffle</param>
|
||||||
|
public static IReadOnlyList<T> Shuffle<T>(this IEnumerable<T> items)
|
||||||
|
{
|
||||||
|
using var provider = RandomNumberGenerator.Create();
|
||||||
|
var list = items.ToList();
|
||||||
|
var n = list.Count;
|
||||||
|
while (n > 1)
|
||||||
|
{
|
||||||
|
var box = new byte[(n / byte.MaxValue) + 1];
|
||||||
|
int boxSum;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
provider.GetBytes(box);
|
||||||
|
boxSum = box.Sum(b => b);
|
||||||
|
} while (!(boxSum < n * (byte.MaxValue * box.Length / n)));
|
||||||
|
|
||||||
|
var k = boxSum % n;
|
||||||
|
n--;
|
||||||
|
(list[k], list[n]) = (list[n], list[k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ConcurrentDictionary{TKey,TValue}" /> class
|
||||||
|
/// that contains elements copied from the specified <see cref="IEnumerable{T}" />
|
||||||
|
/// has the default concurrency level, has the default initial capacity,
|
||||||
|
/// and uses the default comparer for the key type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dict">
|
||||||
|
/// The <see cref="IEnumerable{T}" /> whose elements are copied to the new
|
||||||
|
/// <see cref="ConcurrentDictionary{TKey,TValue}" />.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A new instance of the <see cref="ConcurrentDictionary{TKey,TValue}" /> class</returns>
|
||||||
|
public static ConcurrentDictionary<TKey, TValue> ToConcurrent<TKey, TValue>(
|
||||||
|
this IEnumerable<KeyValuePair<TKey, TValue>> dict)
|
||||||
|
where TKey : notnull
|
||||||
|
=> new(dict);
|
||||||
|
|
||||||
|
public static IndexedCollection<T> ToIndexed<T>(this IEnumerable<T> enumerable)
|
||||||
|
where T : class, IIndexed
|
||||||
|
=> new(enumerable);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a task that will complete when all of the <see cref="Task{TResult}" /> objects in an enumerable
|
||||||
|
/// collection have completed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tasks">The tasks to wait on for completion.</param>
|
||||||
|
/// <typeparam name="TResult">The type of the completed task.</typeparam>
|
||||||
|
/// <returns>A task that represents the completion of all of the supplied tasks.</returns>
|
||||||
|
public static Task<TResult[]> WhenAll<TResult>(this IEnumerable<Task<TResult>> tasks)
|
||||||
|
=> Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a task that will complete when all of the <see cref="Task" /> objects in an enumerable
|
||||||
|
/// collection have completed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tasks">The tasks to wait on for completion.</param>
|
||||||
|
/// <returns>A task that represents the completion of all of the supplied tasks.</returns>
|
||||||
|
public static Task WhenAll(this IEnumerable<Task> tasks)
|
||||||
|
=> Task.WhenAll(tasks);
|
||||||
|
}
|
7
src/Ellise.Common/Extensions/Extensions.cs
Normal file
7
src/Ellise.Common/Extensions/Extensions.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static long ToTimestamp(this in DateTime value)
|
||||||
|
=> (value.Ticks - 621355968000000000) / 10000000;
|
||||||
|
}
|
35
src/Ellise.Common/Extensions/HttpClientExtensions.cs
Normal file
35
src/Ellise.Common/Extensions/HttpClientExtensions.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public static class HttpClientExtensions
|
||||||
|
{
|
||||||
|
public static HttpClient AddFakeHeaders(this HttpClient http)
|
||||||
|
{
|
||||||
|
AddFakeHeaders(http.DefaultRequestHeaders);
|
||||||
|
return http;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddFakeHeaders(this HttpHeaders dict)
|
||||||
|
{
|
||||||
|
dict.Clear();
|
||||||
|
dict.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
|
||||||
|
dict.Add("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.202 Safari/535.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsImage(this HttpResponseMessage msg)
|
||||||
|
=> IsImage(msg, out _);
|
||||||
|
|
||||||
|
public static bool IsImage(this HttpResponseMessage msg, out string? mimeType)
|
||||||
|
{
|
||||||
|
mimeType = msg.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (mimeType is "image/png" or "image/jpeg" or "image/gif")
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long GetContentLength(this HttpResponseMessage msg)
|
||||||
|
=> msg.Content.Headers.ContentLength ?? long.MaxValue;
|
||||||
|
}
|
10
src/Ellise.Common/Extensions/OneOfExtensions.cs
Normal file
10
src/Ellise.Common/Extensions/OneOfExtensions.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using OneOf.Types;
|
||||||
|
using OneOf;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public static class OneOfExtensions
|
||||||
|
{
|
||||||
|
public static bool TryGetValue<T>(this OneOf<T, None> oneOf, out T value)
|
||||||
|
=> oneOf.TryPickT0(out value, out _);
|
||||||
|
}
|
22
src/Ellise.Common/Extensions/PipeExtensions.cs
Normal file
22
src/Ellise.Common/Extensions/PipeExtensions.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public delegate TOut PipeFunc<TIn, out TOut>(in TIn a);
|
||||||
|
public delegate TOut PipeFunc<TIn1, TIn2, out TOut>(in TIn1 a, in TIn2 b);
|
||||||
|
|
||||||
|
public static class PipeExtensions
|
||||||
|
{
|
||||||
|
public static TOut Pipe<TIn, TOut>(this TIn a, Func<TIn, TOut> fn)
|
||||||
|
=> fn(a);
|
||||||
|
|
||||||
|
public static TOut Pipe<TIn, TOut>(this TIn a, PipeFunc<TIn, TOut> fn)
|
||||||
|
=> fn(a);
|
||||||
|
|
||||||
|
public static TOut Pipe<TIn1, TIn2, TOut>(this (TIn1, TIn2) a, PipeFunc<TIn1, TIn2, TOut> fn)
|
||||||
|
=> fn(a.Item1, a.Item2);
|
||||||
|
|
||||||
|
public static (TIn, TExtra) With<TIn, TExtra>(this TIn a, TExtra b)
|
||||||
|
=> (a, b);
|
||||||
|
|
||||||
|
public static async Task<TOut> Pipe<TIn, TOut>(this Task<TIn> a, Func<TIn, TOut> fn)
|
||||||
|
=> fn(await a);
|
||||||
|
}
|
139
src/Ellise.Common/Extensions/StringExtensions.cs
Normal file
139
src/Ellise.Common/Extensions/StringExtensions.cs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
using Ellie.Common.Yml;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Humanizer;
|
||||||
|
using Ellise.Common;
|
||||||
|
|
||||||
|
namespace Ellie.Extensions;
|
||||||
|
|
||||||
|
public static class StringExtensions
|
||||||
|
{
|
||||||
|
private static readonly HashSet<char> _lettersAndDigits = new(Enumerable.Range(48, 10)
|
||||||
|
.Concat(Enumerable.Range(65, 26))
|
||||||
|
.Concat(Enumerable.Range(97, 26))
|
||||||
|
.Select(x => (char)x));
|
||||||
|
|
||||||
|
private static readonly Regex _filterRegex = new(@"discord(?:\.gg|\.io|\.me|\.li|(?:app)?\.com\/invite)\/(\w+)",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
private static readonly Regex _codePointRegex =
|
||||||
|
new(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static string PadBoth(this string str, int length)
|
||||||
|
{
|
||||||
|
var spaces = length - str.Length;
|
||||||
|
var padLeft = (spaces / 2) + str.Length;
|
||||||
|
return str.PadLeft(padLeft, ' ').PadRight(length, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string StripHtml(this string input)
|
||||||
|
=> Regex.Replace(input, "<.*?>", string.Empty);
|
||||||
|
|
||||||
|
public static string? TrimTo(this string? str, int maxLength, bool hideDots = false)
|
||||||
|
=> hideDots ? str?.Truncate(maxLength, string.Empty) : str?.Truncate(maxLength);
|
||||||
|
|
||||||
|
public static string ToTitleCase(this string str)
|
||||||
|
{
|
||||||
|
var tokens = str.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
for (var i = 0; i < tokens.Length; i++)
|
||||||
|
{
|
||||||
|
var token = tokens[i];
|
||||||
|
tokens[i] = token[..1].ToUpperInvariant() + token[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens.Join(" ").Replace(" Of ", " of ").Replace(" The ", " the ");
|
||||||
|
}
|
||||||
|
|
||||||
|
//http://www.dotnetperls.com/levenshtein
|
||||||
|
public static int LevenshteinDistance(this string s, string t)
|
||||||
|
{
|
||||||
|
var n = s.Length;
|
||||||
|
var m = t.Length;
|
||||||
|
var d = new int[n + 1, m + 1];
|
||||||
|
|
||||||
|
// Step 1
|
||||||
|
if (n == 0)
|
||||||
|
return m;
|
||||||
|
|
||||||
|
if (m == 0)
|
||||||
|
return n;
|
||||||
|
|
||||||
|
// Step 2
|
||||||
|
for (var i = 0; i <= n; d[i, 0] = i++)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j <= m; d[0, j] = j++)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3
|
||||||
|
for (var i = 1; i <= n; i++)
|
||||||
|
//Step 4
|
||||||
|
for (var j = 1; j <= m; j++)
|
||||||
|
{
|
||||||
|
// Step 5
|
||||||
|
var cost = t[j - 1] == s[i - 1] ? 0 : 1;
|
||||||
|
|
||||||
|
// Step 6
|
||||||
|
d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7
|
||||||
|
return d[n, m];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Stream> ToStream(this string str)
|
||||||
|
{
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
var sw = new StreamWriter(ms);
|
||||||
|
await sw.WriteAsync(str);
|
||||||
|
await sw.FlushAsync();
|
||||||
|
ms.Position = 0;
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsDiscordInvite(this string str)
|
||||||
|
=> _filterRegex.IsMatch(str);
|
||||||
|
|
||||||
|
public static string Unmention(this string str)
|
||||||
|
=> str.Replace("@", "ම", StringComparison.InvariantCulture);
|
||||||
|
|
||||||
|
public static string SanitizeMentions(this string str, bool sanitizeRoleMentions = false)
|
||||||
|
{
|
||||||
|
str = str.Replace("@everyone", "@everyοne", StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
.Replace("@here", "@һere", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
if (sanitizeRoleMentions)
|
||||||
|
str = str.SanitizeRoleMentions();
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SanitizeRoleMentions(this string str)
|
||||||
|
=> str.Replace("<@&", "<ම&", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
|
public static string SanitizeAllMentions(this string str)
|
||||||
|
=> str.SanitizeMentions().SanitizeRoleMentions();
|
||||||
|
|
||||||
|
public static string ToBase64(this string plainText)
|
||||||
|
{
|
||||||
|
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
|
||||||
|
return Convert.ToBase64String(plainTextBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetInitials(this string txt, string glue = "")
|
||||||
|
=> txt.Split(' ').Select(x => x.FirstOrDefault()).Join(glue);
|
||||||
|
|
||||||
|
public static bool IsAlphaNumeric(this string txt)
|
||||||
|
=> txt.All(c => _lettersAndDigits.Contains(c));
|
||||||
|
|
||||||
|
public static string UnescapeUnicodeCodePoints(this string input)
|
||||||
|
=> _codePointRegex.Replace(input,
|
||||||
|
me =>
|
||||||
|
{
|
||||||
|
var str = me.Groups["code"].Value;
|
||||||
|
var newString = YamlHelper.UnescapeUnicodeCodePoint(str);
|
||||||
|
return newString;
|
||||||
|
});
|
||||||
|
}
|
1
src/Ellise.Common/GlobalUsings.cs
Normal file
1
src/Ellise.Common/GlobalUsings.cs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
global using NonBlocking;
|
36
src/Ellise.Common/Helpers/LogSetup.cs
Normal file
36
src/Ellise.Common/Helpers/LogSetup.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog.Sinks.SystemConsole.Themes;
|
||||||
|
using System.Text;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public static class LogSetup
|
||||||
|
{
|
||||||
|
public static void SetupLogger(Object source)
|
||||||
|
{
|
||||||
|
Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||||
|
.MinimumLevel.Override("System", LogEventLevel.Information)
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.WriteTo.Console(LogEventLevel.Information,
|
||||||
|
theme: GetTheme(),
|
||||||
|
outputTemplate:
|
||||||
|
"[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
|
||||||
|
.Enrich.WithProperty("LogSource", source)
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConsoleTheme GetTheme()
|
||||||
|
{
|
||||||
|
if (Environment.OSVersion.Platform == PlatformID.Unix)
|
||||||
|
return AnsiConsoleTheme.Code;
|
||||||
|
#if DEBUG
|
||||||
|
return AnsiConsoleTheme.Code;
|
||||||
|
#else
|
||||||
|
return ConsoleTheme.None;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
7
src/Ellise.Common/Helpers/StandardConversions.cs
Normal file
7
src/Ellise.Common/Helpers/StandardConversions.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public static class StandardConversions
|
||||||
|
{
|
||||||
|
public static double CelsiusToFahrenheit(double cel)
|
||||||
|
=> (cel * 1.8f) + 32;
|
||||||
|
}
|
100
src/Ellise.Common/Kwum.cs
Normal file
100
src/Ellise.Common/Kwum.cs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
// needs proper invalid input check (character array input out of range)
|
||||||
|
// needs negative number support
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
#pragma warning disable CS8981
|
||||||
|
public readonly struct kwum : IEquatable<kwum>
|
||||||
|
#pragma warning restore CS8981
|
||||||
|
{
|
||||||
|
private const string VALID_CHARACTERS = "23456789abcdefghijkmnpqrstuvwxyz";
|
||||||
|
private readonly int _value;
|
||||||
|
|
||||||
|
public kwum(int num)
|
||||||
|
=> _value = num;
|
||||||
|
|
||||||
|
public kwum(in char c)
|
||||||
|
{
|
||||||
|
if (!IsValidChar(c))
|
||||||
|
throw new ArgumentException("Character needs to be a valid kwum character.", nameof(c));
|
||||||
|
|
||||||
|
_value = InternalCharToValue(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public kwum(in ReadOnlySpan<char> input)
|
||||||
|
{
|
||||||
|
_value = 0;
|
||||||
|
for (var index = 0; index < input.Length; index++)
|
||||||
|
{
|
||||||
|
var c = input[index];
|
||||||
|
if (!IsValidChar(c))
|
||||||
|
throw new ArgumentException("All characters need to be a valid kwum characters.", nameof(input));
|
||||||
|
|
||||||
|
_value += VALID_CHARACTERS.IndexOf(c) * (int)Math.Pow(VALID_CHARACTERS.Length, input.Length - index - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static int InternalCharToValue(in char c)
|
||||||
|
=> VALID_CHARACTERS.IndexOf(c);
|
||||||
|
|
||||||
|
public static bool TryParse(in ReadOnlySpan<char> input, out kwum value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
foreach (var c in input)
|
||||||
|
{
|
||||||
|
if (!IsValidChar(c))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = new(input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static kwum operator +(kwum left, kwum right)
|
||||||
|
=> new(left._value + right._value);
|
||||||
|
|
||||||
|
public static bool operator ==(kwum left, kwum right)
|
||||||
|
=> left._value == right._value;
|
||||||
|
|
||||||
|
public static bool operator !=(kwum left, kwum right)
|
||||||
|
=> !(left == right);
|
||||||
|
|
||||||
|
public static implicit operator long(kwum kwum)
|
||||||
|
=> kwum._value;
|
||||||
|
|
||||||
|
public static implicit operator int(kwum kwum)
|
||||||
|
=> kwum._value;
|
||||||
|
|
||||||
|
public static implicit operator kwum(int num)
|
||||||
|
=> new(num);
|
||||||
|
|
||||||
|
public static bool IsValidChar(char c)
|
||||||
|
=> VALID_CHARACTERS.Contains(c);
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var count = VALID_CHARACTERS.Length;
|
||||||
|
var localValue = _value;
|
||||||
|
var arrSize = (int)Math.Log(localValue, count) + 1;
|
||||||
|
Span<char> chars = new char[arrSize];
|
||||||
|
while (localValue > 0)
|
||||||
|
{
|
||||||
|
localValue = Math.DivRem(localValue, count, out var rem);
|
||||||
|
chars[--arrSize] = VALID_CHARACTERS[rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
=> obj is kwum kw && kw == this;
|
||||||
|
|
||||||
|
public bool Equals(kwum other)
|
||||||
|
=> other == this;
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
=> _value.GetHashCode();
|
||||||
|
}
|
80
src/Ellise.Common/PubSub/EventPubSub.cs
Normal file
80
src/Ellise.Common/PubSub/EventPubSub.cs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public class EventPubSub : IPubSub
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions = new();
|
||||||
|
private readonly object _locker = new();
|
||||||
|
|
||||||
|
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||||
|
where TData : notnull
|
||||||
|
{
|
||||||
|
Func<object, ValueTask> localAction = obj => action((TData)obj);
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
if (!_actions.TryGetValue(key.Key, out var KeyActions))
|
||||||
|
{
|
||||||
|
KeyActions = new();
|
||||||
|
_actions[key.Key] = KeyActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!KeyActions.TryGetValue(action, out var sameActions))
|
||||||
|
{
|
||||||
|
sameActions = new();
|
||||||
|
KeyActions[action] = sameActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
sameActions.Add(localAction);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Pub<TData>(in TypedKey<TData> key, TData data)
|
||||||
|
where TData : notnull
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
if (_actions.TryGetValue(key.Key, out var actions))
|
||||||
|
// if this class ever gets used, this needs to be properly implemented
|
||||||
|
// 1. ignore all valuetasks which are completed
|
||||||
|
// 2. run all other tasks in parallel
|
||||||
|
return actions.SelectMany(kvp => kvp.Value).Select(action => action(data).AsTask()).WhenAll();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Unsub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||||
|
{
|
||||||
|
lock (_locker)
|
||||||
|
{
|
||||||
|
// get subscriptions for this action
|
||||||
|
if (_actions.TryGetValue(key.Key, out var actions))
|
||||||
|
// get subscriptions which have the same action hash code
|
||||||
|
// note: having this as a list allows for multiple subscriptions of
|
||||||
|
// the same insance's/static method
|
||||||
|
{
|
||||||
|
if (actions.TryGetValue(action, out var sameActions))
|
||||||
|
{
|
||||||
|
// remove last subscription
|
||||||
|
sameActions.RemoveAt(sameActions.Count - 1);
|
||||||
|
|
||||||
|
// if the last subscription was the only subscription
|
||||||
|
// we can safely remove this action's dictionary entry
|
||||||
|
if (sameActions.Count == 0)
|
||||||
|
{
|
||||||
|
actions.Remove(action);
|
||||||
|
|
||||||
|
// if our dictionary has no more elements after
|
||||||
|
// removing the entry
|
||||||
|
// it's safe to remove it from the key's subscriptions
|
||||||
|
if (actions.Count == 0)
|
||||||
|
_actions.Remove(key.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/Ellise.Common/PubSub/IPubSub.cs
Normal file
10
src/Ellise.Common/PubSub/IPubSub.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public interface IPubSub
|
||||||
|
{
|
||||||
|
public Task Pub<TData>(in TypedKey<TData> key, TData data)
|
||||||
|
where TData : notnull;
|
||||||
|
|
||||||
|
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||||
|
where TData : notnull;
|
||||||
|
}
|
7
src/Ellise.Common/PubSub/ISeria.cs
Normal file
7
src/Ellise.Common/PubSub/ISeria.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public interface ISeria
|
||||||
|
{
|
||||||
|
byte[] Serialize<T>(T data);
|
||||||
|
T? Deserialize<T>(byte[] data);
|
||||||
|
}
|
63
src/Ellise.Common/QueueRunner.cs
Normal file
63
src/Ellise.Common/QueueRunner.cs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public sealed class QueueRunner
|
||||||
|
{
|
||||||
|
private readonly Channel<Func<Task>> _channel;
|
||||||
|
private readonly int _delayMs;
|
||||||
|
|
||||||
|
public QueueRunner(int delayMs = 0, int maxCapacity = -1)
|
||||||
|
{
|
||||||
|
if (delayMs < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(delayMs));
|
||||||
|
|
||||||
|
_delayMs = delayMs;
|
||||||
|
_channel = maxCapacity switch
|
||||||
|
{
|
||||||
|
0 or < -1 => throw new ArgumentOutOfRangeException(nameof(maxCapacity)),
|
||||||
|
-1 => Channel.CreateUnbounded<Func<Task>>(new UnboundedChannelOptions()
|
||||||
|
{
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
AllowSynchronousContinuations = true,
|
||||||
|
}),
|
||||||
|
_ => Channel.CreateBounded<Func<Task>>(new BoundedChannelOptions(maxCapacity)
|
||||||
|
{
|
||||||
|
Capacity = maxCapacity,
|
||||||
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
AllowSynchronousContinuations = true
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync(CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var func = await _channel.Reader.ReadAsync(cancel);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await func();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Exception executing a staggered func: {ErrorMessage}", ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_delayMs != 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(_delayMs, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask EnqueueAsync(Func<Task> action)
|
||||||
|
=> _channel.Writer.WriteAsync(action);
|
||||||
|
}
|
30
src/Ellise.Common/TypedKey.cs
Normal file
30
src/Ellise.Common/TypedKey.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
namespace Ellise.Common;
|
||||||
|
|
||||||
|
public readonly struct TypedKey<TData>
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
|
||||||
|
public TypedKey(in string key)
|
||||||
|
=> Key = key;
|
||||||
|
|
||||||
|
public static implicit operator TypedKey<TData>(in string input)
|
||||||
|
=> new(input);
|
||||||
|
|
||||||
|
public static implicit operator string(in TypedKey<TData> input)
|
||||||
|
=> input.Key;
|
||||||
|
|
||||||
|
public static bool operator ==(in TypedKey<TData> left, in TypedKey<TData> right)
|
||||||
|
=> left.Key == right.Key;
|
||||||
|
|
||||||
|
public static bool operator !=(in TypedKey<TData> left, in TypedKey<TData> right)
|
||||||
|
=> !(left == right);
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
=> obj is TypedKey<TData> o && o == this;
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
=> Key?.GetHashCode() ?? 0;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> Key;
|
||||||
|
}
|
48
src/Ellise.Common/YamlHelper.cs
Normal file
48
src/Ellise.Common/YamlHelper.cs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace Ellie.Common.Yml;
|
||||||
|
|
||||||
|
public static class YamlHelper
|
||||||
|
{
|
||||||
|
// https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687
|
||||||
|
/// <summary>
|
||||||
|
/// This is modified code from yamldotnet's repo which handles parsing unicode code points
|
||||||
|
/// it is needed as yamldotnet doesn't support unescaped unicode characters
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="point">Unicode code point</param>
|
||||||
|
/// <returns>Actual character</returns>
|
||||||
|
public static string UnescapeUnicodeCodePoint(this string point)
|
||||||
|
{
|
||||||
|
var character = 0;
|
||||||
|
|
||||||
|
// Scan the character value.
|
||||||
|
|
||||||
|
foreach (var c in point)
|
||||||
|
{
|
||||||
|
if (!IsHex(c))
|
||||||
|
return point;
|
||||||
|
|
||||||
|
character = (character << 4) + AsHex(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the value and write the character.
|
||||||
|
|
||||||
|
if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF)
|
||||||
|
return point;
|
||||||
|
|
||||||
|
return char.ConvertFromUtf32(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsHex(char c)
|
||||||
|
=> c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f');
|
||||||
|
|
||||||
|
public static int AsHex(char c)
|
||||||
|
{
|
||||||
|
if (c <= '9')
|
||||||
|
return c - '0';
|
||||||
|
|
||||||
|
if (c <= 'F')
|
||||||
|
return c - 'A' + 10;
|
||||||
|
|
||||||
|
return c - 'a' + 10;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue