简体   繁体   English

创建可以解析百分比的 IFormatProvider

[英]Create IFormatProvider that can parse Percentages

I have some trouble implementing an IFormatProvider class that can parse strings that contain percentages into their numeric equivalent.我在实现一个IFormatProvider类时遇到了一些麻烦,该类可以将包含百分比的字符串解析为它们的等效数字。

The problem is not in the parsing.问题不在于解析。 Stackoverflow provides several solutions to parse a string that contains a percentages into a number. Stackoverflow 提供了多种解决方案来将包含百分比的字符串解析为数字。

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.函数Double.Parse(string, IformatProvider) (等)提供了解析与标准 Double.Parse 略有不同的字符串的可能性。

The problem I have is in the IFormatProvider .我的问题出在IFormatProvider It is possible to order the Parse functions to use a special IFormatProvider .可以命令Parse函数使用特殊的IFormatProvider However I can't give this IFormatProvider any functionality to do special parsing.但是我不能给这个IFormatProvider任何做特殊解析的功能。 (By the way: Formatting to strings works almost fine). (顺便说一句:格式化为字符串几乎可以正常工作)。

MSDN describes the functionality of an IFormatProvider : MSDN 描述了 IFormatProvider 的功能

The IFormatProvider interface supplies an object that provides formatting information for formatting and parsing operations. IFormatProvider 接口提供一个对象,该对象为格式化和解析操作提供格式化信息。 ... Typical parsing methods are Parse and TryParse. ... 典型的解析方法是 Parse 和 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默认的IFormatProviderParse (意味着函数Parse ,而不是动词解析)包含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.所以我想,也许我可以创建我自己的IFormatProvider ,它使用在这个问题的第一行中提到的解决方案,它可以用于根据提供的NumberFormatInfo解析百分比,对于每个具有Parse函数的类型将字符串解析为数字。

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所以我创建了一个简单的IFormatProvider并检查如果我用IFormatProvider调用Double.Parse会发生什么

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事实上, GetFormat被调用,要求一个NumberFormatInfo类型的对象

Class NumberFormatInfo is sealed. NumberFormatInfo类是密封的。 So I can only return a standard NumberFormatInfo , if needed with changed values for properties.因此,如果需要更改属性值,我只能返回标准NumberFormatInfo But I can't return a derived class that provides a special parsing method to parse percentages但是我不能返回一个派生类,它提供了一个特殊的解析方法来解析百分比

String.Format(IFormatProvider, string, args) 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 .我注意到在转换为字符串时使用格式提供程序进行特殊格式设置,对于String.Format工作正常。 In that case GetFormat is called asking for an ICustomFormatter .在这种情况下,调用GetFormat请求ICustomFormatter All you have to do is return an object that implements ICustomFormatter and do the special formatting in ICustomFormatter.Format .您所要做的就是返回一个实现ICustomFormatter的对象并在ICustomFormatter.Format 中进行特殊格式化。

This works as expected.这按预期工作。 After returning the ICustomFormatter, its ICustomFormat.Format is called, where I can do the formatting I want.返回 ICustomFormatter 后,它的 ICustomFormat.Format 被调用,我可以在其中进行我想要的格式设置。

Double.ToString(IFormatProvider) Double.ToString(IFormatProvider)

However, when I used Double.ToString(string, IFormatProvider) I ran into the same problems as with Parse .但是,当我使用Double.ToString(string, IFormatProvider) 时,我遇到了与Parse相同的问题。 In GetFormat a sealed NumberFormatInfo is asked for.GetFormat一个密封的NumberFormatInfo If I return an ICustomFormatter , then the returned value is ignored and the default NumberFormatInfo is used.如果我返回ICustomFormatter ,则忽略返回值并使用默认NumberFormatInfo

Conclusion:结论:

  • String.Format(...) works fine with IFormatProvider, If desired you can do your own formatting String.Format(...) 适用于 IFormatProvider,如果需要,您可以进行自己的格式设置
  • Double.ToString(...) expects a sealed NumberFormatInfo, you can't do your own formatting Double.ToString(...) 需要一个密封的 NumberFormatInfo,你不能自己格式化
  • Double.Parse expects a sealed NumberFormatInfo. Double.Parse 需要一个密封的 NumberFormatInfo。 No custom parsing allowed.不允许自定义解析。

So: how to provide the parsing that MSDN promises in IFormatProvider?那么:如何在 IFormatProvider 中提供 MSDN 承诺的解析?

IFormatProviders supply the data that an object will use in formatting itself. IFormatProviders 提供对象将用于格式化自身的数据。 With them, you can only control what's defined in the NumberFormatInfo and DateTimeFormatInfo objects.使用它们,您只能控制NumberFormatInfoDateTimeFormatInfo对象中定义的内容。

While ICustomFormatter allows the formatting of objects according to arbitrary rules, there is no equivalent parsing API.虽然ICustomFormatter允许根据任意规则格式化对象,但没有等效的解析 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.可以创建这样一个尊重文化的解析 API,它或多或少地使用自定义接口和扩展方法镜像ToString(...)Parse(...) As Jeroen Mostert pointed out in this comment , though, the API is not exactly up to standards with .NET or C#'s newer features.但是,正如 Jeroen Mostert 在此评论中指出的那样,该 API 并不完全符合 .NET 或 C# 新功能的标准。 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() .但是,您不能使用新的静态方法扩展类,因此不幸的是,我们不得不将Parse<double>放在string而不是向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:在这个路口做的一个合理的事情是探索你链接到的其他选项......但继续下去,与ICustomFormatter相对一致的ICustomParser<>可能如下所示:

// 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 .还应该注意的是,MSDN 文档似乎与如何实现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.例如,您可以定义一个 CustomerNumberFormatter,它用特定数字之间的连字符格式化整数类型。 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.例如,如果要扩展 Int32 类型的格式,则不应实现“C”、“D”、“E”、“F”和“G”格式说明符等。
  • A test that the type of the object passed to your Format(String, Object, IFormatProvider) method is a type whose formatting your extension supports.测试传递给您的 Format(String, Object, IFormatProvider) 方法的对象类型是您的扩展支持其格式的类型。 If it is not, call the object's IFormattable implementation, if one exists, or the object's parameterless ToString() method, if it does not.如果不是,则调用对象的 IFormattable 实现(如果存在)或对象的无参数 ToString() 方法(如果不存在)。 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.这些应该传递给类型的 IFormattable 实现。 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:但是, 使用 ICustomFormatter 自定义格式中给出的建议(以及许多 MSDN 示例)似乎建议在无法格式化时返回null

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如果该方法无法格式化对象,则应返回 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.我不建议使用这些代码中的任何一个,但在理解CultureInfoIFormatProvider工作原理方面,这是一个有趣的练习。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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