Apologies for the late reply. I am really busy as always.
With what I have in mind, negative offsets won't pose a security risk. A negative offset is anchored to the end of the collection and a positive offset to the start of it. Meaning: They are distinct types. Offset is exactly like a discriminated union (enum) with discriminator being the sign. Only a limited subset of integer arithmetic makes sense on an offset and such operations can't flip its direction/anchor.
A signed Offset is expressible by an integer, but it is not Int. I think the best description would be some code. Here is my prototype Offset:
public struct Offset: ExpressibleByIntegerLiteral,
LosslessStringConvertible,
Hashable, Comparable, Strideable
{
static var first: Offset { return Offset(0) }
static var last: Offset { return Offset(-1) }
var isFromStart: Bool { value >= 0 }
var isFromEnd: Bool { value < 0 }
fileprivate init(_ value: Int) { self.value = value }
fileprivate var value: Int
}
Along with these operators for convenience:
public func +(lhs: Offset, rhs: Int) -> Offset { lhs.advanced(by: rhs) }
public func -(lhs: Offset, rhs: Int) -> Offset { lhs.advanced(by: -rhs) }
public func -(lhs: Offset, rhs: Offset) -> Int { lhs.distance(to: rhs) }
I choose to only add it to RandomAccessCollection to prevent performance surprises for novice programmers. Here is the core API (using @scanon's trick):
public extension RandomAccessCollection {
func baseIndex(of offset: Offset) -> Index { offset.isFromStart ? self.startIndex : self.endIndex }
func index(of offset: Offset) -> Index { self.index(baseIndex(of: offset), offsetBy: offset.value) }
func callAsFunction(_ offset: Offset) -> Element { return self[index(of: offset)] }
func forwardOffset(of index: Index) -> Offset { Offset(distance(from: startIndex, to: index)) }
func backwardOffset(of index: Index) -> Offset {
precondition(index < endIndex, "Backward (negative) offset for `endIndex` is undefined.")
return Offset(distance(from: endIndex, to: index))
}
func oppositeOffset(_ offset: Offset) -> Offset {
precondition(offset.value >= -count && offset.value < count, "Offset out of range")
return offset.isFromStart ? Offset(offset.value - count) : Offset(offset.value + count)
}
}
Using the above API, we can also add some helpers to Offset itself:
public extension Offset {
func reverse<C>(_ collection: C) -> Offset where C: RandomAccessCollection { collection.oppositeOffset(self) }
func index<C>(_ collection: C) -> C.Index where C: RandomAccessCollection { collection.index(of: self) }
}
We can also go fancy and define BoundOffset<C> and BoundIndex<C> with corresponding collection.bind(offset) and collection.bind(index) to create them and hide the base collection in the above API and have offset.index and index.offset properties. There is quite a bit of design space to explore around these things.
Here is the above code put together
You can paste this in a playground and try it out.
Warning: Not tested beyond a couple of sample statements at the end. I just quickly typed it to capture the idea.
public struct Offset {
static var first: Offset { return Offset(0) }
static var last: Offset { return Offset(-1) }
fileprivate var value: Int
init(_ value: Int) { self.value = value }
var isFromStart: Bool { value >= 0 }
var isFromEnd: Bool { value < 0 }
}
extension Offset: ExpressibleByIntegerLiteral { public init(integerLiteral value: Int) { self.init(value) } }
extension Offset: LosslessStringConvertible {
public init?(_ description: String) {
guard let value = Int(description) else { return nil }
self.init(value)
}
public var description: String { value.description }
}
extension Offset: Hashable {}
extension Offset: Comparable { public static func < (lhs: Offset, rhs: Offset) -> Bool {lhs.value < rhs.value} }
extension Offset: Strideable {
public func advanced(by n: Int) -> Offset {
let result = Offset(value + n)
precondition(isFromEnd == result.isFromEnd, "Offset moved past limit.")
return result
}
public func distance(to other: Offset) -> Int {
precondition(isFromEnd == other.isFromEnd, "Distance is undefined for offsets on opposite ends.")
return value - other.value
}
}
public func +(lhs: Offset, rhs: Int) -> Offset { lhs.advanced(by: rhs) }
public func -(lhs: Offset, rhs: Int) -> Offset { lhs.advanced(by: -rhs) }
public func -(lhs: Offset, rhs: Offset) -> Int { lhs.distance(to: rhs) }
public extension RandomAccessCollection {
func baseIndex(of offset: Offset) -> Index { offset.isFromStart ? self.startIndex : self.endIndex }
func index(of offset: Offset) -> Index { self.index(baseIndex(of: offset), offsetBy: offset.value) }
func callAsFunction(_ offset: Offset) -> Element { return self[index(of: offset)] }
func forwardOffset(of index: Index) -> Offset { Offset(distance(from: startIndex, to: index)) }
func backwardOffset(of index: Index) -> Offset {
precondition(index < endIndex, "Backward (negative) offset for `endIndex` is undefined.")
return Offset(distance(from: endIndex, to: index))
}
func oppositeOffset(_ offset: Offset) -> Offset {
precondition(offset.value >= -count && offset.value < count, "Offset out of range")
return offset.isFromStart ? Offset(offset.value - count) : Offset(offset.value + count)
}
}
public extension Offset {
func reverse<C>(_ collection: C) -> Offset where C: RandomAccessCollection { collection.oppositeOffset(self) }
func index<C>(_ collection: C) -> C.Index where C: RandomAccessCollection { collection.index(of: self) }
}
public struct BoundIndex<C: Collection> {
let parentCollection: C
private var value: C.Index
fileprivate init(index: C.Index, parent: C) {
parentCollection = parent
value = index
}
}
public extension Collection {
func bind(_ index: Index) -> BoundIndex<Self> { BoundIndex(index: index, parent: self) }
}
var a = ["a","b","c","d","e","f","g","h"]
print(a(.first), a(0), a(2), a(-1), a(.last))
let o = Offset.first + 5
print (o - .first)
// print(a(.first-2)) // Precondition failed: Offset moved past limit.
// print(a(.last+3)) // Precondition failed: Offset moved past limit.
// print(Offset.last-Offset.first) // Precondition failed: Distance is undefined for offsets on opposite ends.
The corresponding offset range types can also be smarter than Range<Int> about what they represent.