繁体   English   中英

使用TypeConverter.ConvertFrom用千位分隔符解析数字类型

[英]Parsing Numeric Types with Thousands Separator using TypeConverter.ConvertFrom

我有一个现有的通用方法,该方法用于解析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枚举的重载,并且将纠正这些值的解析,但是由于我使用的是通用方法,因此这些方法在我要创建几种类型特定的方法之前不可用。

有没有一种方法可以在通用方法中解析具有数千个分隔符的数字类型?

您也可以替换默认的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

您可以对引起问题的类型(小数,浮点数)执行类似的操作。

免责声明 :转换器本身的实现非常基础,可能需要完善。 希望能有所帮助。

您可以使用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 });
}

值得注意的是,在ParseParse方法没有考虑NumberFormat.NumberGroupSizes 这意味着以上内容仍将允许使用诸如00,0,0000,00的格式, 00,0,0000,00您在对@Merenwen的答案的评论中提及。

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);
    }
}

只需使用TypeConverter类中的ConvertFromString方法,该方法也接受CultureInfo作为参数。

MSDN

使用指定的上下文和区域性信息将给定的文本转换为对象。

public Object ConvertFromString(
    ITypeDescriptorContext context,
    CultureInfo culture,
    string text
)

通过适当的CultureInfo (我实际上不知道谁在使用千位分隔符),您应该没问题。

您可以创建一个通用类和工厂方法来处理转换并跳过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;
}

该代码说明了所有内容:

(反射,正则表达式和静态初始化的小技巧)

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> ,就像我对ParseHelper<T>.Styles

编辑:忘记了逃脱. -> \\. . -> \\. 首先。 如果需要使用.NET 3.5或更早版本,则可以直接使用MethodInfo代替delegate 正则表达式的含义:

^ -匹配字符串开始
\\d匹配任何数字
{1,3} -1-3(重复规范)
(,\\d{3})* -任意数量的, digit digit digit
(\\.\\d+)? -可选的点和非零数字
$ -匹配字符串结尾

检查是否可以添加MethodInfo / delegate是否存在(如果您将其与不是数字的struct一起使用)。 静态变量也可以通过静态方法/构造函数进行初始化。

如果您希望字符串值为数字,为什么不对它进行消毒呢?

String s = element.Value.Replace(",",String.Empty);

如果您知道可能需要处理货币类型或其他非数字项目,则可以尝试进行更高级的Regex替换。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM