我有一个现有的泛型方法,用于解析XML文件中的各种数字类型
public static Nullable<T> ToNullable<T>(this XElement element) where T : struct
{
Nullable<T> result = new Nullable<T>();
if (element != null)
{
if (element.HasElements) throw new ArgumentException(String.Format("Cannot convert complex element to Nullable<{0}>", typeof(T).Name));
String s = element.Value;
try
{
if (!string.IsNullOrWhiteSpace(s))
{
TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));
result = (T)conv.ConvertFrom(s);
}
}
catch { }
}
return result;
}
不幸的是,输入XML文件开始包含包含数千个分隔符的数字字符串(例如:353,341.37)。逗号的存在现在导致上述方法在转换中失败,但是,我想像任何其他数字类型一样解析它
我知道各种Parse
和TryParse
方法都包含一个接受NumberStyles
枚举的重载,并会正确解析这些值,但由于我使用的是泛型方法,因此这些方法是在我想创建几种特定类型的方法之前不可用。
有没有办法在泛型方法中使用千位分隔符解析数值类型?
答案 0 :(得分:5)
您也可以替换默认的DoubleConverter。
首先,创建一个将成为转换器的类:
class DoubleConverterEx : DoubleConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string)
{
string text = ((string)value).Trim();
// Use the InvariantCulture, which accepts ',' for separator
culture = CultureInfo.InvariantCulture;
NumberFormatInfo formatInfo = (NumberFormatInfo)culture.GetFormat(typeof(NumberFormatInfo));
return Double.Parse(text, NumberStyles.Number, formatInfo);
}
return base.ConvertFrom(value);
}
}
然后将此类注册为double的转换器:
TypeDescriptor.AddAttributes(typeof(double), new TypeConverterAttribute(typeof(DoubleConverterEx)));
现在你的转换器将被调用,因为它都使用带有','分隔符的文化,并且传递允许你的格式的NumberStyles值,它将解析。
测试程序:
static void Main(string[] args)
{
TypeDescriptor.AddAttributes(typeof(double), new TypeConverterAttribute(typeof(DoubleConverterEx)));
TypeConverter converter = TypeDescriptor.GetConverter(typeof(double));
string number = "334,475.79";
double num = (double)converter.ConvertFrom(number);
Console.WriteLine(num);
}
打印:
334475.79
对于导致问题的类型(小数,浮点数),您可以执行类似的操作。
免责声明:转换器本身的实现非常基础,可能需要抛光。 希望有所帮助。
答案 1 :(得分:2)
您可以使用Reflection来获取正确的Parse
方法,并调用传递给您希望用于解析的任何NumberStyles
:
Type type = typeof(T);
//you can change the below to get the different overloads of Parse
MethodInfo parseMethod = type.GetMethod("Parse", new[] { typeof(string), typeof(NumberStyles) });
if (parseMethod != null)
{
result = (T)parseMethod.Invoke(null, new object[] { s, NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands });
}
值得注意的是,Parse
方法不在解析时会考虑NumberFormat.NumberGroupSizes
。这意味着上面的内容仍然允许您在评论@ Merenwen的答案时提到的00,0,0000,00
等格式。
Console.WriteLine(decimal.Parse("00,0,0000,00",
NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands));
//the above writes "0"
您可以自己检查组长度,如果它们不是您所期望的,则抛出异常。以下仅考虑当前文化中的第一个分隔符(这在英国适用于我!):
if (!string.IsNullOrWhiteSpace(s))
{
string integerPart = s.Split(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator[0])[0];
string[] groups = integerPart.Split(CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator[0]);
int maxGroupSize = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes.Max();
for (int i = 0; i < groups.Length; i++)
{
//the first group can be any size that's less than or equal to the max groupsize
//any other group has to be a size that's allowed in NumberGroupSizes
if (!((i == 0 && groups[i].Length <= maxGroupSize)
|| CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes.Contains(groups[i].Length)))
{
throw new InvalidCastException(String.Format("Cannot convert {0} to Nullable<{1}>", s, typeof(T).Name));
}
}
Type type = typeof(T);
MethodInfo parseMethod = type.GetMethod("Parse", new[] { typeof(string), typeof(NumberStyles) });
if (parseMethod != null)
{
result = (T)parseMethod.Invoke(null, new object[] { s, NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands });
}
}
这将为00,0,0000,00
或3,53,341.37
抛出异常,但会为353341.37
返回353,341.37
。
反射方法的唯一缺点是调用调用的性能。要缓解这种情况,您可以动态创建委托,然后调用 。创建一个返回Func<string, Numberstyles, T>
然后调用返回的委托的简单类大致将上述代码的执行时间减半:
public static class ParseMethodFactory<T> where T : struct
{
private static Func<string, NumberStyles, T> cachedDelegate = null;
public static Func<string, NumberStyles, T> GetParseMethod()
{
if (cachedDelegate == null)
{
Type type = typeof(T);
MethodInfo parseMethod = type.GetMethod("Parse", new[] { typeof(string), typeof(NumberStyles) });
if (parseMethod != null)
{
cachedDelegate = (Func<string, NumberStyles, T>)Delegate.CreateDelegate(typeof(Func<string, NumberStyles, T>), parseMethod);
}
}
return cachedDelegate;
}
}
然后你的方法变成:
if (!string.IsNullOrWhiteSpace(s))
{
string integerPart = s.Split(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator[0])[0];
string[] groups = integerPart.Split(CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator[0]);
int maxGroupSize = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes.Max();
for (int i = 0; i < groups.Length; i++)
{
//the first group can be any size that's less than or equal to the max groupsize
//any other group has to be a size that's allowed in NumberGroupSizes
if (!((i == 0 && groups[i].Length <= maxGroupSize)
|| CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes.Contains(groups[i].Length)))
{
throw new InvalidCastException(String.Format("Cannot convert {0} to Nullable<{1}>", s, typeof(T).Name));
}
}
//get the delegate from our factory
Func<string, NumberStyles, T> parseDelegate = ParseMethodFactory<T>.GetParseMethod();
if (parseDelegate != null)
{
//call the delegate
result = parseDelegate(s, NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands);
}
}
答案 2 :(得分:1)
只需使用ConvertFromString
类中的TypeConverter
方法,该方法也接受CultureInfo
作为参数。
来自MSDN:
使用指定的上下文和文化信息将给定文本转换为对象。
public Object ConvertFromString(
ITypeDescriptorContext context,
CultureInfo culture,
string text
)
传递一个合适的CultureInfo
(我实际上并不知道谁使用千位分隔符),你应该没事。
答案 3 :(得分:0)
您可以创建一个处理转换的泛型类和工厂方法,并跳过TypeDescriptor。您需要创建一个非泛型接口,以便我们返回它。
public interface INumberConverter
{
object ConvertFrom(string value);
object ConvertFrom(CultureInfo culture, string value);
}
然后我们可以在构造函数中的转换方法中传递泛型类。
public class GenericNumberConverter<T> : INumberConverter where T : struct
{
private readonly Func<string, NumberStyles, NumberFormatInfo, T> _convertMethod;
public GenericNumberConverter(Func<string, NumberStyles, NumberFormatInfo, T> convertMethod)
{
_convertMethod = convertMethod;
}
public object ConvertFrom(string value)
{
return ConvertFrom(CultureInfo.CurrentCulture, value);
}
public object ConvertFrom(CultureInfo culture, string value)
{
var format = (NumberFormatInfo) culture.GetFormat(typeof (NumberFormatInfo));
return _convertMethod(value.Trim(), NumberStyles.Number, format);
}
}
然后为了简化在数字类型上为这个泛型类创建一个工厂方法,我会把它放在与ToNullable Extension方法相同的类中。
// return back non generic interface to use in method
private static INumberConverter ConverterFactory<T>()
where T : struct
{
var typeCode = Type.GetTypeCode(typeof(T));
switch (typeCode)
{
case TypeCode.SByte:
return new GenericNumberConverter<sbyte>(SByte.Parse);
case TypeCode.Byte:
return new GenericNumberConverter<byte>(Byte.Parse);
case TypeCode.Single:
return new GenericNumberConverter<float>(Single.Parse);
case TypeCode.Decimal:
return new GenericNumberConverter<decimal>(Decimal.Parse);
case TypeCode.Double:
return new GenericNumberConverter<double>(Double.Parse);
case TypeCode.Int16:
return new GenericNumberConverter<short>(Int16.Parse);
case TypeCode.Int32:
return new GenericNumberConverter<int>(Int32.Parse);
case TypeCode.Int64:
return new GenericNumberConverter<long>(Int64.Parse);
case TypeCode.UInt16:
return new GenericNumberConverter<ushort>(UInt16.Parse);
case TypeCode.UInt32:
return new GenericNumberConverter<uint>(UInt32.Parse);
case TypeCode.UInt64:
return new GenericNumberConverter<ulong>(UInt64.Parse);
}
return null;
}
现在你的ToNullable变成了
public static T? ToNullable<T>(this XElement element) where T : struct
{
var result = new T?();
if (element != null)
{
if (element.HasElements) throw new ArgumentException(String.Format("Cannot convert complex element to Nullable<{0}>", typeof(T).Name));
var s = element.Value;
try
{
if (!string.IsNullOrWhiteSpace(s))
{
var numConverter = ConverterFactory<T>();
if (numConverter != null)
{
// interface returns back object so need to cast it
result = (T)numConverter.ConvertFrom(s);
}
else
{
var conv = TypeDescriptor.GetConverter(typeof(T));
result = (T)conv.ConvertFrom(s);
}
}
}
catch { }
}
return result;
}
答案 4 :(得分:0)
(反射,正则表达式和静态初始化的小技巧)
using System;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
namespace NumConvert {
class Program {
// basic reflection method - will accept "1,2,3.4" as well, because number.Parse accepts it
static T ParseReflect<T>(string s) where T:struct {
return (T)typeof(T).GetMethod("Parse", BindingFlags.Static|BindingFlags.Public, null, new Type[] {
typeof(string), typeof(NumberStyles), typeof(IFormatProvider) }, null)
.Invoke(null, new object[] { s, NumberStyles.Number, CultureInfo.InvariantCulture });
}
// regex check added (edit: forgot to escape dot at first)
static string NumberFormat = @"^\d{1,3}(,\d{3})*(\.\d+)?$";
static T ParseWithCheck<T>(string s) where T:struct {
if(!Regex.IsMatch(s, NumberFormat))
throw new FormatException("Not a number");
return ParseReflect<T>(s);
}
// caching (constructed automatically when used for the first time)
static Regex TestNumber = new Regex(NumberFormat);
static class ParseHelper<T> where T:struct {
// signature of parse method
delegate T ParseDelegate(string s, NumberStyles n, IFormatProvider p);
// static initialization by reflection (can use MethodInfo directly if targeting .NET 3.5-)
static ParseDelegate ParseMethod = (ParseDelegate)Delegate.CreateDelegate(
typeof(ParseDelegate), typeof(T).GetMethod("Parse",
BindingFlags.Static|BindingFlags.Public, null, new Type[] {
typeof(string), typeof(NumberStyles), typeof(IFormatProvider) }, null));
// this can be customized for each type (and we can add specific format provider as well if needed)
static NumberStyles Styles = typeof(T) == typeof(decimal)
? NumberStyles.Currency : NumberStyles.Number;
// combined together
public static T Parse(string s) {
if(!TestNumber.IsMatch(s))
throw new FormatException("Not a number");
return ParseMethod(s, Styles, CultureInfo.InvariantCulture);
}
}
// final version
static T Parse<T>(string s) where T:struct {
return ParseHelper<T>.Parse(s);
}
static void Main(string[] args) {
Console.WriteLine(Parse<double>("34,475.79333"));
Console.Read();
}
}
}
输出:
34475.79333
每种类型都可以使用不同的Regex
,NumberStyle
甚至CultureInfo
,只需使用少量类型检查和三元运算符来自定义ParseHelper<T>
,就像我使用{{1 }}
编辑:首先忘记逃避ParseHelper<T>.Styles
。如果您需要定位.NET 3.5或更早版本,则可以直接使用. -> \.
而不是MethodInfo
。正则表达式的含义:
delegate
- 匹配字符串开始
^
- 匹配任何数字
\d
- 一至三(重复说明)
{1,3}
- 任意数量的(,\d{3})*
, digit digit digit
- 可选点和非零数字
数
(\.\d+)?
- 匹配字符串结尾
检查$
/委托是否存在可以添加(如果你曾经使用过不是数字的结构)。静态变量也可以通过静态方法/ consturctor初始化。
答案 5 :(得分:-1)
如果您希望它是数字,为什么不清理字符串值?
String s = element.Value.Replace(",",String.Empty);
如果您知道可能需要处理金钱类型或其他非数字项目,您可以尝试更高级的正则表达式替换。