Skip to content

Commit 24f57c2

Browse files
committed
Fix floating-point number handling #20
1 parent 951a813 commit 24f57c2

File tree

6 files changed

+144
-44
lines changed

6 files changed

+144
-44
lines changed

sources/Deprecated.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
extension CSVEncoder.Configuration {
3+
@available(*, deprecated, renamed: "nonConformingFloatStrategy")
4+
public var floatStrategy: Strategy.NonConformingFloat {
5+
self.nonConformingFloatStrategy
6+
}
7+
}
8+
9+
extension CSVDecoder.Configuration {
10+
@available(*, deprecated, renamed: "nonConformingFloatStrategy")
11+
public var floatStrategy: Strategy.NonConformingFloat {
12+
self.nonConformingFloatStrategy
13+
}
14+
}

sources/declarative/decodable/DecoderConfiguration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ extension CSVDecoder {
1010
/// The strategy to use when decoding Boolean values.
1111
public var boolStrategy: Strategy.BoolDecoding
1212
/// The strategy to use when dealing with non-conforming numbers.
13-
public var floatStrategy: Strategy.NonConformingFloat
13+
public var nonConformingFloatStrategy: Strategy.NonConformingFloat
1414
/// The strategy to use when decoding decimal values.
1515
public var decimalStrategy: Strategy.DecimalDecoding
1616
/// The strategy to use when decoding dates.
@@ -25,7 +25,7 @@ extension CSVDecoder {
2525
self.readerConfiguration = .init()
2626
self.nilStrategy = .empty
2727
self.boolStrategy = .insensitive
28-
self.floatStrategy = .throw
28+
self.nonConformingFloatStrategy = .throw
2929
self.decimalStrategy = .locale(nil)
3030
self.dateStrategy = .deferredToDate
3131
self.dataStrategy = .base64

sources/declarative/decodable/containers/SingleValueDecodingContainer.swift

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -120,36 +120,40 @@ extension ShadowDecoder.SingleValueContainer {
120120
}
121121

122122
func decode(_ type: Float.Type) throws -> Float {
123-
let strategy = self._decoder.source.configuration.floatStrategy
124-
return try self._lowlevelDecode {
125-
if let result = Double($0) {
126-
return abs(result) <= Double(Float.greatestFiniteMagnitude) ? Float(result) : nil
127-
} else if case .convert(let positiveInfinity, let negativeInfinity, let nanSymbol) = strategy {
128-
switch $0 {
129-
case positiveInfinity: return Float.infinity
130-
case negativeInfinity: return -Float.infinity
131-
case nanSymbol: return Float.nan
132-
default: break
123+
try self._lowlevelDecode {
124+
guard let result = Float($0), result.isFinite else {
125+
switch self._decoder.source.configuration.nonConformingFloatStrategy {
126+
case .throw: return nil
127+
case .convert(let positiveInfinity, let negativeInfinity, let nan):
128+
switch $0 {
129+
case positiveInfinity: return .infinity
130+
case negativeInfinity: return -.infinity
131+
case nan: return .nan
132+
default: return nil
133+
}
133134
}
134135
}
135-
return nil
136+
137+
return result
136138
}
137139
}
138140

139141
func decode(_ type: Double.Type) throws -> Double {
140-
let strategy = self._decoder.source.configuration.floatStrategy
141-
return try self._lowlevelDecode {
142-
if let result = Double($0) {
143-
return result
144-
} else if case .convert(let positiveInfinity, let negativeInfinity, let nanSymbol) = strategy {
145-
switch $0 {
146-
case positiveInfinity: return Double.infinity
147-
case negativeInfinity: return -Double.infinity
148-
case nanSymbol: return Double.nan
149-
default: break
142+
try self._lowlevelDecode {
143+
guard let result = Double($0), result.isFinite else {
144+
switch self._decoder.source.configuration.nonConformingFloatStrategy {
145+
case .throw: return nil
146+
case .convert(let positiveInfinity, let negativeInfinity, let nan):
147+
switch $0 {
148+
case positiveInfinity: return .infinity
149+
case negativeInfinity: return -.infinity
150+
case nan: return .nan
151+
default: return nil
152+
}
150153
}
151154
}
152-
return nil
155+
156+
return result
153157
}
154158
}
155159

sources/declarative/encodable/EncoderConfiguration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ extension CSVEncoder {
1010
/// The strategy to use when encoding Boolean values.
1111
public var boolStrategy: Strategy.BoolEncoding
1212
/// The strategy to use when dealing with non-conforming numbers (e.g. `NaN`, `+Infinity`, or `-Infinity`).
13-
public var floatStrategy: Strategy.NonConformingFloat
13+
public var nonConformingFloatStrategy: Strategy.NonConformingFloat
1414
/// The strategy to use when encoding decimal values.
1515
public var decimalStrategy: Strategy.DecimalEncoding
1616
/// The strategy to use when encoding dates.
@@ -25,7 +25,7 @@ extension CSVEncoder {
2525
self.nilStrategy = .empty
2626
self.boolStrategy = .deferredToString
2727
self.writerConfiguration = .init()
28-
self.floatStrategy = .throw
28+
self.nonConformingFloatStrategy = .throw
2929
self.decimalStrategy = .locale(nil)
3030
self.dateStrategy = .deferredToDate
3131
self.dataStrategy = .base64

sources/declarative/encodable/containers/SingleValueEncodingContainer.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,31 +104,37 @@ extension ShadowEncoder.SingleValueContainer {
104104
}
105105

106106
mutating func encode(_ value: Float) throws {
107-
let strategy = self._encoder.sink.configuration.floatStrategy
108107
try self._lowlevelEncoding {
109-
switch strategy {
110-
case .throw: throw CSVEncoder.Error._invalidFloatingPoint(value, codingPath: self.codingPath)
111-
case .convert(let positiveInfinity, let negativeInfinity, let nan):
112-
if value.isNaN {
113-
return nan
114-
} else if value.isInfinite {
115-
return (value < 0) ? negativeInfinity : positiveInfinity
116-
} else { fatalError() }
108+
if value.isNaN {
109+
switch self._encoder.sink.configuration.nonConformingFloatStrategy {
110+
case .throw: throw CSVEncoder.Error._invalidFloatingPoint(value, codingPath: self.codingPath)
111+
case .convert(_, _, let nan): return nan
112+
}
113+
} else if value.isInfinite {
114+
switch self._encoder.sink.configuration.nonConformingFloatStrategy {
115+
case .throw: throw CSVEncoder.Error._invalidFloatingPoint(value, codingPath: self.codingPath)
116+
case .convert(let positive, let negative, _): return (value < 0) ? negative : positive
117+
}
118+
} else {
119+
return value.description
117120
}
118121
}
119122
}
120123

121124
mutating func encode(_ value: Double) throws {
122-
let strategy = self._encoder.sink.configuration.floatStrategy
123125
try self._lowlevelEncoding {
124-
switch strategy {
125-
case .throw: throw CSVEncoder.Error._invalidFloatingPoint(value, codingPath: self.codingPath)
126-
case .convert(let positiveInfinity, let negativeInfinity, let nan):
127-
if value.isNaN {
128-
return nan
129-
} else if value.isInfinite {
130-
return (value < 0) ? negativeInfinity : positiveInfinity
131-
} else { fatalError() }
126+
if value.isNaN {
127+
switch self._encoder.sink.configuration.nonConformingFloatStrategy {
128+
case .throw: throw CSVEncoder.Error._invalidFloatingPoint(value, codingPath: self.codingPath)
129+
case .convert(_, _, let nan): return nan
130+
}
131+
} else if value.isInfinite {
132+
switch self._encoder.sink.configuration.nonConformingFloatStrategy {
133+
case .throw: throw CSVEncoder.Error._invalidFloatingPoint(value, codingPath: self.codingPath)
134+
case .convert(let positive, let negative, _): return (value < 0) ? negative : positive
135+
}
136+
} else {
137+
return value.description
132138
}
133139
}
134140
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import XCTest
2+
import CodableCSV
3+
4+
/// Tests checking support for encoding/decoding floating-points.
5+
final class CodableFloatingPointTests: XCTestCase {
6+
override func setUp() {
7+
self.continueAfterFailure = false
8+
}
9+
}
10+
11+
extension CodableFloatingPointTests {
12+
private struct _Student: Codable, Equatable {
13+
var name: String
14+
var age: Double
15+
}
16+
}
17+
18+
extension CodableFloatingPointTests {
19+
/// Test the regular floating-point encoding/decoding.
20+
func testRegularUsage() throws {
21+
let encoder = CSVEncoder { $0.headers = ["name", "age"] }
22+
let students: [_Student] = [
23+
.init(name: "Heidrun", age: 27.3),
24+
.init(name: "Gudrun", age: 62.0008),
25+
]
26+
27+
let data = try encoder.encode(students, into: Data.self)
28+
29+
let decoder = CSVDecoder { $0.headerStrategy = .firstLine }
30+
let result = try decoder.decode([_Student].self, from: data)
31+
XCTAssertEqual(students, result)
32+
}
33+
34+
// /// Test the regular floating-point encoding/decoding.
35+
// func testThrows() throws {
36+
// let encoder = CSVEncoder { $0.headers = ["name", "age"] }
37+
// let students: [_Student] = [
38+
// .init(name: "Heidrun", age: 27.3),
39+
// .init(name: "Gudrun", age: 62.0008),
40+
// .init(name: "Brunhilde", age: .infinity)
41+
// ]
42+
// XCTAssertThrowsError(try encoder.encode(students, into: Data.self))
43+
//
44+
// let decoder = CSVDecoder { $0.headerStrategy = .firstLine }
45+
// let data = """
46+
// name,age
47+
// Heidrun,27.3
48+
// Gudrun,62.0008
49+
// Brunhilde,inf
50+
// """.data(using: .utf8)!
51+
// XCTAssertThrowsError(try decoder.decode([_Student].self, from: data))
52+
// }
53+
54+
/// Test the regular floating-point encoding/decoding.
55+
func testConversion() throws {
56+
let students: [_Student] = [
57+
.init(name: "Heidrun", age: 27.3),
58+
.init(name: "Gudrun", age: 62.0008),
59+
.init(name: "Brunhilde", age: .infinity)
60+
]
61+
62+
let encoder = CSVEncoder {
63+
$0.headers = ["name", "age"]
64+
$0.nonConformingFloatStrategy = .convert(positiveInfinity: "+∞", negativeInfinity: "-∞", nan: "NaN")
65+
}
66+
67+
let data = try encoder.encode(students, into: Data.self)
68+
69+
let decoder = CSVDecoder {
70+
$0.headerStrategy = .firstLine
71+
$0.nonConformingFloatStrategy = .convert(positiveInfinity: "+∞", negativeInfinity: "-∞", nan: "NaN")
72+
}
73+
let result = try decoder.decode([_Student].self, from: data)
74+
XCTAssertEqual(students, result)
75+
}
76+
}

0 commit comments

Comments
 (0)