Creating a percentage type in C#

c#
10,579

Solution 1

Percentage class should not be concerned with formatting itself for the UI. Rather, implement IFormatProvider and ICustomFormatter to handle formatting logic.

As for conversion, I'd go with standard TypeConverter route, which would allow .NET to handle this class correctly, plus a separate PercentageParser utility class, which would delegate calls to TypeDescriptor to be more usable in external code. In addition, you can provide implicit or explicit conversion operator, if this is required.

And when it comes to Percentage, I don't see any compelling reason to wrap simple decimal into a separate struct other than for semantic expressiveness.

Solution 2

I am actually a little bit flabbergasted at the cavalier attitude toward data quality here. Unfortunately, the colloquial term "percentage" can mean one of two different things: a probability and a variance. The OP doesn't specify which, but since variance is usually calculated, I'm guessing he may mean percentage as a probability or fraction (such as a discount).

The extremely good reason for writing a Percentage class for this purpose has nothing to do with presentation, but with making sure that you prevent those silly silly users from doing things like entering invalid values like -5 and 250.

I'm thinking really more about a Probability class: a numeric type whose valid range is strictly [0,1]. You can encapsulate that rule in ONE place, rather than writing code like this in 37 places:

 public double VeryImportantLibraryMethodNumber37(double consumerProvidedGarbage)
 {
    if (consumerProvidedGarbage < 0 || consumerProvidedGarbage > 1)
      throw new ArgumentOutOfRangeException("Here we go again.");

    return someOtherNumber * consumerProvidedGarbage;
 }

instead you have this nice implementation. No, it's not fantastically obvious improvement, but remember, you're doing that value-checking in each time you're using this value.

 public double VeryImportantLibraryMethodNumber37(Percentage guaranteedCleanData)
 {
    return someOtherNumber * guaranteedCleanData.Value;
 }

Solution 3

It seems like a reasonable thing to do, but I'd reconsider your interface to make it more like other CLR primitive types, e.g. something like.

// all error checking omitted here; you would want range checks etc.
public struct Percentage
{
    public Percentage(decimal value) : this()
    {
        this.Value = value
    }

    public decimal Value { get; private set; }

    public static explicit operator Percentage(decimal d)
    {
        return new Percentage(d);
    }

    public static implicit operator decimal(Percentage p)
    {
        return this.Value;
    }

    public static Percentage Parse(string value)
    {
        return new Percentage(decimal.Parse(value));
    }

    public override string ToString()
    {
        return string.Format("{0}%", this.Value);
    }
}

You'd definitely also want to implement IComparable<T> and IEquatable<T> as well as all the corresponding operators and overrides of Equals, GetHashCode, etc. You'd also probably also want to consider implementing the IConvertible and IFormattable interfaces.

This is a lot of work. The struct is likely to be somewhere in the region of 1000 lines and take a couple of days to do (I know this because it's a similar task to a Money struct I wrote a few months back). If this is of cost-benefit to you, then go for it.

Solution 4

I strongly recommend you just stick with using the double type here (I don't see any use for the decimal type either, as wouldn't actually seem to require base-10 precision in the low decimal places). By creating a Percentage type here, you're really performing unnecessary encapsulation and just making it harder to work with the values in code. If you use a double, which is customary for storying percentages (among many other tasks), you'll find dealing with the BCL and other code a lot nicer in most cases.

The only extra functionality that I can see you need for percentages is the ability to convert to/from a percentage string easily. This can be done very simply anyway using single lines of code, or even extension methods if you want to abstract it slightly.

Converting to percentage string :

public static string ToPercentageString(this double value)
{
    return value.ToString("#0.0%"); // e.g. 76.2%
}

Converting from percentage string :

public static double FromPercentageString(this string value)
{
    return double.Parse(value.SubString(0, value.Length - 1)) / 100;
}

Solution 5

This question reminds me of the Money class Patterns of Enterprise Application Architecture talks about- the link might give you food for thought.

Share:
10,579

Related videos on Youtube

Jack Ryan
Author by

Jack Ryan

Updated on June 04, 2022

Comments

  • Jack Ryan
    Jack Ryan almost 2 years

    My application deals with percentages a lot. These are generally stored in the database in their written form rather than decimal form (50% would be stored as 50 rather than 0.5). There is also the requirement that percentages are formatted consistently throughout the application.

    To this end i have been considering creating a struct called percentage that encapsulates this behaviour. I guess its signature would look something like this:

    public struct Percentage
    {
        public static Percentage FromWrittenValue();
        public static Percentage FromDecimalValue();
    
        public decimal WrittenValue { get; set; }
        public decimal DecimalValue { get; set; }
    }
    

    Is this a reasonable thing to do? It would certianly encapsulate some logic that is repeated many times but it is straightforward logic that peopel are likely to understand. I guess i need to make this type behave like a normal number as much as possible however i am wary of creating implicit conversions to a from decimal in case these confuse people further.

    Any suggestions of how to implement this class? or compelling reasons not to.

  • Jack Ryan
    Jack Ryan almost 15 years
    I appreciate that the logic should be provided by other classes. But to my mind there is still a lot of value in having a percentage class that uses a default ToString method that gives a nice looking percentage string. Especially as this needs to be displayed in multiple presentation layers. (A mixture of Asp.Net, Winforms, and pdf reports).
  • RichardOD
    RichardOD almost 15 years
    If it is the case of needing to convert from strings easily then writing extension methods could be useful.
  • Noldorin
    Noldorin almost 15 years
    @RichardOD: You read my mind. I had just been editing the post to give examples of extension methods.
  • Dovi
    Dovi almost 15 years
    ToString(), as it is done in other .NET classes, can delegate all the work to the IFormatProvider and ICustomFormatter. See DateTime.ToString for an example.
  • Noldorin
    Noldorin almost 15 years
    If he's insistent on taking this approach, it's also worth overloading all the arithmetic and comparison operators.
  • Meydjer Luzzoli
    Meydjer Luzzoli almost 15 years
    @Noldorin - Um, I said "as well as all the corresponding operators and overrides of Equals, GetHashCode, etc" in the context of those interfaces...?
  • Michael Blackburn
    Michael Blackburn over 12 years
    Of course, people who always write perfect code never have to worry about such tosh as semantic expressiveness. They will never again look at their perfect code wondering just what the hell is going on. -- If you'd like a more specific reason, a type for Percentage could be extremely useful, as long as you made it a class which encapsulates a decimal number between 0 and 1 inclusive. This applies very well to probability applications. I'd use this type any time I have to model a number in a bounded range, such as the output of a slider control in the UI or weightings in a neural net.
  • Mister Bee
    Mister Bee about 2 years
    What if -5% and 250% are valid percentages. Which they are in some cases.
  • Michael Blackburn
    Michael Blackburn about 2 years
    @MisterBee in cases where those values are valid, this class wouldn't apply. No class should fit every situation, in fact if you're properly designing using SOLID, a class should fit one-and-only-one situation. In probability/likelihood situations they very much aren't valid. Zero is the lowest possible value (you can't get less likely than impossible) and One the highest (you can't get more likely than certainty).