我在实现IFormatProvider类时遇到了一些问题,该类可以将包含百分比的字符串解析为等效的数字。
问题不在解析中。Stackoverflow提供了多种解决方案,可以将包含百分比的字符串解析为数字。
- 一个解决方案涉及创建一个名为Percentage的新数字类型
- 其他解决方案不尊重不同的文化,或创建新的TypeConverter
我宁愿不实现新类型。IMHO百分比并不是一种新的类型,它只是一种不同的显示数字的方式。百分号就像小数点。在某些文化中,这是一个点,在其他文化中,它是一个逗号。这也不会导致不同的类型,只会导致不同的字符串格式。
函数Double。Parse(string,IformatProvider)(等)提供了解析与标准Double稍有不同的字符串的可能性。解析即可。
我遇到的问题在IFormatProvider
中。可以命令Parse
函数使用特殊的IFormatProvider
。然而,我不能给这个IFormatProvider
任何功能来进行特殊的解析。(顺便说一句:格式化为字符串几乎可以正常工作)。
MSDN描述了IFormatProvider的功能:
IFormatProvider接口提供一个对象,该对象为格式化和解析操作提供格式化信息。。。典型的解析方法是Parse和TryParse。
默认的IFormatProvider
不是包含System中提到的百分比格式的Parse
(意思是函数Parse
,而不是动词解析)字符串。全球化数字格式信息
所以我想,也许我可以创建我自己的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
。但是我不能返回一个提供特殊解析方法来解析百分比的派生类
字符串。格式(IFormatProvider、字符串、args)
我注意到,在转换为字符串时,使用格式提供程序进行特殊格式化对于String.Format
来说效果很好。在这种情况下,调用GetFormat
以请求ICustomFormatter。您所要做的就是返回一个实现ICustomFormatter
的对象,并在ICustomFormatter中进行特殊格式化。总体安排
这是意料之中的事。返回ICustomFormatter后,返回其ICustomFormat。Format被调用,在这里我可以进行我想要的格式化。
加倍。ToString(IFormatProvider)
然而,当我使用Double时。ToString(字符串,IFormatProvider)我遇到了与Parse
相同的问题。在GetFormat
中,需要一个密封的NumberFormatInfo
。如果我返回一个ICustomFormatter
,那么返回的值将被忽略,并使用默认的NumberFormatInfo
。
结论:
- 字符串。Format(…)与IFormatProvider配合使用效果良好,如果需要,您可以自己设置格式
- 加倍。ToString(…)需要一个密封的NumberFormatInfo,你不能自己格式化
- 加倍。Parse需要一个密封的NumberFormatInfo。不允许进行自定义分析
那么:如何提供MSDN在IFormatProvider中承诺的解析?
IFormatProviders提供对象将在格式化自身时使用的数据。使用它们,您只能控制NumberFormatInfo
和DateTimeFormatInfo
对象中定义的内容。
虽然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);
}
}
但是,您不能使用新的静态方法来扩展类,所以不幸的是,我们不得不在string
上放置Parse<double>
,而不是向Double.Parse()
添加重载。
在这个路口,一个合理的做法是探索你链接的其他选项……但要继续下去,一个与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;
}
}
以及一些测试案例:
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)方法的对象的类型是扩展支持其格式化的类型。如果没有,请调用对象的IFormatable实现(如果存在),或者调用对象的无参数ToString()方法(如果不存在)。您应该准备好处理这些方法调用可能引发的任何异常
- 用于处理扩展支持的任何格式字符串的代码
- 用于处理扩展不支持的任何格式字符串的代码。这些应该传递给类型的IFormatable实现。您应该准备好处理这些方法调用可能引发的任何异常
然而,中给出的建议是使用ICustomFormatter进行自定义格式化"(以及许多MSDN示例)似乎建议在无法格式化时返回null
:
该方法返回要格式化的对象的自定义格式化字符串表示。如果该方法不能格式化对象,它应该返回一个空
所以,对这一切都持怀疑态度。我不建议使用这些代码中的任何一个,但这是理解CultureInfo
和IFormatProvider
如何工作的一个有趣的练习。