Skip to content

Commit

Permalink
feat: wrapping sub (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Easyoakland authored Dec 2, 2023
1 parent ffc64c9 commit fed3bed
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 23 deletions.
107 changes: 86 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,26 +737,19 @@ macro_rules! impl_ranged {
Self::new_saturating(self.get().saturating_pow(exp))
}

/// Wrapping integer addition. Computes `self + rhs`, wrapping around the numeric
/// bounds.
/// Compute the rem_euclid of this type with its unsigned type equivalent
// Not public because it doesn't match stdlib's "method_unsigned implemented only for signed type" tradition.
// Also because this isn't implemented for normal types in std.
// TODO maybe make public anyway? It is useful.
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
#[allow(trivial_casts, trivial_numeric_casts)] // needed since some casts have to send unsigned -> unsigned to handle signed -> unsigned
pub const fn wrapping_add(self, rhs: $internal) -> Self {
<Self as $crate::traits::RangeIsValid>::ASSERT;
// Forward to internal type's impl if same as type.
if MIN == $internal::MIN && MAX == $internal::MAX {
// Safety: std's wrapping methods match ranged arithmetic when the range is the internal datatype's range.
return unsafe { Self::new_unchecked(self.get().wrapping_add(rhs)) }
}

let inner = self.get();
// Won't overflow because of std impl forwarding.
let range_len = MAX.abs_diff(MIN) + 1;
// Calculate the offset with proper handling for negative rhs
#[allow(trivial_numeric_casts)] // needed since some casts have to send unsigned -> unsigned to handle signed -> unsigned
const fn rem_euclid_unsigned(
rhs: $internal,
range_len: $unsigned_type
) -> $unsigned_type {
#[allow(unused_comparisons)]
// equivalent to `rem_euclid_unsigned()` if that method existed
let offset = if rhs >= 0 {
if rhs >= 0 {
(rhs as $unsigned_type) % range_len
} else {
// Let ux refer to an n bit unsigned and ix refer to an n bit signed integer.
Expand All @@ -770,7 +763,29 @@ macro_rules! impl_ranged {
// ux::MAX / 2 + 1 = 2^(n-1) so this subtraction will always be a >= 0 after subtraction
// Thus converting rhs signed negative to equivalent positive value in mod range_len arithmetic
((($unsigned_type::MAX / range_len) * range_len) - (rhs_abs)) % range_len
};
}
}

/// Wrapping integer addition. Computes `self + rhs`, wrapping around the numeric
/// bounds.
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
#[allow(trivial_numeric_casts)] // needed since some casts have to send unsigned -> unsigned to handle signed -> unsigned
pub const fn wrapping_add(self, rhs: $internal) -> Self {
<Self as $crate::traits::RangeIsValid>::ASSERT;
// Forward to internal type's impl if same as type.
if MIN == $internal::MIN && MAX == $internal::MAX {
// Safety: std's wrapping methods match ranged arithmetic when the range is the internal datatype's range.
return unsafe { Self::new_unchecked(self.get().wrapping_add(rhs)) }
}

let inner = self.get();

// Won't overflow because of std impl forwarding.
let range_len = MAX.abs_diff(MIN) + 1;

// Calculate the offset with proper handling for negative rhs
let offset = Self::rem_euclid_unsigned(rhs, range_len);

let greater_vals = MAX.abs_diff(inner);
// No wrap
Expand All @@ -780,7 +795,7 @@ macro_rules! impl_ranged {
// if inner < 0: Same as >=0 with caveat:
// `(signed as unsigned).wrapping_add(unsigned) as signed` is the same as
// `signed::checked_add_unsigned(unsigned).unwrap()` or `wrapping_add_unsigned`
// (the difference doesn't matter since it won't overflow the signed type),
// (the difference doesn't matter since it won't overflow),
// but unsigned integers don't have either method so it won't compile that way.
unsafe { Self::new_unchecked(
((inner as $unsigned_type).wrapping_add(offset)) as $internal
Expand All @@ -790,12 +805,62 @@ macro_rules! impl_ranged {
else {
// Safety:
// - offset < range_len by rem_euclid (MIN + ... safe)
// - offset > greater_values from if statement (offset - (greater_values + 1) safe)
// - offset > greater_vals from if statement (offset - (greater_vals + 1) safe)
//
// again using `(signed as unsigned).wrapping_add(unsigned) as signed` = `checked_add_unsigned` trick
unsafe { Self::new_unchecked(
((MIN as $unsigned_type).wrapping_add(
offset - ((greater_vals + 1))
offset - (greater_vals + 1)
)) as $internal
) }
}
}

/// Wrapping integer subtraction. Computes `self - rhs`, wrapping around the numeric
/// bounds.
#[must_use = "this returns the result of the operation, without modifying the original"]
#[inline]
#[allow(trivial_numeric_casts)] // needed since some casts have to send unsigned -> unsigned to handle signed -> unsigned
pub const fn wrapping_sub(self, rhs: $internal) -> Self {
<Self as $crate::traits::RangeIsValid>::ASSERT;
// Forward to internal type's impl if same as type.
if MIN == $internal::MIN && MAX == $internal::MAX {
// Safety: std's wrapping methods match ranged arithmetic when the range is the internal datatype's range.
return unsafe { Self::new_unchecked(self.get().wrapping_sub(rhs)) }
}

let inner = self.get();

// Won't overflow because of std impl forwarding.
let range_len = MAX.abs_diff(MIN) + 1;

// Calculate the offset with proper handling for negative rhs
let offset = Self::rem_euclid_unsigned(rhs, range_len);

let lesser_vals = MIN.abs_diff(inner);
// No wrap
if offset <= lesser_vals {
// Safety:
// if inner >= 0 -> No overflow beyond range (offset <= greater_vals)
// if inner < 0: Same as >=0 with caveat:
// `(signed as unsigned).wrapping_sub(unsigned) as signed` is the same as
// `signed::checked_sub_unsigned(unsigned).unwrap()` or `wrapping_sub_unsigned`
// (the difference doesn't matter since it won't overflow below 0),
// but unsigned integers don't have either method so it won't compile that way.
unsafe { Self::new_unchecked(
((inner as $unsigned_type).wrapping_sub(offset)) as $internal
) }
}
// Wrap
else {
// Safety:
// - offset < range_len by rem_euclid (MAX - ... safe)
// - offset > lesser_vals from if statement (offset - (lesser_vals + 1) safe)
//
// again using `(signed as unsigned).wrapping_sub(unsigned) as signed` = `checked_sub_unsigned` trick
unsafe { Self::new_unchecked(
((MAX as $unsigned_type).wrapping_sub(
offset - (lesser_vals + 1)
)) as $internal
) }
}
Expand Down
38 changes: 36 additions & 2 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,10 @@ macro_rules! tests {
assert_eq!($t::<{ $inner::MIN }, { $inner::MAX }>::MAX.wrapping_add(1),
$t::<{ $inner::MIN }, { $inner::MAX }>::MIN);
for i in 1..127 {
assert_eq!($t::<{ $inner::MIN}, { $inner::MAX - 1 }>::MAX.wrapping_add(i),
$t::<{ $inner::MIN}, { $inner::MAX - 1 }>::new($inner::MIN + i - 1).unwrap_or_else(|| panic!("adding {i}+{} does not yield {}", $inner::MIN, $inner::MAX + i )));
assert_eq!(
$t::<{ $inner::MIN}, { $inner::MAX - 1 }>::MAX.wrapping_add(i),
$t::<{ $inner::MIN}, { $inner::MAX - 1 }>::new($inner::MIN + i - 1).unwrap_or_else(|| panic!("adding {i}+{} does not yield {}", $inner::MIN, $inner::MAX + i ))
);
}
)*
$(if_signed! { $signed
Expand Down Expand Up @@ -423,6 +425,38 @@ macro_rules! tests {
})*
}

#[test]
fn wrapping_sub() {
$(
assert_eq!($t::<5, 10>::MIN.wrapping_sub(0), $t::<5, 10>::MIN);
assert_eq!($t::<5, 10>::MIN.wrapping_sub(1), $t::<5, 10>::MAX);
assert_eq!($t::<5, 10>::new(5 + 1).unwrap().wrapping_sub(1), $t::<5, 10>::MIN);
assert_eq!($t::<5, 10>::MAX.wrapping_sub(1), $t::<5, 10>::new(10 - 1).unwrap());
assert_eq!($t::<{ $inner::MIN }, { $inner::MAX }>::MIN.wrapping_sub(1),
$t::<{ $inner::MIN }, { $inner::MAX }>::MAX);
for i in 1..127 {
assert_eq!(
$t::<{ $inner::MIN + 1 }, { $inner::MAX }>::MIN.wrapping_sub(i),
$t::<{ $inner::MIN + 1 }, { $inner::MAX }>::new($inner::MAX - i + 1).unwrap_or_else(|| panic!("failed test at iteration {i}"))
);
}
)*
$(if_signed! { $signed
for i in -127..=127 {
assert_eq!($t::<-5, 126>::MIN.wrapping_add(i), $t::<-5,126>::MIN.wrapping_sub(-i), "failed test at {i}");
assert_eq!($t::<-5, 126>::MIN.wrapping_add(-i), $t::<-5,126>::MIN.wrapping_sub(i), "failed test at {i}");
}
assert_eq!(
$t::<-5, 126>::MIN.wrapping_add(127).wrapping_add(1),
$t::<-5,126>::MIN.wrapping_sub(-128)
);
assert_eq!(
$t::<-5, 126>::MIN.wrapping_add(-128),
$t::<-5,126>::MIN.wrapping_sub(127).wrapping_sub(1)
);
})*
}

#[test]
fn saturating_sub() {$(
assert_eq!($t::<5, 10>::MIN.saturating_sub(0), $t::<5, 10>::MIN);
Expand Down

0 comments on commit fed3bed

Please sign in to comment.