Leaf Input Parsing complete
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using LeafWeb.Core.Models;
|
||||
|
||||
namespace LeafWeb.Core.Services
|
||||
{
|
||||
public class LeafInputCsvParser : IDisposable
|
||||
{
|
||||
private readonly FileSystemInfo _csvFile;
|
||||
private readonly StreamReader _reader;
|
||||
private readonly CsvReader _csv;
|
||||
|
||||
public LeafInputCsvParser(FileSystemInfo csvFile)
|
||||
{
|
||||
_csvFile = csvFile;
|
||||
if (!csvFile.Exists)
|
||||
throw new FileNotFoundException("Cannot find file '" + csvFile.Name + "'");
|
||||
|
||||
_reader = File.OpenText(csvFile.FullName);
|
||||
var csvConfiguration = new CsvConfiguration {HasHeaderRecord = false};
|
||||
_csv = new CsvReader(_reader, csvConfiguration);
|
||||
}
|
||||
|
||||
public LeafInput Parse()
|
||||
{
|
||||
// First 10 lines
|
||||
var leafInput = ParseLeafInput();
|
||||
leafInput.FileName = _csvFile.Name;
|
||||
|
||||
// Next 3 (Header, Units, and Values)
|
||||
leafInput.Site = ParseLeafInputSite();
|
||||
|
||||
// Next 3 (Header, Units, and Values)
|
||||
leafInput.Photosynthetic = ParseLeafInputPhotosynthetic();
|
||||
|
||||
// Remaining lines (Header, Units, and Data)
|
||||
leafInput.Data = ParseLeafInputData();
|
||||
|
||||
return leafInput;
|
||||
}
|
||||
|
||||
private LeafInput ParseLeafInput()
|
||||
{
|
||||
var items = new List<string>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
if (!_csv.Read())
|
||||
throw new ParseException("Could not read line number " + _csv.Row);
|
||||
|
||||
string field;
|
||||
if (!_csv.TryGetField(0, out field))
|
||||
throw new ParseException("Could not read first field on line number " + _csv.Row);
|
||||
items.Add(field);
|
||||
}
|
||||
return ParsedObjectFactory<LeafInput>.Create(items.ToArray());
|
||||
}
|
||||
|
||||
private string[] GetNextCsvRowValues()
|
||||
{
|
||||
// get values from row
|
||||
if (!_csv.Read())
|
||||
return null;
|
||||
|
||||
// put all the values from this row into an array
|
||||
var values = new List<string>();
|
||||
var index = 0;
|
||||
string value;
|
||||
while (_csv.TryGetField(index, out value))
|
||||
{
|
||||
values.Add(value);
|
||||
index++;
|
||||
}
|
||||
return values.ToArray();
|
||||
}
|
||||
|
||||
private LeafInputSite ParseLeafInputSite()
|
||||
{
|
||||
var titles = GetNextCsvRowValues();
|
||||
if (titles == null)
|
||||
throw new ParseException("Could not read site header row on line number " + _csv.Row);
|
||||
var units = GetNextCsvRowValues();
|
||||
if (units == null)
|
||||
throw new ParseException("Could not read site units row on line number " + _csv.Row);
|
||||
var values = GetNextCsvRowValues();
|
||||
if (values == null)
|
||||
throw new ParseException("Could not read site value row on line number " + _csv.Row);
|
||||
|
||||
var items = new List<Tuple<string, string>>();
|
||||
for (var i = 0; i < titles.Length && i < values.Length; i++)
|
||||
{
|
||||
var item = Tuple.Create(titles[i], values[i]);
|
||||
items.Add(item);
|
||||
}
|
||||
return ParsedObjectFactory<LeafInputSite>.Create(items.ToArray());
|
||||
}
|
||||
|
||||
private LeafInputPhotosynthetic ParseLeafInputPhotosynthetic()
|
||||
{
|
||||
var titles = GetNextCsvRowValues();
|
||||
if (titles == null)
|
||||
throw new ParseException("Could not read photosynthetic header row on line number " + _csv.Row);
|
||||
var units = GetNextCsvRowValues();
|
||||
if (units == null)
|
||||
throw new ParseException("Could not read photosynthetic units row on line number " + _csv.Row);
|
||||
var values = GetNextCsvRowValues();
|
||||
if (values == null)
|
||||
throw new ParseException("Could not read photosynthetic value row on line number " + _csv.Row);
|
||||
|
||||
var items = new List<Tuple<string, string>>();
|
||||
for (var i = 0; i < titles.Length && i < values.Length; i++)
|
||||
{
|
||||
var item = Tuple.Create(titles[i], values[i]);
|
||||
items.Add(item);
|
||||
}
|
||||
return ParsedObjectFactory<LeafInputPhotosynthetic>.Create(items.ToArray());
|
||||
}
|
||||
|
||||
private LeafInputData[] ParseLeafInputData()
|
||||
{
|
||||
var titles = GetNextCsvRowValues();
|
||||
if (titles == null)
|
||||
throw new ParseException("Could not read data header row on line number " + _csv.Row);
|
||||
var units = GetNextCsvRowValues();
|
||||
if (units == null)
|
||||
throw new ParseException("Could not read data units row on line number " + _csv.Row);
|
||||
|
||||
var valueArrays = new List<string[]>();
|
||||
while (true)
|
||||
{
|
||||
var values = GetNextCsvRowValues();
|
||||
if (values == null)
|
||||
break;
|
||||
valueArrays.Add(values);
|
||||
}
|
||||
return ParsedObjectFactory<LeafInputData>.Create(titles, valueArrays.ToArray());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_reader.Dispose();
|
||||
}
|
||||
|
||||
public static void ExportCsv(string filename, IEnumerable<LeafInput> leafInputs)
|
||||
{
|
||||
using (var textWriter = new StreamWriter(filename))
|
||||
{
|
||||
var csv = new CsvWriter(textWriter);
|
||||
csv.WriteRecords(leafInputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace LeafWeb.Core.Services
|
||||
{
|
||||
public class ParseException : Exception
|
||||
{
|
||||
public ParseException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using LeafWeb.Core.Utility;
|
||||
|
||||
namespace LeafWeb.Core.Services
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public class ParseInfoAttribute : Attribute
|
||||
{
|
||||
public int Position { get; private set; }
|
||||
public string Units { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string AlterateTitle { get; private set; }
|
||||
public string ExampleValue { get; private set; }
|
||||
public string[] FieldNames
|
||||
{
|
||||
get
|
||||
{
|
||||
var fieldNames = new[] {Title, Title.SplitCamelCase()};
|
||||
if (!string.IsNullOrEmpty(AlterateTitle))
|
||||
fieldNames = fieldNames.Concat(new[] { AlterateTitle }).ToArray();
|
||||
return fieldNames;
|
||||
}
|
||||
}
|
||||
|
||||
public ParseInfoAttribute(int position, [CallerMemberName]string title = null, string units = null, string alternateTitle = null, string exampleValue = null)
|
||||
{
|
||||
AlterateTitle = alternateTitle;
|
||||
ExampleValue = exampleValue;
|
||||
Title = title;
|
||||
Position = position;
|
||||
Units = units;
|
||||
}
|
||||
|
||||
public bool IsTitleMatch(string title)
|
||||
{
|
||||
return FieldNames.Any(t => string.Compare(title, t, StringComparison.InvariantCultureIgnoreCase) == 0);
|
||||
}
|
||||
|
||||
public bool IsPositionMatch(int index)
|
||||
{
|
||||
return Position == index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Fasterflect;
|
||||
|
||||
namespace LeafWeb.Core.Services
|
||||
{
|
||||
public static class ParsedObjectFactory<T> where T : new()
|
||||
{
|
||||
private static PropertyInfo[] GetProperties()
|
||||
{
|
||||
var propertyInfos = typeof(T).Properties();
|
||||
return propertyInfos.Where(p => p.HasAttribute<ParseInfoAttribute>()).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an object type T filling properties from the given title values
|
||||
/// </summary>
|
||||
/// <param name="titleValues">Colon separated title: values</param>
|
||||
public static T Create(string[] titleValues)
|
||||
{
|
||||
var properties = GetProperties();
|
||||
var obj = new T();
|
||||
// take each of the
|
||||
for (var index = 0; index < titleValues.Length; index++)
|
||||
{
|
||||
PropertyInfo property = null;
|
||||
string value = null;
|
||||
|
||||
var lineNumber = index + 1;
|
||||
var row = titleValues[index];
|
||||
var split = row.Split(':');
|
||||
if (split.Length > 1)
|
||||
{
|
||||
// handles case for "Title : Value"
|
||||
var title = split[0].Trim();
|
||||
value = split[1].Trim();
|
||||
property =
|
||||
properties
|
||||
.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsTitleMatch(title));
|
||||
}
|
||||
if (property == null)
|
||||
{
|
||||
// handles case for row number, i.e. "Value"
|
||||
value = row.Trim();
|
||||
property =
|
||||
properties
|
||||
.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsPositionMatch(lineNumber));
|
||||
}
|
||||
|
||||
if (property != null)
|
||||
{
|
||||
object convertedVal;
|
||||
if (!TryConvertValue(property, value, out convertedVal))
|
||||
throw new ParseException(string.Format("Cannot convert value '{0}' for {1} at line number {2}", value, property.Name, lineNumber));
|
||||
property.Set(obj, convertedVal);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static T[] Create(string[] titles, string[][] valueArrays)
|
||||
{
|
||||
var properties = GetProperties();
|
||||
var objs = new T[valueArrays.Length];
|
||||
|
||||
for (var vIndex = 0; vIndex < valueArrays.Length; vIndex++)
|
||||
{
|
||||
var obj = new T();
|
||||
var values = valueArrays[vIndex];
|
||||
for (var tIndex = 0; tIndex < titles.Length; tIndex++)
|
||||
{
|
||||
var title = titles[tIndex];
|
||||
var value = values[tIndex];
|
||||
var position = tIndex + 1;
|
||||
|
||||
if (IsMissingValue(value))
|
||||
continue;
|
||||
|
||||
var property = MatchProperty(properties, title, position);
|
||||
|
||||
if (property != null)
|
||||
{
|
||||
object convertedVal;
|
||||
if (!TryConvertValue(property, value, out convertedVal))
|
||||
throw new ParseException(string.Format("Cannot convert value '{0}' for {1} in position {2}", value, property.Name, position));
|
||||
property.Set(obj, convertedVal);
|
||||
}
|
||||
}
|
||||
objs[vIndex] = obj;
|
||||
}
|
||||
return objs;
|
||||
}
|
||||
|
||||
public static T Create(Tuple<string, string>[] titleValues)
|
||||
{
|
||||
var properties = GetProperties();
|
||||
var obj = new T();
|
||||
|
||||
for (var index = 0; index < titleValues.Length; index++)
|
||||
{
|
||||
var item = titleValues[index];
|
||||
var position = index + 1;
|
||||
var title = item.Item1.Trim();
|
||||
var value = item.Item2.Trim();
|
||||
|
||||
if (IsMissingValue(value))
|
||||
continue;
|
||||
|
||||
var property = MatchProperty(properties, title, position);
|
||||
|
||||
if (property != null)
|
||||
{
|
||||
object convertedVal;
|
||||
if (!TryConvertValue(property, value, out convertedVal))
|
||||
throw new ParseException(string.Format("Cannot convert value '{0}' for {1} in position {2}", value, property.Name, position));
|
||||
property.Set(obj, convertedVal);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static bool IsMissingValue(string value)
|
||||
{
|
||||
return string.IsNullOrEmpty(value) || value == "NA" || value == "-9999";
|
||||
}
|
||||
|
||||
private static PropertyInfo MatchProperty(PropertyInfo[] properties, string title, int position)
|
||||
{
|
||||
var property =
|
||||
properties
|
||||
.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsTitleMatch(title));
|
||||
|
||||
if (property == null)
|
||||
{
|
||||
property =
|
||||
properties
|
||||
.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsPositionMatch(position));
|
||||
}
|
||||
return property;
|
||||
}
|
||||
|
||||
private static bool TryConvertValue(PropertyInfo property, object value, out object convertedValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
// http://stackoverflow.com/questions/3531318/convert-changetype-fails-on-nullable-types
|
||||
var t = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
convertedValue = Convert.ChangeType(value, t);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
convertedValue = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user