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

Add a way of disabling exponential notation when encoding doubles #374

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 0 additions & 16 deletions Package.resolved

This file was deleted.

13 changes: 13 additions & 0 deletions Sources/Yams/Emitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ public final class Emitter {
case crln
}

public enum NumberFormatStyle {
case scientific
case decimal
}

/// Retrieve this Emitter's binary output.
public internal(set) var data = Data()

Expand Down Expand Up @@ -254,6 +259,14 @@ public final class Emitter {

/// Set the style for mappings (dictionaries)
public var mappingStyle: Node.Mapping.Style = .any

/// Set the style for formatting doubles
public static var doubleFormatStyle: NumberFormatStyle = .scientific

public static let doubleMaximumSignificantDigits = 15
public static let doubleMinimumFractionDigits = 1

public static let floatMaximumSignificantDigits = 7
}

/// Configuration options to use when emitting YAML.
Expand Down
30 changes: 22 additions & 8 deletions Sources/Yams/Representer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,12 @@ private let iso8601WithoutZFormatter: DateFormatter = {
extension Double: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
return .init(doubleFormatter.string(for: self)!.replacingOccurrences(of: "+-", with: "-"), Tag(.float))
switch Emitter.Options.doubleFormatStyle {
case .scientific:
return .init(doubleScientificFormatter.string(for: self)!.replacingOccurrences(of: "+-", with: "-"), Tag(.float))
case .decimal:
return .init(doubleDecimalFormatter.string(for: self)!, Tag(.float))
}
}
}

Expand All @@ -181,21 +186,26 @@ extension Float: ScalarRepresentable {
}
}

private func numberFormatter(with significantDigits: Int) -> NumberFormatter {
private func numberFormatter(with significantDigits: Int, style: NumberFormatter.Style = .scientific, minimumFractionDigits: Int = 0) -> NumberFormatter {
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.numberStyle = .scientific
formatter.usesSignificantDigits = true
formatter.maximumSignificantDigits = significantDigits
formatter.numberStyle = style
if style == .scientific {
formatter.usesSignificantDigits = true
formatter.maximumSignificantDigits = significantDigits
}
formatter.positiveInfinitySymbol = ".inf"
formatter.negativeInfinitySymbol = "-.inf"
formatter.notANumberSymbol = ".nan"
formatter.exponentSymbol = "e+"
formatter.minimumFractionDigits = minimumFractionDigits
return formatter
}

private let doubleFormatter = numberFormatter(with: 15)
private let floatFormatter = numberFormatter(with: 7)
private let doubleDecimalFormatter = numberFormatter(with: Emitter.Options.doubleMaximumSignificantDigits, style: .decimal, minimumFractionDigits: Emitter.Options.doubleMinimumFractionDigits)
private let doubleScientificFormatter = numberFormatter(with: Emitter.Options.doubleMaximumSignificantDigits, style: .scientific)

private let floatFormatter = numberFormatter(with: Emitter.Options.floatMaximumSignificantDigits)

// TODO: Support `Float80`
// extension Float80: ScalarRepresentable {}
Expand Down Expand Up @@ -314,13 +324,17 @@ extension Float: YAMLEncodable {

private extension FloatingPoint where Self: CVarArg {
var formattedStringForCodable: String {
if Emitter.Options.doubleFormatStyle == .decimal {
return doubleDecimalFormatter.string(for: self)!
}

// Since `NumberFormatter` creates a string with insufficient precision for Decode,
// it uses with `String(format:...)`
let string = String(format: "%.*g", DBL_DECIMAL_DIG, self)
// "%*.g" does not use scientific notation if the exponent is less than –4.
// So fallback to using `NumberFormatter` if string does not uses scientific notation.
guard string.lazy.suffix(5).contains("e") else {
return doubleFormatter.string(for: self)!.replacingOccurrences(of: "+-", with: "-")
return doubleScientificFormatter.string(for: self)!.replacingOccurrences(of: "+-", with: "-")
}
return string
}
Expand Down
3 changes: 2 additions & 1 deletion Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ XCTMain([
testCase(ResolverTests.allTests),
testCase(SpecTests.allTests),
testCase(StringTests.allTests),
testCase(YamlErrorTests.allTests)
testCase(YamlErrorTests.allTests),
testCase(DoubleEncodingTests.allTests)
])
36 changes: 36 additions & 0 deletions Tests/YamsTests/DoubleEncodingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import XCTest
import Yams

class DoubleEncodingTests: XCTestCase {
override class func setUp() {
Emitter.Options.doubleFormatStyle = .decimal
}

func testDecimalDoubleStyle() throws {
XCTAssertEqual(try Node(Double(6.85)), "6.85")
}

func testMinimumFractionDigits() throws {
XCTAssertEqual(try Node(Double(6.0)), "6.0")
}

func testDecimalDoubleStyleBox() throws {
print(try Double(6.85).box(), "6.85")
}

func testMinimumFractionDigitsBox() throws {
print(try Double(6.0).box(), "6.0")
}
}

extension DoubleEncodingTests {
static var allTests: [(String, (DoubleEncodingTests) -> () throws -> Void)] {
return [
("testDecimalDoubleStyle", testDecimalDoubleStyle),
("testMinimumFractionDigits", testMinimumFractionDigits),
("testDecimalDoubleStyleBox", testDecimalDoubleStyleBox),
("testMinimumFractionDigitsBox", testMinimumFractionDigitsBox)
]
}
}