Skip to content
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
23 changes: 20 additions & 3 deletions Sources/Yams/Emitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ public final class Emitter {
case crln
}

/// Floating point number format style to use when emitting YAML.
public enum FloatingPointNumberFormatStrategy {
/// Use scientific notation.
case scientific
/// Use decimal notation.
case decimal
}

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

Expand Down Expand Up @@ -281,6 +289,9 @@ public final class Emitter {
/// Redundancy aliasing strategy to use when encoding. Defaults to nil
public var redundancyAliasingStrategy: RedundancyAliasingStrategy?

/// Set the number format strategy to use when emitting YAML.
public var floatingPointNumberFormatStrategy: FloatingPointNumberFormatStrategy = .scientific

/// Create `Emitter.Options` with the specified values.
///
/// - parameter canonical: Set if the output should be in the "canonical" format described in the YAML
Expand All @@ -297,6 +308,7 @@ public final class Emitter {
/// - parameter mappingStyle: Set the style for mappings (dictionaries)
/// - parameter newLineScalarStyle: Set the style for newline-containing scalars
/// - parameter redundancyAliasingStrategy: Set the strategy for identifying
/// - parameter numberFormatStrategy: Set the number format strategy to use when emitting YAML.
/// redundant structures and automatically aliasing them
public init(canonical: Bool = false, indent: Int = 0, width: Int = 0, allowUnicode: Bool = false,
lineBreak: Emitter.LineBreak = .ln,
Expand All @@ -306,7 +318,8 @@ public final class Emitter {
sortKeys: Bool = false, sequenceStyle: Node.Sequence.Style = .any,
mappingStyle: Node.Mapping.Style = .any,
newLineScalarStyle: Node.Scalar.Style = .any,
redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil) {
redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil,
floatingPointNumberFormatStrategy: FloatingPointNumberFormatStrategy = .scientific) {
self.canonical = canonical
self.indent = indent
self.width = width
Expand All @@ -320,6 +333,7 @@ public final class Emitter {
self.mappingStyle = mappingStyle
self.newLineScalarStyle = newLineScalarStyle
self.redundancyAliasingStrategy = redundancyAliasingStrategy
self.floatingPointNumberFormatStrategy = floatingPointNumberFormatStrategy
}
}

Expand All @@ -346,6 +360,7 @@ public final class Emitter {
/// - parameter mappingStyle: Set the style for mappings (dictionaries)
/// - parameter newLineScalarStyle: Set the style for newline-containing scalars
/// - parameter redundancyAliasingStrategy: Set the strategy for identifying redundant
/// - parameter numberFormatStrategy: Set the number format strategy to use when emitting YAML.
/// structures and automatically aliasing them
public init(canonical: Bool = false,
indent: Int = 0,
Expand All @@ -359,7 +374,8 @@ public final class Emitter {
sequenceStyle: Node.Sequence.Style = .any,
mappingStyle: Node.Mapping.Style = .any,
newLineScalarStyle: Node.Scalar.Style = .any,
redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil) {
redundancyAliasingStrategy: RedundancyAliasingStrategy? = nil,
floatingPointNumberFormatStrategy: FloatingPointNumberFormatStrategy = .scientific) {
options = Options(canonical: canonical,
indent: indent,
width: width,
Expand All @@ -372,7 +388,8 @@ public final class Emitter {
sequenceStyle: sequenceStyle,
mappingStyle: mappingStyle,
newLineScalarStyle: newLineScalarStyle,
redundancyAliasingStrategy: redundancyAliasingStrategy)
redundancyAliasingStrategy: redundancyAliasingStrategy,
floatingPointNumberFormatStrategy: floatingPointNumberFormatStrategy)
// configure emitter
yaml_emitter_initialize(&emitter)
yaml_emitter_set_output(&self.emitter, { pointer, buffer, size in
Expand Down
36 changes: 14 additions & 22 deletions Sources/Yams/Encoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ public class YAMLEncoder {
if let aliasingStrategy = options.redundancyAliasingStrategy {
finalUserInfo[.redundancyAliasingStrategyKey] = aliasingStrategy
}
let encoder = _Encoder(userInfo: finalUserInfo,
sequenceStyle: options.sequenceStyle,
mappingStyle: options.mappingStyle,
newlineScalarStyle: options.newLineScalarStyle)
let encoder = _Encoder(userInfo: finalUserInfo, options: options)
var container = encoder.singleValueContainer()
try container.encode(value)
try options.redundancyAliasingStrategy?.releaseAnchorReferences()
Expand All @@ -55,26 +52,25 @@ public class YAMLEncoder {
private class _Encoder: Swift.Encoder {
var node: Node = .unused

init(userInfo: [CodingUserInfoKey: Any] = [:], codingPath: [CodingKey] = [], sequenceStyle: Node.Sequence.Style,
mappingStyle: Node.Mapping.Style, newlineScalarStyle: Node.Scalar.Style) {
init(
userInfo: [CodingUserInfoKey: Any] = [:],
codingPath: [CodingKey] = [],
options: YAMLEncoder.Options
) {
self.userInfo = userInfo
self.codingPath = codingPath
self.sequenceStyle = sequenceStyle
self.mappingStyle = mappingStyle
self.newlineScalarStyle = newlineScalarStyle
self.options = options
}

// MARK: - Swift.Encoder Methods

let codingPath: [CodingKey]
let userInfo: [CodingUserInfoKey: Any]
let sequenceStyle: Node.Sequence.Style
let mappingStyle: Node.Mapping.Style
let newlineScalarStyle: Node.Scalar.Style
let options: YAMLEncoder.Options

func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
if canEncodeNewValue {
node = Node([(Node, Node)](), .implicit, mappingStyle)
node = Node([(Node, Node)](), .implicit, options.mappingStyle)
} else {
precondition(
node.isMapping,
Expand All @@ -86,7 +82,7 @@ private class _Encoder: Swift.Encoder {

func unkeyedContainer() -> UnkeyedEncodingContainer {
if canEncodeNewValue {
node = Node([], .implicit, sequenceStyle)
node = Node([], .implicit, options.sequenceStyle)
} else {
precondition(
node.isSequence,
Expand Down Expand Up @@ -134,19 +130,15 @@ private class _ReferencingEncoder: _Encoder {
reference = .mapping(key.stringValue)
super.init(userInfo: encoder.userInfo,
codingPath: encoder.codingPath + [key],
sequenceStyle: encoder.sequenceStyle,
mappingStyle: encoder.mappingStyle,
newlineScalarStyle: encoder.newlineScalarStyle)
options: encoder.options)
}

init(referencing encoder: _Encoder, at index: Int) {
self.encoder = encoder
reference = .sequence(index)
super.init(userInfo: encoder.userInfo,
codingPath: encoder.codingPath + [_YAMLCodingKey(index: index)],
sequenceStyle: encoder.sequenceStyle,
mappingStyle: encoder.mappingStyle,
newlineScalarStyle: encoder.newlineScalarStyle)
options: encoder.options)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes to passing the options struct. 👏

}

deinit {
Expand Down Expand Up @@ -245,9 +237,9 @@ extension _Encoder: SingleValueEncodingContainer {

private func encode(yamlEncodable encodable: YAMLEncodable) throws {
func encodeNode() {
node = encodable.box()
node = encodable.box(options: options)
if let stringValue = encodable as? (any StringProtocol), stringValue.contains("\n") {
node.scalar?.style = newlineScalarStyle
node.scalar?.style = options.newLineScalarStyle
}
}
try resolveAlias(for: encodable, encode: encodeNode)
Expand Down
102 changes: 69 additions & 33 deletions Sources/Yams/Representer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,40 +62,40 @@ private func represent(_ value: Any) throws -> Node {
/// Type is representable as `Node.scalar`.
public protocol ScalarRepresentable: NodeRepresentable {
/// This value's `Node.scalar` representation.
func represented() -> Node.Scalar
func represented(options: Emitter.Options) -> Node.Scalar
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that changing the ScalarRepresentable protocol is called for. It's a breaking change.
Isn't there another way?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I will double check this.

}

extension ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() throws -> Node {
return .scalar(represented())
return .scalar(represented(options: .init()))
}
}

extension NSNull: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init("null", Tag(.null))
}
}

extension Bool: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init(self ? "true" : "false", Tag(.bool))
}
}

extension Data: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init(base64EncodedString(), Tag(.binary))
}
}

extension Date: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init(iso8601String, Tag(.timestamp))
}

Expand Down Expand Up @@ -157,15 +157,25 @@ 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))
public func represented(options: Emitter.Options) -> Node.Scalar {
let formattedString: String = formattedStringForCodable(
value: self,
floatingPointNumberFormatStrategy: options.floatingPointNumberFormatStrategy,
formatter: doubleFormatter
)
return .init(formattedString.replacingOccurrences(of: "+-", with: "-"), Tag(.float))
}
}

extension Float: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
return .init(floatFormatter.string(for: self)!.replacingOccurrences(of: "+-", with: "-"), Tag(.float))
public func represented(options: Emitter.Options) -> Node.Scalar {
let formattedString: String = formattedStringForCodable(
value: self,
floatingPointNumberFormatStrategy: options.floatingPointNumberFormatStrategy,
formatter: floatFormatter
)
return .init(formattedString.replacingOccurrences(of: "+-", with: "-"), Tag(.float))
}
}

Expand All @@ -190,7 +200,7 @@ private let floatFormatter = numberFormatter(with: 7)

extension BinaryInteger {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init(String(describing: self), Tag(.int))
}
}
Expand Down Expand Up @@ -220,29 +230,29 @@ extension Optional: NodeRepresentable {

extension Decimal: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init(description)
}
}

extension URL: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init(absoluteString)
}
}

extension String: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
let scalar = Node.Scalar(self)
return scalar.resolvedTag.name == .str ? scalar : .init(self, Tag(.str), .singleQuoted)
}
}

extension UUID: ScalarRepresentable {
/// This value's `Node.scalar` representation.
public func represented() -> Node.Scalar {
public func represented(options: Emitter.Options) -> Node.Scalar {
return .init(uuidString)
}
}
Expand All @@ -252,13 +262,13 @@ extension UUID: ScalarRepresentable {
/// Types conforming to this protocol can be encoded by `YamlEncoder`.
public protocol YAMLEncodable: Encodable {
/// Returns this value wrapped in a `Node`.
func box() -> Node
func box(options: Emitter.Options) -> Node
}

extension YAMLEncodable where Self: ScalarRepresentable {
/// Returns this value wrapped in a `Node.scalar`.
public func box() -> Node {
return .scalar(represented())
public func box(options: Emitter.Options) -> Node {
return .scalar(represented(options: options))
}
}

Expand All @@ -281,35 +291,61 @@ extension UUID: YAMLEncodable {}

extension Date: YAMLEncodable {
/// Returns this value wrapped in a `Node.scalar`.
public func box() -> Node {
public func box(options: Emitter.Options) -> Node {
return Node(iso8601StringWithFullNanosecond, Tag(.timestamp))
}
}

extension Double: YAMLEncodable {
/// Returns this value wrapped in a `Node.scalar`.
public func box() -> Node {
return Node(formattedStringForCodable, Tag(.float))
public func box(options: Emitter.Options) -> Node {
let formattedString: String = formattedStringForCodable(
value: self,
floatingPointNumberFormatStrategy: options.floatingPointNumberFormatStrategy,
formatter: doubleFormatter
)
return Node(formattedString, Tag(.float))
}
}

extension Float: YAMLEncodable {
/// Returns this value wrapped in a `Node.scalar`.
public func box() -> Node {
return Node(formattedStringForCodable, Tag(.float))
public func box(options: Emitter.Options) -> Node {
let formattedString: String = formattedStringForCodable(
value: self,
floatingPointNumberFormatStrategy: options.floatingPointNumberFormatStrategy,
formatter: floatFormatter
)
return Node(formattedString, Tag(.float))
}
}

private extension FloatingPoint where Self: CVarArg {
var formattedStringForCodable: String {
// 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: "-")
private func formattedStringForCodable<T: FloatingPoint & CustomStringConvertible & CVarArg>(
value: T,
floatingPointNumberFormatStrategy: Emitter.FloatingPointNumberFormatStrategy,
formatter: NumberFormatter
) -> String {
if floatingPointNumberFormatStrategy == .decimal {
switch value {
case .infinity:
return ".inf"
case -.infinity:
return "-.inf"
case .nan:
return ".nan"
default:
return value.description
}
return string
}

// Since `NumberFormatter` creates a string with insufficient precision for Decode,
// it uses with `String(format:...)`
let string = String(format: "%.*g", DBL_DECIMAL_DIG, value)
// "%*.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 {
formatter.numberStyle = .scientific
return formatter.string(for: value)!.replacingOccurrences(of: "+-", with: "-")
}
return string
}
Loading
Loading