Skip to content

Instantly share code, notes, and snippets.

@DonaldHays
Created February 22, 2016 18:28
Show Gist options
  • Save DonaldHays/10b6e3a2c93f315c4d34 to your computer and use it in GitHub Desktop.
Save DonaldHays/10b6e3a2c93f315c4d34 to your computer and use it in GitHub Desktop.
An implementation of a Path type in Swift
//
// 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