Skip to content

Commit

Permalink
Improved big-number formatting (#346)
Browse files Browse the repository at this point in the history
* Improved integer and floating point `formatted()` methods.

- [IntegerFormatStyle] I removed the trapping conversion to `Int`. IntegerFormatStyle can format big integers since (#262).
- [FloatingPointFormatStyle] I removed the rounding conversion to `Double`. `formatted()` now does whatever `format(_:)` does.
- [Decimal.FormatStyle] N/A (there were no conversions here).

* Reenabled "Decimal Tests" in NumberFormatStyleTests.swift.

- IntegerFormatStyle big integer tests succeed.
- IntegerFormatStyle.Attributed big integer tests fail (output is clamped to `Int64`).

* Fixes IntegerFormatStyle.Attributed.

- Added a numeric string representation case to ICUNumberFormatter.Value.
- IntegerFormatStyle.Attributed now uses the above instead of `Int64(clamping:)`.

* Removed conversions to Decimal in each integer format style (#186).

BinaryInteger's numeric string representation supersedes Decimal in the following cases:

1. IntegerFormatStyle.
2. integerFormatStyle.Attributed.
3. IntegerFormatStyle.Currency.
4. IntegerFormatStyle.Percent.

* Check whether numeric string is zero using Double.

The numeric string format permits redundant zeros (like `+00.00`).

* Removed `isZero` and `doubleValue` from `ICUNumberFormatter.Value`.

Both `isZero` and `doubleValue` were used in `ByteCountFormatStyle`. These values are now taken from `FormatInput` (`Int64`) instead. Removing them from `ICUNumberFormatter.Value` makes it easier to accommodate non-numeric payloads such as strings, which can be used to format arbitrary precision numbers.

* Added `_format(_:doubleValue:)` to `ByteCountFormatStyle`.

Here's an internal method simliar to the `_format(_:)` method removed earlier because  wants its method back! I removed the first method to accommodate `ICUNumberFormatter.Value` cases that cannot implement `doubleValue`. The new method parameterizes the conversion instead, so you can call it whenever conversions to `Double` are possible.
  • Loading branch information
oscbyspro authored Dec 21, 2023
1 parent aad0848 commit c8b6d20
Show file tree
Hide file tree
Showing 6 changed files with 18 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,10 @@ public struct ByteCountFormatStyle: FormatStyle, Sendable {

return false
}

func _format(_ value: ICUNumberFormatter.Value) -> AttributedString {
func _format(_ formatterValue: ICUNumberFormatter.Value, doubleValue: Double) -> AttributedString {
let unit: Unit = allowedUnits.contains(.kb) ? .kilobyte : .byte
if spellsOutZero && value.isZero {
if spellsOutZero && doubleValue.isZero {
let numberFormatter = ICUByteCountNumberFormatter.create(for: "measure-unit/digital-\(unit.name)\(unit == .byte ? " unit-width-full-name" : "")", locale: locale)
guard var attributedFormat = numberFormatter?.attributedFormat(.integer(.zero), unit: unit) else {
// fallback to English if ICU formatting fails
Expand Down Expand Up @@ -192,7 +192,7 @@ public struct ByteCountFormatStyle: FormatStyle, Sendable {
maxSizes = Self.maxBinarySizes
}

let absValue = abs(value.doubleValue)
let absValue = abs(doubleValue)
let bestUnit: Unit = {
var bestUnit = allowedUnits.smallestUnit
for (idx, size) in maxSizes.enumerated() {
Expand All @@ -209,7 +209,7 @@ public struct ByteCountFormatStyle: FormatStyle, Sendable {
}()

let denominator = decimal ? bestUnit.decimalSize : bestUnit.binarySize
let unitValue = value.doubleValue/Double(denominator)
let unitValue = doubleValue/Double(denominator)

let precisionSkeleton: String
switch bestUnit {
Expand All @@ -231,7 +231,7 @@ public struct ByteCountFormatStyle: FormatStyle, Sendable {
let localizedParens = localizedParens(locale: locale)
attributedString.append(AttributedString(localizedParens.0))

var attributedBytes = byteFormatter!.attributedFormat(value, unit: .byte)
var attributedBytes = byteFormatter!.attributedFormat(formatterValue, unit: .byte)
for (value, range) in attributedBytes.runs[\.byteCount] where value == .value {
attributedBytes[range].byteCount = .actualByteCount
}
Expand All @@ -242,13 +242,11 @@ public struct ByteCountFormatStyle: FormatStyle, Sendable {

return attributedString
}



public func format(_ value: Int64) -> AttributedString {
_format(.integer(value))
_format(.integer(value), doubleValue: Double(value))
}
}

}

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension BinaryFloatingPoint {

/// Format `self` with `FloatingPointFormatStyle()`.
public func formatted() -> String {
FloatingPointFormatStyle().format(Double(self))
FloatingPointFormatStyle().format(self)
}

/// Format `self` with the given format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension BinaryInteger {

/// Format `self` using `IntegerFormatStyle()`
public func formatted() -> String {
IntegerFormatStyle().format(Int(self))
IntegerFormatStyle().format(self)
}

/// Format `self` with the given format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,34 +68,14 @@ internal class ICUNumberFormatterBase {
case integer(Int64)
case floatingPoint(Double)
case decimal(Decimal)

var isZero: Bool {
switch self {
case .integer(let num):
return num == 0
case .floatingPoint(let num):
return num == 0
case .decimal(let num):
return num == 0
}
}

var doubleValue: Double {
switch self {
case .integer(let num):
return Double(num)
case .floatingPoint(let num):
return num
case .decimal(let num):
return num.doubleValue
}
}
case numericStringRepresentation(String)

var fallbackDescription: String {
switch self {
case .integer(let i): return String(i)
case .floatingPoint(let d): return String(d)
case .decimal(let d): return d.description
case .numericStringRepresentation(let i): return i
}
}
}
Expand Down Expand Up @@ -138,6 +118,8 @@ internal class ICUNumberFormatterBase {
result = try? FormatResult(formatter: uformatter, value: v)
case .decimal(let v):
result = try? FormatResult(formatter: uformatter, value: v)
case .numericStringRepresentation(let v):
result = try? FormatResult(formatter: uformatter, value: v)
}

guard let result, let str = result.string else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,6 @@ extension IntegerFormatStyle : FormatStyle {
// Formatting Int64 is the fastest option -- try that first.
if let i = Int64(exactly: value) {
str = nf.format(i)
} else if let decimal = Decimal(exactly: value) {
str = nf.format(decimal)
} else {
str = nf.format(value.numericStringRepresentation)
}
Expand Down Expand Up @@ -250,8 +248,6 @@ extension IntegerFormatStyle.Percent : FormatStyle {
// Formatting Int64 is the fastest option -- try that first.
if let i = Int64(exactly: value) {
str = nf.format(i)
} else if let decimal = Decimal(exactly: value) {
str = nf.format(decimal)
} else {
str = nf.format(value.numericStringRepresentation)
}
Expand Down Expand Up @@ -282,8 +278,6 @@ extension IntegerFormatStyle.Currency : FormatStyle {
// Formatting Int64 is the fastest option -- try that first.
if let i = Int64(exactly: value) {
str = nf.format(i)
} else if let decimal = Decimal(exactly: value) {
str = nf.format(decimal)
} else {
str = nf.format(value.numericStringRepresentation)
}
Expand Down Expand Up @@ -511,10 +505,8 @@ extension IntegerFormatStyle {
let numberValue: ICUNumberFormatterBase.Value
if let i = Int64(exactly: value) {
numberValue = .integer(i)
} else if let decimal = Decimal(exactly: value) {
numberValue = .decimal(decimal)
} else {
numberValue = .integer(Int64(clamping: value))
numberValue = .numericStringRepresentation(value.numericStringRepresentation)
}

switch style {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1948,11 +1948,11 @@ extension NumberFormatStyleTests {
XCTAssertEqual(parseableWrapperFunc(GenericWrapper<Double>(), style: .number), parseableWrapperFunc(GenericWrapper<Double>(), style: FloatingPointFormatStyle.number))
}
}
#endif

// MARK: - Decimal Tests
// TODO: Reenable once Decimal has been moved: https://github.com/apple/swift-foundation/issues/43
extension NumberFormatStyleTests {
// MARK: - Big Integer Tests

extension NumberFormatStyleTests {

func testIntegerFormatStyleBigNumberNoCrash() throws {
let uint64Style: IntegerFormatStyle<UInt64> = .init(locale: enUSLocale)
Expand All @@ -1967,7 +1967,6 @@ extension NumberFormatStyleTests {
XCTAssertEqual(uint64Currency.format(UInt64.max), "$18,446,744,073,709,551,615.00")
XCTAssertEqual(UInt64.max.formatted(.currency(code: "USD").locale(enUSLocale)), "$18,446,744,073,709,551,615.00")


let uint64StyleAttributed: IntegerFormatStyle<UInt64>.Attributed = IntegerFormatStyle<UInt64>(locale: enUSLocale).attributed
XCTAssertEqual(String(uint64StyleAttributed.format(UInt64.max).characters), "18,446,744,073,709,551,615")
XCTAssertEqual(String(UInt64.max.formatted(.number.locale(enUSLocale).attributed).characters), "18,446,744,073,709,551,615")
Expand Down Expand Up @@ -1999,4 +1998,3 @@ extension NumberFormatStyleTests {
XCTAssertEqual(Int64.min.formatted(.currency(code: "USD").locale(enUSLocale)), "-$9,223,372,036,854,775,808.00")
}
}
#endif

0 comments on commit c8b6d20

Please sign in to comment.