Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Submit CH3 HW 11 #803

Open
wants to merge 1 commit into
base: Chapter3/Homework/11
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Src/BootCamp.Chapter/BootCamp.Chapter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

<ItemGroup>
<None Update="Input\Transactions.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
22 changes: 22 additions & 0 deletions Src/BootCamp.Chapter/EarningsDTO.cs
Original file line number Diff line number Diff line change
@@ -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<TimeDTO> 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;}
}
}
40 changes: 32 additions & 8 deletions Src/BootCamp.Chapter/Program.cs
Original file line number Diff line number Diff line change
@@ -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<Transaction> 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);
}
}
}
96 changes: 96 additions & 0 deletions Src/BootCamp.Chapter/Transaction.cs
Original file line number Diff line number Diff line change
@@ -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<Transaction> ToTransaction(string filePath)
{
if (!File.Exists(filePath)) throw new NoTransactionsFoundException();

List<Transaction> list = new List<Transaction>();
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;
}
}
}
191 changes: 191 additions & 0 deletions Src/BootCamp.Chapter/TransactionCommand.cs
Original file line number Diff line number Diff line change
@@ -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, Func<IEnumerable<Transaction>, string, object>> cmds = new Dictionary<string, Func<IEnumerable<Transaction>, string, object>>();

public TransactionCommand()
{
//Setup default command options
cmds.Add(TransactionCommand.Time, TransactionCommand.TimeCmd);
cmds.Add(TransactionCommand.City, TransactionCommand.CityCmd);
}

public object RunCmd(IEnumerable<Transaction> 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<Transaction> 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<Transaction> 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<int>(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<decimal>(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<T>(IEnumerable<T> 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
}
}


}
Loading