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);
}
}
但是,您不能使用新的静态方法来扩展类,所以不幸的是,我们不得不在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
所以,对这一切持保留态度。我不建议使用任何代码,但这是一个有趣的练习,可以帮助您了解如何CultureInfo
和IFormatProvider
工作。