Introduction
Every rational number between 0 and 1 can be represented as an eventually periodic sequence of bits. For example, the binary representation of 11/40 is
0.010 0011 0011 0011 ...
where the 0011
part repeats indefinitely.
One way of finding this representation is the following.
Start with r = 11/40, then repeatedly double it and take the fractional part, recording when it goes above 1.
When the value of r repeats, you know you have entered a loop.
1. r = 11/40
2. 2*r = 11/20 < 1 -> next bit is 0, r = 11/20
3. 2*r = 11/10 >= 1 -> next bit is 1, r = 2*r - 1 = 1/10
4. 2*r = 1/5 < 1 -> next bit is 0, r = 1/5
5. 2*r = 2/5 < 1 -> next bit is 0, r = 2/5
6. 2*r = 4/5 < 1 -> next bit is 0, r = 4/5
7. 2*r = 8/5 >= 1 -> next bit is 1, r = 2*r - 1 = 3/5
8. 2*r = 6/5 >= 1 -> next bit is 1, r = 2*r - 1 = 1/5, same as in 4.
The loop 5. -> 6. -> 7. -> 8. now repeats.
To get from the binary string back to 11/40, you can use the formula
(int(prefix) + int(suffix)/(2^len(suffix) - 1)) / 2^len(prefix)
where prefix
is the initial part 010
, suffix
is the repeating part 0011
, and int
converts a binary string to integer.
Given two such representations, we can perform the bitwise XOR operation on them. The resulting sequence will also be periodic, so it represents a rational number.
For some rational numbers, there are two binary representations.
1/4 = 0.010000000...
= 0.001111111...
The choice between them can affect the result of the bitwise XOR. In these cases, we use the former representation, which has infinitely many 0s.
The task
Your inputs are two rational numbers in the half-open interval [0,1). Your output shall be the result of the bitwise XOR operation applied to the inputs, expressed as a rational number. Note that the output can be 1, even though neither of the inputs are.
The exact formats of input and output are flexible, but each rational number should be represented by two integers, the numerator and denominator (with the exception of 0 and 1, which can be represented as 0
and 1
if desired).
You can assume that the inputs are expressed in lowest terms.
The output must be expressed in lowest terms.
A built-in rational number type is an acceptable format, as long as it satisfies these restrictions.
You can ignore any bounds on integers imposed by your language, but your algorithm should theoretically work for all rational numbers.
The lowest byte count wins. Standard code-golf rules apply.
Example
Consider the inputs 11/40 and 3/7.
We write their representations one above the other, delimiting the repeating parts by pipes |
.
Then we extract repeating parts of equal lengths, and apply bitwise XOR to them and the parts before them.
11/40 = 0. 0 1 0|0 0 1 1|0 0 1 1|0 0 1 1|0 0 1 1|0 0 1 1|0 0 1 1|0 0 1 ...
3/7 = 0.|0 1 1|0 1 1|0 1 1|0 1 1|0 1 1|0 1 1|0 1 1|0 1 1|0 1 1|0 1 1|...
-> 0. 0 0 1|0 1 0 1 1 1 1 0 1 0 0 0|0 1 0 1 1 1 1 0 1 0 0 0|0 1 0 ...
The resulting rational number is 89/520.
Test cases
0 0 -> 0
1/2 1/2 -> 0
1/2 1/4 -> 3/4
1/3 2/3 -> 1
1/2 3/4 -> 1/4
5/8 1/3 -> 23/24
1/3 1/5 -> 2/5
15/16 3/19 -> 257/304
15/16 257/304 -> 3/19
3/7 11/40 -> 89/520
5/32 17/24 -> 59/96
16/29 16/39 -> 621001733121535520/696556744961512799
000...
in this cases (which is also what we get if we use the algorithm withr
). For example, in the case5/8, 1/3
we get23/24
because we choose the expansion0.101000...
for5/8
. If we choose instead0.10011111...
as5/8
, the result after XOR becomes19/24
, so this is wrong. Related to Wikipedia: 0.999... \$\endgroup\$(a ^ b) ^ b == a
does not hold. E.g.(19/24 ^ 1/3) ^ 1/3 != 19/24
. That made me lose quite a bit of excitement about this :( \$\endgroup\$