// // 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 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