diff --git a/Sources/IntegerUtilities/CMakeLists.txt b/Sources/IntegerUtilities/CMakeLists.txt index db167c77..e0213e21 100644 --- a/Sources/IntegerUtilities/CMakeLists.txt +++ b/Sources/IntegerUtilities/CMakeLists.txt @@ -9,7 +9,8 @@ See https://swift.org/LICENSE.txt for license information add_library(IntegerUtilities DivideWithRounding.swift - GCD.swift + GreatestCommonDivisor.swift + LeastCommonMultiple.swift Rotate.swift RoundingRule.swift SaturatingArithmetic.swift diff --git a/Sources/IntegerUtilities/GCD.swift b/Sources/IntegerUtilities/GCD.swift deleted file mode 100644 index 28e09bdb..00000000 --- a/Sources/IntegerUtilities/GCD.swift +++ /dev/null @@ -1,40 +0,0 @@ -//===--- GCD.swift --------------------------------------------*- swift -*-===// -// -// This source file is part of the Swift Numerics open source project -// -// Copyright (c) 2021-2024 Apple Inc. and the Swift Numerics project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -/// The [greatest common divisor][gcd] of `a` and `b`. -/// -/// If both inputs are zero, the result is zero. If one input is zero, the -/// result is the absolute value of the other input. -/// -/// The result must be representable within its type. In particular, the gcd -/// of a signed, fixed-width integer type's minimum with itself (or zero) -/// cannot be represented, and results in a trap. -/// -/// gcd(Int.min, Int.min) // Overflow error -/// gcd(Int.min, 0) // Overflow error -/// -/// [gcd]: https://en.wikipedia.org/wiki/Greatest_common_divisor -@inlinable -public func gcd(_ a: T, _ b: T) -> T { - var x = a - var y = b - if x.magnitude < y.magnitude { swap(&x, &y) } - // Avoid overflow when x = signed min, y = -1. - if y.magnitude == 1 { return 1 } - // Euclidean algorithm for GCD. It's worth using Lehmer instead for larger - // integer types, but for now this is good and dead-simple and faster than - // the other obvious choice, the binary algorithm. - while y != 0 { (x, y) = (y, x%y) } - // Try to convert result to T. - if let result = T(exactly: x.magnitude) { return result } - // If that fails, produce a diagnostic. - fatalError("GCD (\(x)) is not representable as \(T.self).") -} diff --git a/Sources/IntegerUtilities/GreatestCommonDivisor.swift b/Sources/IntegerUtilities/GreatestCommonDivisor.swift new file mode 100644 index 00000000..6372b575 --- /dev/null +++ b/Sources/IntegerUtilities/GreatestCommonDivisor.swift @@ -0,0 +1,35 @@ +//===--- GreatestCommonDivisor.swift --------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2021-2024 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// The [greatest common divisor][gcd] of `a` and `b`. +/// +/// If both inputs are zero, the result is zero. If one input is zero, the +/// result is the absolute value of the other input. +/// +/// [gcd]: https://en.wikipedia.org/wiki/Greatest_common_divisor +@inlinable +public func gcd(_ a: T, _ b: T) -> T.Magnitude { + var x = a + var y = b + + if x.magnitude < y.magnitude { + swap(&x, &y) + } + + // Euclidean algorithm for GCD. It's worth using Lehmer instead for larger + // integer types, but for now this is good and dead-simple and faster than + // the other obvious choice, the binary algorithm. + while y != 0 { + (x, y) = (y, x % y) + } + + return x.magnitude +} diff --git a/Sources/IntegerUtilities/LeastCommonMultiple.swift b/Sources/IntegerUtilities/LeastCommonMultiple.swift new file mode 100644 index 00000000..51f699bc --- /dev/null +++ b/Sources/IntegerUtilities/LeastCommonMultiple.swift @@ -0,0 +1,79 @@ +//===--- LeastCommonMultiple.swift --------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2021-2025 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// The [least common multiple][lcm] of `a` and `b`. +/// +/// If either input is zero, the result is zero. +/// +/// The result must be representable within its type. +/// +/// [lcm]: https://en.wikipedia.org/wiki/Least_common_multiple +@inlinable +public func lcm(_ a: T, _ b: T) -> T { + guard (a != 0) && (b != 0) else { + return 0 + } + + let lcm = a.magnitude / gcd(a, b) * b.magnitude + + guard let result = T(exactly: lcm) else { + fatalError("LCM \(lcm) is not representable as \(T.self).") + } + + return result +} + +/// The [least common multiple][lcm] of `a` and `b`. +/// +/// If either input is zero, the result is zero. +/// +/// Throws `LeastCommonMultipleOverflowError` containing the full width result if it is not representable within its type. +/// +/// [lcm]: https://en.wikipedia.org/wiki/Least_common_multiple +@inlinable +public func lcm(_ a: T, _ b: T) throws(LeastCommonMultipleOverflowError) -> T { + guard (a != 0) && (b != 0) else { + return 0 + } + + let reduced = a.magnitude / gcd(a, b) + + // We could use the multipliedFullWidth directly here, but we optimize instead for the non-throwing case because multipliedReportingOverflow is much faster. + let (partialValue, overflow) = reduced.multipliedReportingOverflow(by: b.magnitude) + + guard !overflow, let result = T(exactly: partialValue) else { + let fullWidth = reduced.multipliedFullWidth(by: b.magnitude) + + throw LeastCommonMultipleOverflowError(high: fullWidth.high, low: fullWidth.low) + } + + return result +} + + +/// Error thrown by `lcm`. +/// +/// Thrown when the result of the lcm isn't representable within its type. You can combine `high` and `low` into a double width integer to access the result. +/// +/// For example a `LeastCommonMultipleOverflowError` has `UInt8` as its Magnitude and contains the result in `high: UInt8` and `low: UInt8`. +/// These can be combined into a UInt16 result as `UInt16(high) << 8 | UInt16(low)`. +public struct LeastCommonMultipleOverflowError: Error, Equatable { + public let high: T.Magnitude + public let low: T.Magnitude + + @inlinable + public init(high: T.Magnitude, low: T.Magnitude) { + self.high = high + self.low = low + } +} + +extension LeastCommonMultipleOverflowError: Sendable where T.Magnitude: Sendable { } diff --git a/Tests/IntegerUtilitiesTests/CMakeLists.txt b/Tests/IntegerUtilitiesTests/CMakeLists.txt index 15376625..0e51d787 100644 --- a/Tests/IntegerUtilitiesTests/CMakeLists.txt +++ b/Tests/IntegerUtilitiesTests/CMakeLists.txt @@ -10,7 +10,8 @@ See https://swift.org/LICENSE.txt for license information add_library(IntegerUtilitiesTests DivideTests.swift DoubleWidthTests.swift - GCDTests.swift + GreatestCommonDivisorTests.swift + LeastCommonMultipleTests.swift RotateTests.swift SaturatingArithmeticTests.swift ShiftTests.swift) diff --git a/Tests/IntegerUtilitiesTests/GCDTests.swift b/Tests/IntegerUtilitiesTests/GCDTests.swift deleted file mode 100644 index 6400732c..00000000 --- a/Tests/IntegerUtilitiesTests/GCDTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -//===--- GCDTests.swift ---------------------------------------*- swift -*-===// -// -// This source file is part of the Swift Numerics open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift Numerics 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 -// -//===----------------------------------------------------------------------===// - -import IntegerUtilities -import XCTest - -final class IntegerUtilitiesGCDTests: XCTestCase { - func testGCDInt() { - XCTAssertEqual(gcd(0, 0), 0) - XCTAssertEqual(gcd(0, 1), 1) - XCTAssertEqual(gcd(1, 0), 1) - XCTAssertEqual(gcd(0, -1), 1) - XCTAssertEqual(gcd(1, 1), 1) - XCTAssertEqual(gcd(1, 2), 1) - XCTAssertEqual(gcd(2, 2), 2) - XCTAssertEqual(gcd(4, 2), 2) - XCTAssertEqual(gcd(6, 8), 2) - XCTAssertEqual(gcd(77, 91), 7) - XCTAssertEqual(gcd(24, -36), 12) - XCTAssertEqual(gcd(-24, -36), 12) - XCTAssertEqual(gcd(51, 34), 17) - XCTAssertEqual(gcd(64, 96), 32) - XCTAssertEqual(gcd(-64, 96), 32) - XCTAssertEqual(gcd(4*7*19, 27*25), 1) - XCTAssertEqual(gcd(16*315, 11*315), 315) - XCTAssertEqual(gcd(97*67*53*27*8, 83*67*53*9*32), 67*53*9*8) - XCTAssertEqual(gcd(Int.min, 2), 2) - - // TODO: Enable these when version compatibility allows. - // - // XCTExpectFailure{ gcd(0, Int.min) } - // XCTExpectFailure{ gcd(Int.min, 0) } - // XCTExpectFailure{ gcd(Int.min, Int.min) } - } -} diff --git a/Tests/IntegerUtilitiesTests/GreatestCommonDivisorTests.swift b/Tests/IntegerUtilitiesTests/GreatestCommonDivisorTests.swift new file mode 100644 index 00000000..c0f1e6c1 --- /dev/null +++ b/Tests/IntegerUtilitiesTests/GreatestCommonDivisorTests.swift @@ -0,0 +1,44 @@ +//===--- GreatestCommonDivisorTests.swift ---------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift Numerics 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 +// +//===----------------------------------------------------------------------===// + +import IntegerUtilities +import Testing + +@Suite("Greatest Common Divisor Tests") +struct GreatestCommonDivisorTests { + @Test("gcd") func gcdTests() async throws { + #expect(gcd(0, 0) == 0) + #expect(gcd(0, 1) == 1) + #expect(gcd(1, 0) == 1) + #expect(gcd(0, -1) == 1) + #expect(gcd(-1, 0) == 1) + #expect(gcd(1, 1) == 1) + #expect(gcd(1, 2) == 1) + #expect(gcd(2, 2) == 2) + #expect(gcd(4, 2) == 2) + #expect(gcd(6, 8) == 2) + #expect(gcd(77, 91) == 7) + #expect(gcd(24, -36) == 12) + #expect(gcd(-24, -36) == 12) + #expect(gcd(51, 34) == 17) + #expect(gcd(64, 96) == 32) + #expect(gcd(-64, 96) == 32) + #expect(gcd(4*7*19, 27*25) == 1) + #expect(gcd(16*315, 11*315) == 315) + #expect(gcd(97*67*53*27*8, 83*67*53*9*32) == 67*53*9*8) + #expect(gcd(Int.min, 2) == 2) + #expect(gcd(Int.max, Int.max) == Int.max) + #expect(gcd(0, Int.min) == Int.min.magnitude) + #expect(gcd(Int.min, 0) == Int.min.magnitude) + #expect(gcd(Int.min, Int.min) == Int.min.magnitude) + } +} diff --git a/Tests/IntegerUtilitiesTests/LeastCommonMultipleTests.swift b/Tests/IntegerUtilitiesTests/LeastCommonMultipleTests.swift new file mode 100644 index 00000000..65de37f3 --- /dev/null +++ b/Tests/IntegerUtilitiesTests/LeastCommonMultipleTests.swift @@ -0,0 +1,108 @@ +//===--- LeastCommonMultipleTests.swift ---------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift Numerics 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 +// +//===----------------------------------------------------------------------===// + +import IntegerUtilities +import Testing + +private func lcm_BinaryInteger(_ a: T, _ b: T) -> T { + IntegerUtilities.lcm(a,b) +} + +@Suite("Least Common Multiple Tests") +struct LeastCommonMultipleTests { + @Test("lcm") func lcm_BinaryIntegerTest() async throws { + + #expect(lcm_BinaryInteger(1024, 0) == 0) + #expect(lcm_BinaryInteger(0, 1024) == 0) + #expect(lcm_BinaryInteger(0, 0) == 0) + #expect(lcm_BinaryInteger(1024, 768) == 3072) + #expect(lcm_BinaryInteger(768, 1024) == 3072) + #expect(lcm_BinaryInteger(24, 18) == 72) + #expect(lcm_BinaryInteger(18, 24) == 72) + #expect(lcm_BinaryInteger(6930, 288) == 110880) + #expect(lcm_BinaryInteger(288, 6930) == 110880) + #expect(lcm_BinaryInteger(Int.max, 1) == Int.max) + #expect(lcm_BinaryInteger(1, Int.max) == Int.max) + + #if compiler(>=6.2) + try await #expect( + #require( + String( + bytes: #require(processExitsWith: .failure, observing: [\.standardErrorContent]) { + _ = lcm_BinaryInteger(Int.min, Int.min) + }.standardErrorContent, + encoding: .utf8 + ) + ).contains( + "Fatal error: LCM 9223372036854775808 is not representable as Int." + ) + ) + try await #expect( + #require( + String( + bytes: #require(processExitsWith: .failure, observing: [\.standardErrorContent]) { + _ = lcm_BinaryInteger(Int.min, 1) + }.standardErrorContent, + encoding: .utf8 + ) + ).contains( + "Fatal error: LCM 9223372036854775808 is not representable as Int." + ) + ) + try await #expect( + #require( + String( + bytes: #require(processExitsWith: .failure, observing: [\.standardErrorContent]) { + _ = lcm_BinaryInteger(1, Int.min) + }.standardErrorContent, + encoding: .utf8 + ) + ).contains( + "Fatal error: LCM 9223372036854775808 is not representable as Int." + ) + ) + await #expect(processExitsWith: .failure) { + _ = lcm_BinaryInteger(Int8.min, Int8.max) + } + #endif + } + + @Test("lcm") func lcm_FixedWidthIntegerTests() async throws { + func lcm(_ a: T, _ b: T) throws -> T { + try IntegerUtilities.lcm(a,b) + } + + #expect(try lcm(1024, 0) == 0) + #expect(try lcm(0, 1024) == 0) + #expect(try lcm(0, 0) == 0) + #expect(try lcm(1024, 768) == 3072) + #expect(try lcm(768, 1024) == 3072) + #expect(try lcm(24, 18) == 72) + #expect(try lcm(18, 24) == 72) + #expect(try lcm(6930, 288) == 110880) + #expect(try lcm(288, 6930) == 110880) + #expect(try lcm(Int.max, 1) == Int.max) + #expect(try lcm(1, Int.max) == Int.max) + #expect(throws: LeastCommonMultipleOverflowError(high: 0, low: Int.min.magnitude)) { + try lcm(Int.min, Int.min) + } + #expect(throws: LeastCommonMultipleOverflowError(high: 0, low: Int.min.magnitude)) { + try lcm(Int.min, 1) + } + #expect(throws: LeastCommonMultipleOverflowError(high: 0, low: Int.min.magnitude)) { + try lcm(1, Int.min) + } + #expect(throws: LeastCommonMultipleOverflowError(high: 63, low: 128)) { + try lcm(Int8.min, Int8.max) + } + } +}