Skip to content

Instantly share code, notes, and snippets.

@adamchester
Last active August 29, 2015 14:15
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 adamchester/d37ef02b15b298a47c2b to your computer and use it in GitHub Desktop.
Save adamchester/d37ef02b15b298a47c2b to your computer and use it in GitHub Desktop.
open System
type WeekOf<'T> = { Mo:'T; Tu:'T; We:'T; Th:'T; Fr:'T; Sa:'T; Su:'T }
with
static member FromDays (allDays: (DayOfWeek * 'T)[]) =
let dayFor theDay = snd (allDays |> Array.find (fun (day, data) -> day = theDay))
{ Mo = dayFor DayOfWeek.Monday
Tu = dayFor DayOfWeek.Tuesday
We = dayFor DayOfWeek.Wednesday
Th = dayFor DayOfWeek.Thursday
Fr = dayFor DayOfWeek.Friday
Sa = dayFor DayOfWeek.Saturday
Su = dayFor DayOfWeek.Sunday }
member x.AllDays with get() = [|
DayOfWeek.Monday, x.Mo
DayOfWeek.Tuesday, x.Tu
DayOfWeek.Wednesday, x.We
DayOfWeek.Thursday, x.Th
DayOfWeek.Friday, x.Fr
DayOfWeek.Saturday, x.Sa
DayOfWeek.Sunday, x.Su |]
type SentenceStatus = Remand | Sentenced
type UniqueID = int
type DisplayName = string
type PayLevel = { id:UniqueID; name:DisplayName; amount:decimal }
type AbsentBlame = NotAtFault | AtFault of pay:PayLevel | PartiallyAtFault of pay:PayLevel
type AbsentReason = { id:UniqueID; name:DisplayName; blame:AbsentBlame }
type Attendance = Attended | Absent of reason:AbsentReason
type PayableActivityDetails = { id:UniqueID; name:DisplayName; location:DisplayName; pay:PayLevel }
type PayableActivityAttendance = { activity: PayableActivityDetails; attendance: Attendance }
type DayActivity =
| Unmanaged of pay:PayLevel
| Unemployed of pay:PayLevel
| RefusalToWork of pay:PayLevel
| PayableAcivities of PayableActivityAttendance list
type DayDecision =
| AlreadyPaid
| RefusedToWork
| PayAtLevelNoActivity of pay:PayLevel
| Paid of paid:PayableActivityDetails
| AbsentWithFullPay of paid:PayableActivityDetails
| AbsentWithNoPay of absent:PayableActivityDetails
| AbsentWithPartialPay of paid:PayableActivityDetails
with
member x.Rank with get() = match x with
| AlreadyPaid -> 1
| RefusedToWork -> 2
| AbsentWithPartialPay _ -> 3
| AbsentWithFullPay _ -> 4
| AbsentWithNoPay _ -> 5
| PayAtLevelNoActivity _ -> 6
| Paid _ -> 7
type DayOfWeekDecision =
/// Chosen day, indicating the day was chosen as one to pay when considered
/// as part of the whole week.
| ChosenDay of decision:DayDecision
/// Ignored day, indicated the day was ignored (i.e. not paid) when considered
/// as part of the whole week.
| IgnoredDay of decision:DayDecision
type RankablePay =
| Forced of pay:PayLevel
| HighPriority of pay:PayLevel
| Normal of pay:PayLevel
with member x.Rank with get() = match x with
| Forced p -> 1
| HighPriority p -> 2
| Normal p -> 3
let private calculatePay (aa: PayableActivityAttendance) : RankablePay =
match aa.attendance, aa.activity with
| Attended, act -> Normal(act.pay)
| Absent reason, activity ->
match reason.blame with
| NotAtFault -> Normal(activity.pay)
| PartiallyAtFault pay -> HighPriority(pay)
| AtFault pay -> Forced(pay)
let private chooseActivityOnDayToPay (activities) : DayDecision =
let calculateMap activity = activity, calculatePay activity
let rank (_, pay:RankablePay) = pay.Rank
activities
|> Seq.map calculateMap
|> Seq.sortBy rank
|> Seq.head
|> fun (aa, priority) ->
match aa.attendance with
| Attendance.Attended -> DayDecision.Paid(aa.activity)
| Absent { blame = AtFault(level) } -> AbsentWithNoPay(aa.activity)
| Absent { blame = NotAtFault } -> AbsentWithFullPay(aa.activity)
| Absent { blame = PartiallyAtFault(level) } -> AbsentWithPartialPay(aa.activity)
let private calculateDay (activity: DayActivity) =
match activity with
| Unmanaged level -> activity, DayDecision.PayAtLevelNoActivity(level)
| Unemployed level -> activity, DayDecision.PayAtLevelNoActivity(level)
| RefusalToWork level -> activity, DayDecision.PayAtLevelNoActivity(level)
| PayableAcivities payables -> activity, payables |> chooseActivityOnDayToPay
type CalculatedDayOfWeekDecision = {
activities:DayActivity
dayDecision:DayDecision
dayInWeekDecision:DayOfWeekDecision }
/// Matches a number between (and including) the specifed lo and hi range
let (|BetweenInclusive|_|) lo hi x =
if lo <= x && x <= hi then Some () else None
let calculateWeek (activities: WeekOf<DayActivity>) =
activities.AllDays
// calculate each day
|> Array.map (fun (dayOfWeek, activity) -> dayOfWeek, (calculateDay activity))
// rank the decisions and sort by them
|> Array.sortBy (fun (day, (activity, decision)) -> decision.Rank)
//choose top 5
|> Array.mapi (
fun index (dayOfWeek, (activity, decision)) ->
let makeDecision diwd =
dayOfWeek, { activities = activity; dayDecision = decision; dayInWeekDecision = diwd }
match index with
| BetweenInclusive 0 4 -> makeDecision (ChosenDay(decision))
| _ -> makeDecision (IgnoredDay(decision))
)
|> WeekOf.FromDays
// Learn more about F# at http://fsharp.net. See the 'F# Tutorial' project
// for more guidance on F# programming.
#load "Pay.fs"
module pay =
let level0 = { id=0; name="Level 0"; amount=0.0m; }
let level1 = { id=1; name="Level 1"; amount=4.5m; }
let level4 = { id=4; name="Level 4"; amount=4.431m; }
let level5 = { id=5; name="Level 5"; amount=6.5m; }
let level6 = { id=6; name="Level 6"; amount=7.777m; }
module absent =
let sick = Absent({ id=1; name="Sick"; blame=NotAtFault })
let refuseToAttend = Absent({ id=2; name="Refused to attend"; blame=AtFault(pay.level0) })
let atWork = Absent({ id=3; name="At work"; blame=NotAtFault })
let atCourt = Absent({ id=4; name="At court"; blame=NotAtFault })
let attendedEducation = Absent({ id=5; name="Attended Education"; blame=NotAtFault })
let attendedProgram = Absent({ id=6; name="Attended Program"; blame=NotAtFault })
let suspended = Absent({ id=7; name="Suspended"; blame=NotAtFault })
let lopSepcon = Absent({ id=8; name="LOP/SEPCON"; blame=PartiallyAtFault(pay.level1) })
module job =
let headChefKitchen = { id=100; name="Head Chef"; location="Kitchen"; pay=pay.level6 }
let chefKitchen = { id=101; name="Chef"; location="Kitchen"; pay=pay.level5 }
let cleanerBlock5b = { id=102; name="Cleaner"; location="Block 5B"; pay=pay.level4 }
module program =
let reintegration = { id=200; name="Reintegration"; location="Classroom 1"; pay=pay.level1 }
let lifeSkills = { id=201; name="Life Skills 101"; location="Classroom 2"; pay=pay.level1 }
let weekForAdam =
{ Mo = Unmanaged(pay.level0)
Tu = DayActivity.RefusalToWork(pay.level0)
We = Unemployed(pay.level1)
Th = Unemployed(pay.level1)
Fr = PayableAcivities([ { activity=job.chefKitchen; attendance=Attended }
{ activity=program.lifeSkills; attendance=Attended } ])
Sa = PayableAcivities([ { activity=job.headChefKitchen; attendance=absent.atCourt }
{ activity=job.headChefKitchen; attendance=absent.suspended }])
Su = PayableAcivities([ { activity=job.cleanerBlock5b; attendance=Attended }
{ activity=job.headChefKitchen; attendance=Attended }]) }
let weekPay = weekForAdam |> calculateWeek
weekPay.AllDays
|> Array.map (fun (day, decision) ->
day, (decision.ToString(), decision.activities))
|> WeekOf.FromDays
@vasily-kirichenko
Copy link

You can simplify with member x.Rank with get() = -> member x.Rank =

@vasily-kirichenko
Copy link

let private chooseActivityOnDayToPay (activities) : DayDecision =
    let calculateMap activity = activity, calculatePay activity
    let rank (_, pay:RankablePay) = pay.Rank
    activities
    |> Seq.map calculateMap
    |> Seq.sortBy rank
    |> Seq.head

Seq.head will raise an exception if activities is an empty sequence.

@vasily-kirichenko
Copy link

let private calculateDay (activity: DayActivity) =
    match activity with
    | Unmanaged level -> activity, DayDecision.PayAtLevelNoActivity(level)
    | Unemployed level -> activity, DayDecision.PayAtLevelNoActivity(level)
    | RefusalToWork level -> activity, DayDecision.PayAtLevelNoActivity(level)
    | PayableAcivities payables -> activity, payables |> chooseActivityOnDayToPay

->

let private calculateDay = function
    | Unmanaged level
    | Unemployed level
    | RefusalToWork level -> DayDecision.PayAtLevelNoActivity level
    | PayableAcivities payables -> chooseActivityOnDayToPay payables

And you can pipe activity right here https://gist.github.com/adamchester/d37ef02b15b298a47c2b#file-domain-fs-L118

@adamchester
Copy link
Author

Re: Seq.head actually it's a domain rule I have figured out how to encode in the types. Must be at least one activity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment