From 182ace33eacd020c5340910363e749447175da95 Mon Sep 17 00:00:00 2001 From: TMason11095 Date: Mon, 31 Jul 2023 06:37:09 -0500 Subject: [PATCH] Submit CH3 HW 11: Updated the following for Tests: ProgramTests.Main_When_Valid_MinMax_Command_With_Returns_Expected_Cities_With_Min_Max: -Added "Expected/" to all the expectedFile parameter InlineData entries, as that's where the files were being stored. -Updated all the Tests/Expected files to have the property, Copy to Output Directory, set to Copy if newer, as the files weren't being copied over during testing. -Swapped around the expectedFile parameter InlineData entries to match their respective cmd parameter, as the -money and -items were flipped compared to their expectedFile (cmd "city -money -max" had the expectedFile "CityItemsMax" and cmd "city -items -max" had the expectedFile "CityMoneyMax"). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProgramTests.Main_When_Valid_Time_Command_Creates_File_And_Writes_Stats_For_Every_Hour: -AssertMatchingContents was missing "+ extension" on the OutputFile variable. ProgramTests.Main_When_Valid_Time_Command_With_Range_Creates_File_And_Writes_Stats_For_Every_Hour_Belonging_To_Range: -Main() call was missing "+ extension" on the ValidTransactionsFile variable. ProgramTests.Main_When_Transactions_File_Empty_Or_Not_Found_Throws_NoTransactionsException: -No changes made, but this test will never pass, as the call to Main() only includes the inputFile parameter and doesn't include the command nor the outputFile. So InvalidCommandException will always be thrown before before the inputFile is checked to be able to throw NoTransactionsFoundException. Expected/FullDay.json and .xml: -Hour 23's Earned field is "€10.05". Looking at the Input/Transactions.json and .xml files, the 2 entries for that hour are "€9" and "€12", which average to "€10.5" not "€10.05". Fixed this in both files. Expected/FullDay.json: -"RushHour" has extra spacing before and after the following ":" as well as at the end of the line, which isn't common JSON formatting. Changed ""RushHour" : 22 " to ""RushHour": 22". Expected/FullDay.xml: -Missing Time entry for Hour 0 that the .json version of the file has. -Hour 22 is set as just 2. Expected/Night.json and .xml: -Results in both files are only the Times and don't include RushHour. HW 9 includes RushHour and HW 11 makes no mention that this functionality should be removed. Both the Night and FullDay files also come from the same "time" command. Updated both Night.json and .xml to include RushHour = 22 and follow the formatting from FullDay.json and .xml respectively. Expected/Night.xml: -Hour 22 is set as just 2. -Hour 23's Earned field is "€10.05". The .json version has "€10.5". -------------------------------------------------------------------- -Cloned files over from CH3 HW 9. -Created TransactionDTO and EarningsDTO classes. -Created TransactionSerializer class to serialize/deserialize json/xml files using the new DTO classes. -Updated TransactionCommand class to have command functions return object instead of string to be serialized. -Updated Transaction class to include the string version of the Price property to simplify mapping to the DTO version. -Updated Main() to utilize these new changes. --- Src/BootCamp.Chapter/BootCamp.Chapter.csproj | 5 + Src/BootCamp.Chapter/EarningsDTO.cs | 22 ++ Src/BootCamp.Chapter/Program.cs | 40 +++- Src/BootCamp.Chapter/Transaction.cs | 96 +++++++++ Src/BootCamp.Chapter/TransactionCommand.cs | 191 ++++++++++++++++++ Src/BootCamp.Chapter/TransactionDTO.cs | 28 +++ Src/BootCamp.Chapter/TransactionSerializer.cs | 101 +++++++++ .../BootCamp.Chapter.Tests.csproj | 42 ++++ .../Expected/FullDay.json | 4 +- .../Expected/FullDay.xml | 9 +- .../Expected/Night.json | 47 +++-- .../BootCamp.Chapter.Tests/Expected/Night.xml | 47 +++-- Tests/BootCamp.Chapter.Tests/ProgramTests.cs | 22 +- 13 files changed, 587 insertions(+), 67 deletions(-) create mode 100644 Src/BootCamp.Chapter/EarningsDTO.cs create mode 100644 Src/BootCamp.Chapter/Transaction.cs create mode 100644 Src/BootCamp.Chapter/TransactionCommand.cs create mode 100644 Src/BootCamp.Chapter/TransactionDTO.cs create mode 100644 Src/BootCamp.Chapter/TransactionSerializer.cs diff --git a/Src/BootCamp.Chapter/BootCamp.Chapter.csproj b/Src/BootCamp.Chapter/BootCamp.Chapter.csproj index 7b823c52d..2363decbc 100644 --- a/Src/BootCamp.Chapter/BootCamp.Chapter.csproj +++ b/Src/BootCamp.Chapter/BootCamp.Chapter.csproj @@ -5,6 +5,11 @@ netcoreapp3.1 + + + + + PreserveNewest diff --git a/Src/BootCamp.Chapter/EarningsDTO.cs b/Src/BootCamp.Chapter/EarningsDTO.cs new file mode 100644 index 000000000..619ee4cc2 --- /dev/null +++ b/Src/BootCamp.Chapter/EarningsDTO.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Serialization; + +namespace BootCamp.Chapter +{ + [XmlRoot("Earnings")] + public class EarningsDTO + { + public List Times { get; set; } + public int RushHour { get; set; } + } + + [XmlType("Time")] + public class TimeDTO + { + public int Hour { get; set;} + public int Count { get; set;} + public string Earned { get; set;} + } +} diff --git a/Src/BootCamp.Chapter/Program.cs b/Src/BootCamp.Chapter/Program.cs index bd23746d9..a0437a2b1 100644 --- a/Src/BootCamp.Chapter/Program.cs +++ b/Src/BootCamp.Chapter/Program.cs @@ -1,12 +1,36 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using AutoMapper; namespace BootCamp.Chapter { - public class Program - { - public static void Main(string[] args) - { - - } - } -} + public class Program + { + public static void Main(string[] args) + { + //Args should have 3 variables + if (args.Length != 3) throw new InvalidCommandException(); + //0: ValidTransactionsFile + string transactionFile = args[0]; + //1: cmd + string cmdAndArgs = args[1]; + //2: OutputFile + string outputFile = args[2]; + + //Create serializer obj + TransactionSerializer serializer = new TransactionSerializer(); + + //Grab Transaction objects from given file + List transactions = serializer.DeserializeFile(transactionFile).ToList(); + + //Run command + TransactionCommand transactionCmd = new TransactionCommand(); + object resultDTO = transactionCmd.RunCmd(transactions, cmdAndArgs); + + //Save to output file + serializer.SerializeFile(outputFile, resultDTO); + } + } +} \ No newline at end of file diff --git a/Src/BootCamp.Chapter/Transaction.cs b/Src/BootCamp.Chapter/Transaction.cs new file mode 100644 index 000000000..171a7729c --- /dev/null +++ b/Src/BootCamp.Chapter/Transaction.cs @@ -0,0 +1,96 @@ +using Microsoft.VisualBasic.FileIO; +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Linq; +using System.Globalization; +using System.Diagnostics; +using Newtonsoft.Json.Linq; + +namespace BootCamp.Chapter +{ + public class Transaction + { + public string Shop { get; set; } + public string City { get; set; } + public string Street { get; set; } + public string Item { get; set; } + public DateTime DateTime { get; set; } + private string _price; + public string Price + { + get { return _price; } + set + { + _price = value; + OnPriceChange(); + } + } + public decimal PriceValue { get; private set; } + + private void OnPriceChange() + { + decimal newValue; + if (!decimal.TryParse(Price, NumberStyles.Any, CultureInfo.GetCultureInfo("fr-FR"), out newValue)) throw new FormatException($"{nameof(Transaction.Price)}: {Price} does not have correct decimal formatting."); + PriceValue = newValue; + } + + public Transaction(string shop, string city, string street, string item, DateTime dateTime, string price) + { + Shop = shop; + City = city; + Street = street; + Item = item; + DateTime = dateTime; + Price = price; + } + + public static IEnumerable ToTransaction(string filePath) + { + if (!File.Exists(filePath)) throw new NoTransactionsFoundException(); + + List list = new List(); + using (TextFieldParser parser = new TextFieldParser(filePath)) + { + //Error if file is empty + if (parser.EndOfData) throw new NoTransactionsFoundException(); + + //Set delimiter + parser.SetDelimiters(","); + + //Ignore first line as it's just the property names + parser.ReadLine(); + + //Convert remaining lines to Transaction objects + while (!parser.EndOfData) + { + string[] fields = parser.ReadFields(); + //Error if we don't have 6 fields + if (fields.Length != 6) throw new FormatException($"Line: {string.Join(",", fields)} does not have the correct number of fields."); + + //Convert to correct types + //[0] Shop + string shop = fields[0]; + //[1] City + string city = fields[1]; + //[2] Street + string street = fields[2]; + //[3] Item + string item = fields[3]; + //[4] DateTime + DateTimeOffset dateTimeOffset; + if (!DateTimeOffset.TryParse(fields[4], out dateTimeOffset)) throw new FormatException($"Line: {string.Join(",", fields)} does not have a correct DateTime for {nameof(Transaction.DateTime)}."); + DateTime dateTime = dateTimeOffset.DateTime; + //[5] Price + string price = fields[5]; + + //Create Transaction object + list.Add(new Transaction(shop, city, street, item, dateTime, price)); + } + } + + return list; + } + } +} \ No newline at end of file diff --git a/Src/BootCamp.Chapter/TransactionCommand.cs b/Src/BootCamp.Chapter/TransactionCommand.cs new file mode 100644 index 000000000..79009f7f2 --- /dev/null +++ b/Src/BootCamp.Chapter/TransactionCommand.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Globalization; +using System.Diagnostics; + +namespace BootCamp.Chapter +{ + public class TransactionCommand + { + public static readonly string Time = "time"; + public static readonly string City = "city"; + + //Key: string cmd + //Value: Action that takes string cmdParameters + public Dictionary, string, object>> cmds = new Dictionary, string, object>>(); + + public TransactionCommand() + { + //Setup default command options + cmds.Add(TransactionCommand.Time, TransactionCommand.TimeCmd); + cmds.Add(TransactionCommand.City, TransactionCommand.CityCmd); + } + + public object RunCmd(IEnumerable transactions, string cmdAndArgs) + { + //Error if nothing was given + if (string.IsNullOrWhiteSpace(cmdAndArgs)) throw new InvalidCommandException(); + + //Pull command out + string[] splitCmd = cmdAndArgs.Split(' '); + string cmd = splitCmd[0]; + //Put args back together + string args = splitCmd.Length > 1 ? string.Join(" ", splitCmd[1..]) : string.Empty; + + //Check if cmd is valid + if (!cmds.ContainsKey(cmd)) throw new InvalidCommandException(); + + //Run cmd + return cmds[cmd](transactions, args); + } + + public static object TimeCmd(IEnumerable transactions, string parameters) + { + bool hasParameters = !string.IsNullOrEmpty(parameters); + DateTime startTime = default; + DateTime endTime = default; + + //Parameters should be a time range "20:00-00:00" + if (hasParameters) + { + string formatErrorMessage = $"Command parameter, \"{parameters}\", is not formatted correctly."; + string[] splitTimes = parameters.Split('-'); + //Error if we don't have 2 fields + if (splitTimes.Length != 2) throw new FormatException(formatErrorMessage); + //Get start time + if (!DateTime.TryParseExact(splitTimes[0], "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out startTime)) throw new FormatException(formatErrorMessage); + //Get end time + if (!DateTime.TryParseExact(splitTimes[1], "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out endTime)) throw new FormatException(formatErrorMessage); + } + + //Group transactions by hours + var hoursEarned = from transaction in transactions + group transaction by transaction.DateTime.Hour into hours + select new + { + Hour = hours.Key, + Count = hours.Count(), + DayCount = (from hour in hours select hour.DateTime.Date).Distinct().Count(), + TotalEarned = hours.Sum(transaction => transaction.PriceValue) + } into hours + select new + { + hours.Hour, + hours.Count, + Earned = (hours.TotalEarned / hours.DayCount) + }; + + //Left join to all hours to get a full day list of values + var allHoursEarned = from hour in Enumerable.Range(0, 24).ToList() + join hourEarned in hoursEarned + on hour equals hourEarned.Hour into allHours + from allHour in allHours.DefaultIfEmpty(new + { + Hour = hour, + Count = 0, + Earned = 0m + }) + select allHour; + + //Filter time range if passed in + if (hasParameters) + { + allHoursEarned = from hourEarned in allHoursEarned + where hourEarned.Hour >= startTime.Hour + where hourEarned.Hour <= (endTime.Hour == 0 ? 24 : endTime.Hour)//Change 00:00 end times to 24:00 for range check + select hourEarned; + } + + //Get rush hour (highest earned hour) + int rushHour = (from hourEarned in allHoursEarned + orderby hourEarned.Earned descending + select hourEarned.Hour).First(); + + //Convert to DTO and return + return new EarningsDTO + { + Times = allHoursEarned.Select(h => new TimeDTO + { + Hour = h.Hour, + Count = h.Count, + Earned = string.Format($"€{h.Earned}") + }).ToList(), + RushHour = rushHour + }; + } + public static object CityCmd(IEnumerable transactions, string parameters) + { + //2 parameters required + //Split parameters + string formatErrorMessage = $"Command parameters, \"{parameters}\", are not formatted correctly."; + string[] splitParams = parameters.Split(' '); + //Error if we don't have 2 fields + if (splitParams.Length != 2) throw new FormatException(formatErrorMessage); + //Get what field we're ordering by + CityFilterField filterField; + if (!Enum.TryParse(splitParams[0][1..], out filterField)) throw new FormatException(formatErrorMessage);//Ignore first char as it's '-' + //Get how we're ordering + CityFilterType filterType; + if (!Enum.TryParse(splitParams[1][1..], out filterType)) throw new FormatException(formatErrorMessage);//Ignore first char as it's '-' + + //Group each city + var results = from transaction in transactions + group transaction by transaction.City into cities + select new + { + City = cities.Key, + ItemCount = cities.Count(), + TotalPrice = cities.Sum(c => c.PriceValue) + }; + + //Apply filters + switch (filterField) + { + case CityFilterField.items: + //Order + results = results.OrderBy(c => c.ItemCount); + //Get value from min/max to check for ties + int minMaxCount = GetMinMaxValue(results.Select(r => r.ItemCount), filterType); + //Filter results where only that count matches + results = results.Where(r => r.ItemCount == minMaxCount); + break; + case CityFilterField.money: + //Order + results = results.OrderBy(c => c.TotalPrice); + //Get value from min/max to check for ties + decimal minMaxPrice = GetMinMaxValue(results.Select(r => r.TotalPrice), filterType); + //Filter results where only that count matches + results = results.Where(r => r.TotalPrice == minMaxPrice); + break; + } + + //Combine cities if needed and return + return String.Join(", ", results.Select(r => r.City)); + } + + private static T GetMinMaxValue(IEnumerable values, CityFilterType filterType) + { + return filterType switch + { + CityFilterType.min => values.First(), + CityFilterType.max => values.Last(), + _ => throw new ArgumentException() + }; + } + + private enum CityFilterField + { + items, + money + } + private enum CityFilterType + { + min, + max + } + } + + +} \ No newline at end of file diff --git a/Src/BootCamp.Chapter/TransactionDTO.cs b/Src/BootCamp.Chapter/TransactionDTO.cs new file mode 100644 index 000000000..456bb5414 --- /dev/null +++ b/Src/BootCamp.Chapter/TransactionDTO.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Serialization; + +namespace BootCamp.Chapter +{ + public class TransactionDTO + { + public string Shop { get; set; } + public string City { get; set; } + public string Street { get; set; } + public string Item { get; set; } + public DateTime DateTime { get; set; } + public string Price { get; set; } + } + public class TransactionsDTO + { + [JsonProperty("Transaction")] + public List Transactions { get; set; } + } + public class JsonTransactionsDTO + { + [JsonProperty("Transactions")] + public TransactionsDTO TransactionsDTO { get; set; } + } +} diff --git a/Src/BootCamp.Chapter/TransactionSerializer.cs b/Src/BootCamp.Chapter/TransactionSerializer.cs new file mode 100644 index 000000000..2a557c2ae --- /dev/null +++ b/Src/BootCamp.Chapter/TransactionSerializer.cs @@ -0,0 +1,101 @@ +using AutoMapper; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Serialization; + +namespace BootCamp.Chapter +{ + public class TransactionSerializer + { + private readonly IMapper _mapper = SetupMapper(); + private static IMapper SetupMapper() + { + var config = new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + }); + + return new Mapper(config); + } + + public IEnumerable DeserializeFile(string inputFile) + { + //Validate input file + if (!File.Exists(inputFile)) throw new NoTransactionsFoundException(); + //Prepare DTO list variable + List transactionDTOs = null; + //Get text from file + string fileText = File.ReadAllText(inputFile); + //Check if file is XML or JSON + switch (Path.GetExtension(inputFile)) + { + case ".json": + transactionDTOs = JsonConvert.DeserializeObject>(fileText); + break; + case ".xml": + //Convert to Json first as it's easier to handle (Just needs additional wrapper classes) + XmlDocument xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(fileText); + string jsonFileText = JsonConvert.SerializeXmlNode(xmlDocument); + transactionDTOs = JsonConvert.DeserializeObject(jsonFileText).TransactionsDTO.Transactions; + + //Couldn't figure out how to deserialize a standard XML file + //XmlSerializer xmlSerializer = new XmlSerializer(typeof(TransactionsDTO)); + //using (TextReader reader = new StringReader(fileText)) + //{ + // var test = (TransactionsDTO)xmlSerializer.Deserialize(reader); + //} + break; + } + + //Convert DTO to Transaction obj + return transactionDTOs.Select(dto => _mapper.Map(dto)).ToList(); + } + + public void SerializeFile(string outputFile, object dto) + { + //Serialize based on output file extension type + string resultFileText = Path.GetExtension(outputFile) switch + { + ".json" => JsonConvert.SerializeObject(dto, Newtonsoft.Json.Formatting.Indented), + ".xml" => SerializeXml(dto), + _ => dto.ToString() + }; + + File.WriteAllText(outputFile, resultFileText); + } + + private string SerializeXml(object dto) + { + //Disable xmlns text from file + XmlSerializerNamespaces xmlns = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }); + //Create serializer + XmlSerializer serializer = new XmlSerializer(dto.GetType()); + //Disable xml declarations + XmlWriterSettings settings = new XmlWriterSettings() + { + Indent = true, + OmitXmlDeclaration = true + }; + + string resultText; + using (StringWriter stringWriter = new StringWriter()) + { + using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings)) + { + serializer.Serialize(xmlWriter, dto, xmlns); + resultText = stringWriter.ToString(); + } + } + + return resultText; + } + } +} diff --git a/Tests/BootCamp.Chapter.Tests/BootCamp.Chapter.Tests.csproj b/Tests/BootCamp.Chapter.Tests/BootCamp.Chapter.Tests.csproj index 881fa8126..7117cf713 100644 --- a/Tests/BootCamp.Chapter.Tests/BootCamp.Chapter.Tests.csproj +++ b/Tests/BootCamp.Chapter.Tests/BootCamp.Chapter.Tests.csproj @@ -19,24 +19,66 @@ + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/Tests/BootCamp.Chapter.Tests/Expected/FullDay.json b/Tests/BootCamp.Chapter.Tests/Expected/FullDay.json index b25ad1064..cd3248782 100644 --- a/Tests/BootCamp.Chapter.Tests/Expected/FullDay.json +++ b/Tests/BootCamp.Chapter.Tests/Expected/FullDay.json @@ -118,8 +118,8 @@ { "Hour": 23, "Count": 2, - "Earned": "€10.05" + "Earned": "€10.5" } ], - "RushHour" : 22 + "RushHour": 22 } \ No newline at end of file diff --git a/Tests/BootCamp.Chapter.Tests/Expected/FullDay.xml b/Tests/BootCamp.Chapter.Tests/Expected/FullDay.xml index 17faac45c..5c4e1474d 100644 --- a/Tests/BootCamp.Chapter.Tests/Expected/FullDay.xml +++ b/Tests/BootCamp.Chapter.Tests/Expected/FullDay.xml @@ -1,5 +1,10 @@  + 22 diff --git a/Tests/BootCamp.Chapter.Tests/Expected/Night.json b/Tests/BootCamp.Chapter.Tests/Expected/Night.json index c25a1e147..0222bfaee 100644 --- a/Tests/BootCamp.Chapter.Tests/Expected/Night.json +++ b/Tests/BootCamp.Chapter.Tests/Expected/Night.json @@ -1,22 +1,25 @@ - [ - { - "Hour": 20, - "Count": 3, - "Earned": "€8" - }, - { - "Hour": 21, - "Count": 0, - "Earned": "€0" - }, - { - "Hour": 22, - "Count": 2, - "Earned": "€25" - }, - { - "Hour": 23, - "Count": 2, - "Earned": "€10.5" - } -] +{ + "Times": [ + { + "Hour": 20, + "Count": 3, + "Earned": "€8" + }, + { + "Hour": 21, + "Count": 0, + "Earned": "€0" + }, + { + "Hour": 22, + "Count": 2, + "Earned": "€25" + }, + { + "Hour": 23, + "Count": 2, + "Earned": "€10.5" + } + ], + "RushHour": 22 +} \ No newline at end of file diff --git a/Tests/BootCamp.Chapter.Tests/Expected/Night.xml b/Tests/BootCamp.Chapter.Tests/Expected/Night.xml index 2913f7410..5566aa806 100644 --- a/Tests/BootCamp.Chapter.Tests/Expected/Night.xml +++ b/Tests/BootCamp.Chapter.Tests/Expected/Night.xml @@ -1,22 +1,25 @@ - - - - - - \ No newline at end of file + + + + + + + + 22 + \ No newline at end of file diff --git a/Tests/BootCamp.Chapter.Tests/ProgramTests.cs b/Tests/BootCamp.Chapter.Tests/ProgramTests.cs index f27179eec..4c3682a99 100644 --- a/Tests/BootCamp.Chapter.Tests/ProgramTests.cs +++ b/Tests/BootCamp.Chapter.Tests/ProgramTests.cs @@ -42,7 +42,7 @@ public void Main_When_Valid_Time_Command_Creates_File_And_Writes_Stats_For_Every Program.Main(new []{ ValidTransactionsFile + extension, cmd, OutputFile + extension }); var expectedOutput = "Expected/FullDay" + extension; - AssertMatchingContents(expectedOutput, OutputFile); + AssertMatchingContents(expectedOutput, OutputFile + extension); } [Theory] @@ -52,7 +52,7 @@ public void Main_When_Valid_Time_Command_With_Range_Creates_File_And_Writes_Stat { const string cmd = "time 20:00-00:00"; - Program.Main(new[] { ValidTransactionsFile, cmd, OutputFile + extension }); + Program.Main(new[] { ValidTransactionsFile + extension, cmd, OutputFile + extension }); var expectedOutput = "Expected/Night" + extension; AssertMatchingContents(expectedOutput, OutputFile + extension); @@ -72,15 +72,15 @@ public void Main_When_Valid_DailyRevenue_Command_Creates_File_And_Writes_Revenue } [Theory] - [InlineData("city -money -max", "CityItemsMax", ".json")] - [InlineData("city -money -min", "CityItemsMin", ".json")] - [InlineData("city -items -max", "CityMoneyMax", ".json")] - [InlineData("city -items -min", "CityMoneyMin", ".json")] - [InlineData("city -money -max", "CityItemsMax", ".xml")] - [InlineData("city -money -min", "CityItemsMin", ".xml")] - [InlineData("city -items -max", "CityMoneyMax", ".xml")] - [InlineData("city -items -min", "CityMoneyMin", ".xml")] - public void Main_When_Valid_MinMax_Command_With_Returns_Expected_Cities_With_Min_Max(string cmd, string expectedOutput, string extension) + [InlineData("city -money -max", "Expected/CityMoneyMax", ".json")] + [InlineData("city -money -min", "Expected/CityMoneyMin", ".json")] + [InlineData("city -items -max", "Expected/CityItemsMax", ".json")] + [InlineData("city -items -min", "Expected/CityItemsMin", ".json")] + [InlineData("city -money -max", "Expected/CityMoneyMax", ".xml")] + [InlineData("city -money -min", "Expected/CityMoneyMin", ".xml")] + [InlineData("city -items -max", "Expected/CityItemsMax", ".xml")] + [InlineData("city -items -min", "Expected/CityItemsMin", ".xml")] + public void Main_When_Valid_MinMax_Command_With_Returns_Expected_Cities_With_Min_Max(string cmd, string expectedOutput, string extension) { Program.Main(new[] { ValidTransactionsFile + extension, cmd, OutputFile });