C#で有理数型を実装してみた例です。
有理数で実現したいこと
有理数型として下記の機能を作りたいと思います。
- 加算
- 減算
- 乗算
- 除算
- 比較
- 浮動小数点数への変換
型の定義
longの既約分数として有理数を表現することにします。符号はどちらでも良いですが、分子に集約することにします。
既約分数は分子と分母を最大公約数で割ると良いので、最大公約数をユークリッドの互除法で求めてしまえば良いです。
Fraction.cs
/// <summary>有理数を既約分数で表す</summary>publicreadonlystructFraction:IEquatable<Fraction>,IComparable<Fraction>{/// <summary>分子</summary>publiclongNumerator{get;}/// <summary>分母</summary>publiclongDenominator{get;}publicFraction(long分子,long分母){varnegative=(分子^分母)<0;分子=Math.Abs(分子);分母=Math.Abs(分母);vargcd=Gcd(分母,分子);_numerator=分子/gcd;if(negative)_numerator=-_numerator;_denominator=分母/gcd-1;}/// <summary>/// 最大公約数をユークリッドの互除法で求める/// </summary>publicstaticlongGcd(longa,longb)=>b>a?Gcd(b,a):(b==0?a:Gcd(b,a%b));}四則演算の定義
小学校の算数で習う分数の計算をそのまま実装します。
C# は演算子オーバーロードを定義できるので、素直に実装します。
加算、減算した結果の分母は計算するそれぞれの分母の最小公倍数になります。
x, y の最小公倍数は $ \frac{x y}{最大公約数} $ と表せます。
Fraction.cs
publicstaticFractionoperator-(Fractionx)=>newFraction(-x.Numerator,x.Denominator);publicstaticFractionoperator+(Fractionx,Fractiony){vargcd=Gcd(x.Denominator,y.Denominator);varlcm=x.Denominator/gcd*y.Denominator;returnnewFraction((x.Numerator*y.Denominator+y.Numerator*x.Denominator)/gcd,lcm);}publicstaticFractionoperator-(Fractionx,Fractiony){vargcd=Gcd(x.Denominator,y.Denominator);varlcm=x.Denominator/gcd*y.Denominator;returnnewFraction((x.Numerator*y.Denominator-y.Numerator*x.Denominator)/gcd,lcm);}publicstaticFractionoperator*(Fractionx,Fractiony)=>newFraction(x.Numerator*y.Numerator,x.Denominator*y.Denominator);publicstaticFractionoperator/(Fractionx,Fractiony)=>newFraction(x.Numerator*y.Denominator,x.Denominator*y.Numerator);比較の定義
\frac{a}{b} < \frac{c}{d}
という不等式は両辺に $ bd $ を掛けて、
ad < bd
と変形できるので、long型の演算で定義できます。
Fraction.cs
publicintCompareTo(Fractionother)=>(this.Numerator*other.Denominator).CompareTo(other.Numerator*this.Denominator);publicstaticbooloperator==(Fractionx,Fractiony)=>x.Equals(y);publicstaticbooloperator!=(Fractionx,Fractiony)=>!x.Equals(y);publicstaticbooloperator>=(Fractionx,Fractiony)=>x.CompareTo(y)>=0;publicstaticbooloperator<=(Fractionx,Fractiony)=>x.CompareTo(y)<=0;publicstaticbooloperator>(Fractionx,Fractiony)=>x.CompareTo(y)>0;publicstaticbooloperator<(Fractionx,Fractiony)=>x.CompareTo(y)<0;変換
doubleへの変換やlongからの暗黙的な変換も定義しておくと使いやすいでしょう。longからの変換は分母を1にするだけです。
Fraction.cs
publicdoubleToDouble()=>(double)Numerator/Denominator;publicstaticimplicitoperatorFraction(longx)=>newFraction(x,1);デフォルト値の問題
しかし、このような実装ではデフォルト値が 0/0となってしまいます。
そのため new Fraction(2,3) * default(Fraction)がゼロ除算を引き起こしてしまいます。
new Fraction(2,3) + default(Fraction) == new new Fraction(2,3)new Fraction(2,3) + default(Fraction) == new new Fraction(0,1)
を満たすようにしたいところです。
これは、型の内部表現では分母の値を-1すると解決します。
つまり、分母が1のときは内部では0を保持、分母が2のときは内部では1を保持、という具合です。
こうすることで、default(Fraction) == new new Fraction(0,1)となり上記のデフォルト値での演算も期待通りになります。
Fraction.cs
publicreadonlystructFraction{/// <summary>分子</summary>privatereadonlylong_numerator;/// <summary>分子</summary>publiclongNumerator=>_numerator;/// <summary>分母 - 1 (default を 0/0 ではなく 0/1 にしたい)</summary>privatereadonlylong_denominator;/// <summary>分母</summary>publiclongDenominator=>_denominator+1;publicFraction(long分子,long分母){varnegative=(分子^分母)<0;分子=Math.Abs(分子);分母=Math.Abs(分母);if(分子==0){_numerator=0;_denominator=0;}else{vargcd=Gcd(分母,分子);_numerator=分子/gcd;if(negative)_numerator=-_numerator;_denominator=分母/gcd-1;}}}完成
上記の検討から有理数型を作ることができました。
Fraction.cs
/// <summary>有理数を既約分数で表す</summary>publicreadonlystructFraction:IEquatable<Fraction>,IComparable<Fraction>{/// <summary>分子</summary>privatereadonlylong_numerator;/// <summary>分子</summary>publiclongNumerator=>_numerator;/// <summary>分母 - 1 (default を 0/0 ではなく 0/1 にしたい)</summary>privatereadonlylong_denominator;/// <summary>分母</summary>publiclongDenominator=>_denominator+1;publicFraction(long分子,long分母){varnegative=(分子^分母)<0;分子=Math.Abs(分子);分母=Math.Abs(分母);if(分子==0){_numerator=0;_denominator=0;}else{vargcd=Gcd(分母,分子);_numerator=分子/gcd;if(negative)_numerator=-_numerator;_denominator=分母/gcd-1;}}/// <summary>/// 最大公約数をユークリッドの互除法で求める/// </summary>publicstaticlongGcd(longa,longb)=>b>a?Gcd(b,a):(b==0?a:Gcd(b,a%b));publicoverridestringToString()=>$"{Numerator}/{Denominator}";publicoverrideboolEquals(objectobj)=>objisFractionf&&Equals(f);publicboolEquals(Fractionother)=>this._numerator==other._numerator&&this._denominator==other._denominator;publicoverrideintGetHashCode()=>HashCode.Combine(_numerator,_denominator);publicstaticimplicitoperatorFraction(longx)=>newFraction(x,1);publicstaticFractionoperator-(Fractionx)=>newFraction(-x.Numerator,x.Denominator);publicstaticFractionoperator+(Fractionx,Fractiony){vargcd=Gcd(x.Denominator,y.Denominator);varlcm=x.Denominator/gcd*y.Denominator;returnnewFraction((x.Numerator*y.Denominator+y.Numerator*x.Denominator)/gcd,lcm);}publicstaticFractionoperator-(Fractionx,Fractiony){vargcd=Gcd(x.Denominator,y.Denominator);varlcm=x.Denominator/gcd*y.Denominator;returnnewFraction((x.Numerator*y.Denominator-y.Numerator*x.Denominator)/gcd,lcm);}publicstaticFractionoperator*(Fractionx,Fractiony)=>newFraction(x.Numerator*y.Numerator,x.Denominator*y.Denominator);publicstaticFractionoperator/(Fractionx,Fractiony)=>newFraction(x.Numerator*y.Denominator,x.Denominator*y.Numerator);publicintCompareTo(Fractionother)=>(this.Numerator*other.Denominator).CompareTo(other.Numerator*this.Denominator);publicstaticbooloperator==(Fractionx,Fractiony)=>x.Equals(y);publicstaticbooloperator!=(Fractionx,Fractiony)=>!x.Equals(y);publicstaticbooloperator>=(Fractionx,Fractiony)=>x.CompareTo(y)>=0;publicstaticbooloperator<=(Fractionx,Fractiony)=>x.CompareTo(y)<=0;publicstaticbooloperator>(Fractionx,Fractiony)=>x.CompareTo(y)>0;publicstaticbooloperator<(Fractionx,Fractiony)=>x.CompareTo(y)<0;publicFractionInverse()=>newFraction(Denominator,Numerator);publicdoubleToDouble()=>(double)Numerator/Denominator;}