diff --git a/CHANGELOG.md b/CHANGELOG.md index c84036b..ad93bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` 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. diff --git a/Sources/DevTesting/Random Value Generation/Date+Random.swift b/Sources/DevTesting/Random Value Generation/Date+Random.swift new file mode 100644 index 0000000..b9ce62d --- /dev/null +++ b/Sources/DevTesting/Random Value Generation/Date+Random.swift @@ -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, + 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, + 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 + ) + ) + } +} diff --git a/Sources/DevTesting/Random Value Generation/RandomValueGenerating.swift b/Sources/DevTesting/Random Value Generation/RandomValueGenerating.swift index 54d4182..dcf456b 100644 --- a/Sources/DevTesting/Random Value Generation/RandomValueGenerating.swift +++ b/Sources/DevTesting/Random Value Generation/RandomValueGenerating.swift @@ -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?)`` 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 { get } } @@ -147,6 +156,32 @@ extension RandomValueGenerating { } + // MARK: - Dates + + public var defaultClosedDateRange: ClosedRange { + 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? = 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 { + return Date.random(in: range, using: &randomNumberGenerator) + } + + // MARK: - Numeric Types /// Returns a random binary floating point of the specified type within the specified range. diff --git a/Tests/DevTestingTests/Random Value Generation/Date+RandomTests.swift b/Tests/DevTestingTests/Random Value Generation/Date+RandomTests.swift new file mode 100644 index 0000000..deda78b --- /dev/null +++ b/Tests/DevTestingTests/Random Value Generation/Date+RandomTests.swift @@ -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) }) + } +} diff --git a/Tests/DevTestingTests/Random Value Generation/RandomValueGeneratingTests.swift b/Tests/DevTestingTests/Random Value Generation/RandomValueGeneratingTests.swift index 614ad1f..c972e90 100644 --- a/Tests/DevTestingTests/Random Value Generation/RandomValueGeneratingTests.swift +++ b/Tests/DevTestingTests/Random Value Generation/RandomValueGeneratingTests.swift @@ -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 {