///|
/// Calendar arithmetic on `Date`: counting days, naming weekdays, adding months.
///
/// Nothing in this file needs a clock or a zone. A date is a square on a wall
/// calendar, and the operations below are all the things you can do with squares.
///
///     moon run src/examples/calendar
fn main raise {
  let date = @moondate.Date::new(2026, 9, 22)

  // Where the day sits in the year, and in the week.
  // There are two ways to number a week, and the off-by-one between them is the
  // classic way to get a schedule wrong. `weekday` counts from Sunday as 0, which
  // is what a calendar and cron use. `isoweekday` counts from Monday as 1, which
  // is what ISO 8601 uses.
  println("weekday  " + date.weekday().to_string()) // Tuesday, Sunday = 0
  println("isoweekday " + date.isoweekday().to_string()) // Tuesday, Monday = 1
  println("yearday  " + date.yearday().to_string()) // 1 = 1 January

  // A Sunday is where the two disagree: 0 in one, 7 in the other.
  let sunday = @moondate.Date::new(2026, 9, 27)
  println(
    "Sunday   " +
    sunday.weekday().to_string() +
    " and " +
    sunday.isoweekday().to_string(),
  )

  // The ISO 8601 week date: which year the week belongs to, which week, which day.
  // The year is not always the calendar year — 1 January 2027 belongs to week 53
  // of 2026 — which is why this is a separate call and not three divisions.
  let (iso_year, iso_week, iso_day) = date.isocalendar()
  println("iso      \{iso_year}-W\{iso_week}-\{iso_day}")

  // The number of days since 1970-01-01, and back again. This pair is total over
  // the whole supported range, so it makes a compact key for a date.
  println("days     " + date.days().to_string())
  println("of_days  " + @moondate.Date::of_days(date.days()).to_string())

  // Python's ordinal counts 1 January of year 1 as day 1. Both directions exist.
  println("ordinal  " + date.ordinal().to_string())
  println(
    "of_ordinal " + @moondate.Date::of_ordinal(date.ordinal()).to_string(),
  )

  println("")

  // Month lengths are a table, but a computed one: February is the only month
  // that changes, and only in a leap year.
  let mut month = 1
  let mut lengths = ""
  while month <= 12 {
    lengths = lengths + @moondate.length(2024, month).to_string() + " "
    month = month + 1
  }
  println(lengths)
  println("2024 is a leap year: " + @moondate.leap(2024).to_string())
  println("2100 is a leap year: " + @moondate.leap(2100).to_string())
  println("")

  // Arithmetic is exact and takes you out of the month, the year, and the range
  // you would expect. Negative counts go back.
  println("+10 days    " + date.plus_days(10).to_string())
  println("-100 days   " + date.plus_days(-100).to_string())
  println("+5 months   " + date.plus_months(5).to_string())
  println("-12 months  " + date.plus_months(-12).to_string())
  println("")

  // Adding months clamps rather than overflows: 31 January plus one month is
  // 28 February in a common year, because there is no 31 February to land on.
  let end_of_january = @moondate.Date::new(2026, 1, 31)
  println("31 Jan +1 month " + end_of_january.plus_months(1).to_string())
  println("31 Jan +2 months " + end_of_january.plus_months(2).to_string())
  println("")

  // The distance between two dates is a span of whole days.
  let later = @moondate.Date::new(2027, 3, 1)
  let span = date.diff(later)
  let (days, seconds, nanos) = span.parts()
  println("days between    " + days.to_string())
  println("leftover seconds " + seconds.to_string())
  println("leftover nanos   " + nanos.to_string())
  println("total seconds    " + span.total_seconds().to_string())
  println("")

  // Ordering is structural, so sorting a list of dates is just `sort`.
  let holidays = [
    @moondate.Date::new(2026, 1, 1),
    @moondate.Date::new(2026, 10, 1),
    @moondate.Date::new(2026, 5, 1),
  ]
  holidays.sort()
  for holiday in holidays {
    // `strftime` lives on `Moment`, so a bare date is wrapped in the `Day` shape
    // first. Formatting is a property of the shape, not of the date alone.
    let as_day = @moondate.Day(date=holiday)
    println(holiday.to_string() + "  " + as_day.strftime("%A"))
  }

  // `replace` swaps single fields and re-checks the result, which is how you snap
  // a date to the first of its month without doing arithmetic on months at all.
  let first_of_month = date.replace(day=1)
  println("first of month " + first_of_month.to_string())
}