この記事は Qiita Advent Calendar 2022 に参加しています。
実装だけでなく、各インターフェースの定義もある程度記載しているので、各メンバがどういう役割を持っているのかを調べるのにも役立ちます...たぶん。
はじめに
2022年11月の初めについに、.NET 7
が正式にリリースされました。
また、これと同時にリリースされた C#11
でコードを書くことによって、インターフェースに静的なメンバを定義できるようになりました。
例えば、以下のインターフェースは、T
型の最大値と最小値を表す静的なプロパティを定義しています。
public interface IMinMaxValue<T>
where T : IMinMaxValue<T>
{
public static abstract T MaxValue { get; }
public static abstract T MinValue { get; }
}
この新機能によって、インターフェースで各種演算子も定義できるようになりました。
そこで Generic Math と呼ばれる、数値型の演算をジェネリクスを利用して記述できるようになる機能が導入されました。
詳しくは以下の記事をご覧ください。
今回は、それを利用して分数型を作成していきます。
簡単に仕様を決める
分数型 Frac<T>
を以下のように定義します。
public struct Frac<T> : INumber<Frac<T>>, ISignedNumber<Frac<T>>, IMinMaxValue<Frac<T>>
where T : notnull, INumber<T>, IBinaryInteger<T>, ISignedNumber<T>, IMinMaxValue<T>
-
Frac<T>
を、T
型の値を分母と分子にそれぞれ持つ分数型とします。 - 分母は必ず正の値をとるようにして、分数の正負は分子の正負と一致するようにします。
-
T
型は以下の条件を満たす必要があります。- 符号付き整数である
- 最大値、最小値を持つ
-
Frac<T>
自身もINumber<Frac<T>>
を実装し、Generic Math の機能で利用できるようにします。 - オーバーフローを出来るだけ防ぐため、各演算のあとに必ず約分をするようにします。
実装するインターフェースについて
今回は INumber<TSelf>
を実装していますが、INumber<TSelf>
は IComparable
, IComparable<TSelf>
, IEquatable<TSelf>
, IParsable<TSelf>
, ISpanParsable<TSelf>
, IAdditionOperators<TSelf, TSelf, TSelf>
, IAdditiveIdentity<TSelf, TSelf>,
IComparisonOperators<TSelf, TSelf, bool>
, IDecrementOperators<TSelf>
, IDivisionOperators<TSelf, TSelf, TSelf>
, IEqualityOperators<TSelf, TSelf, bool>
, IIncrementOperators<TSelf>
, IModulusOperators<TSelf, TSelf, TSelf>
, IMultiplicativeIdentity<TSelf, TSelf>
, IMultiplyOperators<TSelf, TSelf, TSelf>
, INumberBase<TSelf>
, ISubtractionOperators<TSelf, TSelf, TSelf>
, IUnaryNegationOperators<TSelf, TSelf>
, IUnaryPlusOperators<TSelf, TSelf>
インターフェースを継承しているため、実装するメソッドの量が非常に多くて大変です。(笑)
そのため、他人に公開するようなものでなければ、必要最低限のインターフェースを実装するだけでも大丈夫だと思います。
例えば、四則演算を実装するだけなら、IAdditionOperators<TSelf, TSelf, TSelf>
, IDivisionOperators<TSelf, TSelf, TSelf>
, IMultiplyOperators<TSelf, TSelf, TSelf>
, ISubtractionOperators<TSelf, TSelf, TSelf>
の4つを実装するだけでよいです。
ただし、上記のように実装された型は、INumber<T>
を制約に含む関数に利用できません。
かわりに、関数内で呼び出されているメンバに応じて制約の範囲を小さくする必要があります。
void DoSomething<T>(T value) where T : INumber<T> // これには利用できない
void DoSomething<T>(T value) where T : IAdditionOperators<T, T, T> // これには利用できる
実装
Frac<T>
は INumber<Frac<T>>
を実装するので、ここから長い長い実装が始まります。
あまりにも長いので、備忘録的な感じで「ここのインターフェースどうやって実装するんだっけ」と思ったときに参照するくらいの使い道しかない記事です。(笑)
今回は整数型ではないためビット演算子の実装はしていません。(自作する型でこれを実装する機会は少なそうです。)
以下で挙げるコードを全部まとめると 800行 くらいになります。本当に長いです。
目次
機能別に分けています。
- 備考
- 下準備
- 算術演算子
- 単項算術演算子
- 比較演算子
- 比較関数
- 定数
-
型変換
INumberBase<TSelf>
-
フォーマッタ
-
文字列に出力
IFormattable
,Object
-
Span<char>
に出力ISpanFormattable
-
文字列に出力
-
パーサー
-
文字列から初期化
IParsable<TSelf>
,INumberBase<TSelf>
-
Span<char>
から初期化ISpanParsable<TSelf>
,INumberBase<TSelf>
-
文字列から初期化
-
数学関数
-
絶対値
INumberBase<TSelf>
-
大小比較による値の選択
INumberBase<TSelf>
,INumber<TSelf>
-
値を範囲内に収める
INumber<TSelf>
-
符号を取得
INumber<TSelf>
-
符号を揃える
INumber<TSelf>
-
絶対値
-
値の状態を検証する関数
INumberBase<TSelf>
備考
checkedステートメントについて
checked {}
または checked()
で囲まれた中の演算は、オーバーフローが逐一チェックされます。
例えば、以下で定義されている DoSomething1
メソッドと DoSomething2
メソッドでは、掛け算によって値がオーバーフローした場合に例外が投げられるようになります。
public int DoSomething1(int arg) {
checked {
return arg + int.MaxValue;
}
}
public int DoSomething2(int arg) {
return checked(arg + int.MaxValue);
}
算術演算子の項で記載しているように checked
付きの演算子の実装は、基本的には checked
が付いていないものの実装を checked {}
で囲むだけでよいと思われます。
下準備
プロパティの宣言
分母と分子のプロパティを宣言します。
分母のデフォルト値は 1
、分子のデフォルト値は 0
とします。
また、分母を変更することはきっと無いだろうということで、分母のセッターは実装しないことにしました。
後で役に立つので、分子の値が 0
になった際に分母の値が 1
になるように実装しておきます。
public partial struct Frac<T>
where T : INumber<T>
{
private T _denom = T.One;
private T _numer = T.Zero;
public T Denominator // 分母
=> _denom;
public T Numerator // 分子
{
get => _numer;
set {
_numer = value;
if (value == T.Zero)
_denom = T.One;
else
Reduce();
}
}
}
今回扱っている分数型では、分母と分子は任意の型になっているので、0
と 1
はそれぞれ T.Zero
, T.One
で表します。
最大公約数を求める関数
分数を約分する際に使います。
実装には、ユークリッドの互除法を用います。
public partial struct Frac<T>
where T : INumber<T>
{
private static T Gcd(T a, T b) {
// a <= b を保障
if (b > a) (a, b) = (b, a);
while (a > T.Zero)
(a, b) = (b % a, a);
return b;
}
}
約分をする関数
分母と分子を、その最大公約数で割ります。
public partial struct Frac<T>
where T : INumber<T>
{
public void Reduce() {
if (this._numer == T.Zero) return;
T gcd = Gcd(this._numer, this._denom);
if (gcd != T.One) {
this._denom /= gcd;
this._numer /= gcd;
}
}
}
コンストラクタ
分母と分子の値をそれぞれ受け取り、代入して初期化します。
public partial struct Frac<T>
{
public Frac(T numerator, T denominator) {
if (denominator == T.Zero)
throw new ArgumentException("分母は0より大きい必要があります。", nameof(denominator));
_denom = denominator;
_numer = numerator;
Reduce();
}
public Frac(T numerator) {
_numer = numerator;
}
}
ハッシュ値
分母と分子のそれぞれのハッシュ値の排他的論理和を取ったものを、分数型のハッシュ値とします。
下準備 での実装により、つねに分母と分子が互いに素であるので、同じ値を表すなら必ず同じハッシュ値になります。
public partial struct Frac<T>
{
public override int GetHashCode() {
return _denom.GetHashCode() ^ _numer.GetHashCode();
}
}
算術演算子
インターフェースの定義に従い、2つの値をとる演算子の引数の名称は left
, right
としています。
このとき、それぞれのメソッドは left (演算子) right
の結果を返します。
加算
IAdditionOperator<TSelf, TSelf, TSelf>
を実装します。
public partial struct Frac<T> : IAdditionOperators<Frac<T>, Frac<T>, Frac<T>>
{
// left + right
public static Frac<T> operator +(Frac<T> left, Frac<T> right) {
return new Frac<T>(
(left._numer * right._denom) + (right._numer * left._denom),
left._denom * right._denom
);
}
// checked(left + right)
public static Frac<T> operator checked +(Frac<T> left, Frac<T> right) {
checked {
return new Frac<T>(
(left._numer * right._denom) + (right._numer * left._denom),
left._denom * right._denom
);
}
}
}
減算
ISubtractionOperators<TSelf, TSelf, TSelf>
を実装します。
public partial struct Frac<T> : ISubtractionOperators<Frac<T>, Frac<T>, Frac<T>>
{
// left - right
public static Frac<T> operator -(Frac<T> left, Frac<T> right) {
return new Frac<T>(
(left._numer * right._denom) - (right._numer * left._denom),
left._denom * right._denom
);
}
// checked(left - right)
public static Frac<T> operator checked -(Frac<T> left, Frac<T> right) {
checked {
return new Frac<T>(
(left._numer * right._denom) - (right._numer * left._denom),
left._denom * right._denom
);
}
}
}
乗算
IMultipliyOperators<TSelf, TSelf, TSelf>
を実装します。
ここからが分数の本領発揮ですね。
public partial struct Frac<T> : IMultiplyOperators<Frac<T>, Frac<T>, Frac<T>>
{
// left * right
public static Frac<T> operator *(Frac<T> left, Frac<T> right) {
return new Frac<T>(
left._numer * right._numer,
left._denom * right._denom
);
}
// checked(left * right)
public static Frac<T> operator checked *(Frac<T> left, Frac<T> right) {
checked {
return new Frac<T>(
left._numer * right._numer,
left._denom * right._denom
);
}
}
}
除算
IDivisionOperators<TSelf, TSelf, TSelf>
を実装します。
乗算と同じく簡単に実装できます。
public partial struct Frac<T> : IDivisionOperators<Frac<T>, Frac<T>, Frac<T>>
{
// left / right
public static Frac<T> operator /(Frac<T> left, Frac<T> right) {
return new Frac<T>(
left._numer * right._denom,
left._denom * right._numer
);
}
// checked(left / right)
public static Frac<T> operator checked /(Frac<T> left, Frac<T> right) {
checked {
return new Frac<T>(
left._numer * right._denom,
left._denom * right._numer
);
}
}
}
剰余演算
IModulusOperators<TSelf, TSelf, TSelf>
を実装します。
方針はいろいろあると思いますが、分母を揃えて分子だけで剰余を求めることにします。
ここでは、 $p\div{q}$ の計算結果 $\dfrac{p}{q}$ について、 $\dfrac{p}{q}=k+\dfrac{r}{s}$ (ただし $k\in{\mathbb{Z}},\ 0\leq{\dfrac{r}{s}}\lt{1}$ ) と表されるとき、 $q\cdot\dfrac{r}{s}$ を余りとして定義します。
剰余演算には checked
付き演算子は利用できません。そのため、IModulusOperators<TSelf, TSelf, TSelf>
を実装する際は checked
の付いていない %
演算子の実装だけでよいです。
public partial struct Frac<T> : IModulusOperators<Frac<T>, Frac<T>, Frac<T>>
{
// left % right
public static Frac<T> operator %(Frac<T> left, Frac<T> right) {
return new Frac<T>(
(left._numer * right._denom) % (left._denom * right._numer),
left._denom * right._denom
);
}
}
上記の実装の説明 (クリック/タップで開閉)
上記の定義において、 $p=\dfrac{p_{分子}}{p_{分母}},\quad q=\dfrac{q_{分子}}{q_{分母}}$ とします。
この時、 $p\div{q}=\dfrac{\color{red}{p_{分子}\cdot{q_{分母}}}}{\color{blue}{p_{分母}\cdot{q_{分子}}}}$ であるので、 $\color{red}{p_{分子}\cdot{q_{分母}}}$ を $\color{blue}{p_{分母}\cdot{q_{分子}}}$ で割ったあまりを $m(\geq{0})$ とすると、
$p\div{q}$ の余りは $\require{cancel} q\cdot{\dfrac{m}{p_{分母}\cdot{q_{分子}}}} = \dfrac{\cancel{q_{分子}}}{q_{分母}}\cdot{\dfrac{m}{p_{分母}\cdot{\cancel{q_{分子}}}}} = \dfrac{m}{p_{分母}\cdot{q_{分母}}}$ と表されます。
インクリメント
IIncrementOperators<TSelf>
を実装します。
public partial struct Frac<T> : IIncrementOperators<Frac<T>> {
// value++
public static Frac<T> operator ++(Frac<T> value) {
value._numer += value._denom;
return value;
}
}
デクリメント
IDecrementOperators<TSelf>
を実装します。
public partial struct Frac<T> : IDecrementOperators<Frac<T>> {
// value--
public static Frac<T> operator -- (Frac<T> value) {
value._numer -= value._denom;
return value;
}
}
単項算術演算子
単項プラス
IUnaryPlusOperators<TSelf, TSelf>
を実装します。
Frac<T> foo = new(1, 2);
としたときの +foo
の値を表すだけなので、foo
の値のままでよいです。
public partial struct Frac<T> : IUnaryPlusOperators<Frac<T>, Frac<T>>
{
// +value
public static Frac<T> operator + (Frac<T> value) {
return value;
}
}
単項マイナス
IUnaryNegationOperators<TSelf, TSelf>
を実装します。
単項プラス に記載した foo
の定義における、-foo
の値を表すので、分子の正負を逆にすればよいです。
public partial struct Frac<T> : IUnaryNegationOperators<Frac<T>, Frac<T>>
{
// -value
public static Frac<T> operator - (Frac<T> value) {
value._numer = -value._numer;
return value;
}
}
比較演算子
等価・不等価
IEqualityOperators<TSelf, TSelf, bool>
を実装します。
下準備 での実装により、つねに分母と分子が互いに素であることと、分子の値が 0
であれば分母の値が同じになることが保障されているので、楽に実装できます。
public partial struct Frac<T> : IEqualityOperators<Frac<T>, Frac<T>, bool>
{
// left == right
public static bool operator ==(Frac<T> left, Frac<T> right) {
return left._numer == right._numer && left._denom == right._denom;
}
// left != right
public static bool operator !=(Frac<T> left, Frac<T> right) {
return !(left == right);
}
}
大小比較
IComparisonOperators<TSelf, TSelf, bool>
を実装します。
先ほどの 等価・不等価 の際と同様の理由によって、場合分けをすることなく実装できます。
public partial struct Frac<T> : IComparisonOperators<Frac<T>, Frac<T>, bool>
{
// left > right
public static bool operator >(Frac<T> left, Frac<T> right) {
return left._numer * right._denom > right._numer * left._denom;
}
// left >= right
public static bool operator >=(Frac<T> left, Frac<T> right) {
return !(left < right);
}
// left < right
public static bool operator <(Frac<T> left, Frac<T> right) {
return left._numer * right._denom < right._numer * left._denom;
}
// left <= right
public static bool operator <=(Frac<T> left, Frac<T> right) {
return !(left > right);
}
}
比較関数
等価・不等価
IEquatable<TSelf>
を実装します。
先ほど定義した演算子を使うのが手っ取り早いでしょう。
public partial struct Frac<T> : IEquatable<Frac<T>>
{
public bool Equals(Frac<T> other) {
return this == other;
}
}
ついでに、object.Equals(object?)
もオーバーライドしておきます。
public partial struct Frac<T>
{
public override bool Equals(object? other) {
return (other is Frac<T> other_f) && this == other_f;
}
}
大小比較
IComparable<TSelf>
と IComparable
を実装します。
int compareResult = a.CompareTo(b);
としたとき、compareResult
は a - b
と同じ符号の値 になります。
引き算の結果を返すだけだと int
型の範囲を超える可能性があるので、条件分岐をして戻り値を決定しています。
public partial struct Frac<T> : IComparable<Frac<T>>, IComparable
{
public int CompareTo(Frac<T> other) {
if (this > other)
return 1;
else if (this == other)
return 0;
else
return -1;
}
public int CompareTo(object? other) {
if (other is null)
return 1;
else if (other is Frac<T> other_f)
return CompareTo(other_f);
else
throw new ArgumentException("Object is not an instance of Frac<T>", nameof(other), null);
}
}
定数
0と1を表す値
INumberBase<TSelf>.Zero
と INumberBase<TSelf>.One
を実装します。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static Frac<T> One => new(T.One, T.One);
public static Frac<T> Zero => new(T.Zero, T.One);
}
マイナス1を表す値
ISignedNumber<TSelf>.NegativeOne
を実装します。
public partial struct Frac<T> : ISignedNumber<Frac<T>>
{
public static Frac<T> NegativeOne => new(T.NegativeOne, T.One);
}
基数
INumberBase<TSelf>.Radix
を実装します。
Microsoftの公式リファレンスによると、「型の基数」を表すらしいのですが、分数型で基数と言われても分かんないので、 T
型の基数を返すようにして、プロパティは明示的実装で隠すことにします。
int
型などでもこのプロパティは明示的実装により隠されているので、「きっと使われないだろう」とか「よくわからん」とかいうメンバは、明示的実装をしておけばよいと思われます。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
static int INumberBase<Frac<T>>.Radix => T.Radix;
}
最大・最小値
IMinMaxValue<TSelf>
を実装します。
public partial struct Frac<T> : IMinMaxValue<Frac<T>>
{
public static Frac<T> MaxValue => new(T.MaxValue, T.One);
public static Frac<T> MinValue => new(T.MinValue, T.One);
}
単位元
加法単位元
IAdditiveIdentity<TSelf, TSelf>
を実装します。
加法単位元 とは、任意の $x$ に対して $x+e=x=e+x$ が成り立つ $e$ のことです。
ここでは $e=0$ となります。
public partial struct Frac<T> : IAdditiveIdentity<Frac<T>, Frac<T>>
{
public static Frac<T> AdditiveIdentity => Frac<T>.Zero;
}
乗法単位元
IMultiplicativeIdentity<TSelf, TSelf>
を実装します。
乗法単位元 とは、任意の $x$ に対して $x\times{e}=x=e\times{x}$ が成り立つ $e$ のことです。
ここでは $e=1$ となります。
public partial struct Frac<T> : IMultiplicativeIdentity<Frac<T>, Frac<T>>
{
public static Frac<T> MultiplicativeIdentity => Frac<T>.One;
}
型変換
INumberBase<TSelf>
を実装します。
変換するメソッドは以下の3種類があります。 (すべて静的メソッドです。)
-
TSelf
型からTOther
型への変換を試みるpublic static virtual bool TryConvertToXXXX<TOther>(TSelf value, out TOther result) where TOther : INumberBase<TOther>;
-
TOther
型からTSelf
型への変換を試みるpublic static virtual bool TryConvertToXXXX<TOther>(TOther value, out TSelf result) where TOther : INumberBase<TOther>;
-
TOther
型からTSelf
型に変換する (例外送出あり)public static virtual TSelf CreateFromXXXX<TOther>(TOther value) where TOther : INumberBase<TOther>;
オーバーフローや、型変換が不可能であった際に例外が投げられます。
なお、今回扱っている分数型でのこのメソッドの実装は、
double
型に変換してから分母を分子で割り、その値を特定の型に変換する方式をとっています。
上記のメソッド名の末尾の XXXX
の部分には、以下で示される、オーバーフローに対する対応方法を表す3種類の文字列が入ります。
オーバーフローのチェックをして変換
XXXX
には Checked
が入ります。
オーバーフローした場合、Try
で始まる関数は false
を返し、それ以外の関数は例外を投げます。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static Frac<T> CreateChecked<TOther>(TOther other) where TOther : INumberBase<TOther> {
if (TOther.TryConvertToChecked<T>(other, out var result))
return new(result);
else
throw new NotSupportedException();
}
public static bool TryConvertFromChecked<TOther>(TOther value, out Frac<T> result) where TOther : INumberBase<TOther> {
if (TOther.TryConvertToChecked<T>(value, out var _result)) {
result = new(_result);
return true;
}
else {
result = default;
return false;
}
}
public static bool TryConvertToChecked<TOther>(Frac<T> value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther> {
if (T.TryConvertToSaturating(value._numer, out double numer) && T.TryConvertToSaturating(value._denom, out double denom)) {
return TOther.TryConvertFromChecked(numer / denom, out result);
}
else {
result = default;
return false;
}
}
}
型の範囲に収めて変換
XXXX
には Saturating
が入ります。
オーバーフローした場合、変換後の型の最大値と最小値のうち、変換前の値に近い (絶対値が小さい) 値が設定されます。
たとえば、 long
型の最大値である $2^{64}-1$ を、 int.CreateSaturating<long>(long)
で int
型に変換すると、その値は、 int
型の最大値である $2^{32}-1$ になります。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static Frac<T> CreateSaturating<TOther>(TOther other) where TOther : INumberBase<TOther> {
if (TOther.TryConvertToSaturating<T>(other, out var result))
return new(result);
else
throw new NotSupportedException();
}
public static bool TryConvertFromSaturating<TOther>(TOther value, out Frac<T> result) where TOther : INumberBase<TOther> {
if (TOther.TryConvertToSaturating<T>(value, out var _result)) {
result = new(_result);
return true;
}
else {
result = default;
return false;
}
}
public static bool TryConvertToSaturating<TOther>(Frac<T> value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther> {
if (T.TryConvertToSaturating(value._numer, out double numer) && T.TryConvertToSaturating(value._denom, out double denom)) {
return TOther.TryConvertFromSaturating(numer / denom, out result);
}
else {
result = default;
return false;
}
}
}
切り捨てて変換
XXXX
には Truncating
が入ります。
変換前の型が整数型の場合は「切り捨て」の名の通り、範囲から溢れた部分のビットは無視されます。
変換前の型が浮動小数点型の場合は、小数部分が切り捨てられます。
たとえば、 long
型の最大値である $2^{64}-1$ を、 int.CreateTruncating<long>(long)
で int
型に変換すると、符号の情報がある方ではない方の末端 の32ビット分のみが残って、全てのビットが立っているので $-1$ になります。
浮動小数点型からの変換で、かつその値が変換後の型の範囲を超えていた場合は、型の範囲に収めて変換 の時と同じような動作をするようです(?)
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static Frac<T> CreateTruncating<TOther>(TOther other) where TOther : INumberBase<TOther> {
if (TOther.TryConvertToTruncating<T>(other, out var result))
return new(result);
else
throw new NotSupportedException();
}
public static bool TryConvertFromTruncating<TOther>(TOther value, out Frac<T> result) where TOther : INumberBase<TOther> {
if (TOther.TryConvertToTruncating<T>(value, out var _result)) {
result = new(_result);
return true;
}
else {
result = default;
return false;
}
}
public static bool TryConvertToTruncating<TOther>(Frac<T> value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther> {
if (T.TryConvertToSaturating(value._numer, out double numer) && T.TryConvertToSaturating(value._denom, out double denom)) {
return TOther.TryConvertFromTruncating(numer / denom, out result);
}
else {
result = default;
return false;
}
}
}
フォーマッタ
今回扱っている分数型では、文字列表現を 分子/分母
(ただし、分母が 1
の時は分子のみの文字列表現) の形式にしています。
文字列に出力
IFormattable
を実装します。
フォーマット指定子は、何も指定されていない場合のみ 分子/分母
の形式で出力し、それ以外の場合は double
型の実数値に変換して、それを文字列として出力することとします。
フォーマット指定子については、Microsoftの 公式リファレンス をご覧ください。
ついでに object.ToString()
もオーバーライドしておきましょう。
public partial struct Frac<T> : IFormattable
{
public override string ToString() {
return (_denom == T.One) ? _numer.ToString() : $"{_numer}/{_denom}";
}
public string ToString(string? format, IFormatProvider? formatProvider) {
return format switch {
null => ToString(),
_ => (double.CreateSaturating(_numer) / double.CreateSaturating(_denom)).ToString(format, formatProvider)
};
}
}
Span<char>
に出力
ISpanFormattable
を実装します。
IFormattable.ToString()
の出力先を Span<char>
に変えるだけなので、やっていることは同じです。
public partial struct Frac<T> : ISpanFormattable
{
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) {
if (format.IsEmpty) {
string result = ToString();
if (result.TryCopyTo(destination)) {
charsWritten = result.Length;
return true;
}
else {
charsWritten = 0;
return false;
}
}
else {
return (double.CreateSaturating(_numer)/double.CreateSaturating(_denom)).TryFormat(destination, out charsWritten, format, provider);
}
}
}
パーサー
文字列から初期化
IParsable<TSelf>
の全メソッドと INumberBase<TSelf>
の一部メソッドを実装します。
以下の実装では全てのメソッドで、受け取った文字列を ReadOnlySpan<char>
に変換して、それ用のメソッドを呼び出しているので、詳細な説明は Span<char>
から初期化 に記載します。
public partial struct Frac<T> : IParsable<Frac<T>>, ISpanParsable<Frac<T>>, INumberBase<Frac<T>>
{
public static Frac<T> Parse(string s, IFormatProvider? provider) {
return Parse(s.AsSpan(), provider);
}
public static Frac<T> Parse(string s, NumberStyles style, IFormatProvider? provider) {
return Parse(s.AsSpan(), style, provider);
}
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Frac<T> result) {
if (s is null) {
result = default;
return false;
}
else {
return TryParse(s.AsSpan(), provider, out result);
}
}
public static bool TryParse(string? s, NumberStyles style, IFormatProvider? provider, [MaybeNullWhen(false)] out Frac<T> result) {
return TryParse(s.AsSpan(), style, provider, out result);
}
}
Span<char>
から初期化
今回扱っている分数型では、文字列表記を 分子/分母
としていたので、それに従って解析、初期化するように実装しています。
また、上記の表記が見つからなかった場合は、分母が 1
であると見なして T
型として解釈します。
下記で実装しているメソッドの引数の型について、以下で簡単に説明します。
-
IFormatProvider
-- 表記法にかかわるカルチャ情報が含まれています。 -
NumberStyles
-- 数値の表現の制約などの情報です。
分子/分母
の表記においては、それぞれを独立した数値として扱うことができるので、 T
型のパーサーに上記の引数をそのまま渡しています。
public partial struct Frac<T> : IParsable<Frac<T>>, ISpanParsable<Frac<T>>, INumberBase<Frac<T>>
{
public static Frac<T> Parse(ReadOnlySpan<char> s, IFormatProvider? provider) {
int slashIdx = s.IndexOf('/');
return slashIdx switch {
-1 => new(T.Parse(s, provider)),
_ => new(T.Parse(s[..slashIdx], provider), T.Parse(s[(slashIdx + 1)..], provider))
};
}
public static Frac<T> Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider) {
int slashIdx = s.IndexOf('/');
return slashIdx switch {
-1 => new(T.Parse(s, style, provider)),
_ => new(T.Parse(s[..slashIdx], style, provider), T.Parse(s[(slashIdx + 1)..], style, provider))
};
}
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out Frac<T> result) {
int slashIdx = s.IndexOf('/');
if (slashIdx == -1) {
if (T.TryParse(s, provider, out var v)) {
result = new(v);
return true;
}
}
else if (T.TryParse(s[..slashIdx], provider, out var numer) && T.TryParse(s[(slashIdx + 1)..], provider, out var denom)) {
result = new(numer, denom);
return true;
}
result = default;
return false;
}
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, [MaybeNullWhen(false)] out Frac<T> result) {
int slashIdx = s.IndexOf('/');
if (slashIdx == -1) {
if (T.TryParse(s, style, provider, out var v)) {
result = new(v);
return true;
}
}
else if (T.TryParse(s[..slashIdx], style, provider, out var numer) && T.TryParse(s[(slashIdx + 1)..], style, provider, out var denom)) {
result = new(numer, denom);
return true;
}
result = default;
return false;
}
}
数学関数
絶対値
INumberBase<TSelf>.Abs(TSelf)
を実装します。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static Frac<T> Abs(Frac<T> value) {
value._numer = T.Abs(value._numer);
return value;
}
}
大小比較による値の選択
INumberBase<TSelf>
の一部メソッドを実装します。
MaxMagnitude(TSelf, TSelf)
と MinMagnitude(TSelf, TSelf)
はそれぞれ大きい/小さいほうの値を返します。
また、 MaxMagnitudeNumber(TSelf, TSelf)
と MinMagnitudeNumber(TSelf, TSelf)
は、 NaN
でない方を優先的に選択する以外の点は、上記のメソッドと同じ挙動をします。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static Frac<T> MaxMagnitude(Frac<T> x, Frac<T> y) {
return (x > y) ? x : y;
}
public static Frac<T> MaxMagnitudeNumber(Frac<T> x, Frac<T> y) {
return MaxMagnitude(x, y);
}
public static Frac<T> MinMagnitude(Frac<T> x, Frac<T> y) {
return (x < y) ? x : y;
}
public static Frac<T> MinMagnitudeNumber(Frac<T> x, Frac<T> y) {
return MinMagnitude(x, y);
}
}
これに加えて、 INumber<TSelf>
の一部メソッドを実装します。
Max(TSelf,TSelf)
と Min(TSelf, TSelf)
はそれぞれ MaxMagnitude(TSelf, TSelf)
, MinMagnitude(TSelf, TSelf)
に対応します。
また、 MaxNumber(TSelf, TSelf)
とMinNumber(TSelf, TSelf)
はそれぞれ MaxMagnitudeNumber(TSelf, TSelf)
, MinMagnitudeNumber(TSelf, TSelf)
に対応します。
public partial struct Frac<T> : INumber<Frac<T>>
{
public static Frac<T> Max(Frac<T> x, Frac<T> y) {
return MaxMagnitude(x, y);
}
public static Frac<T> MaxNumber(Frac<T> x, Frac<T> y) {
return MaxMagnitude(x, y);
}
public static Frac<T> Min(Frac<T> x, Frac<T> y) {
return MinMagnitude(x, y);
}
public static Frac<T> MinNumber(Frac<T> x, Frac<T> y) {
return MinMagnitude(x, y);
}
}
値を範囲内に収める
INumber<TSelf>.Clamp(TSelf, TSelf, TSelf)
を実装します。
value
が [min, max]
の範囲に存在すればその値を、範囲外にあれば min
と max
のうち value
に近い方の値を返します。
public partial struct Frac<T> : INumber<Frac<T>>
{
public static Frac<T> Clamp(Frac<T> value, Frac<T> min, Frac<T> max) {
if (min > max)
throw new ArgumentException("最小値は最大値以下でなければいけません", nameof(min));
return MaxMagnitude(MinMagnitude(value, max), min);
}
}
符号を取得
INumber<TSelf>.Sign(TSelf)
を実装します。
value
と同じ符号の数値を返します。 (公式リファレンス によると、戻り値には -1, 0, 1
を使用するのが良いそうです。)
public partial struct Frac<T> : INumber<Frac<T>>
{
public static int Sign(Frac<T> value) {
return T.Sign(value._numer);
}
}
符号を揃える
INumber<TSelf>.CopySign(TSelf, TSelf)
を実装します。
value
の符号を sign
のものと揃えます。
public partial struct Frac<T> : INumber<Frac<T>>
{
public static Frac<T> CopySign(Frac<T> value, Frac<T> sign) {
value._numer = T.CopySign(value._numer, sign._numer);
return value;
}
}
値の状態を検証する関数
INumberBase<TSelf>
の一部メソッドを実装します。
戻り値が常に同じになるようなメソッドは明示的実装で隠す方がよいと思われます。
値が0かどうか
INumberBase<TSelf>.IsZero(TSelf)
を実装します。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static bool IsZero(Frac<T> value) {
return T.IsZero(value._numer);
}
}
正数・負数の判別
INumberBase<TSelf>.IsPositive(TSelf)
と INumberBase<TSelf>.IsNegative(TSelf)
を実装します。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static bool IsNegative(Frac<T> value) {
return T.IsNegative(value._numer);
}
public static bool IsPositive(Frac<T> value) {
return T.IsPositive(value._numer);
}
}
整数かどうか
INumberBase<TSelf>.IsInteger(TSelf)
を実装します。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static bool IsInteger(Frac<T> value) {
return value._denom == T.One;
}
}
奇数・偶数かどうか
INumberBase<TSelf>.IsEvenInteger(TSelf)
と INumberBase<TSelf>.IsOddInteger(TSelf)
を実装します。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
public static bool IsEvenInteger(Frac<T> value) {
return value._denom == T.One && T.IsEvenInteger(value._numer);
}
public static bool IsOddInteger(Frac<T> value) {
return value._denom == T.One && T.IsOddInteger(value._numer);
}
}
有限値・無限大の判別
INumberBase<TSelf>.IsFinite(TSelf)
と INumberBase<TSelf>.IsInfinity(TSelf)
を実装します。
今回扱っている分数型は常に有限値を取るので、どちらのメソッドも明示的実装をします。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
static bool INumberBase<Frac<T>>. IsFinite(Frac<T> value) {
return true;
}
static bool INumberBase<Frac<T>>. IsInfinity(Frac<T> value) {
return false;
}
}
正の無限大・負の無限大かどうか
INumberBase<TSelf>.IsPositiveInfinity(TSelf)
と INumberBase<TSelf>.IsNegativeInfinity(TSelf)
を実装します。
有限値・無限大の判別 と同様の理由から、どちらのメソッドも明示的実装をします。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
static bool INumberBase<Frac<T>>. IsPositiveInfinity(Frac<T> value) {
return false;
}
static bool INumberBase<Frac<T>>. IsNegativeInfinity(Frac<T> value) {
return false;
}
}
非数 (NaN) かどうか
INumberBase<TSelf>.IsNaN(TSelf)
を実装します。
今回扱っている分数型は非数になることは無いので、明示的実装をします。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
static bool INumberBase<Frac<T>>. IsNaN(Frac<T> value) {
return false;
}
}
実数・純虚数・複素数かどうか
INumberBase<TSelf>.IsRealNumber(TSelf)
, INumberBase<TSelf>.IsImaginaryNumber(TSelf)
, INumberBase<TSelf>.IsComplexNumber(TSelf)
を実装します。
今回扱っている分数型は常に実数なので、これら全てのメソッドで明示的実装をします。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
static bool INumberBase<Frac<T>>. IsRealNumber(Frac<T> value) {
return true;
}
static bool INumberBase<Frac<T>>. IsImaginaryNumber(Frac<T> value) {
return false;
}
static bool INumberBase<Frac<T>>.IsComplexNumber(Frac<T> value) {
return false;
}
}
標準形かどうか
INumberBase<TSelf>.IsCanonical(TSelf)
を実装します。
「標準形 (canonical representation)」について調べてみたのですが、何のことやらさっぱり分かりませんでした......
Wikipedia(英)によると、
In mathematics and computer science, a canonical, normal, or standard form of a mathematical object is a standard way of presenting that object as a mathematical expression. Often, it is one which provides the simplest representation of an object and which allows it to be identified in a unique way.
(DeepL訳)
数学およびコンピュータサイエンスにおいて、数学的対象の正準形式、正規形、または標準形とは、その対象を数式として表現するための標準的な方法である。多くの場合、オブジェクトを最も単純に表現し、一意に識別できるようにするものである。
とあるので、今回扱っている分数型では「既約分数であるかどうか」を言っているのでしょうか......?
そうだと信じて、明示的実装をします。(笑)
public partial struct Frac<T> : INumberBase<Frac<T>>
{
static bool INumberBase<Frac<T>>.IsCanonical(Frac<T> value) {
return true;
}
}
正規化数・非正規化数かどうか
INumberBase<TSelf>.IsNormal(TSelf)
と INumberBase<TSelf>.IsSubnormal(TSelf)
を実装します。
正規化数と非正規化数については、以下の記事を参照ください。
今回扱っている分数型では常に正規化数なので、どちらのメソッドも明示的実装をします。
public partial struct Frac<T> : INumberBase<Frac<T>>
{
static bool INumberBase<Frac<T>>. IsNormal(Frac<T> value) {
return true;
}
static bool INumberBase<Frac<T>>. IsSubnormal(Frac<T> value) {
return false;
}
}
おわりに
以上で最低限必要な実装は全て行いました。
とても長い記事にもかかわらず、ここまで読んでくださりありがとうございます。
正直なところ、ここまでしっかりと実装する場面は、外部に頒布する時くらいしか無いと思います。 (自分が使うだけなら、絶対使わないようなメソッドは NotSupportedException
を投げるようにしても良いと思います。)
おまけ
もう少し高速に動作する分数型
今回扱った分数型では、演算のたびに必ず約分をするようになっていたので、これを利用者の任意で行うようにすることで、もう少し高速に動作する分数型を作れます。
T
型との演算に対応する
T
型の整数を掛けるときに、わざわざ Frac<T>
を作成するのも面倒なので、 T
型を直接利用できる演算子も実装すると、より実用的になります。
たとえば、加算は以下のように実装できます。 (checked
付き演算子の定義は省略しています。)
public partial struct Frac<T> : IAdditionOperator<Frac<T>, T, Frac<T>>
{
public static Frac<T> operator +(Frac<T> left, T right) {
left._numer += right * left._denom;
return left;
}
}