From ed9fc63ccb2c3d3446a6b94796b58f04178e962a Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Tue, 11 Jun 2024 11:02:24 -0400 Subject: [PATCH 1/3] Draft implementation of integer midpoint. --- Sources/IntegerUtilities/Midpoint.swift | 108 ++++++++++++++++++ .../IntegerUtilitiesTests/MidpointTests.swift | 39 +++++++ 2 files changed, 147 insertions(+) create mode 100644 Sources/IntegerUtilities/Midpoint.swift create mode 100644 Tests/IntegerUtilitiesTests/MidpointTests.swift diff --git a/Sources/IntegerUtilities/Midpoint.swift b/Sources/IntegerUtilities/Midpoint.swift new file mode 100644 index 00000000..36a95504 --- /dev/null +++ b/Sources/IntegerUtilities/Midpoint.swift @@ -0,0 +1,108 @@ +//===--- Midpoint.swift ---------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 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 average of `a` and `b`, rounded to an integer according to `rule`. +/// +/// Unlike commonly seen expressions such as `(a+b)/2` or `(a+b) >> 1` or +/// `a + (b-a)/2` (all of which may overflow for fixed-width integers), +/// this function never overflows, and the result is guaranteed to be +/// representable in the result type. +/// +/// The default rounding rule is `.down`, which matches the behavior of +/// `(a + b) >> 1` when that expression does not overflow. Rounding +/// `.towardZero` matches the behavior of `(a + b)/2` when that expression +/// does not overflow. All other rounding modes are supported. +/// +/// Rounding `.down` is generally most efficient; if you do not have a +/// reason to chose a specific other rounding rule, you should use the +/// default. +@inlinable +public func midpoint( + _ a: T, + _ b: T, + rounding rule: RoundingRule = .down +) -> T { + // Isolate bits in a + b with weight 2, and those with weight 1. + let twos = a & b + let ones = a ^ b + let floor = twos + ones >> 1 + let frac = ones & 1 + switch rule { + case .down: + return floor + case .up: + return floor + frac + case .towardZero: + return floor + (floor < 0 ? frac : 0) + case .toNearestOrAwayFromZero: + fallthrough + case .awayFromZero: + return floor + (floor >= 0 ? frac : 0) + case .toNearestOrEven: + return floor + (floor & frac) + case .toOdd: + return floor + (~floor & frac) + case .stochastically: + return floor + (Bool.random() ? frac : 0) + case .requireExact: + precondition(frac == 0) + return floor + } +} + +/// The average of `a` and `b`, rounded to an integer according to `rule`. +/// +/// Unlike commonly seen expressions such as `(a+b)/2` or `(a+b) >> 1` or +/// `a + (b-a)/2` (all of which may overflow), this function never overflows, +/// and the result is guaranteed to be representable in the result type. +/// +/// The default rounding rule is `.down`, which matches the behavior of +/// `(a + b) >> 1` when that expression does not overflow. Rounding +/// `.towardZero` matches the behavior of `(a + b)/2` when that expression +/// does not overflow. All other rounding modes are supported. +/// +/// Rounding `.down` is generally most efficient; if you do not have a +/// reason to chose a specific other rounding rule, you should use the +/// default. +@inlinable +public func midpoint( + _ a: T, + _ b: T, + rounding rule: RoundingRule = .down +) -> T { + // Isolate bits in a + b with weight 2, and those with weight 1 + let twos = a & b + let ones = a ^ b + let floor = twos &+ ones >> 1 + let frac = ones & 1 + switch rule { + case .down: + return floor + case .up: + return floor &+ frac + case .towardZero: + return floor &+ (floor < 0 ? frac : 0) + case .toNearestOrAwayFromZero: + fallthrough + case .awayFromZero: + return floor &+ (floor >= 0 ? frac : 0) + case .toNearestOrEven: + return floor &+ (floor & frac) + case .toOdd: + return floor &+ (~floor & frac) + case .stochastically: + return floor &+ (Bool.random() ? frac : 0) + case .requireExact: + precondition(frac == 0) + return floor + } +} + diff --git a/Tests/IntegerUtilitiesTests/MidpointTests.swift b/Tests/IntegerUtilitiesTests/MidpointTests.swift new file mode 100644 index 00000000..3361a8b8 --- /dev/null +++ b/Tests/IntegerUtilitiesTests/MidpointTests.swift @@ -0,0 +1,39 @@ +//===--- MidpointTests.swift ----------------------------------*- swift -*-===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2021 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 +// +//===----------------------------------------------------------------------===// + +import IntegerUtilities +import XCTest + +final class IntegerUtilitiesMidpointTests: XCTestCase { + func testMidpoint() { + for rule in [ + RoundingRule.down, + .up, + .towardZero, + .awayFromZero, + .toNearestOrEven, + .toNearestOrAwayFromZero, + .toOdd + ] { + for a in -128 ... 127 { + for b in -128 ... 127 { + let ref = (a + b).shifted(rightBy: 1, rounding: rule) + let tst = midpoint(Int8(a), Int8(b), rounding: rule) + if ref != tst { + print(rule, a, b, ref, tst, separator: "\t") + return + } + } + } + } + } +} From 64c062576c19f287494f444b2467fe2e90e2394c Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Mon, 1 Jul 2024 10:02:55 -0400 Subject: [PATCH 2/3] Fixup file header for MidpointTests --- Tests/IntegerUtilitiesTests/MidpointTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/IntegerUtilitiesTests/MidpointTests.swift b/Tests/IntegerUtilitiesTests/MidpointTests.swift index 3361a8b8..f600d10e 100644 --- a/Tests/IntegerUtilitiesTests/MidpointTests.swift +++ b/Tests/IntegerUtilitiesTests/MidpointTests.swift @@ -1,8 +1,8 @@ //===--- MidpointTests.swift ----------------------------------*- swift -*-===// // -// This source file is part of the Swift.org open source project +// This source file is part of the Swift Numerics open source project // -// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Copyright (c) 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 From 53efe07eacf92f4963f217416e1be667fb103c10 Mon Sep 17 00:00:00 2001 From: Stephen Canon Date: Mon, 22 Jul 2024 13:57:57 -0400 Subject: [PATCH 3/3] Adopt new rounding rules for midpoint Also corrects a typo and makes the deprecated toNearestOrAwayFromZero rule public for migration purposes, and removes the BinaryInteger.midpoint function in favor of the FixedWidthInteger overload. I'm open to reinstating the BI implementation in the future, but let's start with just FWI. --- Sources/IntegerUtilities/Midpoint.swift | 58 +++---------------- Sources/IntegerUtilities/RoundingRule.swift | 2 +- .../IntegerUtilitiesTests/MidpointTests.swift | 5 +- 3 files changed, 12 insertions(+), 53 deletions(-) diff --git a/Sources/IntegerUtilities/Midpoint.swift b/Sources/IntegerUtilities/Midpoint.swift index 36a95504..fa3b9759 100644 --- a/Sources/IntegerUtilities/Midpoint.swift +++ b/Sources/IntegerUtilities/Midpoint.swift @@ -9,55 +9,6 @@ // //===----------------------------------------------------------------------===// -/// The average of `a` and `b`, rounded to an integer according to `rule`. -/// -/// Unlike commonly seen expressions such as `(a+b)/2` or `(a+b) >> 1` or -/// `a + (b-a)/2` (all of which may overflow for fixed-width integers), -/// this function never overflows, and the result is guaranteed to be -/// representable in the result type. -/// -/// The default rounding rule is `.down`, which matches the behavior of -/// `(a + b) >> 1` when that expression does not overflow. Rounding -/// `.towardZero` matches the behavior of `(a + b)/2` when that expression -/// does not overflow. All other rounding modes are supported. -/// -/// Rounding `.down` is generally most efficient; if you do not have a -/// reason to chose a specific other rounding rule, you should use the -/// default. -@inlinable -public func midpoint( - _ a: T, - _ b: T, - rounding rule: RoundingRule = .down -) -> T { - // Isolate bits in a + b with weight 2, and those with weight 1. - let twos = a & b - let ones = a ^ b - let floor = twos + ones >> 1 - let frac = ones & 1 - switch rule { - case .down: - return floor - case .up: - return floor + frac - case .towardZero: - return floor + (floor < 0 ? frac : 0) - case .toNearestOrAwayFromZero: - fallthrough - case .awayFromZero: - return floor + (floor >= 0 ? frac : 0) - case .toNearestOrEven: - return floor + (floor & frac) - case .toOdd: - return floor + (~floor & frac) - case .stochastically: - return floor + (Bool.random() ? frac : 0) - case .requireExact: - precondition(frac == 0) - return floor - } -} - /// The average of `a` and `b`, rounded to an integer according to `rule`. /// /// Unlike commonly seen expressions such as `(a+b)/2` or `(a+b) >> 1` or @@ -84,13 +35,19 @@ public func midpoint( let floor = twos &+ ones >> 1 let frac = ones & 1 switch rule { + case .toNearestOrDown: + fallthrough case .down: return floor + case .toNearestOrUp: + fallthrough case .up: return floor &+ frac + case .toNearestOrZero: + fallthrough case .towardZero: return floor &+ (floor < 0 ? frac : 0) - case .toNearestOrAwayFromZero: + case .toNearestOrAway: fallthrough case .awayFromZero: return floor &+ (floor >= 0 ? frac : 0) @@ -105,4 +62,3 @@ public func midpoint( return floor } } - diff --git a/Sources/IntegerUtilities/RoundingRule.swift b/Sources/IntegerUtilities/RoundingRule.swift index 0dc9d83f..325c1a02 100644 --- a/Sources/IntegerUtilities/RoundingRule.swift +++ b/Sources/IntegerUtilities/RoundingRule.swift @@ -256,5 +256,5 @@ extension RoundingRule { /// > Deprecated: Use `.toNearestOrAway` instead. @inlinable @available(*, deprecated, renamed: "toNearestOrAway") - static var toNearestOrAwayFromZero: Self { .toNearestOrAway } + public static var toNearestOrAwayFromZero: Self { .toNearestOrAway } } diff --git a/Tests/IntegerUtilitiesTests/MidpointTests.swift b/Tests/IntegerUtilitiesTests/MidpointTests.swift index f600d10e..2b84cea8 100644 --- a/Tests/IntegerUtilitiesTests/MidpointTests.swift +++ b/Tests/IntegerUtilitiesTests/MidpointTests.swift @@ -20,8 +20,11 @@ final class IntegerUtilitiesMidpointTests: XCTestCase { .up, .towardZero, .awayFromZero, + .toNearestOrDown, + .toNearestOrUp, + .toNearestOrZero, + .toNearestOrAway, .toNearestOrEven, - .toNearestOrAwayFromZero, .toOdd ] { for a in -128 ... 127 {