-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(
HelperCoders
): added non-confirming floats decoding/encoding h…
…elpers
- Loading branch information
1 parent
bee7cc4
commit 6f8241a
Showing
3 changed files
with
178 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import MetaCodable | ||
|
||
/// An ``/MetaCodable/HelperCoder`` that helps decoding/encoding | ||
/// non-confirming floating point values. | ||
/// | ||
/// This type can be used to decode/encode exceptional | ||
/// floating-point values from a specified string representation. | ||
public struct NonConformingCoder<Float>: HelperCoder | ||
where Float: FloatingPoint & Codable & LosslessStringConvertible { | ||
/// The value representing positive infinity. | ||
private let positiveInfinity: String | ||
/// The value representing negative infinity. | ||
private let negativeInfinity: String | ||
/// The value representing not-a-number. | ||
private let nan: String | ||
|
||
/// Creates a new instance of ``/MetaCodable/HelperCoder`` that decodes/encodes | ||
/// exceptional floating-point values matching provided | ||
/// string representations. | ||
/// | ||
/// - Parameters: | ||
/// - positiveInfinity: The value representing positive infinity. | ||
/// - negativeInfinity: The value representing negative infinity. | ||
/// - nan: The value representing not-a-number. | ||
public init( | ||
positiveInfinity: String, | ||
negativeInfinity: String, | ||
nan: String | ||
) { | ||
self.positiveInfinity = positiveInfinity | ||
self.negativeInfinity = negativeInfinity | ||
self.nan = nan | ||
} | ||
|
||
/// Decodes exceptional floating-point values from a specified | ||
/// string representation. | ||
/// | ||
/// - Parameter decoder: The decoder to read data from. | ||
/// - Returns: The float value decoded. | ||
/// | ||
/// - Throws: `DecodingError.typeMismatch` if the encountered | ||
/// string representation can't be converted to float and | ||
/// doesn't match any of the boundaries of this instance. | ||
public func decode(from decoder: Decoder) throws -> Float { | ||
guard let strValue = try? String(from: decoder) else { | ||
return try .init(from: decoder) | ||
} | ||
|
||
switch strValue { | ||
case positiveInfinity: return .infinity | ||
case negativeInfinity: return -.infinity | ||
case nan: return .nan | ||
default: | ||
guard let value = Float(strValue) else { | ||
throw DecodingError.typeMismatch( | ||
String.self, | ||
.init( | ||
codingPath: decoder.codingPath, | ||
debugDescription: """ | ||
"\(strValue)" couldn't convert to float \(Float.self) | ||
""" | ||
) | ||
) | ||
} | ||
return value | ||
} | ||
} | ||
|
||
/// Encodes exceptional floating-point values to a specified | ||
/// string representation. | ||
/// | ||
/// If the float value doesn't match the boundaries actual | ||
/// value is encoded instead of string representation. | ||
/// | ||
/// - Parameters: | ||
/// - value: The float value to encode. | ||
/// - encoder: The encoder to write data to. | ||
public func encode(_ value: Float, to encoder: Encoder) throws { | ||
switch value { | ||
case .infinity: | ||
try positiveInfinity.encode(to: encoder) | ||
case -.infinity: | ||
try negativeInfinity.encode(to: encoder) | ||
case _ where value.isNaN: | ||
try nan.encode(to: encoder) | ||
default: | ||
try value.encode(to: encoder) | ||
} | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
Tests/MetaCodableTests/HelperCoders/NonConformingCoderTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import HelperCoders | ||
import MetaCodable | ||
import XCTest | ||
|
||
final class NonConformingCoderTests: XCTestCase { | ||
func testDecodingActualFloat() throws { | ||
let json = try json(5.5) | ||
let model = try JSONDecoder().decode(Model.self, from: json) | ||
XCTAssertEqual(model.float, 5.5) | ||
let encoded = try JSONEncoder().encode(model) | ||
let parsedModel = try JSONDecoder().decode(Model.self, from: encoded) | ||
XCTAssertEqual(parsedModel.float, 5.5) | ||
} | ||
|
||
func testDecodingStringifiedFloat() throws { | ||
let json = try json("5.5") | ||
let model = try JSONDecoder().decode(Model.self, from: json) | ||
XCTAssertEqual(model.float, 5.5) | ||
let encoded = try JSONEncoder().encode(model) | ||
let parsedModel = try JSONDecoder().decode(Model.self, from: encoded) | ||
XCTAssertEqual(parsedModel.float, 5.5) | ||
} | ||
|
||
func testDecodingPositiveInfinity() throws { | ||
let json = try json("➕♾️") | ||
let model = try JSONDecoder().decode(Model.self, from: json) | ||
XCTAssertEqual(model.float, .infinity) | ||
let encoded = try JSONEncoder().encode(model) | ||
let parsedModel = try JSONDecoder().decode(Model.self, from: encoded) | ||
XCTAssertEqual(parsedModel.float, .infinity) | ||
} | ||
|
||
func testDecodingNegativeInfinity() throws { | ||
let json = try json("➖♾️") | ||
let model = try JSONDecoder().decode(Model.self, from: json) | ||
XCTAssertEqual(model.float, -.infinity) | ||
let encoded = try JSONEncoder().encode(model) | ||
let parsedModel = try JSONDecoder().decode(Model.self, from: encoded) | ||
XCTAssertEqual(parsedModel.float, -.infinity) | ||
} | ||
|
||
func testDecodingNotANumber() throws { | ||
let json = try json("😞") | ||
let model = try JSONDecoder().decode(Model.self, from: json) | ||
XCTAssertTrue(model.float.isNaN) | ||
let encoded = try JSONEncoder().encode(model) | ||
let parsedModel = try JSONDecoder().decode(Model.self, from: encoded) | ||
XCTAssertTrue(parsedModel.float.isNaN) | ||
} | ||
|
||
func testInvalidDecoding() throws { | ||
let json = try json("random") | ||
do { | ||
let _ = try JSONDecoder().decode(Model.self, from: json) | ||
XCTFail("Invalid string to float conversion") | ||
} catch {} | ||
} | ||
} | ||
|
||
fileprivate func json( | ||
_ float: some Codable, | ||
file: StaticString = #file, | ||
line: UInt = #line | ||
) throws -> Data { | ||
let quote = float is String ? "\"" : "" | ||
let jsonStr = """ | ||
{ | ||
"float": \(quote)\(float)\(quote) | ||
} | ||
""" | ||
return try XCTUnwrap( | ||
jsonStr.data(using: .utf8), | ||
file: file, line: line | ||
) | ||
} | ||
|
||
@Codable | ||
fileprivate struct Model { | ||
@CodedBy( | ||
NonConformingCoder<Double>( | ||
positiveInfinity: "➕♾️", | ||
negativeInfinity: "➖♾️", | ||
nan: "😞" | ||
) | ||
) | ||
let float: Double | ||
} |