简体   繁体   中英

Creating a percentage type in C#

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.

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

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.

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

It seems like a reasonable thing to do, but I'd reconsider your interface to make it more like other CLR primitive types, eg 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.

这个问题让我想起了企业应用程序架构中的Money类模式 -这个链接可能会给你一些思考。

I think you may be mixing up presentation and logic here. I would convert the percentage to a decimal or float fraction (0.5) when getting it from the database and then let the presentation deal with the formatting.

I'd not create a separate class for that - this just creates more overhead. I thinkg it will be faster just to use double variables set to the database value.

If it is common knowledge that the database stores percentages as 50 instead of 0.5, everybody will understand statemens like part = (percentage / 100.0) * (double)value .

Even in 2022, .Net 6 I found myself using something just like this. I concur with Michael on his answer for the OP and like to extend it for future Googlers.

Creating a value type would be indispensable in explaining the domain's intent with enforced immutability. Notice especially in the Fraction Record you will get a Quotient that would normally cause an exception however here we can safely show d / 0 with no error, likewise all other inherited children are also granted that protection (It also offers an excellent place to establish simple routines to check validity, data rehydration (as if DBA's don't make mistakes), serialization concerns just to name a few.)

namespace StackOverflowing;

// Honor the simple fraction
public record class Fraction(decimal Dividend, decimal Divisor) 
{
    public decimal Quotient => (Divisor > 0.0M) ? Dividend / Divisor : 0.0M;

    // Display dividend / divisor as the string, not the quotient
    public override string ToString() 
    {
        return $"{Dividend} / {Divisor}";
    }
};

// Honor the decimal based interpretation of the simple fraction
public record class DecimalFraction(decimal Dividend, decimal Divisor) : Fraction(Dividend, Divisor)
{
    // Change the display of this type to the decimal form
    public override string ToString()
    {
        return Quotient.ToString();
    }
};

// Honor the decimal fraction as the basis value but offer a converted value as a percentage
public record class Percent(decimal Value) : DecimalFraction(Value, 100.00M)
{
    // Display the quotient as it represents the simple fraction in a base 10 format aka radix 10
    public override string ToString()
    {
        return Quotient.ToString("p");
    }
};

// Example of a domain value object consumed by an entity or aggregate in finance
public record class PercentagePoint(Percent Left, Percent Right) 
{ 
    public Percent Points => new(Left.Value - Right.Value);

    public override string ToString()
    {
        return $"{Points.Dividend} points";
    }
}



[TestMethod]
public void PercentScratchPad()
{
    var approximatedPiFraction = new Fraction(22, 7);
    var approximatedPiDecimal = new DecimalFraction(22, 7);
    var percent2 = new Percent(2);
    var percent212 = new Percent(212);
    var points = new PercentagePoint(new Percent(50), new Percent(40));

    TestContext.WriteLine($"Approximated Pi Fraction: {approximatedPiFraction}");
    TestContext.WriteLine($"Approximated Pi Decimal: {approximatedPiDecimal}");
    TestContext.WriteLine($"2 Percent: {percent2}");
    TestContext.WriteLine($"212 Percent: {percent212}");
    TestContext.WriteLine($"Percentage Points: {points}");
    TestContext.WriteLine($"Percentage Points as percentage: {points.Points}");
}

 PercentScratchPad Standard Output: TestContext Messages:

Approximated Pi Fraction: 22 / 7
Approximated Pi Decimal: 3.1428571428571428571428571429
2 Percent: 2.00%
212 Percent: 212.00%
Percentage Points: 10 points
Percentage Points as percentage: 10.00%

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM