///|
// Amounts are BigInt counts of 10^-18 currency units. Never converted to Float.
fn factor() -> BigInt {
  1000000000000000000N
}

///|
fn natural(text : String, path : String) -> BigInt raise PricingError {
  if text.length() == 0 || text.length() > 30 {
    invalid("integer", path, "use 1..30 ASCII digits (maximum 10^30 - 1)")
  }
  if text.length() > 1 && text[0] == '0' {
    invalid("integer", path, "remove leading zeroes")
  }
  for c in text {
    if c < '0' || c > '9' {
      invalid("integer", path, "use a nonnegative decimal integer string")
    }
  }
  BigInt::from_string(text)
}

///|
fn decimal(text : String, path : String) -> BigInt raise PricingError {
  if text.length() == 0 || text.length() > 49 {
    invalid("decimal", path, "use at most 30 integer and 18 fractional digits")
  }
  let parts = text.split(".").collect()
  if parts.length() > 2 {
    invalid(
      "decimal", path, "use plain nonnegative decimal notation, without exponent",
    )
  }
  let whole = natural(parts[0].to_owned(), path)
  let fraction = if parts.length() == 2 {
    let part = parts[1].to_owned()
    if part.length() == 0 || part.length() > 18 {
      invalid("decimal", path, "fraction must contain 1..18 ASCII digits")
    }
    for c in part {
      if c < '0' || c > '9' {
        invalid("decimal", path, "fraction must contain only ASCII digits")
      }
    }
    BigInt::from_string(part + "0".repeat(18 - part.length()))
  } else {
    0N
  }
  whole * factor() + fraction
}

///|
fn format_atoms(atoms : BigInt) -> String {
  if atoms < 0N {
    return "-" + format_atoms(-atoms)
  }
  let whole = (atoms / factor()).to_string()
  let remainder = (atoms % factor()).to_string()
  if atoms % factor() == 0N {
    return whole
  }
  let digits = "0".repeat(18 - remainder.length()) + remainder
  let mut end = digits.length()
  while end > 0 && digits[end - 1] == '0' {
    end -= 1
  }
  whole + "." + digits[:end].to_owned()
}

///|
fn label(text : String, path : String) -> Unit raise PricingError {
  if text.length() == 0 || text.length() > 256 {
    invalid(
      "label", path, "use a nonempty label of at most 256 UTF-16 code units",
    )
  }
  for c in text {
    if c < ' ' || c == '\u007f' {
      invalid("label", path, "control characters are not allowed")
    }
  }
}