///|
// Parse the default dateutil grammar into optional fields. Default filling and
// runtime timezone construction are deliberately separate from token selection.
fn parser_select(
  text : String,
  reference_year : Int32,
) -> ParserFields raise DateError {
  let tokens = parser_lex(text)
  let fields = parser_fields()
  let ymd = parser_ymd()
  let count = tokens.length()
  let mut index = 0
  while index < count {
    let token = tokens[index]
    if parser_number(token) is Some(_) {
      index = parser_numeric_token(tokens, index, ymd, fields)
    } else if parser_weekday(token) is Some(day) {
      fields.weekday = Some(day)
    } else if parser_month(token) is Some(month) {
      ymd.append_number(month, label=M)
      if index + 1 < count {
        if ["-", "/"].contains(tokens[index + 1]) {
          let separator = tokens[index + 1]
          ymd.append_text(parser_token(tokens, index + 2))
          if index + 3 < count && tokens[index + 3] == separator {
            ymd.append_text(parser_token(tokens, index + 4))
            index += 2
          }
          index += 2
        } else if index + 4 < count &&
          tokens[index + 1] == " " &&
          tokens[index + 3] == " " &&
          tokens[index + 2].to_lower() == "of" {
          let raw = tokens[index + 4]
          if !raw.to_array().is_empty() &&
            raw.to_array().iter().all(parser_is_digit) {
            let year = parser_convert_year(
              parser_integer(raw),
              false,
              reference_year,
            )
            ymd.append_text(year.to_string(), label=Y)
          }
          index += 4
        }
      }
    } else if parser_ampm(token) is Some(ampm) {
      let hour = fields.hour.unwrap_or_else(() => {
        raise DateError::InvalidDate(message="AM/PM requires an hour")
      })
      if hour < 0 || hour > 12 {
        raise DateError::InvalidDate(message="invalid 12-hour clock hour")
      }
      fields.hour = Some(parser_adjust_ampm(hour, ampm))
      fields.ampm = Some(ampm)
    } else if fields.could_be_tzname(token) {
      fields.tzname = Some(token)
      fields.tzoffset = if ["utc", "gmt", "z"].contains(token) {
        Some(0)
      } else {
        None
      }
      if index + 1 < count && ["+", "-"].contains(tokens[index + 1]) {
        tokens[index + 1] = if tokens[index + 1] == "+" { "-" } else { "+" }
        fields.tzoffset = None
        if parser_utc(token) {
          fields.tzname = None
        }
      }
    } else if fields.hour is Some(_) && ["+", "-"].contains(token) {
      let sign = if token == "+" { 1 } else { -1 }
      let raw = parser_token(tokens, index + 1)
      let length = raw.to_array().length()
      let (hour, minute) = if length == 4 {
        (
          parser_integer(parser_slice(raw, 0, 2)),
          parser_integer(parser_slice(raw, 2, 4)),
        )
      } else if index + 2 < count && tokens[index + 2] == ":" {
        let minute = parser_integer(parser_token(tokens, index + 3))
        index += 2
        (parser_integer(raw), minute)
      } else if length <= 2 {
        (parser_integer(raw), 0)
      } else {
        raise DateError::InvalidDate(message="invalid numeric timezone")
      }
      let offset = Int64::from_int(hour) * 3600L + Int64::from_int(minute) * 60L
      if offset > 2147483647L {
        raise DateError::InvalidDate(message="timezone offset is out of range")
      }
      fields.tzoffset = Some(sign * offset.to_int())
      if index + 5 < count &&
        parser_jump(tokens[index + 2]) &&
        tokens[index + 3] == "(" &&
        tokens[index + 5] == ")" &&
        tokens[index + 4].to_array().length() >= 3 &&
        fields.could_be_tzname(tokens[index + 4], ignore_offset=true) {
        fields.tzname = Some(tokens[index + 4])
        index += 4
      }
      index += 1
    } else if !parser_jump(token) {
      raise DateError::InvalidDate(message="unknown date string token")
    }
    index += 1
  }
  let (year, month, day) = ymd.resolve()
  fields.century = ymd.century
  fields.year = year.map(year => {
    parser_convert_year(year, fields.century, reference_year)
  })
  fields.month = month
  fields.day = day
  if (
      fields.tzoffset == Some(0) &&
      (fields.tzname is None || fields.tzname == Some(""))
    ) ||
    fields.tzname == Some("Z") ||
    fields.tzname == Some("z") {
    fields.tzname = Some("UTC")
    fields.tzoffset = Some(0)
  } else if fields.tzoffset != Some(0) &&
    fields.tzname is Some(name) &&
    parser_utc(name) {
    fields.tzoffset = Some(0)
  }
  fields
}