Skip to content

Instantly share code, notes, and snippets.

@taylorwood
Last active September 23, 2018 22:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save taylorwood/edc42222e6448379a8ca to your computer and use it in GitHub Desktop.
Save taylorwood/edc42222e6448379a8ca to your computer and use it in GitHub Desktop.
Relay Foods exercise
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