2

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

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

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

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

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

MSDN 描述了 IFormatProvider 的功能

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

默认IFormatProviderParse(意味着函数,而不是动词解析)包含System.Globalization.NumberFormatInfoParse中提到的百分比格式的字符串

所以我想,也许我可以创建自己的IFormatProvider,它使用这个问题的第一行中提到的解决方案,这样它就可以用来根据提供的解析百分比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);

确实,GetFormat被调用,要求一个NumberFormatInfo类型的对象

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

String.Format(IFormatProvider,字符串,参数)

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

这按预期工作。返回 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 承诺的解析?

4

1 回答 1

0

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

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

可以创建这样一个尊重文化的解析 API,它或多或少地镜像ToString(...)Parse(...)具有自定义接口和扩展方法。不过,正如 Jeroen Mostert 在此评论中指出的那样,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. 当你被调用一些你无法格式化的东西时,它的实现者注释部分建议调用适当的实现。

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

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

但是,使用 ICustomFormatter 进行自定义格式化”(以及许多 MSDN 示例)中给出的建议似乎建议null在无法格式化时返回:

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

所以,对这一切持保留态度。我不建议使用任何代码,但这是一个有趣的练习,可以帮助您了解如何CultureInfoIFormatProvider工作。

于 2021-05-12T23:59:36.767 回答