Skip to content

Commit b542a92

Browse files
authored
Merge pull request #9 from swhitty/int-decoding-clamping
IntDecodingStrategy.clamping
2 parents 44e0d68 + 9c16e5c commit b542a92

File tree

3 files changed

+142
-21
lines changed

3 files changed

+142
-21
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ let any = try encoder.encode([1, 2, Int?.none, 3])
101101

102102
## Int Decoding Strategy
103103

104-
The decoding of [BinaryInteger](https://developer.apple.com/documentation/swift/binaryinteger) types (`Int`, `UInt` etc) can be adjusted via `intDecodingStrategy`.
104+
The decoding of [`BinaryInteger`](https://developer.apple.com/documentation/swift/binaryinteger) types (`Int`, `UInt` etc) can be adjusted via `intDecodingStrategy`.
105105

106106
The default strategy `IntDecodingStrategy.exact` ensures the source value is exactly represented by the decoded type allowing floating point values with no fractional part to be decoded:
107107

@@ -113,16 +113,26 @@ let values = try KeyValueDecoder().decode([Int8].self, from: [10, 20.0, -30.0, I
113113
_ = try KeyValueDecoder().decode(Int8.self, from: 1000])
114114
```
115115

116-
Values with a fractional part can also be decoded to integers by rounding with any [FloatingPointRoundingRule](https://developer.apple.com/documentation/swift/floatingpointroundingrule):
116+
Values with a fractional part can also be decoded to integers by rounding with any [`FloatingPointRoundingRule`](https://developer.apple.com/documentation/swift/floatingpointroundingrule):
117117

118118
```swift
119119
let decoder = KeyValueDecoder()
120-
decoder.intDecodingStrategy = .rounded(rule: .toNearestOrAwayFromZero)
120+
decoder.intDecodingStrategy = .rounding(rule: .toNearestOrAwayFromZero)
121121

122122
// [10, -21, 50]
123123
let values = try decoder.decode([Int].self, from: [10.1, -20.9, 50.00001]),
124124
```
125125

126+
Values can also be clamped to the representable range:
127+
128+
```swift
129+
let decoder = KeyValueDecoder()
130+
decoder.intDecodingStrategy = .clamping(roundingRule: .toNearestOrAwayFromZero)
131+
132+
// [10, 21, 127, -128]
133+
let values = try decoder.decode([Int8].self, from: [10, 20.5, 1000, -Double.infinity])
134+
```
135+
126136
## UserDefaults
127137
Encode and decode [`Codable`](https://developer.apple.com/documentation/swift/codable) types with [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults):
128138

Sources/KeyValueDecoder.swift

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ public final class KeyValueDecoder {
7676
case exact
7777

7878
/// Decodes all floating point numbers using the provided rounding rule.
79-
case rounded(rule: FloatingPointRoundingRule)
79+
case rounding(rule: FloatingPointRoundingRule)
80+
81+
/// Clamps all integers to their min / max.
82+
/// Floating point conversions are also clamped, rounded when a rule is provided
83+
case clamping(roundingRule: FloatingPointRoundingRule?)
8084
}
8185
}
8286

@@ -182,19 +186,19 @@ private extension KeyValueDecoder {
182186

183187
func getBinaryInteger<T: BinaryInteger>(of type: T.Type = T.self) throws -> T {
184188
if let binaryInt = value as? any BinaryInteger {
185-
guard let val = T(exactly: binaryInt) else {
189+
guard let val = T(from: binaryInt, using: strategy.integers) else {
186190
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "\(valueDescription) at \(codingPath.makeKeyPath()), cannot be exactly represented by \(type)")
187191
throw DecodingError.typeMismatch(type, context)
188192
}
189193
return val
190194
} else if let int64 = (value as? NSNumber)?.getInt64Value() {
191-
guard let val = T(exactly: int64) else {
195+
guard let val = T(from: int64, using: strategy.integers) else {
192196
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "\(valueDescription) at \(codingPath.makeKeyPath()), cannot be exactly represented by \(type)")
193197
throw DecodingError.typeMismatch(type, context)
194198
}
195199
return val
196-
} else if let double = getDoubleValue(from: value, using: strategy.integers) {
197-
guard let val = T(exactly: double) else {
200+
} else if let double = (value as? NSNumber)?.getDoubleValue() {
201+
guard let val = T(from: double, using: strategy.integers) else {
198202
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "\(valueDescription) at \(codingPath.makeKeyPath()), cannot be exactly represented by \(type)")
199203
throw DecodingError.typeMismatch(type, context)
200204
}
@@ -209,17 +213,19 @@ private extension KeyValueDecoder {
209213
}
210214
}
211215

212-
func getDoubleValue(from value: Any, using strategy: IntDecodingStrategy) -> Double? {
213-
guard let double = (value as? NSNumber)?.getDoubleValue() else {
214-
return nil
215-
}
216-
switch strategy {
217-
case .exact:
218-
return double
219-
case .rounded(rule: let rule):
220-
return double.rounded(rule)
221-
}
222-
}
216+
// func getDoubleValue(from value: Any, using strategy: IntDecodingStrategy) -> Double? {
217+
// guard let double = (value as? NSNumber)?.getDoubleValue() else {
218+
// return nil
219+
// }
220+
// switch strategy {
221+
// case .exact:
222+
// return double
223+
// case .rounded(rule: let rule):
224+
// return double.rounded(rule)
225+
// case .clamping(rule: let rule):
226+
// return double.rounded(rule)
227+
// }
228+
// }
223229

224230
func decode(_ type: Bool.Type) throws -> Bool {
225231
try getValue()
@@ -640,6 +646,42 @@ private extension KeyValueDecoder {
640646
}
641647
}
642648

649+
extension BinaryInteger {
650+
651+
init?(from source: Double, using strategy: KeyValueDecoder.IntDecodingStrategy) {
652+
switch strategy {
653+
case .exact:
654+
self.init(exactly: source)
655+
case .rounding(rule: let rule):
656+
self.init(exactly: source.rounded(rule))
657+
case .clamping(roundingRule: let rule):
658+
self.init(clamping: source, rule: rule)
659+
}
660+
}
661+
662+
init?(from source: some BinaryInteger, using strategy: KeyValueDecoder.IntDecodingStrategy) {
663+
switch strategy {
664+
case .exact, .rounding:
665+
self.init(exactly: source)
666+
case .clamping:
667+
self.init(clamping: source)
668+
}
669+
}
670+
671+
private init?(clamping source: Double, rule: FloatingPointRoundingRule? = nil) {
672+
let rounded = rule.map(source.rounded) ?? source
673+
if let int = Int64(exactly: rounded) {
674+
self.init(clamping: int)
675+
} else if source > Double(Int64.max) {
676+
self.init(clamping: Int64.max)
677+
} else if source < Double(Int64.min) {
678+
self.init(clamping: Int64.min)
679+
} else {
680+
return nil
681+
}
682+
}
683+
}
684+
643685
extension NSNumber {
644686
func getInt64Value() -> Int64? {
645687
guard let numberID = getNumberTypeID() else { return nil }

Tests/KeyValueDecoderTests.swift

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ final class KeyValueDecoderTests: XCTestCase {
149149

150150
func testDecodesRounded_Ints() {
151151
let decoder = KeyValueDecoder()
152-
decoder.intDecodingStrategy = .rounded(rule: .toNearestOrAwayFromZero)
152+
decoder.intDecodingStrategy = .rounding(rule: .toNearestOrAwayFromZero)
153153

154154
XCTAssertEqual(
155155
try decoder.decode(Int16.self, from: 10.0),
@@ -247,7 +247,7 @@ final class KeyValueDecoderTests: XCTestCase {
247247

248248
func testDecodesRounded_UInts() {
249249
let decoder = KeyValueDecoder()
250-
decoder.intDecodingStrategy = .rounded(rule: .toNearestOrAwayFromZero)
250+
decoder.intDecodingStrategy = .rounding(rule: .toNearestOrAwayFromZero)
251251

252252
XCTAssertEqual(
253253
try decoder.decode(UInt16.self, from: 10.0),
@@ -902,6 +902,75 @@ final class KeyValueDecoderTests: XCTestCase {
902902
}
903903
}
904904

905+
func testInt_ClampsDoubles() {
906+
XCTAssertEqual(
907+
Int8(from: 1000.0, using: .clamping(roundingRule: nil)),
908+
Int8.max
909+
)
910+
XCTAssertEqual(
911+
Int8(from: -1000.0, using: .clamping(roundingRule: nil)),
912+
Int8.min
913+
)
914+
XCTAssertEqual(
915+
Int8(from: 100.0, using: .clamping(roundingRule: nil)),
916+
100
917+
)
918+
XCTAssertEqual(
919+
Int8(from: 100.5, using: .clamping(roundingRule: .toNearestOrAwayFromZero)),
920+
101
921+
)
922+
XCTAssertEqual(
923+
Int8(from: Double.infinity, using: .clamping(roundingRule: .toNearestOrAwayFromZero)),
924+
Int8.max
925+
)
926+
XCTAssertEqual(
927+
Int8(from: -Double.infinity, using: .clamping(roundingRule: .toNearestOrAwayFromZero)),
928+
Int8.min
929+
)
930+
XCTAssertNil(
931+
Int8(from: Double.nan, using: .clamping(roundingRule: nil))
932+
)
933+
}
934+
935+
func testUInt_ClampsDoubles() {
936+
XCTAssertEqual(
937+
UInt8(from: 1000.0, using: .clamping(roundingRule: nil)),
938+
UInt8.max
939+
)
940+
XCTAssertEqual(
941+
UInt8(from: -1000.0, using: .clamping(roundingRule: nil)),
942+
UInt8.min
943+
)
944+
XCTAssertEqual(
945+
UInt8(from: 100.0, using: .clamping(roundingRule: nil)),
946+
100
947+
)
948+
XCTAssertEqual(
949+
UInt8(from: 100.5, using: .clamping(roundingRule: .toNearestOrAwayFromZero)),
950+
101
951+
)
952+
XCTAssertEqual(
953+
UInt8(from: Double.infinity, using: .clamping(roundingRule: .toNearestOrAwayFromZero)),
954+
UInt8.max
955+
)
956+
XCTAssertEqual(
957+
UInt8(from: -Double.infinity, using: .clamping(roundingRule: .toNearestOrAwayFromZero)),
958+
UInt8.min
959+
)
960+
XCTAssertNil(
961+
UInt8(from: Double.nan, using: .clamping(roundingRule: nil))
962+
)
963+
964+
// [10, , 20.5, 1000, -Double.infinity]
965+
let decoder = KeyValueDecoder()
966+
decoder.intDecodingStrategy = .clamping(roundingRule: .toNearestOrAwayFromZero)
967+
XCTAssertEqual(
968+
try decoder.decode([Int8].self, from: [10, 20.5, 1000, -Double.infinity]),
969+
[10, 21, 127, -128]
970+
)
971+
972+
}
973+
905974
#if !os(WASI)
906975
func testPlistCompatibleDecoder() throws {
907976
let plistAny = try PropertyListEncoder.encodeAny([1, 2, Int?.none, 4])

0 commit comments

Comments
 (0)