Skip to content

Instantly share code, notes, and snippets.

@shawnthroop
Created March 14, 2019 18:51
Show Gist options
  • Save shawnthroop/bac93e38838cf8dfe0205094d4dca943 to your computer and use it in GitHub Desktop.
Save shawnthroop/bac93e38838cf8dfe0205094d4dca943 to your computer and use it in GitHub Desktop.
A value type representing components separated by "/"
// swift 4.2
import Foundation
/// A value type representing a path comprized of components separated by "/"
public struct Path: Hashable {
public typealias Component = String
private(set) public var rawValue: String = ""
init<S: Sequence>(components: S) where S.Element == Component {
append(components)
}
public mutating func append(component: Component) {
if component.isEmpty {
return
}
switch (rawValue.suffix(1), component.prefix(1)) {
case ("/", "/"), ("~", "~"):
rawValue.removeLast()
case ("/", _), (_, "/"), (_, "~"):
break
case (_, _):
rawValue.append("/")
}
rawValue.append(component)
}
public mutating func append(_ components: Component...) {
append(components)
}
public mutating func append<S: Sequence>(_ components: S) where S.Element == Component {
for component in components {
append(component: component)
}
}
public func byAppending(component: Component) -> Path {
return copy(with: { $0.append(component: component) })
}
public func byAppending(_ components: Component...) -> Path {
return byAppending(components)
}
public func byAppending<S: Sequence>(_ components: S) -> Path where S.Element == Component {
return copy(with: { $0.append(components) })
}
public var lastComponent: Component? {
return lastComponentRange.map { Component(rawValue[$0]) }
}
@discardableResult
public mutating func removeLastComponent() -> Component {
guard let componentRange = lastComponentRange else {
return ""
}
var range = componentRange
if let fsCharRange = rawValue.rangeOfCharacter(before: range.lowerBound), rawValue[fsCharRange] == "/" {
range = fsCharRange.lowerBound..<range.upperBound
}
rawValue.removeSubrange(range)
return Component(rawValue[componentRange])
}
}
extension Path {
public init(url: URL) {
self.init(rawValue: url.path)
}
public init(rawValue: String) {
append(component: rawValue)
}
}
extension Path: ExpressibleByStringLiteral {
public typealias StringLiteralType = String
public init(stringLiteral value: StringLiteralType) {
self.init(rawValue: value)
}
}
extension Path: ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = Component
public init(arrayLiteral elements: ArrayLiteralElement...) {
self.init(components: elements)
}
}
extension Path: CustomStringConvertible, CustomPlaygroundDisplayConvertible {
public var description: String {
return rawValue.description
}
public var playgroundDescription: Any {
return "Path(\"\(rawValue)\")"
}
}
extension Path: Codable {
public func encode(to encoder: Encoder) throws {
try rawValue.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
self.init(rawValue: try String(from: decoder))
}
}
extension Path {
static func + (lhs: Path, rhs: Path) -> Path {
return lhs + rhs.rawValue
}
static func + (lhs: Path, rhs: Path.Component) -> Path {
return lhs.byAppending(component: rhs)
}
static func += (lhs: inout Path, rhs: Path) {
lhs += rhs.rawValue
}
static func += (lhs: inout Path, rhs: Path.Component) {
lhs.append(component: rhs)
}
}
private extension Path {
var lastComponentRange: Range<String.Index>? {
return rawValue.reversed().split(separator: "/", maxSplits: 1).first.map { $0.endIndex.base..<$0.startIndex.base }
}
func copy(with transform: (inout Path) throws -> Void) rethrows -> Path {
var result = self
try transform(&result)
return result
}
}
private extension String {
func rangeOfCharacter(before i: Index) -> Range<Index>? {
return index(i, offsetBy: -1, limitedBy: startIndex).map { $0..<i }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment