Created
March 15, 2022 00:49
-
-
Save evariste/b600e20eb531bd20e62cc607673bdb86 to your computer and use it in GitHub Desktop.
Class for decimal representation of unit fractions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Created: 15/03/2022 | |
By: Paul Aljabar | |
Class for decimal representation of unit fractions. | |
""" | |
############################################################################### | |
import sys, argparse | |
############################################################################### | |
def usage(): | |
desc = ('Class for unit fractions. Run as an executable script to test,' | |
' otherwise import from another script.') | |
parser = argparse.ArgumentParser(description=desc) | |
parser.add_argument('-start', type=int, help='Start value.', default=2) | |
parser.add_argument('-end', type=int, help='End value.', default=30) | |
args = parser.parse_args() | |
return args | |
############################################################################### | |
def main(): | |
args = usage() | |
start_val = args.start | |
end_val = args.end | |
test_UnitFractionDecimal(start=start_val, end=end_val) | |
return 0 | |
############################################################################### | |
def test_UnitFractionDecimal(start=2, end=30): | |
print(f'Running from {start} to {end} inclusive.') | |
print('') | |
print('\nInteger and List representation:') | |
for k in range(start,1+end): | |
u = UnitFractionDecimal(k) | |
print(k) | |
print(u) | |
print('\n\n') | |
print('_' * 80) | |
print('\nPretty print unit fraction and (period)') | |
for k in range(start,1+end): | |
u = UnitFractionDecimal(k) | |
period = len(u.recurring_part) | |
print(f'n = {k} ({period})') | |
u.pretty_print() | |
print('\n\n') | |
print('_' * 80) | |
print('\nFloating point values (1/d) and float value from unit fraction representation.') | |
for k in range(start,1+end): | |
u = UnitFractionDecimal(k) | |
print(1/k, float(u)) | |
############################################################################### | |
class UnitFractionDecimal(object): | |
""" | |
Any rational number when written as a decimal has a fixed part and a repeating part | |
E.g. 7/12 = 0.583333333333333.... | |
_ | |
or 7/12 = 0.583 more compactly | |
This class is for UNIT fractions of form 1/d in base 10. | |
""" | |
def __init__(self, d): | |
""" | |
1/d as a decimal. | |
""" | |
assert d > 1 | |
self.d = d | |
# The result of dividing out 2 and 5 completely from d. | |
# d = 2^a 5^b d_strip | |
# Initialise: | |
self.d_strip = d | |
index_2 = 0 | |
index_5 = 0 | |
while self.d_strip % 2 == 0: | |
self.d_strip //= 2 | |
index_2 += 1 | |
while self.d_strip % 5 == 0: | |
self.d_strip //= 5 | |
index_5 += 1 | |
# Get the recurring part of the stripped value. | |
recurring_part = self.get_recurring_part_stripped() | |
if not recurring_part: | |
# Decimal terminates. Recurring part is empty. | |
assert self.d_strip == 1 | |
self.fixed_part = [int(n) for n in str(1/d).replace('0.', '')] | |
self.recurring_part = [] | |
return | |
# There is a recurring part | |
# Adjust the recurring part of d_strip's representation by dividing by 2^a 5^b. | |
# E.g., for 1/30 we strip out 2,5 to get d_strip = 3 | |
# Initially, we have f=[] r=[3] where f is the fixed part and r is the recurring part. | |
# Divide by 2 to get f=[1] r=[6] ( 1/6 = 0.166666...) | |
# Divide by 5 to get f=[0] r=[3] ( 1/30 = 0.0333333...) | |
self.recurring_part = recurring_part | |
# Alongside, build up the fixed part, initialised to empty list. | |
self.fixed_part = [] | |
for _ in range(index_2): | |
self.divide_by_2() | |
for _ in range(index_5): | |
self.divide_by_5() | |
return | |
def divide_by_2(self): | |
# Get proposals for new fixed and recurring parts. | |
rec_new = self.div_list_by_2(self.recurring_part) | |
fix_new = self.div_list_by_2(self.fixed_part) | |
fix_new, rec_new = self.resolve_carry(fix_new, rec_new) | |
fix_new, rec_new = self.merge_fixed_rec(fix_new, rec_new) | |
self.fixed_part = fix_new | |
self.recurring_part = rec_new | |
return | |
def divide_by_5(self): | |
# Get proposals for new fixed and recurring parts. | |
rec_new = self.div_list_by_5(self.recurring_part) | |
fix_new = self.div_list_by_5(self.fixed_part) | |
fix_new, rec_new = self.resolve_carry(fix_new, rec_new) | |
fix_new, rec_new = self.merge_fixed_rec(fix_new, rec_new) | |
self.fixed_part = fix_new | |
self.recurring_part = rec_new | |
return | |
def resolve_carry(self, fix_new, rec_new): | |
""" | |
Given proposals for new fixed and recurring parts upon division | |
by 2 or 5, resolve any carrying and additions at the same place value. | |
The processing depends on which of the new fixed and recurring | |
parts is an 'extension' of the existing fixed and recurring parts. | |
Four different cases: | |
Case 1. Fixed and recurring stay at the same length: | |
__ __ | |
0.0834 / 2 = 0.0417 | |
Fixed: [0 8] -> [0 4] | |
Recurring: [3 4] -> [1 7] | |
Neither the fixed nor the recurring gets extended, we can just join them up for the result. | |
Case 2. Fixed only extends: | |
___ | |
0.17835 / 5 | |
Fixed: [1 7] -> [0 3 4] the last element in the new fixed has a smaller place value corresponding to the first | |
in the recurring. | |
Recurring: [8 3 5] -> [1 6 7] (new is same length as old) | |
Writing the sum out fully would look like: | |
0.034 | |
167 | |
167 | |
167... | |
0.03567167167... | |
So the fixed part needs to 'pull in the first element of the recurring part and the recurring part needs to | |
'cycle' ([1 6 7] -> [6 7 1]). | |
___ | |
i.e. 0.035671 | |
Case 3: Only recurring part extends: | |
___ | |
0.28371 / 2 (recurring part has period 3) | |
Fixed: [2 8] -> [1 4] | |
Recur: [3 7 1] -> [1 8 5 5] (Length is period plus one) | |
The period must stay the same | |
Written as (kind of vertical) sum, the result is: | |
0.14 | |
1855 | |
1855 | |
1855... | |
0.141856856856... | |
So, the leading element of the extended recurring part is appended to the fixed part. | |
And, the leading element is 'carried' in and added to the remaining recurring part: | |
[1 4] -> [1 4 1] | |
[1 8 5 5] -> [8 5 6] | |
Case 4: Both the fixed and recurring parts extend. | |
___ | |
0.28871 / 5 | |
Period is 3. | |
Direct division of fixed and recurring by 5 gives: | |
Fixed: [2 8] -> [0 5 6] | |
Recur: [8 7 1] -> [1 7 4 2] | |
Each list extends by 1. | |
Written as (kind of vertical) sum, the result is: | |
0.056 | |
1742 | |
1742 | |
1742... | |
0.057743743743... | |
So the leading element of the recurring part is 'carried' in an added to the fixed part: | |
[0 5 6] | |
+ [ 1] -> [0 5 7] | |
And, the tail of the recurring part becomes the new recurring part after carrying in and adding the head: | |
[7 4 2] | |
+ [ 1] -> [7 4 3] | |
So we end up with | |
fixed: [0 5 7] | |
recurring: [7 4 3] | |
___ | |
I.e. 0.057743 | |
""" | |
assert len(rec_new) > 0 | |
fix_extended = len(fix_new) > len(self.fixed_part) | |
rec_extended = len(rec_new) > len(self.recurring_part) | |
if fix_extended: | |
assert len(fix_new) == 1 + len(self.fixed_part) | |
if rec_extended: | |
assert len(rec_new) == 1 + len(self.recurring_part) | |
if not (fix_extended or rec_extended): | |
return fix_new, rec_new | |
if fix_extended and (not rec_extended): | |
# Last element of proposed fixed part has same place value as first element of recurring part. | |
c = rec_new[0] | |
fix_new, c = self.add_carry(fix_new, c) | |
assert c == 0 | |
# If recursive part is longer than one, cycle so it starts at second position. | |
if len(rec_new) > 1: | |
rec_new = rec_new[1:] + rec_new[:1] | |
if rec_extended and (not fix_extended): | |
carry, rec_new = rec_new[0], rec_new[1:] | |
fix_new = fix_new + [carry] | |
c = carry | |
rec_new, c = self.add_carry(rec_new, c) | |
assert c == 0 | |
assert len(rec_new) == len(self.recurring_part) | |
if fix_extended and rec_extended: | |
carry, rec_new = rec_new[0], rec_new[1:] | |
c = carry | |
fix_new, c = self.add_carry(fix_new, c) | |
assert c == 0 | |
c = carry | |
rec_new, c = self.add_carry(rec_new, c) | |
assert c == 0 | |
assert len(rec_new) == len(self.recurring_part) | |
return fix_new, rec_new | |
def merge_fixed_rec(self, fixed, rec): | |
if len(fixed) == 0: | |
return fixed, rec | |
# Check if the last entry in fixed, when concatenated with the first | |
# p-1 entries of the recurring part can form a cycle of the recurring part. | |
# E.g. fixed [0 3] recurring [4 7 3] can be re-written as fixed [0] recurring [3 4 7]. | |
end_f = fixed[-1] | |
period = len(self.recurring_part) | |
cycles_rec = [rec[k:]+rec[:k] for k in range(period)] | |
while [end_f] + rec[:-1] in cycles_rec: | |
# Take the last element of the fixed part. | |
fixed = fixed[:-1] | |
# Make the recurring part start with element taken from the end of the fixed part. | |
rec = [end_f] + rec[:-1] | |
if not fixed: | |
# We've emptied out the fixed part. | |
break | |
# Update to see if we can continue. | |
end_f = fixed[-1] | |
cycles_rec = [rec[k:]+rec[:k] for k in range(period)] | |
return fixed, rec | |
@staticmethod | |
def add_carry(digit_list, carry): | |
n_digits = len(digit_list) | |
if n_digits == 0: | |
return [carry], 0 | |
for k in reversed(range(n_digits)): | |
v = digit_list[k] + carry | |
digit_list[k] = v % 10 | |
carry = v // 10 | |
return digit_list, carry | |
@staticmethod | |
def div_list_by_2(digit_list): | |
""" | |
Divide by 10 and multiply by 5 | |
""" | |
if not digit_list: | |
return [] | |
rem = 0 | |
output = [] | |
for d in digit_list: | |
v = rem * 10 + d | |
output.append(v // 2) | |
rem = v % 2 | |
if rem > 0: | |
output.append(5) | |
return output | |
@staticmethod | |
def div_list_by_5(digit_list): | |
""" | |
Divide by 10 and multiply by 2 | |
""" | |
if not digit_list: | |
return [] | |
rem = 0 | |
output = [] | |
for d in digit_list: | |
val = 10* rem + d | |
output.append(val // 5) | |
rem = val % 5 | |
if rem > 0: | |
output.append(2 * rem) | |
return output | |
def __str__(self): | |
f = '0.' + ''.join(map(str, self.fixed_part)) | |
r = ''.join(map(str, self.recurring_part)) | |
s = ' ' * len(f) + '_'*len(r) | |
return f'{s}\n{f}{r}' | |
def __float__(self): | |
len_fix = len(self.fixed_part) | |
period = len(self.recurring_part) | |
f = self.digit_list_to_integer(self.fixed_part) | |
val = f | |
if period > 0: | |
r = self.digit_list_to_integer(self.recurring_part) | |
val = f + r / (10**period - 1) | |
val /= 10**len_fix | |
return val | |
@staticmethod | |
def digit_list_to_integer(digit_list): | |
val = 0 | |
for d in digit_list: | |
val = 10*val + d | |
return val | |
def get_recurring_part_stripped(self): | |
L = self.len_recurring_stripped() | |
A = [(10**k % self.d_strip) for k in range(L)] | |
digits_1 = [ 10*a//self.d_strip for a in A] | |
return digits_1 | |
def len_recurring_stripped(self): | |
""" | |
This function assumes that neither 2 nor 5 is a factor of d. | |
(for speed) | |
""" | |
assert self.d_strip % 2 > 0 | |
assert self.d_strip % 5 > 0 | |
if self.d_strip == 1: | |
return 0 | |
L = 1 | |
pow_ten = 10 | |
while pow_ten % self.d_strip > 1: | |
L += 1 | |
pow_ten *= 10 | |
return L | |
def __repr__(self): | |
return self.fixed_part.__repr__() + ' ' + self.recurring_part.__repr__() | |
def pretty_print(self): | |
print(self.__str__()) | |
return | |
############################################################################### | |
if __name__ == '__main__': | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment