///|
// Round the exact binary64 value to a decimal quantum, then convert the decimal
// back to binary64. This mirrors CPython's correctly rounded dtoa/strtod path;
// multiplying a Double by 10^digits first can turn a near-tie into a false tie.
fn relative_decimal_round(
  value : Double,
  digits : Int32,
) -> Double raise DateError {
  relative_finite(value)
  if digits < 0 || digits > 11 {
    raise DateError::InvalidDate(
      message="relative decimal precision is out of range",
    )
  }
  if value == 0.0 {
    return value
  }
  let bits = value.reinterpret_as_uint64()
  let negative = bits >> 63 != 0UL
  let encoded_exponent = ((bits >> 52) & 2047UL).to_int()
  let fraction = bits & 4503599627370495UL
  let (mantissa, exponent) = if encoded_exponent == 0 {
    (fraction, -1074)
  } else {
    (fraction + 4503599627370496UL, encoded_exponent - 1023 - 52)
  }
  let mut scale = 1L
  for _ in 0..= 0 {
    (numerator.shl(exponent), @bigint.BigInt::from_int(1))
  } else {
    (numerator, @bigint.BigInt::from_int(1).shl(-exponent))
  }
  let quotient = numerator / denominator
  let remainder = numerator % denominator
  let comparison = (remainder + remainder).compare(denominator)
  let rounded = if comparison > 0 ||
    (comparison == 0 && !(quotient % @bigint.BigInt::from_int(2)).is_zero()) {
    quotient + @bigint.BigInt::from_int(1)
  } else {
    quotient
  }
  let text = (if negative { "-" } else { "" }) +
    rounded.to_string() +
    "e-" +
    digits.to_string()
  let result = @string.parse_double(text) catch {
    _ =>
      raise DateError::InvalidDate(message="relative decimal conversion failed")
  }
  relative_finite(result)
  result
}

///|
fn relative_integer_double(value : Double) -> Double {
  if value == 0.0 {
    0.0
  } else {
    value.trunc()
  }
}

///|
/// Return integer-valued relative day/time components using dateutil's staged
/// decimal rounding. Absolute fields, leapdays and weekday remain unchanged.
///
/// ```mbt check
/// test {
///   let delta = @date.RelativeDelta::new(days=1.5, hours=2.0).normalized()
///   assert_eq((delta.days(), delta.hours()), (1.0, 14.0))
///   let next = @date.datetime(2024, 1, 31).add_relative_delta(
///     @date.RelativeDelta::new(months=1L),
///   )
///   assert_eq((next.month(), next.day()), (2, 29))
/// }
/// ```
pub fn RelativeDelta::normalized(
  self : RelativeDelta,
) -> RelativeDelta raise DateError {
  let days = relative_integer_double(self.days)
  let hours_f = relative_decimal_round(
    self.hours + 24.0 * (self.days - days),
    11,
  )
  let hours = relative_integer_double(hours_f)
  let minutes_f = relative_decimal_round(
    self.minutes + 60.0 * (hours_f - hours),
    10,
  )
  let minutes = relative_integer_double(minutes_f)
  let seconds_f = relative_decimal_round(
    self.seconds + 60.0 * (minutes_f - minutes),
    8,
  )
  let seconds = relative_integer_double(seconds_f)
  let microseconds = round_even_to_int64(
    self.microseconds + 1000000.0 * (seconds_f - seconds),
  )
  RelativeDelta::new(
    years=self.years,
    months=Int64::from_int(self.months),
    days~,
    hours~,
    minutes~,
    seconds~,
    microseconds=microseconds.to_double(),
    leapdays=self.leapdays,
    year?=self.year,
    month?=self.month,
    day?=self.day,
    hour?=self.hour,
    minute?=self.minute,
    second?=self.second,
    microsecond?=self.microsecond,
    on_weekday?=self.weekday,
  )
}