diff --git a/Sources/DataTransferObjects/QueryResult/QueryResult.swift b/Sources/DataTransferObjects/QueryResult/QueryResult.swift index 9f9924b..59b218c 100644 --- a/Sources/DataTransferObjects/QueryResult/QueryResult.swift +++ b/Sources/DataTransferObjects/QueryResult/QueryResult.swift @@ -50,16 +50,67 @@ public struct TimeSeriesQueryResult: Codable, Hashable, Equatable { public let rows: [TimeSeriesQueryResultRow] } +/// Wrapper around the Double type that also accepts encoding and decoding as "Infinity" and "-Infinity" +public struct DoubleWrapper: Codable, Hashable, Equatable { + let value: Double + + public init(_ doubleValue: Double) { + value = doubleValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let stringValue = try? container.decode(String.self) { + if stringValue == "Infinity" { + value = Double.infinity + } else if stringValue == "-Infinity" { + value = -Double.infinity + } else { + guard let parsedDoubleValue = NumberFormatter().number(from: stringValue)?.doubleValue else { + throw DecodingError.dataCorrupted(.init( + codingPath: [], + debugDescription: "Could not parse value as Double", + underlyingError: nil + )) + } + + value = parsedDoubleValue + } + + return + } + + value = try container.decode(Double.self) + } + + enum CodingKeys: CodingKey { + case value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + if value == Double.infinity { + try container.encode("Infinity") + } else if value == -Double.infinity { + try container.encode("-Infinity") + } else { + try container.encode(value) + } + } +} + /// Time series queries return an array of JSON objects, where each object represents a value as described in the time-series query. /// For instance, the daily average of a dimension for the last one month. public struct TimeSeriesQueryResultRow: Codable, Hashable, Equatable { - public init(timestamp: Date, result: [String: Double]) { + public init(timestamp: Date, result: [String: DoubleWrapper]) { self.timestamp = timestamp self.result = result } public let timestamp: Date - public let result: [String: Double] + public let result: [String: DoubleWrapper] } /// GroupBy queries return an array of JSON objects, where each object represents a grouping as described in the group-by query. diff --git a/Tests/QueryResultTests/QueryResultTests.swift b/Tests/QueryResultTests/QueryResultTests.swift index 753e866..d7c04f3 100644 --- a/Tests/QueryResultTests/QueryResultTests.swift +++ b/Tests/QueryResultTests/QueryResultTests.swift @@ -15,8 +15,10 @@ class QueryResultTests: XCTestCase { let exampleQueryResult = QueryResult.timeSeries( TimeSeriesQueryResult(rows: [ - TimeSeriesQueryResultRow(timestamp: randomDate - 3600, result: ["test": 11]), - TimeSeriesQueryResultRow(timestamp: randomDate, result: ["test": 12]), + TimeSeriesQueryResultRow(timestamp: randomDate - 3600, result: ["test": DoubleWrapper(11)]), + TimeSeriesQueryResultRow(timestamp: randomDate, result: ["test": DoubleWrapper(12)]), + TimeSeriesQueryResultRow(timestamp: randomDate, result: ["test": DoubleWrapper(Double.infinity)]), + TimeSeriesQueryResultRow(timestamp: randomDate, result: ["test": DoubleWrapper(-Double.infinity)]), ] ) ) @@ -27,7 +29,9 @@ class QueryResultTests: XCTestCase { { "rows": [ {"result":{"test":11},"timestamp":"2021-10-21T11:00:00+0000"}, - {"result":{"test":12},"timestamp":"2021-10-21T12:00:00+0000"} + {"result":{"test":12},"timestamp":"2021-10-21T12:00:00+0000"}, + {"result":{"test":"Infinity"},"timestamp":"2021-10-21T12:00:00+0000"}, + {"result":{"test":"-Infinity"},"timestamp":"2021-10-21T12:00:00+0000"} ], "type":"timeSeriesResult" } @@ -83,6 +87,30 @@ class QueryResultTests: XCTestCase { let decodedResult = try JSONDecoder.telemetryDecoder.decode(TimeSeriesQueryResultRow.self, from: exampleResult.data(using: .utf8)!) - XCTAssertEqual(decodedResult.result, ["d0": 1_609_459_200_000]) + XCTAssertEqual(decodedResult.result, ["d0": DoubleWrapper(1_609_459_200_000)]) + } + + func testDecodingInfinity() throws { + let exampleResult = """ + [ + {"timestamp":"2022-12-19T00:00:00.000Z","result":{"min":"Infinity","max":"-Infinity","mean":0.0}}, + {"timestamp":"2022-12-20T00:00:00.000Z","result":{"min":0.02,"max":5775.11,"mean":938.24}}, + ] + """ + .filter { !$0.isWhitespace } + + let decodedResult = try JSONDecoder.telemetryDecoder.decode([TimeSeriesQueryResultRow].self, from: exampleResult.data(using: .utf8)!) + + XCTAssertEqual(decodedResult.first?.result, [ + "mean": DoubleWrapper(0.0), + "max": DoubleWrapper(-Double.infinity), + "min": DoubleWrapper(Double.infinity), + ]) + + XCTAssertEqual(decodedResult.last?.result, [ + "mean": DoubleWrapper(938.24), + "max": DoubleWrapper(5775.11), + "min": DoubleWrapper(0.02), + ]) } }