@@ -45,6 +45,33 @@ public final class TOONEncoder {
4545 /// The delimiter to use for array values and tabular rows.
4646 public var delimiter : Delimiter = . comma
4747
48+ /// Strategy for encoding negative zero values.
49+ public enum NegativeZeroEncodingStrategy : Hashable , Sendable {
50+ /// Normalizes `-0.0` to `0`.
51+ case normalize
52+
53+ /// Preserves `-0.0` as `-0`.
54+ case preserve
55+ }
56+
57+ /// The strategy to use for encoding negative zero values.
58+ public var negativeZeroEncodingStrategy : NegativeZeroEncodingStrategy = . normalize
59+
60+ /// Strategy for encoding non-conforming floating-point values.
61+ public enum NonConformingFloatEncodingStrategy : Hashable , Sendable {
62+ /// Encodes non-finite values as `null`.
63+ case null
64+
65+ /// Throws an error on non-finite values.
66+ case `throw`
67+
68+ /// Encodes non-finite values as string literals.
69+ case convertToString( positiveInfinity: String , negativeInfinity: String , nan: String )
70+ }
71+
72+ /// The strategy to use for encoding non-conforming floating-point values.
73+ public var nonConformingFloatEncodingStrategy : NonConformingFloatEncodingStrategy = . null
74+
4875 /// Key folding mode.
4976 public enum KeyFolding : Hashable , Sendable {
5077 /// No key folding.
@@ -115,6 +142,8 @@ public final class TOONEncoder {
115142 /// Default settings:
116143 /// - `indent`: 2 spaces
117144 /// - `delimiter`: `.comma`
145+ /// - `negativeZeroEncodingStrategy`: `.normalize`
146+ /// - `nonConformingFloatEncodingStrategy`: `.null`
118147 /// - `keyFolding`: `.disabled`
119148 /// - `flattenDepth`: `Int.max`
120149 /// - `limits`: `.default`
@@ -148,6 +177,10 @@ public final class TOONEncoder {
148177 v = encoder. encodedValue
149178 }
150179
180+ if case . throw = nonConformingFloatEncodingStrategy {
181+ try validateNonConformingFloats ( in: v)
182+ }
183+
151184 var output : [ String ] = [ ]
152185 encodeValue ( v, output: & output, depth: 0 )
153186
@@ -646,12 +679,32 @@ public final class TOONEncoder {
646679 case . double( let doubleValue) :
647680 // Check for non-finite numbers first
648681 if !doubleValue. isFinite {
649- return " null "
682+ switch nonConformingFloatEncodingStrategy {
683+ case . null:
684+ return " null "
685+ case . throw:
686+ return " null "
687+ case . convertToString( let positiveInfinity, let negativeInfinity, let nan) :
688+ let literal : String
689+ if doubleValue. isNaN {
690+ literal = nan
691+ } else if doubleValue. sign == . minus {
692+ literal = negativeInfinity
693+ } else {
694+ literal = positiveInfinity
695+ }
696+ return encodeStringLiteral ( literal, delimiter: delimiter)
697+ }
650698 }
651699
652700 // Format numbers in decimal form without scientific notation
653701 if doubleValue == 0.0 && doubleValue. sign == . minus {
654- return " 0 " // Convert -0 to 0
702+ switch negativeZeroEncodingStrategy {
703+ case . normalize:
704+ return " 0 "
705+ case . preserve:
706+ return " -0 "
707+ }
655708 }
656709
657710 if let formatted = numberFormatter. string ( from: NSNumber ( value: doubleValue) ) {
@@ -677,6 +730,32 @@ public final class TOONEncoder {
677730 }
678731 }
679732
733+ private func validateNonConformingFloats( in value: Value ) throws {
734+ switch value {
735+ case . double( let doubleValue) :
736+ guard doubleValue. isFinite else {
737+ throw EncodingError . invalidValue (
738+ doubleValue,
739+ EncodingError . Context (
740+ codingPath: [ ] ,
741+ debugDescription: " Non-conforming float value: \( doubleValue) "
742+ )
743+ )
744+ }
745+ case . array( let array) :
746+ for item in array {
747+ try validateNonConformingFloats ( in: item)
748+ }
749+ case . object( let values, let keyOrder) :
750+ for key in keyOrder {
751+ guard let nestedValue = values [ key] else { continue }
752+ try validateNonConformingFloats ( in: nestedValue)
753+ }
754+ case . null, . bool, . int, . string, . date, . url, . data:
755+ break
756+ }
757+ }
758+
680759 private func encodeStringLiteral( _ value: String , delimiter: String = " , " )
681760 -> String
682761 {
0 commit comments