【问题标题】:Create IFormatProvider that can parse Percentages创建可以解析百分比的 IFormatProvider
【发布时间】:2018-02-02 03:20:15
【问题描述】:

我在实现 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,它使用本问题第一行中提到的解决方案,以便根据提供的NumberFormatInfo 解析百分比,例如每种类型都有 Parse 函数将字符串解析为数字。

用法如下:

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

我尝试过的 (这是第一个要求的)

所以我创建了一个简单的IFormatProvider 并检查如果我用IFormatProvider 调用Double.Parse 会发生什么

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

调用使用:

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

确实,GetFormat 被调用,请求 NumberFormatInfo 类型的对象

NumberFormatInfo 类已密封。因此,如果需要更改属性值,我只能返回标准NumberFormatInfo。但是我不能返回提供特殊解析方法来解析百分比的派生类

String.Format(IFormatProvider, string, args)

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

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

Double.ToString(IFormatProvider)

但是,当我使用Double.ToString(string, IFormatProvider) 时,我遇到了与Parse 相同的问题。在GetFormat 中要求密封NumberFormatInfo。如果我返回一个ICustomFormatter,那么返回的值将被忽略并使用默认的NumberFormatInfo

结论:

  • String.Format(...) 可以与 IFormatProvider 很好地配合使用,如果需要,您可以进行自己的格式化
  • Double.ToString(...) 需要一个密封的 NumberFormatInfo,您不能自己进行格式化
  • Double.Parse 需要一个密封的 NumberFormatInfo。不允许自定义解析。

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

【问题讨论】:

  • 对于IFormatProviderDouble 支持NumberFormatInfoCultureInfo(但仅限于NumberFormatCultureInfo)。在找出允许的数字样式后,它将解析委托给一个讨厌的不安全方法,该方法无疑已根据作者的最佳能力进行了优化。这就是代码,这就是它所允许的全部。您不能使用完全自定义的IFormatProvider 来解析和格式化双精度数,至少不能通过Double.[Try]Parse
  • 结论 MSDN 声明:IFormatProvider 接口提供了一个对象,该对象为...解析操作提供格式信息,似乎没有完全实现,对于 String.Format 与 Double.ToString 也不同
  • MSDN 说对象“提供格式化信息”并没有错。这与承诺您可以完全挂钩解析不同(实际上您不能)。公平地说,这是 .NET 1.0 中的一种设计,它并不是最引人注目的(GetFormat 返回一个object,真的吗?)
  • 好吧,杰罗恩,你是对的。 GetFormat 必须能够返回 ICustomFormatters 以及 NumberFormatInfo 以及 DateTimeFormatInfo,因此返回 Object。但你说得有道理。最好是返回带有函数 Format 和 Parse 接口的对象

标签: c# parsing iformatprovider


【解决方案1】:

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&lt;double&gt; 放在 string 上,而不是在 Double.Parse() 上添加重载。

在这个交界处,一个合理的做法是探索您链接到的其他选项...但要继续,与ICustomFormatter 相对一致的ICustomParser&lt;&gt; 可能如下所示:

// 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 的工作原理。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-14
    • 2013-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-30
    • 1970-01-01
    相关资源
    最近更新 更多