From f75d91a4e97b048aed17f845e877607a64a1229b Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 18 Jul 2024 10:08:16 -0700 Subject: [PATCH 1/3] Add SPI imports for SwiftCorelibsFoundation usage of Decimal Also adds some tests for the exported functionality that SCL-F uses. --- .../Decimal/CMakeLists.txt | 3 +- .../Decimal/Decimal+Compatibility.swift | 469 ++++++++++++++++++ .../Decimal/Decimal+Conformances.swift | 124 +++++ .../Decimal/Decimal+Math.swift | 25 + .../Decimal/Decimal.swift | 35 +- .../Calendar/Calendar_ICU.swift | 6 +- .../Date+ICU.swift | 2 + .../DecimalTests.swift | 34 ++ .../JSONEncoderTests.swift | 1 + .../DecimalTests+Locale.swift | 68 +++ 10 files changed, 751 insertions(+), 16 deletions(-) create mode 100644 Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift create mode 100644 Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift diff --git a/Sources/FoundationEssentials/Decimal/CMakeLists.txt b/Sources/FoundationEssentials/Decimal/CMakeLists.txt index 81cade38a..2fdd8ccc7 100644 --- a/Sources/FoundationEssentials/Decimal/CMakeLists.txt +++ b/Sources/FoundationEssentials/Decimal/CMakeLists.txt @@ -14,4 +14,5 @@ target_sources(FoundationEssentials PRIVATE Decimal.swift Decimal+Conformances.swift - Decimal+Math.swift) + Decimal+Math.swift + Decimal+Compatibility.swift) diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift b/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift new file mode 100644 index 000000000..cf6686c2f --- /dev/null +++ b/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift @@ -0,0 +1,469 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// NSDecimal compatibility API + +#if FOUNDATION_FRAMEWORK +// For feature flag +internal import _ForSwiftFoundation +#endif + +#if FOUNDATION_FRAMEWORK +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Decimal { + public typealias RoundingMode = NSDecimalNumber.RoundingMode + public typealias CalculationError = NSDecimalNumber.CalculationError +} + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Decimal { + @available(swift, obsoleted: 4, message: "Please use arithmetic operators instead") + @_transparent + public mutating func add(_ other: Decimal) { + self += other + } + + @available(swift, obsoleted: 4, message: "Please use arithmetic operators instead") + @_transparent + public mutating func subtract(_ other: Decimal) { + self -= other + } + + @available(swift, obsoleted: 4, message: "Please use arithmetic operators instead") + @_transparent + public mutating func multiply(by other: Decimal) { + self *= other + } + + @available(swift, obsoleted: 4, message: "Please use arithmetic operators instead") + @_transparent + public mutating func divide(by other: Decimal) { + self /= other + } +} + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +extension Decimal : _ObjectiveCBridgeable { + @_semantics("convertToObjectiveC") + public func _bridgeToObjectiveC() -> NSDecimalNumber { + return NSDecimalNumber(decimal: self) + } + + public static func _forceBridgeFromObjectiveC(_ x: NSDecimalNumber, result: inout Decimal?) { + if !_conditionallyBridgeFromObjectiveC(x, result: &result) { + fatalError("Unable to bridge \(_ObjectiveCType.self) to \(self)") + } + } + + public static func _conditionallyBridgeFromObjectiveC(_ input: NSDecimalNumber, result: inout Decimal?) -> Bool { + result = input.decimalValue + return true + } + + @_effects(readonly) + public static func _unconditionallyBridgeFromObjectiveC(_ source: NSDecimalNumber?) -> Decimal { + guard let src = source else { return Decimal(_exponent: 0, _length: 0, _isNegative: 0, _isCompact: 0, _reserved: 0, _mantissa: (0, 0, 0, 0, 0, 0, 0, 0)) } + return src.decimalValue + } +} +#endif + +// MARK: - Bridging code to C functions +// We have one implementation function for each, and an entry point for both Darwin (cdecl, exported from the framework), and swift-corelibs-foundation (SPI here and available via that package as API) + +#if FOUNDATION_FRAMEWORK +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +public func pow(_ x: Decimal, _ y: Int) -> Decimal { + let result = try? x._power( + exponent: UInt(y), roundingMode: .plain + ) + return result ?? .nan +} +#else +@_spi(SwiftCorelibsFoundation) +public func _pow(_ x: Decimal, _ y: Int) -> Decimal { + let result = try? x._power( + exponent: UInt(y), roundingMode: .plain + ) + return result ?? .nan +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalAdd( + _ result: UnsafeMutablePointer, + _ lhs: UnsafePointer, + _ rhs: UnsafePointer, + _ roundingMode: Decimal.RoundingMode +) -> Decimal.CalculationError { + do { + let addition = try lhs.pointee._add( + rhs: rhs.pointee, roundingMode: roundingMode + ) + result.pointee = addition.result + if addition.lossOfPrecision { + return .lossOfPrecision + } else { + return .noError + } + } catch { + let converted = _convertError(error) + result.pointee = .nan + return converted + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalAdd") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalAdd(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalAdd(result, lhs, rhs, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalAdd(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalAdd(result, lhs, rhs, roundingMode) +} +#endif + + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalSubtract( + _ result: UnsafeMutablePointer, + _ lhs: UnsafePointer, + _ rhs: UnsafePointer, + _ roundingMode: Decimal.RoundingMode +) -> Decimal.CalculationError { + do { + let subtraction = try lhs.pointee._subtract( + rhs: rhs.pointee, roundingMode: roundingMode + ) + result.pointee = subtraction + return .noError + } catch { + let converted = _convertError(error) + result.pointee = .nan + return converted + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalSubtract") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalSubtract(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalSubtract(result, lhs, rhs, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalSubtract(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalSubtract(result, lhs, rhs, roundingMode) +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalMultiply( + _ result: UnsafeMutablePointer, + _ lhs: UnsafePointer, + _ rhs: UnsafePointer, + _ roundingMode: Decimal.RoundingMode +) -> Decimal.CalculationError { + do { + let product = try lhs.pointee._multiply( + by: rhs.pointee, roundingMode: roundingMode + ) + result.pointee = product + return .noError + } catch { + let converted = _convertError(error) + result.pointee = .nan + return converted + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalMultiply") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalMultiply(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalMultiply(result, lhs, rhs, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalMultiply(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalMultiply(result, lhs, rhs, roundingMode) +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalDivide( + _ result: UnsafeMutablePointer, + _ lhs: UnsafePointer, + _ rhs: UnsafePointer, + _ roundingMode: Decimal.RoundingMode +) -> Decimal.CalculationError { + do { + let product = try lhs.pointee._divide( + by: rhs.pointee, roundingMode: roundingMode + ) + result.pointee = product + return .noError + } catch { + let converted = _convertError(error) + result.pointee = .nan + return converted + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalDivide") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalDivide(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalDivide(result, lhs, rhs, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalDivide(_ result: UnsafeMutablePointer, _ lhs: UnsafePointer, _ rhs: UnsafePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalDivide(result, lhs, rhs, roundingMode) +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalPower( + _ result: UnsafeMutablePointer, + _ decimal: UnsafePointer, + _ exponent: Int, + _ roundingMode: Decimal.RoundingMode +) -> Decimal.CalculationError { + do { + let power = try decimal.pointee._power(exponent: UInt(exponent), roundingMode: roundingMode) + result.pointee = power + return .noError + } catch { + let converted = _convertError(error) + result.pointee = .nan + return converted + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalPower") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalPower(_ result: UnsafeMutablePointer, _ decimal: UnsafePointer, _ exponent: Int, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalPower(result, decimal, exponent, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalPower(_ result: UnsafeMutablePointer, _ decimal: UnsafePointer, _ exponent: Int, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalPower(result, decimal, exponent, roundingMode) +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalMultiplyByPowerOf10( + _ result: UnsafeMutablePointer, + _ decimal: UnsafePointer, + _ power: CShort, + _ roundingMode: Decimal.RoundingMode +) -> Decimal.CalculationError { + do { + let product = try decimal.pointee._multiplyByPowerOfTen(power: Int(power), roundingMode: roundingMode) + result.pointee = product + return .noError + } catch { + let converted = _convertError(error) + result.pointee = .nan + return converted + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalMultiplyByPowerOf10") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalMultiplyByPowerOf10(_ result: UnsafeMutablePointer, _ decimal: UnsafePointer, _ power: CShort, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalMultiplyByPowerOf10(result, decimal, power, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalMultiplyByPowerOf10(_ result: UnsafeMutablePointer, _ decimal: UnsafePointer, _ power: CShort, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalMultiplyByPowerOf10(result, decimal, power, roundingMode) +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalCompare( + _ lhs: UnsafePointer, + _ rhs: UnsafePointer +) -> ComparisonResult { + return Decimal._compare(lhs: lhs.pointee, rhs: rhs.pointee) +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalCompare") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalCompare(_ lhs: UnsafePointer, _ rhs: UnsafePointer) -> ComparisonResult { + __NSDecimalCompare(lhs, rhs) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalCompare(_ lhs: UnsafePointer, _ rhs: UnsafePointer) -> ComparisonResult { + __NSDecimalCompare(lhs, rhs) +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalRound( + _ result: UnsafeMutablePointer, + _ decimal: UnsafePointer, + _ scale: Int, + _ roundingMode: Decimal.RoundingMode +) { + do { + let rounded = try decimal.pointee._round( + scale: scale, + roundingMode: roundingMode + ) + result.pointee = rounded + } catch { + // Noop since this method does not + // return a calculation error + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalRound") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalRound(_ result: UnsafeMutablePointer, _ decimal: UnsafePointer, _ scale: Int, _ roundingMode: Decimal.RoundingMode) { + __NSDecimalRound(result, decimal, scale, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalRound(_ result: UnsafeMutablePointer, _ decimal: UnsafePointer, _ scale: Int, _ roundingMode: Decimal.RoundingMode) { + __NSDecimalRound(result, decimal, scale, roundingMode) +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalNormalize( + _ lhs: UnsafeMutablePointer, + _ rhs: UnsafeMutablePointer, + _ roundingMode: Decimal.RoundingMode +) -> Decimal.CalculationError { + do { + var a = lhs.pointee + var b = rhs.pointee + let lossPrecision = try Decimal._normalize( + a: &a, b: &b, roundingMode: roundingMode + ) + lhs.pointee = a + rhs.pointee = b + if lossPrecision { + return .lossOfPrecision + } + return .noError + } catch { + let converted = _convertError(error) + return converted + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalNormalize") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalNormalize(_ lhs: UnsafeMutablePointer, _ rhs: UnsafeMutablePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalNormalize(lhs, rhs, roundingMode) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalNormalize(_ lhs: UnsafeMutablePointer, _ rhs: UnsafeMutablePointer, _ roundingMode: Decimal.RoundingMode) -> Decimal.CalculationError { + __NSDecimalNormalize(lhs, rhs, roundingMode) +} +#endif + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalCompact") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalCompact(_ number: UnsafeMutablePointer) { + var value = number.pointee + value.compact() + number.pointee = value +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalCompact(_ number: UnsafeMutablePointer) { + var value = number.pointee + value.compact() + number.pointee = value +} +#endif + +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func __NSDecimalString( + _ decimal: UnsafePointer, + _ locale: Any? = nil +) -> String { + let useLocale = locale as? Locale + return decimal.pointee.toString(with: useLocale) +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("NSDecimalString") +@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) +@usableFromInline internal func NSDecimalString(_ decimal: UnsafePointer, _ locale: Any? = nil) -> String { + __NSDecimalString(decimal, locale) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSDecimalString(_ decimal: UnsafePointer, _ locale: Any? = nil) -> String { + __NSDecimalString(decimal, locale) +} +#endif + +internal func __NSStringToDecimal( + _ string: String, + processedLength: UnsafeMutablePointer, + result: UnsafeMutablePointer +) { + let parsed = Decimal.decimal( + from: string.utf8, + decimalSeparator: ".".utf8, + matchEntireString: false + ) + processedLength.pointee = parsed.processedLength + if let parsedResult = parsed.result { + result.pointee = parsedResult + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("_NSStringToDecimal") +internal func _NSStringToDecimal(_ string: String, processedLength: UnsafeMutablePointer, result: UnsafeMutablePointer) { + __NSStringToDecimal(string, processedLength: processedLength, result: result) +} +#else +@_spi(SwiftCorelibsFoundation) +public func _NSStringToDecimal(_ string: String, processedLength: UnsafeMutablePointer, result: UnsafeMutablePointer) { + __NSStringToDecimal(string, processedLength: processedLength, result: result) +} +#endif + +private func _convertError(_ error: any Error) -> Decimal.CalculationError { + guard let calculationError = error as? Decimal._CalculationError else { + return .noError + } + switch calculationError { + case .overflow: + return .overflow + case .underflow: + return .underflow + case .divideByZero: + return .divideByZero + } +} diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift b/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift index 4e0276c81..74d3078e2 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift @@ -14,6 +14,130 @@ internal import _ForSwiftFoundation #endif // FOUNDATION_FRAMEWORK +/* + // Could be silently inexact for float and double. + extension Scanner { + + public func scanDecimal(_ dcm: inout Decimal) -> Bool { + if let result = scanDecimal() { + dcm = result + return true + } else { + return false + } + } + + public func scanDecimal() -> Decimal? { + + var result = Decimal.zero + let string = self._scanString + let length = string.length + var buf = _NSStringBuffer(string: string, start: self._scanLocation, end: length) + var tooBig = false + let ds = (locale as? Locale ?? Locale.current).decimalSeparator?.first ?? Character(".") + buf.skip(_skipSet) + var neg = false + var ok = false + + if buf.currentCharacter == unichar(unicodeScalarLiteral: "-") || buf.currentCharacter == unichar(unicodeScalarLiteral: "+") { + ok = true + neg = buf.currentCharacter == unichar(unicodeScalarLiteral: "-") + buf.advance() + buf.skip(_skipSet) + } + + // build the mantissa + while let numeral = decimalValue(buf.currentCharacter) { + ok = true + if tooBig || multiplyBy10(&result,andAdd:numeral) != .noError { + tooBig = true + if result._exponent == Int32(Int8.max) { + repeat { + buf.advance() + } while decimalValue(buf.currentCharacter) != nil + return nil + } + result._exponent += 1 + } + buf.advance() + } + + // get the decimal point + if let us = UnicodeScalar(buf.currentCharacter), Character(us) == ds { + ok = true + buf.advance() + // continue to build the mantissa + while let numeral = decimalValue(buf.currentCharacter) { + if tooBig || multiplyBy10(&result,andAdd:numeral) != .noError { + tooBig = true + } else { + if result._exponent == Int32(Int8.min) { + repeat { + buf.advance() + } while decimalValue(buf.currentCharacter) != nil + return nil + } + result._exponent -= 1 + } + buf.advance() + } + } + + if buf.currentCharacter == unichar(unicodeScalarLiteral: "e") || buf.currentCharacter == unichar(unicodeScalarLiteral: "E") { + ok = true + var exponentIsNegative = false + var exponent: Int32 = 0 + + buf.advance() + if buf.currentCharacter == unichar(unicodeScalarLiteral: "-") { + exponentIsNegative = true + buf.advance() + } else if buf.currentCharacter == unichar(unicodeScalarLiteral: "+") { + buf.advance() + } + + while let numeral = decimalValue(buf.currentCharacter) { + exponent = 10 * exponent + Int32(numeral) + guard exponent <= 2*Int32(Int8.max) else { + return nil + } + + buf.advance() + } + + if exponentIsNegative { + exponent = -exponent + } + exponent += result._exponent + guard exponent >= Int32(Int8.min) && exponent <= Int32(Int8.max) else { + return nil + } + result._exponent = exponent + } + + // No valid characters have been seen upto this point so error out. + guard ok == true else { return nil } + + result.isNegative = neg + + // if we get to this point, and have NaN, then the input string was probably "-0" + // or some variation on that, and normalize that to zero. + if result.isNaN { + result = Decimal(0) + } + + result.compact() + self._scanLocation = buf.location + return result + } + + // Copied from Scanner.swift + private func decimalValue(_ ch: unichar) -> Int? { + guard let s = UnicodeScalar(ch), s.isASCII else { return nil } + return Character(s).wholeNumberValue + } + } + */ @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension Decimal : CustomStringConvertible { public init?(string: __shared String, locale: __shared Locale? = nil) { diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift index 68e0bd5c4..172454463 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Math.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Math.swift @@ -266,6 +266,19 @@ extension Decimal { result._exponent = secureExponent return result } + + internal func _multiplyBy10AndAdd( + number: UInt16 + ) throws -> Decimal { + do { + var result = try _multiply(byShort: 10) + result = try result._add(number) + return result + } catch { + throw _CalculationError.overflow + } + } + internal func _divide(by divisor: UInt16) throws -> (result: Decimal, remainder: UInt16) { let (resultValue, remainder) = try Self._integerDivideByShort( @@ -702,6 +715,18 @@ extension Decimal { } return value } + + #if FOUNDATION_FRAMEWORK + #else + @_spi(SwiftCorelibsFoundation) + public var _int64Value: Int64 { int64Value } + + @_spi(SwiftCorelibsFoundation) + public var _uint64Value: UInt64 { uint64Value } + + @_spi(SwiftCorelibsFoundation) + public var _doubleValue: Double { doubleValue } + #endif } // MARK: - Integer Mathmatics diff --git a/Sources/FoundationEssentials/Decimal/Decimal.swift b/Sources/FoundationEssentials/Decimal/Decimal.swift index b7cb2154f..6d385404f 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal.swift @@ -22,7 +22,8 @@ import ucrt @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) public struct Decimal: Sendable { - internal typealias Mantissa = (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16) + @_spi(SwiftCorelibsFoundation) + public typealias Mantissa = (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16) internal struct Storage: Sendable { var exponent: Int8 @@ -60,6 +61,7 @@ public struct Decimal: Sendable { self.storage.lengthFlagsAndReserved |= newLength // set the new length } } + // Bool internal var _isNegative: UInt32 { get { @@ -73,6 +75,7 @@ public struct Decimal: Sendable { } } } + // Bool internal var _isCompact: UInt32 { get { @@ -86,6 +89,7 @@ public struct Decimal: Sendable { } } } + // Only 18 bits internal var _reserved: UInt32 { get { @@ -117,7 +121,8 @@ public struct Decimal: Sendable { } } - internal init( + @_spi(SwiftCorelibsFoundation) + public init( _exponent: Int32 = 0, _length: UInt32, _isNegative: UInt32 = 0, @@ -126,17 +131,25 @@ public struct Decimal: Sendable { _mantissa: Mantissa ) { let length: UInt8 = (UInt8(truncatingIfNeeded: _length) & 0xF) << 4 - let isNagitive: UInt8 = UInt8(truncatingIfNeeded: _isNegative & 0x1) == 0 ? 0 : 0b00001000 + let isNegative: UInt8 = UInt8(truncatingIfNeeded: _isNegative & 0x1) == 0 ? 0 : 0b00001000 let isCompact: UInt8 = UInt8(truncatingIfNeeded: _isCompact & 0x1) == 0 ? 0 : 0b00000100 let reservedLeft: UInt8 = UInt8(truncatingIfNeeded: (_reserved & 0x3FFFF) >> 16) self.storage = .init( exponent: Int8(truncatingIfNeeded: _exponent), - lengthFlagsAndReserved: length | isNagitive | isCompact | reservedLeft, + lengthFlagsAndReserved: length | isNegative | isCompact | reservedLeft, reserved: UInt16(truncatingIfNeeded: _reserved & 0xFFFF), mantissa: _mantissa ) } + @_spi(SwiftCorelibsFoundation) + public init(mantissa: UInt64, exponent: Int16, isNegative: Bool) { + var d = Decimal(mantissa) + d._exponent += Int32(exponent) + d._isNegative = isNegative ? 1 : 0 + self = d + } + public init() { self.storage = .init( exponent: 0, @@ -170,7 +183,8 @@ extension Decimal { // MARK: - String extension Decimal { - internal func toString(with locale: Locale? = nil) -> String { + @_spi(SwiftCorelibsFoundation) + public func toString(with locale: Locale? = nil) -> String { if self.isNaN { return "NaN" } @@ -221,7 +235,8 @@ extension Decimal { return String(buffer.reversed()) } - internal static func decimal( + @_spi(SwiftCorelibsFoundation) + public static func decimal( from stringView: String.UTF8View, decimalSeparator: String.UTF8View, matchEntireString: Bool @@ -299,9 +314,7 @@ extension Decimal { } continue } - guard let product = try? multiplyBy10AndAdd( - result, - number: UInt16(digitValue) + guard let product = try? result._multiplyBy10AndAdd(number: UInt16(digitValue) ) else { tooBigToFit = true incrementExponent(&result) @@ -324,9 +337,7 @@ extension Decimal { guard !tooBigToFit else { continue } - guard let product = try? multiplyBy10AndAdd( - result, - number: UInt16(digitValue) + guard let product = try? result._multiplyBy10AndAdd(number: UInt16(digitValue) ) else { tooBigToFit = true continue diff --git a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift index 191cbc263..026945d62 100644 --- a/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift +++ b/Sources/FoundationInternationalization/Calendar/Calendar_ICU.swift @@ -18,10 +18,10 @@ import FoundationEssentials import Android #elseif canImport(Glibc) import Glibc -#endif - -#if canImport(CRT) +#elseif canImport(CRT) import CRT +#elseif canImport(Darwin) +import Darwin #endif internal import _FoundationICU diff --git a/Sources/FoundationInternationalization/Date+ICU.swift b/Sources/FoundationInternationalization/Date+ICU.swift index 3e253b672..3895915ea 100644 --- a/Sources/FoundationInternationalization/Date+ICU.swift +++ b/Sources/FoundationInternationalization/Date+ICU.swift @@ -19,6 +19,8 @@ internal import _FoundationICU import Android #elseif canImport(Glibc) import Glibc +#elseif canImport(Darwin) +import Darwin #endif /// Internal extensions on Date, for interop with ICU. diff --git a/Tests/FoundationEssentialsTests/DecimalTests.swift b/Tests/FoundationEssentialsTests/DecimalTests.swift index b89f921cb..3da2d9654 100644 --- a/Tests/FoundationEssentialsTests/DecimalTests.swift +++ b/Tests/FoundationEssentialsTests/DecimalTests.swift @@ -17,6 +17,7 @@ import TestSupport #if FOUNDATION_FRAMEWORK @testable import Foundation #else +@_spi(SwiftCorelibsFoundation) @testable import FoundationEssentials #endif @@ -1208,6 +1209,17 @@ final class DecimalTests : XCTestCase { XCTAssertNotEqual(x.nextUp, x) } + #if FOUNDATION_FRAMEWORK + #else + func test_toString() { + let decimal = Decimal(string: "-123456.789")! + XCTAssertEqual(decimal.toString(with: nil), "-123456.789") + let en = decimal.toString(with: Locale(identifier: "en_GB")) + XCTAssertEqual(en, "-123456.789") + let fr = decimal.toString(with: Locale(identifier: "fr_FR")) + XCTAssertEqual(fr, "-123456,789") + } + func test_int64Value() { XCTAssertEqual(Decimal(-1).int64Value, -1) XCTAssertEqual(Decimal(0).int64Value, 0) @@ -1230,4 +1242,26 @@ final class DecimalTests : XCTestCase { let pi = Decimal(Double.pi) XCTAssertEqual(pi.int64Value, 3) } + + func test_doubleValue() { + XCTAssertEqual(Decimal(0).doubleValue, 0) + XCTAssertEqual(Decimal(1).doubleValue, 1) + XCTAssertEqual(Decimal(-1).doubleValue, -1) + XCTAssertTrue(Decimal.nan.doubleValue.isNaN) + XCTAssertEqual(Decimal(UInt64.max).doubleValue, Double(1.8446744073709552e+19)) + } + + func test_decimalFromString() { + let string = "x123x" + let scanLocation = 1 + + let start = string.index(string.startIndex, offsetBy: scanLocation, limitedBy: string.endIndex)! + let substring = string[start.. Date: Fri, 19 Jul 2024 11:58:54 -0700 Subject: [PATCH 2/3] Rename and rearrange some internal vs SPI methods --- .../Decimal/Decimal+Compatibility.swift | 4 ++-- .../Decimal/Decimal+Conformances.swift | 4 ++-- .../FoundationEssentials/Decimal/Decimal.swift | 18 ++++++++++++++++-- .../JSON/JSONDecoder.swift | 2 +- .../DecimalTests.swift | 6 +++--- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift b/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift index cf6686c2f..3b0c45feb 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift @@ -410,7 +410,7 @@ public func _NSDecimalCompact(_ number: UnsafeMutablePointer) { _ locale: Any? = nil ) -> String { let useLocale = locale as? Locale - return decimal.pointee.toString(with: useLocale) + return decimal.pointee._toString(with: useLocale) } #if FOUNDATION_FRAMEWORK @@ -431,7 +431,7 @@ internal func __NSStringToDecimal( processedLength: UnsafeMutablePointer, result: UnsafeMutablePointer ) { - let parsed = Decimal.decimal( + let parsed = Decimal._decimal( from: string.utf8, decimalSeparator: ".".utf8, matchEntireString: false diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift b/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift index 74d3078e2..9c224a2ef 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift @@ -142,7 +142,7 @@ internal import _ForSwiftFoundation extension Decimal : CustomStringConvertible { public init?(string: __shared String, locale: __shared Locale? = nil) { let decimalSeparator = locale?.decimalSeparator ?? "." - guard let value = Decimal.decimal( + guard let value = Decimal._decimal( from: string.utf8, decimalSeparator: decimalSeparator.utf8, matchEntireString: false @@ -153,7 +153,7 @@ extension Decimal : CustomStringConvertible { } public var description: String { - return self.toString() + return self._toString() } } diff --git a/Sources/FoundationEssentials/Decimal/Decimal.swift b/Sources/FoundationEssentials/Decimal/Decimal.swift index 6d385404f..f664f685a 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal.swift @@ -183,8 +183,23 @@ extension Decimal { // MARK: - String extension Decimal { +#if FOUNDATION_FRAMEWORK +#else @_spi(SwiftCorelibsFoundation) public func toString(with locale: Locale? = nil) -> String { + _toString(with: locale) + } + + @_spi(SwiftCorelibsFoundation) + public static func decimal( + from stringView: String.UTF8View, + decimalSeparator: String.UTF8View, + matchEntireString: Bool + ) -> (result: Decimal?, processedLength: Int) { + _decimal(from: stringView, decimalSeparator: decimalSeparator, matchEntireString: matchEntireString) + } +#endif + internal func _toString(with locale: Locale? = nil) -> String { if self.isNaN { return "NaN" } @@ -235,8 +250,7 @@ extension Decimal { return String(buffer.reversed()) } - @_spi(SwiftCorelibsFoundation) - public static func decimal( + internal static func _decimal( from stringView: String.UTF8View, decimalSeparator: String.UTF8View, matchEntireString: Bool diff --git a/Sources/FoundationEssentials/JSON/JSONDecoder.swift b/Sources/FoundationEssentials/JSON/JSONDecoder.swift index 7bff04fd7..8fff08777 100644 --- a/Sources/FoundationEssentials/JSON/JSONDecoder.swift +++ b/Sources/FoundationEssentials/JSON/JSONDecoder.swift @@ -1074,7 +1074,7 @@ extension FixedWidthInteger { extension Decimal { init?(entire string: String) { - guard let value = Decimal.decimal( + guard let value = Decimal._decimal( from: string.utf8, decimalSeparator: ".".utf8, matchEntireString: true diff --git a/Tests/FoundationEssentialsTests/DecimalTests.swift b/Tests/FoundationEssentialsTests/DecimalTests.swift index 3da2d9654..9163d1878 100644 --- a/Tests/FoundationEssentialsTests/DecimalTests.swift +++ b/Tests/FoundationEssentialsTests/DecimalTests.swift @@ -112,10 +112,10 @@ final class DecimalTests : XCTestCase { func test_DescriptionWithLocale() { let decimal = Decimal(string: "-123456.789")! - XCTAssertEqual(decimal.toString(with: nil), "-123456.789") - let en = decimal.toString(with: Locale(identifier: "en_GB")) + XCTAssertEqual(decimal._toString(with: nil), "-123456.789") + let en = decimal._toString(with: Locale(identifier: "en_GB")) XCTAssertEqual(en, "-123456.789") - let fr = decimal.toString(with: Locale(identifier: "fr_FR")) + let fr = decimal._toString(with: Locale(identifier: "fr_FR")) XCTAssertEqual(fr, "-123456,789") } From 4191b7e157ba46a554d46edff35a5e5873b3a81e Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Fri, 19 Jul 2024 12:00:29 -0700 Subject: [PATCH 3/3] Remove obsolete comment --- .../Decimal/Decimal+Conformances.swift | 124 ------------------ .../DecimalTests+Locale.swift | 1 - 2 files changed, 125 deletions(-) diff --git a/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift b/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift index 9c224a2ef..264089263 100644 --- a/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift +++ b/Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift @@ -14,130 +14,6 @@ internal import _ForSwiftFoundation #endif // FOUNDATION_FRAMEWORK -/* - // Could be silently inexact for float and double. - extension Scanner { - - public func scanDecimal(_ dcm: inout Decimal) -> Bool { - if let result = scanDecimal() { - dcm = result - return true - } else { - return false - } - } - - public func scanDecimal() -> Decimal? { - - var result = Decimal.zero - let string = self._scanString - let length = string.length - var buf = _NSStringBuffer(string: string, start: self._scanLocation, end: length) - var tooBig = false - let ds = (locale as? Locale ?? Locale.current).decimalSeparator?.first ?? Character(".") - buf.skip(_skipSet) - var neg = false - var ok = false - - if buf.currentCharacter == unichar(unicodeScalarLiteral: "-") || buf.currentCharacter == unichar(unicodeScalarLiteral: "+") { - ok = true - neg = buf.currentCharacter == unichar(unicodeScalarLiteral: "-") - buf.advance() - buf.skip(_skipSet) - } - - // build the mantissa - while let numeral = decimalValue(buf.currentCharacter) { - ok = true - if tooBig || multiplyBy10(&result,andAdd:numeral) != .noError { - tooBig = true - if result._exponent == Int32(Int8.max) { - repeat { - buf.advance() - } while decimalValue(buf.currentCharacter) != nil - return nil - } - result._exponent += 1 - } - buf.advance() - } - - // get the decimal point - if let us = UnicodeScalar(buf.currentCharacter), Character(us) == ds { - ok = true - buf.advance() - // continue to build the mantissa - while let numeral = decimalValue(buf.currentCharacter) { - if tooBig || multiplyBy10(&result,andAdd:numeral) != .noError { - tooBig = true - } else { - if result._exponent == Int32(Int8.min) { - repeat { - buf.advance() - } while decimalValue(buf.currentCharacter) != nil - return nil - } - result._exponent -= 1 - } - buf.advance() - } - } - - if buf.currentCharacter == unichar(unicodeScalarLiteral: "e") || buf.currentCharacter == unichar(unicodeScalarLiteral: "E") { - ok = true - var exponentIsNegative = false - var exponent: Int32 = 0 - - buf.advance() - if buf.currentCharacter == unichar(unicodeScalarLiteral: "-") { - exponentIsNegative = true - buf.advance() - } else if buf.currentCharacter == unichar(unicodeScalarLiteral: "+") { - buf.advance() - } - - while let numeral = decimalValue(buf.currentCharacter) { - exponent = 10 * exponent + Int32(numeral) - guard exponent <= 2*Int32(Int8.max) else { - return nil - } - - buf.advance() - } - - if exponentIsNegative { - exponent = -exponent - } - exponent += result._exponent - guard exponent >= Int32(Int8.min) && exponent <= Int32(Int8.max) else { - return nil - } - result._exponent = exponent - } - - // No valid characters have been seen upto this point so error out. - guard ok == true else { return nil } - - result.isNegative = neg - - // if we get to this point, and have NaN, then the input string was probably "-0" - // or some variation on that, and normalize that to zero. - if result.isNaN { - result = Decimal(0) - } - - result.compact() - self._scanLocation = buf.location - return result - } - - // Copied from Scanner.swift - private func decimalValue(_ ch: unichar) -> Int? { - guard let s = UnicodeScalar(ch), s.isASCII else { return nil } - return Character(s).wholeNumberValue - } - } - */ @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) extension Decimal : CustomStringConvertible { public init?(string: __shared String, locale: __shared Locale? = nil) { diff --git a/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift b/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift index fce021d7d..c39b7ff16 100644 --- a/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift +++ b/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift @@ -14,7 +14,6 @@ import TestSupport #endif -// TODO: Reenable these tests once DateFormatStyle has been ported final class DecimalLocaleTests : XCTestCase { func test_stringWithLocale() { let en_US = Locale(identifier: "en_US")