// Availability via 24-block boolean mask // WIP // range0: [0..n-1] range0 : n -> when n is 0 then [] _ then append (range0 (n - 1)) (n - 1); // overlaps: does busy interval b overlap block i? overlaps : b i dayStart blockSize -> (b.start < (dayStart + (i * blockSize) + blockSize)) and (b.end > (dayStart + (i * blockSize))); // anyBusyOnBlock: reduce OR over busy intervals for block i anyBusyOnBlock : busyList i dayStart blockSize -> reduce (acc b -> acc or (overlaps b i dayStart blockSize)) false busyList; // allFreeOnBlock: everyone free on block i? allFreeOnBlock : busyLists i dayStart blockSize -> reduce (acc bl -> acc and (when (anyBusyOnBlock bl i dayStart blockSize) is true then false _ then true)) true busyLists; // stepRuns: accumulate contiguous free runs over blocks with min-block filtering stepRuns : busyLists dayStart blockSize minBlocks acc i -> when acc.inRun is true then when (allFreeOnBlock busyLists i dayStart blockSize) is true then { inRun: true, start: acc.start, runs: acc.runs } _ then ( when ((i - acc.start) >= minBlocks) is true then { inRun: false, start: 0, runs: append acc.runs { start: acc.start, end: i } } _ then { inRun: false, start: 0, runs: acc.runs } ) _ then when (allFreeOnBlock busyLists i dayStart blockSize) is true then { inRun: true, start: i, runs: acc.runs } _ then acc; // finalizeRuns: close trailing run if needed (with min-block filtering) finalizeRuns : acc totalBlocks minBlocks -> when acc.inRun is true then ( when ((totalBlocks - acc.start) >= minBlocks) is true then append acc.runs { start: acc.start, end: totalBlocks } _ then acc.runs ) _ then acc.runs; // convertRunsToMinutes: map block runs to minute intervals (shape-guarded) convertRunsToMinutes : runs dayStart blockSize -> when (shape runs).kind is "List" then map (r -> { start: dayStart + (r.start * blockSize), end: dayStart + (r.end * blockSize) }) runs _ then []; // takeLimit: slice helper takeLimit : limit lst -> slice lst 0 (math.min (length lst) limit); // findCommonAvailability using mask approach // runnerFor: folder factory capturing inputs, returns (acc i -> ...) runnerFor : calendars dayStart dayEnd minMinutes acc i -> stepRuns (values calendars) dayStart ((dayEnd - dayStart) / 24) (math.ceil (minMinutes / ((dayEnd - dayStart) / 24))) acc i; // buildRuns: produce runs list from inputs buildRuns : calendars dayStart dayEnd minMinutes -> finalizeRuns ( reduce (runnerFor calendars dayStart dayEnd minMinutes) { inRun: false, start: 0, runs: [] } (range0 24) ) 24 (math.ceil (minMinutes / ((dayEnd - dayStart) / 24))); // slotsFor: convert runs to minute intervals slotsFor : calendars dayStart dayEnd minMinutes -> convertRunsToMinutes (buildRuns calendars dayStart dayEnd minMinutes) dayStart ((dayEnd - dayStart) / 24); // findCommonAvailability: top-level pipeline findCommonAvailability : calendars dayStart dayEnd minMinutes limit -> takeLimit limit (slotsFor calendars dayStart dayEnd minMinutes); // ---------- Example usage ---------- // Working window 09:00-17:00 dayStart : 9 * 60; // 540 dayEnd : 17 * 60; // 1020 minSlot : 30; // minutes limit : 5; // Calendars: each value is a sorted list of busy intervals { start, end } in minutes calendars : { alice: [ { start: 570, end: 630 } { start: 720, end: 780 } { start: 960, end: 1020 } ], bob: [ { start: 540, end: 555 } { start: 660, end: 720 } { start: 840, end: 870 } ], carol: [ { start: 600, end: 660 } { start: 750, end: 810 } { start: 915, end: 960 } ] }; io.out "loaded"; available : findCommonAvailability calendars dayStart dayEnd minSlot limit; io.out "done"; io.out available;