mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
800 lines
31 KiB
Swift
Executable File
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
|