diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..72de34f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,388 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +nuget.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml \ No newline at end of file diff --git a/C#/Car.cs b/C#/Car.cs deleted file mode 100644 index 6015da69..00000000 --- a/C#/Car.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TollFeeCalculator -{ - public class Car : Vehicle - { - public String GetVehicleType() - { - return "Car"; - } - } -} \ No newline at end of file diff --git a/C#/Motorbike.cs b/C#/Motorbike.cs deleted file mode 100644 index 8258109f..00000000 --- a/C#/Motorbike.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TollFeeCalculator -{ - public class Motorbike : Vehicle - { - public string GetVehicleType() - { - return "Motorbike"; - } - } -} diff --git a/C#/TollCalculator.cs b/C#/TollCalculator.cs deleted file mode 100644 index b24c794d..00000000 --- a/C#/TollCalculator.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Globalization; -using TollFeeCalculator; - -public class TollCalculator -{ - - /** - * Calculate the total toll fee for one day - * - * @param vehicle - the vehicle - * @param dates - date and time of all passes on one day - * @return - the total toll fee for that day - */ - - public int GetTollFee(Vehicle vehicle, DateTime[] dates) - { - DateTime intervalStart = dates[0]; - int totalFee = 0; - foreach (DateTime date in dates) - { - int nextFee = GetTollFee(date, vehicle); - int tempFee = GetTollFee(intervalStart, vehicle); - - long diffInMillies = date.Millisecond - intervalStart.Millisecond; - long minutes = diffInMillies/1000/60; - - if (minutes <= 60) - { - if (totalFee > 0) totalFee -= tempFee; - if (nextFee >= tempFee) tempFee = nextFee; - totalFee += tempFee; - } - else - { - totalFee += nextFee; - } - } - if (totalFee > 60) totalFee = 60; - return totalFee; - } - - private bool IsTollFreeVehicle(Vehicle vehicle) - { - if (vehicle == null) return false; - String vehicleType = vehicle.GetVehicleType(); - return vehicleType.Equals(TollFreeVehicles.Motorbike.ToString()) || - vehicleType.Equals(TollFreeVehicles.Tractor.ToString()) || - vehicleType.Equals(TollFreeVehicles.Emergency.ToString()) || - vehicleType.Equals(TollFreeVehicles.Diplomat.ToString()) || - vehicleType.Equals(TollFreeVehicles.Foreign.ToString()) || - vehicleType.Equals(TollFreeVehicles.Military.ToString()); - } - - public int GetTollFee(DateTime date, Vehicle vehicle) - { - if (IsTollFreeDate(date) || IsTollFreeVehicle(vehicle)) return 0; - - int hour = date.Hour; - int minute = date.Minute; - - if (hour == 6 && minute >= 0 && minute <= 29) return 8; - else if (hour == 6 && minute >= 30 && minute <= 59) return 13; - else if (hour == 7 && minute >= 0 && minute <= 59) return 18; - else if (hour == 8 && minute >= 0 && minute <= 29) return 13; - else if (hour >= 8 && hour <= 14 && minute >= 30 && minute <= 59) return 8; - else if (hour == 15 && minute >= 0 && minute <= 29) return 13; - else if (hour == 15 && minute >= 0 || hour == 16 && minute <= 59) return 18; - else if (hour == 17 && minute >= 0 && minute <= 59) return 13; - else if (hour == 18 && minute >= 0 && minute <= 29) return 8; - else return 0; - } - - private Boolean IsTollFreeDate(DateTime date) - { - int year = date.Year; - int month = date.Month; - int day = date.Day; - - if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday) return true; - - if (year == 2013) - { - if (month == 1 && day == 1 || - month == 3 && (day == 28 || day == 29) || - month == 4 && (day == 1 || day == 30) || - month == 5 && (day == 1 || day == 8 || day == 9) || - month == 6 && (day == 5 || day == 6 || day == 21) || - month == 7 || - month == 11 && day == 1 || - month == 12 && (day == 24 || day == 25 || day == 26 || day == 31)) - { - return true; - } - } - return false; - } - - private enum TollFreeVehicles - { - Motorbike = 0, - Tractor = 1, - Emergency = 2, - Diplomat = 3, - Foreign = 4, - Military = 5 - } -} \ No newline at end of file diff --git a/C#/Vehicle.cs b/C#/Vehicle.cs deleted file mode 100644 index 19fe04e4..00000000 --- a/C#/Vehicle.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TollFeeCalculator -{ - public interface Vehicle - { - String GetVehicleType(); - } -} \ No newline at end of file diff --git a/Evolve.sln b/Evolve.sln new file mode 100644 index 00000000..9e6b8c96 --- /dev/null +++ b/Evolve.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30523.141 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TollFeeCalculator.Tests", "TollFeeCalculator.Tests\TollFeeCalculator.Tests.csproj", "{31BD9FC0-2F91-4B4F-A9F3-CA73484918CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TollFeeCalculator", "TollFeeCalculator\TollFeeCalculator.csproj", "{447CB602-0687-413D-A36E-F81D1BBC09F5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {31BD9FC0-2F91-4B4F-A9F3-CA73484918CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31BD9FC0-2F91-4B4F-A9F3-CA73484918CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31BD9FC0-2F91-4B4F-A9F3-CA73484918CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31BD9FC0-2F91-4B4F-A9F3-CA73484918CF}.Release|Any CPU.Build.0 = Release|Any CPU + {447CB602-0687-413D-A36E-F81D1BBC09F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {447CB602-0687-413D-A36E-F81D1BBC09F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {447CB602-0687-413D-A36E-F81D1BBC09F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {447CB602-0687-413D-A36E-F81D1BBC09F5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AA915CBE-0743-4D7F-BACD-1A66EA1C6BC7} + EndGlobalSection +EndGlobal diff --git a/Java/Car.java b/Java/Car.java deleted file mode 100644 index e0ec832c..00000000 --- a/Java/Car.java +++ /dev/null @@ -1,7 +0,0 @@ - -public class Car implements Vehicle { - @Override - public String getType() { - return "Car"; - } -} diff --git a/Java/Motorbike.java b/Java/Motorbike.java deleted file mode 100644 index 14056ff9..00000000 --- a/Java/Motorbike.java +++ /dev/null @@ -1,7 +0,0 @@ - -public class Motorbike implements Vehicle { - @Override - public String getType() { - return "Motorbike"; - } -} diff --git a/Java/TollCalculator.java b/Java/TollCalculator.java deleted file mode 100644 index d239f327..00000000 --- a/Java/TollCalculator.java +++ /dev/null @@ -1,110 +0,0 @@ - -import java.util.*; -import java.util.concurrent.*; - -public class TollCalculator { - - /** - * Calculate the total toll fee for one day - * - * @param vehicle - the vehicle - * @param dates - date and time of all passes on one day - * @return - the total toll fee for that day - */ - public int getTollFee(Vehicle vehicle, Date... dates) { - Date intervalStart = dates[0]; - int totalFee = 0; - for (Date date : dates) { - int nextFee = getTollFee(date, vehicle); - int tempFee = getTollFee(intervalStart, vehicle); - - TimeUnit timeUnit = TimeUnit.MINUTES; - long diffInMillies = date.getTime() - intervalStart.getTime(); - long minutes = timeUnit.convert(diffInMillies, TimeUnit.MILLISECONDS); - - if (minutes <= 60) { - if (totalFee > 0) totalFee -= tempFee; - if (nextFee >= tempFee) tempFee = nextFee; - totalFee += tempFee; - } else { - totalFee += nextFee; - } - } - if (totalFee > 60) totalFee = 60; - return totalFee; - } - - private boolean isTollFreeVehicle(Vehicle vehicle) { - if(vehicle == null) return false; - String vehicleType = vehicle.getType(); - return vehicleType.equals(TollFreeVehicles.MOTORBIKE.getType()) || - vehicleType.equals(TollFreeVehicles.TRACTOR.getType()) || - vehicleType.equals(TollFreeVehicles.EMERGENCY.getType()) || - vehicleType.equals(TollFreeVehicles.DIPLOMAT.getType()) || - vehicleType.equals(TollFreeVehicles.FOREIGN.getType()) || - vehicleType.equals(TollFreeVehicles.MILITARY.getType()); - } - - public int getTollFee(final Date date, Vehicle vehicle) { - if(isTollFreeDate(date) || isTollFreeVehicle(vehicle)) return 0; - Calendar calendar = GregorianCalendar.getInstance(); - calendar.setTime(date); - int hour = calendar.get(Calendar.HOUR_OF_DAY); - int minute = calendar.get(Calendar.MINUTE); - - if (hour == 6 && minute >= 0 && minute <= 29) return 8; - else if (hour == 6 && minute >= 30 && minute <= 59) return 13; - else if (hour == 7 && minute >= 0 && minute <= 59) return 18; - else if (hour == 8 && minute >= 0 && minute <= 29) return 13; - else if (hour >= 8 && hour <= 14 && minute >= 30 && minute <= 59) return 8; - else if (hour == 15 && minute >= 0 && minute <= 29) return 13; - else if (hour == 15 && minute >= 0 || hour == 16 && minute <= 59) return 18; - else if (hour == 17 && minute >= 0 && minute <= 59) return 13; - else if (hour == 18 && minute >= 0 && minute <= 29) return 8; - else return 0; - } - - private Boolean isTollFreeDate(Date date) { - Calendar calendar = GregorianCalendar.getInstance(); - calendar.setTime(date); - int year = calendar.get(Calendar.YEAR); - int month = calendar.get(Calendar.MONTH); - int day = calendar.get(Calendar.DAY_OF_MONTH); - - int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); - if (dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) return true; - - if (year == 2013) { - if (month == Calendar.JANUARY && day == 1 || - month == Calendar.MARCH && (day == 28 || day == 29) || - month == Calendar.APRIL && (day == 1 || day == 30) || - month == Calendar.MAY && (day == 1 || day == 8 || day == 9) || - month == Calendar.JUNE && (day == 5 || day == 6 || day == 21) || - month == Calendar.JULY || - month == Calendar.NOVEMBER && day == 1 || - month == Calendar.DECEMBER && (day == 24 || day == 25 || day == 26 || day == 31)) { - return true; - } - } - return false; - } - - private enum TollFreeVehicles { - MOTORBIKE("Motorbike"), - TRACTOR("Tractor"), - EMERGENCY("Emergency"), - DIPLOMAT("Diplomat"), - FOREIGN("Foreign"), - MILITARY("Military"); - private final String type; - - TollFreeVehicles(String type) { - this.type = type; - } - - public String getType() { - return type; - } - } -} - diff --git a/Java/Vehicle.java b/Java/Vehicle.java deleted file mode 100644 index 289ec7df..00000000 --- a/Java/Vehicle.java +++ /dev/null @@ -1,5 +0,0 @@ - -public interface Vehicle { - - public String getType(); -} diff --git a/C#/LICENSE b/LICENSE similarity index 100% rename from C#/LICENSE rename to LICENSE diff --git a/TollFeeCalculator.Tests/CalendarUtilsTests.cs b/TollFeeCalculator.Tests/CalendarUtilsTests.cs new file mode 100644 index 00000000..9ea095b3 --- /dev/null +++ b/TollFeeCalculator.Tests/CalendarUtilsTests.cs @@ -0,0 +1,68 @@ +using NUnit.Framework; +using TollFeeCalculator.Utils; + +namespace TollFeeCalculator.Tests +{ + [TestFixture] + public class CalendarUtilsTests + { + [SetUp] + public void Setup() + { + } + + [Test] + [TestCase(2019, ExpectedResult = "2019-04-21")] + [TestCase(2020, ExpectedResult = "2020-04-12")] + [TestCase(2021, ExpectedResult = "2021-04-04")] + [TestCase(2022, ExpectedResult = "2022-04-17")] + [TestCase(2023, ExpectedResult = "2023-04-09")] + [TestCase(2024, ExpectedResult = "2024-03-31")] + [TestCase(2025, ExpectedResult = "2025-04-20")] + [TestCase(2026, ExpectedResult = "2026-04-05")] + [TestCase(2027, ExpectedResult = "2027-03-28")] + [TestCase(2028, ExpectedResult = "2028-04-16")] + public string CalculateEasterDay_Returns_Correct_Date(int year) + { + var easterDayDate = CalendarUtils.CalculateEasterDay(year); + + return easterDayDate.ToString("yyyy-MM-dd"); + } + + [Test] + [TestCase(2019, ExpectedResult = "2019-06-22")] + [TestCase(2020, ExpectedResult = "2020-06-20")] + [TestCase(2021, ExpectedResult = "2021-06-26")] + [TestCase(2022, ExpectedResult = "2022-06-25")] + [TestCase(2023, ExpectedResult = "2023-06-24")] + [TestCase(2024, ExpectedResult = "2024-06-22")] + [TestCase(2025, ExpectedResult = "2025-06-21")] + [TestCase(2026, ExpectedResult = "2026-06-20")] + [TestCase(2027, ExpectedResult = "2027-06-26")] + [TestCase(2028, ExpectedResult = "2028-06-24")] + public string CalculateMidsummerDay_Returns_Correct_Date(int year) + { + var midsummerDayDate = CalendarUtils.CalculateMidsummerDay(year); + + return midsummerDayDate.ToString("yyyy-MM-dd"); + } + + [Test] + [TestCase(2019, ExpectedResult = "2019-11-02")] + [TestCase(2020, ExpectedResult = "2020-10-31")] + [TestCase(2021, ExpectedResult = "2021-11-06")] + [TestCase(2022, ExpectedResult = "2022-11-05")] + [TestCase(2023, ExpectedResult = "2023-11-04")] + [TestCase(2024, ExpectedResult = "2024-11-02")] + [TestCase(2025, ExpectedResult = "2025-11-01")] + [TestCase(2026, ExpectedResult = "2026-10-31")] + [TestCase(2027, ExpectedResult = "2027-11-06")] + [TestCase(2028, ExpectedResult = "2028-11-04")] + public string CalculateAllSaintsDay_Returns_Correct_Date(int year) + { + var allSaintsDayDate = CalendarUtils.CalculateAllSaintsDay(year); + + return allSaintsDayDate.ToString("yyyy-MM-dd"); + } + } +} \ No newline at end of file diff --git a/TollFeeCalculator.Tests/DateTimeExtensionsTests.cs b/TollFeeCalculator.Tests/DateTimeExtensionsTests.cs new file mode 100644 index 00000000..956a9314 --- /dev/null +++ b/TollFeeCalculator.Tests/DateTimeExtensionsTests.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using TollFeeCalculator.Utils; + +namespace TollFeeCalculator.Tests +{ + [TestFixture] + public class DateTimeExtensionsTests + { + [SetUp] + public void Setup() + { + } + + private static IEnumerable TimesInsideInterval + { + get + { + yield return new TestCaseData(new DateTime(2021, 1, 1, 10, 0, 0)); + yield return new TestCaseData(new DateTime(2021, 1, 1, 12, 30, 0)); + yield return new TestCaseData(new DateTime(2021, 1, 1, 15, 29, 59)); + } + } + + private static IEnumerable TimesOutsideInterval + { + get + { + yield return new TestCaseData(new DateTime(2021, 1, 1, 9, 59, 59)); + yield return new TestCaseData(new DateTime(2021, 1, 1, 15, 30, 0)); + yield return new TestCaseData(new DateTime(2021, 1, 1, 0, 0, 0)); + } + } + + [Test] + [TestCaseSource(nameof(TimesInsideInterval))] + public void IsBetweenTimes_Returns_True_Inside_Interval(DateTime date) + { + Assert.IsTrue(date.IsBetweenTimes("10:00", "15:30")); + } + + [Test] + [TestCaseSource(nameof(TimesOutsideInterval))] + public void IsBetweenTimes_Returns_False_Outside_Interval(DateTime date) + { + Assert.IsFalse(date.IsBetweenTimes("10:00", "15:30")); + } + } +} diff --git a/TollFeeCalculator.Tests/TollCalculatorTests.cs b/TollFeeCalculator.Tests/TollCalculatorTests.cs new file mode 100644 index 00000000..f7229f48 --- /dev/null +++ b/TollFeeCalculator.Tests/TollCalculatorTests.cs @@ -0,0 +1,160 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using TollFeeCalculator.Models; + +namespace TollFeeCalculator.Tests +{ + [TestFixture] + public class TollCalculatorTests + { + private TollCalculator _tollCalculator; + private static DateTime _goodFridayDate = new DateTime(2021, 4, 2, 8, 0, 0); // Långfredag + private static DateTime _tollFreeDateButNotTollFreeTime = new DateTime(2021, 1, 1, 8, 0, 0); + private static DateTime _notTollFreeDateAndNotTollFreeTime = new DateTime(2021, 1, 4, 7, 0, 0); + private static DateTime _notTollFreeDateButTollFreeTime = new DateTime(2021, 1, 4, 20, 0, 0); + private static IVehicle _tollFreeVehicle = new Motorbike(); + private static IVehicle _notTollFreeVehicle = new Car(); + + [SetUp] + public void Setup() + { + _tollCalculator = new TollCalculator(); + } + + private static IEnumerable TollFreeVehicleTypesTestData + { + get + { + yield return new TestCaseData(_tollFreeVehicle, true); + yield return new TestCaseData(_notTollFreeVehicle, false); + } + } + + private static IEnumerable TollFreeDatesTestData + { + get + { + yield return new TestCaseData(_tollFreeDateButNotTollFreeTime, true); + yield return new TestCaseData(_notTollFreeDateAndNotTollFreeTime, false); + } + } + + private static IEnumerable DatesAtDifferentRushHours + { + get + { + yield return new TestCaseData(_notTollFreeDateAndNotTollFreeTime, 18); // HighRush + yield return new TestCaseData(_notTollFreeDateAndNotTollFreeTime.AddHours(2), 8); // LowRush + yield return new TestCaseData(_notTollFreeDateAndNotTollFreeTime.AddHours(8), 13); // MediumRush + yield return new TestCaseData(_notTollFreeDateAndNotTollFreeTime.AddHours(12), 0); // NoRush + } + } + + [Test] + [TestCaseSource(nameof(TollFreeVehicleTypesTestData))] + public void Total_Fee_Is_Zero_For_TollFree_Vehicles(IVehicle vehicle, bool expectedResult) + { + var dates = new DateTime[] { _notTollFreeDateAndNotTollFreeTime }; + + var totalFee = _tollCalculator.GetTotalTollFeeForDay(vehicle, dates); + var feeIsZero = totalFee == 0; + + Assert.AreEqual(feeIsZero, expectedResult); + } + + [Test] + [TestCaseSource(nameof(TollFreeDatesTestData))] + public void Total_Fee_Is_Zero_For_TollFree_Dates(DateTime date, bool expectedResult) + { + var vehicle = _notTollFreeVehicle; + var dates = new DateTime[] { date }; + + var totalFee = _tollCalculator.GetTotalTollFeeForDay(vehicle, dates); + var feeIsZero = totalFee == 0; + + Assert.AreEqual(feeIsZero, expectedResult); + } + + [Test] + public void Total_Fee_Is_Zero_For_GoodFriday_Date() + { + var vehicle = _notTollFreeVehicle; + var dates = new DateTime[] + { + _goodFridayDate, + }; + + var totalFee = _tollCalculator.GetTotalTollFeeForDay(vehicle, dates); + + Assert.AreEqual(0, totalFee); + } + + [Test] + public void Maximum_Fee_Is_Sixty_For_One_Day() + { + var vehicle = _notTollFreeVehicle; + var dates = new DateTime[] + { + _notTollFreeDateAndNotTollFreeTime, // 07:00 HighRush 18 SEK + _notTollFreeDateAndNotTollFreeTime.AddHours(2), // 09:00 LowRush 8 SEK + _notTollFreeDateAndNotTollFreeTime.AddHours(4), // 11:00 LowRush 8 SEK + _notTollFreeDateAndNotTollFreeTime.AddHours(6), // 13:00 LowRush 8 SEK + _notTollFreeDateAndNotTollFreeTime.AddHours(8), // 15:00 MediumRush 13 SEK + _notTollFreeDateAndNotTollFreeTime.AddHours(10), // 17:00 MediumRush 13 SEK + }; // Total: 68 SEK + + var totalFee = _tollCalculator.GetTotalTollFeeForDay(vehicle, dates); + + Assert.AreEqual(60, totalFee); + } + + [Test] + [TestCaseSource(nameof(DatesAtDifferentRushHours))] + public void Fees_Will_Differ_Depending_On_Time_Of_Day(DateTime date, int expectedResult) + { + var vehicle = _notTollFreeVehicle; + var dates = new DateTime[] { date }; + + var totalFee = _tollCalculator.GetTotalTollFeeForDay(vehicle, dates); + + Assert.AreEqual(expectedResult, totalFee); + } + + [Test] + public void Only_Highest_Fee_Applies_For_Multiple_Fees_Within_Same_Hour() + { + var vehicle = _notTollFreeVehicle; + var dates = new DateTime[] + { + _notTollFreeDateAndNotTollFreeTime.AddMinutes(45), // 07:45 HighRush 18 SEK + _notTollFreeDateAndNotTollFreeTime.AddMinutes(75), // 08:15 MediumRush 13 SEK + }; + + var totalFee = _tollCalculator.GetTotalTollFeeForDay(vehicle, dates); + + Assert.AreEqual(18, totalFee); + } + + [Test] + public void Large_Set_Of_Fees_Returns_Correct_Total_Fee() + { + var vehicle = _notTollFreeVehicle; + var dates = new DateTime[] + { + _notTollFreeDateAndNotTollFreeTime, // 07:00 HighRush 18 SEK + _notTollFreeDateAndNotTollFreeTime.AddMinutes(15), // 07:15 HighRush 0 SEK (Too soon after previous fee) + _notTollFreeDateAndNotTollFreeTime.AddMinutes(75), // 08:15 MediumRush 13 SEK + _notTollFreeDateAndNotTollFreeTime.AddHours(6), // 13:00 LowRush 8 SEK + _notTollFreeDateAndNotTollFreeTime.AddHours(8), // 15:00 MediumRush 0 SEK (Next fee is higher and within an hour) + _notTollFreeDateAndNotTollFreeTime.AddMinutes(8*60 + 45), // 15:45 HighRush 18 SEK + _notTollFreeDateButTollFreeTime, // 20:00 NoRush 0 SEK + _notTollFreeDateButTollFreeTime.AddHours(2), // 22:00 NoRush 0 SEK + }; // Total: 57 SEK + + var totalFee = _tollCalculator.GetTotalTollFeeForDay(vehicle, dates); + + Assert.AreEqual(57, totalFee); + } + } +} diff --git a/TollFeeCalculator.Tests/TollFeeCalculator.Tests.csproj b/TollFeeCalculator.Tests/TollFeeCalculator.Tests.csproj new file mode 100644 index 00000000..6de94fe1 --- /dev/null +++ b/TollFeeCalculator.Tests/TollFeeCalculator.Tests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + diff --git a/TollFeeCalculator/Models/Car.cs b/TollFeeCalculator/Models/Car.cs new file mode 100644 index 00000000..178c30d9 --- /dev/null +++ b/TollFeeCalculator/Models/Car.cs @@ -0,0 +1,10 @@ +namespace TollFeeCalculator.Models +{ + public class Car : IVehicle + { + public VehicleType GetVehicleType() + { + return VehicleType.Car; + } + } +} \ No newline at end of file diff --git a/TollFeeCalculator/Models/IVehicle.cs b/TollFeeCalculator/Models/IVehicle.cs new file mode 100644 index 00000000..f919d8c6 --- /dev/null +++ b/TollFeeCalculator/Models/IVehicle.cs @@ -0,0 +1,7 @@ +namespace TollFeeCalculator.Models +{ + public interface IVehicle + { + VehicleType GetVehicleType(); + } +} \ No newline at end of file diff --git a/TollFeeCalculator/Models/Motorbike.cs b/TollFeeCalculator/Models/Motorbike.cs new file mode 100644 index 00000000..58f314ba --- /dev/null +++ b/TollFeeCalculator/Models/Motorbike.cs @@ -0,0 +1,10 @@ +namespace TollFeeCalculator.Models +{ + public class Motorbike : IVehicle + { + public VehicleType GetVehicleType() + { + return VehicleType.Motorbike; + } + } +} diff --git a/TollFeeCalculator/Models/RushHourType.cs b/TollFeeCalculator/Models/RushHourType.cs new file mode 100644 index 00000000..87784896 --- /dev/null +++ b/TollFeeCalculator/Models/RushHourType.cs @@ -0,0 +1,10 @@ +namespace TollFeeCalculator.Models +{ + public enum RushHourType + { + NoRush = 0, + LowRush = 1, + MediumRush = 2, + HighRush = 3, + } +} diff --git a/TollFeeCalculator/Models/VehicleType.cs b/TollFeeCalculator/Models/VehicleType.cs new file mode 100644 index 00000000..6155221b --- /dev/null +++ b/TollFeeCalculator/Models/VehicleType.cs @@ -0,0 +1,13 @@ +namespace TollFeeCalculator.Models +{ + public enum VehicleType + { + Motorbike = 0, + Tractor = 1, + Emergency = 2, + Diplomat = 3, + Foreign = 4, + Military = 5, + Car = 6, + } +} diff --git a/TollFeeCalculator/Program.cs b/TollFeeCalculator/Program.cs new file mode 100644 index 00000000..158331fd --- /dev/null +++ b/TollFeeCalculator/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace TollFeeCalculator +{ + class Program + { + // Console is not used. In retrospect I could have created this project as a library instead. + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/TollFeeCalculator/TollCalculator.cs b/TollFeeCalculator/TollCalculator.cs new file mode 100644 index 00000000..fa1f7ab8 --- /dev/null +++ b/TollFeeCalculator/TollCalculator.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using TollFeeCalculator.Models; +using TollFeeCalculator.Utils; + +// Note that since this is an assignment for a potential job, I have written more elaborate comments than usual in my code. +// This is just a way to document my thought process for the reviewers since I can't communicate with them any other way. + +// Also note that Exceptions thrown by this code are intended to be caught, logged and handled within the code of the calling system. + +public class TollCalculator +{ + + /** + * Calculate the total toll fee for one day + * + * @param vehicle - the vehicle + * @param dates - date and time of all passes on one day + * @return - the total toll fee for that day + */ + public int GetTotalTollFeeForDay(IVehicle vehicle, DateTime[] dates) + { + if (dates is null || dates.Length == 0) + return 0; + + var dayOfYear = dates.First().DayOfYear; + + if (dates.Any(date => date.DayOfYear != dayOfYear)) + throw new ArgumentException("Not all dates are from the same day!"); + + var sortedTollFees = GetSortedTollFees(vehicle, dates); + var totalFee = GetTotalFee(sortedTollFees); + + return Math.Min(totalFee, 60); + } + + private int GetTotalFee(Dictionary sortedTollFees) + { + if (sortedTollFees.Count == 0) + return 0; + + // Condition: + // A vehicle should only be charged once an hour + // In the case of multiple fees in the same hour period, the highest one applies. + + var totalTollFee = 0; + var sortedDates = sortedTollFees.Keys.OrderBy(date => date.Ticks).ToArray(); + var intervalStart = sortedDates[0]; + + foreach (var date in sortedDates) + { + if (date < intervalStart) + continue; + else + intervalStart = date; + + var intervalEnd = intervalStart.AddHours(1); + + var fee = GetHighestFeeInInterval(sortedTollFees, intervalStart, intervalEnd); + + totalTollFee += fee; + intervalStart = intervalEnd; + } + + return totalTollFee; + } + + private int GetHighestFeeInInterval(Dictionary sortedTollFees, DateTime intervalStart, DateTime intervalEnd) + { + var highestFee = 0; + var keysInInterval = sortedTollFees.Keys.Where(key => key.IsInInterval(intervalStart, intervalEnd)); + + foreach (var key in keysInInterval) + { + var fee = sortedTollFees[key]; + + if (fee > highestFee) + highestFee = fee; + } + + return highestFee; + } + + private Dictionary GetSortedTollFees(IVehicle vehicle, DateTime[] dates) + { + var tollFees = new Dictionary(); + var sortedDates = dates.OrderBy(date => date.TimeOfDay); + + foreach (var date in sortedDates) + { + var tollFee = GetTollFee(date, vehicle); + + if (tollFee > 0) + tollFees.Add(date, tollFee); + } + + return tollFees; + } + + private int GetTollFee(DateTime date, IVehicle vehicle) + { + if (IsTollFreeDate(date) || IsTollFreeVehicle(vehicle)) + return 0; + + var rushHourType = GetRushHourType(date); + + switch(rushHourType) + { + case RushHourType.LowRush: + return 8; + case RushHourType.MediumRush: + return 13; + case RushHourType.HighRush: + return 18; + default: + return 0; + } + } + + private bool IsTollFreeVehicle(IVehicle vehicle) + { + if (vehicle is null) + throw new ArgumentNullException($"Parameter '{nameof(vehicle)}' is null."); + + var vehicleType = vehicle.GetVehicleType(); + + switch (vehicleType) + { + case VehicleType.Motorbike: + case VehicleType.Tractor: + case VehicleType.Emergency: + case VehicleType.Diplomat: + case VehicleType.Foreign: + case VehicleType.Military: + return true; + case VehicleType.Car: + return false; + default: + throw new ArgumentOutOfRangeException($"Encountered unknown vehicle type '{vehicleType}'."); + } + } + + private bool IsTollFreeDate(DateTime date) + { + if (IsWeekend(date) || IsHoliday(date)) + return true; + + return false; + } + + private bool IsWeekend(DateTime date) + { + return date.DayOfWeek == DayOfWeek.Saturday + || date.DayOfWeek == DayOfWeek.Sunday; + } + + private bool IsHoliday(DateTime date) + { + // This one is quite tricky to determine, since not all holidays have static dates. There are also different holidays in different countries. + // Since the fees in the assignment intro are denoted in SEK, I will assume that it is the Swedish holidays that are of interest for this app. + + // I see 4 alternatives for implementing this logic: + + // 1. Hardcode the holiday dates for all the years the app is expected to be in use. Should be easy enough to google the dates. + // Obviously a flawed option, since the app cannot be used longer than the hardcoded dates allow for. + // Also, holiday dates may change due to political decisions during the lifetime of the app. + + // 2. Make the holiday dates configurable, by adding a config file or a setting saved in a database or registry. + // This makes the system very flexible, but the dates need to be maintained and updated yearly by hand. + + // 3. Lookup the holiday dates using some external third-party logic. There is probably some public API or .NET library out there that already does this calculation anyway. + // Drawbacks are: Dependency on third-party services or libraries. Overhead for implementing the API client/downloading the library, risk that the API is down. + + // 4. Implement local methods for calculating the date of each of the moving holidays depending on the year. + // This requires researching the rules that determine the date of each moving holiday, to get every calculation exactly right. + // This approach is still vulnerable to political decisions that may change any of those rules during the lifetime of the app. + + // Which of these approaches I would choose in a real world scenario would depend on the circumstances I guess... + // Most likely I would implement my own methods in most cases. Maybe even with an option to override calculations with configured values. + + // For the purpose of this assignment I'll implement methods based on the info here: http://hogtider.se/helgdagar-i-sverige/ + // I'll include the three "helgdagsaftnar" (Midsommarafton, Julafton, Nyårsafton) in my definition of holidays. + // Note that while "Påskafton" is not a "helgdagsafton" it is always a Saturday, so it will be toll free in this app due to the weekend condition. Same with "Pingstafton". + // If the year is a "skottår" there will be an additional day. Skottdagen is not a holiday (unless it's a Sunday). + + // All Sundays are holidays. + if (date.DayOfWeek == DayOfWeek.Sunday) + return true; + + var staticHolidays = new List<(int Month, int Day)>() + { + (1, 1), + (1, 6), + (5, 1), + (6, 6), + (12, 24), + (12, 25), + (12, 26), + (12, 31), + }; + + var movingHolidays = GetMovingHolidays(date.Year); // (Ordinary Sundays not included) + + var holidays = new List<(int Month, int Day)>(); + holidays.AddRange(staticHolidays); + holidays.AddRange(movingHolidays); + + var tollFeeDate = (date.Month, date.Day); + + if (holidays.Any(holiday => holiday.Month == tollFeeDate.Month && holiday.Day == tollFeeDate.Day)) + return true; + + return false; + } + + private List<(int Month, int Day)> GetMovingHolidays(int year) + { + var easterDay = CalendarUtils.CalculateEasterDay(year); + var midsummerDay = CalendarUtils.CalculateMidsummerDay(year); + var allSaintsDay = CalendarUtils.CalculateAllSaintsDay(year); + var ascensionDay = easterDay.AddDays(39); // "Kristi Himmelsfärdsdag", sixth Thursday after Easter Day. + var pentecostDay = easterDay.AddDays(49); // "Pingstdagen", 7 weeks after Easter Day. + + var movingHolidayDates = new List() + { + easterDay.AddDays(-2), + easterDay, + easterDay.AddDays(1), + ascensionDay, + pentecostDay, + midsummerDay.AddDays(-1), + midsummerDay, + allSaintsDay, + }; + + var movingHolidays = new List<(int Month, int Day)>(); + + foreach (var date in movingHolidayDates) + { + movingHolidays.Add((date.Month, date.Day)); + } + + return movingHolidays; + } + + private RushHourType GetRushHourType(DateTime date) + { + if (date.IsBetweenTimes("06:00", "06:30")) return RushHourType.LowRush; + if (date.IsBetweenTimes("06:30", "07:00")) return RushHourType.MediumRush; + if (date.IsBetweenTimes("07:00", "08:00")) return RushHourType.HighRush; + if (date.IsBetweenTimes("08:00", "08:30")) return RushHourType.MediumRush; + if (date.IsBetweenTimes("08:30", "15:00")) return RushHourType.LowRush; + if (date.IsBetweenTimes("15:00", "15:30")) return RushHourType.MediumRush; + if (date.IsBetweenTimes("15:30", "17:00")) return RushHourType.HighRush; + if (date.IsBetweenTimes("17:00", "18:00")) return RushHourType.MediumRush; + if (date.IsBetweenTimes("18:00", "18:30")) return RushHourType.LowRush; + + return RushHourType.NoRush; + + // Logic translated from these conditions: + + //if (hour == 6 && minute >= 0 && minute <= 29) return 8; + //else if (hour == 6 && minute >= 30 && minute <= 59) return 13; + //else if (hour == 7 && minute >= 0 && minute <= 59) return 18; + //else if (hour == 8 && minute >= 0 && minute <= 29) return 13; + //else if (hour >= 8 && hour <= 14 && minute >= 30 && minute <= 59) return 8; + //else if (hour == 15 && minute >= 0 && minute <= 29) return 13; + //else if (hour == 15 && minute >= 0 || hour == 16 && minute <= 59) return 18; + //else if (hour == 17 && minute >= 0 && minute <= 59) return 13; + //else if (hour == 18 && minute >= 0 && minute <= 29) return 8; + //else return 0; + + // Note that one of the conditions doesn't really make sense: + + //else if (hour >= 8 && hour <= 14 && minute >= 30 && minute <= 59) return 8; + + // This condition doesn't include times like e.g. 10:00 since the minute is less than 30. + // But it makes no sense to make the first 30 minutes of these hours free, so I assume that this is just an error in the condition. + } +} \ No newline at end of file diff --git a/TollFeeCalculator/TollFeeCalculator.csproj b/TollFeeCalculator/TollFeeCalculator.csproj new file mode 100644 index 00000000..c73e0d16 --- /dev/null +++ b/TollFeeCalculator/TollFeeCalculator.csproj @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp3.1 + + + diff --git a/TollFeeCalculator/Utils/CalendarUtils.cs b/TollFeeCalculator/Utils/CalendarUtils.cs new file mode 100644 index 00000000..0274e18e --- /dev/null +++ b/TollFeeCalculator/Utils/CalendarUtils.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TollFeeCalculator.Utils +{ + public static class CalendarUtils + { + public static DateTime CalculateEasterDay(int year) + { + // Gauss Easter formula, code copied from here (second hit on Google): https://codereview.stackexchange.com/questions/193847/find-easter-on-any-given-year + // The variable names don't have meaningful names because the numbers themselves don't really have a distinct meaning. + // The formula takes into account astronomical movements and such things, it is outside the scope of this assignment to fully understand it. + // The actual output will be tested in a unit test against some known Easter Day dates to verify that the formula is correct. + + // Edit: Note that in my first attempt I used the code from here (first hit on Google): https://www.geeksforgeeks.org/how-to-calculate-the-easter-date-for-a-given-year-using-gauss-algorithm/ + // Interestingly, this code only passed 8 out of 10 unit tests. So I had to switch to the current algorithm which passes all 10 unit tests. + + int a = year % 19; + int b = year / 100; + int c = (b - (b / 4) - ((8 * b + 13) / 25) + (19 * a) + 15) % 30; + int d = c - (c / 28) * (1 - (c / 28) * (29 / (c + 1)) * ((21 - a) / 11)); + int e = d - ((year + (year / 4) + d + 2 - b + (b / 4)) % 7); + int month = 3 + ((e + 40) / 44); + int day = e + 28 - (31 * (month / 4)); + + return new DateTime(year, month, day); + } + + public static DateTime CalculateMidsummerDay(int year) + { + var potentialDates = new List() + { + new DateTime(year, 6, 20), + new DateTime(year, 6, 21), + new DateTime(year, 6, 22), + new DateTime(year, 6, 23), + new DateTime(year, 6, 24), + new DateTime(year, 6, 25), + new DateTime(year, 6, 26), + }; + + foreach (var date in potentialDates) + { + if (date.DayOfWeek == DayOfWeek.Saturday) + return date; + } + + // Logically unreachable code since one of the potential dates must be a Saturday, but the compiler doesn't know that. + return potentialDates.First(); + } + + public static DateTime CalculateAllSaintsDay(int year) + { + var potentialDates = new List() + { + new DateTime(year, 10, 31), + new DateTime(year, 11, 1), + new DateTime(year, 11, 2), + new DateTime(year, 11, 3), + new DateTime(year, 11, 4), + new DateTime(year, 11, 5), + new DateTime(year, 11, 6), + }; + + foreach (var date in potentialDates) + { + if (date.DayOfWeek == DayOfWeek.Saturday) + return date; + } + + // Logically unreachable code since one of the potential dates must be a Saturday, but the compiler doesn't know that. + return potentialDates.First(); + } + } +} diff --git a/TollFeeCalculator/Utils/DateTimeExtensions.cs b/TollFeeCalculator/Utils/DateTimeExtensions.cs new file mode 100644 index 00000000..01df5309 --- /dev/null +++ b/TollFeeCalculator/Utils/DateTimeExtensions.cs @@ -0,0 +1,26 @@ +using System; + +namespace TollFeeCalculator.Utils +{ + public static class DateTimeExtensions + { + public static bool IsBetweenTimes(this DateTime date, string startTime, string endTime) + { + var startHour = int.Parse(startTime.Split(':')[0]); + var startMinute = int.Parse(startTime.Split(':')[1]); + var endHour = int.Parse(endTime.Split(':')[0]); + var endMinute = int.Parse(endTime.Split(':')[1]); + + var intervalStart = new DateTime(date.Year, date.Month, date.Day, startHour, startMinute, 0); + var intervalEnd = new DateTime(date.Year, date.Month, date.Day, endHour, endMinute, 0); + + return IsInInterval(date, intervalStart, intervalEnd); + } + + public static bool IsInInterval(this DateTime date, DateTime intervalStart, DateTime intervalEnd) + { + return date >= intervalStart + && date < intervalEnd; + } + } +}