Skip to content

Instantly share code, notes, and snippets.

@MichalBrylka
Last active December 9, 2022 14:24
Show Gist options
  • Save MichalBrylka/0b226418e297fbe6d7413e0c812c4d19 to your computer and use it in GitHub Desktop.
Save MichalBrylka/0b226418e297fbe6d7413e0c812c4d19 to your computer and use it in GitHub Desktop.
Generic matrix
using System.Buffers;
namespace GenMath;
public static class Extensions
{
public static TResult[] ToArray<TResult>(this Range range, Func<int, TResult> transformer)
{
if (range.Start.IsFromEnd || range.End.IsFromEnd) throw new NotSupportedException("'FromEnd' ranges are not supported ");
return Enumerable.Range(range.Start.Value, range.End.Value - range.Start.Value + 1).Select(transformer).ToArray();
}
public unsafe static void CopyTo2D<T>(this ReadOnlySpan<T> source, T[,] target)
where T : unmanaged
{
fixed (T* pSource = source, pTarget = target)
{
for (int i = 0; i < source.Length; i++)
pTarget[i] = pSource[i];
}
}
public static string FormatToString(this ISpanFormattable formattable, string? format, IFormatProvider? formatProvider = null)
{
formatProvider ??= CultureInfo.InvariantCulture;
int bufferLength;
char[]? arrayBuffer = null;
Span<char> buffer = stackalloc char[bufferLength = 64];
var formatAsSpan = format.AsSpan();
try
{
if (!formattable.TryFormat(buffer, out var charsWritten, formatAsSpan, formatProvider))
{
do
{
if (arrayBuffer != null)
ArrayPool<char>.Shared.Return(arrayBuffer);
bufferLength *= 2;
buffer = arrayBuffer = ArrayPool<char>.Shared.Rent(bufferLength);
} while (!formattable.TryFormat(buffer, out charsWritten, formatAsSpan, formatProvider));
}
return buffer[..charsWritten].ToString();
}
finally
{
if (arrayBuffer != null)
ArrayPool<char>.Shared.Return(arrayBuffer);
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11.0</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.CodeAnalysis" />
<Using Include="System.Globalization" />
<Using Include="System.Numerics" />
<Using Include="System.Runtime.CompilerServices" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Nemesis.TextParsers" Version="2.6.3" />
</ItemGroup>
</Project>
namespace GenMath;
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public partial class Matrix<TNumber> : ISpanParsable<Matrix<TNumber>>, ISpanFormattable,
IAdditionOperators<Matrix<TNumber>, Matrix<TNumber>, Matrix<TNumber>>,
IMultiplyOperators<Matrix<TNumber>, Matrix<TNumber>, Matrix<TNumber>>,
IAdditionOperators<Matrix<TNumber>, TNumber, Matrix<TNumber>>
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>,
INumberBase<TNumber>
{
private readonly TNumber[,] _data;
public int Rows { get; }
public int Columns { get; }
public int Size { get; }
public TNumber this[int iRow, int iCol] => _data[iRow, iCol];
public Matrix(TNumber[,] data)
{
_data = data;
Rows = data.GetLength(0);
Columns = data.GetLength(1);
Size = Rows * Columns;
}
public unsafe Matrix(TNumber[] data, int columns)
{
var data2d = new TNumber[data.Length / columns, columns];
fixed (TNumber* pSource = data, pTarget = data2d)
{
for (int i = 0; i < data.Length; i++)
pTarget[i] = pSource[i];
}
_data = data2d;
Rows = data2d.GetLength(0);
Columns = data2d.GetLength(1);
}
#region Math
public Matrix<TNumber> Minor(int iRow, int iCol)
{
var minor = new TNumber[Rows - 1, Columns - 1];
int m = 0;
for (int i = 0; i < Rows; i++)
{
if (i == iRow)
continue;
int n = 0;
for (int j = 0; j < Columns; j++)
{
if (j == iCol)
continue;
minor[m, n] = this[i, j];
n++;
}
m++;
}
return new(minor);
}
public TNumber Determinant()
{
if (Rows != Columns) throw new("Determinant of a non-square matrix doesn't exist");
var det = TNumber.Zero;
if (Rows == 1) return this[0, 0];
if (Rows == 2) return this[0, 0] * this[1, 1] - this[0, 1] * this[1, 0];
for (int j = 0; j < Columns; j++)
{
TNumber reduced = this[0, j] * Minor(0, j).Determinant();
if (j % 2 == 1)
reduced = -reduced;
det += reduced;
}
return det;
}
public unsafe TNumber Min()
{
if (Size == 0) throw new("Matrix is empty");
TNumber result;
fixed (TNumber* pData = _data)
{
var p = pData;
result = *p;
for (int i = 1; i < Size; i++)
result = Min(result, *p++);
}
return result;
static TNumber Min(TNumber x, TNumber y)
{
if ((x != y) && !TNumber.IsNaN(x))
return x < y ? x : y;
return TNumber.IsNegative(x) ? x : y;
}
}
public unsafe TNumber Max()
{
if (Size == 0) throw new("Matrix is empty");
TNumber result;
fixed (TNumber* pData = _data)
{
var p = pData;
result = *p;
for (int i = 1; i < Size; i++)
result = Max(result, *p++);
}
return result;
static TNumber Max(TNumber x, TNumber y)
{
if (x != y)
return TNumber.IsNaN(x) ? x : y < x ? x : y;
return TNumber.IsNegative(y) ? x : y;
}
}
public unsafe TNumber Sum()
{
var result = TNumber.Zero;
fixed (TNumber* pData = _data)
{
var p = pData;
for (int i = 0; i < Size; i++)
result += *p++;
}
return result;
}
public unsafe TResult Sum<TResult>() where TResult : INumber<TResult>
{
var result = TResult.Zero;
fixed (TNumber* pData = _data)
{
var p = pData;
for (int i = 0; i < Size; i++)
result += TResult.CreateChecked(*p++);
}
return result;
}
public TResult Average<TResult>() where TResult : INumber<TResult>
{
TResult sum = Sum<TResult>();
return TResult.CreateChecked(sum) / TResult.CreateChecked(Size);
}
#endregion
#region Operators
public unsafe static Matrix<TNumber> operator +(Matrix<TNumber> left, Matrix<TNumber> right)
{
if (left.Rows != right.Rows || left.Columns != right.Columns) throw new("Sum of 2 matrices is only possible when they are same size");
var data = new TNumber[left.Rows, left.Columns];
var size = left.Rows * left.Columns;
fixed (TNumber* lSource = left._data, rSource = right._data, target = data)
{
for (int i = 0; i < size; i++)
target[i] = lSource[i] + rSource[i];
}
return new Matrix<TNumber>(data);
}
public unsafe static Matrix<TNumber> operator checked +(Matrix<TNumber> left, Matrix<TNumber> right)
{
if (left.Rows != right.Rows || left.Columns != right.Columns) throw new("Sum of 2 matrices is only possible when they are same size");
var data = new TNumber[left.Rows, left.Columns];
var size = left.Rows * left.Columns;
fixed (TNumber* lSource = left._data, rSource = right._data, target = data)
{
for (int i = 0; i < size; i++)
target[i] = checked(lSource[i] + rSource[i]);
}
return new Matrix<TNumber>(data);
}
public unsafe static Matrix<TNumber> operator +(Matrix<TNumber> left, TNumber right)
{
var data = new TNumber[left.Rows, left.Columns];
var size = left.Rows * left.Columns;
fixed (TNumber* lSource = left._data, target = data)
{
for (int i = 0; i < size; i++)
target[i] = lSource[i] + right;
}
return new Matrix<TNumber>(data);
}
public static Matrix<TNumber> operator *(Matrix<TNumber> a, Matrix<TNumber> b)
{
int rowsA = a.Rows, colsA = a.Columns, rowsB = b.Rows, colsB = b.Columns;
if (colsA != rowsB) throw new("Matrixes can't be multiplied");
var data = new TNumber[rowsA, colsB];
for (int i = 0; i < rowsA; i++)
{
for (int j = 0; j < colsB; j++)
{
var temp = TNumber.Zero;
for (int k = 0; k < colsA; k++)
temp += a[i, k] * b[k, j];
data[i, j] = temp;
}
}
return new Matrix<TNumber>(data);
}
#endregion
}
using System.Buffers;
using Nemesis.TextParsers;
using Nemesis.TextParsers.Utils;
namespace GenMath;
/// <summary>Parsing and formatting operation for matrices</summary>
public interface IMatrixTextFormat
{
/// <summary>
/// Parse matrix from text buffer
/// </summary>
Matrix<TNumber> Parse<TNumber>(ReadOnlySpan<char> s)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>;
/// <summary>
/// Attempt to format current matrix in provided text buffer
/// </summary>
/// <param name="matrix">Current matrix</param>
/// <param name="destination">The span in which to write this instance's value formatted as a span of characters</param>
/// <param name="charsWritten">When this method returns, contains the number of characters that were written in destination</param>
/// <param name="format">A span containing the characters that represent a standard or custom format string that defines the acceptable format for destination</param>
/// <returns>true if the formatting was successful; otherwise, false</returns>
bool TryFormat<TNumber>(Matrix<TNumber> matrix, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>;
}
/// <summary>
/// Parse/format matrices in form of
/// 1 2 3
/// 4 5 6
/// 7 8 9
/// </summary>
public readonly struct StandardFormat : IMatrixTextFormat
{
private readonly IFormatProvider _underlyingProvider;
private readonly NumberStyles? _numberStyles;
private readonly char _elementSeparator;
private static readonly char[] _rowSeparators = Environment.NewLine.ToCharArray();
public StandardFormat() : this(CultureInfo.InvariantCulture) { }
public StandardFormat(IFormatProvider? underlyingProvider, NumberStyles numberStyles = NumberStyles.Any)
{
_numberStyles = numberStyles;
_underlyingProvider = underlyingProvider ?? CultureInfo.InvariantCulture;
(_underlyingProvider, _elementSeparator) = GetParameters();
}
private (IFormatProvider Provider, char ElementSeparator) GetParameters()
{
var provider = _underlyingProvider ?? CultureInfo.InvariantCulture;
char elementSeparator = _elementSeparator != '\0'
? _elementSeparator
: (provider is CultureInfo ci ? ci.TextInfo.ListSeparator.Trim().Single() : ';');
return (provider, elementSeparator);
}
public Matrix<TNumber> Parse<TNumber>(ReadOnlySpan<char> s)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>
{
var (provider, elementSeparator) = GetParameters();
var numberStyles = _numberStyles ?? NumberStyles.Any;
var rowsEnumerator = s.Split(_rowSeparators, true).GetEnumerator();
if (!rowsEnumerator.MoveNext()) throw new FormatException("Non empty text is expected");
var firstRow = rowsEnumerator.Current;
int numCols = 0;
using var buffer = new ValueSequenceBuilder<TNumber>(stackalloc TNumber[32]);
foreach (var col in firstRow.Split(elementSeparator, true))
{
if (col.IsEmpty) continue;
buffer.Append(TNumber.Parse(col, numberStyles, provider));
numCols++;
}
int numRows = 1;
while (rowsEnumerator.MoveNext())
{
var row = rowsEnumerator.Current;
if (row.IsEmpty) continue;
foreach (var col in row.Split(elementSeparator, true))
{
if (col.IsEmpty) continue;
buffer.Append(TNumber.Parse(col, numberStyles, provider));
}
numRows++;
}
var matrix = new TNumber[numRows, numCols];
buffer.AsSpan().CopyTo2D(matrix);
return new Matrix<TNumber>(matrix);
}
public bool TryFormat<TNumber>(Matrix<TNumber> matrix, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>
{
var (provider, elementSeparator) = GetParameters();
var newLine = _rowSeparators.AsSpan();
var newLineLen = newLine.Length;
int charsWrittenSoFar = 0;
for (int i = 0; i < matrix.Rows; i++)
{
for (int j = 0; j < matrix.Columns; j++)
{
bool tryFormatSucceeded = matrix[i, j].TryFormat(destination[charsWrittenSoFar..], out var tryFormatCharsWritten, format, provider);
charsWrittenSoFar += tryFormatCharsWritten;
if (!tryFormatSucceeded)
{
charsWritten = charsWrittenSoFar;
return false;
}
if (j < matrix.Columns - 1)
{
if (destination.Length < charsWrittenSoFar + 2)
{
charsWritten = charsWrittenSoFar;
return false;
}
destination[charsWrittenSoFar++] = elementSeparator;
destination[charsWrittenSoFar++] = ' ';
}
}
if (i < matrix.Rows - 1)
{
if (destination.Length < charsWrittenSoFar + newLineLen)
{
charsWritten = charsWrittenSoFar;
return false;
}
newLine.CopyTo(destination[charsWrittenSoFar..]);
charsWrittenSoFar += newLineLen;
}
}
charsWritten = charsWrittenSoFar;
return true;
}
}
/// <summary>
/// Parse/format matrices in form of [1,2,3 ; 4,5,6 ; 7,8,9]
/// </summary>
public sealed class MatlabFormat : IMatrixTextFormat
{
private const char _elementSeparator = ',';
private const char _rowSeparator = ';';
private const char _openingBracket = '[';
private const char _closingBracket = ']';
private MatlabFormat() { }
public static IMatrixTextFormat Instance { get; } = new MatlabFormat();
public Matrix<TNumber> Parse<TNumber>(ReadOnlySpan<char> s)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>
{
s = s.Trim().TrimStart(_openingBracket).TrimEnd(_closingBracket).Trim();
var rowsEnumerator = s.Split(_rowSeparator, true).GetEnumerator();
if (!rowsEnumerator.MoveNext()) throw new FormatException("Non empty text is expected");
var firstRow = rowsEnumerator.Current;
int numCols = 0;
using var buffer = new ValueSequenceBuilder<TNumber>(stackalloc TNumber[32]);
foreach (var col in firstRow.Split(_elementSeparator, ' ', true))
{
if (col.IsEmpty) continue;
buffer.Append(TNumber.Parse(col, NumberStyles.Any, CultureInfo.InvariantCulture));
numCols++;
}
int numRows = 1;
while (rowsEnumerator.MoveNext())
{
var row = rowsEnumerator.Current;
if (row.IsEmpty) continue;
foreach (var col in row.Split(_elementSeparator, ' ', true))
{
if (col.IsEmpty) continue;
buffer.Append(TNumber.Parse(col, NumberStyles.Any, CultureInfo.InvariantCulture));
}
numRows++;
}
var matrix = new TNumber[numRows, numCols];
buffer.AsSpan().CopyTo2D(matrix);
return new Matrix<TNumber>(matrix);
}
public bool TryFormat<TNumber>(Matrix<TNumber> matrix, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>
{
charsWritten = 0;
if (destination.Length < 1) return false;
destination[charsWritten++] = _openingBracket;
for (int i = 0; i < matrix.Rows; i++)
{
for (int j = 0; j < matrix.Columns; j++)
{
bool tryFormatSucceeded = matrix[i, j].TryFormat(destination[charsWritten..], out var tryFormatCharsWritten, format, CultureInfo.InvariantCulture);
charsWritten += tryFormatCharsWritten;
if (!tryFormatSucceeded)
{
return false;
}
if (j < matrix.Columns - 1)
{
if (destination.Length < charsWritten + 2)
{
return false;
}
destination[charsWritten++] = _elementSeparator;
destination[charsWritten++] = ' ';
}
}
if (i < matrix.Rows - 1)
{
if (destination.Length < charsWritten + 1)
return false;
destination[charsWritten++] = ' ';
destination[charsWritten++] = _rowSeparator;
destination[charsWritten++] = ' ';
}
}
if (destination.Length < charsWritten + 1) return false;
destination[charsWritten++] = _closingBracket;
return true;
}
}
/// <summary>
/// Parse/format matrices in form of {{1,2,3},{3,2,1},{2,1,3}}
/// </summary>
public sealed class MathematicaFormat : IMatrixTextFormat
{
private const char _elementSeparator = ',';
private const char _openingBrace = '{';
private const char _closingBrace = '}';
private MathematicaFormat() { }
public static IMatrixTextFormat Instance { get; } = new MathematicaFormat();
public Matrix<TNumber> Parse<TNumber>(ReadOnlySpan<char> s)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>
{
s = s.Trim();
ScanUntil(ref s, _openingBrace, () => "No matrix row starter");
ExtractToLast(ref s, _closingBrace, () => "No matrix row terminator");
s = s.Trim();
static void ScanUntil(ref ReadOnlySpan<char> s, char element, Func<string> errorBuilder)
{
var start = s.IndexOf(element);
if (start < 0) throw new FormatException(errorBuilder());
if (start + 1 >= s.Length) throw new FormatException("Unexpected end of string");
s = s[(start + 1)..];
}
static void ExtractToLast(ref ReadOnlySpan<char> s, char element, Func<string> errorBuilder)
{
var start = s.LastIndexOf(element);
if (start < 0) throw new FormatException(errorBuilder());
s = s[..start];
}
static ReadOnlySpan<char> ExtractUntil(ref ReadOnlySpan<char> s, char element, Func<string> errorBuilder)
{
var start = s.IndexOf(element);
if (start < 0) throw new FormatException(errorBuilder());
var result = s[..(start)];
s = s[(start + 1)..];
return result;
}
ScanUntil(ref s, _openingBrace, () => "No first row starter");
var firstRow = ExtractUntil(ref s, _closingBrace, () => "No first row terminator");
int numCols = 0;
using var numbers = new ValueSequenceBuilder<TNumber>(stackalloc TNumber[32]);
foreach (var col in firstRow.Split(_elementSeparator, true))
{
if (col.IsEmpty) continue;
numbers.Append(TNumber.Parse(col, NumberStyles.Any, CultureInfo.InvariantCulture));
numCols++;
}
int numRows = 1;
while (!s.IsEmpty)
{
numRows++;
ScanUntil(ref s, _elementSeparator, () => "No rows separator");
ScanUntil(ref s, _openingBrace, () => $"No starter for row {numRows}");
var row = ExtractUntil(ref s, _closingBrace, () => $"No terminator for row {numRows}");
foreach (var col in row.Split(_elementSeparator, true))
{
if (col.IsEmpty) continue;
numbers.Append(TNumber.Parse(col, NumberStyles.Any, CultureInfo.InvariantCulture));
}
}
var matrix = new TNumber[numRows, numCols];
numbers.AsSpan().CopyTo2D(matrix);
return new Matrix<TNumber>(matrix);
}
public bool TryFormat<TNumber>(Matrix<TNumber> matrix, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format)
where TNumber : unmanaged, IComparisonOperators<TNumber, TNumber, bool>, INumberBase<TNumber>
{
charsWritten = 0;
if (destination.Length < 2) return false;
destination[charsWritten++] = _openingBrace;
for (int i = 0; i < matrix.Rows; i++)
{
if (destination.Length < charsWritten + 1) return false;
destination[charsWritten++] = _openingBrace;
for (int j = 0; j < matrix.Columns; j++)
{
if (destination.Length < charsWritten + 1) return false;
destination[charsWritten++] = _openingBrace;
bool tryFormatSucceeded = matrix[i, j].TryFormat(destination[charsWritten..], out var tryFormatCharsWritten, format, CultureInfo.InvariantCulture);
charsWritten += tryFormatCharsWritten;
if (!tryFormatSucceeded) return false;
if (j < matrix.Columns - 1)
{
if (destination.Length < charsWritten + 2) return false;
destination[charsWritten++] = _elementSeparator;
destination[charsWritten++] = ' ';
}
if (destination.Length < charsWritten + 1) return false;
destination[charsWritten++] = _closingBrace;
}
if (i < matrix.Rows - 1)
{
if (destination.Length < charsWritten + 2) return false;
destination[charsWritten++] = _elementSeparator;
destination[charsWritten++] = ' ';
}
if (destination.Length < charsWritten + 1) return false;
destination[charsWritten++] = _closingBrace;
}
if (destination.Length < charsWritten + 1) return false;
destination[charsWritten++] = _closingBrace;
return true;
}
}
partial class Matrix<TNumber>
{
#region Parse
public static Matrix<TNumber> Parse(ReadOnlySpan<char> s, IMatrixTextFormat? matrixTextFormat = null) => (matrixTextFormat ?? default(StandardFormat)).Parse<TNumber>(s);
public static Matrix<TNumber> Parse(ReadOnlySpan<char> s, IFormatProvider? provider) => Parse(s, new StandardFormat(provider));
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out Matrix<TNumber> result)
{
try
{
result = s.IsEmpty ? new Matrix<TNumber>(new TNumber[0, 0]) : Parse(s, provider);
return true;
}
catch
{
result = default;
return false;
}
}
public static Matrix<TNumber> Parse(string s, IFormatProvider? provider) => Parse(s.AsSpan(), provider);
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Matrix<TNumber> result)
{
try
{
result = s switch
{
null => default,
"" => new Matrix<TNumber>(new TNumber[0, 0]),
_ => Parse(s.AsSpan(), provider)
};
return result is not null;
}
catch
{
result = default;
return false;
}
}
#endregion
#region Format
public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format, IMatrixTextFormat? matrixTextFormat = null)
=> (matrixTextFormat ?? default(StandardFormat)).TryFormat(this, destination, out charsWritten, format);
public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format, IFormatProvider? provider)
=> TryFormat(destination, out charsWritten, format, new StandardFormat(provider));
public string ToString(string? format, IMatrixTextFormat? matrixTextFormat = null)
{
matrixTextFormat ??= default(StandardFormat);
int bufferLength;
char[]? arrayBuffer = null;
Span<char> buffer = stackalloc char[bufferLength = 64];
var formatAsSpan = format.AsSpan();
try
{
if (!matrixTextFormat.TryFormat(this, buffer, out var charsWritten, formatAsSpan))
{
do
{
if (arrayBuffer != null)
ArrayPool<char>.Shared.Return(arrayBuffer);
bufferLength *= 2;
buffer = arrayBuffer = ArrayPool<char>.Shared.Rent(bufferLength);
} while (!matrixTextFormat.TryFormat(this, buffer, out charsWritten, formatAsSpan));
}
return buffer[..charsWritten].ToString();
}
finally
{
if (arrayBuffer != null)
ArrayPool<char>.Shared.Return(arrayBuffer);
}
}
public string ToString(string? format, IFormatProvider? provider) => ToString(format, new StandardFormat(provider));
public override string? ToString() => ToString("G", (IMatrixTextFormat?)null);
private string GetDebuggerDisplay() => ToString(null, MatlabFormat.Instance);
#endregion
}
using GenMath;
var matrix = new Matrix<double>(new[,] {
{ 1.0, 2.0, 3.0, 4.0 },
{ 4.0, 3.0, 2.0, 1.0 },
{ 2.0, 1.0, 3.0, 4.0 },
{ 2.0, 3.0, 1.0, 4.0 }
});
var det = matrix.Determinant(); //-60
// 1. Parsing and formatting
var parsed = Matrix<float>.Parse("""
1, 2, 3, 4
4, 3, 2, 1
2, 1, 3, 4
2, 3, 1, 4
""", CultureInfo.InvariantCulture);
var det2 = parsed.Determinant(); //-60
var a = Matrix<int>.Parse("""
1, 2, 3, 4
5, 6, 7, 8
9, 10, 11, 12
13, 14, 15, 16
""");
var matlabFormat = Matrix<float>.Parse("[1 2 3 ; 4 5 6 ; 7 8 9]", MatlabFormat.Instance);
var mathematicaFormat = Matrix<float>.Parse("{{1,2,3},{30,20,10},{2.2,1.1,3.3}}", MathematicaFormat.Instance);
//to use TryFormat you need to provide a buffer and attempt
Span<char> buffer = stackalloc char[128];
if ((a + 100).TryFormat(buffer, out var written, @"X3"))
{
var formattedHex = buffer[..written].ToString();
/*065, 066, 067, 068
069, 06A, 06B, 06C
06D, 06E, 06F, 070
071, 072, 073, 074*/
}
else { /*retry with bigger buffer or throw... */}
//or use helper method:
var formattedNum3 = a.FormatToString(@"000");
// 2. Operators
var multiplication = a * a; //[90, 100, 110, 120 ; 202, 228, 254, 280 ; 314, 356, 398, 440 ; 426, 484, 542, 600]
var sum = a.Sum<float>();//sum of elements: 136
var avg = a.Average<int>(); //8 not 8.5
var avgProper = a.Average<double>(); //8.5
var b = a + a;
var bChecked = checked(a + a); //works since resulting elements do not fall out of Int32 range
var bNum = a + 5; //adding numbers elemnt-wise is supported
//var bNum2 = 5 + a; // not implemented yet
var big = new Matrix<byte>(new byte[,]{
{ 240, 241, 242 },
{ 243, 244, 245 },
{ 246, 247, 248 }
});
var sumBig = big + big;
//var sumBigChecked = checked(big + big); //System.OverflowException
var max = a.Max(); //15
//var maxEmptyInt = new Matrix<int>(new int[0, 0]).Max(); //System.Exception: 'Matrix is empty'
var rationalMatrix = new Matrix<Rational<long>>(
(1..9).ToArray(i => new Rational<long>(
i * 10 * (i % 2 == 0 ? 1 : -1),
//--------------------------------
i * 123
)),
3);
var text = rationalMatrix.ToString();
// ⁻¹⁰⁄₁₂₃ ²⁰⁄₂₄₆ ⁻³⁰⁄₃₆₉
// ⁴⁰⁄₄₉₂ ⁻⁵⁰⁄₆₁₅ ⁶⁰⁄₇₃₈
//⁻⁷⁰⁄₈₆₁ ⁸⁰⁄₉₈₄ ⁻⁹⁰⁄₁₁₀₇
namespace GenMath;
public readonly partial record struct Rational<TNumber>(TNumber Numerator, TNumber Denominator) : IEquatable<Rational<TNumber>>, IComparisonOperators<Rational<TNumber>, Rational<TNumber>, bool>,
INumberBase<Rational<TNumber>>
where TNumber : IBinaryInteger<TNumber> //make sense to allow only integers for numerator and denominator
{
public static Rational<TNumber> Zero => new(TNumber.Zero, TNumber.One);
public static Rational<TNumber> One => new(TNumber.One, TNumber.One);
public static Rational<TNumber> AdditiveIdentity => Zero;
public static Rational<TNumber> MultiplicativeIdentity => One;
public static int Radix => TNumber.Radix;
public Rational() : this(TNumber.Zero, TNumber.One) { }
public Rational<TNumber> Simplify()
{
var (num, denom) = this;
int signNumer = TNumber.Sign(num), signDenom = TNumber.Sign(denom);
if (signDenom < 0 && (signNumer < 0 || signNumer > 0))
{
num = -num;
denom = -denom;
}
if (num == TNumber.Zero || num == TNumber.One || num == -TNumber.One) return this;
var gcd = GreatestCommonDivisor(num, denom);
return gcd > TNumber.One ? new Rational<TNumber>(num / gcd, denom / gcd) : this;
}
private static TNumber GreatestCommonDivisor(TNumber a, TNumber b) => b == TNumber.Zero ? a : GreatestCommonDivisor(b, a % b);
/*private static Rational<TNumber> NormalizeSign(Rational<TNumber> value)
{
var (num, denom) = value;
int signNumer = TNumber.Sign(num), signDenom = TNumber.Sign(denom);
return signDenom < 0 && (signNumer < 0 || signNumer > 0) ? new Rational<TNumber>(-num, -denom) : value;
}*/
public static Rational<TNumber> Abs(Rational<TNumber> value) =>
value.Numerator < TNumber.Zero && value.Denominator < TNumber.Zero ? new Rational<TNumber>(TNumber.Abs(value.Numerator), TNumber.Abs(value.Denominator)) : value;
public static bool IsCanonical(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsComplexNumber(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsEvenInteger(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsFinite(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsImaginaryNumber(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsInfinity(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsInteger(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsNaN(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsNegative(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsNegativeInfinity(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsNormal(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsOddInteger(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsPositive(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsPositiveInfinity(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsRealNumber(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsSubnormal(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static bool IsZero(Rational<TNumber> value) => TNumber.IsZero(value.Numerator);
public static Rational<TNumber> MaxMagnitude(Rational<TNumber> x, Rational<TNumber> y)
{
throw new NotImplementedException();
}
public static Rational<TNumber> MaxMagnitudeNumber(Rational<TNumber> x, Rational<TNumber> y)
{
throw new NotImplementedException();
}
public static Rational<TNumber> MinMagnitude(Rational<TNumber> x, Rational<TNumber> y)
{
throw new NotImplementedException();
}
public static Rational<TNumber> MinMagnitudeNumber(Rational<TNumber> x, Rational<TNumber> y)
{
throw new NotImplementedException();
}
public static Rational<TNumber> Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider)
{
throw new NotImplementedException();
}
public static Rational<TNumber> Parse(string s, NumberStyles style, IFormatProvider? provider)
{
throw new NotImplementedException();
}
public static Rational<TNumber> Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
{
throw new NotImplementedException();
}
public static Rational<TNumber> Parse(string s, IFormatProvider? provider)
{
throw new NotImplementedException();
}
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out Rational<TNumber> result)
{
result = default;
if (s.IsEmpty) return false;
try
{
result = Parse(s, style, provider);
return true;
}
catch (Exception)
{
return false;
}
}
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out Rational<TNumber> result)
=> TryParse(s, NumberStyles.Integer, provider, out result);
public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out Rational<TNumber> result)
{
if (s == null)
{
result = default;
return false;
}
return TryParse(s.AsSpan(), style, provider, out result);
}
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Rational<TNumber> result)
=> TryParse(s.AsSpan(), NumberStyles.Integer, provider, out result);
private static readonly string TopDigits = "⁰¹²³⁴⁵⁶⁷⁸⁹";
private static readonly string BottomDigits = "₀₁₂₃₄₅₆₇₈₉";
private static readonly char TopMinus = '⁻';
private static readonly char BottomMinus = '₋';
private static readonly char Divider = '⁄';
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{
var (num, denom) = this;
int signNumer = TNumber.Sign(num), signDenom = TNumber.Sign(denom);
if (signDenom < 0 && (signNumer < 0 || signNumer > 0))
{
num = -num;
denom = -denom;
}
provider ??= CultureInfo.InvariantCulture;
charsWritten = 0;
if (destination.Length < 3) return false;
bool tryFormatSucceeded = num.TryFormat(destination, out var tryFormatCharsWritten, format, provider);
charsWritten += tryFormatCharsWritten;
if (!tryFormatSucceeded || destination.Length < charsWritten + 2) return false;
var numBlock = destination[..charsWritten];
for (int i = 0; i < numBlock.Length; i++)
{
var c = numBlock[i];
if (!IsSimpleDigit(c) && c != '-') return false;
numBlock[i] = c == '-' ? TopMinus : TopDigits[c - '0'];
}
if (destination.Length < charsWritten + 2) return false;
destination[charsWritten++] = Divider;
tryFormatSucceeded = denom.TryFormat(destination[charsWritten..], out tryFormatCharsWritten, format, provider);
var startOfDenomBlock = charsWritten;
charsWritten += tryFormatCharsWritten;
if (!tryFormatSucceeded)
return false;
var denomBlock = destination.Slice(startOfDenomBlock, tryFormatCharsWritten);
for (int i = 0; i < denomBlock.Length; i++)
{
var c = denomBlock[i];
if (!IsSimpleDigit(c) && c != '-') return false;
denomBlock[i] = c == '-' ? BottomMinus : BottomDigits[c - '0'];
}
return true;
static bool IsSimpleDigit(char c) => (uint)c < 128 && (uint)(c - '0') <= '9' - '0';
}
public string ToString(string? format, IFormatProvider? formatProvider) => this.FormatToString(format, formatProvider);
public override string? ToString() => ToString("G", null);
static bool INumberBase<Rational<TNumber>>.TryConvertFromChecked<TOther>(TOther value, out Rational<TNumber> result)
{
throw new NotImplementedException();
}
static bool INumberBase<Rational<TNumber>>.TryConvertFromSaturating<TOther>(TOther value, out Rational<TNumber> result)
{
throw new NotImplementedException();
}
static bool INumberBase<Rational<TNumber>>.TryConvertFromTruncating<TOther>(TOther value, out Rational<TNumber> result)
{
throw new NotImplementedException();
}
static bool INumberBase<Rational<TNumber>>.TryConvertToChecked<TOther>(Rational<TNumber> value, out TOther result)
{
throw new NotImplementedException();
}
static bool INumberBase<Rational<TNumber>>.TryConvertToSaturating<TOther>(Rational<TNumber> value, out TOther result)
{
throw new NotImplementedException();
}
static bool INumberBase<Rational<TNumber>>.TryConvertToTruncating<TOther>(Rational<TNumber> value, out TOther result)
{
throw new NotImplementedException();
}
public static bool operator >(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static bool operator >=(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static bool operator <(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static bool operator <=(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator +(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator --(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator /(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator ++(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator *(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator -(Rational<TNumber> left, Rational<TNumber> right)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator -(Rational<TNumber> value)
{
throw new NotImplementedException();
}
public static Rational<TNumber> operator +(Rational<TNumber> value)
{
throw new NotImplementedException();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment