创建可以解析百分比的IFormatProvider

时间:2017-08-24 09:45:57

标签: c# parsing iformatprovider

我在实现IFormatProvider类时遇到一些麻烦,该类可以将包含百分比的字符串解析为它们的数字等价物。

问题不在于解析。 Stackoverflow提供了几种解决方案来将包含百分比的字符串解析为数字。

我宁愿不实现新类型。恕我直言,百分比不是新类型,它只是一种显示数字的不同方式。百分号与小数点相似。在某些文化中,这是一个点,在其他文化中,这是一个逗号。这也不会导致不同的类型,只会导致不同的字符串格式化。

函数Double.Parse(string, IformatProvider)(等)提供了解析与标准Double.Parse略有不同的字符串的可能性。

我遇到的问题是IFormatProvider。可以命令Parse函数使用特殊IFormatProvider。但是我不能给这个IFormatProvider任何功能来做特殊的解析。 (顺便说一句:格式化为字符串几乎可以正常工作)。

MSDN describes the functionality of an IFormatProvider

  

IFormatProvider接口提供一个对象,该对象提供格式化和解析操作的格式信息。 ...典型的解析方法是Parse和TryParse。

默认IFormatProvider没有Parse(意思是函数Parse,而不是动词解析)包含System.Globalization.NumberFormatInfo

中提到的百分比格式的字符串

所以我想,也许我可以创建自己的IFormatProvider,它使用这个问题的第一行中提到的解决方案,以便它可以用来根据提供的{{1来解析百分比对于每个具有NumberFormatInfo函数的类型,将字符串解析为数字。

用法是:

Parse

我尝试了什么 (这是第一个要求的)

所以我创建了一个简单的string txt = ... // might contain a percentage // convert to double: IFormatProvider percentFormatProvider = new PercentFormatProvider(...) double d = Double.Parse(percentageTxt, percentFormatProvider) ,并检查了如果我使用IFormatProvider

致电Double.Parse,会发生什么
IFormatProvider

使用:

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

确实,string txt = "0.25%"; IFormatProvider percentParseProvider = new PercentParseProvider(); double d = Double.Parse(txt, percentParseProvider); 被调用,要求输入NumberFormatInfo

类型的对象

班级GetFormat已被封存。所以我只能返回标准NumberFormatInfo,如果需要,可以更改属性值。但我无法返回一个派生类,它提供了一种特殊的解析方法来解析百分比

String.Format(IFormatProvider,string,args)

我注意到在转换为字符串时使用格式提供程序进行特殊格式化,适用于NumberFormatInfo。在这种情况下,String.Format被称为要求ICustomFormatter。您所要做的就是返回一个实现GetFormat的对象,并在ICustomFormatter.Format中执行特殊格式设置。

这可以按预期工作。返回ICustomFormatter后,调用其ICustomFormat.Format,我可以在其中进行我想要的格式化。

Double.ToString(的IFormatProvider)

然而,当我使用Double.ToString(string, IFormatProvider)时遇到与ICustomFormatter相同的问题。在Parse中,要求提供密封的GetFormat。如果我返回NumberFormatInfo,则会忽略返回的值,并使用默认的ICustomFormatter

结论:

  • String.Format(...)适用于IFormatProvider,如果需要,您可以进行自己的格式化
  • Double.ToString(...)需要密封的NumberFormatInfo,你不能自己做格式化
  • Double.Parse需要一个密封的NumberFormatInfo。不允许自定义解析。

那么:如何提供MSDN在IFormatProvider中承诺的解析?

1 个答案:

答案 0 :(得分:0)

IFormatProviders 提供对象将用于格式化自身的数据。使用它们,您只能控制 NumberFormatInfoDateTimeFormatInfo 对象中定义的内容。

虽然 ICustomFormatter 允许根据任意规则格式化对象,但没有等效的解析 API。

可以创建这样一个尊重文化的解析 API,它或多或少地使用自定义接口和扩展方法来反映 ToString(...)Parse(...)。但是,正如 Jeroen Mostert 在 this comment 中指出的那样,API 并不完全符合 .NET 或 C# 新功能的标准。一项不会偏离语法的简单改进是泛型支持。

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

但是,您不能使用新的静态方法来扩展类,因此很遗憾,我们不得不将 Parse<double> 放在 string 上,而不是向 Double.Parse() 添加重载。

在这个路口做的一个合理的事情是探索你链接到的其他选项......但是继续下去,与 ICustomParser<> 相对一致的 ICustomFormatter 可能看起来像这样:

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

还有一些测试用例:

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

还应注意,MSDN 文档似乎与如何实现 ICustomFormatter 相矛盾。 Notes to Implementers 部分建议在调用无法格式化的内容时调用适当的实现。

<块引用>

扩展实现是为已经具有格式支持的类型提供自定义格式的实现。例如,您可以定义一个 CustomerNumberFormatter,它用特定数字之间的连字符格式化整数类型。在这种情况下,您的实施应包括以下内容:

  • 扩展对象格式的格式字符串定义。这些格式字符串是必需的,但它们不能与类型的现有格式字符串冲突。例如,如果您要扩展 Int32 类型的格式,则不应实现“C”、“D”、“E”、“F”和“G”格式说明符等。
  • 测试传递给您的 Format(String, Object, IFormatProvider) 方法的对象类型是您的扩展支持其格式的类型。如果不是,则调用对象的 IFormattable 实现(如果存在)或对象的无参数 ToString() 方法(如果不存在)。您应该准备好处理这些方法调用可能引发的任何异常。
  • 处理扩展程序支持的任何格式字符串的代码。
  • 用于处理您的扩展程序不支持的任何格式字符串的代码。这些应该传递给类型的 IFormattable 实现。您应该准备好处理这些方法调用可能引发的任何异常。

但是,Custom formatting with ICustomFormatter"(以及许多 MSDN 示例)中给出的建议似乎建议在无法格式化时返回 null

<块引用>

该方法返回要格式化的对象的自定义格式化字符串表示形式。如果该方法无法格式化对象,则应返回 null

所以,对这一切持保留态度。我不建议使用这些代码中的任何一个,但在理解 CultureInfoIFormatProvider 的工作原理方面,这是一个有趣的练习。