@@ -270,22 +270,12 @@ module UnitPeriod =
270270 [<Struct; StructuredFormatDisplay( " {Html}" ) >]
271271 type ScheduleLength =
272272 | PaymentCount of Payments : int
273- | MaxDuration of Days : int<DurationDay>
273+ | MaxDuration of StartDate : Date * Days : int<DurationDay>
274274
275275 member sl.Html =
276276 match sl with
277277 | PaymentCount payments -> $" <i>payment count</i> {payments}"
278- | MaxDuration days -> $" <i>max duration</i> {days}"
279-
280- /// generates a suggested number of payments to constrain the loan within a certain duration
281- let maxPaymentCount ( maxDuration : int < DurationDay >) ( config : Config ) =
282- match config with
283- | Daily _ -> float maxDuration
284- | Weekly( multiple, _) when multiple > 0 -> float maxDuration / ( float multiple * 7. )
285- | SemiMonthly _ -> float maxDuration / 15.
286- | Monthly( multiple, _, _, _) when multiple > 0 -> float maxDuration / ( float multiple * 30. )
287- | _ -> 1.
288- |> int
278+ | MaxDuration(_, days) -> $" <i>max duration</i> {days}"
289279
290280 /// direction in which to generate the schedule: forward works forwards from a given date and reverse works backwards
291281 [<Struct; RequireQualifiedAccess>]
@@ -297,49 +287,173 @@ module UnitPeriod =
297287
298288 /// generate a payment schedule based on a unit-period config
299289 let generatePaymentSchedule scheduleLength direction unitPeriodConfig =
300- let count =
290+ let adjustMonthEnd monthEndTrackingDay ( date : Date ) =
291+ if date.Day > 15 && monthEndTrackingDay > 28 then
292+ TrackingDay.toDate date.Year date.Month monthEndTrackingDay
293+ else
294+ date
295+
296+ let initCount = 0
297+
298+ match unitPeriodConfig |> Config.constrain with
299+ | Daily firstPaymentDate ->
300+ match scheduleLength with
301+ | PaymentCount count ->
302+ match direction with
303+ | Direction.Forward -> Array.init count ( fun i -> firstPaymentDate.AddDays i)
304+ | Direction.Reverse -> Array.init count ( fun i -> firstPaymentDate.AddDays - i)
305+ | MaxDuration( startDate, duration) ->
306+ match direction with
307+ | Direction.Forward ->
308+ Array.unfold
309+ ( fun count ->
310+ let nextDate = firstPaymentDate.AddDays count
311+
312+ if nextDate <= startDate.AddDays +( int duration) then
313+ Some( nextDate, count + 1 )
314+ else
315+ None
316+ )
317+ initCount
318+ | Direction.Reverse ->
319+ Array.unfold
320+ ( fun count ->
321+ let nextDate = firstPaymentDate.AddDays - count
322+
323+ if nextDate <= startDate.AddDays -( int duration) then
324+ Some( nextDate, count - 1 )
325+ else
326+ None
327+ )
328+ initCount
329+ | Weekly( multiple, firstPaymentDate) ->
330+ match scheduleLength with
331+ | PaymentCount count ->
332+ match direction with
333+ | Direction.Forward -> Array.init count ( fun i -> firstPaymentDate.AddDays( i * 7 * multiple))
334+ | Direction.Reverse -> Array.init count ( fun i -> firstPaymentDate.AddDays -( i * 7 * multiple))
335+ | MaxDuration( startDate, duration) ->
336+ match direction with
337+ | Direction.Forward ->
338+ Array.unfold
339+ ( fun i ->
340+ let nextDate = firstPaymentDate.AddDays( i * 7 * multiple)
341+
342+ if nextDate <= startDate.AddDays +( int duration) then
343+ Some( nextDate, i + 1 )
344+ else
345+ None
346+ )
347+ initCount
348+ | Direction.Reverse ->
349+ Array.unfold
350+ ( fun i ->
351+ let nextDate = firstPaymentDate.AddDays -( i * 7 * multiple)
352+
353+ if nextDate <= startDate.AddDays -( int duration) then
354+ Some( nextDate, i - 1 )
355+ else
356+ None
357+ )
358+ initCount
359+ | SemiMonthly( year, month, td1, td2) ->
360+ let firstPaymentDate = TrackingDay.toDate year month td1
361+
362+ let offset , monthEndTrackingDay =
363+ if td1 > td2 then 1 , td1 else 0 , td2
364+ |> fun ( o , metd ) ->
365+ match direction with
366+ | Direction.Forward -> o, metd
367+ | Direction.Reverse -> o - 1 , metd
368+
301369 match scheduleLength with
302- | PaymentCount c -> c
303- | MaxDuration d -> maxPaymentCount d unitPeriodConfig
370+ | PaymentCount count ->
371+ match direction with
372+ | Direction.Forward -> [| 0 .. ( count - 1 ) |]
373+ | Direction.Reverse -> [| 0 .. - 1 .. -( count - 1 ) |]
374+ |> Array.collect ( fun c -> [|
375+ firstPaymentDate.AddMonths c |> adjustMonthEnd monthEndTrackingDay
376+
377+ firstPaymentDate.AddMonths( c + offset)
378+ |> fun d -> TrackingDay.toDate d.Year d.Month td2
379+ |> adjustMonthEnd monthEndTrackingDay
380+ |])
381+ |> Array.take count
382+
383+ | MaxDuration( startDate, duration) ->
384+ Array.unfold
385+ ( fun count ->
386+ let nextDate1 =
387+ firstPaymentDate.AddMonths count |> adjustMonthEnd monthEndTrackingDay
388+
389+ let nextDate2 =
390+ firstPaymentDate.AddMonths( count + offset)
391+ |> fun d -> TrackingDay.toDate d.Year d.Month td2
392+ |> adjustMonthEnd monthEndTrackingDay
393+
394+ match direction with
395+ | Direction.Forward ->
396+ let finalPaymentDate = startDate.AddDays( int duration)
397+
398+ let output = [|
399+ if nextDate1 <= finalPaymentDate then
400+ yield nextDate1
401+ if nextDate2 <= finalPaymentDate then
402+ yield nextDate2
403+ |]
404+
405+ if output.Length > 0 then Some( output, count + 1 ) else None
406+ | Direction.Reverse ->
407+ let finalPaymentDate = startDate.AddDays -( int duration)
408+
409+ let output = [|
410+ if nextDate1 >= finalPaymentDate then
411+ yield nextDate1
412+ if nextDate2 >= finalPaymentDate then
413+ yield nextDate2
414+ |]
415+
416+ if output.Length > 0 then Some( output, count - 1 ) else None
417+ )
418+ initCount
419+ |> Array.collect id
420+ | Monthly( multiple, year, month, trackingDay) ->
421+ let firstPaymentDate = TrackingDay.toDate year month trackingDay
304422
305- if count = 0 then
306- [||]
307- else
308- let adjustMonthEnd monthEndTrackingDay ( d : Date ) =
309- if d.Day > 15 && monthEndTrackingDay > 28 then
310- TrackingDay.toDate d.Year d.Month monthEndTrackingDay
311- else
312- d
313-
314- let generate upc =
315- match upc |> Config.constrain with
316- | Daily startDate -> Array.map startDate.AddDays
317- | Weekly( multiple, startDate) -> Array.map ( fun c -> startDate.AddDays( c * 7 * multiple))
318- | SemiMonthly( year, month, td1, td2) ->
319- let startDate = TrackingDay.toDate year month td1
320-
321- let offset , monthEndTrackingDay =
322- if td1 > td2 then 1 , td1 else 0 , td2
323- |> fun ( o , metd ) ->
324- match direction with
325- | Direction.Forward -> o, metd
326- | Direction.Reverse -> o - 1 , metd
327-
328- Array.collect ( fun c -> [|
329- startDate.AddMonths c |> adjustMonthEnd monthEndTrackingDay
330-
331- startDate.AddMonths( c + offset)
332- |> fun d -> TrackingDay.toDate d.Year d.Month td2
333- |> adjustMonthEnd monthEndTrackingDay
334- |])
335- >> Array.take count
336- | Monthly( multiple, year, month, td) ->
337- let startDate = TrackingDay.toDate year month td
338- Array.map ( fun c -> startDate.AddMonths( c * multiple) |> adjustMonthEnd td)
339-
340- match direction with
341- | Direction.Forward -> [| 0 .. ( count - 1 ) |] |> generate unitPeriodConfig
342- | Direction.Reverse -> [| 0 .. - 1 .. -( count - 1 ) |] |> generate unitPeriodConfig |> Array.sort
423+ match scheduleLength with
424+ | PaymentCount c ->
425+ match direction with
426+ | Direction.Forward ->
427+ Array.init c ( fun i -> firstPaymentDate.AddMonths( i * multiple) |> adjustMonthEnd trackingDay)
428+ | Direction.Reverse ->
429+ Array.init c ( fun i -> firstPaymentDate.AddMonths(- i * multiple) |> adjustMonthEnd trackingDay)
430+ | MaxDuration( startDate, duration) ->
431+ match direction with
432+ | Direction.Forward ->
433+ Array.unfold
434+ ( fun count ->
435+ let nextDate =
436+ firstPaymentDate.AddMonths( count * multiple) |> adjustMonthEnd trackingDay
437+
438+ if nextDate <= startDate.AddDays +( int duration) then
439+ Some( nextDate, count + 1 )
440+ else
441+ None
442+ )
443+ initCount
444+ | Direction.Reverse ->
445+ Array.unfold
446+ ( fun count ->
447+ let nextDate =
448+ firstPaymentDate.AddMonths(- count * multiple) |> adjustMonthEnd trackingDay
449+
450+ if nextDate <= startDate.AddDays -( int duration) then
451+ Some( nextDate, count - 1 )
452+ else
453+ None
454+ )
455+ initCount
456+ |> Array.sort
343457
344458 /// for a given interval and array of dates, devise the unit-period config
345459 let detect direction interval transferDates =
0 commit comments