Skip to content

Commit 716e22f

Browse files
Merge pull request #218 from stephentyrone/saturating-arithmetic
Implement saturating arithmetic on FixedWidthInteger
2 parents 0bb0a9a + 35e15a3 commit 716e22f

File tree

10 files changed

+595
-29
lines changed

10 files changed

+595
-29
lines changed

Sources/IntegerUtilities/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ add_library(IntegerUtilities
1212
GCD.swift
1313
Rotate.swift
1414
RoundingRule.swift
15+
SaturatingArithmetic.swift
1516
ShiftWithRounding.swift)
1617
set_target_properties(IntegerUtilities PROPERTIES
1718
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

Sources/IntegerUtilities/DivideWithRounding.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,13 @@ extension BinaryInteger {
126126
}
127127
}
128128

129-
// TODO: make this API and make it possible to implement more
130-
// efficiently. Customization point on new/revised integer
131-
// protocol? Shouldn't have to go through .words.
129+
// TODO: make this API and make it possible to implement more efficiently.
130+
// Customization point on new/revised integer protocol? Shouldn't have to
131+
// go through .words.
132+
132133
/// The index of the most-significant set bit.
133134
///
134-
/// - Precondition: self is assumed to be non-zero (to be changed
135+
/// - Precondition: self is assumed to be non-zero (should be changed
135136
/// if/when this becomes API).
136137
@usableFromInline
137138
internal var _msb: Int {

Sources/IntegerUtilities/README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ _Note: This module is only present on `main`, and has not yet been stabilized an
44

55
## Utilities defined for `BinaryInteger`
66

7-
The following API are defined for all integer types conforming to `BinaryInteger`:
7+
The following API are defined for all integer types:
88

99
- The `gcd(_:_:)` free function implements the _Greatest Common Divisor_ operation.
1010

@@ -36,7 +36,33 @@ The following API are defined for signed integer types:
3636

3737
- The `rotated(right:)` and `rotated(left:)` methods implement _bitwise rotation_ for signed and unsigned integer types.
3838
The count parameter may be any `BinaryInteger` type.
39+
40+
### [Saturating Arithmetic][saturating]
41+
42+
The following saturating operations are defined as methods on `FixedWidthInteger`:
43+
44+
- `addingWithSaturation(_:)`
45+
- `subtractingWithSaturation(_:)`
46+
- `negatedWithSaturation(_:)`
47+
- `multipliedWithSaturation(by:)`
48+
- `shiftedWithSaturation(leftBy:rounding:)`
49+
50+
These implement _saturating arithmetic_.
51+
They are an alternative to the usual `+`, `-`, and `*` operators, which trap if the result cannot be represented in the argument type, and `&+`, `&-`, `&*`, and `<<`, which wrap out-of-range results modulo 2ⁿ for some n.
52+
Instead these methods clamp the result to the representable range of the type:
53+
```
54+
let x: Int8 = 84
55+
let y: Int8 = 100
56+
let a = x + y // traps due to overflow
57+
let b = x &+ y // wraps to -72
58+
let c = x.addingWithSaturation(y) // saturates to 127
59+
```
60+
61+
If you are using saturating arithmetic, you may also want to perform saturating conversions between integer types; this functionality is provided by the standard library via the [`init(clamping:)` API][clamping].
3962

4063
## Types
4164

4265
The `RoundingRule` enum is used with shift, division, and round operations to specify how to round their results to a representable value.
66+
67+
[saturating]: https://en.wikipedia.org/wiki/Saturation_arithmetic
68+
[clamping]: https://developer.apple.com/documentation/swift/binaryinteger/init(clamping:)

Sources/IntegerUtilities/RoundingRule.swift

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,84 @@
1414
///
1515
/// [Wikipedia](https://en.wikipedia.org/wiki/Rounding#Rounding_to_integer)
1616
/// provides a good overview of different rounding rules.
17+
///
18+
/// Examples using rounding to integer to illustrate the various options:
19+
/// ```
20+
/// value | down | up | towardZero | awayFromZero |
21+
/// =======+==============+==============+==============+==============+
22+
/// 1.5 | 1 | 2 | 1 | 2 |
23+
/// -------+--------------+--------------+--------------+--------------+
24+
/// -0.5 | -1 | 0 | 0 | -1 |
25+
/// -------+--------------+--------------+--------------+--------------+
26+
/// 0.3 | 0 | 1 | 0 | 1 |
27+
/// -------+--------------+--------------+--------------+--------------+
28+
/// 2 | 2 | 2 | 2 | 2 |
29+
/// -------+--------------+--------------+--------------+--------------+
30+
///
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+
/// -------+--------------+-------------------------+------------------+
41+
///
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+
/// -------+-----------------------+----------------+
52+
/// ```
1753
public enum RoundingRule {
1854
/// Produces the closest representable value that is less than or equal
1955
/// to the value being rounded.
2056
///
2157
/// This is the default rounding mode for integer shifts, including the
22-
/// shift operators defined in the standard library.
58+
/// shift operators defined in the standard library.
59+
///
60+
/// Examples:
61+
/// - `(-4).divided(by: 3, rounding: .down)` is `-2`, because –2 is the
62+
/// largest integer less than –4/3 = –1.3̅
63+
/// - `5.shifted(rightBy: 1, rounding: .down)` is `2`, because 2 is the
64+
/// largest integer less than 5/2 = 2.5.
2365
case down
2466

2567
/// Produces the closest representable value that is greater than or equal
2668
/// to the value being rounded.
69+
///
70+
/// Examples:
71+
/// - `(-4).divided(by: 3, rounding: .up)` is `-1`, because –1 is the
72+
/// smallest integer greater than –4/3 = –1.3̅
73+
/// - `5.shifted(rightBy: 1, rounding: .up)` is `3`, because 3 is the
74+
/// smallest integer greater than 5/2 = 2.5.
2775
case up
2876

2977
/// Produces the closest representable value whose magnitude is less than
3078
/// or equal to that of the value being rounded.
79+
///
80+
/// Examples:
81+
/// - `(-4).divided(by: 3, rounding: .towardZero)` is `-1`, because –1
82+
/// is the closest integer to –4/3 = –1.3̅ with smaller magnitude.
83+
/// - `5.shifted(rightBy: 1, rounding: .towardZero)` is `2`, because 2
84+
/// is the closest integer to 5/2 = 2.5 with smaller magnitude.
3185
case towardZero
3286

33-
/// Produces the closest representable value whose magnitude is greater than
34-
/// or equal to that of the value being rounded.
87+
/// Produces the closest representable value whose magnitude is greater
88+
/// than or equal to that of the value being rounded.
89+
///
90+
/// Examples:
91+
/// - `(-4).divided(by: 3, rounding: .awayFromZero)` is `-2`, because –2
92+
/// is the closest integer to –4/3 = –1.3̅ with greater magnitude.
93+
/// - `5.shifted(rightBy: 1, rounding: .awayFromZero)` is `3`, because 3
94+
/// is the closest integer to 5/2 = 2.5 with greater magnitude.
3595
case awayFromZero
3696

3797
/// If the value being rounded is representable, that value is returned.
@@ -45,24 +105,83 @@ public enum RoundingRule {
45105
/// we get is the same as if we rounded directly to p₂ in the desired mode
46106
/// so long as p₂ + 1 < p₁. Other rounding modes do not have this property,
47107
/// and admit _double roundings_ when interoperating with some modes.
108+
///
109+
/// Examples:
110+
/// - `(-4).divided(by: 3, rounding: .toOdd)` is `-1`, because –4/3 = –1.3̅
111+
/// is not an exact integer, and –1 is the closest odd integer.
112+
/// - `4.shifted(rightBy: 1, rounding: .toOdd)` is `2`,
113+
/// even though 2 is even, because 4/2 is exactly 2 and no rounding occurs.
48114
case toOdd
49115

50116
/// Produces the representable value that is closest to the value being
51117
/// rounded. If two values are equally close, the one that has greater
52118
/// magnitude is returned.
119+
///
120+
/// Examples:
121+
/// - `(-4).divided(by: 3, rounding: .toNearestOrAwayFromZero)`
122+
/// is `-1`, because –4/3 = –1.3̅ is closer to –1 than it is to –2.
123+
///
124+
/// - `5.shifted(rightBy: 1, rounding: .toNearestOrAwayFromZero)` is `3`,
125+
/// because 5/2 = 2.5 is equally close to 2 and 3, and 3 is further away
126+
/// from zero.
53127
case toNearestOrAwayFromZero
54128

55129
/// Produces the representable value that is closest to the value being
56130
/// rounded. If two values are equally close, the one whose least
57131
/// significant bit is not set is returned.
132+
///
133+
/// Examples:
134+
/// - `(-4).divided(by: 3, rounding: .toNearestOrEven)`
135+
/// is `-1`, because –4/3 = –1.3̅ is closer to –1 than it is to –2.
136+
///
137+
/// - `5.shifted(rightBy: 1, rounding: .toNearestOrEven)` is `2`,
138+
/// because 5/2 = 2.5 is equally close to 2 and 3, and 2 is even.
58139
case toNearestOrEven
59140

60141
/// Adds a uniform random value from [0, d) to the value being rounded,
61142
/// where d is the distance between the two closest representable values,
62143
/// then rounds the sum downwards.
144+
///
145+
/// Unlike all the other rounding modes, this mode is _not deterministic_;
146+
/// repeated calls to rounding operations with this mode will generally
147+
/// produce different results. There is a tradeoff implicit in using this
148+
/// mode: you can sacrifice _reproducible_ results to get _more accurate_
149+
/// results in aggregate. For a contrived but illustrative example, consider
150+
/// the following:
151+
/// ```
152+
/// let data = Array(repeating: 1, count: 100)
153+
/// let result = data.reduce(0) {
154+
/// $0 + $1.divided(by: 3, rounding: rule)
155+
/// }
156+
/// ```
157+
/// because 1/3 is always the same value between 0 and 1, any
158+
/// deterministic rounding rule must produce either 0 or 100 for
159+
/// this computation. But rounding `stochastically` will
160+
/// produce a value close to 33. The _error_ of the computation
161+
/// is smaller, but the result will now change between runs of the
162+
/// program.
163+
///
164+
/// For this simple case a better solution would be to add the
165+
/// values first, and then divide. This gives a result that is both
166+
/// reproducible _and_ accurate:
167+
/// ```
168+
/// let result = data.reduce(0, +)/3
169+
/// ```
170+
/// but this isn't always possible in more sophisticated scenarios,
171+
/// and in those cases this rounding rule may be useful.
172+
///
173+
/// Examples:
174+
/// - `(-4).divided(by: 3, rounding: .stochastically)`
175+
/// will be –1 with probability 2/3 and –2 with probability 1/3.
176+
/// - `5.shifted(rightBy: 1, rounding: .stochastically)`
177+
/// will be 2 with probability 1/2 and 3 with probability 1/2.
63178
case stochastically
64179

65180
/// If the value being rounded is representable, that value is returned.
66181
/// Otherwise, a precondition failure occurs.
182+
///
183+
/// Examples:
184+
/// - `(-4).divided(by: 3, rounding: .requireExact)` will trap,
185+
/// because –4/3 = –1.3̅ is not an integer.
67186
case requireExact
68187
}

0 commit comments

Comments
 (0)