Skip to content

Commit 3ee0781

Browse files
committed
Add new rounding modes: toNearestOr[Down,Up,Zero,Away]
These were omitted in the first pass over integer rounding rules, but are generally useful and make good sense to have available. In particular, toNearestOrUp is the natural mode for a lot of fixed-point arithmetic, and frequently has HW support on SIMD units, so it makes good sense to have a name for that. Once you add that, having nearestOrDown also makes sense, and then nearestOrZero should exist by analogy to nearestOrAway. Also beefed up testing and refactored the division rounding code somewhat.
1 parent 53afff1 commit 3ee0781

File tree

7 files changed

+654
-250
lines changed

7 files changed

+654
-250
lines changed

Sources/IntegerUtilities/DivideWithRounding.swift

Lines changed: 99 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -64,44 +64,58 @@ extension BinaryInteger {
6464
// disagree, we have to adjust q downward and r to match.
6565
if other.signum() != r.signum() { return q-1 }
6666
return q
67+
6768
case .up:
6869
// For rounding up, we want to have r have the opposite sign of
6970
// other; if not, we adjust q upward and r to match.
7071
if other.signum() == r.signum() { return q+1 }
7172
return q
73+
7274
case .towardZero:
7375
// This is exactly what the `/` operator did for us.
7476
return q
75-
case .toOdd:
76-
// If q is already odd, we're done.
77-
if q._lowWord & 1 == 1 { return q }
78-
// Otherwise, q is even but inexact; it was originally rounded toward
79-
// zero, so rounding away from zero instead will make it odd.
80-
fallthrough
77+
8178
case .awayFromZero:
82-
// To round away from zero, we apply the adjustments for both down
83-
// and up.
84-
if other.signum() != r.signum() { return q-1 }
85-
return q+1
86-
case .toNearestOrAwayFromZero:
87-
// For round to nearest or away, the condition we want to satisfy is
88-
// |r| <= |other/2|, with sign(q) != sign(r) when equality holds.
79+
break
80+
81+
case .toNearestOrDown:
82+
if r.magnitude > other.magnitude.shifted(rightBy: 1, rounding: .down) ||
83+
2*r.magnitude == other.magnitude && other.signum() != r.signum() {
84+
break
85+
}
86+
return q
87+
88+
case .toNearestOrUp:
89+
if r.magnitude > other.magnitude.shifted(rightBy: 1, rounding: .down) ||
90+
2*r.magnitude == other.magnitude && other.signum() == r.signum() {
91+
break
92+
}
93+
return q
94+
95+
case .toNearestOrZero:
96+
if r.magnitude <= other.magnitude.shifted(rightBy: 1, rounding: .down) {
97+
return q
98+
}
99+
// Otherwise, round q away from zero.
100+
101+
case .toNearestOrAway:
89102
if r.magnitude < other.magnitude.shifted(rightBy: 1, rounding: .up) {
90103
return q
91104
}
92-
// The (q,r) we have does not satisfy the to nearest or away condition;
93-
// round away from zero to choose the other representative of (q, r).
94-
if other.signum() != r.signum() { return q-1 }
95-
return q+1
105+
96106
case .toNearestOrEven:
97-
// For round to nearest or away, the condition we want to satisfy is
98-
// |r| <= |other/2|, with q even when equality holds.
99-
if r.magnitude > other.magnitude.shifted(rightBy: 1, rounding: .down) ||
100-
2*r.magnitude == other.magnitude && q._lowWord & 1 == 1 {
101-
if (other > 0) != (r > 0) { return q-1 }
102-
return q+1
107+
// First guarantee that |r| <= |other/2|; if not we have to round away
108+
// instead, so break to do that.
109+
if r.magnitude > other.magnitude.shifted(rightBy: 1, rounding: .down) ||
110+
2*r.magnitude == other.magnitude && !q.isMultiple(of: 2) {
111+
break
103112
}
104113
return q
114+
115+
case .toOdd:
116+
// If q is already odd, we have the correct result.
117+
if q._lowWord & 1 == 1 { return q }
118+
105119
case .stochastically:
106120
var qhi: UInt64
107121
var rhi: UInt64
@@ -121,9 +135,13 @@ extension BinaryInteger {
121135
return q+1
122136
}
123137
return q
138+
124139
case .requireExact:
125140
preconditionFailure("Division was not exact.")
126141
}
142+
143+
// We didn't have the right result, so round q away from zero.
144+
return other.signum() == r.signum() ? q+1 : q-1
127145
}
128146

129147
// TODO: make this API and make it possible to implement more efficiently.
@@ -221,46 +239,70 @@ extension SignedInteger {
221239
// For rounding down, we want to have r match the sign of other
222240
// rather than self; this means that if the signs of r and other
223241
// disagree, we have to adjust q downward and r to match.
224-
if other.signum() != r.signum() { return (q-1, r+other) }
225-
return (q, r)
242+
return other.signum() == r.signum() ? (q, r) : (q-1, r+other)
243+
226244
case .up:
227245
// For rounding up, we want to have r have the opposite sign of
228246
// other; if not, we adjust q upward and r to match.
229-
if other.signum() == r.signum() { return (q+1, r-other) }
230-
return (q, r)
247+
return other.signum() == r.signum() ? (q+1, r-other) : (q, r)
248+
231249
case .towardZero:
232250
// This is exactly what the `/` operator did for us.
233251
return (q, r)
234-
case .toOdd:
235-
// If q is already odd, we're done.
236-
if q._lowWord & 1 == 1 { return (q, r) }
237-
// Otherwise, q is even but inexact; it was originally rounded toward
238-
// zero, so rounding away from zero instead will make it odd.
239-
fallthrough
252+
240253
case .awayFromZero:
241-
// To round away from zero, we apply the adjustments for both down
242-
// and up.
243-
if other.signum() != r.signum() { return (q-1, r+other) }
244-
return (q+1, r-other)
245-
case .toNearestOrAwayFromZero:
246-
// For round to nearest or away, the condition we want to satisfy is
247-
// |r| <= |other/2|, with sign(q) != sign(r) when equality holds.
248-
if r.magnitude < other.magnitude.shifted(rightBy: 1, rounding: .up) {
254+
break
255+
256+
case .toNearestOrDown:
257+
// If |r| < |other/2|, we already rounded q to nearest. If the are
258+
// equal and q is negative, then we already broke the tie in the right
259+
// direction. However, we don't have access to the before-rounding q,
260+
// which may have rounded up to zero, losing the sign information, so
261+
// we have to look at other and r instead.
262+
if 2*r.magnitude < other.magnitude ||
263+
2*r.magnitude == other.magnitude && other.signum() == r.signum() {
264+
return (q, r)
265+
}
266+
267+
case .toNearestOrUp:
268+
// If |r| < |other/2|, we already rounded q to nearest. If the are
269+
// equal and q is non-negative, then we already broke the tie in the
270+
// right direction.
271+
if 2*r.magnitude < other.magnitude ||
272+
2*r.magnitude == other.magnitude && other.signum() != r.signum() {
273+
return (q, r)
274+
}
275+
276+
case .toNearestOrZero:
277+
// Check first if |r| <= |other/2|. If this holds, we have already
278+
// rounded q correctly. Because we're working with magnitudes, we can
279+
// safely compute 2r without worrying about overflow, even for fixed-
280+
// width types, because r cannot be .min (because |r| < |other| by
281+
// construction).
282+
if 2*r.magnitude <= other.magnitude {
249283
return (q, r)
250284
}
251-
// The (q,r) we have does not satisfy the to nearest or away condition;
252-
// round away from zero to choose the other representative of (q, r).
253-
if other.signum() != r.signum() { return (q-1, r+other) }
254-
return (q+1, r-other)
285+
286+
case .toNearestOrAway:
287+
// Check first if |r| < |other/2|. If this holds, we already rounded
288+
// q to nearest.
289+
if 2*r.magnitude < other.magnitude {
290+
return (q, r)
291+
}
292+
255293
case .toNearestOrEven:
256-
// For round to nearest or away, the condition we want to satisfy is
257-
// |r| <= |other/2|, with q even when equality holds.
258-
if r.magnitude > other.magnitude.shifted(rightBy: 1, rounding: .down) ||
259-
2*r.magnitude == other.magnitude && q._lowWord & 1 == 1 {
260-
if (other > 0) != (r > 0) { return (q-1, r+other) }
261-
return (q+1, r-other)
294+
// If |r| < |other/2|, we already rounded q to nearest. If the are
295+
// equal and q is even, then we already broke the tie in the right
296+
// direction.
297+
if 2*r.magnitude < other.magnitude ||
298+
2*r.magnitude == other.magnitude && q.isMultiple(of: 2) {
299+
return (q, r)
262300
}
263-
return (q, r)
301+
302+
case .toOdd:
303+
// If q is already odd, we have the correct result.
304+
if q._lowWord & 1 == 1 { return (q, r) }
305+
264306
case .stochastically:
265307
var qhi: UInt64
266308
var rhi: UInt64
@@ -280,9 +322,14 @@ extension SignedInteger {
280322
return (q+1, r-other)
281323
}
282324
return (q, r)
325+
283326
case .requireExact:
284327
preconditionFailure("Division was not exact.")
285328
}
329+
330+
// Fallthrough behavior is to round q away from zero and adjust r to
331+
// match.
332+
return other.signum() == r.signum() ? (q+1, r-other) : (q-1, r+other)
286333
}
287334
}
288335

Sources/IntegerUtilities/RoundingRule.swift

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22
//
33
// This source file is part of the Swift Numerics open source project
44
//
5-
// Copyright (c) 2021 Apple Inc. and the Swift Numerics project authors
5+
// Copyright (c) 2021-2024 Apple Inc. and the Swift Numerics project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
99
//
1010
//===----------------------------------------------------------------------===//
1111

12+
// TODO: it's unfortunate that we can't specify a custom random source
13+
// for the stochastic rounding rule, but I don't see a nice way to have
14+
// that share the API with the other rounding rules, because we'd then
15+
// have to take either the rule in-out or have an additional RNG/state
16+
// parameter. The same problem applies to rounding with dithering and
17+
// any other stateful rounding method. We should consider adding a
18+
// stateful rounding API down the road to support those use cases.
19+
1220
/// A rule that defines how to select one of the two representable results
1321
/// closest to a given value.
1422
///
@@ -17,38 +25,56 @@
1725
///
1826
/// Examples using rounding to integer to illustrate the various options:
1927
/// ```
28+
/// Directed rounding rules
29+
///
2030
/// value | down | up | towardZero | awayFromZero |
2131
/// =======+==============+==============+==============+==============+
22-
/// 1.5 | 1 | 2 | 1 | 2 |
32+
/// -1.5 | -2 | -1 | -1 | -2 |
2333
/// -------+--------------+--------------+--------------+--------------+
2434
/// -0.5 | -1 | 0 | 0 | -1 |
2535
/// -------+--------------+--------------+--------------+--------------+
26-
/// 0.3 | 0 | 1 | 0 | 1 |
36+
/// 0.5 | 0 | 1 | 0 | 1 |
37+
/// -------+--------------+--------------+--------------+--------------+
38+
/// 0.7 | 0 | 1 | 0 | 1 |
2739
/// -------+--------------+--------------+--------------+--------------+
28-
/// 2 | 2 | 2 | 2 | 2 |
40+
/// 1.2 | 1 | 2 | 1 | 2 |
2941
/// -------+--------------+--------------+--------------+--------------+
42+
/// 2.0 | 2 | 2 | 2 | 2 |
43+
/// -------+--------------+--------------+--------------+--------------+
44+
///
45+
/// toNearestOr... rounding rules
3046
///
31-
/// value | toOdd | toNearestOrAwayFromZero | toNearestOrEven |
32-
/// =======+==============+=========================+==================+
33-
/// 1.5 | 1 | 2 | 2 |
34-
/// -------+--------------+-------------------------+------------------+
35-
/// -0.5 | -1 | -1 | 0 |
36-
/// -------+--------------+-------------------------+------------------+
37-
/// 0.3 | 1 | 0 | 0 |
38-
/// -------+--------------+-------------------------+------------------+
39-
/// 2 | 2 | 2 | 2 |
40-
/// -------+--------------+-------------------------+------------------+
47+
/// value | orDown | orUp | orZero | orAway | orEven |
48+
/// =======+==========+==========+==========+==========+==========+
49+
/// -1.5 | -2 | -1 | -1 | -2 | -2 |
50+
/// -------+----------+----------+----------+----------+----------+
51+
/// -0.5 | -1 | 0 | 0 | -1 | 0 |
52+
/// -------+----------+----------+----------+----------+----------+
53+
/// 0.5 | 0 | 1 | 0 | 1 | 0 |
54+
/// -------+----------+----------+----------+----------+----------+
55+
/// 0.7 | 1 | 1 | 1 | 1 | 1 |
56+
/// -------+----------+----------+----------+----------+----------+
57+
/// 1.2 | 1 | 1 | 1 | 1 | 1 |
58+
/// -------+----------+----------+----------+----------+----------+
59+
/// 2.0 | 2 | 2 | 2 | 2 | 2 |
60+
/// -------+----------+----------+----------+----------+----------+
4161
///
42-
/// value | stochastically | requireExact |
43-
/// =======+=======================+================+
44-
/// 1.5 | 50% 1, 50% 2 | trap |
45-
/// -------+-----------------------+----------------+
46-
/// -0.5 | 50% -1, 50% 0 | trap |
47-
/// -------+-----------------------+----------------+
48-
/// 0.3 | 70% 0, 30% 1 | trap |
49-
/// -------+-----------------------+----------------+
50-
/// 2 | 2 | 2 |
51-
/// -------+-----------------------+----------------+
62+
/// Specialized rounding rules
63+
///
64+
/// value | toOdd | stochastically | requireExact |
65+
/// =======+==============+=======================+================+
66+
/// -1.5 | -1 | 50% -2, 50% -1 | trap |
67+
/// -------+--------------+-----------------------+----------------+
68+
/// -0.5 | -1 | 50% -1, 50% 0 | trap |
69+
/// -------+--------------+-----------------------+----------------+
70+
/// 0.5 | 1 | 50% 0, 50% 1 | trap |
71+
/// -------+--------------+-----------------------+----------------+
72+
/// 0.7 | 1 | 30% 0, 70% 1 | trap |
73+
/// -------+--------------+-----------------------+----------------+
74+
/// 1.2 | 1 | 80% 1, 20% 2 | trap |
75+
/// -------+--------------+-----------------------+----------------+
76+
/// 2.0 | 2 | 2 | 2 |
77+
/// -------+--------------+-----------------------+----------------+
5278
/// ```
5379
public enum RoundingRule {
5480
/// Produces the closest representable value that is less than or equal
@@ -113,18 +139,54 @@ public enum RoundingRule {
113139
/// even though 2 is even, because 4/2 is exactly 2 and no rounding occurs.
114140
case toOdd
115141

142+
/// Produces the representable value that is closest to the value being
143+
/// rounded. If two values are equally close, the one that is less than
144+
/// the value being rounded is chosen.
145+
///
146+
/// Examples:
147+
/// - `(-4).divided(by: 3, rounding: .toNearestOrDown)`
148+
/// is `-1`, because –4/3 = –1.3̅ is closer to –1 than it is to –2.
149+
///
150+
/// - `5.shifted(rightBy: 1, rounding: .toNearestOrDown)` is `2`,
151+
/// because 5/2 = 2.5 is equally close to 2 and 3, and 2 is less.
152+
case toNearestOrDown
153+
154+
/// Produces the representable value that is closest to the value being
155+
/// rounded. If two values are equally close, the one that is greater than
156+
/// the value being rounded is chosen.
157+
///
158+
/// Examples:
159+
/// - `(-4).divided(by: 3, rounding: .toNearestOrUp)`
160+
/// is `-1`, because –4/3 = –1.3̅ is closer to –1 than it is to –2.
161+
///
162+
/// - `5.shifted(rightBy: 1, rounding: .toNearestOrUp)` is `3`,
163+
/// because 5/2 = 2.5 is equally close to 2 and 3, and 3 is greater.
164+
case toNearestOrUp
165+
166+
/// Produces the representable value that is closest to the value being
167+
/// rounded. If two values are equally close, the one that has smaller
168+
/// magnitude is returned.
169+
///
170+
/// Examples:
171+
/// - `(-4).divided(by: 3, rounding: .toNearestOrZero)`
172+
/// is `-1`, because –4/3 = –1.3̅ is closer to –1 than it is to –2.
173+
///
174+
/// - `5.shifted(rightBy: 1, rounding: .toNearestOrZero)` is `3`,
175+
/// because 5/2 = 2.5 is equally close to 2 and 3, and 2 is closer to zero.
176+
case toNearestOrZero
177+
116178
/// Produces the representable value that is closest to the value being
117179
/// rounded. If two values are equally close, the one that has greater
118180
/// magnitude is returned.
119181
///
120182
/// Examples:
121-
/// - `(-4).divided(by: 3, rounding: .toNearestOrAwayFromZero)`
183+
/// - `(-4).divided(by: 3, rounding: .toNearestOrAway)`
122184
/// is `-1`, because –4/3 = –1.3̅ is closer to –1 than it is to –2.
123185
///
124-
/// - `5.shifted(rightBy: 1, rounding: .toNearestOrAwayFromZero)` is `3`,
186+
/// - `5.shifted(rightBy: 1, rounding: .toNearestOrAway)` is `3`,
125187
/// because 5/2 = 2.5 is equally close to 2 and 3, and 3 is further away
126188
/// from zero.
127-
case toNearestOrAwayFromZero
189+
case toNearestOrAway
128190

129191
/// Produces the representable value that is closest to the value being
130192
/// rounded. If two values are equally close, the one whose least
@@ -185,3 +247,14 @@ public enum RoundingRule {
185247
/// because –4/3 = –1.3̅ is not an integer.
186248
case requireExact
187249
}
250+
251+
extension RoundingRule {
252+
/// Produces the representable value that is closest to the value being
253+
/// rounded. If two values are equally close, the one that has greater
254+
/// magnitude is returned.
255+
///
256+
/// > Deprecated: Use `.toNearestOrAway` instead.
257+
@inlinable
258+
@available(*, deprecated, renamed: "toNearestOrAway")
259+
static var toNearestOrAwayFromZero: Self { .toNearestOrAway }
260+
}

0 commit comments

Comments
 (0)