///|
fn date_time_function(name : String) -> Bool {
  ["YEAR", "MONTH", "DAY", "HOURS", "MINUTES", "SECONDS", "TIMEZONE", "TZ"].contains(
    name,
  )
}

///|
fn date_digits(
  chars : Array[Char],
  start : Int32,
  length : Int32,
) -> Int32 raise QueryError {
  let mut result = 0
  for i = start; i < start + length; i = i + 1 {
    let c = chars[i]
    if !digit(c) {
      raise Evaluation("invalid dateTime digits")
    }
    result = result * 10 + c.to_int() - '0'.to_int()
  }
  result
}

///|
fn date_time_part(
  name : String,
  term : Term,
  budget : Budget,
) -> Term raise QueryError {
  if term.kind != LiteralKind || term.datatype != xsd_namespace() + "dateTime" {
    raise Evaluation("xsd:dateTime required")
  }
  budget.tick(n=term.lexical.length())
  let chars = term.lexical.trim().to_owned().to_array()
  if chars.length() < 19 ||
    chars[4] != '-' ||
    chars[7] != '-' ||
    chars[10] != 'T' ||
    chars[13] != ':' ||
    chars[16] != ':' {
    raise Evaluation("expected four-digit dateTime year and time")
  }
  let year = date_digits(chars, 0, 4)
  let month = date_digits(chars, 5, 2)
  let day = date_digits(chars, 8, 2)
  let hour = date_digits(chars, 11, 2)
  let minute = date_digits(chars, 14, 2)
  let second = date_digits(chars, 17, 2)
  let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
  let month_days = match month {
    2 => if leap { 29 } else { 28 }
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
  if year == 0 ||
    month < 1 ||
    month > 12 ||
    day < 1 ||
    day > month_days ||
    hour > 23 ||
    minute > 59 ||
    second > 59 {
    raise Evaluation("dateTime component out of range")
  }
  let mut end = 19
  if chars.get(end) == Some('.') {
    end += 1
    let start = end
    while chars.get(end) is Some(c) && digit(c) {
      end += 1
    }
    if end == start {
      raise Evaluation("empty dateTime fraction")
    }
  }
  let zone = String::from_array(chars[end:])
  let offset = if zone == "Z" {
    Some(0)
  } else if zone.is_empty() {
    None
  } else {
    let tz = zone.to_array()
    if tz.length() != 6 || (tz[0] != '+' && tz[0] != '-') || tz[3] != ':' {
      raise Evaluation("invalid dateTime timezone")
    }
    let h = date_digits(tz, 1, 2)
    let m = date_digits(tz, 4, 2)
    if h > 14 || m > 59 || (h == 14 && m != 0) {
      raise Evaluation("dateTime timezone out of range")
    }
    Some((h * 60 + m) * (if tz[0] == '-' { -1 } else { 1 }))
  }
  match name {
    "YEAR" => integer(year)
    "MONTH" => integer(month)
    "DAY" => integer(day)
    "HOURS" => integer(hour)
    "MINUTES" => integer(minute)
    "SECONDS" => {
      let digits = budget.limits.max_numeric_digits
      let value = typed_literal(
        String::from_array(chars[17:end]),
        xsd_namespace() + "decimal",
      )
      numeric(value, digits).term(digits)
    }
    "TZ" => literal(zone)
    _ => {
      guard offset is Some(total) else {
        raise Evaluation("dateTime has no timezone")
      }
      let absolute = if total < 0 { -total } else { total }
      let h = absolute / 60
      let m = absolute % 60
      let text = if total == 0 {
        "PT0S"
      } else {
        (if total < 0 { "-PT" } else { "PT" }) +
        (if h > 0 { h.to_string() + "H" } else { "" }) +
        (if m > 0 { m.to_string() + "M" } else { "" })
      }
      typed_literal(text, xsd_namespace() + "dayTimeDuration")
    }
  }
}