10進数クラスの話

Javaプログラマ向け、Objective-Cの10進数クラスの話。

JDK 7, Xcode 5.0


浮動小数点型では、10進数の小数部を正しく表現できないことが多い。
具体的には、2の-n乗の値の組み合わせでしか小数値を表せない。
これはIEEE 754の仕様上、当然のことである。

たとえば、double型の0.1は、以下のように微妙な誤差をもって表現される。
これはJavaでもObjective-Cでも同じ。

int main(int argc, const char** argv)
{
    NSLog(@"%.20lf", 0.1);    // 0.10000000000000000555
}


さて、Javaという言語においては、10進数を正確に扱うためにjava.math.BigDecimalという標準APIが用意されている。BigDecimal仮数部が任意精度であるため、非常に使い勝手がよい。指数部はスケールという言葉で表現されている。スケールは、仮数部に対して、最下位桁から数えて何桁目に小数点が位置するかを表す。(負の値の場合10のn乗倍)
スケールは任意精度ではなく、32ビット整数値である。

たとえば、0.35という値のBigDecimalインスタンスを生成するには以下のようにする。

BigDecimal val = new BigDecimal(BigInteger.valueOf(35L), 2);


では、Objective-Cではどうか。
NSDecimalNumberという似たような整数クラスがサポートされている。

まず、BigDecimalとの違いを要約しておく。

  • 指数部(Javaのscaleに相当するexponent)の符号が逆である
  • 指数部精度が-128〜127である(Javaでは32ビット整数)
  • 仮数部がshort[8]で表現されており、精度は38桁である(Javaでは任意精度)
  • 仮数部や指数部に対する直接的なデータ参照ができない(Javaではできる)
  • 仮数部と指数部の内部データが自動的に正規化される

とまあ、BigDecimalと比べると、制約は多い。


さきほどのJavaの例と同じように、0.35という値のNSDecimalNumberクラスを生成するコードは以下のようになる。

NSDecimalNumber* val = [NSDecimalNumber decimalNumberWithMantissa:35LL exponent:-2 isNegative:NO];

NSDecimalNumberも、BigDecimalと同様、不変クラスであり、四則演算メソッドがファクトリメソッドとして用意されている。

以下、前述したJavaBigDecimalとの違いについて補足。


指数部(Javaのscaleに相当するexponent)の符号が逆である

コードを見るとわかるように、指数部の符号がJavaと逆である。
Javaでは、unscaledVal * (10^-scale)
NSDecimalNumberでは、mantissa * (10^exponent)
である。
ちなみに、上記コード例のファクトリーメソッドにおける、第一引数の仮数部(mantissa)の型はunsigned long long型であり、これは実際にNSDecimalNumberが持ち得る仮数部より遥かに精度が低い。
第三引数は負の値の場合にYESを指定する。


指数部精度が-128〜127である(Javaでは32ビット整数)

NSDecimal構造体ではint型となっているが、ファクトリーメソッドの引数ではshort型となっており、APIリファレンスでは-128〜127となっている。


仮数部がshort[8]で表現されており、精度は38桁である(Javaでは任意精度)

short[8]は128ビット。
2^128が
340282366920938463463374607431768211456
であるからして、38桁目までは有効である。
JavaBigDecimal仮数部が任意精度であることと比べると少し不安かもしれない。
しかしまあ、よほどの計算をする場合でなければ、38桁の仮数部というのは十分すぎる。


仮数部や指数部に対する直接的なデータ参照ができない(Javaではできる)

BigDecimal#scaleやBigDecimal#unscaledValueに対応するメソッドは、NSDecimalNumberには用意されていない。
どうしてもというなら、NSDecimal構造体から _exponent や _mantissa メンバにアクセスすることはできる。
しかしながら、NSDecimal構造体のメンバはprivateだとリファレンスには書いてある。*1

Used to describe a decimal number.

typedef struct {
signed int _exponent:8;
unsigned int _length:4;
unsigned int _isNegative:1;
unsigned int _isCompact:1;
unsigned int _reserved:18;
unsigned short _mantissa[NSDecimalMaxSize];
} NSDecimal;

Discussion
The fields of NSDecimal are private.

【参考】
http://stackoverflow.com/questions/12643297/get-exponent-of-nsdecimalnumber-in-objective-c


仮数部と指数部の内部データが自動的に正規化される

JavaBigDecimalは、オブジェクトの構築時に指定した仮数部と指数部は通常そのまま保持されるが、Objective-CのNSDecimalNumberは、仮数部を縮めるような形で自動的に正規化される。もっとも、NSDecimalNumberの内部データに直接アクセスする手段が公式には存在しないのだから、通常、意識する必要はないだろう。

以下に正規化による違いを示す。

Java

BigDecimal val = new BigDecimal(BigInteger.valueOf(1200L), -1);
System.out.println(val.unscaledValue());    // 1200
System.out.println(val.scale());            // -1

Objective-C

NSDecimalNumber* dn = [NSDecimalNumber decimalNumberWithMantissa:1200LL exponent:1 isNegative:NO];
NSLog(@"%d", [dn decimalValue]._mantissa[0]);   // 12
NSLog(@"%d", [dn decimalValue]._exponent);      // 3

*1:構造体なので、メンバはpublicであり、コーディングレベルではアクセス可能。ただし、リファレンスにprivateだと書いてあるのだから、APIのユーザーがこれらのメンバに直接アクセスすることは好ましくはないだろう。