From b78ab6614dbc12fd88712f2d59e70da6948e29b2 Mon Sep 17 00:00:00 2001 From: Michael Krisper <michael.krisper@tugraz.at> Date: Tue, 2 Aug 2016 17:07:56 +0200 Subject: [PATCH] CSV File: Robust CSV Reader (from VB.NET), Updated Tests for Aux Electric System --- .../Models/Declaration/ElectricSystem.cs | 11 +- VectoCore/VectoCore/Utils/StreamExtensions.cs | 18 +++ VectoCore/VectoCore/Utils/VectoCSVFile.cs | 103 +++++++----------- VectoCore/VectoCore/VectoCore.csproj | 2 + .../VectoCoreTest/FileIO/VectoCSVFileTest.cs | 39 +++++++ .../Models/Declaration/DeclarationDataTest.cs | 67 ++++-------- 6 files changed, 128 insertions(+), 112 deletions(-) create mode 100644 VectoCore/VectoCore/Utils/StreamExtensions.cs diff --git a/VectoCore/VectoCore/Models/Declaration/ElectricSystem.cs b/VectoCore/VectoCore/Models/Declaration/ElectricSystem.cs index 5293685895..62943196d5 100644 --- a/VectoCore/VectoCore/Models/Declaration/ElectricSystem.cs +++ b/VectoCore/VectoCore/Models/Declaration/ElectricSystem.cs @@ -56,14 +56,17 @@ namespace TUGraz.VectoCore.Models.Declaration NormalizeTable(table); foreach (DataRow row in table.Rows) { - var name = row.Field<string>("Technology"); - foreach (MissionType mission in Enum.GetValues(typeof(MissionType))) { - Data[Tuple.Create(mission, name)] = row.ParseDouble(mission.ToString().ToLower()).SI<Watt>(); + var name = row.Field<string>("technology"); + foreach (DataColumn col in table.Columns) { + if (col.Caption != "technology") { + Data[Tuple.Create(col.Caption.ParseEnum<MissionType>(), name)] = + row.ParseDouble(col).SI<Watt>(); + } } } } - public override Watt Lookup(MissionType missionType, string technology) + public override Watt Lookup(MissionType missionType, string technology = "Standard technology") { var value = base.Lookup(missionType, technology); return value / _alternator.Lookup(missionType); diff --git a/VectoCore/VectoCore/Utils/StreamExtensions.cs b/VectoCore/VectoCore/Utils/StreamExtensions.cs new file mode 100644 index 0000000000..7ff3dff43a --- /dev/null +++ b/VectoCore/VectoCore/Utils/StreamExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace TUGraz.VectoCore.Utils +{ + internal static class StreamExtensions + { + public static IEnumerable<string> ReadLines(this Stream stream) + { + using (var reader = new StreamReader(stream, Encoding.UTF8)) { + while (!reader.EndOfStream) { + yield return reader.ReadLine(); + } + } + } + } +} \ No newline at end of file diff --git a/VectoCore/VectoCore/Utils/VectoCSVFile.cs b/VectoCore/VectoCore/Utils/VectoCSVFile.cs index 5c9f77ca31..0f46cd2a54 100644 --- a/VectoCore/VectoCore/Utils/VectoCSVFile.cs +++ b/VectoCore/VectoCore/Utils/VectoCSVFile.cs @@ -37,6 +37,7 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Microsoft.VisualBasic.FileIO; using TUGraz.VectoCommon.Exceptions; using TUGraz.VectoCommon.Models; using TUGraz.VectoCommon.Utils; @@ -59,8 +60,8 @@ namespace TUGraz.VectoCore.Utils public static class VectoCSVFile { private static readonly Regex HeaderFilter = new Regex(@"\[.*?\]|\<|\>", RegexOptions.Compiled); - private const char Delimiter = ','; - private const char Comment = '#'; + private const string Delimiter = ","; + private const string Comment = "#"; /// <summary> /// Reads a CSV file which is stored in Vecto-CSV-Format. @@ -90,84 +91,59 @@ namespace TUGraz.VectoCore.Utils /// <returns>A DataTable which represents the CSV File.</returns> public static DataTable ReadStream(Stream stream, bool ignoreEmptyColumns = false, bool fullHeader = false) { - try { - return ReadData(ReadLines(stream), ignoreEmptyColumns, fullHeader); - } catch (Exception e) { - LogManager.GetLogger(typeof(VectoCSVFile).FullName).Error(e); - throw new VectoException("Failed to read stream: " + e.Message, e); - } - } + var p = new TextFieldParser(stream) { + TextFieldType = FieldType.Delimited, + Delimiters = new[] { Delimiter }, + CommentTokens = new[] { Comment }, + HasFieldsEnclosedInQuotes = true, + TrimWhiteSpace = true + }; - private static IEnumerable<string> ReadLines(Stream stream) - { - using (var reader = new StreamReader(stream, Encoding.UTF8)) { - while (!reader.EndOfStream) { - yield return reader.ReadLine(); - } - } - } + string[] colsWithoutComment; - /// <summary> - /// - /// </summary> - /// <param name="allLines"></param> - /// <param name="ignoreEmptyColumns"></param> - /// <param name="fullHeader"></param> - /// <returns></returns> - private static DataTable ReadData(IEnumerable<string> allLines, bool ignoreEmptyColumns = false, - bool fullHeader = false) - { - // trim, remove comments and filter empty lines - var lines = allLines - .Select(l => l.Trim()) - .Select(l => l.Contains(Comment) ? l.Substring(0, l.IndexOf(Comment)) : l) - .Where(l => !string.IsNullOrWhiteSpace(l)) - .GetEnumerator(); - - // start the enumerable - lines.MoveNext(); - - // add columns - var line = lines.Current; - if (!fullHeader) { - line = HeaderFilter.Replace(line, ""); + try { + colsWithoutComment = p.ReadFields() + .Select(l => l.Contains(Comment) ? l.Substring(0, l.IndexOf(Comment)) : l) + .ToArray(); + } catch (ArgumentNullException) { + throw new CSVReadException("CSV Read Error: File was empty."); } - double tmp; - var splittedColumns = line - .Split(Delimiter); - var columns = splittedColumns - .Select(col => col.Trim()) + double tmp; + var columns = colsWithoutComment + .Select(l => fullHeader ? l : HeaderFilter.Replace(l, "")) + .Select(l => l.Trim()) .Where(col => !double.TryParse(col, NumberStyles.Any, CultureInfo.InvariantCulture, out tmp)) .ToList(); - if (columns.Count > 0) { - // first line was a valid header: advance to first data line - lines.MoveNext(); - } else { + var firstLineIsData = columns.Count == 0; + + if (firstLineIsData) { LogManager.GetLogger(typeof(VectoCSVFile).FullName) .Warn("No valid Data Header found. Interpreting the first line as data line."); // set the validColumns to: {"0", "1", "2", "3", ...} for all columns in first line. - columns = splittedColumns.Select((_, index) => index.ToString()).ToList(); + columns = colsWithoutComment.Select((_, i) => i.ToString()).ToList(); } var table = new DataTable(); foreach (var col in columns) { table.Columns.Add(col); } - if (lines.Current == null) { + + if (p.EndOfData) return table; - } - // read data into table - var i = 0; - do { - i++; - line = lines.Current; - var cells = line.Split(Delimiter).Select(s => s.Trim()).ToArray(); - if (cells.Length != table.Columns.Count && !ignoreEmptyColumns) { + do { + var cells = firstLineIsData + ? colsWithoutComment + : p.ReadFields() + .Select(l => l.Contains(Comment) ? l.Substring(0, l.IndexOf(Comment)) : l) + .Select(s => s.Trim()) + .ToArray(); + firstLineIsData = false; + if (table.Columns.Count != cells.Length && !ignoreEmptyColumns) { throw new CSVReadException( - string.Format("Line {0}: The number of values is not correct. Expected {1} Columns, Got {2} Columns", i, + string.Format("Line {0}: The number of values is not correct. Expected {1} Columns, Got {2} Columns", p.LineNumber, table.Columns.Count, cells.Length)); } @@ -176,9 +152,10 @@ namespace TUGraz.VectoCore.Utils table.Rows.Add(cells); } catch (InvalidCastException e) { throw new CSVReadException( - string.Format("Line {0}: The data format of a value is not correct. {1}", i, e.Message), e); + string.Format("Line {0}: The data format of a value is not correct. {1}", p.LineNumber, e.Message), e); } - } while (lines.MoveNext()); + } while (!p.EndOfData); + return table; } diff --git a/VectoCore/VectoCore/VectoCore.csproj b/VectoCore/VectoCore/VectoCore.csproj index 9fa840bdcc..1161a099e8 100644 --- a/VectoCore/VectoCore/VectoCore.csproj +++ b/VectoCore/VectoCore/VectoCore.csproj @@ -87,6 +87,7 @@ <Reference Include="itextsharp"> <HintPath>..\..\packages\iTextSharp.5.5.9\lib\itextsharp.dll</HintPath> </Reference> + <Reference Include="Microsoft.VisualBasic" /> <Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> <HintPath>..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> @@ -281,6 +282,7 @@ <Compile Include="Models\Simulation\DataBus\IVehicleInfo.cs" /> <Compile Include="Models\Simulation\IVehicleContainer.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Utils\StreamExtensions.cs" /> <Compile Include="Utils\SwitchExtension.cs" /> <Compile Include="Utils\VectoCSVFile.cs" /> <Compile Include="Utils\DelaunayMap.cs" /> diff --git a/VectoCore/VectoCoreTest/FileIO/VectoCSVFileTest.cs b/VectoCore/VectoCoreTest/FileIO/VectoCSVFileTest.cs index 450e460d61..aaac221396 100644 --- a/VectoCore/VectoCoreTest/FileIO/VectoCSVFileTest.cs +++ b/VectoCore/VectoCoreTest/FileIO/VectoCSVFileTest.cs @@ -78,6 +78,45 @@ namespace TUGraz.VectoCore.Tests.FileIO CollectionAssert.AreEqual(new[] { "4", "5", "6" }, table.Rows[1].ItemArray); } + [Test] + public void VectoCSVFile_ReadStream_Escaped() + { + var stream = "a,b,c\n\"1,1\",2,3\n4,5,6".GetStream(); + var table = VectoCSVFile.ReadStream(stream); + + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, table.Columns.Cast<DataColumn>().Select(c => c.ColumnName)); + Assert.AreEqual(2, table.Rows.Count); + + CollectionAssert.AreEqual(new[] { "1,1", "2", "3" }, table.Rows[0].ItemArray); + CollectionAssert.AreEqual(new[] { "4", "5", "6" }, table.Rows[1].ItemArray); + } + + [Test] + public void VectoCSVFile_ReadStream_Comment() + { + var stream = "a,b,c\n\"1,1\",2,3#asdf\n4,5,6".GetStream(); + var table = VectoCSVFile.ReadStream(stream); + + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, table.Columns.Cast<DataColumn>().Select(c => c.ColumnName)); + Assert.AreEqual(2, table.Rows.Count); + + CollectionAssert.AreEqual(new[] { "1,1", "2", "3" }, table.Rows[0].ItemArray); + CollectionAssert.AreEqual(new[] { "4", "5", "6" }, table.Rows[1].ItemArray); + } + + [Test] + public void VectoCSVFile_ReadStream_EscapedComment() + { + var stream = "a,b,c\n\"1,1\",2,\"3#asdf\"\n4,5,6".GetStream(); + var table = VectoCSVFile.ReadStream(stream); + + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, table.Columns.Cast<DataColumn>().Select(c => c.ColumnName)); + Assert.AreEqual(2, table.Rows.Count); + + CollectionAssert.AreEqual(new[] { "1,1", "2", "3" }, table.Rows[0].ItemArray); + CollectionAssert.AreEqual(new[] { "4", "5", "6" }, table.Rows[1].ItemArray); + } + [Test] public void VectoCSVFile_ReadStream_No_Header() { diff --git a/VectoCore/VectoCoreTest/Models/Declaration/DeclarationDataTest.cs b/VectoCore/VectoCoreTest/Models/Declaration/DeclarationDataTest.cs index 864997088f..8acfdc7976 100644 --- a/VectoCore/VectoCoreTest/Models/Declaration/DeclarationDataTest.cs +++ b/VectoCore/VectoCoreTest/Models/Declaration/DeclarationDataTest.cs @@ -44,7 +44,6 @@ using TUGraz.VectoCore.InputData.Reader.Impl; using TUGraz.VectoCore.Models.Declaration; using TUGraz.VectoCore.Models.SimulationComponent.Data; using TUGraz.VectoCore.Tests.Utils; -using TUGraz.VectoCore.Utils; namespace TUGraz.VectoCore.Tests.Models.Declaration { @@ -266,52 +265,30 @@ namespace TUGraz.VectoCore.Tests.Models.Declaration torque.SI<NewtonMeter>() * Math.Pow((angularSpeed / referenceSpeed).Cast<Scalar>(), 2), torqueLookup); } - [Test] - public void AuxElectricSystemTest() + [ + TestCase(MissionType.LongHaul, "Standard technology", 1200, 0.7), + TestCase(MissionType.RegionalDelivery, "Standard technology", 1000, 0.7), + TestCase(MissionType.UrbanDelivery, "Standard technology", 1000, 0.7), + TestCase(MissionType.MunicipalUtility, "Standard technology", 1000, 0.7), + TestCase(MissionType.Construction, "Standard technology", 1000, 0.7), + TestCase(MissionType.LongHaul, "Standard technology - LED headlights, all", 1150, 0.7), + TestCase(MissionType.RegionalDelivery, "Standard technology - LED headlights, all", 950, 0.7), + TestCase(MissionType.UrbanDelivery, "Standard technology - LED headlights, all", 950, 0.7), + TestCase(MissionType.MunicipalUtility, "Standard technology - LED headlights, all", 950, 0.7), + TestCase(MissionType.Construction, "Standard technology - LED headlights, all", 950, 0.7), + ] + public void AuxElectricSystemTest(MissionType mission, string technology, double value, double efficiency) { - var es = DeclarationData.ElectricSystem; - - var expected = new[] { - new { Mission = MissionType.LongHaul, Base = 1240.SI<Watt>(), LED = 1190.SI<Watt>(), Efficiency = 0.7 }, - new { - Mission = MissionType.RegionalDelivery, - Base = 1055.SI<Watt>(), - LED = 1005.SI<Watt>(), - Efficiency = 0.7 - }, - new { - Mission = MissionType.UrbanDelivery, - Base = 974.SI<Watt>(), - LED = 924.SI<Watt>(), - Efficiency = 0.7 - }, - new { - Mission = MissionType.MunicipalUtility, - Base = 974.SI<Watt>(), - LED = 924.SI<Watt>(), - Efficiency = 0.7 - }, - new { - Mission = MissionType.Construction, - Base = 975.SI<Watt>(), - LED = 925.SI<Watt>(), - Efficiency = 0.7 - }, - new { Mission = MissionType.HeavyUrban, Base = 0.SI<Watt>(), LED = 0.SI<Watt>(), Efficiency = 1.0 }, - new { Mission = MissionType.Urban, Base = 0.SI<Watt>(), LED = 0.SI<Watt>(), Efficiency = 1.0 }, - new { Mission = MissionType.Suburban, Base = 0.SI<Watt>(), LED = 0.SI<Watt>(), Efficiency = 1.0 }, - new { Mission = MissionType.Interurban, Base = 0.SI<Watt>(), LED = 0.SI<Watt>(), Efficiency = 1.0 }, - new { Mission = MissionType.Coach, Base = 0.SI<Watt>(), LED = 0.SI<Watt>(), Efficiency = 1.0 } - }; - Assert.AreEqual(expected.Length, Enum.GetValues(typeof(MissionType)).Length); - - foreach (var expectation in expected) { - var baseConsumption = es.Lookup(expectation.Mission, null); - var leds = es.Lookup(expectation.Mission, "LED lights"); + AssertHelper.AreRelativeEqual(value / efficiency, DeclarationData.ElectricSystem.Lookup(mission, technology)); + } - AssertHelper.AreRelativeEqual(expectation.Base / expectation.Efficiency, baseConsumption); - AssertHelper.AreRelativeEqual(expectation.LED / expectation.Efficiency, leds); - } + [ + TestCase(MissionType.Interurban, "Standard technology"), + TestCase(MissionType.LongHaul, "Standard technology - Flux-Compensator") + ] + public void AuxElectricSystem_NotExistingError(MissionType mission, string technology) + { + AssertHelper.Exception<VectoException>(() => { DeclarationData.ElectricSystem.Lookup(mission, technology); }); } [ -- GitLab