Random `Clock` duration

I am currently trying to implement a function which gives me a random duration between zero and an upper limit from a generic Clock.

Since Clock.Duration is only constrained to DurationProtocol, I don't think there is a possibility to implement this, other than using Swift.Duration as a requirement. Fortunately Swift.Duration exposes seconds and attoseconds as raw integers by accessing components. These are two Int64s so to get a correct random number from a range, I'd have to combine those two to a Int128. Fortunately, again, this was added in Swift 6.

This was the final product, of what I came up with:

extension Clock where Duration == Swift.Duration {
  
  func randomDuration(upTo maxDuration: Duration) -> Duration {
    let random = Int128.random(in: 0..<(Int128(maxDuration.components.seconds) * 1_000_000_000_000_000_000 + Int128(maxDuration.components.attoseconds)))
    return Duration(secondsComponent: Int64(random / 1_000_000_000_000_000_000), attosecondsComponent: Int64(random % 1_000_000_000_000_000_000))
  }
}

However this seems verbose and is limited to Clocks that have Swift.Duration as their Duration. Someone got a better idea on how to solve this?

The Duration associated type is constrained to conform to DurationProtocol which refines AdditiveArithmetic. That doesn't even have to be integers, though, so I don't know if there's a straightforward way to do something generic over all Clock types due to this. Maybe you could constrain to just SignedNumeric and figure out how to generate random values of that?

I've done something similar, but just on Swift.Duration:

extension Duration {
  public var seconds: Double {
    let (seconds, attoseconds) = self.components
    return Double(seconds) + (Double(attoseconds) * 0.000_000_000__000_000_001)
  }

  public static func random(
    in range: ClosedRange<Duration>,
    using rng: inout some RandomNumberGenerator
  ) -> Duration {
    Duration.seconds(
      Double.random(
        in: range.lowerBound.seconds...range.upperBound.seconds,
        using: &rng
      )
    )
  }

  public static func random(in range: ClosedRange<Duration>) -> Duration {
    var rng = SystemRandomNumberGenerator()
    return random(in: range, using: &rng)
  }
}

Alternatively, you could even just do something like

extension Duration {
  static func randomSeconds(in range: ClosedRange<Double>) -> Self {
    .seconds(Double.random(in: range))
  }
}
4 Likes

Ah okay. The Double approach seems to be a bit less verbose and also does not require iOS 18+ like Int128. Thanks for that.

You don't need either an 128-bit integer or floating-point types:

For a uniform random value, generate a random seconds component between 0...maxDuration.components.seconds and a random attoseconds component between 0..<1_000_000_000_000_000_000. Reject the random value and generate another one until (seconds, attoseconds) < maxDuration.components.

That's also interesting. Comes with the downside, that lower max durations (especially below 1 second) have higher risks of running (almost) indefinitely, right?

extension Clock where Duration == Swift.Duration {
  
  func randomDuration(upTo maxDuration: Duration) -> Duration {
    var randomDuration: Duration
    repeat {
      let randomSeconds = Int64.random(in: 0...maxDuration.components.seconds)
      let randomAttoseconds = Int64.random(in: 0..<1_000_000_000_000_000_000)
      randomDuration = .init(secondsComponent: randomSeconds, attosecondsComponent: randomAttoseconds)
    } while randomDuration >= maxDuration
  }
}

But this approach can have a non-uniform, completely unpredictable, possibly very loin, in-theory not guaranteed bound runtime, no?

In practice it's quite efficient (for the specific bound in question, even using fairly naive rejection sampling, only one sample is needed 97.5% of the time, and two samples suffice 99.95% of the time. So it's "non-uniform" but in a very uninteresting way where it's "always" fast enough not to matter (i.e. it's "possibly" very long, but it's much more likely that a massive asteroid hits the earth while your program is running, which makes the possibility pretty uninteresting.)

3 Likes

For attosecond max durations performance can be pathological—if that's a case you need to handle, anything under 9 seconds is less than Int64.max attoseconds, so you could branch on that condition and use 64-bit arithmetic in the obvious manner to handle it.

1 Like

Ah of course, I missed this fast path. Thank you. This definitely performs better and requires no type juggling and multiplication. I like it.

I just looked at the source code of Duration and Int128 and both of them have underscored low and high (U)Int64s properties with underscored initializers. This would probably be the most succinct option. A bummer that it is underscored.

extension Clock where Duration == Swift.Duration {
  
  func randomDuration(upTo maxDuration: Duration) -> Duration {
    let random = Int128.random(in: 0..<Int128(_low: maxDuration._low, _high: maxDuration._high))
    return .init(_high: random._high, low: random._low)
  }
}

What you're getting at is that Duration's API hasn't been updated since the (U)Int128 has been added to the language. (There are also a handful of other spots among the Duration APIs which some folks have been meaning to get to.)

It would make sense that the ultimate final shape of the APIs here allow you to retrieve and set a duration in 128-bit attoseconds directly.

4 Likes