If you want a cyclic integer type with
BITS bits (minimum 2), set constants
HALF = 1 << (BITS - 1); MASK = HALF - 1;noting that
(HALF | MASK) has all low
BITS set.
Then, the signed distance from
from to
to is
((to - from) & MASK) - ((to - from) & HALF)and will always evaluate to a value in -
HALF....
MASK, inclusive.
I suggest writing it using a suitable
int_fastN_t type (
N=8, 16, 32, 64, whatever can represent -
HALF), and form
int_fastN_t d = to - from; return (d & MASK) - (d & HALF);for the signed distance.
For example, with
BITS=4,
HALF=8,
MASK=7:
- Distance from 0 to 7 is +7
- Distance from 7 to 0 is -7
- Distance from 0 to 8 is -8, and distance from 8 to 0 is -8, because halfway is always negative
- Distance from 15 to 0 is +1
- Distance from 0 to 15 is -1
One can use standard operations like addition, subtraction, multiplication and division with such values using
intN_t or
int_fastN_t types, as long as
N >
BITS.
When
N ==
BITS,
(intN_t)(to - from) is the signed distance exactly.
The unsigned distance can be written as
int_fastN_t d = to - from; return (d & HALF) ? half - (d & MASK) : (d & MASK);which will always return a nonnegative distance in 0..
HALF, inclusive.
The test for
from <=
to (but correctly handling the wrapping) can be written as
!((to - from) & HALF)Testing whether
c is between
a and
b (inclusive, correctly handling the wrapping), can be written as
int_fastN_t ac = (c - a) & (MASK | HALF); int_fastN_t cb = (b - c) & (MASK | HALF); int_fastN_t ab = (b - a) & HALF; return (!ac) || (!cb) || (ab == (ac & HALF) && ab == (cb & HALF));which is equivalent to testing that the order of c,a and b,c are the same as b,a. The
(!ac) tests for equality on the lower bound, and
(!cb) for equality on the upper bound.
Midway between
a and
b is easiest to calculate as
(a + signed_distance(a, b) / 2) & (MASK | HALF);In general, interpolating between
a and
b via
n/
N,
N > 0, can be done via
(a + (signed_distance(a, b) * n) / N)) & (MASK | HALF);if your intermediate type has sufficient range (enough bits for the temporary product). It yields
a when
n=0, and b when n=N.
If you have two unsigned integer variables
from and
to in 0..
FULL-1, where
FULL is the size of the full range (and not a power of two because for those you want to use the above instead), 2·
HALF=
FULL, then the signed distance from
from to
to can be calculated using
int_fastN_t d = to - from; return (d > HALF) ? d - FULL : (d < -HALF) ? d + FULL : d;The distance from
FULL-1 to 0 is +1, and from 0 to
FULL-1 is -1; this too wraps correctly. Assuming inputs are within valid range, the returned value is in -
HALF...+
HALF, inclusive.
Unsigned distance is just the absolute value of the above, for example
int_fastN_t d = to - from; return (d > HALF) ? FULL - d : (d < -HALF) ? FULL + d : (d < 0) ? -d : d;For
from <=
to, you can use
int_fastN_t d = to - from; return (d < -HALF) || (d >= 0 && d <= HALF));Midway between
a and
b can be calculated as
int_fastN_t d = a + signed_distance(a, b)/2; return (d < 0) ? d + FULL : (d >= FULL) ? d - FULL : d;Linear interpolation from
n=0 at
a to
n=
N at
b is similarly
int_fastN_t d = a + (signed_distance(a, b) * n) / N; return (d < 0) ? d + FULL : (d >= FULL) ? d - FULL : d;This expression yields 0 if
from ==
to, 1 if
from <
to, and 2 if
from >
to. I call it
compare(from, to): int_fastN_t d = to - from; return (d != 0) + (d >= HALF) + (d > -HALF && d < 0);With that, it is easy to check if
c is between
a and
b, assuming they are all in 0..
FULL-1:
return ((compare(a, c) | compare(c, b) | compare(a, b)) != 3);Essentially, it verifies that c-a and b-c are in the same direction as b-a.
It is possible to combine these two, using low bits for the binary (fractional or angle) part, and upper bits for an arbitrary cyclic range, by simply shifting
FULL and
HALF left by fractional bits for the combined arbitrary range. (That is, your arbitrary range will have
FULL and
HALF with at least
BITS least significant bits zero.)
To simplify things, one could use full 16 bits for the fractional/angle part (for use with
uint16_t or
uint_fast16_t), and the leftover 14 bits or so for the integral angle. We need a couple of extra bits, to make sure the integral part can represent values in -
FULL..2*
FULL-1, inclusive. The angular precision is then 360°/65536 = 45°/8192 = 0.0054931640625°, or 10125/512 = 19.775390625 arcseconds. For sines and cosines, you need a quarter wave, which here would be split into 16384 steps.
A suitable value for the integral number of turns would be 16380 = 65·252 = 2340·7. On architectures with an unsigned 32-bit by 32-bit multiplication returning the upper 32 bits, compilers can optimize "(uint32_t)(x) % 16380" and "(uint16_t)(x) % 16380" to one multiplication, some bit shifts and a couple of additions, because it is sufficiently close to a power of two (2¹⁴ = 16384). On such architectures (Cortex-M3/M4/M7, RISC-V rv32, x86, x86-64, ARM64/AARCH64), using
uint32_t as the combined type, enforcing wrapping after operations that modify the full turns part via
combined %= 1073479680; (1073479680 = 16380·65536) generates just one multiplication, some bit shifts, and a couple of additions and subtractions.