Nick
Nick

Reputation: 533

C# decimal (type string) rounded at the last char

This might seem as duplication, but I couldn't find a proper answer (questions were close enough but..) I have a string which represents a decimal number, which always has many decimal places, at least 20, sometimes up to 2000 (represents specific verification calculations, i.e. like 'are digits 135 to 147 prime number X etc. - just to give you some context)

For example:

123.829743892473218762329384241002373824970132871283923423961723816273823447623528347662123874999

I am trying to do roundation to penultimate digit. I built a small method which (kinda) works. BUT (as in the example above).. if the last digit is >4 and penultimate digit is 9 this means that I must raise also the previous digit, and cut the last 0 and if the-then previous number is also 9 that means that I must raise also the previous digit, and so on and so forth.

Ex.

123.99999 // must become 124
123.823467283762378  // must become 123.82346728376238
123.823467283762398  // must become 123.8234672837624 (notice the last 0 has gone)
123.09999999 // must become 123.1 (notice no trailing zeros needed..)
122.00000009 // must become 122.0000001
124.81379281 // must become simply 124.8137928
129.07872345 // must become 129.0787235
129.07872344 // must become 129.0787234

and so on and so forth. In other words, it is just roundation of the number (which is a string!!) by cutting only the last digit, but continue the roundation forward to the left until is not needed. Roundation is needed only for the last decimal place (not if number is integer) and the rule is that if digit at last place is >4 then digit is cut and previous (to the left) digit is being raised by 1, ignoring trailing zero if any, and the rule continues up to the last digit of the integer part (i.e. 123.99999 becomes 124, but integer 124 stays as is etc.).

Could someone help me build a string extension for this?

using System;
    public class Example
    {
        public string round(string LargeDecimal)
        {
            Console.WriteLine("Number as string is: " + LargeDecimal);
            
            int lastDigit = (int)char.GetNumericValue(LargeDecimal[LargeDecimal.Length -1]);   // get last character
            Console.WriteLine("lastDigit = " + lastDigit.ToString());
            
            string number = LargeDecimal.Remove(LargeDecimal.Length - 1);  // delete last character
            Console.WriteLine("Now number is " + number);
            
            if (lastDigit > 4)
            {
                Console.WriteLine("Last digit {0} was >4", lastDigit.ToString());
                int newLastDigit = (int)char.GetNumericValue(number[number.Length -1]);
                Console.WriteLine("Next to left last digit is {0} which will be raised by 1 and become {1}", newLastDigit.ToString(), (newLastDigit +1).ToString());
                newLastDigit += 1;                             //increase by one
                number = number.Remove(number.Length - 1);  // delete ex-penultimate (and now last) character
                number = number + newLastDigit.ToString(); 
                return number;       // and add a digit increased by 1
            }
            else
            {
                return LargeDecimal.Remove(LargeDecimal.Length - 2);
            }
        }
        public Example()
        {}
    }

public class Program
{
    public static void Main(string[] args)
    {
        string myNumber = "124.2398478278268985738276523548769";

        Example myExample = new Example();
    
        string result = myExample.round(myNumber);

        Console.WriteLine("Now I have " + result);
    }
}

Upvotes: 3

Views: 299

Answers (3)

Tim Schmelter
Tim Schmelter

Reputation: 460148

Well, this is tricky because with 2000 decimal places you can't use decimal. So maybe there's something easier than this, but it might work for you(.NET fiddle demo):

public static string RoundLongNumber(this string input)
{
    if (!ValidLongNumber(input, out bool isInteger) || isInteger) return input;
    int index = input.IndexOf('.');
    string part1 = input.Remove(index);
    string part2 = input.Substring(index + 1);
    StringBuilder sb = new StringBuilder(part2);

    while(true)
    {
        if (LastInt() <= 4)
        {
            return BuildNumber();
        }

        sb.Length = sb.Length - 1; // remove last
        int lastInt = LastInt() + 1;
        while (lastInt == 10)
        {
            sb.Length = sb.Length - 1;
            if (sb.Length == 0)
            {
                // just integer remaining
                int num = int.Parse(part1);
                return (++num).ToString();
            }
            lastInt = LastInt() + 1;
        }

        sb[sb.Length - 1] = (char)(lastInt + '0');
        if (lastInt != 9) return BuildNumber();
    }

    int LastInt() => sb[sb.Length - 1] - '0';
    string BuildNumber() => part1 + "." + sb.ToString();
}

private static bool ValidLongNumber(string number, out bool isInteger)
{
    isInteger = true;
    if (string.IsNullOrWhiteSpace(number)) return false;
    int pointCount = 0;
    foreach(char c in number)
    {
        bool isDigit = char.IsDigit(c);
        bool isPoint = c == '.';
        if (!isDigit) isInteger = false;
        if (isPoint) pointCount++;
        if(pointCount > 1 || (!isPoint && !isDigit)) return false;
    }
    return true;
}

Here's your sample:

public static void Main()
{
    var strings = new List<string> {
    "123.99999", // must become 124
    "129.99999", // must become 130
    "123.823467283762378",  // must become 123.82346728376238
    "123.823467283762398",  // must become 123.8234672837624 (notice the last 0 has gone)
    "123.09999999", // must become 123.1 (notice no trailing zeros needed..)
    "122.00000009", // must become 122.0000001
    "124.81379281", // must become simply 124.8137928 >>> Why? Should remain same
    "129.07872345", // must become 129.0787235
    "129.07872344", // must become 129.0787234        >>> Why? Should remain same
    };

    IEnumerable<string> results = strings.Select(s => s.RoundLongNumber());
    foreach(var res in results)
    {
        Console.WriteLine(res);
    }
}

Note that two results are different, but either you haven't explained that rule or your expectation was wrong:

124.81379281 // must become simply 124.8137928
129.07872344 // must become 129.0787234

Why? For my understanding both should remain same.

Upvotes: 1

D-Shih
D-Shih

Reputation: 46219

Because your input value is a string type, I would use decimal.TryParse to make sure the input is valid decimal number.

Then you can try to use a simple algorithm to calculate float Length from your input then do Math.Round

static decimal RoundFirstSignificantDigit(string input) {
    
    decimal significantDigit;
    if (!decimal.TryParse(input,out significantDigit))
    {
        throw new ArgumentException("Invalid input!!");
    }
    var floatLength = input.Split('.')[1].Length;

    return Math.Round(significantDigit, floatLength - 1, MidpointRounding.AwayFromZero);
}

c# online

Edit

if your input with big decimals, due to c# decimal only allow the range between ±1.0 × 10-28 to ±7.9228 × 1028 & didn't support BigDecimal like java currently.

I think there are two ways to do that.

  1. Making your own BigDecimal round logic.
  2. You can try to use IKVM library which supports you to call BigDecimal from java.

You can make your expectation by IKVM simply.

static string RoundFirstSignificantDigit(string input) {
    BigDecimal significantDigit = new BigDecimal(input);
    var floatLength = input.Split('.')[1].Length;
    return significantDigit.setScale(floatLength - 1, BigDecimal.ROUND_HALF_UP).toString().TrimEnd('0');
}

c# online

Upvotes: 1

L&#233;o SALVADOR
L&#233;o SALVADOR

Reputation: 764

Here is a solution:

    public static decimal RoundLastChar(this string input)
    {
        decimal inputDecimal = Convert.ToDecimal(input, new CultureInfo("en-US"));
        int decimalPlaces = BitConverter.GetBytes(decimal.GetBits(inputDecimal)[3])[2];
        if (decimalPlaces == 0) return inputDecimal;
        decimal result = Math.Round(inputDecimal, decimalPlaces - 1, MidpointRounding.AwayFromZero).Normalize();
        return result;
    }

    public static decimal Normalize(this decimal value)
    {
        return value / 1.000000000000000000000000000000000m;
    }

The method to get the number of decimal places comes frome here, and the method to Normalize a decimal comes from here.

Upvotes: 1

Related Questions