web-dev-qa-db-ja.com

カルチャに関係なく、小数点以下の桁数を見つける

異なる文化情報で安全に使用できる小数の小数点以下の桁数を(intとして)引き出すための簡潔で正確な方法があるのだろうか?

例えば:
19.0は1を返します。
27.5999は4を返すはずです
19.12は2を返すはずです
等。

ピリオドで文字列を分割して小数点以下の桁を見つけるクエリを作成しました。

int priceDecimalPlaces = price.ToString().Split('.').Count() > 1 
                  ? price.ToString().Split('.').ToList().ElementAt(1).Length 
                  : 0;

しかし、これは、「。」を使用する地域でのみ機能するということです。小数点として使用されるため、異なるシステム間で非常に脆弱です。

68
Jesse Carter

Joeの方法 を使用してこの問題を解決しました:)

decimal argument = 123.456m;
int count = BitConverter.GetBytes(decimal.GetBits(argument)[3])[2];
140
burning_LEGION

提供された回答はいずれも、10進数に変換されたマジックナンバー「-0.01f」に対して十分なものではなかったためです。つまり、GetDecimal((decimal)-0.01f);

これは、この邪悪で怪物的な問題、つまりポイントの後の小数点以下の桁数をカウントするという非常に複雑な問題に対する実用的な実装であると思われるものです。数学フォーラムを読む..単純な3年生の数学。

public static class MathDecimals
{
    public static int GetDecimalPlaces(decimal n)
    {
        n = Math.Abs(n); //make sure it is positive.
        n -= (int)n;     //remove the integer part of the number.
        var decimalPlaces = 0;
        while (n > 0)
        {
            decimalPlaces++;
            n *= 10;
            n -= (int)n;
        }
        return decimalPlaces;
    }
}

private static void Main(string[] args)
{
    Console.WriteLine(1/3m); //this is 0.3333333333333333333333333333
    Console.WriteLine(1/3f); //this is 0.3333333

    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.0m));                  //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(1/3m));                  //28
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)(1 / 3f)));     //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-1.123m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces(43.12345m));             //5
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0));                     //0
    Console.WriteLine(MathDecimals.GetDecimalPlaces(0.01m));                 //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces(-0.001m));               //3
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.00000001f)); //8
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.0001234f));   //7
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)0.01f));        //2
    Console.WriteLine(MathDecimals.GetDecimalPlaces((decimal)-0.01f));       //2
}
20
G.Y

おそらく @ fixagon's answer で解決策を使用するでしょう。

ただし、Decimal構造体には小数の数を取得するメソッドがありませんが、 Decimal.GetBits を呼び出してバイナリ表現を抽出し、整数値とスケールを使用して小数。

これはおそらく、文字列として書式設定するよりも速いでしょうが、違いに気付くには非常に多くの小数を処理する必要があります。

演習として実装を終了します。

17
Joe

小数点以下の桁数を見つけるための最適なソリューションの1つは、 burning_LEGIONの投稿 に示されています。

ここでは、STSdbフォーラムの記事の一部を使用しています: 小数点以下の桁数

MSDNでは、次の説明を読むことができます。

」10進数は、符号、値の各桁が0〜9の範囲の数値、およびaの位置を示すスケーリング係数で構成される浮動小数点値です。数値の整数部分と小数部分を区切る浮動小数点。 "

そしてまた:

」Decimal値のバイナリ表現は、1ビット符号、96ビット整数、および96ビット整数の除算とその一部を指定するために使用されるスケーリング係数で構成されますは、小数です。スケーリング係数は、暗黙的に10の数値であり、0から28の範囲の指数になります。 "

内部レベルでは、10進数値は4つの整数値で表されます。

Decimal internal representation

内部表現を取得するための公開されているGetBits関数があります。この関数は、int []配列を返します。

[__DynamicallyInvokable] 
public static int[] GetBits(decimal d)
{
    return new int[] { d.lo, d.mid, d.hi, d.flags };
}

返される配列の4番目の要素には、スケール係数と符号が含まれます。また、MSDNによると、スケーリング係数は暗黙的に10であり、0から28の範囲の指数に引き上げられます。これがまさに必要なものです。

したがって、上記のすべての調査に基づいて、メソッドを構築できます。

private const int SIGN_MASK = ~Int32.MinValue;

public static int GetDigits4(decimal value)
{
    return (Decimal.GetBits(value)[3] & SIGN_MASK) >> 16;
}

ここでは、SIGN_MASKを使用して記号を無視します。論理演算の後、実際のスケールファクターを受け取るために結果を16ビット右にシフトしました。最後に、この値は小数点以下の桁数を示します。

ここで、MSDNはスケーリングファクターが10進数の末尾のゼロも保持することにも注意してください。末尾のゼロは、算術演算または比較演算の10進数の値に影響しません。ただし、適切なフォーマット文字列が適用されると、ToStringメソッドによって末尾のゼロが明らかになる場合があります。

このソリューションは最高のソリューションのように見えますが、待ってください。 C#のプライベートメソッドにアクセスする を使用すると、式を使用してflagsフィールドへの直接アクセスを構築し、int配列の構築を回避できます。

public delegate int GetDigitsDelegate(ref Decimal value);

public class DecimalHelper
{
    public static readonly DecimalHelper Instance = new DecimalHelper();

    public readonly GetDigitsDelegate GetDigits;
    public readonly Expression<GetDigitsDelegate> GetDigitsLambda;

    public DecimalHelper()
    {
        GetDigitsLambda = CreateGetDigitsMethod();
        GetDigits = GetDigitsLambda.Compile();
    }

    private Expression<GetDigitsDelegate> CreateGetDigitsMethod()
    {
        var value = Expression.Parameter(typeof(Decimal).MakeByRefType(), "value");

        var digits = Expression.RightShift(
            Expression.And(Expression.Field(value, "flags"), Expression.Constant(~Int32.MinValue, typeof(int))), 
            Expression.Constant(16, typeof(int)));

        //return (value.flags & ~Int32.MinValue) >> 16

        return Expression.Lambda<GetDigitsDelegate>(digits, value);
    }
}

このコンパイルされたコードは、GetDigitsフィールドに割り当てられます。関数はrefとして10進数値を受け取るため、実際のコピーは実行されず、値への参照のみが実行されることに注意してください。 DecimalHelperからGetDigits関数を使用するのは簡単です。

decimal value = 3.14159m;
int digits = DecimalHelper.Instance.GetDigits(ref value);

これは、小数値の小数点以下の桁数を取得するための最速の方法です。

15
Kris

invariantCultureを使用できます

string priceSameInAllCultures = price.ToString(System.Globalization.CultureInfo.InvariantCulture);

別の可能性は、そのようなことをすることです:

private int GetDecimals(decimal d, int i = 0)
{
    decimal multiplied = (decimal)((double)d * Math.Pow(10, i));
    if (Math.Round(multiplied) == multiplied)
        return i;
    return GetDecimals(d, i+1);
}
11
fixagon

小数の内部表現に依存することは、クールではありません。

これはどう:

    int CountDecimalDigits(decimal n)
    {
        return n.ToString(System.Globalization.CultureInfo.InvariantCulture)
                //.TrimEnd('0') uncomment if you don't want to count trailing zeroes
                .SkipWhile(c => c != '.')
                .Skip(1)
                .Count();
    }
10
Clement

ここでのほとんどの人は、小数がストレージと印刷にとって重要であると10進数を考慮することに気付いていないようです。

したがって、0.1m、0.10m、および0.100mは等しいと比較される場合があり、それらは別々に(それぞれ値/スケール1/1、10/2、および100/3として)保存され、それぞれ0.1、0.10、および0.100として印刷されます、ToString()によって。

そのため、「精度が高すぎる」と報告するソリューションは、実際にdecimalの用語でcorrect精度を報告しています。

さらに、数学ベースのソリューション(10の累乗で乗算するなど)は非常に遅くなる可能性があります(10進数は、算術演算の場合、2倍から40倍遅くなり、浮動小数点を混在させることは不正確になる可能性が高いためです) )。同様に、切り捨ての手段としてintまたはlongにキャストするとエラーが発生しやすくなります(decimalの範囲はこれらのいずれよりもはるかに大きく、96ビット整数に基づいています)。

そのようにエレガントではありませんが、次の方法はおそらく精度を得るための最速の方法の1つです(「末尾のゼロを除く10進数の場所」として定義される場合)。

public static int PrecisionOf(decimal d) {
  var text = d.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0');
  var decpoint = text.IndexOf('.');
  if (decpoint < 0)
    return 0;
  return text.Length - decpoint - 1;
}

不変のカルチャは「。」を保証します小数点として、末尾のゼロは切り取られますが、それは小数点の後にいくつの位置が残っているかを見るだけです(1つある場合)。

編集:戻り値の型をintに変更

6
Zastai

また、別の方法として、小数点以下の桁数をカウントするスケールプロパティを持つSqlDecimal型を使用します。 10進値をSqlDecimalにキャストしてから、Scaleにアクセスします。

((SqlDecimal)(decimal)yourValue).Scale
5
RitchieD

昨日、文字列の分割やカルチャに依存せずに小数点以下の桁数を返す簡潔なメソッドを書きました。これは理想的です。

public int GetDecimalPlaces(decimal decimalNumber) { // 
try {
    // PRESERVE:BEGIN
        int decimalPlaces = 1;
        decimal powers = 10.0m;
        if (decimalNumber > 0.0m) {
            while ((decimalNumber * powers) % 1 != 0.0m) {
                powers *= 10.0m;
                ++decimalPlaces;
            }
        }
return decimalPlaces;
2
Jesse Carter

これまでのところ、リストされているソリューションのほぼすべてがGCメモリを割り当てています。これは非常にC#の方法ですが、パフォーマンスが重要な環境では理想からはほど遠いものです。 (割り当てを行わないものはループを使用し、末尾のゼロも考慮しません。)

したがって、GC Allocを回避するには、安全でないコンテキストでスケールビットにアクセスするだけです。それは壊れやすいように聞こえるかもしれませんが、 Microsoftの参照ソース のとおり、decimalの構造レイアウトはSequentialであり、フィールドの順序を変更しないようにコメントがあります:

    // NOTE: Do not change the order in which these fields are declared. The
    // native methods in this class rely on this particular order.
    private int flags;
    private int hi;
    private int lo;
    private int mid;

ご覧のとおり、最初のintはフラグフィールドです。ドキュメンテーションおよび他のコメントで述べられているように、16-24のビットのみがスケールをエンコードし、符号をエンコードする31番目のビットを避ける必要があることを知っています。 intは4バイトのサイズなので、これを安全に行うことができます。

internal static class DecimalExtensions
{
  public static byte GetScale(this decimal value)
  {
    unsafe
    {
      byte* v = (byte*)&value;
      return v[2];
    }
  }
}

これは、バイト配列のGC割り当てまたはToString変換がないため、最もパフォーマンスの高いソリューションになります。 Unity 2019.1で.Net 4.xおよび.Net 3.5に対してテストしました。これが失敗するバージョンがある場合は、お知らせください。

Edit:

安全でないコードの外部で同じポインターロジックを実際に達成するために、明示的な構造体レイアウトを使用する可能性について思い出させてくれた@Zastaiに感謝します。

[StructLayout(LayoutKind.Explicit)]
public struct DecimalHelper
{
    const byte k_SignBit = 1 << 7;

    [FieldOffset(0)]
    public decimal Value;

    [FieldOffset(0)]
    public readonly uint Flags;
    [FieldOffset(0)]
    public readonly ushort Reserved;
    [FieldOffset(2)]
    byte m_Scale;
    public byte Scale
    {
        get
        {
            return m_Scale;
        }
        set
        {
            if(value > 28)
                throw new System.ArgumentOutOfRangeException("value", "Scale can't be bigger than 28!")
            m_Scale = value;
        }
    }
    [FieldOffset(3)]
    byte m_SignByte;
    public int Sign
    {
        get
        {
            return m_SignByte > 0 ? -1 : 1;
        }
    }
    public bool Positive
    {
        get
        {
            return (m_SignByte & k_SignBit) > 0 ;
        }
        set
        {
            m_SignByte = value ? (byte)0 : k_SignBit;
        }
    }
    [FieldOffset(4)]
    public uint Hi;
    [FieldOffset(8)]
    public uint Lo;
    [FieldOffset(12)]
    public uint Mid;

    public DecimalHelper(decimal value) : this()
    {
        Value = value;
    }

    public static implicit operator DecimalHelper(decimal value)
    {
        return new DecimalHelper(value);
    }

    public static implicit operator decimal(DecimalHelper value)
    {
        return value.Value;
    }
}

元の問題を解決するには、ValueおよびScale以外のすべてのフィールドを削除できますが、誰かがそれらをすべて持っていると便利かもしれません。

2

コードで次のメカニズムを使用します

  public static int GetDecimalLength(string tempValue)
    {
        int decimalLength = 0;
        if (tempValue.Contains('.') || tempValue.Contains(','))
        {
            char[] separator = new char[] { '.', ',' };
            string[] tempstring = tempValue.Split(separator);

            decimalLength = tempstring[1].Length;
        }
        return decimalLength;
    }

10進数の入力= 3.376; var instring = input.ToString();

getDecimalLength(instring)を呼び出します

1
Srikanth
string number = "123.456789"; // Convert to string
int length = number.Substring(number.IndexOf(".") + 1).Length;  // 6
1
Eva Chang

あなたが試すことができます:

int priceDecimalPlaces =
        price.ToString(System.Globalization.CultureInfo.InvariantCulture)
              .Split('.')[1].Length;
1
NicoRiff

この方法を使用することをお勧めします:

    public static int GetNumberOfDecimalPlaces(decimal value, int maxNumber)
    {
        if (maxNumber == 0)
            return 0;

        if (maxNumber > 28)
            maxNumber = 28;

        bool isEqual = false;
        int placeCount = maxNumber;
        while (placeCount > 0)
        {
            decimal vl = Math.Round(value, placeCount - 1);
            decimal vh = Math.Round(value, placeCount);
            isEqual = (vl == vh);

            if (isEqual == false)
                break;

            placeCount--;
        }
        return Math.Min(placeCount, maxNumber); 
    }
0
Veysel Özdemir

再帰を使用すると、次のことができます。

private int GetDecimals(decimal n, int decimals = 0)  
{  
    return n % 1 != 0 ? GetDecimals(n * 10, decimals + 1) : decimals;  
}
0
Mars