使用TypeConverter.ConvertFrom使用数千个分隔符解析数值类型

时间:2013-12-21 01:20:51

标签: c# parsing generics

我有一个现有的泛型方法,用于解析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)。逗号的存在现在导致上述方法在转换中失败,但是,我想像任何其他数字类型一样解析它

我知道各种ParseTryParse方法都包含一个接受NumberStyles枚举的重载,并会正确解析这些值,但由于我使用的是泛型方法,因此这些方法是在我想创建几种特定类型的方法之前不可用。

有没有办法在泛型方法中使用千位分隔符解析数值类型?

6 个答案:

答案 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,003,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

每种类型都可以使用不同的RegexNumberStyle甚至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);

如果您知道可能需要处理金钱类型或其他非数字项目,您可以尝试更高级的正则表达式替换。