Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] WIP: Swifty numeric formatting #177

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
95 changes: 95 additions & 0 deletions Sources/FormattersModule/CollectionPadding.swift
Original file line number Diff line number Diff line change
@@ -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<C: Collection>(
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
}
}
200 changes: 200 additions & 0 deletions Sources/FormattersModule/FloatFormatting.swift
Original file line number Diff line number Diff line change
@@ -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<I: FloatingPoint>(
_ 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<I: FloatingPoint>(
_ 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
}
}
54 changes: 54 additions & 0 deletions Sources/FormattersModule/Formatting.swift
Original file line number Diff line number Diff line change
@@ -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: FixedWidthInteger, OS: TextOutputStream>(_: I, into: inout OS)
}
extension FixedWidthIntegerFormatter {
public func format<I: FixedWidthInteger>(_ x: I) -> String {
var result = ""
self.format(x, into: &result)
return result
}
}
Loading