///|
/// A bounded decimal that preserves the coefficient and number of fractional digits.
pub(all) struct ExactDecimal {
  coefficient_value : Int64
  scale_value : Int
  negative_zero_value : Bool
} derive(Eq, Debug)

///|
fn decimal_error(message : String) -> NmeaError {
  NmeaError::from_diagnostic(
    Diagnostic::new(FieldInvalidDecimal, Error, message, SourceRef::sentence(0)),
  )
}

///|
/// Parses a base-10 decimal without accepting exponent notation.
pub fn ExactDecimal::parse(
  text : String,
  max_scale? : Int = 9,
) -> Result[ExactDecimal, NmeaError] {
  if max_scale < 0 || text.length() == 0 {
    return Err(decimal_error("decimal is empty or has an invalid scale limit"))
  }
  let mut index = 0
  let mut negative = false
  if text[0].to_int() == 45 {
    negative = true
    index = 1
  } else if text[0].to_int() == 43 {
    index = 1
  }
  if index >= text.length() {
    return Err(decimal_error("decimal has no digits"))
  }
  let mut coefficient : Int64 = 0L
  let mut scale = 0
  let mut digit_count = 0
  let mut seen_dot = false
  let mut saw_digit = false
  while index < text.length() {
    let code = text[index].to_int()
    if code == 46 {
      if seen_dot {
        return Err(decimal_error("decimal contains multiple decimal points"))
      }
      seen_dot = true
      index = index + 1
      continue
    }
    if code < 48 || code > 57 {
      return Err(decimal_error("decimal contains a non-digit character"))
    }
    saw_digit = true
    digit_count = digit_count + 1
    if digit_count > 18 {
      return Err(decimal_error("decimal exceeds the supported digit bound"))
    }
    if seen_dot {
      scale = scale + 1
      if scale > max_scale {
        return Err(decimal_error("decimal exceeds the configured scale"))
      }
    }
    coefficient = coefficient * 10L + (code - 48).to_int64()
    index = index + 1
  }
  if !saw_digit {
    return Err(decimal_error("decimal has no digits"))
  }
  let signed = if negative { -coefficient } else { coefficient }
  Ok({
    coefficient_value: signed,
    scale_value: scale,
    negative_zero_value: negative && coefficient == 0L,
  })
}

///|
pub fn ExactDecimal::coefficient(self : ExactDecimal) -> Int64 {
  self.coefficient_value
}

///|
pub fn ExactDecimal::scale(self : ExactDecimal) -> Int {
  self.scale_value
}

///|
pub fn ExactDecimal::is_negative_zero(self : ExactDecimal) -> Bool {
  self.negative_zero_value
}

///|
pub fn ExactDecimal::compare(self : ExactDecimal, other : ExactDecimal) -> Int {
  let mut left = self.coefficient_value
  let mut right = other.coefficient_value
  if self.scale_value < other.scale_value {
    let mut count = 0
    while count < other.scale_value - self.scale_value {
      left = left * 10L
      count = count + 1
    }
  } else if other.scale_value < self.scale_value {
    let mut count = 0
    while count < self.scale_value - other.scale_value {
      right = right * 10L
      count = count + 1
    }
  }
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
pub fn ExactDecimal::to_string(self : ExactDecimal) -> String {
  let negative = self.coefficient_value < 0L || self.negative_zero_value
  let magnitude = if self.coefficient_value < 0L {
    -self.coefficient_value
  } else {
    self.coefficient_value
  }
  if self.scale_value == 0 {
    return (if negative { "-" } else { "" }) + magnitude.to_string()
  }
  let digits = magnitude.to_string()
  let padded = if digits.length() <= self.scale_value {
    "0" + String::repeat("0", self.scale_value - digits.length()) + digits
  } else {
    digits
  }
  let split = padded.length() - self.scale_value
  let whole = padded[:split].to_owned()
  let fraction = padded[split:].to_owned()
  (if negative { "-" } else { "" }) + whole + "." + fraction
}

///|
pub fn ExactDecimal::to_double(self : ExactDecimal) -> Double {
  let mut divisor = 1.0
  for _ in 0..