diff --git a/Sources/RawStructuredFieldValues/PseudoDecimal.swift b/Sources/RawStructuredFieldValues/PseudoDecimal.swift index e5cf92c..7687cb0 100644 --- a/Sources/RawStructuredFieldValues/PseudoDecimal.swift +++ b/Sources/RawStructuredFieldValues/PseudoDecimal.swift @@ -115,7 +115,7 @@ public struct PseudoDecimal: Hashable, Sendable { } } - private static func validate(mantissa: Int64, exponent: Int8) throws { + package static func validate(mantissa: Int64, exponent: Int8) throws { // We compare against our upper and lower bounds. The maximum allowable magnitude of the mantissa is contingent // on the exponent. switch exponent { diff --git a/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift b/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift index ac8530f..7859852 100644 --- a/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift +++ b/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift @@ -299,12 +299,7 @@ extension _StructuredFieldEncoder: SingleValueEncodingContainer { } func encode(_ data: Decimal) throws { - let significand = (data.significand.magnitude as NSNumber).intValue // Yes, really. - guard let exponent = Int8(exactly: data.exponent) else { - throw StructuredHeaderError.invalidIntegerOrDecimal - } - - let pd = PseudoDecimal(mantissa: significand * (data.isSignMinus ? -1 : 1), exponent: Int(exponent)) + let pd = try PseudoDecimal(data) try self.currentStackEntry.storage.insertBareItem(.decimal(pd)) } @@ -469,12 +464,7 @@ extension _StructuredFieldEncoder { } func append(_ value: Decimal) throws { - let significand = (value.significand.magnitude as NSNumber).intValue // Yes, really. - guard let exponent = Int8(exactly: value.exponent) else { - throw StructuredHeaderError.invalidIntegerOrDecimal - } - - let pd = PseudoDecimal(mantissa: significand * (value.isSignMinus ? -1 : 1), exponent: Int(exponent)) + let pd = try PseudoDecimal(value) try self.currentStackEntry.storage.appendBareItem(.decimal(pd)) } @@ -652,12 +642,7 @@ extension _StructuredFieldEncoder { } func encode(_ value: Decimal, forKey key: String) throws { - let significand = (value.significand.magnitude as NSNumber).intValue // Yes, really. - guard let exponent = Int8(exactly: value.exponent) else { - throw StructuredHeaderError.invalidIntegerOrDecimal - } - - let pd = PseudoDecimal(mantissa: significand * (value.isSignMinus ? -1 : 1), exponent: Int(exponent)) + let pd = try PseudoDecimal(value) try self.currentStackEntry.storage.insertBareItem(.decimal(pd), atKey: key) } @@ -994,3 +979,28 @@ extension Item { self.init(bareItem: partialItem.bareItem!, parameters: partialItem.parameters) } } + +extension PseudoDecimal { + fileprivate init(_ decimal: Decimal) throws { + var significand = (decimal.significand.magnitude as NSNumber).intValue // Yes, really. + var decimalExponent = decimal.exponent + + // Foundation stores trailing zeros as a positive exponent (100 is 1e2), so fold them into the significand. + while decimalExponent > 0 { + let (scaled, overflow) = significand.multipliedReportingOverflow(by: 10) + guard !overflow else { + throw StructuredHeaderError.invalidIntegerOrDecimal + } + significand = scaled + decimalExponent -= 1 + } + + guard let exponent = Int8(exactly: decimalExponent) else { + throw StructuredHeaderError.invalidIntegerOrDecimal + } + + let mantissa = significand * (decimal.isSignMinus ? -1 : 1) + try Self.validate(mantissa: Int64(mantissa), exponent: exponent) + self.init(mantissa: mantissa, exponent: Int(exponent)) + } +} diff --git a/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift b/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift index a6ce1b8..0392b7f 100644 --- a/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift +++ b/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift @@ -61,6 +61,16 @@ final class StructuredFieldEncoderTests: XCTestCase { ) } + func testEncodeDecimalWithPositiveExponent() throws { + // Foundation stores trailing zeros as a positive exponent, so 100 is 1e2. + let encoder = StructuredFieldValueEncoder() + XCTAssertEqual(Array("100.0".utf8), try encoder.encode(ItemField(Decimal(100)))) + XCTAssertEqual(Array("900000000000.0".utf8), try encoder.encode(ItemField(Decimal(string: "9e11")!))) + XCTAssertEqual(Array("-100.0, 100.0".utf8), try encoder.encode(List([Decimal(-100), Decimal(100)]))) + XCTAssertThrowsError(try encoder.encode(ItemField(Decimal(string: "1e12")!))) + XCTAssertThrowsError(try encoder.encode(List([Decimal(string: "1e12")!]))) + } + func testEncodeKeyedItemHeader() throws { struct KeyedItem: Equatable, StructuredFieldValue { static var structuredFieldType: StructuredFieldType {