Files

800 lines
31 KiB
Swift
Executable File

//
// Sprinter.swift
// Sprinter
//
// Version 0.2.1
//
// Created by Nick Lockwood on 20/11/2017.
// Copyright © 2017 Nick Lockwood. All rights reserved.
//
// Distributed under the permissive MIT license
// Get the latest version from here:
//
// https://github.com/nicklockwood/Sprinter
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
/// An opaque type used to wrap a parsed format string
/// Can be used to efficiently perform operations such as formatting and validation
public struct FormatString {
enum Flag: Unicode.Scalar {
case groupThousands = "'"
case leftJustified = "-"
case alwaysShowSign = "+"
case padToSignedWidth = " "
case alternativeForm = "#"
case leadingZeros = "0"
fileprivate init?(_ input: inout String.UnicodeScalarView.SubSequence) {
guard let flag = input.first.flatMap({ Flag(rawValue: $0) }) else {
return nil
}
input.removeFirst()
self = flag
}
}
enum FieldWidth: CustomStringConvertible {
case parameter(UInt)
case constant(UInt)
var index: UInt? {
switch self {
case let .parameter(index): return index
case .constant: return nil
}
}
var description: String {
switch self {
case .parameter:
return "*"
case let .constant(width):
return "\(width)"
}
}
fileprivate init?(_ input: inout String.UnicodeScalarView.SubSequence, _: inout UInt) throws {
if input.readCharacter("*") {
if let index = try input.readPositiveInt() {
guard input.readCharacter("$") else {
if let first = input.first {
throw FormatString.Error.unexpectedToken(Character(first))
}
throw FormatString.Error.unexpectedEndOfString
}
self = .parameter(index)
return
}
self = .parameter(index)
index += 1
return
}
if let index = try input.readUInt() {
self = .constant(index)
return
}
return nil
}
}
enum LengthModifier: String {
case char = "hh"
case short = "h"
case long = "l"
case longLong = "ll"
case intmax = "j"
case size = "z"
case ptrdiff = "t"
case longDouble = "L"
/// Apple-specific
case quadword = "q" // equivalent to ll
fileprivate init?(_ input: inout String.UnicodeScalarView.SubSequence) {
guard let first = input.first else {
return nil
}
switch first {
case "h":
input.removeFirst()
if input.readCharacter("h") {
self = .char
return
}
self = .short
case "l":
input.removeFirst()
if input.readCharacter("l") {
self = .longLong
return
}
self = .long
default:
if let modifier = LengthModifier(rawValue: String(Character(first))) {
input.removeFirst()
self = modifier
return
}
return nil
}
}
}
enum ConversionSpecifier: Unicode.Scalar {
case decimal = "d"
case int = "i" // equivalent to d
case octal = "o"
case unsigned = "u"
case hex = "x"
case uppercaseHex = "X"
case float = "f"
case uppercaseFloat = "F" // equivalent to f
case exponential = "e"
case uppercaseExponential = "E"
case variablePrecisionFloat = "g"
case uppercasevariablePrecisionFloat = "G"
case hexFloat = "a"
case uppercaseHexFloat = "A"
case char = "c"
case string = "s"
case pointer = "p"
case bytesWritten = "n"
case wideChar = "C" // equivalent to lc
case wideString = "S" // equivalent to ls
case percentChar = "%"
/// Apple-specific
case uppercaseDecimal = "D" // equivalent to d
case uppercaseOctal = "O" // equivalent to o
case uppercaseUnsigned = "U" // equivalent to u
case object = "@"
fileprivate init(_ input: inout String.UnicodeScalarView.SubSequence) throws {
guard let first = input.first else {
throw Error.unexpectedEndOfString
}
guard let specifier = ConversionSpecifier(rawValue: first) else {
throw Error.unexpectedToken(Character(first))
}
input.removeFirst()
self = specifier
}
}
struct Placeholder {
private(set) var index: UInt = 0
private(set) var flags = [Flag]()
private(set) var fieldWidth: FieldWidth?
private(set) var precision: FieldWidth?
private(set) var modifier: LengthModifier?
let specifier: ConversionSpecifier
private var formatter: NumberFormatter?
private var formatString = ""
func getSwiftType() throws -> Any.Type? {
switch specifier {
case .decimal,
.uppercaseDecimal,
.int,
.octal,
.uppercaseOctal,
.hex,
.uppercaseHex:
guard let modifier = modifier else { return Int.self } // spec says Int32
switch modifier {
case .char: return CChar.self
case .short: return CShort.self
case .long: return CLong.self
case .longLong,
.quadword: return CLongLong.self
case .intmax: return intmax_t.self
case .size: return size_t.self
case .ptrdiff: return ptrdiff_t.self
case .longDouble: throw Error.modifierMismatch(modifier.rawValue, Character(specifier.rawValue))
}
case .unsigned,
.uppercaseUnsigned:
guard let modifier = modifier else { return UInt.self } // spec says UInt32
switch modifier {
case .char: return CUnsignedChar.self
case .short: return CUnsignedShort.self
case .long: return CUnsignedLong.self
case .longLong,
.quadword: return CUnsignedLongLong.self
case .intmax: return uintmax_t.self
case .size: return UInt.self
case .ptrdiff: return UInt.self
case .longDouble: throw Error.modifierMismatch(modifier.rawValue, Character(specifier.rawValue))
}
case .float,
.uppercaseFloat,
.variablePrecisionFloat,
.uppercasevariablePrecisionFloat,
.exponential,
.uppercaseExponential,
.hexFloat,
.uppercaseHexFloat:
guard let modifier = modifier else { return Double.self }
switch modifier {
case .longDouble:
#if os(macOS)
return Float80.self
#else
return Double.self
#endif
default: throw Error.modifierMismatch(modifier.rawValue, Character(specifier.rawValue))
}
case .char:
guard let modifier = modifier else { return Character.self } // spec says CChar
switch modifier {
case .long: return Character.self // IEEE says wint_t, Apple says unichar
default: throw Error.modifierMismatch(modifier.rawValue, Character(specifier.rawValue))
}
case .wideChar:
try modifier.map { throw Error.modifierMismatch($0.rawValue, Character(specifier.rawValue)) }
return Character.self // IEEE says wint_t, Apple says unichar
case .string:
guard let modifier = modifier else { return String.self } // spec says UnsafePointer<CChar>
switch modifier {
case .long: return String.self // IEEE says wchar_t*, Apple says Unichar*
default: throw Error.modifierMismatch(modifier.rawValue, Character(specifier.rawValue))
}
case .wideString:
try modifier.map { throw Error.modifierMismatch($0.rawValue, Character(specifier.rawValue)) }
return String.self // IEEE says wchar_t*, Apple says Unichar*
case .pointer:
try modifier.map { throw Error.modifierMismatch($0.rawValue, Character(specifier.rawValue)) }
return AnyObject.self
case .bytesWritten:
throw Error.unsupportedSpecifier(Character(specifier.rawValue))
case .percentChar:
return nil
case .object:
try modifier.map { throw Error.modifierMismatch($0.rawValue, Character(specifier.rawValue)) }
return Any.self
}
}
func buildFormatString() -> String {
var format = "%"
format += flags.map { "\($0.rawValue)" }.joined()
fieldWidth.map { format += "\($0)" }
precision.map { format += ".\($0)" }
if let modifier = modifier {
format += "\(modifier.rawValue)\(specifier.rawValue)"
} else {
switch specifier {
case .int,
.decimal:
format += "zd"
case .uppercaseDecimal:
format += "zD"
case .unsigned:
format += "tu"
case .uppercaseUnsigned:
format += "tU"
case .hex:
format += "tx"
case .uppercaseHex:
format += "tX"
case .octal:
format += "to"
case .uppercaseOctal:
format += "tO"
default:
format += "\(specifier.rawValue)"
}
}
return format
}
func buildFormatter(fieldWidth w: Int?, precision p: Int?, locale: Locale?) -> NumberFormatter? {
let fieldWidth: Int?
if let width = self.fieldWidth {
switch width {
case .parameter where w == nil:
return nil // Can't build formatter
case .parameter:
fieldWidth = w
case let .constant(value):
fieldWidth = Int(value)
}
} else {
fieldWidth = nil
}
let precision: Int?
if let width = self.precision {
switch width {
case .parameter where p == nil:
return nil // Can't build formatter
case .parameter:
precision = p
case let .constant(value):
precision = Int(value)
}
} else {
precision = nil
}
let formatter: NumberFormatter
switch specifier {
case .decimal,
.uppercaseDecimal,
.int,
.unsigned,
.uppercaseUnsigned:
if !flags.contains(.groupThousands) {
return nil // Prefer `String(format:)` for performance
}
formatter = NumberFormatter()
formatter.allowsFloats = false
formatter.minimumIntegerDigits = precision ?? 1
case .float,
.uppercaseFloat:
if !flags.contains(.groupThousands) {
return nil // Prefer `String(format:)` for performance
}
formatter = NumberFormatter()
formatter.minimumIntegerDigits = 1
formatter.minimumFractionDigits = precision ?? 6
formatter.maximumFractionDigits = precision ?? 6
formatter.alwaysShowsDecimalSeparator = flags.contains(.alternativeForm)
case .variablePrecisionFloat,
.uppercasevariablePrecisionFloat:
if !flags.contains(.groupThousands) {
return nil // Prefer `String(format:)` for performance
}
formatter = NumberFormatter()
formatter.minimumIntegerDigits = 1
formatter.minimumFractionDigits = 0
if flags.contains(.alternativeForm) {
formatter.minimumSignificantDigits = precision ?? 6
} else {
formatter.maximumSignificantDigits = precision ?? 6
}
case .octal,
.uppercaseOctal,
.hex,
.uppercaseHex,
.exponential,
.uppercaseExponential,
.hexFloat,
.uppercaseHexFloat,
.char,
.string,
.wideChar,
.wideString,
.pointer,
.bytesWritten,
.percentChar,
.object:
return nil // Not handled by NumberFormatter
}
formatter.locale = locale
let infinity = String(format: "%\(specifier.rawValue)", locale: locale, Double.infinity)
formatter.positiveInfinitySymbol = infinity
formatter.negativeInfinitySymbol = "-\(infinity)"
formatter.formatWidth = fieldWidth ?? 0
formatter.numberStyle = .decimal
if flags.contains(.leftJustified) {
formatter.paddingPosition = .afterSuffix
} else if flags.contains(.leadingZeros) {
formatter.paddingCharacter = formatter.zeroSymbol ?? "0"
}
if flags.contains(.alwaysShowSign) {
formatter.positivePrefix = formatter.plusSign
} else if flags.contains(.padToSignedWidth) {
formatter.positivePrefix = " "
}
return formatter
}
func print(_ value: Any, _ fieldWidth: Int?, _ precision: Int?, locale: Locale?) -> String {
if let formatter = formatter ?? buildFormatter(
fieldWidth: fieldWidth,
precision: precision,
locale: locale
), let number = value as? NSNumber {
// Seems like this can never be nil, but better to be safe
return formatter.string(from: number) ?? ""
}
switch specifier {
case .decimal,
.uppercaseDecimal,
.int,
.octal,
.uppercaseOctal,
.hex,
.uppercaseHex,
.unsigned,
.uppercaseUnsigned:
let value = Int(truncating: value as! NSNumber)
if let fieldWidth = fieldWidth, let precision = precision {
return String(format: formatString, locale: locale, fieldWidth, precision, value)
} else if let width = fieldWidth ?? precision {
return String(format: formatString, locale: locale, width, value)
}
return String(format: formatString, locale: locale, value)
case .float,
.uppercaseFloat,
.variablePrecisionFloat,
.uppercasevariablePrecisionFloat,
.exponential,
.uppercaseExponential,
.hexFloat,
.uppercaseHexFloat:
#if os(macOS)
if let value = value as? Float80 {
return "\(value)" // TODO: respect formatting options
}
#endif
if let fieldWidth = fieldWidth, let precision = precision {
return String(format: formatString, locale: locale, fieldWidth, precision, value as! Double)
} else if let width = fieldWidth ?? precision {
return String(format: formatString, locale: locale, width, value as! Double)
}
return String(format: formatString, locale: locale, value as! Double)
case .pointer:
return String(format: formatString, locale: locale, (value as AnyObject).hash)
case .bytesWritten,
.percentChar, // Shouldn't actually happen
.object,
.string,
.wideString,
.char,
.wideChar:
return "\(value)"
}
}
fileprivate init?(
_ input: inout String.UnicodeScalarView.SubSequence,
_ index: inout UInt,
locale: Locale?
) throws {
guard input.readCharacter("%") else { return nil }
guard let first = input.first else {
throw Error.unexpectedEndOfString
}
// index, flag, field width, precision
if let int = try input.readPositiveInt() {
if input.readCharacter("$") {
self.index = int
flags = try input.readFlags()
fieldWidth = try FieldWidth(&input, &index)
} else {
fieldWidth = .constant(int)
}
} else {
flags = try input.readFlags()
fieldWidth = try FieldWidth(&input, &index)
}
if input.readCharacter(".") {
precision = try FieldWidth(&input, &index) ?? .constant(0)
}
if self.index == 0, first != "%" {
self.index = index
index += 1
}
// modifier modifier
modifier = LengthModifier(&input)
// conversion specifier
specifier = try ConversionSpecifier(&input)
if specifier == .percentChar, first != "%" {
throw Error.modifierMismatch("\(first)", "%")
}
// set up formatter
formatter = buildFormatter(fieldWidth: nil, precision: nil, locale: locale)
if formatter == nil {
formatString = buildFormatString()
}
}
}
enum Token {
case string(String)
case placeholder(Placeholder)
}
public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, Equatable {
case unexpectedEndOfString
case unexpectedToken(Character)
case duplicateFlag(Character)
case unsupportedFlag(Character)
case unsupportedSpecifier(Character)
case modifierMismatch(String, Character)
case typeMismatch(Int, Any.Type, Any.Type)
case argumentMismatch(Int, Any.Type, Any.Type)
case missingArgument(Int)
public var errorDescription: String? {
return description
}
public var description: String {
switch self {
case .unexpectedEndOfString:
return "Format string ended unexpectedly"
case let .unexpectedToken(char):
return "Unexpected character '\(char)' in format string"
case let .duplicateFlag(char):
return "Format string contains duplicate flag '\(char)'"
case let .unsupportedFlag(char):
return "Formatting flag '\(char)' is not currently supported"
case let .unsupportedSpecifier(char):
return "The format specifier '\(char)' is not currently supported"
case let .modifierMismatch(modifier, specifier):
return "Length modifier '\(modifier)' cannot be used with format specifier '\(specifier)'"
case let .typeMismatch(index, type1, type2):
return "Type mismatch for placeholders with index #\(index): '\(type1)' vs '\(type2)'"
case let .argumentMismatch(index, type1, type2):
return "Type mismatch for argument #\(index): '\(type1)' vs '\(type2)'"
case let .missingArgument(index):
return "Missing argument #\(index)"
}
}
public static func == (lhs: Error, rhs: Error) -> Bool {
switch (lhs, rhs) {
case (.unexpectedEndOfString, .unexpectedEndOfString):
return true
case let (.unexpectedToken(lhs), .unexpectedToken(rhs)),
let (.duplicateFlag(lhs), .duplicateFlag(rhs)),
let (.unsupportedFlag(lhs), .unsupportedFlag(rhs)),
let (.unsupportedSpecifier(lhs), .unsupportedSpecifier(rhs)):
return lhs == rhs
case let (.modifierMismatch(lmodifier, lspecifier), .modifierMismatch(rmodifier, rspecifier)):
return lmodifier == rmodifier && lspecifier == rspecifier
case let (.typeMismatch(lindex, ltype1, ltype2), .typeMismatch(rindex, rtype1, rtype2)),
let (.argumentMismatch(lindex, ltype1, ltype2), .argumentMismatch(rindex, rtype1, rtype2)):
return lindex == rindex && ltype1 == rtype1 && ltype2 == rtype2
case let (.missingArgument(lindex), .missingArgument(rindex)):
return lindex == rindex
case (.unexpectedEndOfString, _),
(.unexpectedToken, _),
(.duplicateFlag, _),
(.unsupportedFlag, _),
(.unsupportedSpecifier, _),
(.modifierMismatch, _),
(.typeMismatch, _),
(.argumentMismatch, _),
(.missingArgument, _):
return false
}
}
}
/// Internal representation
let locale: Locale?
let tokens: [Token]
/// Just the placeholder values - useful for testing
var placeholders: [Placeholder] {
return tokens.flatMap { (token: Token) -> [Placeholder] in
if case let .placeholder(placeholder) = token {
return [placeholder]
}
return []
}
}
/// The required argument types for formatting
public let types: [Any.Type]
/// Parse the format string, and create a FormatString wrapper if valid
public init(_ format: String, locale: Locale? = nil) throws {
var characters = String.UnicodeScalarView.SubSequence(format.unicodeScalars)
var tokens = [Token]()
var typesByIndex = [UInt: Any.Type]()
var index: UInt = 1
while !characters.isEmpty {
if let placeholder = try Placeholder(&characters, &index, locale: locale) {
tokens.append(.placeholder(placeholder))
guard let type = try placeholder.getSwiftType() else {
continue
}
if let fieldWidthIndex = placeholder.fieldWidth?.index {
typesByIndex[fieldWidthIndex] = Int.self
}
if let precisionIndex = placeholder.precision?.index {
typesByIndex[precisionIndex] = Int.self
}
let typeIndex = placeholder.index
if let oldType = typesByIndex[typeIndex], oldType != type {
throw Error.typeMismatch(Int(typeIndex), oldType, type)
}
typesByIndex[typeIndex] = type
}
var string = ""
while let first = characters.first {
if first == "%" { break }
characters.removeFirst()
string.append(Character(first))
}
if !string.isEmpty {
tokens.append(.string(string))
}
}
self.locale = locale
self.tokens = tokens
if typesByIndex.isEmpty {
types = []
} else {
var types = [Any.Type]()
let indexes = typesByIndex.keys.sorted()
for index in 1 ... indexes.last! {
types.append(typesByIndex[index] ?? Any.self)
}
self.types = types
}
}
/// Print the formatted string with the specified arguments
public func print(arguments: [Any]) throws -> String {
return try tokens.map { token -> String in
switch token {
case let .string(string):
return string
case let .placeholder(placeholder):
if placeholder.specifier == .percentChar {
return "%"
}
let value = try argument(at: placeholder.index, in: arguments)
let fieldWidth = try placeholder.fieldWidth?.index.map {
try argument(at: $0, in: arguments)
} as? Int
let precision = try placeholder.precision?.index.map {
try argument(at: $0, in: arguments)
} as? Int
return placeholder.print(value, fieldWidth, precision, locale: locale)
}
}.joined()
}
/// Variadic form of the print function
public func print(_ arguments: Any...) throws -> String {
if arguments.count == 1, let array = arguments.first as? [Any], array.count == types.count {
// If only argument is an array, and count matches types, treat as argument array
return try print(arguments: array)
}
return try print(arguments: arguments)
}
// MARK: private
private func argument(at index: UInt, in arguments: [Any]) throws -> Any {
let zeroBasedIndex = Int(index) - 1
if index > arguments.count {
throw Error.missingArgument(Int(index))
}
let argument = arguments[zeroBasedIndex]
let expectedType = types[zeroBasedIndex]
guard let value = cast(argument, as: expectedType) else {
throw Error.argumentMismatch(Int(index), Swift.type(of: argument), expectedType)
}
return value
}
private func cast(_ value: Any, as type: Any.Type) -> Any? {
if type == Swift.type(of: value) {
return value
}
switch (type, value) {
// Integer promotion
case (is Int.Type, let value as Int32):
return Int(value)
case (is Int.Type, let value as Int16):
return Int(value)
case (is Int.Type, let value as UInt16):
return Int(value)
case (is Int.Type, let value as Int8):
return Int(value)
case (is Int.Type, let value as UInt8):
return Int(value)
// Double promotion
case (is Double.Type, let value as NSNumber):
return Double(truncating: value)
// Character promotion
case (is Character.Type, let value as String):
return value.first
case (is Character.Type, let value as Unicode.Scalar):
return Character(value)
case (is Character.Type, let value as Int):
return UnicodeScalar(value).map(Character.init)
case (is Character.Type, let value as UInt32):
return UnicodeScalar(value).map(Character.init)
case (is Character.Type, let value as UInt16):
return UnicodeScalar(value).map(Character.init)
case (is Character.Type, let value as UInt8):
return Character(UnicodeScalar(value))
case (is Character.Type, let value as Int8):
return Character(UnicodeScalar(UInt8(value)))
// String promotion
case (is String.Type, let value as NSString):
return value as String
// Pointer promotion
case _ where type == AnyObject.self:
return value as AnyObject
// Any promotion
case _ where type == Any.self:
return value
default: // Any
return nil
}
}
}
private extension String.UnicodeScalarView.SubSequence {
mutating func readCharacter(_ character: Unicode.Scalar) -> Bool {
if first == character {
removeFirst()
return true
}
return false
}
mutating func readCharacter(_ matching: (Unicode.Scalar) -> Bool) -> Unicode.Scalar? {
if first.map(matching) == true {
return popFirst()
}
return nil
}
mutating func readUInt() throws -> UInt? {
if readCharacter("0") {
return 0
}
return try readPositiveInt()
}
mutating func readPositiveInt() throws -> UInt? {
var intString = ""
if let digit = readCharacter({ "123456789".unicodeScalars.contains($0) }) {
intString.append(Character(digit))
while let digit = readCharacter({ "0123456789".unicodeScalars.contains($0) }) {
intString.append(Character(digit))
}
}
if intString.isEmpty {
return nil
}
guard let int = UInt(intString) else {
throw FormatString.Error.unexpectedToken(intString.first!)
}
return int
}
mutating func readFlags() throws -> [FormatString.Flag] {
var flags = [FormatString.Flag]()
while let flag = FormatString.Flag(&self) {
if flags.contains(flag) {
throw FormatString.Error.duplicateFlag(Character(flag.rawValue))
}
flags.append(flag)
}
return flags
}
}
// Specification references:
// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html
// http://pubs.opengroup.org/onlinepubs/009695399/functions/printf.html