Skip to content

Commit 54d5ef1

Browse files
Added IndexRangeExpression to determine ranges properly
1 parent 2648457 commit 54d5ef1

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed

Sources/CodableDatastore/Datastore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ extension Datastore {
5454
return nil
5555
}
5656

57-
public func load(_ range: any RangeExpression<IdentifierType>) async throws -> AsyncStream<CodedType> {
57+
public func load(_ range: any IndexRangeExpression<IdentifierType>) async throws -> AsyncStream<CodedType> {
5858
return AsyncStream<CodedType> { continuation in
5959
continuation.finish()
6060
}
@@ -77,7 +77,7 @@ extension Datastore where AccessMode == ReadWrite {
7777

7878
extension Datastore {
7979
public func load<IndexedValue>(
80-
_ range: any RangeExpression<IdentifierType>,
80+
_ range: any IndexRangeExpression<IdentifierType>,
8181
from keypath: KeyPath<CodedType, Indexed<IndexedValue>>
8282
) async throws -> AsyncStream<CodedType> {
8383
return AsyncStream<CodedType> { continuation in
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//
2+
// IndexRangeExpression.swift
3+
// CodableDatastore
4+
//
5+
// Created by Dimitri Bouniol on 2023-06-05.
6+
// Copyright © 2023 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
/// A type of bound found on either end of a range.
10+
public enum RangeBoundExpression<Bound: Comparable>: Equatable {
11+
/// The bound reaches to the extent of the set the range is within.
12+
case extent
13+
14+
/// The bound reaches up to, but not including the given value.
15+
case excluding(Bound)
16+
17+
/// The bound reaches through the given value.
18+
case including(Bound)
19+
}
20+
21+
extension RangeBoundExpression: Sendable where Bound: Sendable { }
22+
23+
/// A type that can represent a range within an index.
24+
public protocol IndexRangeExpression<Bound> {
25+
associatedtype Bound: Comparable
26+
27+
/// The definition of the lower bound of the range.
28+
var lowerBoundExpression: RangeBoundExpression<Bound> { get }
29+
30+
/// The definition of the upper bound of the range.
31+
var upperBoundExpression: RangeBoundExpression<Bound> { get }
32+
}
33+
34+
/// The position relative to a range.
35+
public enum RangePosition: Equatable {
36+
/// A value appears before the range.
37+
case before
38+
39+
/// A value appears within the range.
40+
case within
41+
42+
/// A value appears after the range.
43+
case after
44+
}
45+
46+
extension IndexRangeExpression {
47+
/// The position of a value relative to the range.
48+
func position(of value: Bound) -> RangePosition {
49+
switch lowerBoundExpression {
50+
case .extent: break
51+
case .excluding(let lowerBound):
52+
if value <= lowerBound { return .before }
53+
case .including(let lowerBound):
54+
if value < lowerBound { return .before }
55+
}
56+
57+
switch upperBoundExpression {
58+
case .extent: break
59+
case .excluding(let upperBound):
60+
if value >= upperBound { return .after }
61+
case .including(let upperBound):
62+
if value > upperBound { return .after }
63+
}
64+
65+
return .within
66+
}
67+
}
68+
69+
extension Range: IndexRangeExpression {
70+
@inlinable
71+
public var lowerBoundExpression: RangeBoundExpression<Bound> { .including(lowerBound) }
72+
@inlinable
73+
public var upperBoundExpression: RangeBoundExpression<Bound> { .excluding(upperBound) }
74+
}
75+
76+
extension ClosedRange: IndexRangeExpression {
77+
@inlinable
78+
public var lowerBoundExpression: RangeBoundExpression<Bound> { .including(lowerBound) }
79+
@inlinable
80+
public var upperBoundExpression: RangeBoundExpression<Bound> { .including(upperBound) }
81+
}
82+
83+
extension PartialRangeUpTo: IndexRangeExpression {
84+
@inlinable
85+
public var lowerBoundExpression: RangeBoundExpression<Bound> { .extent }
86+
@inlinable
87+
public var upperBoundExpression: RangeBoundExpression<Bound> { .excluding(upperBound) }
88+
}
89+
90+
extension PartialRangeThrough: IndexRangeExpression {
91+
@inlinable
92+
public var lowerBoundExpression: RangeBoundExpression<Bound> { .extent }
93+
@inlinable
94+
public var upperBoundExpression: RangeBoundExpression<Bound> { .including(upperBound) }
95+
}
96+
97+
extension PartialRangeFrom: IndexRangeExpression {
98+
@inlinable
99+
public var lowerBoundExpression: RangeBoundExpression<Bound> { .including(lowerBound) }
100+
@inlinable
101+
public var upperBoundExpression: RangeBoundExpression<Bound> { .extent }
102+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// IndexRangeExpressionTests.swift
3+
// CodableDatastore
4+
//
5+
// Created by Dimitri Bouniol on 2023-06-05.
6+
// Copyright © 2023 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import XCTest
10+
@testable import CodableDatastore
11+
12+
final class IndexRangeExpressionTests: XCTestCase {
13+
func testLowerBound() throws {
14+
func lowerBoundExpression<B>(_ range: any IndexRangeExpression<B>) -> RangeBoundExpression<B> {
15+
range.lowerBoundExpression
16+
}
17+
18+
XCTAssertEqual(lowerBoundExpression(1..<2), .including(1))
19+
XCTAssertEqual(lowerBoundExpression(1...2), .including(1))
20+
XCTAssertEqual(lowerBoundExpression(..<2), .extent)
21+
XCTAssertEqual(lowerBoundExpression(...2), .extent)
22+
XCTAssertEqual(lowerBoundExpression(1...), .including(1))
23+
}
24+
25+
func testUpperBound() throws {
26+
func upperBoundExpression<B>(_ range: any IndexRangeExpression<B>) -> RangeBoundExpression<B> {
27+
range.upperBoundExpression
28+
}
29+
30+
XCTAssertEqual(upperBoundExpression(1..<2), .excluding(2))
31+
XCTAssertEqual(upperBoundExpression(1...2), .including(2))
32+
XCTAssertEqual(upperBoundExpression(..<2), .excluding(2))
33+
XCTAssertEqual(upperBoundExpression(...2), .including(2))
34+
XCTAssertEqual(upperBoundExpression(1...), .extent)
35+
}
36+
37+
func testPosition() throws {
38+
func position<B>(of value: B, in range: some IndexRangeExpression<B>) -> RangePosition {
39+
range.position(of: value)
40+
}
41+
42+
XCTAssertEqual(position(of: 0, in: 1..<3), .before)
43+
XCTAssertEqual(position(of: 0, in: 1...3), .before)
44+
XCTAssertEqual(position(of: 0, in: ..<3), .within)
45+
XCTAssertEqual(position(of: 0, in: ...3), .within)
46+
XCTAssertEqual(position(of: 0, in: 1...), .before)
47+
48+
XCTAssertEqual(position(of: 1, in: 1..<3), .within)
49+
XCTAssertEqual(position(of: 1, in: 1...3), .within)
50+
XCTAssertEqual(position(of: 1, in: ..<3), .within)
51+
XCTAssertEqual(position(of: 1, in: ...3), .within)
52+
XCTAssertEqual(position(of: 1, in: 1...), .within)
53+
54+
XCTAssertEqual(position(of: 2, in: 1..<3), .within)
55+
XCTAssertEqual(position(of: 2, in: 1...3), .within)
56+
XCTAssertEqual(position(of: 2, in: ..<3), .within)
57+
XCTAssertEqual(position(of: 2, in: ...3), .within)
58+
XCTAssertEqual(position(of: 2, in: 1...), .within)
59+
60+
XCTAssertEqual(position(of: 3, in: 1..<3), .after)
61+
XCTAssertEqual(position(of: 3, in: 1...3), .within)
62+
XCTAssertEqual(position(of: 3, in: ..<3), .after)
63+
XCTAssertEqual(position(of: 3, in: ...3), .within)
64+
XCTAssertEqual(position(of: 3, in: 1...), .within)
65+
66+
XCTAssertEqual(position(of: 4, in: 1..<3), .after)
67+
XCTAssertEqual(position(of: 4, in: 1...3), .after)
68+
XCTAssertEqual(position(of: 4, in: ..<3), .after)
69+
XCTAssertEqual(position(of: 4, in: ...3), .after)
70+
XCTAssertEqual(position(of: 4, in: 1...), .within)
71+
}
72+
}

0 commit comments

Comments
 (0)