Last active
August 29, 2015 14:15
-
-
Save adamchester/d37ef02b15b298a47c2b to your computer and use it in GitHub Desktop.
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 | |
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 |
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
// 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 | |
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.
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
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
You can simplify
with member x.Rank with get() =
->member x.Rank =