ค้นหาจำนวนตำแหน่งทศนิยมในค่าทศนิยมโดยไม่คำนึงถึงวัฒนธรรม


92

ฉันสงสัยว่ามีวิธีที่กระชับและถูกต้องในการดึงจำนวนตำแหน่งทศนิยมในค่าทศนิยม (เป็น 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;

แต่สำหรับฉันแล้วสิ่งนี้จะใช้ได้เฉพาะในภูมิภาคที่ใช้เครื่องหมาย "." เป็นตัวคั่นทศนิยมดังนั้นจึงเปราะมากในระบบต่างๆ


ทศนิยมตามชื่อคำถาม
Jesse Carter

แล้วรูปแบบบางอย่างที่ตรงกันก่อนที่จะแยก? โดยทั่วไป \ d + (\ D) \ d + โดยที่ \ D ส่งกลับตัวคั่น (. ฯลฯ )
Anshul

7
นี่ไม่ใช่คำถามปลายปิดเพราะในตอนแรกบลัชออนอาจปรากฏขึ้น การขอ19.0คืนสินค้า1เป็นรายละเอียดการดำเนินการเกี่ยวกับการจัดเก็บค่า19.0ภายใน ความจริงก็คือว่ามันถูกต้องตามกฎหมายที่ดีเลิศสำหรับโปรแกรมการจัดเก็บนี้เป็น190×10⁻¹หรือหรือ1900×10⁻² 19000×10⁻³ทั้งหมดนี้มีค่าเท่ากัน ความจริงที่ว่ามันใช้การแสดงครั้งแรกเมื่อได้รับค่า19.0Mและสิ่งนี้ถูกเปิดเผยเมื่อใช้ToStringโดยไม่มีตัวระบุรูปแบบนั้นเป็นเพียงเรื่องบังเอิญและเป็นสิ่งที่น่ายินดี ยกเว้นจะไม่มีความสุขเมื่อผู้คนพึ่งพาเลขชี้กำลังในกรณีที่ไม่ควรทำ
ErikE

หากคุณต้องการประเภทที่สามารถดำเนินการ "จำนวนของตำแหน่งทศนิยมที่ใช้" เมื่อมันถูกสร้างขึ้นเพื่อให้คุณได้อย่างน่าเชื่อถือสามารถแยกแยะความแตกต่าง19Mจาก19.0Mจาก19.00Mคุณจะต้องสร้างคลาสใหม่ที่รวมกลุ่มมูลค่าอ้างอิงเป็นหนึ่งในสถานที่ให้บริการและจำนวน ตำแหน่งทศนิยมเป็นคุณสมบัติอื่น
ErikE

1
แม้ว่าคลาสทศนิยมสามารถ "แยกแยะ" 19m, จาก 19.0m จาก 19.00m? ตัวเลขที่มีนัยสำคัญเปรียบเสมือนหนึ่งในกรณีการใช้งานที่สำคัญ 19.0m * 1.0m คืออะไร? ดูเหมือนว่าจะพูดว่า 19.00m บางที C # devs กำลังทำคณิตศาสตร์ผิด: P? เลขนัยสำคัญอีกครั้งเป็นของจริง หากคุณไม่ชอบเลขนัยสำคัญคุณไม่ควรใช้คลาสทศนิยม
Nicholi

คำตอบ:


168

ฉันใช้วิธีของโจเพื่อแก้ปัญหานี้ :)

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

6
หลังจากดูสิ่งนี้เพิ่มเติมและเห็นว่ามันใช้งานได้จริงฉันกำลังทำเครื่องหมายว่าเป็นคำตอบเพราะในความคิดของฉันเป็นวิธีการคืนตำแหน่งทศนิยมที่รัดกุมและสง่างามที่สุดที่ฉันเคยเห็นที่นี่ จะ +1 อีกครั้งถ้าทำได้: D
Jesse Carter

9
decimalนับจำนวนหลักหลังจากโคม่านั่นคือสาเหตุที่คุณพบ "ปัญหา" นี้คุณต้องโยนทศนิยมเป็นสองเท่าและเป็นทศนิยมอีกครั้งเพื่อแก้ไข: BitConverter.GetBytes (decimal.GetBits ((decimal) (อาร์กิวเมนต์คู่) [3]) [ 2];
burning_LEGION

3
สิ่งนี้ไม่ได้ผลสำหรับฉัน ค่าที่กลับมาจาก SQL คือ 21.17 มันบอก 4 หลัก ชนิดข้อมูลถูกกำหนดให้เป็น DECIMAL (12,4) ดังนั้นอาจเป็นเช่นนั้น (โดยใช้ Entity Framework)
PeterX

11
@Nicholi - ไม่มีนี้ไม่ดีเป็นพิเศษเพราะวิธีการที่จะอาศัยอยู่กับตำแหน่งของบิตพื้นฐานของทศนิยม - บางสิ่งบางอย่างซึ่งมีหลายวิธีที่จะเป็นตัวแทนของหมายเลขเดียวกัน คุณจะไม่ทดสอบชั้นเรียนโดยพิจารณาจากสถานะของเขตข้อมูลส่วนตัวหรือไม่?
m.edmondson

14
ไม่แน่ใจว่าควรจะหรูหราหรือดีเกี่ยวกับเรื่องนี้ นี่เป็นเรื่องที่สับสนพอ ๆ กับที่ได้รับ ใครจะรู้ว่ามันใช้งานได้ในทุกกรณี เป็นไปไม่ได้ที่จะตรวจสอบให้แน่ใจ
usr

25

เนื่องจากไม่มีคำตอบใดที่ให้คำตอบที่ดีพอสำหรับเลขวิเศษ "-0.01f" ที่แปลงเป็นทศนิยม .. กล่าวคือ: GetDecimal((decimal)-0.01f);
ฉันสามารถสันนิษฐานได้ว่าไวรัสผายลมขนาดมหึมาโจมตีทุกคนเมื่อ 3 ปีก่อน :)
นี่คือสิ่งที่ดูเหมือนจะใช้งานได้ การนำไปใช้กับปัญหาที่ชั่วร้ายและมหึมานี้ปัญหาที่ซับซ้อนมากในการนับตำแหน่งทศนิยมหลังจุด - ไม่มีสตริงไม่มีวัฒนธรรมไม่จำเป็นต้องนับบิตและไม่จำเป็นต้องอ่านฟอรัมคณิตศาสตร์ .. คณิตศาสตร์ป. 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
}

6
การแก้ปัญหาของคุณจะล้มเหลวในหลายกรณีที่มีเลขศูนย์ต่อท้ายและตัวเลขมีความสำคัญ 0.01 ม. * 2.0 ม. = 0.020 ม. ควรเป็น 3 หลักวิธีของคุณจะส่งกลับ 2 ดูเหมือนว่าคุณจะเข้าใจไม่ถูกต้องว่าเกิดอะไรขึ้นเมื่อคุณโยน 0.01f เป็นทศนิยม จุดลอยตัวนั้นไม่แม่นยำโดยเนื้อแท้ดังนั้นค่าไบนารีจริงที่เก็บไว้ที่ 0.01f จึงไม่แน่นอน เมื่อคุณร่ายเป็นทศนิยม (สัญกรณ์ตัวเลขที่มีโครงสร้างมาก) คุณอาจไม่ได้ 0.01m (คุณได้ 0.010m จริงๆ) โซลูชัน GetBits นั้นถูกต้องสำหรับการรับจำนวนหลักจากทศนิยม วิธีแปลงเป็นทศนิยมเป็นกุญแจสำคัญ
Nicholi

2
@Nicholi 0.020m เท่ากับ 0.02m .. เลขศูนย์ต่อท้ายไม่มีนัยสำคัญ OP กำลังถามว่า "ไม่คำนึงถึงวัฒนธรรม" ในชื่อเรื่องและอธิบายเฉพาะเจาะจงมากขึ้นว่า ".. ที่จะใช้กับข้อมูลวัฒนธรรมต่างๆได้อย่างปลอดภัย .. " - ดังนั้นฉันคิดว่าคำตอบของฉันยังคงถูกต้องมากกว่าคำตอบอื่น ๆ
GY

6
OP กล่าวโดยเฉพาะ: "19.0 ควรกลับ 1" รหัสนี้ล้มเหลวในกรณีนั้น
daniloquio

9
บางทีนี่อาจไม่ใช่สิ่งที่ OP ต้องการ แต่คำตอบนี้ตรงกับความต้องการของฉันมากกว่าคำตอบยอดนิยมของคำถามนี้
Arsen Zahray

2
สองบรรทัดแรกจะถูกแทนที่ด้วยn = n % 1; if (n < 0) n = -n;เพราะค่ามากกว่าint.MaxValueจะทำให้เกิดการเช่นOverflowException 2147483648.12345
ความชิงชัง

24

ผมอาจจะใช้วิธีการแก้ปัญหาใน@ คำตอบของ

อย่างไรก็ตามในขณะที่โครงสร้างทศนิยมไม่มีเมธอดในการรับจำนวนทศนิยมคุณสามารถเรียกDecimal.GetBitsเพื่อแยกการแทนค่าไบนารีจากนั้นใช้ค่าจำนวนเต็มและมาตราส่วนเพื่อคำนวณจำนวนทศนิยม

อาจเร็วกว่าการจัดรูปแบบเป็นสตริงแม้ว่าคุณจะต้องประมวลผลทศนิยมจำนวนมากเพื่อสังเกตความแตกต่าง

ฉันจะปล่อยให้การใช้งานเป็นแบบฝึกหัด


1
ขอบคุณ @Joe ที่เป็นวิธีที่เรียบร้อยในการเข้าหามัน ขึ้นอยู่กับว่าเจ้านายของฉันรู้สึกอย่างไรเกี่ยวกับการใช้โซลูชันอื่นฉันจะดูที่การนำความคิดของคุณไปใช้ จะเป็นการออกกำลังกายที่สนุกแน่นอน :)
Jesse Carter

17

หนึ่งในโซลูชั่นที่ดีที่สุดสำหรับการหาจำนวนของตัวเลขหลังจุดทศนิยมจะแสดงในการโพสต์ของ burning_LEGION

นี่ฉันใช้ชิ้นส่วนจากบทความฟอรั่ม STSdb: จำนวนตัวเลขหลังจุดทศนิยม

ใน MSDN เราสามารถอ่านคำอธิบายต่อไปนี้:

"ตัวเลขทศนิยมคือค่าทศนิยมที่ประกอบด้วยเครื่องหมายเป็นค่าตัวเลขโดยแต่ละหลักในค่ามีค่าตั้งแต่ 0 ถึง 9 และตัวคูณมาตราส่วนที่ระบุตำแหน่งของจุดทศนิยมลอยตัวที่แยกอินทิกรัลและเศษส่วน ส่วนของค่าตัวเลข "

และนอกจากนี้ยังมี:

"การแทนค่าฐานสิบสองแบบไบนารีประกอบด้วยเครื่องหมาย 1 บิตจำนวนเต็ม 96 บิตและตัวคูณมาตราส่วนที่ใช้ในการหารจำนวนเต็ม 96 บิตและระบุว่าส่วนใดของมันเป็นเศษทศนิยมปัจจัยที่กำหนดคือ โดยปริยายเลข 10 ยกเป็นเลขชี้กำลังตั้งแต่ 0 ถึง 28 "

ในระดับภายในค่าทศนิยมจะแสดงด้วยค่าจำนวนเต็มสี่ค่า

การแทนค่าทศนิยมภายใน

มีฟังก์ชัน GetBits ที่เปิดเผยต่อสาธารณะสำหรับการเป็นตัวแทนภายใน ฟังก์ชันส่งคืนอาร์เรย์ int []:

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

องค์ประกอบที่สี่ของอาร์เรย์ที่ส่งคืนประกอบด้วยตัวคูณมาตราส่วนและเครื่องหมาย และตามที่ 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 ยังกล่าวว่าปัจจัยการปรับขนาดยังรักษาค่าศูนย์ต่อท้ายในเลขฐานสิบ เลขศูนย์ต่อท้ายไม่มีผลต่อค่าของเลขฐานสิบในการคำนวณทางคณิตศาสตร์หรือการเปรียบเทียบ อย่างไรก็ตามค่าศูนย์ต่อท้ายอาจถูกเปิดเผยโดยเมธอด ToString หากใช้สตริงรูปแบบที่เหมาะสม

โซลูชันนี้ดูเหมือนจะดีที่สุด แต่เดี๋ยวก่อนยังมีอีก โดยการเข้าถึงเมธอดส่วนตัวใน C #เราสามารถใช้นิพจน์เพื่อสร้างการเข้าถึงโดยตรงไปยังฟิลด์แฟล็กและหลีกเลี่ยงการสร้างอาร์เรย์ 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 ดังนั้นจึงไม่มีการคัดลอกจริง แต่เป็นการอ้างอิงถึงค่าเท่านั้น การใช้ฟังก์ชัน GetDigits จาก DecimalHelper นั้นง่ายมาก:

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

นี่เป็นวิธีที่เร็วที่สุดในการหาจำนวนหลักหลังจุดทศนิยมสำหรับค่าทศนิยม


3
ทศนิยม r = (ทศนิยม) -0.01f; และการแก้ปัญหาล้มเหลว (สำหรับคำตอบทั้งหมดที่ฉันเห็นในหน้านี้ ... ) :)
GY

5
หมายเหตุ: เกี่ยวกับสิ่งทั้งหมด (ทศนิยม) 0.01f คุณกำลังสร้างจุดลอยตัวโดยเนื้อแท้แล้วไม่แม่นยำไปยังบางสิ่งที่มีโครงสร้างเหมือนทศนิยม ดูผลลัพธ์ของ Console.WriteLine ((ทศนิยม) 0.01f) ทศนิยมที่ถูกสร้างขึ้นในการแคสจริงมี 3 หลักนั่นคือเหตุผลที่โซลูชันทั้งหมดระบุว่า 3 แทนที่จะเป็น 2 ทุกอย่างทำงานได้จริงตามที่คาดไว้ "ปัญหา" คือคุณคาดหวังว่าค่าทศนิยมจะแน่นอน พวกเขาจะไม่.
Nicholi

@Nicholi จุดของคุณล้มเหลวเมื่อคุณรู้0.01และ0.010เป็นตัวเลขที่เท่ากันทุกประการ นอกจากนี้แนวคิดที่ว่าประเภทข้อมูลที่เป็นตัวเลขมีความหมาย "จำนวนหลักที่ใช้" บางประเภทที่สามารถพึ่งพาได้นั้นผิดพลาดโดยสิ้นเชิง (อย่าสับสนกับ "จำนวนหลักที่อนุญาต" อย่าสับสนการนำเสนอ (การแสดงผล ค่าตัวเลขที่อยู่ในฐานโดยเฉพาะอย่างยิ่งเช่นการขยายตัวทศนิยมของค่าที่ระบุโดยการขยายตัวไบนารี 111) มูลค่าพื้นฐานย้ำ! ตัวเลขไม่ได้ตัวเลขไม่ว่าพวกเขาสร้างขึ้นจากตัวเลข .
ErikE

6
มีมูลค่าเทียบเท่ากัน แต่ไม่อยู่ในเลขนัยสำคัญ ซึ่งเป็นกรณีการใช้งานขนาดใหญ่ของคลาสทศนิยม ถ้าฉันถามว่ามีกี่หลักใน 0.010m ตัวอักษรคุณจะพูดแค่ 2 หรือเปล่า แม้ว่าคะแนนของครูคณิตศาสตร์ / วิทยาศาสตร์ทั่วโลกจะบอกคุณว่า 0 สุดท้ายมีความสำคัญ? ปัญหาที่เราอ้างถึงนั้นแสดงออกมาโดยการร่ายจากจุดลอยไปเป็นทศนิยม ไม่ใช่การใช้งาน GetBits ซึ่งทำตามที่ระบุไว้ หากคุณไม่สนใจเลขนัยสำคัญแสดงว่าคุณมีปัญหาและไม่ควรใช้คลาสทศนิยมตั้งแต่แรก
Nicholi

1
@theberserker เท่าที่ฉันจำได้ไม่มีการจับ - ควรใช้ได้ทั้งสองวิธี
Kristiyan Dimitrov

13

การอาศัยการแทนทศนิยมภายในไม่ใช่เรื่องเจ๋ง

แล้วสิ่งนี้ล่ะ:

    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();
    }

11

คุณสามารถใช้ 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);
}

สิ่งนี้ช่วยฉันหาจำนวนตำแหน่งทศนิยมในทศนิยมได้อย่างไร ฉันไม่มีปัญหาในการแปลงทศนิยมเป็นสตริงที่ดีในทุกวัฒนธรรม ตามคำถามที่ฉันพยายามหาจำนวนตำแหน่งทศนิยมที่อยู่บนทศนิยม
Jesse Carter

@JesseCarter: .มันหมายความว่าคุณสามารถแยก
Austin Salonen

@AustinSalonen จริงเหรอ? ฉันไม่ทราบว่าการใช้ InvariantCulture จะบังคับใช้จุดเป็นตัวคั่นทศนิยม
Jesse Carter

อย่างที่คุณเคยทำมาก่อนหน้านี้มันจะเหวี่ยงราคาเป็นสตริงด้วย. เป็นตัวคั่นทศนิยม แต่ไม่ได้เป็นวิธีของมันส่วนใหญ่สง่างามในความคิดของฉัน ...
fixagon


8

คนส่วนใหญ่ที่นี่ดูเหมือนจะไม่รู้ว่าทศนิยมถือว่าศูนย์ต่อท้ายมีความสำคัญต่อการจัดเก็บและการพิมพ์

ดังนั้น 0.1 ม. 0.10 ม. และ 0.100 ม. อาจเปรียบเทียบได้เท่ากันโดยจะจัดเก็บต่างกัน (เป็นค่า / มาตราส่วน 1/1, 10/2 และ 100/3 ตามลำดับ) และจะพิมพ์เป็น 0.1, 0.10 และ 0.100 ตามลำดับ โดย ToString() .

ด้วยเหตุนี้โซลูชันที่รายงานว่า "ความแม่นยำสูงเกินไป" จึงรายงานความแม่นยำที่ถูกต้องdecimalตามเงื่อนไข

นอกจากนี้วิธีการแก้ปัญหาทางคณิตศาสตร์ (เช่นการคูณด้วยเลขยกกำลัง 10) อาจจะช้ามาก (ทศนิยมจะช้ากว่าเลขคณิตประมาณ 40 เท่าและคุณไม่ต้องการผสมในทศนิยมเพราะอาจทำให้เกิดความไม่แม่นยำ ). ในทำนองเดียวกันการแคสต์ไปยังintหรือlongเป็นวิธีการตัดทอนนั้นมีโอกาสเกิดข้อผิดพลาดได้ง่าย ( decimalมีช่วงที่มากกว่าค่าใดค่าหนึ่งมากโดยอิงจากจำนวนเต็ม 96 บิต)

แม้ว่าจะไม่หรูหราเช่นนี้ แต่สิ่งต่อไปนี้น่าจะเป็นวิธีที่เร็วที่สุดวิธีหนึ่งในการรับความแม่นยำ (เมื่อกำหนดเป็น "ทศนิยมไม่รวมศูนย์ต่อท้าย"):

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;
}

วัฒนธรรมคงที่รับประกัน "." ในฐานะจุดทศนิยมศูนย์ต่อท้ายจะถูกตัดออกจากนั้นก็เป็นเพียงเรื่องของการดูจำนวนตำแหน่งที่เหลืออยู่หลังจากจุดทศนิยม (ถ้ามีแม้แต่ตำแหน่งเดียว)

แก้ไข: เปลี่ยน return type เป็น int


1
@mvmorten ไม่แน่ใจว่าทำไมคุณถึงรู้สึกว่าจำเป็นต้องเปลี่ยนประเภท return เป็น int; ไบต์แสดงค่าที่ส่งคืนได้แม่นยำยิ่งขึ้น: ช่วงที่ไม่ได้ลงนามและช่วงเล็ก ๆ (0-29 ในทางปฏิบัติ)
Zastai

1
ฉันยอมรับว่าการแก้ปัญหาแบบวนซ้ำและการคำนวณนั้นช้า (นอกเหนือจากการไม่บันทึกเลขศูนย์ต่อท้าย) อย่างไรก็ตามการจัดสรรสตริงสำหรับสิ่งนี้และดำเนินการแทนนั้นไม่ใช่สิ่งที่ต้องทำมากที่สุดโดยเฉพาะอย่างยิ่งในบริบทที่สำคัญด้านประสิทธิภาพและด้วย GC ที่ช้า การเข้าถึงมาตราส่วนผ่านลอจิกของตัวชี้เป็นเรื่องที่ดีเร็วขึ้นและไม่มีการจัดสรร
Martin Tilo Schmitz

ใช่การปรับขนาดสามารถทำได้อย่างมีประสิทธิภาพมากขึ้น แต่นั่นจะรวมถึงศูนย์ต่อท้ายด้วย และการลบออกต้องใช้เลขคณิตในส่วนจำนวนเต็ม
Zastai

6

และนี่เป็นอีกวิธีหนึ่งให้ใช้ชนิด SqlDecimal ซึ่งมีคุณสมบัติมาตราส่วนที่มีจำนวนตัวเลขทางขวาของทศนิยม แคสต์ค่าทศนิยมของคุณเป็น SqlDecimal จากนั้นเข้าถึงมาตราส่วน

((SqlDecimal)(decimal)yourValue).Scale

1
เมื่อดูที่รหัสอ้างอิงของ Microsoft การส่งไปยัง SqlDecimal ภายในจะใช้GetBytesดังนั้นจึงจัดสรรอาร์เรย์ไบต์แทนการเข้าถึงไบต์ในบริบทที่ไม่ปลอดภัย มีแม้แต่บันทึกและแสดงความคิดเห็นรหัสในรหัสอ้างอิงโดยระบุว่าและจะทำอย่างไรแทน ทำไมพวกเขาถึงไม่เป็นปริศนาสำหรับฉัน ฉันจะไม่เข้าใจสิ่งนี้และเข้าถึงสเกลบิตโดยตรงแทนที่จะซ่อน GC Alloc ในแคสต์นี้เพราะมันไม่ชัดเจนว่ามันทำอะไรภายใต้ประทุน
Martin Tilo Schmitz

4

จนถึงขณะนี้โซลูชันที่ระบุไว้เกือบทั้งหมดกำลังจัดสรรหน่วยความจำ GC ซึ่งเป็นวิธี C # ในการทำสิ่งต่างๆ แต่ยังห่างไกลจากสภาพแวดล้อมที่สำคัญด้านประสิทธิภาพ (สิ่งที่ไม่จัดสรรการใช้งานลูปและไม่คำนึงถึงศูนย์ต่อท้ายด้วย)

ดังนั้นเพื่อหลีกเลี่ยง GC Allocs คุณสามารถเข้าถึงสเกลบิตในบริบทที่ไม่ปลอดภัยได้ นั่นอาจฟังดูเปราะบาง แต่ตามแหล่งอ้างอิงของ Microsoftเค้าโครงโครงสร้างของฐานสิบเป็นลำดับและยังมีความคิดเห็นอยู่ในนั้นเพื่อไม่ให้เปลี่ยนลำดับของฟิลด์:

    // 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 ฉันได้ทดสอบกับ. Net 4.x และ. Net 3.5 ใน Unity 2019.1 หากมีเวอร์ชันใดที่ล้มเหลวโปรดแจ้งให้เราทราบ

แก้ไข:

ขอบคุณ @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แต่บางทีมันอาจจะเป็นประโยชน์สำหรับคนที่จะมีพวกเขาทั้งหมด


1
คุณยังสามารถหลีกเลี่ยงโค้ดที่ไม่ปลอดภัยได้โดยการเข้ารหัสโครงสร้างของคุณเองด้วยรูปแบบ Explict - ใส่ทศนิยมที่ตำแหน่ง 0 จากนั้นไบต์ / ints ในตำแหน่งที่เหมาะสม สิ่งที่ชอบ:[StructLayout(LayoutKind.Explicit)] public struct DecimalHelper { [FieldOffset(0)] public decimal Value; [FieldOffset(0)] public uint Flags; [FieldOffset(0)] public ushort Reserved; [FieldOffset(2)] public byte Scale; [FieldOffset(3)] public DecimalSign Sign; [FieldOffset(4)] public uint ValuePart1; [FieldOffset(8)] public ulong ValuePart2; }
Zastai

ขอบคุณ @Zastai จุดดี ฉันได้รวมแนวทางนั้นไว้ด้วยเช่นกัน :)
Martin Tilo Schmitz

1
สิ่งหนึ่งที่ควรทราบ: การตั้งสเกลนอกช่วง 0-28 ทำให้เกิดการแตกหัก ToString () มีแนวโน้มที่จะทำงาน แต่การคำนวณล้มเหลว
Zastai

ขอบคุณอีกครั้ง @Zastai ฉันได้เพิ่มการตรวจสอบแล้ว :)
Martin Tilo Schmitz

อีกประการหนึ่ง: หลาย ๆ คนที่นี่ไม่ต้องการคำนึงถึงศูนย์ทศนิยมต่อท้าย หากคุณกำหนดconst decimal Foo = 1.0000000000000000000000000000m;แล้วหารทศนิยมด้วยสิ่งนั้นจะปรับขนาดให้เป็นมาตราส่วนต่ำสุดเท่าที่จะเป็นไปได้ (กล่าวคือจะไม่รวมทศนิยมต่อท้ายอีกต่อไป) ฉันไม่ได้เปรียบเทียบสิ่งนี้เพื่อดูว่าเร็วกว่าวิธีการแบบสตริงที่ฉันแนะนำที่อื่นหรือไม่
Zastai

3

ฉันใช้บางอย่างที่คล้ายกับคำตอบของ Clement:

private int GetSignificantDecimalPlaces(decimal number, bool trimTrailingZeros = true)
{
  string stemp = Convert.ToString(number);

  if (trimTrailingZeros)
    stemp = stemp.TrimEnd('0');

  return stemp.Length - 1 - stemp.IndexOf(
         Application.CurrentCulture.NumberFormat.NumberDecimalSeparator);
}

อย่าลืมใช้ System.Windows.Forms เพื่อเข้าถึง Application.CurrentCulture


2

ฉันเขียนวิธีการสั้น ๆ เมื่อวานนี้ซึ่งส่งคืนจำนวนตำแหน่งทศนิยมโดยไม่ต้องพึ่งพาการแยกสตริงหรือวัฒนธรรมใด ๆ ที่เหมาะ:

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;

@ fix-like-codings คล้ายกับคำตอบที่สองของคุณแม้ว่าสำหรับสิ่งนี้ฉันชอบวิธีการซ้ำมากกว่าการใช้การเรียกซ้ำ
Jesse Carter

โพสต์ต้นฉบับระบุว่า: 19.0 should return 1. โซลูชันนี้จะถือว่าทศนิยม 1 ตำแหน่งน้อยที่สุดและละเว้นเลขศูนย์ต่อท้าย ทศนิยมสามารถมีได้เนื่องจากใช้ตัวคูณมาตราส่วน สามารถเข้าถึงตัวคูณมาตราส่วนได้เช่นเดียวกับไบต์ 16-24 ขององค์ประกอบที่มีดัชนี 3 ในอาร์เรย์ที่ได้รับจากDecimal.GetBytes()หรือโดยใช้ตรรกะตัวชี้
Martin Tilo Schmitz

1

ฉันใช้กลไกต่อไปนี้ในรหัสของฉัน

  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;
    }

อินพุตทศนิยม = 3.376; var instring = input ToString ();

โทร GetDecimalLength (instring)


1
สิ่งนี้ใช้ไม่ได้สำหรับฉันเนื่องจากการแทนค่า ToString () ของค่าทศนิยมจะเพิ่ม "00" ลงในส่วนท้ายของข้อมูลของฉัน - ฉันใช้ประเภทข้อมูลทศนิยม (12,4) จาก SQL Server
PeterX

คุณสามารถแคสต์ข้อมูลของคุณเป็นทศนิยมประเภท c # และลองวิธีแก้ปัญหา สำหรับฉันเมื่อฉันใช้ Tostring () กับค่าทศนิยม c # ฉันไม่เคยเห็น "00"
Srikanth

1

การใช้การเรียกซ้ำคุณสามารถทำได้:

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

โพสต์ต้นฉบับระบุว่า: 19.0 should return 1. โซลูชันนี้จะละเว้นศูนย์ต่อท้าย ทศนิยมสามารถมีได้เนื่องจากใช้ตัวคูณมาตราส่วน สามารถเข้าถึงสเกลแฟคเตอร์ได้เช่นเดียวกับไบต์ 16-24 ขององค์ประกอบที่มีดัชนี 3 ในDecimal.GetBytes()อาร์เรย์หรือโดยใช้ตรรกะตัวชี้
Martin Tilo Schmitz


0

คุณสามารถลอง:

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

7
สิ่งนี้จะไม่ล้มเหลวเมื่อทศนิยมเป็นจำนวนเต็มหรือไม่? [1]
Silvermind

0

ฉันขอแนะนำให้ใช้วิธีนี้:

    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

เป็นวิธีการขยายทศนิยมที่คำนึงถึง:

  • วัฒนธรรมที่แตกต่าง
  • จำนวนทั้งหมด
  • ตัวเลขติดลบ
  • ชุดต่อท้ายเลขศูนย์บนตำแหน่งทศนิยม (เช่น 1.2300M จะส่งคืน 2 ไม่ใช่ 4)
public static class DecimalExtensions
{
    public static int GetNumberDecimalPlaces(this decimal source)
    {
        var parts = source.ToString(CultureInfo.InvariantCulture).Split('.');

        if (parts.Length < 2)
            return 0;

        return parts[1].TrimEnd('0').Length;
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.