/// <summary> /// Compares two <see cref="FuelTaxDetail"/> elements based on their attributes. /// </summary> /// <param name="detail1">The first detail.</param> /// <param name="detail2">The second detail.</param> /// <param name="options">The grouping options.</param> /// <returns><c>true</c> iff both details share the same attributes, depending on the <see cref="options"/>.</returns> static bool AreAttributesEqual(FuelTaxDetail detail1, FuelTaxDetail detail2, dynamic options) { return(detail1.Jurisdiction == detail2.Jurisdiction && (!options.DriverIdentification || detail1.Driver.Equals(detail2.TollRoad)) && (!options.TollRoadIdentification || detail1.TollRoad == detail2.TollRoad) && (!options.AuthorityIdentification || detail1.Authority == detail2.Authority)); }
/// <summary> /// Clips a <see cref="FuelTaxDetail"/> at the nearest hour at or before a given time. /// </summary> /// <param name="detail">The detail.</param> /// <param name="dateTime">The time.</param> static void TrimRight(FuelTaxDetail detail, DateTime dateTime) { var hour = dateTime.Ticks / HourTicks; var exitHour = detail.ExitTime.Ticks / HourTicks; if (detail.ExitTime.Ticks % HourTicks > 0) { exitHour++; } if (hour < exitHour) { var hourCount = detail.HourlyOdometer.Count; var hourIndex = (int)(hourCount - exitHour + hour); detail.ExitTime = new DateTime(hour * HourTicks, DateTimeKind.Utc); detail.ExitOdometer = detail.HourlyOdometer[hourIndex]; detail.ExitGpsOdometer = detail.HourlyGpsOdometer[hourIndex]; detail.ExitLatitude = detail.HourlyLatitude[hourIndex]; detail.ExitLongitude = detail.HourlyLongitude[hourIndex]; var removeCount = hourCount - hourIndex; detail.HourlyOdometer.RemoveRange(hourIndex, removeCount); detail.HourlyGpsOdometer.RemoveRange(hourIndex, removeCount); detail.HourlyLatitude.RemoveRange(hourIndex, removeCount); detail.HourlyLongitude.RemoveRange(hourIndex, removeCount); } }
/// <summary> /// Groups successive <see cref="FuelTaxDetail"/> elements by their attributes, depending on the <see cref="options"/>. /// </summary> /// <param name="details">The details.</param> /// <param name="options">The options.</param> /// <returns>A list of detail groups.</returns> static List <List <FuelTaxDetail> > Group(IList <FuelTaxDetail> details, dynamic options) { List <List <FuelTaxDetail> > groups = new List <List <FuelTaxDetail> > { new List <FuelTaxDetail>() }; List <FuelTaxDetail> group = groups[0]; FuelTaxDetail previousDetail = null; foreach (var detail in details) { if (previousDetail == null || AreAttributesEqual(detail, previousDetail, options)) { group.Add(detail); } else { group = new List <FuelTaxDetail> { detail }; groups.Add(group); } previousDetail = detail; } return(groups); }
/// <summary> /// Clips a <see cref="FuelTaxDetail"/> at the nearest hour at or before a given time. /// </summary> /// <param name="detail">The detail.</param> /// <param name="dateTime">The time.</param> static void TrimLeft(FuelTaxDetail detail, DateTime dateTime) { var hour = dateTime.Ticks / HourTicks; var enterHour = detail.EnterTime.Ticks / HourTicks; if (hour > enterHour) { var hourIndex = (int)(hour - enterHour - 1); detail.EnterTime = new DateTime(hour * HourTicks, DateTimeKind.Utc); detail.EnterOdometer = detail.HourlyOdometer[hourIndex]; detail.EnterGpsOdometer = detail.HourlyGpsOdometer[hourIndex]; detail.EnterLatitude = detail.HourlyLatitude[hourIndex]; detail.EnterLongitude = detail.HourlyLongitude[hourIndex]; var removeCount = hourIndex + 1; detail.HourlyOdometer.RemoveRange(0, removeCount); detail.HourlyGpsOdometer.RemoveRange(0, removeCount); detail.HourlyLatitude.RemoveRange(0, removeCount); detail.HourlyLongitude.RemoveRange(0, removeCount); } }
/// <summary> /// Calculates fuel usage for a collection of fuel tax details, classified by jurisdiction and fuel type. /// </summary> /// <param name="api">The Geotab API.</param> /// <param name="device">The device.</param> /// <param name="details">The fuel tax details.</param> /// <returns>A list of fuel tax data objects.</returns> static Dictionary <string, Dictionary <FuelType, FuelUsage> > GetFuelUsageByJurisdiction(API api, GoDevice device, IList <FuelTaxDetail> details) { var fuelUsageByJurisdiction = new Dictionary <string, Dictionary <FuelType, FuelUsage> >(); // Get the details' time interval. DateTime fromDate = details[0].EnterTime; DateTime toDate = details[details.Count - 1].ExitTime; // Get the fuel transactions within the details' time interval. Search fuelTransactionSearch = new FuelTransactionSearch { VehicleIdentificationNumber = device.VehicleIdentificationNumber, FromDate = fromDate, ToDate = toDate }; List <FuelTransaction> fuelTransactions = api.Call <IList <FuelTransaction> >("Get", typeof(FuelTransaction), new { search = fuelTransactionSearch }).ToList(); fuelTransactions.Sort((transaction1, transaction2) => DateTime.Compare(transaction1.DateTime.Value, transaction2.DateTime.Value)); // Calculate total purchased fuel by fuel type and jurisdiction. Dictionary <FuelType, double> fuelPurchasedByType = new Dictionary <FuelType, double>(); double totalDistance = 0; foreach (var fuelTransaction in fuelTransactions) { // Locate the fuel transaction within a detail. FuelTaxDetail detail = details.Last(x => x.EnterTime <= fuelTransaction.DateTime); // Update jurisdiction fuel usage. string jurisdiction = detail.Jurisdiction; if (jurisdiction != null) { if (!fuelUsageByJurisdiction.TryGetValue(jurisdiction, out Dictionary <FuelType, FuelUsage> fuelUsageByType)) { fuelUsageByType = new Dictionary <FuelType, FuelUsage>(); fuelUsageByJurisdiction.Add(jurisdiction, fuelUsageByType); } FuelTransactionProductType?productType = fuelTransaction.ProductType; FuelType fuelType = productType == null ? FuelType.None : GetFuelType(productType.Value); if (!fuelUsageByType.TryGetValue(fuelType, out FuelUsage fuelUsage)) { fuelUsage = new FuelUsage(); fuelUsageByType.Add(fuelType, fuelUsage); } double volume = fuelTransaction.Volume.Value; fuelUsage.FuelPurchased += volume; if (!fuelPurchasedByType.ContainsKey(fuelType)) { fuelPurchasedByType.Add(fuelType, 0); } fuelPurchasedByType[fuelType] += volume; } } // Resolve fuel type None into the fuel type with the largest purchased volume. if (fuelPurchasedByType.TryGetValue(FuelType.None, out double _)) { FuelType topFuelType = FuelType.None; double topVolume = 0; foreach (var fuelTypePurchased in fuelPurchasedByType) { FuelType fuelType = fuelTypePurchased.Key; if (fuelType != FuelType.None) { double volume = fuelTypePurchased.Value; if (volume > topVolume) { topVolume = volume; topFuelType = fuelType; } } } if (topFuelType != FuelType.None) { fuelPurchasedByType[topFuelType] += fuelPurchasedByType[FuelType.None]; fuelPurchasedByType.Remove(FuelType.None); } foreach (var fuelUsageByType in fuelUsageByJurisdiction.Values) { if (fuelUsageByType.TryGetValue(FuelType.None, out FuelUsage typeNoneFuelUsage)) { fuelUsageByType[topFuelType].FuelPurchased += typeNoneFuelUsage.FuelPurchased; fuelUsageByType.Remove(FuelType.None); } } } // Create fuel usage stumps for jurisdictions where no fuel transactions occurred. foreach (var detail in details) { string jurisdiction = detail.Jurisdiction; if (jurisdiction != null) { if (!fuelUsageByJurisdiction.TryGetValue(jurisdiction, out Dictionary <FuelType, FuelUsage> fuelUsageByType)) { fuelUsageByType = new Dictionary <FuelType, FuelUsage>(); fuelUsageByJurisdiction.Add(jurisdiction, fuelUsageByType); } double detailDistance = detail.ExitOdometer - detail.EnterOdometer; foreach (var fuelType in fuelPurchasedByType.Keys) { if (!fuelUsageByType.TryGetValue(fuelType, out FuelUsage fuelUsage)) { fuelUsage = new FuelUsage(); fuelUsageByType.Add(fuelType, fuelUsage); } fuelUsage.Distance += detailDistance; } totalDistance += detailDistance; } } // Calculate fuel economy for each fuel type. Fill in fuel usage per jurisdiction and fuel type. foreach (var typeVolume in fuelPurchasedByType) { FuelType fuelType = typeVolume.Key; double totalVolume = typeVolume.Value; double volumePerKm = totalVolume / totalDistance; foreach (var fuelUsageByType in fuelUsageByJurisdiction.Values) { FuelUsage fuelUsage = fuelUsageByType[fuelType]; fuelUsage.FuelUsed = volumePerKm * fuelUsage.Distance; fuelUsage.FuelEconomy = volumePerKm * 100; } } return(fuelUsageByJurisdiction); }