Last active
December 9, 2022 14:24
-
-
Save MichalBrylka/0b226418e297fbe6d7413e0c812c4d19 to your computer and use it in GitHub Desktop.
Generic matrix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
// ⁻¹⁰⁄₁₂₃ ²⁰⁄₂₄₆ ⁻³⁰⁄₃₆₉ | |
// ⁴⁰⁄₄₉₂ ⁻⁵⁰⁄₆₁₅ ⁶⁰⁄₇₃₈ | |
//⁻⁷⁰⁄₈₆₁ ⁸⁰⁄₉₈₄ ⁻⁹⁰⁄₁₁₀₇ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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