File: frapp.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 # frapp [floating-point values...] 27 # 28 # FRactional APProximations tries to find fractions which are close to the 29 # floating-point value(s) given. 30 31 32 from fractions import Fraction 33 from math import ceil, floor, isinf, isnan 34 from sys import argv, exit, stderr, stdout 35 36 37 # info is the help message shown when asked to 38 info = ''' 39 frapp [floating-point values...] 40 41 FRactional APProximations tries to find fractions which are close to the 42 floating-point value(s) given. 43 '''.strip() 44 45 # handle standard help cmd-line options, quitting right away in that case 46 if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): 47 print(info, file=stderr) 48 exit(0) 49 50 51 def run(s: str) -> None: 52 '''Show fractions approximating the target value given.''' 53 54 # handle invalid inputs: NaNs and the infinities are unmatchable 55 value = float(s) 56 if isnan(value) or isinf(value): 57 raise ValueError(f'invalid number {value}') 58 59 # show exact fraction first, with its 0-difference from the target 60 print(f'{Fraction(s)}\t{s}\t0') 61 # if it's an integer, don't bother searching for approximations 62 if Fraction(s).denominator == 1: 63 return 64 65 posvalue = abs(value) 66 min_diff = posvalue 67 68 for den in range(1, 1_000_000): 69 # restrict loop-range of numerators for a noticeable speed-up 70 start = int(floor(posvalue * den)) 71 stop = int(ceil(posvalue * den)) 72 73 for num in range(start, stop + 1): 74 f = num / den 75 diff = (f - posvalue) / posvalue 76 if abs(diff) < min_diff: 77 if value < 0: 78 print('-', end='') 79 print(f'{num}/{den}\t{f}\t{diff}') 80 min_diff = abs(diff) 81 82 83 if len(argv) < 2: 84 print(info, file=stderr) 85 msg = '\x1b[31mexpected floating-point values as arguments\x1b[0m' 86 print(msg, file=stderr) 87 exit(1) 88 89 try: 90 stdout.reconfigure(newline='\n', encoding='utf-8') 91 # running inside a func speeds things up in older versions of python 92 if len(argv) == 2: 93 run(argv[1]) 94 else: 95 for e in argv[1:]: 96 # print(e, file=stderr) 97 print(f'\x1b[38;5;26m{e}\x1b[0m', file=stderr) 98 run(e) 99 except (BrokenPipeError, KeyboardInterrupt): 100 # quit quietly, instead of showing a confusing error message 101 stderr.flush() 102 stderr.close() 103 except Exception as err: 104 print(f'\x1b[31m{err}\x1b[0m', file=stderr) 105 exit(1)