diff --git a/Sources/IntegerUtilities/CMakeLists.txt b/Sources/IntegerUtilities/CMakeLists.txt index 6dcac644..e0213e21 100644 --- a/Sources/IntegerUtilities/CMakeLists.txt +++ b/Sources/IntegerUtilities/CMakeLists.txt @@ -10,6 +10,7 @@ See https://swift.org/LICENSE.txt for license information add_library(IntegerUtilities DivideWithRounding.swift GreatestCommonDivisor.swift + LeastCommonMultiple.swift Rotate.swift RoundingRule.swift SaturatingArithmetic.swift diff --git a/Sources/IntegerUtilities/LeastCommonMultiple.swift b/Sources/IntegerUtilities/LeastCommonMultiple.swift new file mode 100644 index 00000000..627d621c --- /dev/null +++ b/Sources/IntegerUtilities/LeastCommonMultiple.swift @@ -0,0 +1,123 @@ +//===--- 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 + } + + return T(a.magnitude / gcd(a.magnitude, b.magnitude) * b.magnitude) +} + +/// 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. +/// +/// > Note: For retrieving the result as `T` or the fullwidth result on overflow, calling `leastCommonMultiple()` is faster than calling +/// `leastCommonMultipleReportingOverflow()` followed by ` leastCommonMultipleFullWidth()`. +/// +/// [lcm]: https://en.wikipedia.org/wiki/Least_common_multiple +@inlinable +public func leastCommonMultiple(_ a: T, _ b: T) throws(LeastCommonMultipleOverflowError) -> T { + guard (a != 0) && (b != 0) else { + return 0 + } + + let reduced = a.magnitude / gcd(a.magnitude, b.magnitude) + + // 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 +} + +/// Returns the [least common multiple][lcm] of `a` and `b`, along with a Boolean value indicating whether overflow occurred in the operation. +/// +/// If either input is zero, the result is zero. +/// +/// - Returns: A tuple containing the result of the function along with a Boolean value indicating whether overflow occurred. If the overflow component is false, the partialValue component contains the entire result. If the +/// overflow component is true, an overflow occurred and the partialValue component contains the truncated result of the operation. +/// +/// [lcm]: https://en.wikipedia.org/wiki/Least_common_multiple +@inlinable +public func leastCommonMultipleReportingOverflow(_ a: T, _ b: T) -> (partialValue: T, overflow: Bool) { + guard (a != 0) && (b != 0) else { + return (partialValue: 0, overflow: false) + } + + let reduced = a.magnitude / gcd(a.magnitude, b.magnitude) + + let (partialValue, overflow) = reduced.multipliedReportingOverflow(by: b.magnitude) + + guard !overflow, let result = T(exactly: partialValue) else { + return (partialValue: T(truncatingIfNeeded: partialValue), overflow: true) + } + + return (partialValue: result, overflow: false) +} + +/// Returns a tuple containing the high and low parts of the result of the [least common multiple][lcm] of `a` and `b`. +/// +/// If either input is zero, the result is zero. +/// +/// You can combine `high` and `low` into a double width integer to access the result. +/// +/// For example `leastCommonMultipleFullWidth` 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)`. +/// +/// - Returns: A tuple containing the high and low parts of the result. +/// +/// [lcm]: https://en.wikipedia.org/wiki/Least_common_multiple +@inlinable +public func leastCommonMultipleFullWidth(_ a: T, _ b: T) -> (high: T.Magnitude, low: T.Magnitude) { + guard (a != 0) && (b != 0) else { + return (high: 0, low: 0) + } + + let reduced = a.magnitude / gcd(a.magnitude, b.magnitude) + + return reduced.multipliedFullWidth(by: b.magnitude) +} + +/// Error thrown by `leastCommonMultiple`. +/// +/// 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 9fd0cd7a..0e51d787 100644 --- a/Tests/IntegerUtilitiesTests/CMakeLists.txt +++ b/Tests/IntegerUtilitiesTests/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(IntegerUtilitiesTests DivideTests.swift DoubleWidthTests.swift GreatestCommonDivisorTests.swift + LeastCommonMultipleTests.swift RotateTests.swift SaturatingArithmeticTests.swift ShiftTests.swift) diff --git a/Tests/IntegerUtilitiesTests/GreatestCommonDivisorTests.swift b/Tests/IntegerUtilitiesTests/GreatestCommonDivisorTests.swift index f22a3d36..92476a28 100644 --- a/Tests/IntegerUtilitiesTests/GreatestCommonDivisorTests.swift +++ b/Tests/IntegerUtilitiesTests/GreatestCommonDivisorTests.swift @@ -14,7 +14,7 @@ import IntegerUtilities import Testing struct `Greatest Common Divisor Tests` { - @Test func `gcd`() async throws { + @Test func `gcd()`() async throws { #expect(gcd(0, 0) == 0) #expect(gcd(0, 1) == 1) #expect(gcd(1, 0) == 1) diff --git a/Tests/IntegerUtilitiesTests/LeastCommonMultipleTests.swift b/Tests/IntegerUtilitiesTests/LeastCommonMultipleTests.swift new file mode 100644 index 00000000..150ef994 --- /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_ForceBinaryInteger(_ a: T, _ b: T) -> T { + IntegerUtilities.lcm(a,b) +} + +struct `Least Common Multiple Tests` { + @Test func `lcm()`() async throws { + #expect(lcm_ForceBinaryInteger(1024, 0) == 0) + #expect(lcm_ForceBinaryInteger(0, 1024) == 0) + #expect(lcm_ForceBinaryInteger(0, 0) == 0) + #expect(lcm_ForceBinaryInteger(1024, 768) == 3072) + #expect(lcm_ForceBinaryInteger(768, 1024) == 3072) + #expect(lcm_ForceBinaryInteger(24, 18) == 72) + #expect(lcm_ForceBinaryInteger(18, 24) == 72) + #expect(lcm_ForceBinaryInteger(6930, 288) == 110880) + #expect(lcm_ForceBinaryInteger(288, 6930) == 110880) + #expect(lcm_ForceBinaryInteger(Int.max, 1) == Int.max) + #expect(lcm_ForceBinaryInteger(1, Int.max) == Int.max) + await #expect(processExitsWith: .failure) { + _ = lcm_ForceBinaryInteger(Int.min, Int.min) + } + await #expect(processExitsWith: .failure) { + _ = lcm_ForceBinaryInteger(Int.min, 1) + } + await #expect(processExitsWith: .failure) { + _ = lcm_ForceBinaryInteger(1, Int.min) + } + await #expect(processExitsWith: .failure) { + _ = lcm_ForceBinaryInteger(Int8.min, Int8.max) + } + } + + @Test func `leastCommonMultiple()`() async throws { + #expect(try leastCommonMultiple(1024, 0) == 0) + #expect(try leastCommonMultiple(0, 1024) == 0) + #expect(try leastCommonMultiple(0, 0) == 0) + #expect(try leastCommonMultiple(1024, 768) == 3072) + #expect(try leastCommonMultiple(768, 1024) == 3072) + #expect(try leastCommonMultiple(24, 18) == 72) + #expect(try leastCommonMultiple(18, 24) == 72) + #expect(try leastCommonMultiple(6930, 288) == 110880) + #expect(try leastCommonMultiple(288, 6930) == 110880) + #expect(try leastCommonMultiple(Int.max, 1) == Int.max) + #expect(try leastCommonMultiple(1, Int.max) == Int.max) + #expect(throws: LeastCommonMultipleOverflowError(high: 0, low: Int.min.magnitude)) { + try leastCommonMultiple(Int.min, Int.min) + } + #expect(throws: LeastCommonMultipleOverflowError(high: 0, low: Int.min.magnitude)) { + try leastCommonMultiple(Int.min, 1) + } + #expect(throws: LeastCommonMultipleOverflowError(high: 0, low: Int.min.magnitude)) { + try leastCommonMultiple(1, Int.min) + } + #expect(throws: LeastCommonMultipleOverflowError(high: 63, low: 128)) { + try leastCommonMultiple(Int8.min, Int8.max) + } + } + + @Test func `leastCommonMultipleReportingOverflow()`() async throws { + #expect(leastCommonMultipleReportingOverflow(1024, 0) == (partialValue: 0, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(0, 1024) == (partialValue: 0, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(0, 0) == (partialValue: 0, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(1024, 768) == (partialValue: 3072, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(768, 1024) == (partialValue: 3072, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(24, 18) == (partialValue: 72, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(18, 24) == (partialValue: 72, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(6930, 288) == (partialValue: 110880, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(288, 6930) == (partialValue: 110880, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(Int.max, 1) == (partialValue: Int.max, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(1, Int.max) == (partialValue: Int.max, overflow: false)) + #expect(leastCommonMultipleReportingOverflow(Int.min, Int.min) == (partialValue: Int(truncatingIfNeeded: Int.min), overflow: true)) + #expect(leastCommonMultipleReportingOverflow(Int.min, 1) == (partialValue: Int(truncatingIfNeeded: Int.min), overflow: true)) + #expect(leastCommonMultipleReportingOverflow(1, Int.min) == (partialValue: Int(truncatingIfNeeded: Int.min), overflow: true)) + #expect(leastCommonMultipleReportingOverflow(Int8.min, Int8.max) == (partialValue: Int8(truncatingIfNeeded: Int16(Int8.min).magnitude * Int16(Int8.max).magnitude), overflow: true)) + } + + @Test func `leastCommonMultipleFullWidth()`() async throws { + #expect(leastCommonMultipleFullWidth(1024, 0) == (high: 0, low: 0)) + #expect(leastCommonMultipleFullWidth(0, 1024) == (high: 0, low: 0)) + #expect(leastCommonMultipleFullWidth(0, 0) == (high: 0, low: 0)) + #expect(leastCommonMultipleFullWidth(1024, 768) == (high: 0, low: 3072)) + #expect(leastCommonMultipleFullWidth(768, 1024) == (high: 0, low: 3072)) + #expect(leastCommonMultipleFullWidth(24, 18) == (high: 0, low: 72)) + #expect(leastCommonMultipleFullWidth(18, 24) == (high: 0, low: 72)) + #expect(leastCommonMultipleFullWidth(6930, 288) == (high: 0, low: 110880)) + #expect(leastCommonMultipleFullWidth(288, 6930) == (high: 0, low: 110880)) + #expect(leastCommonMultipleFullWidth(Int.max, 1) == (high: 0, low: Int.max.magnitude)) + #expect(leastCommonMultipleFullWidth(1, Int.max) == (high: 0, low: Int.max.magnitude)) + #expect(leastCommonMultipleFullWidth(Int.min, Int.min) == (high: 0, low: Int.min.magnitude)) + #expect(leastCommonMultipleFullWidth(Int.min, 1) == (high: 0, low: Int.min.magnitude)) + #expect(leastCommonMultipleFullWidth(1, Int.min) == (high: 0, low: Int.min.magnitude)) + #expect(leastCommonMultipleFullWidth(Int8.min, Int8.max) == (high: 63, low: 128)) + } +}