Skip to content

Instantly share code, notes, and snippets.

@lucasteles
Created January 25, 2025 23:13
Show Gist options
  • Save lucasteles/9356e0074339df510f098855d1f83025 to your computer and use it in GitHub Desktop.
Save lucasteles/9356e0074339df510f098855d1f83025 to your computer and use it in GitHub Desktop.
Fixed point number
public readonly struct Fixed :
IEquatable<Fixed>,
IComparable<Fixed>,
IFormattable, IUtf8SpanFormattable,
IComparisonOperators<Fixed, Fixed, bool>,
IAdditionOperators<Fixed, Fixed, Fixed>,
ISubtractionOperators<Fixed, Fixed, Fixed>,
IIncrementOperators<Fixed>,
IDecrementOperators<Fixed>
{
const float FloatFactor = 65536.0f;
const int DigitOffset = 16;
public static readonly Fixed One = new(1);
public static readonly Fixed Zero = new(0);
public static readonly Fixed E = new(Math.E);
public static readonly Fixed Pi = new(Math.PI);
public static readonly Fixed MaxValue = new(raw: long.MaxValue);
public static readonly Fixed MinValue = new(raw: long.MinValue);
public readonly long RawValue;
Fixed(long raw) => RawValue = raw;
public Fixed(float value)
{
var input = value * FloatFactor;
var rounded = MathF.Round(input);
RawValue = (long)rounded;
}
public Fixed(double value)
{
var input = value * FloatFactor;
var rounded = Math.Round(input);
RawValue = (long)rounded;
}
public Fixed(int value) => RawValue = value << DigitOffset;
public Fixed(short value) => RawValue = value << DigitOffset;
public static Fixed Abs(Fixed fix) => fix.RawValue < 0 ? new(-fix.RawValue) : fix;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInteger(Fixed value) => value == Truncate(value);
public static Fixed Truncate(Fixed x) => new(x.ToInt());
public static Fixed Round(Fixed x, int digits, MidpointRounding mode = MidpointRounding.ToEven) =>
new(Math.Round(x.ToDouble(), digits, mode));
public static bool IsNegative(Fixed value) => value.RawValue < 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPositive(Fixed value) => value.RawValue > 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsZero(Fixed value) => value.RawValue is 0;
public static Fixed Max(Fixed a, Fixed b) => b.RawValue > a.RawValue ? b : a;
public static Fixed Min(Fixed a, Fixed b) => b.RawValue < a.RawValue ? b : a;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Equals(Fixed left, Fixed right) => left.RawValue == right.RawValue;
public bool Equals(Fixed other) => Equals(this, other);
public override bool Equals(object? obj) => obj is Fixed other && Equals(other);
public override int GetHashCode() => RawValue.GetHashCode();
public Fixed Truncate() => Truncate(this);
public Fixed Round(int digits, MidpointRounding mode = MidpointRounding.ToEven) => Round(this, digits, mode);
public float ToFloat() => RawValue / FloatFactor;
public double ToDouble() => RawValue / (double)FloatFactor;
public long ToLong() => RawValue >> DigitOffset;
public int ToInt() => (int)ToLong();
public override string ToString() => $"F{ToDouble():F16}";
public string ToString(string? format, IFormatProvider? formatProvider) =>
ToDouble().ToString(format, formatProvider);
public int CompareTo(Fixed other) => RawValue.CompareTo(other.RawValue);
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format,
IFormatProvider? provider) => ToDouble().TryFormat(destination, out charsWritten, format, provider);
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format,
IFormatProvider? provider) => ToDouble().TryFormat(utf8Destination, out bytesWritten, format, provider);
public static Fixed operator +(Fixed left, Fixed right) => new(raw: left.RawValue + right.RawValue);
public static Fixed operator +(Fixed value) => new(raw: +value.RawValue);
public static Fixed operator ++(Fixed value) => new(raw: value.RawValue + 1);
public static Fixed operator -(Fixed left, Fixed right) => new(raw: left.RawValue - right.RawValue);
public static Fixed operator -(Fixed value) => new(raw: -value.RawValue);
public static Fixed operator --(Fixed value) => new(raw: value.RawValue - 1);
public static Fixed operator *(Fixed left, Fixed right) =>
new(raw: (left.RawValue * right.RawValue) >> DigitOffset);
public static Fixed operator /(Fixed left, Fixed right) =>
new(raw: (left.RawValue << DigitOffset) / right.RawValue);
public static Fixed operator %(Fixed left, Fixed right) => new(left.ToDouble() % right.ToDouble());
public static bool operator ==(Fixed left, Fixed right) => Equals(left, right);
public static bool operator !=(Fixed left, Fixed right) => !Equals(left, right);
public static bool operator >(Fixed left, Fixed right) => left.RawValue > right.RawValue;
public static bool operator >=(Fixed left, Fixed right) => left.RawValue >= right.RawValue;
public static bool operator <(Fixed left, Fixed right) => left.RawValue < right.RawValue;
public static bool operator <=(Fixed left, Fixed right) => left.RawValue <= right.RawValue;
public static implicit operator Fixed(int value) => new(value);
public static implicit operator Fixed(short value) => new(value);
public static implicit operator Fixed(float value) => new(value);
public static implicit operator Fixed(double value) => new(value);
public static implicit operator double(Fixed value) => value.ToDouble();
public static implicit operator float(Fixed value) => value.ToFloat();
public static explicit operator int(Fixed value) => value.ToInt();
public static explicit operator long(Fixed value) => value.ToLong();
public static bool Gt(Fixed left, Fixed right) => left > right;
public static bool Lt(Fixed left, Fixed right) => left < right;
public static bool Gte(Fixed left, Fixed right) => left >= right;
public static bool Lte(Fixed left, Fixed right) => left <= right;
}
public class FixedTests
{
[Test]
public void ShouldReturnMax() => Fixed.MaxValue.RawValue.Should().Be(long.MaxValue);
[Test]
public void ShouldReturnMin() => Fixed.MinValue.RawValue.Should().Be(long.MinValue);
[Test]
public void ShouldPi() => Fixed.Pi.ToFloat().Should().BeApproximately(MathF.PI, 0.0001f);
[Test]
public void ShouldTruncate()
{
Fixed value = 1.785;
value.Truncate().Should().Be(Fixed.One);
}
[Test]
public void ShouldRoundUp()
{
Fixed value = 1.785;
value.Round(2).Should().Be(1.79);
}
[Test]
public void ShouldRoundDown()
{
Fixed value = 1.784;
value.Round(2).Should().Be(1.78);
}
[Test]
public void ShouldSum()
{
Fixed a = 1.56;
Fixed b = 1.56;
(a + b).Should().Be(3.12);
}
[Test]
public void ShouldSubtract()
{
Fixed a = 3.12;
Fixed b = 1.56;
(a - b).Should().Be(1.56);
}
[Test]
public void ShouldSubtractNegative()
{
Fixed a = 1.56;
Fixed b = 3.12;
(a - b).Should().Be(-1.56);
}
[Test]
public void ShouldMultiply()
{
Fixed a = 1.56;
(a * 2).Should().Be(3.12);
}
[Test]
public void ShouldDivide()
{
Fixed a = 3.12;
(a / 2).Should().Be(1.56);
}
[Test]
public void ShouldCompareGreater()
{
Fixed a = 1.12;
Fixed b = 1.1;
(a > b).Should().BeTrue();
(a >= b).Should().BeTrue();
}
[Test]
public void ShouldCompareLess()
{
Fixed a = 1.1;
Fixed b = 1.12;
(a < b).Should().BeTrue();
(a <= b).Should().BeTrue();
}
[Test]
public void ShouldModInt()
{
Fixed a = 3;
Fixed b = 2;
(a % b).Should().Be(1);
}
[Test]
public void ShouldModFloat()
{
Fixed a = 3.5;
Fixed b = 2;
(a % b).Should().Be(1.5);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment