views:

371

answers:

5

Background

I'm working on a symmetric rounding class and I find that I'm stuck with regards to how to best find the number at position x that I will be rounding. I'm sure there is an efficient mathematical way to find the single digit and return it without having to resort to string parsing.

Problem

Suppose, I have the following (C#) psuedo-code:

var position = 3;
var value = 102.43587m;
// I want this no ↑ (that is 5)

protected static int FindNDigit(decimal value, int position)
{
    // This snippet is what I am searching for
}

Also, it is worth noting that if my value is a whole number, I will need to return a zero for the result of FindNDigit.

Does anyone have any hints on how I should approach this problem? Is this something that is blaringly obvious that I'm missing?

+25  A: 
(int)(value * Math.Pow(10, position)) % 10
Andrey
+1, beat me by seconds :)
Justin Ethier
You should strip off the digits to the left of "position" first to avoid unnecessary overflows.
mbeckish
@mbeckish it is enough for simple cases. for complex you have to do it in loop, multiply and strip
Andrey
Math.Pow() returns the type double and that upsets the compiler a bit. Besides, casting decimal to an int is really bad idea. Aren't You missing a pair of parenthesis?
Maciej Hehl
@mbeckish, Andrey: Would it not also be possible to just ignore overflow in this case? As long as you aren't modifying `value` itself, there's no reason you'd need to worry about the lost data there. @Maciej Hehl: How does this look? `decimal.Floor(value * Math.Pow(10, position)) % 10`
JAB
@Maciej - the cast to int gets rid of any extra numbers to the right of the decimal point after shifting; they're not needed.
SethO
@SethO Well, it does if an overflow doesn't happen and if You take the range of values, the decimal type can store and assume a uniform distribution of values (I know, usually it's not uniform, but we know nothing about it) the probability of the overflow not happening is very very low.
Maciej Hehl
+21  A: 

How about:

(int)(value * Math.Pow(10, position)) % 10

Basically you multiply by 10 ^ pos in order to move that digit to the one's place, and then you use the modulus operator % to divide out the rest of the number.

Justin Ethier
+1 for including an explanation :). I'm sure the code would include this as a comment.
hypoxide
I like how both simple answers are EXACTLY the same. Creepy.
Rubys
A: 

How about this:

protected static int FindNDigit(decimal value, int position)
{
    var index = value.ToString().IndexOf(".");
    position = position + index;
    return (int)Char.GetNumericValue(value.ToString(), position);
}
Paul Kearney - pk
Good effort, but he said that he did not want to resort to string parsing...
Justin Ethier
Yep. Missed that constraint. :(
Paul Kearney - pk
+2  A: 

Edited: Totally had the wrong and opposite answer here. I was calculating the position to the left of the decimal instead of the right. See the upvoted answers for the correct code.

JasDev
yeah, did you try to run it? remainder (%) doesn't work with double.
Andrey
My mistake. I quickly ran it in VS2010 interactive and used integer instead of decimal. I'll see if Math.DivRem() will work.
JasDev
+3  A: 
using System;

public static class DecimalExtensions
{
    public static int DigitAtPosition(this decimal number, int position)
    {
        if (position <= 0)
        {
            throw new ArgumentException("Position must be positive.");
        }

        if (number < 0)
        {
            number = Math.Abs(number);
        }

        number -= Math.Floor(number);

        if (number == 0)
        {
            return 0;
        }

        if (position == 1)
        {
            return (int)(number * 10);
        }

        return (number * 10).DigitAtPosition(position - 1);
    }
}

Edit: If you wish, you may separate the recursive call from the initial call, to remove the initial conditional checks during recursion:

using System;

public static class DecimalExtensions
{
    public static int DigitAtPosition(this decimal number, int position)
    {
        if (position <= 0)
        {
            throw new ArgumentException("Position must be positive.");
        }

        if (number < 0)
        {
            number = Math.Abs(number);
        }

        return number.digitAtPosition(position);
    }

    static int digitAtPosition(this decimal sanitizedNumber, int validPosition)
    {
        sanitizedNumber -= Math.Floor(sanitizedNumber);

        if (sanitizedNumber == 0)
        {
            return 0;
        }

        if (validPosition == 1)
        {
            return (int)(sanitizedNumber * 10);
        }

        return (sanitizedNumber * 10).digitAtPosition(validPosition - 1);
    }

Here's a few tests:

using System;
using Xunit;

public class DecimalExtensionsTests
{
                         // digit positions
                         // 1234567890123456789012345678
    const decimal number = .3216879846541681986310378765m;

    [Fact]
    public void Throws_ArgumentException_if_position_is_zero()
    {
        Assert.Throws<ArgumentException>(() => number.DigitAtPosition(0));
    }

    [Fact]
    public void Throws_ArgumentException_if_position_is_negative()
    {
        Assert.Throws<ArgumentException>(() => number.DigitAtPosition(-5));
    }

    [Fact]
    public void Works_for_1st_digit()
    {
        Assert.Equal(3, number.DigitAtPosition(1));
    }

    [Fact]
    public void Works_for_28th_digit()
    {
        Assert.Equal(5, number.DigitAtPosition(28));
    }

    [Fact]
    public void Works_for_negative_decimals()
    {
        const decimal negativeNumber = -number;
        Assert.Equal(5, negativeNumber.DigitAtPosition(28));
    }

    [Fact]
    public void Returns_zero_for_whole_numbers()
    {
        const decimal wholeNumber = decimal.MaxValue;
        Assert.Equal(0, wholeNumber.DigitAtPosition(1));
    }

    [Fact]
    public void Returns_zero_if_position_is_greater_than_the_number_of_decimal_digits()
    {
        Assert.Equal(0, number.DigitAtPosition(29));
    }

    [Fact]
    public void Does_not_throw_if_number_is_max_decimal_value()
    {
        Assert.DoesNotThrow(() => decimal.MaxValue.DigitAtPosition(1));
    }

    [Fact]
    public void Does_not_throw_if_number_is_min_decimal_value()
    {
        Assert.DoesNotThrow(() => decimal.MinValue.DigitAtPosition(1));
    }

    [Fact]
    public void Does_not_throw_if_position_is_max_integer_value()
    {
        Assert.DoesNotThrow(() => number.DigitAtPosition(int.MaxValue));
    }
}
Sam Pearson
What does this solution cover that the others do not? Does it deal with edge cases?
Elijah
Yes, it should deal with all edge cases. The most upvoted answer deals with too few values before overflowing, IMO.
Sam Pearson
(not to mention it doesn't compile)
Sam Pearson
Accepted because this solution deals with real-world edge cases.
Elijah