Created
February 22, 2016 18:28
-
-
Save DonaldHays/10b6e3a2c93f315c4d34 to your computer and use it in GitHub Desktop.
An implementation of a Path type in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Path.swift | |
// | |
// Created by Donald Hays on 9/17/15. | |
// | |
import Darwin | |
/// `Path` represents the basic components of a file system path. | |
public struct Path: Hashable, CustomStringConvertible, StringLiteralConvertible, ArrayLiteralConvertible { | |
// MARK: - | |
// MARK: Public Static Properties | |
/// The separator String used to mark partitions between components in a | |
/// path. | |
public static let separator: String = { | |
// For Win32, will need to return `\`. | |
return "/" | |
}() | |
// MARK: - | |
// MARK: Public Properties | |
/// `/` for an absolute path, nil for a relative path. | |
public var root: String? | |
/// An array of directories in the path, or `nil` if there are none. | |
public var directories: [String]? | |
/// The name of the file, without its extension, or nil for a directory | |
/// path. | |
public var fileName: String? | |
/// The extension of the file, without a period, or nil for a directory path | |
/// or a file path without an extension. | |
public var fileExtension: String? | |
/// `true` if the path is absolute, `false` otherwise. | |
public var isAbsolute: Bool { | |
return root != nil | |
} | |
/// A String representation of the receiver. | |
public var pathString: String { | |
let root = self.root ?? "" | |
let directories: String | |
if let directoryArray = self.directories { | |
directories = directoryArray.joinWithSeparator(Path.separator) + "/" | |
} else { | |
directories = "" | |
} | |
let fileName = self.fileName ?? "" | |
let dotFileExtension: String | |
if let fileExtension = fileExtension { | |
dotFileExtension = "." + fileExtension | |
} else { | |
dotFileExtension = "" | |
} | |
return root + directories + fileName + dotFileExtension | |
} | |
/// A normalized version of the receiver. See `normalize` for details. | |
public var normalized: Path { | |
var newComponents = self | |
newComponents.normalize() | |
return newComponents | |
} | |
public var hashValue: Int { | |
return normalized.pathString.hashValue | |
} | |
public var description: String { | |
return pathString | |
} | |
// MARK: - | |
// MARK: Lifecycle | |
/// Creates a `Path` from the components of a specified path string. | |
public init(_ path: String) { | |
let characters = path.characters | |
var index = characters.startIndex | |
// If the string is empty, just bail now | |
if index == characters.endIndex { | |
return | |
} | |
// If the string begins with the separator character, assign it as root. | |
// Will probably need an `#if os(Win32)` for this part later, since the | |
// root on Windows is more like `C:\`, and not its path separator `\`. | |
if String(characters[index]) == Path.separator { | |
root = Path.separator | |
index = index.successor() | |
} | |
// After this loop ends, directories will contain all of the directories | |
// in the path, while currentComponent will contain the file if there is | |
// a file component. During the loop, currentComponent will also store | |
// directories until they're put in the directories list. | |
var directories = [String]() | |
var currentComponent: String? = nil | |
while index != characters.endIndex { | |
let character = String(characters[index]) | |
index = index.successor() | |
// If we encounter the separator, then currentComponent is a | |
// directory and should be appended to the directories list. | |
if character == Path.separator { | |
// If the path separator appears twice in a row "//", this will | |
// result in currentComponent being nil, which will cause it to | |
// not be appended to the directory list, which is desired | |
// behavior. | |
if let directory = currentComponent { | |
directories.append(directory) | |
currentComponent = nil | |
} | |
} else { | |
// If currentComponent exists, append character to it, | |
// otherwise create it with a default value of character. | |
if let component = currentComponent { | |
currentComponent = component + character | |
} else { | |
currentComponent = character | |
} | |
} | |
} | |
// If currentComponent is equal to either "." or "..", it's related to | |
// directories, not file. | |
if let directive = currentComponent where directive == "." || directive == ".." { | |
directories.append(directive) | |
currentComponent = nil | |
} | |
// Only assign to the directories property if our list of directories | |
// is not zero-length, because we only want it to be non-nil if the path | |
// String actually defined directories. | |
if directories.count > 0 { | |
self.directories = directories | |
} | |
// If currentComponent is not nil, it specifies the file. | |
if let file = currentComponent { | |
let characters = file.characters | |
// The final `.` character in the string represents the separator | |
// between file name and extension. | |
let lastIndexOfPeriod: String.CharacterView.Index? = characters.indices.reduce(nil) { | |
return characters[$1] == "." ? $1 : $0 | |
} | |
// If we have a separator index, separate the file name and | |
// extension parts. | |
if let extensionSeparatorIndex = lastIndexOfPeriod { | |
// If the string doesn't begin with the separator, then the | |
// file name is the substring leading up to the separator. | |
if extensionSeparatorIndex != file.startIndex { | |
self.fileName = file[path.startIndex ..< extensionSeparatorIndex] | |
} | |
// If the string doesn't end with the separator, then the file | |
// extension is the substring following the separator. | |
let firstExtensionCharacterIndex = extensionSeparatorIndex.successor() | |
if firstExtensionCharacterIndex != file.endIndex { | |
self.fileExtension = file[firstExtensionCharacterIndex ..< file.endIndex] | |
} | |
} else { | |
// If there's no separator index, the entire string is the | |
// file name. | |
self.fileName = file | |
} | |
} | |
} | |
/// Creates a `Path` by joining specified strings together with the system | |
/// separator and then interpreting the components from the joined string. | |
public init(_ paths: [String]) { | |
self.init(paths.joinWithSeparator(Path.separator)) | |
} | |
/// Creates a `Path` by joining specified strings together with the system | |
/// separator and then interpreting the components from the joined string. | |
public init(_ paths: String...) { | |
self.init(paths.joinWithSeparator(Path.separator)) | |
} | |
public init(stringLiteral value: StringLiteralType) { | |
self.init(value) | |
} | |
public init(extendedGraphemeClusterLiteral value: StringLiteralType) { | |
self.init(value) | |
} | |
public init(unicodeScalarLiteral value: Character) { | |
self.init(String(value)) | |
} | |
public init(arrayLiteral elements: String...) { | |
self.init(elements) | |
} | |
// MARK: - | |
// MARK: Public API | |
/// Normalizes the directories of the receiver, removing instances of "." | |
/// and popping directories off the list when encountering "..". | |
public mutating func normalize() { | |
if let directories = directories { | |
var newDirectories = [String]() | |
for directory in directories { | |
if directory == ".." { | |
newDirectories.popLast() | |
} else if directory != "." { | |
newDirectories.append(directory) | |
} | |
} | |
self.directories = newDirectories | |
} | |
} | |
} | |
public func == (lhs: Path, rhs: Path) -> Bool { | |
return lhs.normalized.pathString == rhs.normalized.pathString | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment