2

I have some trouble implementing an IFormatProvider class that can parse strings that contain percentages into their numeric equivalent.

The problem is not in the parsing. Stackoverflow provides several solutions to parse a string that contains a percentages into a number.

I'd rather not implement a new type. IMHO a percentage is not a new type, it is just a different way of displaying a number. A percentage sign is like the decimal point. In some cultures this is a dot, in other cultures this is a comma. This also does not lead to different types, only to different string formatting.

The functions Double.Parse(string, IformatProvider) (et al), provide possibilities to parse strings slightly different than the standard Double.Parse would do.

The problem I have is in the IFormatProvider. It is possible to order the Parse functions to use a special IFormatProvider. However I can't give this IFormatProvider any functionality to do special parsing. (By the way: Formatting to strings works almost fine).

MSDN describes the functionality of an IFormatProvider:

The IFormatProvider interface supplies an object that provides formatting information for formatting and parsing operations. ... Typical parsing methods are Parse and TryParse.

The default IFormatProvider does not Parse (meaning the function Parse, not the verb parse) strings that contains the percentage format as mentioned in System.Globalization.NumberFormatInfo

So I thought, maybe I could create my own IFormatProvider, that uses the solutions mentioned in the first lines of this question in such a way that it can be used to parse percentages according to the provided NumberFormatInfo, for every type that has Parse functions to parse strings into numbers.

Usage would be:

string txt = ...  // might contain a percentage
// convert to double:
IFormatProvider percentFormatProvider = new PercentFormatProvider(...)
double d = Double.Parse(percentageTxt, percentFormatProvider)

What I've tried (that's the first what's being asked for)

So I created a simple IFormatProvider and checked what happened if I would call Double.Parse with the IFormatProvider

class PercentParseProvider : IFormatProvider
{
    public object GetFormat(Type formatType)
    {
        ...
    }
}

Called using:

string txt = "0.25%";
IFormatProvider percentParseProvider = new PercentParseProvider();
double d = Double.Parse(txt, percentParseProvider);

And indeed, GetFormat is called, asking for an object of type NumberFormatInfo

Class NumberFormatInfo is sealed. So I can only return a standard NumberFormatInfo, if needed with changed values for properties. But I can't return a derived class that provides a special parsing method to parse percentages

String.Format(IFormatProvider, string, args)

I've noticed that using a format provider to do special formatting when converting to strings, works fine for String.Format. In that case GetFormat is called asking for an ICustomFormatter. All you have to do is return an object that implements ICustomFormatter and do the special formatting in ICustomFormatter.Format.

This works as expected. After returning the ICustomFormatter, its ICustomFormat.Format is called, where I can do the formatting I want.

Double.ToString(IFormatProvider)

However, when I used Double.ToString(string, IFormatProvider) I ran into the same problems as with Parse. In GetFormat a sealed NumberFormatInfo is asked for. If I return an ICustomFormatter, then the returned value is ignored and the default NumberFormatInfo is used.

Conclusion:

  • String.Format(...) works fine with IFormatProvider, If desired you can do your own formatting
  • Double.ToString(...) expects a sealed NumberFormatInfo, you can't do your own formatting
  • Double.Parse expects a sealed NumberFormatInfo. No custom parsing allowed.

So: how to provide the parsing that MSDN promises in IFormatProvider?

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • 1
    For the `IFormatProvider`, `Double` supports `NumberFormatInfo` and `CultureInfo` (but only to get at the `NumberFormat` of that `CultureInfo`). After finding out the allowed number styles, it delegates parsing to an icky unsafe method that's no doubt optimized to the best of the writer's abilities. That's the code, that's all it allows. You can't use a fully custom `IFormatProvider` to parse and format doubles, at least not through `Double.[Try]Parse`. – Jeroen Mostert Aug 24 '17 at 10:02
  • Conclusion MSDN statment: The IFormatProvider interface supplies an object that provides formatting information for ... parsing operations, seems not to be implemented fully, also differently for String.Format vs Double.ToString – Harald Coppoolse Aug 24 '17 at 10:06
  • The MSDN isn't wrong when it says that the object "provides formatting *information*". That's not the same as promising you can fully hook the parsing (and indeed you can't). To be fair, this is a design from way back when in .NET 1.0 and it's not the most compelling (`GetFormat` returns an `object`, really?) – Jeroen Mostert Aug 24 '17 at 10:10
  • Ok, Jeroen, you're right. GetFormat must be able to either return ICustomFormatters as well as NumberFormatInfo as well as DateTimeFormatInfo, hence return Object. But you have a point. Better would have been to return object with interface with functions Format and Parse – Harald Coppoolse Aug 24 '17 at 10:24

1 Answers1

0

IFormatProviders supply the data that an object will use in formatting itself. With them, you can only control what's defined in the NumberFormatInfo and DateTimeFormatInfo objects.

While ICustomFormatter allows the formatting of objects according to arbitrary rules, there is no equivalent parsing API.

You could create such a culture-respecting parsing API that more-or-less mirrors ToString(...) and Parse(...) with a custom interface and extension methods. As Jeroen Mostert pointed out in this comment, though, the API is not exactly up to standards with .NET or C#'s newer features. One easy improvement that doesn't deviate from the syntax much is Generics support.

public interface ICustomParser<T> where T : IFormattable {
    T Parse(string format, string text, IFormatProvider formatProvider);
}

public static class CustomParserExtensions
{
    public static T Parse<T>(this string self, string format, IFormatProvider formatProvider) where T : IFormattable
    {
        var parser = (formatProvider?.GetFormat(typeof(ICustomParser<T>)) as ICustomParser<T> ?? null);
        if (parser is null) // fallback to some other implementation. I'm not actually sure this is correct.
            return (T)Convert.ChangeType(self, typeof(T));

        var numberFormat = formatProvider.GetFormat(typeof(NumberFormatInfo)) as NumberFormatInfo ?? CultureInfo.CurrentCulture.NumberFormat;
        return parser.Parse(format, self, numberFormat);
    }
}

You can't have extend classes with new static methods, though, so we unfortunately have to put Parse<double> here on string instead of adding an overload to Double.Parse().

A reasonable thing to do at this junction would be to explore the other options you linked to... But to carry on, an ICustomParser<> that would be relatively consistent with ICustomFormatter might look like this:

// Using the same "implements ICustomFormat, IFormatProvider" pattern where we return ourselves
class PercentParser : ICustomParser<double>, IFormatProvider
{
    private NumberFormatInfo numberFormat;

    // If constructed with a specific culture, use that one instead of the Current thread's
    // If this were a Formatter, I think this would be the only way to provide a CultureInfo when invoked via String.Format() (aside from altering the thread's CurrentCulture)
    public PercentParser(IFormatProvider culture)
    {
        numberFormat = culture?.NumberFormat;
    }
    
    public object GetFormat(Type formatType)
    {
        if (typeof(ICustomParser<double>) == formatType) return this;
        if (typeof(NumberFormatInfo) == formatType) return numberFormat;
        return null;
    }
    
    public double Parse(string format, string text, IFormatProvider formatProvider)
    {
        var numberFmt = formatProvider.GetFormat(typeof(NumberFormatInfo)) as NumberFormatInfo ?? this.numberFormat ?? CultureInfo.CurrentCulture.NumberFormat;

        // This and TrimPercentDetails(string, out int) are left as an exercise to the reader. It would be very easy to provide a subtly incorrect solution.
        if (IKnowHowToParse(format))
        {
            value = TrimPercentDetails(value, out int numberNegativePattern);

            // Now that we've handled the percentage sign and positive/negative patterns, we can let double.Parse handle the rest.
            // But since it doesn't know that it's formatted as a percentage, so we have to lie to it a little bit about the NumberFormat:
            numberFmt = (NumberFormatInfo)numberFmt.Clone(); // make a writable copy

            numberFmt.NumberDecimalDigits = numberFmt.PercentDecimalDigits;
            numberFmt.NumberDecimalSeparator = numberFmt.PercentDecimalSeparator;
            numberFmt.NumberGroupSeparator = numberFmt.PercentGroupSeparator;
            numberFmt.NumberGroupSizes = numberFmt.PercentGroupSizes;
            // Important note! These values mean different things from percentNegativePattern. See the Reference Documentation's Remarks for both for valid values and their interpretations!
            numberFmt.NumberNegativePattern = numberNegativePattern; // and you thought `object GetFormat(Type)` was bad!

        }
        
        return double.Parse(value, numberFmt) / 100;
    }
}

And some test cases:

Assert(.1234 == "12.34%".Parse<double>("p", new PercentParser(CultureInfo.InvariantCulture.NumberFormat));

// Start with a known culture and change it all up:
var numberFmt = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
numberFmt.PercentDemicalDigits = 4;
numberFmt.PercentDecimalSeparator = "~a";
numberFmt.PercentGroupSeparator = " & ";
numberFmt.PercentGroupSizes = new int[] { 4, 3 };
numberFmt.PercentSymbol = "percent";
numberFmt.NegativeSign = "¬!-";
numberFmt.PercentNegativePattern = 8;
numberFmt.PercentPositivePattern = 3;

// ensure our number will survive a round-trip
double d = double.Parse((-123456789.1011121314 * 100).ToString("R", CultureInfo.InvariantCulture));
var formatted = d.ToString("p", numberFmt);
double parsed = formatted.Parse<double>("p", new PercentParser(numberFmt))
// Some precision loss due to rounding with NumberFormatInfo.PercentDigits, above, so convert back again to verify. This may not be entirely correct
Assert(formatted == parsed.ToString("p", numberFmt);

It should also be noted that the MSDN documentation seems to contradict itself about how to implement ICustomFormatter. It's Notes to Implementers section advises calling the appropriate implementation when you are called with something you can't format.

Extension implementations are implementations that provide custom formatting for a type that already has formatting support. For example, you could define a CustomerNumberFormatter that formats an integral type with hyphens between specific digits. In this case, your implementation should include the following:

  • A definition of format strings that extend the formatting of the object. These format strings are required, but they must not conflict with the type's existing format strings. For example, if you are extending formatting for the Int32 type, you should not implement the "C", "D", "E", "F", and "G" format specifiers, among others.
  • A test that the type of the object passed to your Format(String, Object, IFormatProvider) method is a type whose formatting your extension supports. If it is not, call the object's IFormattable implementation, if one exists, or the object's parameterless ToString() method, if it does not. You should be prepared to handle any exceptions these method calls might throw.
  • Code to handle any format strings that your extension supports.
  • Code to handle any format strings that your extension does not support. These should be passed on to the type's IFormattable implementation. You should be prepared to handle any exceptions these method calls might throw.

However, the advice given in Custom formatting with ICustomFormatter" (and many of the MSDN examples) seems to recommend returning null when unable to format:

The method returns a custom formatted string representation of the object to be formatted. If the method cannot format the object, it should return a null

So, take all this with a grain of salt. I don't recommend using any of this code, but it was an interesting exercise in understanding just how CultureInfo and IFormatProvider work.

Clayton Hughes
  • 1,015
  • 2
  • 9
  • 22