diff --git a/Package.swift b/Package.swift index 565d5d60..4c1cc499 100644 --- a/Package.swift +++ b/Package.swift @@ -25,16 +25,18 @@ let package = Package( // User-facing modules .target(name: "ComplexModule", dependencies: ["RealModule"]), .target(name: "Numerics", dependencies: ["ComplexModule", "RealModule"]), - .target(name: "RealModule", dependencies: ["_NumericsShims"]), - + .target(name: "RealModule", dependencies: ["_NumericsShims", "FormattersModule"]), + .target(name: "FormattersModule", dependencies: []), + // Implementation details .target(name: "_NumericsShims", dependencies: []), .target(name: "_TestSupport", dependencies: ["Numerics"]), - + // Unit test bundles .testTarget(name: "ComplexTests", dependencies: ["_TestSupport"]), .testTarget(name: "RealTests", dependencies: ["_TestSupport"]), - + .testTarget(name: "FormattersTests", dependencies: ["_TestSupport"]), + // Test executables .target(name: "ComplexLog", dependencies: ["Numerics", "_TestSupport"], path: "Tests/Executable/ComplexLog"), .target(name: "ComplexLog1p", dependencies: ["Numerics", "_TestSupport"], path: "Tests/Executable/ComplexLog1p") diff --git a/Sources/FormattersModule/CollectionPadding.swift b/Sources/FormattersModule/CollectionPadding.swift new file mode 100644 index 00000000..d1d3d645 --- /dev/null +++ b/Sources/FormattersModule/CollectionPadding.swift @@ -0,0 +1,95 @@ +//===--- CollectionPadding.swift ------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +public enum CollectionBound { + case start + case end +} +extension CollectionBound { + internal var inverted: CollectionBound { self == .start ? .end : .start } +} + +extension RangeReplaceableCollection { + internal mutating func pad( + to newCount: Int, using fill: Self.Element, at bound: CollectionBound = .end + ) { + guard newCount > 0 else { return } + + let currentCount = self.count + guard newCount > currentCount else { return } + + let filler = repeatElement(fill, count: newCount &- currentCount) + let insertIdx = bound == .start ? self.startIndex : self.endIndex + self.insert(contentsOf: filler, at: insertIdx) + } + // TODO: Align/justify version, which just swaps the bound? +} + + +// Intersperse +extension Collection where SubSequence == Self { + fileprivate mutating func _eat(_ n: Int = 1) -> SubSequence { + defer { self = self.dropFirst(n) } + return self.prefix(n) + } +} + +// NOTE: The below would be more efficient with RRC method variants +// that returned the new valid indices. Instead, we have to create a new +// collection and reassign self. Similarly, we could benefit from a slide +// operation that can leave temporarily uninitialized spaces inside the +// collection. +extension RangeReplaceableCollection { + internal mutating func intersperse( + _ newElement: Element, every n: Int, startingFrom bound: CollectionBound + ) { + self.intersperse( + contentsOf: CollectionOfOne(newElement), every: n, startingFrom: bound) + } + + internal mutating func intersperse( + contentsOf newElements : C, every n: Int, startingFrom bound: CollectionBound + ) where C.Element == Element { + precondition(n > 0) + + let currentCount = self.count + guard currentCount > n else { return } + + let remainder = currentCount % n + + var result = Self() + let interspersedCount = newElements.count + let insertCount = (currentCount / n) - (remainder == 0 ? 1 : 0) + let newCount = currentCount + interspersedCount * insertCount + defer { + assert(result.count == newCount) + } + result.reserveCapacity(newCount) + + var selfConsumer = self[...] + + // When we start from the end, any remainder will appear as a prefix. + // Otherwise, the remainder will fall out naturally from the main loop. + if remainder != 0 && bound == .end { + result.append(contentsOf: selfConsumer._eat(remainder)) + assert(!selfConsumer.isEmpty, "Guarded count above") + result.append(contentsOf: newElements) + } + + while !selfConsumer.isEmpty { + result.append(contentsOf: selfConsumer._eat(n)) + if !selfConsumer.isEmpty { + result.append(contentsOf: newElements) + } + } + self = result + } +} diff --git a/Sources/FormattersModule/FloatFormatting.swift b/Sources/FormattersModule/FloatFormatting.swift new file mode 100644 index 00000000..aa8578b7 --- /dev/null +++ b/Sources/FormattersModule/FloatFormatting.swift @@ -0,0 +1,200 @@ +//===--- FloatFormatting.swift --------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + + +/// Specifies how a float should be formatted. +/// +/// The output of alignment is not meant for end-user consumption, use a +/// locale-rich formatter for that. This is meant for machine and programmer +/// use (e.g. log files, textual formats, or anywhere `printf` is used). +public struct FloatFormatting: Hashable { + // NOTE: fprintf will read from C locale. Swift print uses dot. + // We could consider a global var for the c locale's character. + + /// The radix character to use. + public var radixPoint: Character + + /// Whether the include an explicit positive sign, if positive. + public var explicitPositiveSign: Bool + + /// Whether to use uppercase (TODO: hex and/or exponent characters?) + public var uppercase: Bool + + // Note: no includePrefix for FloatFormatting; it doesn't exist for + // fprintf (%a always prints a prefix, %efg don't need one), so why + // introduce it here. + + public enum Notation: Hashable { + /// Swift's String(floating-point) formatting. + case decimal + + /// Hexadecimal formatting. Only permitted for BinaryFloatingPoint types. + case hex + + /// Prints all digits before the radix point, and `precision` digits following + /// the radix point. If `precision` is zero, the radix point is omitted. + /// + /// Note that very large floating-point values may print quite a lot of digits + /// when using this format, even if `precision` is zero--up to hundreds for + /// `Double`, and thousands for `Float80`. Note also that this format is + /// very likely to print non-zero values as all-zero. If either of these is a concern + /// for your use, consider using `.optimal` or `.hybrid` instead. + /// + /// Systems may impose an upper bound on the number of digits that are + /// supported following the radix point. + /// + /// This corresponds to C's `%f` formatting used with `fprintf`. + case fixed(precision: Int32 = 6) + + /// Prints the number in the form [-]d.ddd...dde±dd, with `precision` significant + /// digits following the radix point. Systems may impose an upper bound on the number + /// of digits that are supported. + /// + /// This corresponds to C's `%e` formatting used with `fprintf`. + case exponential(precision: Int32 = 6) + + /// Behaves like `.fixed` when the number is scaled close to 1.0, and like + /// `.exponential` if it has a very large or small exponent. + /// + /// The corresponds to C's `%g` formatting used with `fprintf`. + case hybrid(precision: Int32 = 6) + } + + /// The notation to use. Swift's default formatting behavior corresponds to `.decimal`. + public var notation: Notation + + /// The separator formatting options to use. + public var separator: SeparatorFormatting + + public init( + radixPoint: Character = ".", + explicitPositiveSign: Bool = false, + uppercase: Bool = false, + notation: Notation = .decimal, + separator: SeparatorFormatting = .none + ) { + self.radixPoint = radixPoint + self.explicitPositiveSign = explicitPositiveSign + self.uppercase = uppercase + self.notation = notation + self.separator = separator + } + + /// Format as a decimal (Swift's default printing format). + public static var decimal: FloatFormatting { .decimal() } + + /// Format as a decimal (Swift's default printing format). + public static func decimal( + radixPoint: Character = ".", + explicitPositiveSign: Bool = false, + uppercase: Bool = false, + separator: SeparatorFormatting = .none + ) -> FloatFormatting { + return FloatFormatting( + radixPoint: radixPoint, + explicitPositiveSign: explicitPositiveSign, + uppercase: uppercase, + notation: .decimal, + separator: separator + ) + } + + /// Format as a hex float. + public static var hex: FloatFormatting { .hex() } + + /// Format as a hex float. + public static func hex( + radixPoint: Character = ".", + explicitPositiveSign: Bool = false, + uppercase: Bool = false, + separator: SeparatorFormatting = .none + ) -> FloatFormatting { + return FloatFormatting( + radixPoint: radixPoint, + explicitPositiveSign: explicitPositiveSign, + uppercase: uppercase, + notation: .hex, + separator: separator + ) + } +} + +extension FloatFormatting { + // Returns a fprintf-compatible length modifier for a given argument type + private static func _formatStringLengthModifier( + _ type: I.Type + ) -> String? { + switch type { + // fprintf formatters promote Float to Double + case is Float.Type: return "" + case is Double.Type: return "" + // fprintf formatters use L for Float80 + case is Float80.Type: return "L" + default: return nil + } + } + + // TODO: Are we making these public yet? + public func toFormatString( + _ align: String.Alignment = .none, for type: I.Type + ) -> String? { + + // No separators supported + guard separator == SeparatorFormatting.none else { return nil } + + // Radix character simply comes from C locale, so require it be + // default. + guard radixPoint == "." else { return nil } + + // Make sure this is a type that fprintf supports. + guard let lengthMod = FloatFormatting._formatStringLengthModifier(type) else { return nil } + + var specification = "%" + + // 1. Flags + // IEEE: `+` The result of a signed conversion shall always begin with a sign ( '+' or '-' ) + if explicitPositiveSign { + specification += "+" + } + + // IEEE: `-` The result of the conversion shall be left-justified within the field. The + // conversion is right-justified if this flag is not specified. + if align.anchor == .start { + specification += "-" + } + + // Padding has to be space + guard align.fill == " " else { + return nil + } + + if align.minimumColumnWidth > 0 { + specification += "\(align.minimumColumnWidth)" + } + + // 3. Precision and conversion specifier. + switch notation { + case let .fixed(p): + specification += "\(p)" + lengthMod + (uppercase ? "F" : "f") + case let .exponential(p): + specification += "\(p)" + lengthMod + (uppercase ? "E" : "e") + case let .hybrid(p): + specification += "\(p)" + lengthMod + (uppercase ? "G" : "g") + case .hex: + guard type.radix == 2 else { return nil } + specification += lengthMod + (uppercase ? "A" : "a") + default: + return nil + } + + return specification + } +} diff --git a/Sources/FormattersModule/Formatting.swift b/Sources/FormattersModule/Formatting.swift new file mode 100644 index 00000000..2bfcf44f --- /dev/null +++ b/Sources/FormattersModule/Formatting.swift @@ -0,0 +1,54 @@ +//===--- Formatting.swift -------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Specify separators to insert during formatting. +public struct SeparatorFormatting: Hashable { + /// The separator character to use. + public var separator: Character? + + /// The spacing between separators. + public var spacing: Int + + public init(separator: Character? = nil, spacing: Int = 3) { + self.separator = separator + self.spacing = spacing + } + + // TODO: Consider modeling `none` as `nil` separator formatting... + + /// No separators. + public static var none: SeparatorFormatting { + SeparatorFormatting() + } + + /// Insert `separator` every `n`characters. + public static func every( + _ n: Int, separator: Character + ) -> SeparatorFormatting { + SeparatorFormatting(separator: separator, spacing: n) + } + + /// Insert `separator` every thousands. + public static func thousands(separator: Character) -> SeparatorFormatting { + .every(3, separator: separator) + } +} + +public protocol FixedWidthIntegerFormatter { + func format(_: I, into: inout OS) +} +extension FixedWidthIntegerFormatter { + public func format(_ x: I) -> String { + var result = "" + self.format(x, into: &result) + return result + } +} diff --git a/Sources/FormattersModule/IntFormatting.swift b/Sources/FormattersModule/IntFormatting.swift new file mode 100644 index 00000000..f5a8af6d --- /dev/null +++ b/Sources/FormattersModule/IntFormatting.swift @@ -0,0 +1,181 @@ +//===--- IntFormatting.swift ----------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Specifies how an integer should be formatted. +/// +/// The output of alignment is not meant for end-user consumption, use a +/// locale-rich formatter for that. This is meant for machine and programmer +/// use (e.g. log files, textual formats, or anywhere `printf` is used). +public struct IntegerFormatting: Hashable { + /// The base to use for the string representation. `radix` must be at least 2 and at most 36. + /// The default is 10. + public var radix: Int + + /// Explicitly print a positive sign.TODO + public var explicitPositiveSign: Bool + + /// Include the integer literal prefix for binary, octal, or hexadecimal bases. + public var includePrefix: Bool + + /// Whether to use uppercase letters to represent numerals + /// greater than 9 (default is to use lowercase) + public var uppercase: Bool + + /// TODO: docs + public var minDigits: Int + + /// The separator formatting options to use. + public var separator: SeparatorFormatting + + public init( + radix: Int = 10, + explicitPositiveSign: Bool = false, + includePrefix: Bool = true, + uppercase: Bool = false, + minDigits: Int = 1, + separator: SeparatorFormatting = .none + ) { + precondition(radix >= 2 && radix <= 36) + + self.radix = radix + self.explicitPositiveSign = explicitPositiveSign + self.includePrefix = includePrefix + self.uppercase = uppercase + self.minDigits = minDigits + self.separator = separator + } + + /// Format as a decimal integer. + public static func decimal( + explicitPositiveSign: Bool = false, + minDigits: Int = 1, + separator: SeparatorFormatting = .none + ) -> IntegerFormatting { + return IntegerFormatting( + radix: 10, + explicitPositiveSign: explicitPositiveSign, + minDigits: minDigits, + separator: separator) + } + + /// Format as a decimal integer. + public static var decimal: IntegerFormatting { .decimal() } + + /// Format as a hexadecimal integer. + public static func hex( + explicitPositiveSign: Bool = false, + includePrefix: Bool = true, + uppercase: Bool = false, + minDigits: Int = 1, + separator: SeparatorFormatting = .none + ) -> IntegerFormatting { + return IntegerFormatting( + radix: 16, + explicitPositiveSign: explicitPositiveSign, + includePrefix: includePrefix, + uppercase: uppercase, + minDigits: minDigits, + separator: separator) + } + + /// Format as a hexadecimal integer. + public static var hex: IntegerFormatting { .hex() } + + /// Format as an octal integer. + public static func octal( + explicitPositiveSign: Bool = false, + includePrefix: Bool = true, + uppercase: Bool = false, + minDigits: Int = 1, // TODO: document if prefix is zero! + separator: SeparatorFormatting = .none + ) -> IntegerFormatting { + IntegerFormatting( + radix: 8, + explicitPositiveSign: explicitPositiveSign, + includePrefix: includePrefix, + uppercase: uppercase, + minDigits: minDigits, + separator: separator) + } + + /// Format as an octal integer. + public static var octal: IntegerFormatting { .octal() } + + /// TODO: binary + +} + +extension IntegerFormatting { + // On Prefixes + // + // `fprintf` has oddball prefix behaviors. + // * We want signed and unsigned prefixes (former cannot be easily emulated) + // * The precision-adjusting octal prefix won't be missed. + // * Nor the special case for minDigits == 0 + // * We want a hexadecimal prefix to be printed if requested, even for + // the value 0. + // * We don't want to conflate prefix capitalization with hex-digit + // capitalization. + // * A binary prefix for radix 2 is nice to have + // + // Instead, we go with Swift literal syntax. If a prefix is requested, + // and radix is: + // 2: "0b1010" + // 8: "0o1234" + // 16: "0x89ab" + // + // This can be sensibly emulated using `fprintf` for unsigned types by just + // adding it before the specifier. + fileprivate var _prefix: String { + guard includePrefix else { return "" } + switch radix { + case 2: return "0b" + case 8: return "0o" + case 16: return "0x" + default: return "" + } + } +} + +extension IntegerFormatting: FixedWidthIntegerFormatter { + public func format( + _ i: I, into os: inout OS + ) { + if i == 0 && self.minDigits == 0 { + return + } + + // Sign + if I.isSigned { + if i < 0 { + os.write("-") + } else if self.explicitPositiveSign { + os.write("+") + } + } + + // Prefix + os.write(self._prefix) + + // Digits + let number = String( + i.magnitude, radix: self.radix, uppercase: self.uppercase + ).aligned(.right(columns: self.minDigits, fill: "0")) + if let separator = self.separator.separator { + var num = number + num.intersperse( + separator, every: self.separator.spacing, startingFrom: .end) + os.write(num) + } else { + os.write(number) + } + } +} diff --git a/Sources/FormattersModule/Interpolations.swift b/Sources/FormattersModule/Interpolations.swift new file mode 100644 index 00000000..a0f7ed1b --- /dev/null +++ b/Sources/FormattersModule/Interpolations.swift @@ -0,0 +1,110 @@ +//===--- Interpolations.swift ---------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// Conform to this protocol to customize the behavior of Swifty `printf`-style interpolations. +public protocol SwiftyStringFormatting { + // %s, but general over anything that can be printed + mutating func appendInterpolation( + _ s: S, + maxPrefixLength: Int, // Int.max by default + align: String.Alignment // .none + ) where S.Element: CustomStringConvertible + + // %x, %X, %o, %d, %i + // TODO: %u? + mutating func appendInterpolation( + _ value: I, + format: IntegerFormatting, // .decimal(minDigits: 1) by default + align: String.Alignment // .none + ) + + // %f, %F + // TODO: FloatFormatting struct + mutating func appendInterpolation( + _ value: F, + explicitRadix: Bool, // false by default + precision: Int?, // nil by default + uppercase: Bool, // false by default + zeroFillFinite: Bool, // false by default + minDigits: Int, // 1 by default + explicitPositiveSign: Character?, // nil by default + align: String.Alignment) // .none + +} + +extension DefaultStringInterpolation: SwiftyStringFormatting { + + public mutating func appendInterpolation( + _ seq: S, + maxPrefixLength: Int = Int.max, + align: String.Alignment = .none + ) where S.Element: CustomStringConvertible { + var str = "" + var iter = seq.makeIterator() + var count = 0 + while let next = iter.next(), count < maxPrefixLength { + str.append(next.description) + count += 1 + } + appendInterpolation(str.aligned(align)) + } + + public mutating func appendInterpolation( + _ value: I, + format: IntegerFormatting = .decimal(minDigits: 1), + align: String.Alignment = .none + ) { + appendInterpolation(format.format(value).aligned(align)) + } + + + // %f, %F + public mutating func appendInterpolation( + _ value: F, + explicitRadix: Bool = false, + precision: Int? = nil, + uppercase: Bool = false, + zeroFillFinite: Bool = false, + minDigits: Int = 1, + explicitPositiveSign: Character? = nil, + align: String.Alignment = .none + ) { + + // TODO: body should be extracted into a format method, can be invoked + // outside of interpolation context + + let valueStr: String + if value.isNaN { + valueStr = uppercase ? "NAN" : "nan" + } else if value.isInfinite { + valueStr = uppercase ? "INF" : "inf" + } else { + if let dValue = value as? Double { + valueStr = String(dValue) + } else if let fValue = value as? Float { + valueStr = String(fValue) + } else { + fatalError("TODO") + } + + // FIXME: Precision, minDigits, radix, zeroFillFinite, ... + guard explicitRadix == false else { fatalError() } + guard precision == nil else { fatalError() } + guard uppercase == false else { fatalError() } + guard minDigits == 1 else { fatalError() } + guard zeroFillFinite == false else { fatalError() } + guard explicitPositiveSign == nil else { fatalError() } + } + + appendInterpolation(valueStr.aligned(align)) + } + +} diff --git a/Sources/FormattersModule/StringAlignment.swift b/Sources/FormattersModule/StringAlignment.swift new file mode 100644 index 00000000..39397091 --- /dev/null +++ b/Sources/FormattersModule/StringAlignment.swift @@ -0,0 +1,94 @@ +//===--- StringAlignment.swift --------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension String { + /// Specify the alignment of a string, for machine-formatting purposes + /// + /// The output of alignment is not meant for end-user consumption, use a + /// locale-rich formatter for that. This is meant for machine and programmer + /// use (e.g. log files, textual formats, or anywhere `printf` is used). + /// + /// NOTE: One `Character` is currently considered one column, though they + /// may commonly be printed out differently. What is considered one or + /// two columns is application-specific. The Unicode standard does not dictate this. + /// + /// TODO: We can consider adding a half-sensible approximation function, + /// or even use a user-supplied function here. + public struct Alignment: Hashable { + /// The minimum number of characters + public var minimumColumnWidth: Int + + /// Where to align + public var anchor: CollectionBound + + /// The Character to use to reach `minimumColumnWidth` + public var fill: Character + + public init( + minimumColumnWidth: Int = 0, + anchor: CollectionBound = .end, + fill: Character = " " + ) { + self.minimumColumnWidth = minimumColumnWidth + self.anchor = anchor + self.fill = fill + } + + /// Specify a right-aligned string. + public static var right: Alignment { Alignment(anchor: .end) } + + /// Specify a left-aligned string. + public static var left: Alignment { Alignment(anchor: .start) } + + /// No aligment requirements + public static var none: Alignment { .right } + + /// Specify a right-aligned string. + public static func right( + columns: Int = 0, fill: Character = " " + ) -> Alignment { + Alignment.right.columns(columns).fill(fill) + } + /// Specify a left-aligned string. + public static func left( + columns: Int = 0, fill: Character = " " + ) -> Alignment { + Alignment.left.columns(columns).fill(fill) + } + + /// Specify the minimum number of columns. + public func columns(_ i: Int) -> Alignment { + var result = self + result.minimumColumnWidth = i + return result + } + + public func fill(_ c: Character) -> Alignment { + var result = self + result.fill = c + return result + } + } +} + +extension StringProtocol { + /// Align `self`, according to `align`. + public func aligned(_ align: String.Alignment) -> String { + var copy = String(self) + copy.pad(to: align.minimumColumnWidth, using: align.fill, at: align.anchor.inverted) + return copy + } + + /// Indent `self` by `columns`, using `fill` (default space). + public func indented(_ columns: Int, fill: Character = " ") -> String { + String(repeating: fill, count: columns) + self + } +} diff --git a/Sources/RealModule/Formatters.swift b/Sources/RealModule/Formatters.swift new file mode 100644 index 00000000..e6626878 --- /dev/null +++ b/Sources/RealModule/Formatters.swift @@ -0,0 +1,13 @@ +//===--- Formatters.swift -------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@_exported import FormattersModule + diff --git a/Tests/FormattersTests/FormattersTests.swift b/Tests/FormattersTests/FormattersTests.swift new file mode 100644 index 00000000..b6c178e4 --- /dev/null +++ b/Tests/FormattersTests/FormattersTests.swift @@ -0,0 +1,72 @@ +//===--- FormattersTest.swift ---------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2019 - 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import FormattersModule + + +final class FrmattersTest: XCTestCase { + public func testIntegerFormatting() { + // TODO: More exhaustive programmatic tests using the formatter structs + } + + // Some quick ad-hoc tests + public func testIntegerFormattingAdHoc() { + var buffer = "" + func put(_ s: String) { + buffer += s + } + func expect(_ s: String) { + XCTAssertEqual(buffer, s) + buffer = "" + } + + put(""" + \(12345678, format: .decimal(minDigits: 2), align: .right(columns: 9, fill: " ")) + """) + expect(" 12345678") + + put("\(54321, format: .hex)") + expect("0xd431") + + put("\(54321, format: .hex(includePrefix: false, uppercase: true))") + expect("D431") + + put("\(1234567890, format: .hex(includePrefix: true, minDigits: 12), align: .right(columns: 20))") + expect(" 0x0000499602d2") + + put("\(9876543210, format: .hex(explicitPositiveSign: true), align: .right(columns: 20, fill: "-"))") + expect("--------+0x24cb016ea") + + put("\("Hi there", align: .left(columns: 20))!") + expect("Hi there !") + + put("\(-1234567890, format: .hex(includePrefix: true, minDigits: 12), align: .right(columns: 20))") + expect(" -0x0000499602d2") + + put("\(-1234567890, format: .hex(minDigits: 12, separator: .every(2, separator: "_")), align: .right(columns: 22))") + expect(" -0x00_00_49_96_02_d2") + + put("\(-1234567890, format: .hex(minDigits: 10, separator: .every(4, separator: "_")), align: .right(columns: 22))") + expect(" -0x00_4996_02d2") + + put("\(1234567890, format: .decimal(separator: .thousands(separator: "⌟")))") + expect("1⌟234⌟567⌟890") + + put("\(98765, format: .hex(includePrefix: true, minDigits: 8, separator: .every(2, separator: "_")))") + expect("0x00_01_81_cd") + + put("\(12345, format: .hex(minDigits: 5))") + expect("0x03039") + + } + +}