deadlines.ry

module deadlines
  purpose: Find the tasks that are due soon and count the working days left for each.

import std.console
import std.time exposing Date, Weekday, InvalidDate

public type Task
  purpose: Something to finish by a date.
  has title: Text
  has due: Date
  can Compare by due
end

public type TaskError is one of
  purpose: Why a task line could not be read.
  MissingComma(line: Text)
end

public function parse_task(line: Text) returns Task or fails with TaskError or InvalidDate
  purpose: Read a line of the form "title, 2024-05-10" into a task.
  example: parse_task("report, 2024-05-10")
    is Task(title: "report", due: Date(year: 2024, month: 5, day: 10))
  example: parse_task("report") fails with MissingComma(line: "report")
  example: parse_task("report, 2024-13-10") fails with InvalidDate(input: "2024-13-10")

  let parts be line.split(",")
  if parts.length() is not 2 then fail with MissingComma(line: line) end
  let title be parts.at(0) otherwise fail with MissingComma(line: line)
  let due_text be parts.at(1) otherwise fail with MissingComma(line: line)
  let due be time.parse_date(due_text.trim()) otherwise fail
  return Task(title: title.trim(), due: due)
end

function is_working_day(date: Date) returns Boolean
  match date.weekday()
    when Saturday then return false
    when Sunday then return false
    otherwise return true
  end
end

public function working_days_between(start: Date, finish: Date) returns Integer
  purpose: Count the Monday to Friday days after the start, up to and including the finish.
  tags: dates, calendar
  example: working_days_between(
    start: Date(year: 2024, month: 5, day: 3),
    finish: Date(year: 2024, month: 5, day: 6)
  ) is 1
  example: working_days_between(
    start: Date(year: 2024, month: 5, day: 6),
    finish: Date(year: 2024, month: 5, day: 6)
  ) is 0

  let mutable total be 0
  let mutable current be start
  repeat until current is at least finish
    change current to current.plus_days(1)
    if is_working_day(current) then change total to total + 1 end
  end
  return total
end

public function due_soon(tasks: List of Task, today: Date, within_days: Integer)
  returns List of Task
  purpose: Tasks due between today and the given number of days ahead, earliest first.

  let limit be today.plus_days(within_days)
  return for each task in tasks
    where task.due is at least today and task.due is at most limit
    sorted by task.due
    collect task
end

public function main() needs console, time
  purpose: Show which of the sample tasks are due within the next ten days.

  let today be time.today()
  let tasks be [
    Task(title: "quarterly report", due: Date(year: 2026, month: 10, day: 9)),
    Task(title: "renew certificate", due: Date(year: 2026, month: 11, day: 30)),
    Task(title: "team review", due: Date(year: 2026, month: 10, day: 13))
  ]
  for each task in due_soon(tasks: tasks, today: today, within_days: 10)
    let remaining be working_days_between(start: today, finish: task.due)
    console.print("{task.title}: due {task.due}, {remaining} working days left")
  end
end