Last active
September 23, 2018 22:18
-
-
Save taylorwood/edc42222e6448379a8ca to your computer and use it in GitHub Desktop.
Relay Foods exercise
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open System | |
// Data types // | |
/// Describes an Address, its physical location, and the times it will be occupied. | |
[<StructuredFormatDisplay("{Name}")>] | |
type Address = { | |
Name: string | |
Street: string | |
City: string | |
State: string | |
Zip: string | |
Availability: Availability | |
Position: Position | |
} | |
and Availability = { | |
DaysOfWeek: Set<DayOfWeek> | |
StartHour: float | |
EndHour: float | |
} | |
and Position = { Latitude: decimal; Longitude: decimal } | |
/// Describes a range of time. | |
// NOTE: using non-UTC DateTime instead of DateTimeOffset for simplicity, assumes everything is happening in same time zone | |
type TimeSlot = { Start: DateTime; End: DateTime } | |
/// Describes a pickup location, time slot, and distance relative to a customer's location. | |
[<StructuredFormatDisplay("{PickupAddress}")>] | |
type PickupOption = { | |
PickupAddress: Address | |
TimeSlot: TimeSlot | |
Distance: float | |
} | |
// Functions to operate on data types // | |
/// Calculates the distance in miles between two Positions. | |
let getMileDistance p1 p2 = | |
// using Haversine formula found on the internet! | |
let degreesToRadians = (*) (Math.PI / 180.) | |
let p1LatRad = degreesToRadians (float p1.Latitude) | |
let p2LatRad = degreesToRadians (float p2.Latitude) | |
let latDiff = degreesToRadians (float p2.Latitude - float p1.Latitude) | |
let longDiff = degreesToRadians (float p2.Longitude - float p1.Longitude) | |
let a = Math.Sin(latDiff / 2.) * Math.Sin(latDiff / 2.) + | |
Math.Cos(p1LatRad) * Math.Cos(p2LatRad) * | |
Math.Sin(longDiff / 2.) * Math.Sin(longDiff / 2.) | |
let c = 2. * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1. - a)) | |
let earthRadiusMiles = 3956.5467 // from wolfram alpha | |
earthRadiusMiles * c | |
/// Attempts to get the TimeSlot for a given DateTime and Availability type. | |
let getTimeSlot (pickupDate: DateTime) availability = | |
let hourOfDay = pickupDate.Date.AddHours | |
if availability.DaysOfWeek.Contains pickupDate.DayOfWeek then | |
Some { Start = hourOfDay availability.StartHour; End = hourOfDay availability.EndHour } | |
else | |
None | |
/// Gets a list of PickupOptions based on a date and customer address. | |
let getPickupOptions dateTime pickupLocations address = | |
[ for loc in pickupLocations do | |
let timeSlot = getTimeSlot dateTime loc.Availability | |
match timeSlot with | |
| Some t -> | |
yield { PickupAddress = loc; | |
TimeSlot = t; | |
Distance = getMileDistance address.Position loc.Position } | |
| None -> () | |
] | |
|> List.sortBy (fun po -> po.Distance) // prefer closest locations | |
/// Get the available PickupOptions for a given date. | |
let getPickupOptionsForDate date customerAddresses pickupLocations = | |
let pickupOptions = customerAddresses |> Seq.map (getPickupOptions date pickupLocations) | |
Seq.zip customerAddresses pickupOptions | |
/// Determines if a customer will be at an address during a time slot. | |
let isCustomerAtAddress address window = | |
let overlaps t1 t2 = // simple intersection check for two time slots | |
let t1Start = t1.Start.Ticks | |
let t1End = t1.End.Ticks | |
let t2Start = t2.Start.Ticks | |
let t2End = t2.End.Ticks | |
not(t1Start > t2End || t2Start > t1End) | |
let timeSlot = getTimeSlot window.Start address.Availability | |
match timeSlot with | |
| Some ts -> overlaps ts window | |
| None -> false | |
/// Prints pickup options to stdout. | |
let printPickupOptions pickupOptionsByAddress = | |
for pickupOptionByAddress in pickupOptionsByAddress do | |
let custAddr, pickupOptions = pickupOptionByAddress | |
match pickupOptions with | |
| [] -> // will never happen if pickup locations are defined for every day of the week | |
printfn "Sorry, there are no locations available for pickup today." | |
| _ -> | |
printfn "Pickup options near %s:" custAddr.Name | |
for pickupOption in pickupOptions do | |
let timeSlot = pickupOption.TimeSlot | |
printf "\t%s (%.1fm away) between %s and %s." | |
pickupOption.PickupAddress.Name | |
pickupOption.Distance | |
(timeSlot.Start.ToShortTimeString()) | |
(timeSlot.End.ToShortTimeString()) | |
// helpful reminder to customer if they're nearby pickup during this time slot | |
if isCustomerAtAddress custAddr timeSlot then | |
printf " You'll probably be at %s then." custAddr.Name | |
printf "\r\n" | |
// Exercise functionality below... // | |
// convenience function for creating test addresses | |
let createAddress name street position availability = | |
{ Name = name; Street = street; City = "Washington"; State = "DC"; Zip = "20037-1234"; Position = position; Availability = availability } | |
// convenience function for creating availabilities | |
let createAvailability daysOfWeek startHour endHour = | |
if endHour <= startHour then failwith "End hour must be after start hour" | |
{ DaysOfWeek = Set.ofSeq daysOfWeek; StartHour = startHour; EndHour = endHour } | |
// define my two personal addresses | |
let myAddresses = [ | |
createAddress // I sleep at the Lincoln Monument | |
"Home" "2 Lincoln Memorial Cir NW" {Latitude = 38.889277m; Longitude = -77.050122m} | |
(createAvailability [DayOfWeek.Saturday; DayOfWeek.Sunday] 0. 24.) | |
createAddress // I work 9-5 at the Smithsonian | |
"Work" "1400 Constitution Ave NW" {Latitude = 38.892089m; Longitude = -77.030035m} | |
(createAvailability [DayOfWeek.Monday; DayOfWeek.Tuesday; DayOfWeek.Wednesday; DayOfWeek.Thursday; DayOfWeek.Friday] 9. 17.) | |
] | |
// define some pickup locations | |
let pickupLocations = [ | |
createAddress | |
"The White House" "1600 Pennsylvania Avenue" {Latitude = 38.8977m; Longitude = -77.0365m} | |
(createAvailability [DayOfWeek.Monday; DayOfWeek.Tuesday; DayOfWeek.Friday] 9.5 15.) | |
createAddress | |
"Washington Monument" "2 15th St NW" {Latitude = 38.889469m; Longitude = -77.035258m} | |
(createAvailability [DayOfWeek.Monday; DayOfWeek.Thursday; DayOfWeek.Saturday] 14. 19.25) | |
createAddress | |
"Watergate Complex" "700 New Hampshire Ave NW" {Latitude = 38.898237m; Longitude = -77.054809m} | |
(createAvailability [DayOfWeek.Wednesday; DayOfWeek.Thursday; DayOfWeek.Friday] 20.75 23.66) | |
createAddress | |
"United States Capitol" "East Capitol St NE & First St SE" {Latitude = 38.889932m; Longitude = -77.009048m} | |
(createAvailability [DayOfWeek.Tuesday; DayOfWeek.Thursday] 11. 17.) | |
createAddress | |
"Denny's" "1250 Bladensburg Rd NE" {Latitude = 38.906786m; Longitude = -76.979460m} | |
(createAvailability [DayOfWeek.Monday; DayOfWeek.Wednesday; DayOfWeek.Saturday; DayOfWeek.Sunday] 8. 14.5) | |
] | |
let rng = Random() | |
// the below lines can be evaluated repeatedly in FSI to test the code with random dates | |
let pickupDate = DateTime.Now.Date.AddDays(float (rng.Next 30)) | |
printfn "Suggesting pickup locations for %s..." (pickupDate.ToLongDateString()) | |
pickupLocations | |
|> getPickupOptionsForDate pickupDate myAddresses | |
|> printPickupOptions |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment