Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions touki.tests/TestSupport/TypeExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

namespace TestSupport;

// Appears to be a flaw in the CS8620 analyzer with params ReadOnlySpan<>. Errors only show up in output.
// To avoid PR and AI noise, we're using null-forgiving operator in the tests below to work around this.

public class TypeExtensionTests
{
[Fact]
Expand All @@ -23,23 +26,23 @@ public void GetFullNestedType_NonGenericNested_ReturnsNestedType()
public void GetFullNestedType_GenericNestedWithParentGenerics_ReturnsConstructedNestedType()
{
Type parentType = typeof(OuterClass<int, string>);
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(double));
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(double)!);
nestedType.Should().Be<OuterClass<int, string>.GenericNested<double>>();
}

[Fact]
public void GetFullNestedType_GenericNestedWithNonGenericParent_ReturnsConstructedNestedType()
{
Type parentType = typeof(OuterClass);
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(double));
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(double)!);
nestedType.Should().Be<OuterClass.GenericNested<double>>();
}

[Fact]
public void GetFullNestedType_GenericNestedWithMultipleParameters_ReturnsConstructedNestedType()
{
Type parentType = typeof(OuterClass);
Type nestedType = parentType.GetFullNestedType("MultiGenericNested", typeof(int), typeof(string));
Type nestedType = parentType.GetFullNestedType("MultiGenericNested", typeof(int)!, typeof(string)!);
nestedType.Should().Be<OuterClass.MultiGenericNested<int, string>>();
}

Expand Down Expand Up @@ -91,7 +94,7 @@ public void GetFullNestedType_GenericNestedWithoutExplicitParameters_ThrowsArgum
public void GetFullNestedType_GenericParentDefinition_ThrowsArgumentException()
{
Type parentType = typeof(OuterClass<,>);
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(double));
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(double)!);
act.Should().Throw<ArgumentException>()
.WithMessage("The parent type cannot be a type definition.*")
.WithParameterName("type");
Expand All @@ -101,7 +104,7 @@ public void GetFullNestedType_GenericParentDefinition_ThrowsArgumentException()
public void GetFullNestedType_WrongGenericParameterCount_ThrowsArgumentException()
{
Type parentType = typeof(OuterClass<int, string>);
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(double), typeof(float));
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(double)!, typeof(float)!);
act.Should().Throw<ArgumentException>()
.WithMessage("Could not find GenericNested in OuterClass`2");
}
Expand All @@ -119,15 +122,15 @@ public void GetFullNestedType_TooFewGenericParameters_ThrowsArgumentException()
public void GetFullNestedType_TooFewGenericParametersForNonGenericParent_ThrowsArgumentException()
{
Type parentType = typeof(OuterClass);
Action act = () => parentType.GetFullNestedType("MultiGenericNested", typeof(int));
Action act = () => parentType.GetFullNestedType("MultiGenericNested", typeof(int)!);
act.Should().Throw<ArgumentException>();
}

[Fact]
public void GetFullNestedType_TooManyGenericParametersForNonGenericParent_ThrowsArgumentException()
{
Type parentType = typeof(OuterClass);
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(int), typeof(string), typeof(double));
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(int)!, typeof(string)!, typeof(double)!);
act.Should().Throw<ArgumentException>();
}

Expand Down Expand Up @@ -161,7 +164,7 @@ public void GetFullNestedType_PrivateNestedType_ReturnsNestedType()
public void GetFullNestedType_ComplexGenericCombination_ReturnsConstructedNestedType()
{
Type parentType = typeof(OuterClass<List<int>, Dictionary<string, double>>);
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(HashSet<bool>));
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(HashSet<bool>)!);
nestedType.Should().NotBeNull();
nestedType.GenericTypeArguments.Should().HaveCount(3);
nestedType.GenericTypeArguments[0].Should().Be<List<int>>();
Expand All @@ -173,7 +176,7 @@ public void GetFullNestedType_ComplexGenericCombination_ReturnsConstructedNested
public void GetFullNestedType_NestedGenericDefinitionRequiresParameter_ThrowsArgumentException()
{
Type parentType = typeof(OuterClass);
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(int));
Type nestedType = parentType.GetFullNestedType("GenericNested", typeof(int)!);
Action act = () => nestedType.GetFullNestedType("DeeplyNested");
act.Should().Throw<ArgumentException>()
.WithMessage("Could not find DeeplyNested in GenericNested`1");
Expand All @@ -183,7 +186,7 @@ public void GetFullNestedType_NestedGenericDefinitionRequiresParameter_ThrowsArg
public void GetFullNestedType_ParameterCountMismatchWithParentGenerics_ThrowsArgumentException()
{
Type parentType = typeof(OuterClassWithInheritedGenerics<int, string>);
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(double));
Action act = () => parentType.GetFullNestedType("GenericNested", typeof(double)!);
act.Should().Throw<ArgumentException>()
.WithMessage("Could not find GenericNested in OuterClassWithInheritedGenerics`2");
}
Expand Down
29 changes: 16 additions & 13 deletions touki/Framework/System/Globalization/InternalDateTimeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ namespace System;

internal static class InternalDateTimeExtensions
{
// Exactly the same as GetDatePart, except computing all of
// year/month/day rather than just one of them. Used when all three
// are needed rather than redoing the computations for each.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void GetDate(this ref readonly DateTime dateTime, out int year, out int month, out int day) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetDate(out year, out month, out day);
extension(ref readonly DateTime dateTime)
{
// Exactly the same as GetDatePart, except computing all of
// year/month/day rather than just one of them. Used when all three
// are needed rather than redoing the computations for each.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void GetDate(out int year, out int month, out int day) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetDate(out year, out month, out day);

internal static void GetTime(this ref readonly DateTime dateTime, out int hour, out int minute, out int second) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetTime(out hour, out minute, out second);
internal void GetTime(out int hour, out int minute, out int second) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetTime(out hour, out minute, out second);

internal static void GetTime(this ref readonly DateTime dateTime, out int hour, out int minute, out int second, out int millisecond) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetTime(out hour, out minute, out second, out millisecond);
internal void GetTime(out int hour, out int minute, out int second, out int millisecond) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetTime(out hour, out minute, out second, out millisecond);

internal static void GetTimePrecise(this ref readonly DateTime dateTime, out int hour, out int minute, out int second, out int tick) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetTimePrecise(out hour, out minute, out second, out tick);
internal void GetTimePrecise(out int hour, out int minute, out int second, out int tick) =>
Unsafe.As<DateTime, InternalDateTime>(ref Unsafe.AsRef(in dateTime)).GetTimePrecise(out hour, out minute, out second, out tick);
}

internal readonly struct InternalDateTime
private readonly struct InternalDateTime
{
// Number of 100ns ticks per time unit
private const long TicksPerMillisecond = 10000;
Expand Down
47 changes: 25 additions & 22 deletions touki/Framework/Touki/Io/StreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,36 @@ namespace Touki.Io;
/// </summary>
public static partial class StreamExtensions
{
/// <summary>
/// Allows writing a <see cref="ReadOnlySpan{Char}"/> to a <see cref="TextWriter"/>.
/// </summary>
public static void Write(this TextWriter writer, ReadOnlySpan<char> value)
extension(TextWriter writer)
{
if (value.Length > 0)
/// <summary>
/// Allows writing a <see cref="ReadOnlySpan{Char}"/> to a <see cref="TextWriter"/>.
/// </summary>
public void Write(ReadOnlySpan<char> value)
{
char[] buffer = ArrayPool<char>.Shared.Rent(value.Length);
value.CopyTo(buffer);
writer.Write(buffer, 0, value.Length);
ArrayPool<char>.Shared.Return(buffer);
if (value.Length > 0)
{
char[] buffer = ArrayPool<char>.Shared.Rent(value.Length);
value.CopyTo(buffer);
writer.Write(buffer, 0, value.Length);
ArrayPool<char>.Shared.Return(buffer);
}
}
}

/// <summary>
/// Allows writing a <see cref="ReadOnlySpan{Char}"/> to a <see cref="TextWriter"/>.
/// </summary>
public static void WriteLine(this TextWriter writer, ReadOnlySpan<char> value)
{
if (value.Length > 0)
/// <summary>
/// Allows writing a <see cref="ReadOnlySpan{Char}"/> to a <see cref="TextWriter"/>.
/// </summary>
public void WriteLine(ReadOnlySpan<char> value)
{
char[] buffer = ArrayPool<char>.Shared.Rent(value.Length);
value.CopyTo(buffer);
writer.Write(buffer, 0, value.Length);
ArrayPool<char>.Shared.Return(buffer);
}
if (value.Length > 0)
{
char[] buffer = ArrayPool<char>.Shared.Rent(value.Length);
value.CopyTo(buffer);
writer.Write(buffer, 0, value.Length);
ArrayPool<char>.Shared.Return(buffer);
}

writer.WriteLine();
writer.WriteLine();
}
}
}
12 changes: 7 additions & 5 deletions touki/Framework/Touki/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ namespace Touki;
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// Determines whether the current type can be assigned to a variable of the specified <paramref name="targetType"/>.
/// </summary>
public static bool IsAssignableTo(this Type? type, Type? targetType) =>
targetType?.IsAssignableFrom(type) ?? false;
extension(Type? type)
{
/// <summary>
/// Determines whether the current type can be assigned to a variable of the specified <paramref name="targetType"/>.
/// </summary>
public bool IsAssignableTo(Type? targetType) => targetType?.IsAssignableFrom(type) ?? false;
}
}
93 changes: 42 additions & 51 deletions touki/Touki/Buffers/SpanExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,68 +9,59 @@ namespace Touki;
/// </summary>
public static partial class SpanExtensions
{
/// <summary>
/// Slice the given <paramref name="span"/> at null, if present.
/// </summary>
public static ReadOnlySpan<char> SliceAtNull(this ReadOnlySpan<char> span)
extension(ReadOnlySpan<char> span)
{
int index = span.IndexOf('\0');
return index == -1 ? span : span[..index];
}

/// <summary>
/// Slice the given <paramref name="span"/> at null, if present.
/// </summary>
public static Span<char> SliceAtNull(this Span<char> span)
{
int index = span.IndexOf('\0');
return index == -1 ? span : span[..index];
/// <summary>
/// Slice the given span at null, if present.
/// </summary>
public ReadOnlySpan<char> SliceAtNull()
{
int index = span.IndexOf('\0');
return index == -1 ? span : span[..index];
}
}

/// <summary>
/// Splits into strings on the given <paramref name="delimiter"/>.
/// </summary>
public static IEnumerable<string> SplitToEnumerable(this ReadOnlySpan<char> span, char delimiter, bool includeEmptyStrings = false)
extension(Span<char> span)
{
List<string> strings = [];
SpanReader<char> reader = new(span);
while (reader.TryReadTo(delimiter, out var next))
/// <summary>
/// Slice the given span at null, if present.
/// </summary>
public Span<char> SliceAtNull()
{
if (includeEmptyStrings || !next.IsEmpty)
{
strings.Add(next.ToString());
}
int index = span.IndexOf((char)0);
return index == -1 ? span : span[..index];
}

return strings;
}

/// <summary>
/// Returns the exact comparison of two spans as if they were strings, including embedded nulls.
/// </summary>
/// <remarks>
/// <para>
/// Use this method when you want the exact same result you would get from ordinally comparing two strings
/// that have the same content.
/// </para>
/// </remarks>
public static int CompareOrdinalAsString(this ReadOnlySpan<char> span1, ReadOnlySpan<char> span2)
extension(ReadOnlySpan<char> span1)
{
int sharedLength = Math.Min(span1.Length, span2.Length);
/// <summary>
/// Returns the exact comparison of two spans as if they were strings, including embedded nulls.
/// </summary>
/// <remarks>
/// <para>
/// Use this method when you want the exact same result you would get from ordinally comparing two strings
/// that have the same content.
/// </para>
/// </remarks>
public int CompareOrdinalAsString(ReadOnlySpan<char> span2)
{
int sharedLength = Math.Min(span1.Length, span2.Length);

int result = span1[..sharedLength].SequenceCompareTo(span2[..sharedLength]);
int result = span1[..sharedLength].SequenceCompareTo(span2[..sharedLength]);

if (result != 0 || span1.Length == span2.Length)
{
// If the spans are equal and the same length, or we found a mismatch, return the result.
return result;
}
if (result != 0 || span1.Length == span2.Length)
{
// If the spans are equal and the same length, or we found a mismatch, return the result.
return result;
}

// If we've fully matched the shared length, follow the logic string would do. If there is no shared length
// or the shared length is odd, we return the next character in the longer span, inverted if it is from
// the second span (effectively comparing to "null").
return sharedLength != 0 && sharedLength % 2 == 0
? span1.Length - span2.Length
: span1.Length > span2.Length ? span1[sharedLength] : -span2[sharedLength];
// If we've fully matched the shared length, follow the logic string would do. If there is no shared length
// or the shared length is odd, we return the next character in the longer span, inverted if it is from
// the second span (effectively comparing to "null").
return sharedLength != 0 && sharedLength % 2 == 0
? span1.Length - span2.Length
: span1.Length > span2.Length ? span1[sharedLength] : -span2[sharedLength];
}
}
}
35 changes: 19 additions & 16 deletions touki/Touki/Buffers/SpanReaderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,28 @@ namespace Touki;
/// </summary>
public static class SpanReaderExtensions
{
/// <summary>
/// Tries to read an integer from the current position of the <see cref="SpanReader{T}"/>.
/// </summary>
/// <param name="reader">The <see cref="SpanReader{T}"/> to read from.</param>
/// <param name="value">When successful, contains the read integer.</param>
/// <returns><see langword="true"/> if an integer was successfully read; otherwise, <see langword="false"/>.</returns>
public static bool TryReadPositiveInteger(this ref SpanReader<char> reader, out uint value)
extension(ref SpanReader<char> reader)
{
// Read digits until we hit a non-digit character or the end of the span.
value = default;
bool foundDigit = false;

while (reader.TryPeek(out char next) && char.IsDigit(next))
/// <summary>
/// Tries to read an integer from the current position of the <see cref="SpanReader{T}"/>.
/// </summary>
/// <param name="value">When successful, contains the read integer.</param>
/// <returns><see langword="true"/> if an integer was successfully read; otherwise, <see langword="false"/>.</returns>
public bool TryReadPositiveInteger(out uint value)
{
value = value * 10u + (uint)(next - '0');
reader.Advance(1);
foundDigit = true;
}
// Read digits until we hit a non-digit character or the end of the span.
value = default;
bool foundDigit = false;

return foundDigit;
while (reader.TryPeek(out char next) && char.IsDigit(next))
{
value = value * 10u + (uint)(next - '0');
reader.Advance(1);
foundDigit = true;
}

return foundDigit;
}
}
}
Loading