///|
/// A Temporal unit, from `Table 21: Temporal units by descending magnitude`.
///
/// The declaration order is significant: comparison operators use it, so a
/// larger unit compares greater. `Auto` sorts below every real unit, matching
/// the discriminant order `temporal_rs` relies on.
///
/// Spec: 
pub(all) enum DateTimeUnit {
  /// The `"auto"` sentinel, resolved to a concrete unit before use.
  Auto
  /// Nanoseconds.
  Nanosecond
  /// Microseconds.
  Microsecond
  /// Milliseconds.
  Millisecond
  /// Seconds.
  Second
  /// Minutes.
  Minute
  /// Hours.
  Hour
  /// Days.
  Day
  /// Weeks.
  Week
  /// Months.
  Month
  /// Years.
  Year
} derive(Eq, Compare, Hash, Debug)

///|
pub impl Show for DateTimeUnit with fn output(self, logger) {
  logger.write_string(self.to_singular())
}

///|
/// Returns the singular option string for this unit, as accepted and produced
/// by the Temporal API.
pub fn DateTimeUnit::to_singular(self : DateTimeUnit) -> String {
  match self {
    Auto => "auto"
    Year => "year"
    Month => "month"
    Week => "week"
    Day => "day"
    Hour => "hour"
    Minute => "minute"
    Second => "second"
    Millisecond => "millisecond"
    Microsecond => "microsecond"
    Nanosecond => "nanosecond"
  }
}

///|
/// Parses a unit from its option string. Both singular and plural spellings
/// are accepted, as in `GetTemporalUnitValuedOption`.
///
/// ```mbt check
/// test {
///   inspect(@temporal.DateTimeUnit::of_string("days"), content="day")
///   inspect(@temporal.DateTimeUnit::of_string("nanosecond"), content="nanosecond")
/// }
/// ```
pub fn DateTimeUnit::of_string(s : String) -> DateTimeUnit raise TemporalError {
  match s {
    "auto" => Auto
    "year" | "years" => Year
    "month" | "months" => Month
    "week" | "weeks" => Week
    "day" | "days" => Day
    "hour" | "hours" => Hour
    "minute" | "minutes" => Minute
    "second" | "seconds" => Second
    "millisecond" | "milliseconds" => Millisecond
    "microsecond" | "microseconds" => Microsecond
    "nanosecond" | "nanoseconds" => Nanosecond
    _ => raise RangeError("'\{s}' is not a valid Temporal unit")
  }
}

///|
/// Returns whether this is one of the calendar units: year, month or week.
pub fn DateTimeUnit::is_calendar_unit(self : DateTimeUnit) -> Bool {
  self is (Year | Month | Week)
}

///|
/// Returns whether this unit belongs to the date group: year, month, week or
/// day.
pub fn DateTimeUnit::is_date_unit(self : DateTimeUnit) -> Bool {
  self is (Year | Month | Week | Day)
}

///|
/// Returns whether this unit belongs to the time group: hour through
/// nanosecond.
pub fn DateTimeUnit::is_time_unit(self : DateTimeUnit) -> Bool {
  self is (Hour | Minute | Second | Millisecond | Microsecond | Nanosecond)
}

///|
/// Returns the length of this unit in nanoseconds, or `None` for units whose
/// length is not fixed (year, month, week) and for `Auto`.
pub fn DateTimeUnit::as_nanoseconds(self : DateTimeUnit) -> Int64? {
  match self {
    Year | Month | Week | Auto => None
    Day => Some(NS_PER_DAY)
    Hour => Some(NS_PER_HOUR)
    Minute => Some(NS_PER_MINUTE)
    Second => Some(NS_PER_SECOND)
    Millisecond => Some(1_000_000L)
    Microsecond => Some(1_000L)
    Nanosecond => Some(1L)
  }
}

///|
/// `MaximumTemporalDurationRoundingIncrement`: the largest rounding increment
/// permitted for this unit, or `None` when unbounded.
pub fn DateTimeUnit::to_maximum_rounding_increment(self : DateTimeUnit) -> Int? {
  match self {
    Year | Month | Week | Day | Auto => None
    Hour => Some(24)
    Minute | Second => Some(60)
    Millisecond | Microsecond | Nanosecond => Some(1000)
  }
}

///|
/// `LargerOfTwoTemporalUnits`.
pub fn DateTimeUnit::larger(
  self : DateTimeUnit,
  other : DateTimeUnit,
) -> DateTimeUnit {
  if self >= other {
    self
  } else {
    other
  }
}

///|
/// Returns the ordinal of this unit in `Table 21`, counting down from year.
///
/// Used by `BubbleRelativeDuration` to step between adjacent units.
fn DateTimeUnit::table_index(self : DateTimeUnit) -> Int raise TemporalError {
  match self {
    Year => 0
    Month => 1
    Week => 2
    Day => 3
    Hour => 4
    Minute => 5
    Second => 6
    Millisecond => 7
    Microsecond => 8
    Nanosecond => 9
    Auto => raise AssertError("'auto' units are not allowed during comparison")
  }
}

///|
/// Inverse of [`DateTimeUnit::table_index`].
fn unit_from_table_index(index : Int) -> DateTimeUnit {
  match index {
    0 => Year
    1 => Month
    2 => Week
    3 => Day
    4 => Hour
    5 => Minute
    6 => Second
    7 => Millisecond
    8 => Microsecond
    9 => Nanosecond
    _ => Auto
  }
}

///|
/// Which units an option is allowed to name.
///
/// Spec: 
pub(all) enum UnitGroup {
  /// Date units only.
  Date
  /// Time units only.
  Time
  /// Any unit.
  DateTime
} derive(Eq, Debug)

///|
/// Validates that `unit` is permitted in this group.
///
/// `extra_unit` names an additional unit that is allowed even when it is not
/// part of the group, which is how `auto` is threaded through.
fn UnitGroup::validate_unit(
  self : UnitGroup,
  unit : DateTimeUnit?,
  extra_unit : DateTimeUnit?,
) -> DateTimeUnit? raise TemporalError {
  if unit == extra_unit {
    return unit
  }
  match self {
    Date =>
      match unit {
        Some(u) if u.is_date_unit() => unit
        None => unit
        _ => raise RangeError("unit was not part of the date unit group")
      }
    Time =>
      match unit {
        Some(u) if u.is_time_unit() => unit
        None => unit
        _ => raise RangeError("unit was not part of the time unit group")
      }
    DateTime =>
      if unit != Some(Auto) {
        unit
      } else {
        raise RangeError("'auto' units are not allowed during comparison")
      }
  }
}

///|
/// Validates a unit option that must be present.
fn UnitGroup::validate_required_unit(
  self : UnitGroup,
  unit : DateTimeUnit?,
  extra_unit : DateTimeUnit?,
) -> DateTimeUnit raise TemporalError {
  match unit {
    None => raise RangeError("unit is required")
    Some(u) => {
      self.validate_unit(Some(u), extra_unit) |> ignore
      u
    }
  }
}

///|
/// How to handle a field that is out of range for its calendar.
pub(all) enum Overflow {
  /// Clamp the field into range. This is the default.
  Constrain
  /// Raise a `RangeError`.
  Reject
} derive(Eq, Debug)

///|
pub impl Default for Overflow with fn default() {
  Constrain
}

///|
pub impl Show for Overflow with fn output(self, logger) {
  logger.write_string(
    match self {
      Constrain => "constrain"
      Reject => "reject"
    },
  )
}

///|
/// Parses an `overflow` option value.
pub fn Overflow::of_string(s : String) -> Overflow raise TemporalError {
  match s {
    "constrain" => Constrain
    "reject" => Reject
    _ => raise RangeError("'\{s}' is not a valid overflow value")
  }
}

///|
/// How to resolve a wall-clock time that is ambiguous or nonexistent in a time
/// zone, as happens around a DST transition.
pub(all) enum Disambiguation {
  /// Pick the earlier instant for ambiguous times and the later one for gaps.
  /// This is the default.
  Compatible
  /// Always pick the earlier instant.
  Earlier
  /// Always pick the later instant.
  Later
  /// Raise a `RangeError`.
  Reject
} derive(Eq, Debug)

///|
pub impl Default for Disambiguation with fn default() {
  Compatible
}

///|
pub impl Show for Disambiguation with fn output(self, logger) {
  logger.write_string(
    match self {
      Compatible => "compatible"
      Earlier => "earlier"
      Later => "later"
      Reject => "reject"
    },
  )
}

///|
/// Parses a `disambiguation` option value.
pub fn Disambiguation::of_string(
  s : String,
) -> Disambiguation raise TemporalError {
  match s {
    "compatible" => Compatible
    "earlier" => Earlier
    "later" => Later
    "reject" => Reject
    _ => raise RangeError("'\{s}' is not a valid disambiguation value")
  }
}

///|
/// How to treat a UTC offset that disagrees with the named time zone.
pub(all) enum OffsetDisambiguation {
  /// Trust the offset and ignore the time zone's own rules.
  Use
  /// Use the offset when it matches a possible instant, otherwise fall back to
  /// disambiguation.
  Prefer
  /// Ignore the offset entirely.
  Ignore
  /// Raise a `RangeError` on any mismatch.
  Reject
} derive(Eq, Debug)

///|
pub impl Show for OffsetDisambiguation with fn output(self, logger) {
  logger.write_string(
    match self {
      Use => "use"
      Prefer => "prefer"
      Ignore => "ignore"
      Reject => "reject"
    },
  )
}

///|
/// Parses an `offset` option value.
pub fn OffsetDisambiguation::of_string(
  s : String,
) -> OffsetDisambiguation raise TemporalError {
  match s {
    "use" => Use
    "prefer" => Prefer
    "ignore" => Ignore
    "reject" => Reject
    _ => raise RangeError("'\{s}' is not a valid offset option value")
  }
}

///|
/// The rounding mode applied when a value falls between two increments.
pub(all) enum RoundingMode {
  /// Toward positive infinity.
  Ceil
  /// Toward negative infinity.
  Floor
  /// Away from zero.
  Expand
  /// Toward zero.
  Trunc
  /// To nearest, ties toward positive infinity.
  HalfCeil
  /// To nearest, ties toward negative infinity.
  HalfFloor
  /// To nearest, ties away from zero. This is the default for rounding.
  HalfExpand
  /// To nearest, ties toward zero.
  HalfTrunc
  /// To nearest, ties to even.
  HalfEven
} derive(Eq, Debug)

///|
pub impl Default for RoundingMode with fn default() {
  HalfExpand
}

///|
pub impl Show for RoundingMode with fn output(self, logger) {
  logger.write_string(
    match self {
      Ceil => "ceil"
      Floor => "floor"
      Expand => "expand"
      Trunc => "trunc"
      HalfCeil => "halfCeil"
      HalfFloor => "halfFloor"
      HalfExpand => "halfExpand"
      HalfTrunc => "halfTrunc"
      HalfEven => "halfEven"
    },
  )
}

///|
/// Parses a `roundingMode` option value.
pub fn RoundingMode::of_string(s : String) -> RoundingMode raise TemporalError {
  match s {
    "ceil" => Ceil
    "floor" => Floor
    "expand" => Expand
    "trunc" => Trunc
    "halfCeil" => HalfCeil
    "halfFloor" => HalfFloor
    "halfExpand" => HalfExpand
    "halfTrunc" => HalfTrunc
    "halfEven" => HalfEven
    _ => raise RangeError("'\{s}' is not a valid roundingMode value")
  }
}

///|
/// `NegateRoundingMode`: mirrors the mode about zero, used to turn an `until`
/// rounding mode into the `since` one.
pub fn RoundingMode::negate(self : RoundingMode) -> RoundingMode {
  match self {
    Ceil => Floor
    Floor => Ceil
    HalfCeil => HalfFloor
    HalfFloor => HalfCeil
    Trunc => Trunc
    Expand => Expand
    HalfTrunc => HalfTrunc
    HalfExpand => HalfExpand
    HalfEven => HalfEven
  }
}

///|
/// `GetUnsignedRoundingMode`: resolves a signed mode against the sign of the
/// value being rounded.
pub fn RoundingMode::to_unsigned(
  self : RoundingMode,
  is_positive : Bool,
) -> UnsignedRoundingMode {
  match self {
    Ceil => if is_positive { Infinity } else { Zero }
    Trunc => Zero
    Floor => if is_positive { Zero } else { Infinity }
    Expand => Infinity
    HalfCeil => if is_positive { HalfInfinity } else { HalfZero }
    HalfTrunc => HalfZero
    HalfFloor => if is_positive { HalfZero } else { HalfInfinity }
    HalfExpand => HalfInfinity
    HalfEven => HalfEven
  }
}

///|
/// A rounding mode with the sign of the operand already folded in.
pub(all) enum UnsignedRoundingMode {
  /// Round away from zero.
  Infinity
  /// Round toward zero.
  Zero
  /// To nearest, ties away from zero.
  HalfInfinity
  /// To nearest, ties toward zero.
  HalfZero
  /// To nearest, ties to even.
  HalfEven
} derive(Eq, Debug)

///|
/// Whether `toString` should include the calendar annotation.
pub(all) enum DisplayCalendar {
  /// Include it only for a non-ISO calendar. This is the default.
  Auto
  /// Always include it.
  Always
  /// Never include it.
  Never
  /// Always include it, flagged critical with a `!`.
  Critical
} derive(Eq, Debug)

///|
pub impl Default for DisplayCalendar with fn default() {
  Auto
}

///|
pub impl Show for DisplayCalendar with fn output(self, logger) {
  logger.write_string(
    match self {
      Auto => "auto"
      Always => "always"
      Never => "never"
      Critical => "critical"
    },
  )
}

///|
/// Parses a `calendarName` option value.
pub fn DisplayCalendar::of_string(
  s : String,
) -> DisplayCalendar raise TemporalError {
  match s {
    "auto" => Auto
    "always" => Always
    "never" => Never
    "critical" => Critical
    _ => raise RangeError("'\{s}' is not a valid calendarName value")
  }
}

///|
/// Whether `toString` should include the UTC offset.
pub(all) enum DisplayOffset {
  /// Include it. This is the default.
  Auto
  /// Omit it.
  Never
} derive(Eq, Debug)

///|
pub impl Default for DisplayOffset with fn default() {
  Auto
}

///|
pub impl Show for DisplayOffset with fn output(self, logger) {
  logger.write_string(
    match self {
      Auto => "auto"
      Never => "never"
    },
  )
}

///|
/// Parses an `offset` display option value.
pub fn DisplayOffset::of_string(
  s : String,
) -> DisplayOffset raise TemporalError {
  match s {
    "auto" => Auto
    "never" => Never
    _ => raise RangeError("'\{s}' is not a valid offset display value")
  }
}

///|
/// Whether `toString` should include the time zone annotation.
pub(all) enum DisplayTimeZone {
  /// Include it. This is the default.
  Auto
  /// Omit it.
  Never
  /// Include it, flagged critical with a `!`.
  Critical
} derive(Eq, Debug)

///|
pub impl Default for DisplayTimeZone with fn default() {
  Auto
}

///|
pub impl Show for DisplayTimeZone with fn output(self, logger) {
  logger.write_string(
    match self {
      Auto => "auto"
      Never => "never"
      Critical => "critical"
    },
  )
}

///|
/// Parses a `timeZoneName` option value.
pub fn DisplayTimeZone::of_string(
  s : String,
) -> DisplayTimeZone raise TemporalError {
  match s {
    "auto" => Auto
    "never" => Never
    "critical" => Critical
    _ => raise RangeError("'\{s}' is not a valid timeZoneName value")
  }
}

///|
/// The number of fractional-second digits `toString` should emit.
pub(all) enum Precision {
  /// Emit as many digits as needed, and none when the sub-second part is zero.
  Auto
  /// Emit exactly this many digits, from 0 to 9.
  Digit(Int)
  /// Stop at minutes, emitting no seconds at all.
  Minute
} derive(Eq, Debug)

///|
pub impl Default for Precision with fn default() {
  Auto
}

///|
/// A validated rounding increment, in `1 ..= 10^9`.
pub struct RoundingIncrement(Int) derive(Eq, Compare, Debug)

///|
pub impl Show for RoundingIncrement with fn output(self, logger) {
  logger.write_string(self.get().to_string())
}

///|
pub impl Default for RoundingIncrement with fn default() {
  RoundingIncrement(1)
}

///|
/// A rounding increment of one, meaning "round to the unit itself".
pub let rounding_increment_one : RoundingIncrement = RoundingIncrement(1)

///|
/// Creates a rounding increment, rejecting values outside `1 ..= 10^9`.
///
/// ```mbt check
/// test {
///   inspect(@temporal.RoundingIncrement::try_new(30), content="30")
/// }
/// ```
pub fn RoundingIncrement::try_new(
  increment : Int,
) -> RoundingIncrement raise TemporalError {
  if increment < 1 || increment > 1_000_000_000 {
    raise RangeError("roundingIncrement must be between 1 and 10**9")
  }
  RoundingIncrement(increment)
}

///|
/// Returns the numeric value of the increment.
pub fn RoundingIncrement::get(self : RoundingIncrement) -> Int {
  self.0
}

///|
/// `ValidateTemporalRoundingIncrement`: checks the increment evenly divides
/// `dividend` and does not exceed the permitted maximum.
fn RoundingIncrement::validate(
  self : RoundingIncrement,
  dividend : Int64,
  inclusive : Bool,
) -> Unit raise TemporalError {
  let max = if inclusive { dividend } else { dividend - 1L }
  let increment = self.get().to_int64()
  if increment > max {
    raise RangeError("roundingIncrement exceeds its maximum")
  }
  if rem_euclid(dividend, increment) != 0L {
    raise RangeError("dividend is not divisible by roundingIncrement")
  }
}

///|
/// The unit-and-rounding options accepted by the `until` and `since` methods.
pub struct DifferenceSettings {
  /// The largest unit the resulting duration may use.
  largest_unit : DateTimeUnit?
  /// The unit the result is rounded to.
  smallest_unit : DateTimeUnit?
  /// How to round.
  rounding_mode : RoundingMode?
  /// The rounding increment.
  increment : RoundingIncrement?
} derive(Default, Debug)

///|
/// Builds difference settings; every field defaults to unset.
pub fn DifferenceSettings::new(
  largest_unit? : DateTimeUnit,
  smallest_unit? : DateTimeUnit,
  rounding_mode? : RoundingMode,
  increment? : RoundingIncrement,
) -> DifferenceSettings {
  { largest_unit, smallest_unit, rounding_mode, increment }
}

///|
/// The options accepted by the `round` methods.
pub struct RoundingOptions {
  /// The largest unit the result may use.
  largest_unit : DateTimeUnit?
  /// The unit to round to.
  smallest_unit : DateTimeUnit?
  /// How to round.
  rounding_mode : RoundingMode?
  /// The rounding increment.
  increment : RoundingIncrement?
} derive(Debug)

///|
pub impl Default for RoundingOptions with fn default() {
  {
    largest_unit: Some(Auto),
    smallest_unit: None,
    rounding_mode: None,
    increment: None,
  }
}

///|
/// Builds rounding options; every field defaults to unset.
pub fn RoundingOptions::new(
  largest_unit? : DateTimeUnit,
  smallest_unit? : DateTimeUnit,
  rounding_mode? : RoundingMode,
  increment? : RoundingIncrement,
) -> RoundingOptions {
  { largest_unit, smallest_unit, rounding_mode, increment }
}

///|
/// The options controlling how `toString` renders sub-second precision.
pub struct ToStringRoundingOptions {
  /// How many fractional digits to emit.
  precision : Precision
  /// The unit to round to, which overrides `precision` when set.
  smallest_unit : DateTimeUnit?
  /// How to round; defaults to truncation.
  rounding_mode : RoundingMode?
} derive(Default, Debug)

///|
/// Builds `toString` rounding options.
pub fn ToStringRoundingOptions::new(
  precision? : Precision = Auto,
  smallest_unit? : DateTimeUnit,
  rounding_mode? : RoundingMode,
) -> ToStringRoundingOptions {
  { precision, smallest_unit, rounding_mode }
}

///|
/// `toString` rounding options with every choice made.
priv struct ResolvedToStringRoundingOptions {
  precision : Precision
  smallest_unit : DateTimeUnit
  rounding_mode : RoundingMode
  increment : RoundingIncrement
}

///|
/// Resolves the `smallestUnit` and `fractionalSecondDigits` options against
/// each other, as `ToSecondsStringPrecisionRecord` does.
fn ToStringRoundingOptions::resolve(
  self : ToStringRoundingOptions,
) -> ResolvedToStringRoundingOptions raise TemporalError {
  let rounding_mode = self.rounding_mode.unwrap_or(Trunc)
  match self.smallest_unit {
    Some(Minute) =>
      {
        precision: Minute,
        smallest_unit: Minute,
        rounding_mode,
        increment: rounding_increment_one,
      }
    Some(Second) =>
      {
        precision: Digit(0),
        smallest_unit: Second,
        rounding_mode,
        increment: rounding_increment_one,
      }
    Some(Millisecond) =>
      {
        precision: Digit(3),
        smallest_unit: Millisecond,
        rounding_mode,
        increment: rounding_increment_one,
      }
    Some(Microsecond) =>
      {
        precision: Digit(6),
        smallest_unit: Microsecond,
        rounding_mode,
        increment: rounding_increment_one,
      }
    Some(Nanosecond) =>
      {
        precision: Digit(9),
        smallest_unit: Nanosecond,
        rounding_mode,
        increment: rounding_increment_one,
      }
    Some(_) => raise RangeError("smallestUnit must be a valid time unit")
    None =>
      match self.precision {
        Auto =>
          {
            precision: Auto,
            smallest_unit: Nanosecond,
            rounding_mode,
            increment: rounding_increment_one,
          }
        Digit(0) =>
          {
            precision: Digit(0),
            smallest_unit: Second,
            rounding_mode,
            increment: rounding_increment_one,
          }
        // Fewer digits than a unit provides means rounding by a power of ten.
        Digit(d) if d >= 1 && d <= 3 =>
          {
            precision: Digit(d),
            smallest_unit: Millisecond,
            rounding_mode,
            increment: RoundingIncrement::try_new(pow10(3 - d)),
          }
        Digit(d) if d >= 4 && d <= 6 =>
          {
            precision: Digit(d),
            smallest_unit: Microsecond,
            rounding_mode,
            increment: RoundingIncrement::try_new(pow10(6 - d)),
          }
        Digit(d) if d >= 7 && d <= 9 =>
          {
            precision: Digit(d),
            smallest_unit: Nanosecond,
            rounding_mode,
            increment: RoundingIncrement::try_new(pow10(9 - d)),
          }
        // `Minute` precision only has a meaning when paired with an explicit
        // `smallestUnit`, which the arm above handles.
        _ => raise RangeError("invalid fractionalDigits precision value")
      }
  }
}

///|
/// Returns `10^exp` for small non-negative exponents.
fn pow10(exp : Int) -> Int {
  let mut result = 1
  for _ in 0.. ResolvedRoundingOptions {
  {
    largest_unit: Auto,
    smallest_unit: options.smallest_unit,
    increment: options.increment,
    rounding_mode: options.rounding_mode,
  }
}

///|
/// `GetDifferenceSettings`: resolves the options for `until` and `since`.
fn ResolvedRoundingOptions::from_diff_settings(
  options : DifferenceSettings,
  operation : DifferenceOperation,
  unit_group : UnitGroup,
  fallback_largest : DateTimeUnit,
  fallback_smallest : DateTimeUnit,
) -> ResolvedRoundingOptions raise TemporalError {
  unit_group.validate_unit(options.largest_unit, Some(Auto)) |> ignore
  let increment = options.increment.unwrap_or(rounding_increment_one)
  let rounding_mode = match operation {
    Since => options.rounding_mode.unwrap_or(Trunc).negate()
    Until => options.rounding_mode.unwrap_or(Trunc)
  }
  let smallest_unit = options.smallest_unit.unwrap_or(fallback_smallest)
  unit_group.validate_unit(options.smallest_unit, None) |> ignore
  let default_largest_unit = smallest_unit.larger(fallback_largest)
  let largest_unit = match options.largest_unit {
    None | Some(Auto) => default_largest_unit
    Some(u) => u
  }
  if largest_unit < smallest_unit {
    raise RangeError("smallestUnit must be smaller than largestUnit")
  }
  if smallest_unit.to_maximum_rounding_increment() is Some(max) {
    increment.validate(max.to_int64(), false)
  }
  { largest_unit, smallest_unit, increment, rounding_mode }
}

///|
/// Resolves the options for `PlainDateTime.round`.
fn ResolvedRoundingOptions::from_datetime_options(
  options : RoundingOptions,
) -> ResolvedRoundingOptions raise TemporalError {
  let increment = options.increment.unwrap_or(rounding_increment_one)
  let rounding_mode = options.rounding_mode.unwrap_or(HalfExpand)
  let smallest_unit = UnitGroup::Time.validate_required_unit(
    options.smallest_unit,
    Some(Day),
  )
  // Rounding to whole days only permits an increment of one, and that
  // increment is inclusive of the dividend.
  let (maximum, inclusive) = if smallest_unit == Day {
    (1L, true)
  } else {
    match smallest_unit.to_maximum_rounding_increment() {
      Some(max) => (max.to_int64(), false)
      None => raise RangeError("smallestUnit must be a valid time unit")
    }
  }
  increment.validate(maximum, inclusive)
  { largest_unit: Auto, smallest_unit, increment, rounding_mode }
}

///|
/// Resolves the options for `PlainTime.round`.
fn ResolvedRoundingOptions::from_time_options(
  options : RoundingOptions,
) -> ResolvedRoundingOptions raise TemporalError {
  let smallest_unit = match options.smallest_unit {
    None => raise RangeError("smallestUnit is required")
    Some(u) => u
  }
  let increment = options.increment.unwrap_or(rounding_increment_one)
  let rounding_mode = options.rounding_mode.unwrap_or(HalfExpand)
  let max = match smallest_unit.to_maximum_rounding_increment() {
    Some(max) => max
    None => raise RangeError("smallestUnit must be a valid time unit")
  }
  increment.validate(max.to_int64(), false)
  { largest_unit: Auto, smallest_unit, increment, rounding_mode }
}

///|
/// Resolves the options for `Instant.round`.
fn ResolvedRoundingOptions::from_instant_options(
  options : RoundingOptions,
) -> ResolvedRoundingOptions raise TemporalError {
  let increment = options.increment.unwrap_or(rounding_increment_one)
  let rounding_mode = options.rounding_mode.unwrap_or(HalfExpand)
  let smallest_unit = UnitGroup::Time.validate_required_unit(
    options.smallest_unit,
    None,
  )
  // An Instant has no calendar, so every time unit is bounded by the day.
  let maximum = match smallest_unit {
    Hour => 24L
    Minute => 24L * 60L
    Second => 24L * 3600L
    Millisecond => MS_PER_DAY
    Microsecond => MS_PER_DAY * 1000L
    Nanosecond => NS_PER_DAY
    _ => raise RangeError("invalid roundTo unit provided")
  }
  increment.validate(maximum, true)
  { largest_unit: Auto, smallest_unit, increment, rounding_mode }
}

///|
/// Returns whether applying these options would leave a value unchanged.
fn ResolvedRoundingOptions::is_noop(self : ResolvedRoundingOptions) -> Bool {
  self.smallest_unit == Nanosecond && self.increment == rounding_increment_one
}