Skip to content

Commit

Permalink
feat(HelperCoders): added non-confirming floats decoding/encoding h…
Browse files Browse the repository at this point in the history
…elpers
  • Loading branch information
soumyamahunt committed Nov 2, 2023
1 parent bee7cc4 commit 6f8241a
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 0 deletions.
1 change: 1 addition & 0 deletions Sources/HelperCoders/HelperCoders.docc/HelperCoders.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Level up ``/MetaCodable``'s generated implementations with helpers assisting com
- Decoding basic data type (i.e `Bool`, `Int`, `String`) from any other basic data types (i.e `Bool`, `Int`, `String`).
- Custom `Date` decoding/encoding approach, i.e. converting from UNIX timestamp, text formatted date etc.
- Custom `Data` decoding/encoding approach, i.e. converting from base64 text etc.
- Decoding/encoding non-confirming floats with text based infinity and not-a-number representations.

## Installation

Expand Down
90 changes: 90 additions & 0 deletions Sources/HelperCoders/NonConformingCoder.swift
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 Tests/MetaCodableTests/HelperCoders/NonConformingCoderTests.swift
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
}

0 comments on commit 6f8241a

Please sign in to comment.