Усечь два десятичных разряда без округления


109

Допустим, у меня есть значение 3,4679, а мне нужно 3,46, как я могу усечь его до двух десятичных знаков без округления в большую сторону?

Я пробовал следующее, но все три дали мне 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));
}

Это возвращает 3.46, но почему-то кажется грязным:

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

Ответы:


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

Помните, что такие дроби не могут быть точно представлены с плавающей запятой.


13
Используйте десятичные числа для своих значений, и этот ответ будет работать. Вряд ли он всегда будет работать в любом представлении с плавающей запятой.
driis

1
Это заставляет меня задуматься, можно ли указать направление округления в литералах с плавающей запятой. Хмммм.
Steve314,

Должен быть какой-то способ сказать программисту, что вычисления с предположением, что число может хранить более 308 цифр, совершенно неуместны. Дабл может хранить только 15. Переполнение здесь очень характерно, оно довольно сильно переполняется.
Ханс Пассан

Извините, я думал, что "значение" десятичное.
nightcoder

54

Было бы более полезно иметь полную функцию для реального использования усечения десятичной дроби в C #. Его можно довольно легко преобразовать в метод расширения Decimal, если хотите:

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

Если вам нужен VB.NET, попробуйте следующее:

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

Тогда используйте это так:

decimal result = TruncateDecimal(0.275, 2);

или

Dim result As Decimal = TruncateDecimal(0.275, 2)

1
Это приведет к переполнению больших чисел.
nightcoder

1
Чтобы добавить к ночному кодеру, тот факт, что вы используете Int32 в качестве посредника в своей функции, вызовет переполнение. Вы должны использовать Int64, если вам действительно нужно преобразовать его в Integer. Возникает вопрос, почему вы все равно хотите нести эти дополнительные накладные расходы, поскольку Truncate все равно возвращает десятичные интегралы. Просто сделайте что-нибудь вроде: decimal step = (decimal) Math.Pow (10, precision); return Math.Truncate (шаг * значение) / шаг;
Sarel Esterhuizen

Я сбросил приведение к Integer. Я оставил им отдельные строки для лучшей читаемости и понимания того, как работает функция.
Corgalore

27

Используйте оператор модуля:

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

результат: 0.54


1
Я не понимаю (читай: не тратил время на проверку) всех этих других модных решений, это именно то , что я искал. Спасибо!
Isaac Baker

Запуск этого на .Net Fiddle вызывает щелчки0.5400 ... Ответ Д. Нестерова ниже дал ожидаемый 0.54.
ttugates

Вы ведь понимаете, @ttugates, что 0,54 и 0,5400 - это одно и то же значение, верно? Неважно, сколько нулей следует за ним, если / пока не придет время форматировать для отображения - в этом случае результат будет таким же, если отформатирован правильно: $"{0.54m:C}"производит "$0.54"и да, $"{0.5400m:C}"производит "$0.54".
Леонард Льюис,

26

Универсальный и быстрый метод (без Math.Pow()/ умножения) для 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
Я провел это через все тесты, упомянутые в других ответах, и он отлично работает. Удивлен, что не набирает больше голосов. Стоит отметить, что десятичные дроби могут быть только от 0 до 28 (для большинства людей это нормально).
RichardOD

1
Я поддерживаю это. Это лучший ответ. +1
Бранко Димитриевич

1
Отличный ответ, это то, что я называю «мыслить нестандартно»
bruno.almeida

23

Одна проблема с другими примерами заключается в том, что они умножают входное значение перед его делением. Здесь есть крайний случай, когда вы можете переполнить десятичное число, сначала умножив его, крайний случай, но кое-что, с чем я столкнулся. С дробной частью безопаснее разбираться отдельно следующим образом:

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

Я знаю, что это старый, но я заметил и проблема с этим. Фактор, который у вас есть, - это int, поэтому, если вы выполняете усечение до большого количества десятичных знаков (скажем, 25), это приведет к тому, что конечный результат будет иметь ошибку точности. Я исправил это, изменив тип множителя на десятичный.
TheKingDave,

@TheKingDave: вероятно, это не имеет значения, но, поскольку фактор не может иметь десятичные дроби, лучше моделировать его так долго, верно?
Игнасио Солер Гарсия

@SoMoS Для меня Decimal работал лучше, потому что он дал мне самые высокие значения хранения для фактора. У него все еще есть ограничение, но оно достаточно велико для моего приложения. С другой стороны, Лонг не мог хранить достаточно большие числа для моего приложения. Например, если вы выполняете Truncate (25) с long, будет некоторая неточность.
TheKingDave

Обновлено, чтобы разрешить усечение до большего количества мест в соответствии с предложением @TheKingDave, спасибо.
Тим Ллойд

6

Я оставлю решение для десятичных чисел.

Некоторые из представленных здесь решений для десятичных дробей склонны к переполнению (если мы передадим очень большое десятичное число, и метод попытается его умножить).

Решение Тима Ллойда защищено от переполнения, но не слишком быстро.

Следующее решение примерно в 2 раза быстрее и не имеет проблем с переполнением:

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
Я не люблю добавлять к нему суффикс Ex. C # поддерживает перегрузку, ваш Truncateметод будет объединен с собственными .net, что дает пользователю беспроблемный опыт.
Gqqnbig

1
Ваш алгоритм дает неверные результаты. По умолчанию режим MidpointRounding - Banker's Rounding, который округляет 0,5 до ближайшего четного значения. Assert.AreEqual(1.1m, 1.12m.TruncateEx(1));выходит из строя из-за этого. Если вы укажете «нормальное» округление (AwayFromZero) в вызове Math.Round, то Assert.AreEqual(0m, 0m.TruncateEx(1));не удастся
Джон Сенчина

1
Единственный способ, которым это решение будет работать, - это если вы используете MidpointRounding.AwayFromZeroспециальный код для обработки значения 0.
Джон Сенчина,

1
Джон прав: 0m.TruncateEx (0) приводит к -1, если 0 не обрабатывается явно. Аналогично -11m.TruncateEx (0) приводит к -10, если в Math.Round не используется MidpointRounding.AwayFromZero. Хотя, похоже, хорошо работает с этими модификациями.
Хо Хо Хо

1
Даже с изменениями для AwayFromZero и явной обработкой 0, -9999999999999999999999999999m.TruncateEx (0) приводит к -9999999999999999999999999998, поэтому в некоторых случаях он все еще подвержен ошибкам.
Хо Хо Хо

3

Это старый вопрос, но многие ответы не работают или переполняются для больших чисел. Я считаю, что ответ Д. Нестерова самый лучший: надежный, простой и быстрый. Я просто хочу добавить свои два цента. Я поигрался с десятичными знаками, а также проверил исходный код . Из public Decimal (int lo, int mid, int hi, bool isNegative, byte scale) документации конструктора .

Двоичное представление десятичного числа состоит из 1-битового знака, 96-битного целого числа и коэффициента масштабирования, используемого для деления целого числа и указания того, какая его часть является десятичной дробью. Коэффициент масштабирования неявно представляет собой число 10, возведенное в степень в диапазоне от 0 до 28.

Зная это, мой первый подход состоял в том, чтобы создать другой decimal, масштаб которого соответствует десятичным дробям, которые я хотел отбросить, затем усечь его и, наконец, создать десятичную дробь с желаемым масштабом.

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

Этот метод не быстрее, чем у Д. Нестерова, и он сложнее, поэтому я еще немного поигрался. Я предполагаю, что необходимость создавать вспомогательный объект decimalи дважды извлекать биты делает его медленнее. Во второй попытке я сам манипулировал компонентами, возвращаемыми методом Decimal.GetBits (Decimal d) . Идея состоит в том, чтобы разделить компоненты на 10 столько раз, сколько необходимо, и уменьшить масштаб. Код основан (в значительной степени) на методе 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;
    }

Я не проводил строгих тестов производительности, но на MacOS Sierra 10.12.6, процессоре Intel Core i3 3,06 ГГц и таргетинге на .NetCore 2.1 этот метод кажется намного быстрее, чем метод Д. Нестерова (я не буду приводить цифры, поскольку , как я уже упоминал, мои тесты не являются строгими). Тот, кто реализует это, должен оценить, окупается ли прирост производительности добавленной сложностью кода.


Мне пришлось проголосовать за все мысли и усилия. Вы поставили точку отсчета Нестерова и продолжаете идти - снимая шляпу.
AndrewBenjamin



1

Вот способ расширения:

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

Если вы не слишком беспокоитесь о производительности и конечным результатом может быть строка, следующий подход будет устойчивым к проблемам с плавающей точностью:

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
Собственно, жесткое кодирование '.' не очень хорошая идея, лучше используйте System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator [0]
Давид Айрапетян

0

Вот моя реализация функции TRUNC

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

как насчет этого?

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

Помимо вышеперечисленных решений, мы можем добиться еще одним способом.

    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

В некоторых случаях этого может быть достаточно.

У меня было десятичное значение SubCent = 0,00999999999999999999999999M, которое имеет тенденцию форматироваться в | SubCent: 0,010000 | via string.Format("{0:N6}", SubCent );и многие другие варианты форматирования.

Моим требованием было не округлять значение SubCent, но и не регистрировать каждую цифру.

Следующее отвечало моим требованиям:

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

Что возвращает строку: | SubCent: 0.0099999 |

Чтобы учесть значение, имеющее целую часть, начнем с того, что нужно.

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

Что возвращает строку:

valFmt4 = "567890.00999999"

0

я использую эту функцию для усечения значения после десятичного числа в строковой переменной

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
Хотя этот код может ответить на вопрос, предоставление дополнительного контекста относительно того, как и / или почему он решает проблему, улучшит долгосрочную ценность ответа.
Nic3500 08

0

Вот что я сделал:

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

вычитание двух введенных чисел без округления десятичных знаков в большую или меньшую сторону. потому что другие решения мне не подходят. не знаю, сработает ли это для других, я просто хочу поделиться этим :) Надеюсь, это сработает для тех, кто находит решение проблемы, похожей на мою. Спасибо

PS: я новичок, поэтому не стесняйтесь указывать на что-нибудь по этому поводу. : D это хорошо, если вы действительно имеете дело с деньгами, из-за центов, не так ли? у него только 2 десятичных знака, а округление - нет.


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

На самом деле вы хотите 3.46 из 3.4679. Это всего лишь представление символов. Так что здесь нет ничего общего с математической функцией. Математическая функция не предназначена для этой работы. Просто используйте следующий код.

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

Приведенный выше код можно изменить для любых чисел. Поместите следующий код в событие нажатия кнопки.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.