Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# DevTesting Changelog

## 1.3.0: October 2, 2025

Adds functions for randomly generating dates within a specified range.

- `Date` has been extended with two static functions, both spelled `random(in:using:)`, that
generate a random date in either a closed or half-open range.
- `RandomValueGenerating` has been extended with a new property requirement and two new functions.

- The new property is a `ClosedRange<Date>` spelled `defaultClosedDateRange`. This property
provides a default closed date range to use when generating random dates. A default
implementation is provided.
- The two new functions, both spelled `randomDate(in:)`, generate a random date in either a
closed or half-open range using the aforementioned `Date` extension. The closed range
version’s range parameter is optional. When `nil` is used (the default),
`defaultClosedDateRange` is used.


## 1.2.0: September 24, 2025

This update bumps the minimum supported version of Apple’s OSes to 26.
Expand Down
51 changes: 51 additions & 0 deletions Sources/DevTesting/Random Value Generation/Date+Random.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// Date+Random.swift
// DevTesting
//
// Created by Prachi Gauriar on 10/2/25.
//

import Foundation

extension Date {
/// Returns a random date in the specified range.
///
/// - Parameters:
/// - range: The closed range in which to create a random value. `range` must not be empty.
/// - generator: The random number generator to use when creating the new random value.
/// - Returns: A random date within the bounds of range.
public static func random(
in range: Range<Date>,
using generator: inout some RandomNumberGenerator
) -> Date {
let lowerBound = range.lowerBound.timeIntervalSinceReferenceDate
let upperBound = range.upperBound.timeIntervalSinceReferenceDate
return Date(
timeIntervalSinceReferenceDate: .random(
in: lowerBound ..< upperBound,
using: &generator
)
)
}


/// Returns a random date in the specified range.
///
/// - Parameters:
/// - range: The half-open range in which to create a random value.
/// - generator: The random number generator to use when creating the new random value.
/// - Returns: A random date within the bounds of range.
public static func random(
in range: ClosedRange<Date>,
using generator: inout some RandomNumberGenerator
) -> Date {
let lowerBound = range.lowerBound.timeIntervalSinceReferenceDate
let upperBound = range.upperBound.timeIntervalSinceReferenceDate
return Date(
timeIntervalSinceReferenceDate: .random(
in: lowerBound ... upperBound,
using: &generator
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ public protocol RandomValueGenerating {

/// The seed used by the random number generator.
var randomSeed: UInt64 { get set }

/// The default closed date range when generating random dates.
///
/// This range is used by ``randomDate(in:)-(ClosedRange<Date>?)`` when a `nil` range is specified.
///
/// The default implementation returns a closed range whose lower bound is the 00:00:00 UTC on 1 January 2001, and
/// whose upper bound is 1,577,836,800 seconds (approximately 50 years) later. The specific value of this range may
/// change in the future, though it is unlikely to change before 2040.
var defaultClosedDateRange: ClosedRange<Date> { get }
}


Expand Down Expand Up @@ -147,6 +156,32 @@ extension RandomValueGenerating {
}


// MARK: - Dates

public var defaultClosedDateRange: ClosedRange<Date> {
return Date(timeIntervalSinceReferenceDate: 0) ... Date(timeIntervalSinceReferenceDate: 1_577_836_800)
}


/// Returns a random date in the specified closed range.
///
/// - Parameter range: The closed range in which to create a random value. `range` must not be empty. If `nil`, the
/// default closed date range (``defaultClosedDateRange``) is used.
/// - Returns: A random date within the bounds of range.
public mutating func randomDate(in range: ClosedRange<Date>? = nil) -> Date {
return Date.random(in: range ?? defaultClosedDateRange, using: &randomNumberGenerator)
}


/// Returns a random date in the specified half-open range.
///
/// - Parameter range: The half-open range in which to create a random value. `range` must not be empty.
/// - Returns: A random date within the bounds of range.
public mutating func randomDate(in range: Range<Date>) -> Date {
return Date.random(in: range, using: &randomNumberGenerator)
}


// MARK: - Numeric Types

/// Returns a random binary floating point of the specified type within the specified range.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Date+RandomTests.swift
// DevTesting
//
// Created by Prachi Gauriar on 10/2/25.
//

import DevTesting
import Foundation
import Testing

struct DateRandomTests {
@Test
func halfOpenRange() {
var rng = SystemRandomNumberGenerator()

let min = TimeInterval.random(in: -10_000_000 ..< 0)
let max = TimeInterval.random(in: 0 ... 10_000_000)
let range = Date(timeIntervalSinceReferenceDate: min) ..< Date(timeIntervalSinceReferenceDate: max)

let dates = Set((0 ..< 100).map { _ in Date.random(in: range, using: &rng) })
#expect(dates.count == 100)
#expect(dates.allSatisfy { range.contains($0) })
}


@Test
func closedRange() {
var rng = SystemRandomNumberGenerator()

let min = TimeInterval.random(in: -10_000_000 ..< 0)
let max = TimeInterval.random(in: 0 ... 10_000_000)
let range = Date(timeIntervalSinceReferenceDate: min) ... Date(timeIntervalSinceReferenceDate: max)

let dates = Set((0 ..< 100).map { _ in Date.random(in: range, using: &rng) })
#expect(dates.count == 100)
#expect(dates.allSatisfy { range.contains($0) })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,40 @@ struct RandomValueGeneratingTests {
}


@Test
mutating func randomDateUsesRandomNumberGenerator_halfOpenRange() {
let range = Date(timeIntervalSinceReferenceDate: -100_000) ..< Date(timeIntervalSinceReferenceDate: 100_000)

for _ in iterationRange {
let randomDate = generator.randomDate(in: range)
let expectedData = Date.random(in: range, using: &rng)
#expect(randomDate == expectedData)
}
}


@Test
mutating func randomDateUsesRandomNumberGenerator_closedRange_whenRangeIsNil() {
for _ in iterationRange {
let randomDate = generator.randomDate(in: generator.defaultClosedDateRange)
let expectedData = Date.random(in: generator.defaultClosedDateRange, using: &rng)
#expect(randomDate == expectedData)
}
}


@Test
mutating func randomDateUsesRandomNumberGenerator_closedRange_whenRangeIsSpecified() {
let range = Date(timeIntervalSinceReferenceDate: -100_000) ... Date(timeIntervalSinceReferenceDate: 100_000)

for _ in iterationRange {
let randomDate = generator.randomDate(in: range)
let expectedData = Date.random(in: range, using: &rng)
#expect(randomDate == expectedData)
}
}


@Test
mutating func randomFloatUsesRandomNumberGenerator_halfOpenRange() {
for _ in iterationRange {
Expand Down