///|
fn duration_millis(text : String) -> Int64 raise ParseError {
  let cs = text.to_array()
  let mut i = 0
  let mut previous = 8
  let mut total = 0L
  while i < cs.length() {
    let begin = i
    let mut amount = 0L
    while i < cs.length() && digit(cs[i]) {
      let d = (cs[i].to_int() - 48).to_int64()
      if amount > (9223372036854L - d) / 10L {
        raise Invalid("duration overflow")
      }
      amount = amount * 10L + d
      i += 1
    }
    if i == begin || i == cs.length() {
      raise Invalid("duration requires integer and unit")
    }
    let (order, multiplier) = match cs[i] {
      'y' => (7, 31536000000L)
      'w' => (6, 604800000L)
      'd' => (5, 86400000L)
      'h' => (4, 3600000L)
      'm' =>
        if i + 1 < cs.length() && cs[i + 1] == 's' {
          i += 1
          (1, 1L)
        } else {
          (3, 60000L)
        }
      's' => (2, 1000L)
      _ => raise Invalid("unknown duration unit")
    }
    i += 1
    if order >= previous {
      raise Invalid("duration units must be descending without repetition")
    }
    previous = order
    if amount > (9223372036854L - total) / multiplier {
      raise Invalid("duration overflow")
    }
    total += amount * multiplier
  }
  if cs.is_empty() {
    raise Invalid("empty duration")
  }
  total
}

///|
/// Convert a PromQL numeric or duration literal using the pinned language rules.
pub fn number_value(literal : String) -> Double raise ParseError {
  if literal.is_empty() || literal.length() > 100000 {
    raise Invalid("invalid numeric literal size")
  }
  let (negative, text) = if literal.has_prefix("-") {
    (true, literal[1:].to_owned())
  } else if literal.has_prefix("+") {
    (false, literal[1:].to_owned())
  } else {
    (false, literal)
  }
  let lower = text.to_lower()
  let value = if lower == "inf" {
    @double.infinity
  } else if lower == "nan" {
    @double.not_a_number
  } else if lower.has_prefix("0x") {
    let cs = text.to_array()
    let mut i = 2
    if i < cs.length() && cs[i] == '_' {
      i += 1
    }
    let begin = i
    let mut n = 0L
    while i < cs.length() {
      if cs[i] == '_' {
        if i == begin ||
          i + 1 >= cs.length() ||
          hex_digit(cs[i - 1]) < 0 ||
          hex_digit(cs[i + 1]) < 0 {
          raise Invalid("hexadecimal underscore placement")
        }
        i += 1
        continue
      }
      let d = hex_digit(cs[i])
      if d < 0 || n > (9223372036854775807L - d.to_int64()) / 16L {
        raise Invalid("hexadecimal integer syntax or signed-64 overflow")
      }
      n = n * 16L + d.to_int64()
      i += 1
    }
    if i == begin {
      raise Invalid("hexadecimal digits required")
    }
    n.to_double()
  } else if text
    .iter()
    .any(c => {
      c == 'm' || c == 's' || c == 'h' || c == 'd' || c == 'w' || c == 'y'
    }) {
    duration_millis(text).to_double() / 1000.0
  } else {
    let original = text.to_array()
    for i, c in original {
      if c == '_' &&
        (
          i == 0 ||
          i + 1 == original.length() ||
          !digit(original[i - 1]) ||
          !digit(original[i + 1])
        ) {
        raise Invalid("decimal underscore placement")
      }
    }
    let clean = text.replace_all(old="_", new="")
    let cs = clean.to_array()
    let mut i = 0
    let mut count = 0
    while i < cs.length() && digit(cs[i]) {
      i += 1
      count += 1
    }
    if i < cs.length() && cs[i] == '.' {
      i += 1
      while i < cs.length() && digit(cs[i]) {
        i += 1
        count += 1
      }
    }
    if count == 0 {
      raise Invalid("decimal digits required")
    }
    if i < cs.length() && (cs[i] == 'e' || cs[i] == 'E') {
      i += 1
      if i < cs.length() && (cs[i] == '+' || cs[i] == '-') {
        i += 1
      }
      let start = i
      while i < cs.length() && digit(cs[i]) {
        i += 1
      }
      if i == start {
        raise Invalid("exponent digits required")
      }
    }
    if i != cs.length() {
      raise Invalid("invalid decimal syntax")
    }
    let mut octal = cs.length() > 1 &&
      cs[0] == '0' &&
      cs.iter().all(c => c >= '0' && c <= '7')
    let mut integer = 0L
    if octal {
      for c in cs {
        let d = (c.to_int() - 48).to_int64()
        if integer > (9223372036854775807L - d) / 8L {
          octal = false
          break
        }
        integer = integer * 8L + d
      }
    }
    if octal {
      integer.to_double()
    } else {
      let parsed = @string.parse_double(clean) catch {
        _ => raise Invalid("numeric conversion failed")
      }
      if parsed.is_inf() {
        raise Invalid("numeric literal overflows float64")
      }
      parsed
    }
  }
  if negative {
    -value
  } else {
    value
  }
}