VB.NETだってDecimalの小数点以下の桁数を知りたい

ばっど☆のうはう

System.Deciamlの小数点以下の数字を取得する方法について少し調べたことがあったので、それについてのメモです。

結論的にはいささかバッドノウハウ感が漂う感じになります。 いくらAPIが内部データ構造を公開しているからってそれを取り出してビット演算を使用して桁数を無理やり取り出すのは少し気が引けるのです。

浮動小数点数なんて飾りです 偉い人には・・・

普段から生産性アプリケーションを書いて生産性に寄与している皆さんには常識的な事ですが、浮動小数点数は特定の小数以外は正確に表すことが出来ず誤差を含みます。 浮動小数点数で消費税を計算してるとそのうち誤差が出てきて『なんで誤差が生じるんだー』みたいになって責任問題になってああああああああああああ。

多少の誤差よりも速度が求められる科学技術計算ならいざ知らず、1円でもズレるとデスクが爆心地になるような用途では小数点以下をきっちり表せるデータ型が欲しくなります。

そんな戦場(開発現場)で戦い(デスマーチ)を続ける戦士(社畜)達に与えらし武器がSystem.Decimal構造体です。 これを使えば0.1を1億回足し合わせるという不毛な計算をしてもきっちり1000万になる優れものです。

さらに、VB.NETではDecimalというプリミティブ型として扱えるので扱いやすさも折り紙つきです。

それで、小数点以下の桁数の話は?

まぁ、小数点以下の桁数を取得できるメソッドがあればそれを使えば一発じゃね?と思ってとりあえずmsdnを見てみるとありませんでした

ところが、Decimalの等価なバイナリ形式を返してくれるメソッドがありました。

Decimal.GetBits メソッド (System)

Decimalは内部的に整数及び符号、そして整数を除算して小数部を表現するスケールファクタで構成されています。 だったらそのスケールファクタをあーんな方法やこーんな方法でぶっこ抜いてやれば小数点以下の桁数を取得できるという訳です。

Option Strict On

Class HelloWorld
    Public Shared Sub Main(ByVal args As String())
        Console.WriteLine(GetScale(1.2345D))
    End Sub

    Public Shared Function GetScale(value As Decimal) As Integer
        Dim bits = Decimal.GetBits(value)
        Return bits(3) >> 16 And &HFF
    End Function
End Class

特に難しいところはなく、16ビット右にシフトした後スケールファクタ部分をビットマスクで取り出して終わりです。

せっかくなんで、拡張メソッドを用いてもっとスタイリッシュかつクールにしてみましょう。

Option Strict On

Imports System.Runtime.CompilerServices

Class HelloWorld
    Public Shared Sub Main(ByVal args As String())
        Dim d = 1.2345D
        Console.WriteLine(d.GetScale())
    End Sub
End Class

Module DecimalGetScale
    <Extension()>
    Public Function GetScale(value As Decimal) As Integer
        Dim bits = Decimal.GetBits(value)
        Return bits(3) >> 16 And &HFF
    End Function
End Module

拡張メソッドを用いる事であたかもDecimalのインスタンスメソッドのように自分で定義したメソッドを呼び出せています。

ただ、Decimalのメソッドの大半がクラスメソッドなのでインスタンスメソッドでこんな操作があるのに違和感を感じるのと、拡張メソッドは対象の型の提供者以外は特に注意して実装しないといろいろ厄介なことになるかもなので、素直にSharedなメソッドとして実装するのが無難*1かもしれません。

*1:そもそもこんな使い方を想定して実装されたものではないが・・・