2012-08-17 6 views
6

Estoy analizando un archivo CSV y colocando los datos en una estructura. Estoy usando el TextFieldParser de this question y está funcionando como un amuleto, excepto que devuelve un String[]. En este momento tengo la fea proceso de:¿Completar struct con String []?

String[] row = parser.ReadFields(); 
DispatchCall call = new DispatchCall(); 
if (!int.TryParse(row[0], out call.AccountID)) { 
    Console.WriteLine("Invalid Row: " + parser.LineNumber); 
    continue; 
} 
call.WorkOrder = row[1]; 
call.Description = row[2]; 
call.Date = row[3]; 
call.RequestedDate = row[4]; 
call.EstStartDate = row[5]; 
call.CustomerID = row[6]; 
call.CustomerName = row[7]; 
call.Caller = row[8]; 
call.EquipmentID = row[9]; 
call.Item = row[10]; 
call.TerritoryDesc = row[11]; 
call.Technician = row[12]; 
call.BillCode = row[13]; 
call.CallType = row[14]; 
call.Priority = row[15]; 
call.Status = row[16]; 
call.Comment = row[17]; 
call.Street = row[18]; 
call.City = row[19]; 
call.State = row[20]; 
call.Zip = row[21]; 
call.EquipRemarks = row[22]; 
call.Contact = row[23]; 
call.ContactPhone = row[24]; 
call.Lat = row[25]; 
call.Lon = row[26]; 
call.FlagColor = row[27]; 
call.TextColor = row[28]; 
call.MarkerName = row[29]; 

La estructura consiste en todos aquellos campos siendo String s excepción de AccountID ser un int. Me molesta que no estén fuertemente tipados, pero vamos a ver eso por ahora. Dado que parser.ReadFields() devuelve String[], ¿hay alguna forma más eficiente de completar una estructura (posiblemente convirtiendo algunos valores como row[0] que necesitan convertirse en int) con los valores en la matriz?

** EDIT: ** Una restricción se me olvidó mencionar que puede impacto qué tipo de soluciones va a funcionar es que esta estructura es [Serializable] y se enviará Tcp en otro lugar.

+1

Use la reflexión. – Grozz

+0

La reflexión sería definitivamente menos eficiente, simplemente viviría con ella, como es – RobJohnson

+0

CsvHelper podría serle de mucha ayuda https://github.com/JoshClose/CsvHelper/wiki/Basics – KeesDijk

Respuesta

7

Su millaje puede variar dependiendo de si es una solución mejor, pero podría usar la reflexión y definir una clase Attribute que use para marcar los miembros de su estructura. El atributo tomaría el índice de matriz como argumento. Asignar el valor del elemento del conjunto de la derecha se produciría mediante el uso de la reflexión.

Se podría definir su atributo de la siguiente manera:

[AttributeUsage(AttributeTargets.Property)] 
public sealed class ArrayStructFieldAttribute : Attribute 
{ 
    public ArrayStructFieldAttribute(int index) 
    { 
     this.index = index; 
    } 

    private readonly int index; 

    public int Index { 
     get { 
      return index; 
     } 
    } 
} 

Esto significa que el atributo sólo se puede utilizar para asociar un valor int llamado Index con una propiedad.

Entonces, usted podría marcar sus propiedades en la estructura con ese atributo (sólo algunas líneas ejemplares):

[ArrayStructField(1)] 
public string WorkOrder { // ... 

[ArrayStructField(19)] 
public string City { // ... 

Los valores podrían ser establecidos con el objeto Type para su tipo de estructura (puede obtenerlo con el operador typeof):

foreach (PropertyInfo prop in structType.GetProperties()) { 
    ArrayStructFieldAttribute attr = prop.GetCustomAttributes(typeof(ArrayStructFieldAttribute), false).Cast<ArrayStructFieldAttribute>().FirstOrDefault(); 
    if (attr != null) { 
     // we have found a property that you want to load from an array element! 
     if (prop.PropertyType == typeof(string)) { 
      // the property is a string property, no conversion required 
      prop.SetValue(boxedStruct, row[attr.Index]); 
     } else if (prop.PropertyType == typeof(int)) { 
      // the property is an int property, conversion required 
      int value; 
      if (!int.TryParse(row[attr.Index], out value)) { 
       Console.WriteLine("Invalid Row: " + parser.LineNumber); 
      } else { 
       prop.SetValue(boxedStruct, value); 
      } 
     } 
    } 
} 

Este código itera sobre todas las propiedades de su tipo de estructura. Para cada propiedad, verifica nuestro tipo de atributo personalizado definido anteriormente. Si tal atributo está presente, y si el tipo de propiedad es string o int, el valor se copia del índice de matriz respectivo.

Estoy comprobando string y int propiedades, ya que son los dos tipos de datos que ha mencionado en su pregunta. a pesar de que solo tiene un índice en particular que contiene un valor int ahora, es bueno para el mantenimiento si este código está preparado para manejar cualquier índice como una cadena o una propiedad int.

Tenga en cuenta que para un mayor número de tipos de manejar, me gustaría sugerir que no utilizan una cadena de if y else if, sino más bien un Dictionary<Type, Func<string, object>> que mapea los tipos de propiedad a las funciones de conversión.

0

Utilice la reflexión como @Grozz se sugiere en el comentario. Marque cada propiedad de la clase struct con un atributo (es decir, [ColumnOrdinal]) y luego use esto para asignar la información con la columna correcta. Si tiene doble, decimal y así sucesivamente como objetivo, también debería considerar usar Convert.ChangeType para convertir correctamente en el tipo de destino.Si no está satisfecho con las actuaciones, puede disfrutar de crear un DynamicMethod sobre la marcha, más desafiante, pero realmente brillante y hermoso. El desafío es escribir la instrucción IL en la memoria para hacer las "tuberías" que hizo a mano (generalmente creo un código de ejemplo, y luego miro dentro con espía IL como punto de partida). por supuesto, almacenará en caché en algún lugar dichos métodos dinámicos, por lo que su creación se solicita solo una vez.

0

Lo primero que se le viene a la mente es utilizar la reflexión para iterar sobre las propiedades y relacionarlas con los elementos en el string[] según el valor de un atributo.

public struct DispatchCall 
{ 
    [MyAttribute(CsvIndex = 1)] 
    public string WorkOrder { get; set; } 
} 

MyAttribute no sería más que un atributo personalizado con un índice que se correspondería a la posición de campo en el CSV.

var row = parser.ReadFields(); 

    for each property that has MyAttribute... 
     var indexAttrib = MyAttribute attached to property 
     property.Value = row[indexAttrib.Index] 
    next 

(Pseudocódigo, obviamente)

o

[StructLayout(LayoutKind.Sequential)] // keep fields in order 
public strict DispatchCall 
{ 
    public string WorkOrder; 
    public string Description; 
} 

StructLayout mantendrá los campos de struct en orden, por lo que puede iterar sobre ellos sin tener que especificar explícitamente un número de columna para cada campo . Eso puede ahorrar algo de mantenimiento si tiene muchos campos.

O bien, puede saltarse el proceso en su totalidad, y almacenar los nombres de campo en un diccionario:

var index = new Dictionary<int, string>(); 

/// populate index with row index : field name values, preferable from some sort of config file or database 
index[0] = "WorkOrder"; 
index[1] = "Description"; 
... 

var values = new Dictionary<string,object>(); 

for(var i=0;i<row.Length;i++) 
{ 
    values.Add(index[i],row[i]); 
} 

Eso es más fácil de cargar, pero en realidad no se aprovechan de la tipificación fuerte, lo que hace que esta a menos de ideal.

También podría generar un método dinámico o una plantilla T4. Se podría generar código a partir de un archivo de asignación en el formato de carga que

0,WorkOrder 
1,Description 
... 

, y generar un método que tiene este aspecto:

/// emit this 
    call.WorkOrder = row[0]; 
    call.Description = row[1]; 

etc.

Ese enfoque se utiliza en algunos micro-ORM flotando alrededor y parece funcionar bastante bien.

Idealmente, su archivo CSV incluiría una fila con nombres de campos que facilitaría esto mucho más.

O, aún otro enfoque, use StructLayout junto con un método dinámico para evitar tener que mantener un campo: mapping de column_index aparte de la propia estructura.

O, crear una enumeración

public enum FieldIndex 
{ 
WorkOrder=0 
, 
Description // only have to specify explicit value for the first item in the enum 
, /// .... 
, 
MAX /// useful for getting the maximum enum integer value 
} 

for(var i=0;i<FieldIndex.MAX;i++) 
{ 
    var fieldName = ((FieldIndex)i).ToString(); /// get string enum name 
    var value = row[i]; 

    // use reflection to find the property/field FIELDNAME, and set it's value to VALUE. 
} 
1

Si desea crear algo muy flexible que puede marcar cada propiedad en DispatchCall utilizando un atributo personalizado. Algo como esto:

class DispatchCall { 

    [CsvColumn(0)] 
    public Int32 AccountId { get; set; } 

    [CsvColumn(1)] 
    public String WorkOrder { get; set; } 

    [CsvColumn(3, Format = "yyyy-MM-dd")] 
    public DateTime Date { get; set; } 

} 

Esto le permite asociar cada propiedad con una columna. Para cada fila, puede iterar sobre todas las propiedades y mediante el uso del atributo puede asignar el valor correcto a la propiedad correcta. Tendrás que hacer alguna conversión de tipo de cadena a números, fechas y quizás enumeraciones. Puede agregar propiedades adicionales al atributo para ayudarlo en ese proceso.En el ejemplo que he inventado Format que debe ser usado cuando un DateTime se analice:

Object ParseValue(String value, TargetType targetType, String format) { 
    if (targetType == typeof(String)) 
    return value; 
    if (targetType == typeof(Int32)) 
    return Int32.Parse(value); 
    if (targetType == typeof(DateTime)) 
    DateTime.ParseExact(value, format, CultureInfo.InvariantCulture); 
    ... 
} 

Usando TryParse métodos en el código anterior se puede mejorar la gestión de errores por lo que le permite proporcionar más contexto cuando se encuentra un valor no se puede analizar.

Desafortunadamente, este enfoque no es muy eficiente porque el código de reflexión se ejecutará para cada fila en su archivo de entrada. Si desea hacer esto más eficiente, necesita crear dinámicamente un método compilado al reflejar una vez más DispatchCall que luego puede aplicar en cada fila. Es posible pero no particularmente fácil.

1

¿De qué medida depende de la biblioteca que está utilizando? He encontrado que File Helpers es bastante útil para este tipo de cosas. El código sería algo como:

using FileHelpers; 

// ... 

[DelimitedRecord(",")] 
class DispatchCall { 
    // Just make sure these are in order 
    public int AccountID { get; set; } 
    public string WorkOrder { get; set; } 
    public string Description { get; set; } 
    // ... 
} 

// And then to call the code 
var engine = new FileHelperEngine(typeof(DispatchCall)); 
engine.Options.IgnoreFirstLines = 1; // If you have a header row 
DispatchCall[] data = engine.ReadFile(FileName) as DispatchCall[]; 

Ahora tiene una matriz DispatchCall, y el motor hizo todo el trabajo pesado para usted.

0

Si va por la velocidad que podría una declaración de cambio frágil.

var columns = parser.ReadFields(); 

for (var i = 0; i < columns.Length; i++) 
{ 
    SetValue(call, i, columns[i]); 
} 

private static void SetValue(DispatchCall call, int column, string value) 
{ 
    switch column 
    { 
     case 0: 
      SetValue(ref call.AccountId, (value) => int.Parse, value); 
      return; 

     case 1: 
      SetValue(ref call.WorkOrder, (value) => value, value); 
      return; 

     ... 

     default: 
      throw new UnexpectedColumnException(); 
    }  
} 

private static void SetValue<T>(
    ref T property, 
    Func<string, T> setter 
    value string) 
{ 
    property = setter(value); 
} 

Es una pena que no permite TextFieldParser a leer un campo a la vez, entonces se podría evitar la construcción y la indexación de la matriz columnas.

Cuestiones relacionadas