Obetnij Dwa miejsca po przecinku bez zaokrąglania


108

Powiedzmy, że mam wartość 3,4679 i chcę 3,46, jak mogę to skrócić do dwóch miejsc po przecinku bez zaokrąglania w górę?

Próbowałem następujących rzeczy, ale wszystkie trzy dają mi 3,47:

void Main()
{
    Console.Write(Math.Round(3.4679, 2,MidpointRounding.ToEven));
    Console.Write(Math.Round(3.4679, 2,MidpointRounding.AwayFromZero));
    Console.Write(Math.Round(3.4679, 2));
}

Zwraca to 3.46, ale wydaje się po prostu brudne:

void Main()
{
    Console.Write(Math.Round(3.46799999999 -.005 , 2));
}
c#  math  rounding 

Odpowiedzi:


152
value = Math.Truncate(100 * value) / 100;

Uważaj, że takie ułamki nie mogą być dokładnie reprezentowane w postaci zmiennoprzecinkowej.


13
Użyj wartości dziesiętnych, a ta odpowiedź zadziała. Jest mało prawdopodobne, aby zawsze działał w jakiejkolwiek reprezentacji zmiennoprzecinkowej.
driis

1
To sprawia, że ​​zastanawiam się, czy powinno być możliwe określenie kierunku zaokrąglania w literałach zmiennoprzecinkowych. Hmmmm.
Steve314

Musi być jakiś sposób, aby powiedzieć programiście, że obliczenia przy założeniu, że liczba może przechowywać więcej niż 308 cyfr, jest rażąco niewłaściwe. Double może przechowywać tylko 15. Przepełnienie jest tutaj bardzo ważną cechą, raczej źle się przepełniło.
Hans Passant

Przepraszam, myślałem, że „wartość” jest dziesiętna.
nightcoder

54

Bardziej przydatne byłoby posiadanie pełnej funkcji obcięcia ułamka dziesiętnego w języku C # w świecie rzeczywistym. Można to łatwo przekonwertować na metodę rozszerzenia Decimal, jeśli chcesz:

public decimal TruncateDecimal(decimal value, int precision)
{
    decimal step = (decimal)Math.Pow(10, precision);
    decimal tmp = Math.Truncate(step * value);
    return tmp / step;
}

Jeśli potrzebujesz VB.NET, spróbuj tego:

Function TruncateDecimal(value As Decimal, precision As Integer) As Decimal
    Dim stepper As Decimal = Math.Pow(10, precision)
    Dim tmp As Decimal = Math.Truncate(stepper * value)
    Return tmp / stepper
End Function

Następnie użyj tego w ten sposób:

decimal result = TruncateDecimal(0.275, 2);

lub

Dim result As Decimal = TruncateDecimal(0.275, 2)

1
Spowoduje to przepełnienie w przypadku dużych liczb.
nightcoder

1
Aby dodać do nocnego kodera, fakt, że używasz Int32 jako pośrednika w swojej funkcji, spowoduje przepełnienia. Powinieneś użyć Int64, jeśli naprawdę musisz rzutować go na Integer. Powstaje pytanie, dlaczego i tak chcesz ponieść ten dodatkowy narzut, skoro Truncate i tak zwraca całki dziesiętne. Po prostu zrób coś takiego: decimal step = (decimal) Math.Pow (10, precyzja); return Math.Truncate (krok * wartość) / krok;
Sarel Esterhuizen

Rzuciłem obsadę na Integer. Zostawiłem je w osobnych wierszach dla lepszej czytelności i zrozumienia działania tej funkcji.
Corgalore

27

Użyj operatora modułu:

var fourPlaces = 0.5485M;
var twoPlaces = fourPlaces - (fourPlaces % 0.01M);

wynik: 0,54


1
Nie rozumiem (czytaj: nie spędziłem czasu na weryfikowaniu) wszystkich innych wymyślnych rozwiązań, to robi dokładnie to , czego szukałem. Dziękuję Ci!
Isaac Baker

Uruchomienie tego na .Net Fiddle clicky daje 0.5400... Odpowiedź D. Nesterova poniżej dała oczekiwane 0.54.
ttugates

Zdajesz sobie sprawę, @ttugates, że 0,54 i 0,5400 to dokładnie ta sama wartość, prawda? Nie ma znaczenia, ile zer nastąpi, chyba że / dopóki nie nadejdzie czas na formatowanie do wyświetlenia - w takim przypadku wynik będzie taki sam, jeśli zostanie poprawnie sformatowany: $"{0.54m:C}"produkuje "$0.54"i tak, $"{0.5400m:C}"produkuje "$0.54".
Leonard Lewis

25

Uniwersalna i szybka metoda (bez Math.Pow()/ mnożenia) dla System.Decimal:

decimal Truncate(decimal d, byte decimals)
{
    decimal r = Math.Round(d, decimals);

    if (d > 0 && r > d)
    {
        return r - new decimal(1, 0, 0, false, decimals);
    }
    else if (d < 0 && r < d)
    {
        return r + new decimal(1, 0, 0, false, decimals);
    }

    return r;
}

4
Przeprowadziłem to przez wszystkie testy wymienione w innych odpowiedziach i działa idealnie. Zaskoczony, że nie ma więcej pozytywnych głosów. Warto zauważyć, że liczby dziesiętne mogą wynosić tylko od 0 do 28 (prawdopodobnie OK dla większości ludzi).
RichardOD

1
Popieram to. To najlepsza odpowiedź. +1
Branko Dimitrijevic,

1
Świetna odpowiedź, to właśnie nazywam „myśl nieszablonową”
bruno.almeida

23

Problem z innymi przykładami polega na tym, że mnożą one wartość wejściową przed jej podzieleniem. Jest tu przypadek skrajny, w którym można przepełnić dziesiętne, mnożąc najpierw, przypadek krawędziowy, ale coś, z czym się spotkałem. Bezpieczniej jest osobno zająć się częścią ułamkową w następujący sposób:

    public static decimal TruncateDecimal(this decimal value, int decimalPlaces)
    {
        decimal integralValue = Math.Truncate(value);

        decimal fraction = value - integralValue;

        decimal factor = (decimal)Math.Pow(10, decimalPlaces);

        decimal truncatedFraction = Math.Truncate(fraction * factor) / factor;

        decimal result = integralValue + truncatedFraction;

        return result;
    }

Wiem, że to jest stare, ale zauważyłem i problem z tym. Czynnikiem, który tu masz, jest liczba int, więc jeśli skracasz do dużej liczby miejsc dziesiętnych (powiedzmy 25), spowoduje to, że wynik końcowy będzie miał błąd dokładności. Naprawiłem to, zmieniając typ czynnika na dziesiętny.
TheKingDave

@TheKingDave: prawdopodobnie nie ma to znaczenia, ale ponieważ czynnik nie może mieć ułamków dziesiętnych, lepiej byłoby go zamodelować tak długo, prawda?
Ignacio Soler Garcia

@SoMoS Dla mnie Decimal działał lepiej, ponieważ dał mi najwyższe wartości pamięci dla współczynnika. Nadal ma ograniczenie, ale jest wystarczająco duże dla mojej aplikacji. Z drugiej strony Long nie był w stanie przechowywać wystarczająco dużych liczb dla mojej aplikacji. Na przykład, jeśli wykonasz Truncate (25) z long, wystąpią pewne niedokładności.
TheKingDave

Zaktualizowano, aby umożliwić obcinanie do większej liczby miejsc zgodnie z sugestią @TheKingDave, dzięki.
Tim Lloyd,

6

Zostawię rozwiązanie dla liczb dziesiętnych.

Niektóre rozwiązania dotyczące liczb dziesiętnych są tutaj podatne na przepełnienie (jeśli podamy bardzo dużą liczbę dziesiętną, a metoda spróbuje ją pomnożyć).

Rozwiązanie Tima Lloyda jest chronione przed przepełnieniem, ale nie jest zbyt szybkie.

Poniższe rozwiązanie jest około 2 razy szybsze i nie ma problemu z przepełnieniem:

public static class DecimalExtensions
{
    public static decimal TruncateEx(this decimal value, int decimalPlaces)
    {
        if (decimalPlaces < 0)
            throw new ArgumentException("decimalPlaces must be greater than or equal to 0.");

        var modifier = Convert.ToDecimal(0.5 / Math.Pow(10, decimalPlaces));
        return Math.Round(value >= 0 ? value - modifier : value + modifier, decimalPlaces);
    }
}

[Test]
public void FastDecimalTruncateTest()
{
    Assert.AreEqual(-1.12m, -1.129m. TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.120m. TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.125m. TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.1255m.TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.1254m.TruncateEx(2));
    Assert.AreEqual(0m,      0.0001m.TruncateEx(3));
    Assert.AreEqual(0m,     -0.0001m.TruncateEx(3));
    Assert.AreEqual(0m,     -0.0000m.TruncateEx(3));
    Assert.AreEqual(0m,      0.0000m.TruncateEx(3));
    Assert.AreEqual(1.1m,    1.12m.  TruncateEx(1));
    Assert.AreEqual(1.1m,    1.15m.  TruncateEx(1));
    Assert.AreEqual(1.1m,    1.19m.  TruncateEx(1));
    Assert.AreEqual(1.1m,    1.111m. TruncateEx(1));
    Assert.AreEqual(1.1m,    1.199m. TruncateEx(1));
    Assert.AreEqual(1.2m,    1.2m.   TruncateEx(1));
    Assert.AreEqual(0.1m,    0.14m.  TruncateEx(1));
    Assert.AreEqual(0,      -0.05m.  TruncateEx(1));
    Assert.AreEqual(0,      -0.049m. TruncateEx(1));
    Assert.AreEqual(0,      -0.051m. TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.14m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.15m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.16m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.19m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.199m. TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.101m. TruncateEx(1));
    Assert.AreEqual(0m,     -0.099m. TruncateEx(1));
    Assert.AreEqual(0m,     -0.001m. TruncateEx(1));
    Assert.AreEqual(1m,      1.99m.  TruncateEx(0));
    Assert.AreEqual(1m,      1.01m.  TruncateEx(0));
    Assert.AreEqual(-1m,    -1.99m.  TruncateEx(0));
    Assert.AreEqual(-1m,    -1.01m.  TruncateEx(0));
}

2
Nie lubię dodawania do niego przyrostka „Ex”. C # obsługuje przeciążanie, twoja Truncatemetoda zostanie zgrupowana razem z natywnymi .net, zapewniając użytkownikowi bezproblemową obsługę .
Gqqnbig

1
Twój algorytm daje nieprawidłowe wyniki. Domyślnym trybem zaokrąglania punktu środkowego jest zaokrąglanie bankiera, które zaokrągla 0,5 do najbliższej parzystej wartości. Assert.AreEqual(1.1m, 1.12m.TruncateEx(1));zawodzi z tego powodu. Jeśli określisz „normalne” zaokrąglanie (AwayFromZero) w wywołaniu Math.Round, to Assert.AreEqual(0m, 0m.TruncateEx(1));zakończy się niepowodzeniem
Jon Senchyna

1
Jedynym sposobem, w jaki to rozwiązanie zadziała, jest użycie MidpointRounding.AwayFromZeroi specjalnie kodu do obsługi wartości 0.
Jon Senchyna

1
Jon jest poprawny: 0m.TruncateEx (0) daje -1, chyba że 0 jest jawnie obsługiwane. Podobnie -11m.TruncateEx (0) daje w wyniku -10, chyba że w Math.Round użyto MidpointRounding.AwayFromZero. Wydaje się jednak, że działa dobrze z tymi modyfikacjami.
Ho Ho Ho

1
Nawet ze zmianami dla AwayFromZero i jawną obsługą 0, -9999999999999999999999999999m, TruncateEx (0) daje -9999999999999999999999999998, więc w niektórych przypadkach jest to nadal zawodne.
Ho Ho Ho

3

To stare pytanie, ale wiele odpowiedzi nie działa dobrze lub przepełnia się przy dużych liczbach. Myślę, że odpowiedź D. Niestierowa jest najlepsza: solidna, prosta i szybka. Chcę tylko dodać moje dwa centy. Bawiłem się liczbami dziesiętnymi, a także sprawdzałem kod źródłowy . Z public Decimal (int lo, int mid, int hi, bool isNegative, byte scale) dokumentacji konstruktora .

Binarna reprezentacja liczby dziesiętnej składa się ze znaku 1-bitowego, 96-bitowej liczby całkowitej oraz współczynnika skalowania używanego do dzielenia liczby całkowitej i określania, która z jej części jest ułamkiem dziesiętnym. Współczynnik skalowania jest niejawnie liczbą 10 podniesioną do wykładnika z zakresu od 0 do 28.

Wiedząc o tym, moje pierwsze podejście polegało na stworzeniu kolejnej, decimalktórej skala odpowiada liczbom dziesiętnym, które chciałem odrzucić, a następnie skrócić ją i ostatecznie utworzyć ułamek dziesiętny z pożądaną skalą.

private const int ScaleMask = 0x00FF0000;
    public static Decimal Truncate(decimal target, byte decimalPlaces)
    {
        var bits = Decimal.GetBits(target);
        var scale = (byte)((bits[3] & (ScaleMask)) >> 16);

        if (scale <= decimalPlaces)
            return target;

        var temporalDecimal = new Decimal(bits[0], bits[1], bits[2], target < 0, (byte)(scale - decimalPlaces));
        temporalDecimal = Math.Truncate(temporalDecimal);

        bits = Decimal.GetBits(temporalDecimal);
        return new Decimal(bits[0], bits[1], bits[2], target < 0, decimalPlaces);
    }

Ta metoda nie jest szybsza niż metoda D. Niestierowa i jest bardziej złożona, więc bawiłem się trochę więcej. Domyślam się, że konieczność stworzenia pomocniczego decimali dwukrotnego pobrania bitów powoduje, że jest wolniejszy. Za drugim razem samodzielnie manipulowałem składnikami zwracanymi przez metodę Decimal.GetBits (Decimal d) . Chodzi o to, aby podzielić składniki 10 razy tyle razy, ile potrzeba i zmniejszyć skalę. Kod jest oparty (w dużym stopniu) na metodzie Decimal.InternalRoundFromZero (ref Decimal d, int decimalCount) .

private const Int32 MaxInt32Scale = 9;
private const int ScaleMask = 0x00FF0000;
    private const int SignMask = unchecked((int)0x80000000);
    // Fast access for 10^n where n is 0-9        
    private static UInt32[] Powers10 = new UInt32[] {
        1,
        10,
        100,
        1000,
        10000,
        100000,
        1000000,
        10000000,
        100000000,
        1000000000
    };

    public static Decimal Truncate(decimal target, byte decimalPlaces)
    {
        var bits = Decimal.GetBits(target);
        int lo = bits[0];
        int mid = bits[1];
        int hi = bits[2];
        int flags = bits[3];

        var scale = (byte)((flags & (ScaleMask)) >> 16);
        int scaleDifference = scale - decimalPlaces;
        if (scaleDifference <= 0)
            return target;

        // Divide the value by 10^scaleDifference
        UInt32 lastDivisor;
        do
        {
            Int32 diffChunk = (scaleDifference > MaxInt32Scale) ? MaxInt32Scale : scaleDifference;
            lastDivisor = Powers10[diffChunk];
            InternalDivRemUInt32(ref lo, ref mid, ref hi, lastDivisor);
            scaleDifference -= diffChunk;
        } while (scaleDifference > 0);


        return new Decimal(lo, mid, hi, (flags & SignMask)!=0, decimalPlaces);
    }
    private static UInt32 InternalDivRemUInt32(ref int lo, ref int mid, ref int hi, UInt32 divisor)
    {
        UInt32 remainder = 0;
        UInt64 n;
        if (hi != 0)
        {
            n = ((UInt32)hi);
            hi = (Int32)((UInt32)(n / divisor));
            remainder = (UInt32)(n % divisor);
        }
        if (mid != 0 || remainder != 0)
        {
            n = ((UInt64)remainder << 32) | (UInt32)mid;
            mid = (Int32)((UInt32)(n / divisor));
            remainder = (UInt32)(n % divisor);
        }
        if (lo != 0 || remainder != 0)
        {
            n = ((UInt64)remainder << 32) | (UInt32)lo;
            lo = (Int32)((UInt32)(n / divisor));
            remainder = (UInt32)(n % divisor);
        }
        return remainder;
    }

Nie przeprowadziłem rygorystycznych testów wydajności, ale na procesorze MacOS Sierra 10.12.6, 3,06 GHz Intel Core i3 i docelowym .NetCore 2.1 ta metoda wydaje się być znacznie szybsza niż D.Nesterov (nie podam liczb, ponieważ jak wspomniałem, moje testy nie są rygorystyczne). Każdy, kto to zaimplementuje, musi ocenić, czy wzrost wydajności opłaca się za dodatkową złożoność kodu.


Musiałem zagłosować za całą tą myślą i wysiłkiem. Ustawiłeś Niestierowa jako punkt odniesienia i kontynuowałeś - czapki z głów.
AndrewBenjamin,

2

czy to zadziała dla Ciebie?

Console.Write(((int)(3.4679999999*100))/100.0);


1

Oto metoda rozszerzenia:

public static decimal? TruncateDecimalPlaces(this decimal? value, int places)
    {
        if (value == null)
        {
            return null;
        }

        return Math.Floor((decimal)value * (decimal)Math.Pow(10, places)) / (decimal)Math.Pow(10, places);

    } // end

0

Jeśli nie martwisz się zbytnio o wydajność, a wynikiem końcowym może być ciąg, następujące podejście będzie odporne na problemy z płynną precyzją:

string Truncate(double value, int precision)
{
    if (precision < 0)
    {
        throw new ArgumentOutOfRangeException("Precision cannot be less than zero");
    }

    string result = value.ToString();

    int dot = result.IndexOf('.');
    if (dot < 0)
    {
        return result;
    }

    int newLength = dot + precision + 1;

    if (newLength == dot + 1)
    {
        newLength--;
    }

    if (newLength > result.Length)
    {
        newLength = result.Length;
    }

    return result.Substring(0, newLength);
}

6
Właściwie to „na sztywno”. Nie jest to dobry pomysł, lepsze wykorzystanie System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator [0]
David Airapetyan

0

Oto moja implementacja funkcji LICZBA.CAŁK

private static object Tranc(List<Expression.Expression> p)
{
    var target = (decimal)p[0].Evaluate();

    // check if formula contains only one argument
    var digits = p.Count > 1
        ? (decimal) p[1].Evaluate()
        : 0;

    return Math.Truncate((double)target * Math.Pow(10, (int)digits)) / Math.Pow(10, (int)digits);
}

0

a co z tym?

Function TruncateDecimal2(MyValue As Decimal) As Decimal
        Try
            Return Math.Truncate(100 * MyValue) / 100
        Catch ex As Exception
            Return Math.Round(MyValue, 2)
        End Try
End Function

0

Oprócz powyższych rozwiązań możemy osiągnąć jeszcze jeden sposób.

    decimal val=23.5678m,finalValue;

    //take the decimal part    
     int decimalPos = val.ToString().IndexOf('.');
     string decimalPart = val.ToString().Substring(decimalPosition+1,val.ToString().Length);
    //will result.56
   string wholePart=val.ToString().Substring(0,decimalPos-1);
   //concantinate and parse for decimal.
  string truncatedValue=wholePart+decimalPart;//"23.56"
  bool isDecimal=Decimal.tryParse(truncatedValue,out finalValue);//finalValue=23.56

0

W pewnych warunkach może to wystarczyć.

Miałem dziesiętną wartość SubCent = 0,0099999999999999999999999999M, która ma tendencję do formatowania do | SubCent: 0,010000 | via string.Format("{0:N6}", SubCent );i wiele innych opcji formatowania.

Moim wymaganiem było nie zaokrąglanie wartości SubCent, ale też nie rejestrowanie każdej cyfry.

Następujące spełniły moje wymagania:

string.Format("SubCent:{0}|", 
    SubCent.ToString("N10", CultureInfo.InvariantCulture).Substring(0, 9));

Która zwraca ciąg: | SubCent: 0,0099999 |

Aby pomieścić wartość mającą część całkowitą, poniżej podano początek.

tmpValFmt = 567890.0099999933999229999999M.ToString("0.0000000000000000000000000000");
decPt = tmpValFmt.LastIndexOf(".");
if (decPt < 0) decPt = 0;
valFmt4 = string.Format("{0}", tmpValFmt.Substring(0, decPt + 9));

Która zwraca ciąg:

valFmt4 = "567890.00999999"

0

Używam tej funkcji do obcinania wartości po przecinku w zmiennej ciągu

public static string TruncateFunction(string value)
    {
        if (string.IsNullOrEmpty(value)) return "";
        else
        {
            string[] split = value.Split('.');
            if (split.Length > 0)
            {
                string predecimal = split[0];
                string postdecimal = split[1];
                postdecimal = postdecimal.Length > 6 ? postdecimal.Substring(0, 6) : postdecimal;
                return predecimal + "." + postdecimal;

            }
            else return value;
        }
    }

1
Chociaż ten kod może odpowiedzieć na pytanie, dostarczenie dodatkowego kontekstu dotyczącego tego, jak i / lub dlaczego rozwiązuje problem, poprawiłoby długoterminową wartość odpowiedzi.
Nic3500 08

0

Oto co zrobiłem:

        c1 = a1 - b1;
        d1 = Math.Ceiling(c1 * 100) / 100;

odejmowanie dwóch wprowadzonych liczb bez zaokrąglania miejsc po przecinku w górę lub w dół. bo inne rozwiązania u mnie nie działają. nie wiem, czy to zadziała u innych, po prostu chcę się tym podzielić :) Mam nadzieję, że zadziała jednak dla tych, którzy znajdują rozwiązanie problemu podobnego do mojego. Dzięki

PS: Jestem początkującym, więc śmiało mogę coś na ten temat wskazać. : D to jest dobre, jeśli faktycznie masz do czynienia z pieniędzmi, z powodu centów, prawda? ma tylko 2 miejsca po przecinku, a zaokrąglenie to nie, nie.


0
        public static void ReminderDigints(decimal? number, out decimal? Value,  out decimal? Reminder)
        {
            Reminder = null;
            Value = null;
            if (number.HasValue)
            {
                Value = Math.Floor(number.Value);
                Reminder = (number - Math.Truncate(number.Value));
            }
        }



        decimal? number= 50.55m;             
        ReminderDigints(number, out decimal? Value, out decimal? Reminder);

0
public static decimal TruncateDecimalPlaces(this decimal value, int precision)
    {
        try
        {
            step = (decimal)Math.Pow(10, precision);
            decimal tmp = Math.Truncate(step * value);
            return tmp / step;
        }
        catch (OverflowException)
        {
            step = (decimal)Math.Pow(10, -1 * precision);
            return value - (value % step);
        }
    }

-2

Właściwie chcesz 3,46 od 3,4679. To jest tylko reprezentacja znaków, więc nie ma to nic wspólnego z funkcją matematyczną, funkcja matematyczna nie jest przeznaczona do wykonywania tej pracy. Po prostu użyj poniższego kodu.

Dim str1 As String
str1=""
str1 ="3.4679" 
  Dim substring As String = str1.Substring(0, 3)

    ' Write the results to the screen.
    Console.WriteLine("Substring: {0}", substring)

Or 
    Please use the following code.
Public function result(ByVal x1 As Double) As String 
  Dim i as  Int32
  i=0
  Dim y as String
  y = ""
  For Each ch as Char In x1.ToString
    If i>3 then
     Exit For
    Else
    y + y +ch
    End if
    i=i+1
  Next
  return y
End Function

Powyższy kod można modyfikować dla dowolnych liczb Umieść następujący kod w zdarzeniu kliknięcia przycisku

Dim str As String 
str= result(3.4679)
 MsgBox("The number is " & str)

-2

co powiesz na

var i = Math.Truncate(number);var r = i + Math.Truncate((number - i) * 100) / 100;
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.