我在实现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
。
结论:
那么:如何提供MSDN在IFormatProvider中承诺的解析?
答案 0 :(得分:0)
IFormatProviders 提供对象将用于格式化自身的数据。使用它们,您只能控制 NumberFormatInfo
和 DateTimeFormatInfo
对象中定义的内容。
虽然 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,它用特定数字之间的连字符格式化整数类型。在这种情况下,您的实施应包括以下内容:
但是,Custom formatting with ICustomFormatter"(以及许多 MSDN 示例)中给出的建议似乎建议在无法格式化时返回 null
:
该方法返回要格式化的对象的自定义格式化字符串表示形式。如果该方法无法格式化对象,则应返回 null
所以,对这一切持保留态度。我不建议使用这些代码中的任何一个,但在理解 CultureInfo
和 IFormatProvider
的工作原理方面,这是一个有趣的练习。